First layout of the game

This commit is contained in:
Mysaa Java
2026-08-24 15:24:50 +02:00
parent 902ba26df3
commit 639a5565ad
16 changed files with 698 additions and 7 deletions
@@ -1,13 +1,30 @@
package com.bernard.nodecames;
import java.util.UUID;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.servlet.view.RedirectView;
import com.bernard.nodecames.game.GameManager;
@Controller
public class HttpController {
@GetMapping("/")
public String index() {
return "index.html";
}
@GetMapping("/")
public String index() {
return "index.html";
}
@GetMapping("/room/{id}/grid")
public String grid(@PathVariable("id") String id) {
return "grid";
}
@GetMapping("/create-room")
public RedirectView createRoom() {
UUID uuid = GameManager.newGrid();
return new RedirectView("/room/"+uuid.toString()+"/grid");
}
}
@@ -0,0 +1,44 @@
package com.bernard.nodecames.frontend;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
import org.springframework.web.socket.config.annotation.WebSocketTransportRegistration;
import org.springframework.web.socket.server.standard.ServletServerContainerFactoryBean;
@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfiguration implements WebSocketMessageBrokerConfigurer {
@Bean
public ServletServerContainerFactoryBean createWebSocketContainer() {
ServletServerContainerFactoryBean container = new ServletServerContainerFactoryBean();
container.setMaxTextMessageBufferSize(8192);
container.setMaxBinaryMessageBufferSize(8192);
return container;
}
@Override
public void registerStompEndpoints(StompEndpointRegistry registry) {
registry.addEndpoint("/socket").withSockJS();
}
@Override
public void configureMessageBroker(MessageBrokerRegistry config) {
// STOMP messages whose destination header begins with /app are routed to
// @MessageMapping methods in @Controller classes
config.setApplicationDestinationPrefixes("/app");
// Use the built-in message broker for subscriptions and broadcasting and
// route messages whose destination header begins with /topic or /queue to the broker
config.enableSimpleBroker("/topic");
}
@Override
public void configureWebSocketTransport(WebSocketTransportRegistration registry) {
registry.setMessageSizeLimit(4 * 8192);
registry.setTimeToFirstMessage(30000);
}
}
@@ -0,0 +1,75 @@
package com.bernard.nodecames.frontend;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.messaging.Message;
import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import com.bernard.nodecames.game.GameManager;
import com.bernard.nodecames.model.Grid;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.JsonNodeFactory;
@Controller
public class WebSocketController {
private static final JsonNodeFactory jsn = JsonNodeFactory.instance;
@Autowired
GameManager gm;
@GetMapping("/room/{id}/game/{player}")
public Object grid(@PathVariable("id") String gridId, @PathVariable("player") String playerStr) {
char player;
if(playerStr.equals("player-a"))
player = 'A';
else if (playerStr.equals("player-b"))
player = 'B';
else
throw new IllegalArgumentException("Unknown player type");
Grid g = GameManager.findGrid(gridId);
return new ResponseEntity<>(GameManager.gridData(g, player), HttpStatus.OK);
}
@MessageMapping("submit-hint")
@SendTo("/topic/submit-hint")
public JsonNode submitHint(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) {
String hint = content.asObject().get("hint").stringValue();
int wordCount = content.asObject().get("hintWordCount").intValue();
Grid g = GameManager.findGrid(roomId);
gm.proposeHint(g, player.charAt(0), hint, wordCount);
return content;
}
@MessageMapping("point-card")
@SendTo("/topic/point-card")
public JsonNode pointCard(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) {
int cardIndex = content.asObject().get("cardIndex").intValue();
Grid g = GameManager.findGrid(roomId);
gm.pointCard(g, player.charAt(0), cardIndex);
return content;
}
@MessageMapping("end-guessing")
@SendTo("/topic/end-guessing")
public String endGuessing(@Header("room") String roomId, @Header("player") String player) {
Grid g = GameManager.findGrid(roomId);
gm.endGuessing(g, player.charAt(0));
return "";
}
}
@@ -0,0 +1,219 @@
package com.bernard.nodecames.game;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.stream.IntStream;
import org.springframework.stereotype.Service;
import com.bernard.nodecames.model.Card;
import com.bernard.nodecames.model.GameEvent;
import com.bernard.nodecames.model.Grid;
import com.bernard.nodecames.model.Hint;
import com.bernard.nodecames.model.Card.Color;
import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
@Service
public class GameManager {
private static final JsonNodeFactory jsn = JsonNodeFactory.instance;
private static Map<UUID, Grid> games = new HashMap<>();
public static JsonNode gridData(Grid g, char player) {
ArrayNode cardsNode = jsn.arrayNode(g.getCardCount());
for (int i = 0; i < g.getCardCount(); i++) {
ObjectNode cardNode = jsn.objectNode();
Card c = g.getCards()[i];
cardNode.set("word", jsn.stringNode(c.getWord()));
cardNode.set("color", jsn.stringNode((player=='A'?c.getColorA():c.getColorB()).name().toLowerCase()));
if((player=='A' && c.isRevealedA()) || (player=='B' && c.isRevealedB()))
cardNode.set("otherColor", jsn.stringNode((player=='A'?c.getColorB():c.getColorA()).name().toLowerCase()));
cardsNode.add(cardNode);
}
ArrayNode eventsNode = jsn.arrayNode(g.getGameEvents().size());
for(GameEvent ge : g.getGameEvents()) {
ObjectNode geNode = jsn.objectNode();
geNode.set("type", jsn.stringNode(ge.getType().name().toLowerCase()));
geNode.set("issuer", jsn.stringNode(Character.toString(ge.getIssuer())));
switch (ge.getType()) {
case GameEvent.Type.HINT:
Hint hint = (Hint)ge.getData();
geNode.set("word", jsn.stringNode(hint.getWord()));
geNode.set("wordCount", jsn.numberNode(hint.getWordCount()));
break;
case GameEvent.Type.GUESS:
Card c = (Card)ge.getData();
Integer pos = IntStream.range(0, g.getCardCount())
.filter(i -> c == g.getCards()[i])
.findFirst()
.getAsInt();
geNode.set("card-index", jsn.numberNode(pos));
break;
case GameEvent.Type.END_GUESSING:
geNode.set("manual", jsn.booleanNode((Boolean)ge.getData()));
break;
case GameEvent.Type.START_GAME:
break;
}
eventsNode.add(geNode);
}
ObjectNode currentHintNode = jsn.objectNode();
if(g.getCurrentHint() != null) {
currentHintNode.set("word", jsn.stringNode(g.getCurrentHint().getWord()));
currentHintNode.set("wordCount", jsn.numberNode(g.getCurrentHint().getWordCount()));
}
ObjectNode out = jsn.objectNode();
out.set("cards", cardsNode);
out.set("phase", jsn.stringNode(g.getPhase().name().toLowerCase()));
out.set("events", eventsNode);
if(g.getCurrentHint() != null)
out.set("current-hint", currentHintNode);
out.set("current-guess-count", jsn.numberNode(g.getCurrentGuessCount()));
out.set("used-hints", jsn.numberNode(g.getUsedHints()));
out.set("used-whites", jsn.numberNode(g.getUsedWhites()));
return out;
}
public static Grid findGrid(String gridId) {
return games.get(UUID.fromString(gridId));
}
public static UUID newGrid() {
UUID uuid = UUID.randomUUID();
String[] words = new String[25];
for (int i = 0; i < words.length; i++)
words[i] = "MotNumero"+Integer.toString(i);
List<Card> cards = new ArrayList<>(25);
int i = 0;
cards.add(new Card(words[i++], Color.BLACK, Color.BLACK));
for (;i < 4; i++)
cards.add(new Card(words[i], Color.GREEN, Color.GREEN));
for (;i < 9; i++)
cards.add(new Card(words[i], Color.WHITE, Color.GREEN));
for (;i < 14; i++)
cards.add(new Card(words[i], Color.GREEN, Color.WHITE));
cards.add(new Card(words[i++], Color.GREEN, Color.BLACK));
cards.add(new Card(words[i++], Color.BLACK, Color.GREEN));
cards.add(new Card(words[i++], Color.WHITE, Color.BLACK));
cards.add(new Card(words[i++], Color.BLACK, Color.WHITE));
for (;i < 25; i++)
cards.add(new Card(words[i], Color.WHITE, Color.WHITE));
Collections.shuffle(cards);
Grid g = new Grid(uuid.toString(), (Card[])cards.toArray(new Card[cards.size()]));
games.put(uuid, g);
return uuid;
}
public void proposeHint(Grid g, char player, String hint, int wordCount) {
if (!(((player == 'A') && (g.getPhase() == Grid.Phase.HINTING_A)) ||
((player == 'B') && (g.getPhase() == Grid.Phase.HINTING_B)) ||
g.getPhase() == Grid.Phase.HINTING_ANY)) {
throw new RuntimeException("This action is not allowed now");
}
Hint h = new Hint(hint, wordCount);
g.incrementHintCount();
g.newGameEvent(GameEvent.newHintEvent(h, player));
if (player == 'A')
g.setPhase(Grid.Phase.GUESSING_B);
else
g.setPhase(Grid.Phase.GUESSING_A);
g.setCurrentHint(h);
g.setCurrentGuessCount(0);
}
public void pointCard(Grid g, char player, int cardIndex) {
if (!(((player == 'A') && (g.getPhase() == Grid.Phase.GUESSING_A)) ||
((player == 'B') && (g.getPhase() == Grid.Phase.GUESSING_B)))) {
throw new RuntimeException("This action is not allowed now");
}
if (cardIndex<0 || cardIndex>g.getCardCount()) {
throw new IllegalArgumentException("The given card index is invalid");
}
Card c = g.getCards()[cardIndex];
boolean endedGuessing = false;
if (player == 'A') {
if (c.isRevealedA() || (c.isRevealedB() && c.getColorA() == Card.Color.GREEN)) {
throw new RuntimeException("Card has already been revealed");
}
c.setRevealedA(true);
switch(c.getColorB()) {
case Card.Color.GREEN:
break;
case Card.Color.WHITE:
g.incrementWhiteCount();
endedGuessing = true;
break;
case Card.Color.BLACK:
endedGuessing = true;
break;
}
} else {
if (c.isRevealedB() || (c.isRevealedA() && c.getColorB() == Card.Color.GREEN)) {
throw new RuntimeException("Card has already been revealed");
}
c.setRevealedB(true);
switch(c.getColorA()) {
case Card.Color.GREEN:
break;
case Card.Color.WHITE:
g.incrementWhiteCount();
endedGuessing = true;
break;
case Card.Color.BLACK:
endedGuessing = true;
break;
}
}
g.newGameEvent(GameEvent.newGuessEvent(c, player));
g.incrementCurrentGuessCount();
if (!g.getCurrentHint().canStillGuess(g.getCurrentGuessCount()))
endedGuessing = true;
if(endedGuessing) {
g.setCurrentHint(null);
if (player == 'A')
g.setPhase(Grid.Phase.HINTING_A);
else
g.setPhase(Grid.Phase.HINTING_B);
g.newGameEvent(GameEvent.newEndGuessingEvent(false, player));
}
}
public void endGuessing(Grid g, char player) {
if (!(((player == 'A') && (g.getPhase() == Grid.Phase.GUESSING_A)) ||
((player == 'B') && (g.getPhase() == Grid.Phase.GUESSING_B)))) {
throw new RuntimeException("This action is not allowed now");
}
g.setCurrentHint(null);
if (player == 'A')
g.setPhase(Grid.Phase.HINTING_B);
else
g.setPhase(Grid.Phase.HINTING_A);
g.newGameEvent(GameEvent.newEndGuessingEvent(true, player));
}
}
@@ -0,0 +1,29 @@
package com.bernard.nodecames.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@AllArgsConstructor
@Getter
@Setter
public class Card {
String word;
Color colorA;
Color colorB;
// revealedA is wether A knows B's color
boolean revealedA;
boolean revealedB;
public Card(String word, Color colorA, Color colorB) {
this(word, colorA, colorB, false, false);
}
public enum Color {
GREEN,
WHITE,
BLACK;
}
}
@@ -1,20 +1,25 @@
package com.bernard.nodecames.model;
import java.util.List;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Entity
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Game {
@Id
@GeneratedValue
private Long id;
private String name;
String name;
}
@@ -0,0 +1,45 @@
package com.bernard.nodecames.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
public class GameEvent {
Type type;
Object data;
// 'A' for player A, 'B' for player B, '_' for none
char issuer;
private GameEvent(Type type, Object data, char issuer){
this.data = data;
this.issuer = issuer;
this.type = type;
}
public static GameEvent newHintEvent(Hint h, char issuer) {
return new GameEvent(Type.HINT, h, issuer);
}
public static GameEvent newGuessEvent(Card c, char issuer) {
return new GameEvent(Type.GUESS, c, issuer);
}
public static GameEvent newEndGuessingEvent(boolean manual, char issuer) {
return new GameEvent(Type.END_GUESSING, manual, issuer);
}
public static GameEvent newStartGameEvent() {
return new GameEvent(Type.START_GAME, null, '_');
}
@AllArgsConstructor
public enum Type {
HINT(Hint.class),
GUESS(Card.class),
END_GUESSING(Boolean.class),
START_GAME(Object.class);
Class<?> dataType;
}
}
@@ -0,0 +1,61 @@
package com.bernard.nodecames.model;
import java.util.ArrayList;
import java.util.List;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@NoArgsConstructor
@AllArgsConstructor
@Getter
@Setter
public class Grid {
String id;
Card[] cards;
List<GameEvent> gameEvents;
Hint currentHint = null;
int currentGuessCount;
int usedHints;
int usedWhites;
Phase phase;
public Grid(String name, Card[] cards) {
this(name, cards, new ArrayList<>(), null, 0, 0, 0, Phase.HINTING_ANY);
}
public void incrementHintCount() {
this.setUsedHints(this.getUsedHints()+1);
}
public void incrementWhiteCount() {
this.setUsedWhites(this.getUsedWhites()+1);
}
public void incrementCurrentGuessCount() {
this.setCurrentGuessCount(this.getCurrentGuessCount()+1);
}
public void newGameEvent(GameEvent event) {
this.gameEvents.add(event);
}
public int getCardCount() {
return this.getCards().length;
}
public enum Phase {
HINTING_ANY,
HINTING_A,
HINTING_B,
GUESSING_A,
GUESSING_B;
}
}
@@ -0,0 +1,17 @@
package com.bernard.nodecames.model;
import lombok.AllArgsConstructor;
import lombok.Getter;
@AllArgsConstructor
@Getter
public class Hint {
String word;
int wordCount;
public boolean canStillGuess(int guessCount) {
return (wordCount == -1) || (guessCount < (wordCount + 1));
}
}