Remade game logic, added sudden death

This commit is contained in:
Mysaa Java
2026-08-31 02:08:15 +02:00
parent bee583e6b2
commit cfccdcea5e
7 changed files with 448 additions and 247 deletions
@@ -7,7 +7,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.messaging.handler.annotation.Header; import org.springframework.messaging.handler.annotation.Header;
import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.handler.annotation.Payload;
import org.springframework.messaging.handler.annotation.SendTo;
import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller; import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@@ -15,17 +14,14 @@ import org.springframework.web.bind.annotation.PathVariable;
import com.bernard.nodecames.game.GameManager; import com.bernard.nodecames.game.GameManager;
import com.bernard.nodecames.model.Card; import com.bernard.nodecames.model.Card;
import com.bernard.nodecames.model.GameEvent;
import com.bernard.nodecames.model.Grid; import com.bernard.nodecames.model.Grid;
import com.bernard.nodecames.model.Card.Color;
import com.bernard.nodecames.model.Grid.Phase; import com.bernard.nodecames.model.Grid.Phase;
import lombok.AllArgsConstructor;
import tools.jackson.databind.JsonNode; import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.JsonNodeFactory; import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode;
@Controller @Controller
@AllArgsConstructor
public class WebSocketController { public class WebSocketController {
private static final JsonNodeFactory jsn = JsonNodeFactory.instance; private static final JsonNodeFactory jsn = JsonNodeFactory.instance;
@@ -33,12 +29,25 @@ public class WebSocketController {
private GameManager gm; private GameManager gm;
private SimpMessagingTemplate template; private SimpMessagingTemplate template;
public void publishNewPhase(Grid g) { public WebSocketController(GameManager gm, SimpMessagingTemplate template) {
this.template.convertAndSend( this.gm = gm;
"/topic/new-phase", this.template = template;
g.getPhase().name().toLowerCase() gm.setWsc(this);
}
public void onNewEvent(Grid g, GameEvent ge) {
this.template.convertAndSend("/topic/new-event",
gm.eventData(g, ge)
); );
} }
public void onNewPhase(Grid g, Phase p) {
this.template.convertAndSend(
"/topic/new-phase",
gm.phaseData(g.getPhase())
);
}
public void publishCardUpdateToPlayer(Grid g, int cardIndex, char player) { public void publishCardUpdateToPlayer(Grid g, int cardIndex, char player) {
Card c = g.getCards()[cardIndex]; Card c = g.getCards()[cardIndex];
this.template.convertAndSend( this.template.convertAndSend(
@@ -68,42 +77,27 @@ public class WebSocketController {
} }
@MessageMapping("submit-hint") @MessageMapping("submit-hint")
@SendTo("/topic/submit-hint") public void submitHint(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) {
public JsonNode submitHint(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) {
String hint = content.asObject().get("hint").stringValue(); String hint = content.asObject().get("hint").stringValue();
int wordCount = content.asObject().get("hintWordCount").intValue(); int wordCount = content.asObject().get("hintWordCount").intValue();
Grid g = gm.findGrid(roomId); Grid g = gm.findGrid(roomId);
gm.proposeHint(g, player.charAt(0), hint, wordCount); gm.proposeHint(g, player.charAt(0), hint, wordCount);
content.asObject().set("player", jsn.stringNode(player));
this.publishNewPhase(g);
return content;
} }
@MessageMapping("point-card") @MessageMapping("point-card")
@SendTo("/topic/point-card") public void pointCard(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) {
public JsonNode pointCard(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) {
int cardIndex = content.asObject().get("cardIndex").intValue(); int cardIndex = content.asObject().get("cardIndex").intValue();
Grid g = gm.findGrid(roomId); Grid g = gm.findGrid(roomId);
Color c = gm.pointCard(g, player.charAt(0), cardIndex); gm.pointCard(g, player.charAt(0), cardIndex);
content.asObject().set("color", jsn.stringNode(c.name().toLowerCase()));
content.asObject().set("player", jsn.stringNode(player));
if(g.getPhase() != Phase.GUESSING_A && g.getPhase() != Phase.GUESSING_B)
publishNewPhase(g);
publishCardUpdate(g, cardIndex); publishCardUpdate(g, cardIndex);
return content;
} }
@MessageMapping("end-guessing") @MessageMapping("end-guessing")
@SendTo("/topic/end-guessing") public void endGuessing(@Header("room") String roomId, @Header("player") String player) {
public JsonNode endGuessing(@Header("room") String roomId, @Header("player") String player) {
Grid g = gm.findGrid(roomId); Grid g = gm.findGrid(roomId);
gm.endGuessing(g, player.charAt(0)); gm.endGuessing(g, player.charAt(0));
ObjectNode out = jsn.objectNode();
out.set("player", jsn.stringNode(player));
return out;
} }
} }
@@ -7,6 +7,7 @@ import java.util.HashMap;
import java.util.HashSet; import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.Scanner; import java.util.Scanner;
import java.util.Set; import java.util.Set;
@@ -17,12 +18,15 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.bernard.nodecames.frontend.WebSocketController;
import com.bernard.nodecames.model.Card; import com.bernard.nodecames.model.Card;
import com.bernard.nodecames.model.Card.Color;
import com.bernard.nodecames.model.GameEvent; import com.bernard.nodecames.model.GameEvent;
import com.bernard.nodecames.model.Grid; import com.bernard.nodecames.model.Grid;
import com.bernard.nodecames.model.Grid.Phase;
import com.bernard.nodecames.model.Hint; import com.bernard.nodecames.model.Hint;
import com.bernard.nodecames.model.Card.Color;
import lombok.Setter;
import tools.jackson.databind.JsonNode; import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.JsonNodeFactory; import tools.jackson.databind.node.JsonNodeFactory;
@@ -33,12 +37,30 @@ public class GameManager {
private static final JsonNodeFactory jsn = JsonNodeFactory.instance; private static final JsonNodeFactory jsn = JsonNodeFactory.instance;
public static final int MAX_HINT_COUNT = 9;
//TODO Remove static, make something more Spring Boot-y //TODO Remove static, make something more Spring Boot-y
private static Map<UUID, Grid> games = new HashMap<>(); private static Map<UUID, Grid> games = new HashMap<>();
@Setter
WebSocketController wsc;
@Value("classpath:dictionaries/fr.txt") @Value("classpath:dictionaries/fr.txt")
Resource dictFr; Resource dictFr;
private void newGameEvent(Grid g, GameEvent ge) {
g.newGameEvent(ge);
wsc.onNewEvent(g, ge);
}
private void setPhase(Grid g, Phase phase) {
g.setPhase(phase);
if(!phase.isAnyoneGuessing()) {
g.setCurrentHint(null);
}
wsc.onNewPhase(g, phase);
}
//TODO do Spring boot magic to only generate this once //TODO do Spring boot magic to only generate this once
public List<String> allWords() { public List<String> allWords() {
try (Scanner wordsScanner = new Scanner(dictFr.getInputStream())) { try (Scanner wordsScanner = new Scanner(dictFr.getInputStream())) {
@@ -70,71 +92,8 @@ public class GameManager {
return out; return out;
} }
public JsonNode cardData(Card c, char player) { private char other(char player) {
ObjectNode cardNode = jsn.objectNode(); return (player=='A')?'B':'A';
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()));
cardNode.set("revealed-a", jsn.booleanNode(c.isRevealedA()));
cardNode.set("revealed-b", jsn.booleanNode(c.isRevealedB()));
return cardNode;
}
public JsonNode eventData(Grid g, GameEvent ge) {
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;
}
return geNode;
}
public JsonNode gridData(Grid g, char player) {
ArrayNode cardsNode = jsn.arrayNode(g.getCardCount());
for (int i = 0; i < g.getCardCount(); i++)
cardsNode.add(cardData(g.getCards()[i], player));
ArrayNode eventsNode = jsn.arrayNode(g.getGameEvents().size());
for(GameEvent ge : g.getGameEvents())
eventsNode.add(eventData(g, ge));
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 Grid findGrid(String gridId) { public Grid findGrid(String gridId) {
@@ -169,27 +128,159 @@ public class GameManager {
return uuid; return uuid;
} }
/**********************************
* JSON ACCESSORS *
**********************************/
public JsonNode cardData(Card c, char player) {
ObjectNode cardNode = jsn.objectNode();
cardNode.set("word", jsn.stringNode(c.getWord()));
cardNode.set("color", jsn.stringNode((player=='A'?c.getColorA():c.getColorB()).name().toLowerCase()));
if((player=='A' && c.isColorBPublic()) || (player=='B' && c.isColorAPublic()))
cardNode.set("otherColor", jsn.stringNode((player=='A'?c.getColorB():c.getColorA()).name().toLowerCase()));
cardNode.set("revealed-a", jsn.booleanNode(c.isColorBPublic()));
cardNode.set("revealed-b", jsn.booleanNode(c.isColorAPublic()));
return cardNode;
}
public JsonNode eventData(Grid g, GameEvent ge) {
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));
geNode.set("color", jsn.stringNode(((ge.getIssuer()=='A')?c.getColorB():c.getColorA()).name().toLowerCase()));
break;
case GameEvent.Type.END_GUESSING:
geNode.set("manual", jsn.booleanNode((Boolean)ge.getData()));
break;
case GameEvent.Type.GAME_END:
geNode.set("win", jsn.booleanNode((Boolean)ge.getData()));
break;
case GameEvent.Type.SUDDEN_DEATH:
break;
}
return geNode;
}
public JsonNode phaseData(Phase p) {
ObjectNode out = jsn.objectNode();
out.set("a-guessing", jsn.booleanNode(p.isAGuessing()));
out.set("b-guessing", jsn.booleanNode(p.isBGuessing()));
out.set("a-hinting", jsn.booleanNode(p.isAHinting()));
out.set("b-hinting", jsn.booleanNode(p.isBHinting()));
out.set("sudden-death", jsn.booleanNode(p.isSuddenDeath()));
out.set("game-ended", jsn.booleanNode(p.isGameEnded()));
return out;
}
public JsonNode gridData(Grid g, char player) {
ArrayNode cardsNode = jsn.arrayNode(g.getCardCount());
for (int i = 0; i < g.getCardCount(); i++)
cardsNode.add(cardData(g.getCards()[i], player));
ArrayNode eventsNode = jsn.arrayNode(g.getGameEvents().size());
for(GameEvent ge : g.getGameEvents())
eventsNode.add(eventData(g, ge));
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", phaseData(g.getPhase()));
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;
}
/*################################*
* GAME LOGIC *
*################################*/
/**
@returns true iff all green cards of this player have been revealed either side
*/
public boolean allGreenRevealed(Grid g, char player) {
for(Card c : g.getCards()) {
if (c.getColor(player) == Color.GREEN && !c.isColorPublic(player) &&
!(c.getColor(other(player)) == Color.GREEN && c.isColorPublic(other(player))))
return false;
}
return true;
}
/**********************************
* GAME ACTIONS *
**********************************/
private void endGame(Grid g, boolean win) {
g.setWon(Optional.of(win));
setPhase(g, Phase.gameEnded());
newGameEvent(g, GameEvent.newGameEndEvent(win));
}
public void proposeHint(Grid g, char player, String hint, int wordCount) { public void proposeHint(Grid g, char player, String hint, int wordCount) {
if (!(((player == 'A') && (g.getPhase() == Grid.Phase.HINTING_A)) || if (!g.getPhase().isHinting(player)) {
((player == 'B') && (g.getPhase() == Grid.Phase.HINTING_B)) ||
g.getPhase() == Grid.Phase.HINTING_ANY)) {
throw new RuntimeException("This action is not allowed now"); throw new RuntimeException("This action is not allowed now");
} }
Hint h = new Hint(hint, wordCount); Hint h = new Hint(hint, wordCount);
g.incrementHintCount(); 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.setCurrentHint(h);
g.setCurrentGuessCount(0); g.setCurrentGuessCount(0);
newGameEvent(g, GameEvent.newHintEvent(h, player));
setPhase(g, Phase.guessing(other(player)));
}
/**
Sets the phase of the grid when player ends its guessing round.
@param g Must neither be ended neither be in a sudden death
*/
private void endGuessingRound(Grid g, char player) {
// We're done with this round of guesses
if(g.getUsedHints() >= MAX_HINT_COUNT) {
// We're entering sudden death
newGameEvent(g, GameEvent.newSuddenDeath());
if(allGreenRevealed(g, player)) {
// If all my greens are revealed, i'm the only one to guess
setPhase(g, Phase.suddenDeathOnly(player));
} else {
// Else, we are both guessing
setPhase(g, Phase.suddenDeathBoth());
}
} else {
// Next regular hinting round
if(allGreenRevealed(g, player)) {
// I can't hint anymore
setPhase(g, Phase.hinting(other(player)));
} else {
setPhase(g, Phase.hinting(player));
}
}
} }
public Card.Color pointCard(Grid g, char player, int cardIndex) { public Card.Color pointCard(Grid g, char player, int cardIndex) {
if (!(((player == 'A') && (g.getPhase() == Grid.Phase.GUESSING_A)) || if (!g.getPhase().isGuessing(player)) {
((player == 'B') && (g.getPhase() == Grid.Phase.GUESSING_B)))) {
throw new RuntimeException("This action is not allowed now"); throw new RuntimeException("This action is not allowed now");
} }
if (cardIndex<0 || cardIndex>g.getCardCount()) { if (cardIndex<0 || cardIndex>g.getCardCount()) {
@@ -197,69 +288,52 @@ public class GameManager {
} }
Card c = g.getCards()[cardIndex]; Card c = g.getCards()[cardIndex];
boolean endedGuessing = false; if(c.isColorPublic(other(player)) || (c.getColor(player) == Card.Color.GREEN && c.isColorPublic(player)))
throw new RuntimeException("Card has already been revealed");
c.setColorPublic(other(player), true);
Card.Color guessedColor = c.getColor(other(player));
if (player == 'A') { newGameEvent(g, GameEvent.newGuessEvent(c, player));
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(); g.incrementCurrentGuessCount();
if (!g.getCurrentHint().canStillGuess(g.getCurrentGuessCount())) if(guessedColor == Color.BLACK)
endedGuessing = true; endGame(g, false);
else if (g.getPhase().isSuddenDeath() && guessedColor == Color.WHITE)
if(endedGuessing) { endGame(g, false);
g.setCurrentHint(null); else if (allGreenRevealed(g, other(player)) && guessedColor == Color.GREEN) {
if (player == 'A') // i.e. i made my last guess
g.setPhase(Grid.Phase.HINTING_A); if (allGreenRevealed(g, player)) {
else endGame(g, true);
g.setPhase(Grid.Phase.HINTING_B); } else if (g.getPhase().isSuddenDeath()) {
g.newGameEvent(GameEvent.newEndGuessingEvent(false, player)); // Sudden death with only the other
setPhase(g, Phase.suddenDeathOnly(other(player)));
} else {
endGuessingRound(g, player);
} }
return (player == 'A')?c.getColorB():c.getColorA(); } else if (!allGreenRevealed(g, other(player)) && guessedColor == Color.GREEN) {
// There is still cards i should find
if (!g.getPhase().isSuddenDeath() && !g.getCurrentHint().canStillGuess(g.getCurrentGuessCount())) {
// We're done with this round of guesses
endGuessingRound(g, player);
} else {
// I continue guessing cards in this round
}
} else if (guessedColor == Color.WHITE && !g.getPhase().isSuddenDeath()){
endGuessingRound(g, player);
} else {
throw new IllegalStateException("This should logically never happen");
}
if(!g.getPhase().isAnyoneGuessing())
// It means this pick ended the guessing turn
newGameEvent(g, GameEvent.newEndGuessingEvent(false, player));
return guessedColor;
} }
public void endGuessing(Grid g, char player) { public void endGuessing(Grid g, char player) {
if (!(((player == 'A') && (g.getPhase() == Grid.Phase.GUESSING_A)) || if (!g.getPhase().isGuessing(player)) {
((player == 'B') && (g.getPhase() == Grid.Phase.GUESSING_B)))) {
throw new RuntimeException("This action is not allowed now"); throw new RuntimeException("This action is not allowed now");
} }
endGuessingRound(g, player);
g.setCurrentHint(null); newGameEvent(g, GameEvent.newEndGuessingEvent(true, player));
if (player == 'A')
g.setPhase(Grid.Phase.HINTING_B);
else
g.setPhase(Grid.Phase.HINTING_A);
g.newGameEvent(GameEvent.newEndGuessingEvent(true, player));
} }
} }
@@ -13,14 +13,33 @@ public class Card {
Color colorA; Color colorA;
Color colorB; Color colorB;
// revealedA is wether A knows B's color boolean colorAPublic;
boolean revealedA; boolean colorBPublic;
boolean revealedB;
public Card(String word, Color colorA, Color colorB) { public Card(String word, Color colorA, Color colorB) {
this(word, colorA, colorB, false, false); this(word, colorA, colorB, false, false);
} }
public Color getColor(char player) {
if(player == 'A') return this.getColorA();
else return this.getColorB();
}
public boolean isColorPublic(char player) {
if(player == 'A') return this.isColorAPublic();
else return this.isColorBPublic();
}
public void setColorPublic(char player, boolean colorPublic) {
if(player == 'A') this.setColorAPublic(colorPublic);
else this.setColorBPublic(colorPublic);
}
@Override
public String toString() {
return String.format("%s [%s%s(%s%s)]", getWord(),
getColorA().name().charAt(0), getColorB().name().charAt(0),
isColorAPublic()?'O':'X', isColorBPublic()?'O':'X');
}
public enum Color { public enum Color {
GREEN, GREEN,
WHITE, WHITE,
@@ -1,7 +1,5 @@
package com.bernard.nodecames.model; package com.bernard.nodecames.model;
import java.util.List;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue; import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id; import jakarta.persistence.Id;
@@ -28,8 +28,11 @@ public class GameEvent {
public static GameEvent newEndGuessingEvent(boolean manual, char issuer) { public static GameEvent newEndGuessingEvent(boolean manual, char issuer) {
return new GameEvent(Type.END_GUESSING, manual, issuer); return new GameEvent(Type.END_GUESSING, manual, issuer);
} }
public static GameEvent newStartGameEvent() { public static GameEvent newGameEndEvent(boolean won) {
return new GameEvent(Type.START_GAME, null, '_'); return new GameEvent(Type.GAME_END, won, '_');
}
public static GameEvent newSuddenDeath() {
return new GameEvent(Type.SUDDEN_DEATH, Object.class, '_');
} }
@AllArgsConstructor @AllArgsConstructor
@@ -37,7 +40,8 @@ public class GameEvent {
HINT(Hint.class), HINT(Hint.class),
GUESS(Card.class), GUESS(Card.class),
END_GUESSING(Boolean.class), END_GUESSING(Boolean.class),
START_GAME(Object.class); GAME_END(Boolean.class),
SUDDEN_DEATH(Object.class);
Class<?> dataType; Class<?> dataType;
} }
@@ -2,11 +2,10 @@ package com.bernard.nodecames.model;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@@ -29,9 +28,10 @@ public class Grid {
int usedWhites; int usedWhites;
Phase phase; Phase phase;
Optional<Boolean> won;
public Grid(String name, Card[] cards) { public Grid(String name, Card[] cards) {
this(name, cards, new ArrayList<>(), null, 0, 0, 0, Phase.HINTING_ANY); this(name, cards, new ArrayList<>(), null, 0, 0, 0, Phase.bothHinting(), Optional.empty());
} }
public void incrementHintCount() { public void incrementHintCount() {
@@ -50,12 +50,59 @@ public class Grid {
return this.getCards().length; return this.getCards().length;
} }
public enum Phase { @EqualsAndHashCode
HINTING_ANY, @Getter
HINTING_A, public static class Phase {
HINTING_B, private boolean aHinting;
GUESSING_A, private boolean bHinting;
GUESSING_B; private boolean aGuessing;
private boolean bGuessing;
private boolean suddenDeath;
boolean gameEnded;
private Phase(boolean aHinting,
boolean bHinting,
boolean aGuessing,
boolean bGuessing,
boolean suddenDeath,
boolean gameEnded) {
this.aHinting = aHinting;
this.bHinting = bHinting;
this.aGuessing = aGuessing;
this.bGuessing = bGuessing;
this.suddenDeath = suddenDeath;
this.gameEnded = gameEnded;
}
public boolean isHinting(char player) {
return (player == 'A')?this.isAHinting():this.isBHinting();
}
public boolean isGuessing(char player) {
return (player == 'A')?this.isAGuessing():this.isBGuessing();
}
public boolean isAnyoneGuessing() {
return this.isAGuessing() || this.isBGuessing();
}
public static Phase bothHinting() {
return new Phase(true, true, false, false, false, false);
}
public static Phase hinting(char player) {
return new Phase((player == 'A'), (player == 'B'), false, false, false, false);
}
public static Phase guessing(char player) {
return new Phase(false, false, (player == 'A'), (player == 'B'), false, false);
}
public static Phase suddenDeathBoth() {
return new Phase(false, false, true, true, true, false);
}
public static Phase suddenDeathOnly(char player) {
return new Phase(false, false, player == 'A', player == 'B', true, false);
}
public static Phase gameEnded() {
return new Phase(false,false,false,false,false,true);
}
} }
} }
+132 -67
View File
@@ -12,36 +12,34 @@ function getRoomId() {
return res[1] return res[1]
} }
/*
* Update html to match state
*/
function updatePhase(phase) { function updatePhase(phase) {
if(phase == "hinting_any" || (phase == "hinting_a" && player == 'A') || (phase == "hinting_b" && player == 'B')) { p = player.toLowerCase()
// Hinting o = (player=='A')?'b':'a'
$('.card-button').css('visibility', 'hidden') if(phase[p+'-guessing']) {
$('#hint-text').hide()
$('#submit-hint').show()
$('#other-playing').hide()
$('#end-guessing').hide()
} else if ((phase == "hinting_b" && player == 'A') || (phase == "hinting_a" && player == 'B')) {
// Wait for the other one
$('.card-button').css('visibility', 'hidden')
$('#hint-text').hide()
$('#submit-hint').hide()
$('#other-playing').show()
$('#end-guessing').hide()
} else if ((phase == "guessing_a" && player == 'B') || (phase == "guessing_b" && player == 'A')) {
// Wait for the other one
$('.card-button').css('visibility', 'hidden')
$('#hint-text').show()
$('#submit-hint').hide()
$('#other-playing').show()
$('#end-guessing').hide()
} else if ((phase == "guessing_a" && player == 'A') || (phase == "guessing_b" && player == 'B')) {
// Guessing
$('.card-button').css('visibility', 'visible') $('.card-button').css('visibility', 'visible')
$('.card-button.untouchable').css('visibility', 'hidden') $('.card-button.untouchable').css('visibility', 'hidden')
$('#end-guessing-button').show()
} else {
$('.card-button').css('visibility', 'hidden')
$('#end-guessing-button').hide()
}
if(phase[p+'-guessing'] || phase[o+'-guessing']) {
$('#hint-text').show() $('#hint-text').show()
} else {
$('#hint-text').hide()
}
if(phase[p+'-hinting']) {
$('#submit-hint').show()
} else {
$('#submit-hint').hide() $('#submit-hint').hide()
}
if(!phase[p+'-guessing'] & !phase[p+'-hinting']) {
$('#other-playing').show()
} else {
$('#other-playing').hide() $('#other-playing').hide()
$('#end-guessing').show()
} }
} }
@@ -49,6 +47,20 @@ function setHint(hint, wordCount) {
$('#hint-text').text(hint+" in "+wordCount) $('#hint-text').text(hint+" in "+wordCount)
} }
function createCard(i) {
html = `
<li class="card ${cards[i].color}" id="card-${i}">
<span class="card-word">${cards[i].word}</span><br/>
<div class="card-bottom-bar">
<div id="card-${i}-a-public">A</div>
<button class="card-button" id="card-${i}-button">Toucher</button>
<div id="card-${i}-b-public">B</div>
</div>
</li>
`
$('#cards-list').append($(html))
$(`#card-${i}-button`).on('click', pointCard)
}
function updateCard(i) { function updateCard(i) {
card = cards[i] card = cards[i]
if(card['revealed-a']) { if(card['revealed-a']) {
@@ -76,26 +88,102 @@ function updateCard(i) {
} }
} }
function addHintEvent(issuer, hint, hintWordCount) {
$("#event-log").append($(`
<li>
${issuer} proposed "${hint}" in ${hintWordCount} word(s)
</li>
`))
}
function addPointEvent(issuer, cardIndex, color) {
$("#event-log").append($(`
<li>
${issuer} pointed card ${cards[cardIndex].word} (${color})
</li>
`))
}
function addEndGuessingEvent(issuer, manual) {
if(manual) {
$("#event-log").append($(`
<li>
${issuer} ended guessing
</li>
`))
} else {
$("#event-log").append($(`
<li>
${issuer} can't guess anymore
</li>
`))
}
}
function addSuddenDeathEvent() {
$("#event-log").append($(`
<li>
Starting sudden death
</li>
`))
}
function addEndGameEvent(issuer, won) {
if(won) {
$("#event-log").append($(`
<li>
You won the game !
</li>
`))
} else {
$("#event-log").append($(`
<li>
You lost the game !
</li>
`))
}
}
function addEvent(e) {
switch(e.type) {
case "hint":
addHintEvent(e.issuer, e.word, e.wordCount)
setHint(e.word, e.wordCount)
break
case "guess":
addPointEvent(e.issuer, e['card-index'], e.color)
break
case "end_guessing":
addEndGuessingEvent(e.issuer, e.manual)
break
case "game_end":
addEndGameEvent(e.issuer, e.win)
break
case "sudden_death":
addSuddenDeathEvent()
break
}
}
/*
* Listeners
*/
function initGame(data) { function initGame(data) {
cards = data['cards'] cards = data['cards']
$('#cards-list').empty() $('#cards-list').empty()
for(var i = 0; i<cards.length; i++) { for(var i = 0; i<cards.length; i++) {
html = ` createCard(i)
<li class="card ${cards[i].color}" id="card-${i}">
<span class="card-word">${cards[i].word}</span><br/>
<div class="card-bottom-bar">
<div id="card-${i}-a-public">A</div>
<button class="card-button" id="card-${i}-button">Toucher</button>
<div id="card-${i}-b-public">B</div>
</div>
</li>
`
$('#cards-list').append($(html))
$(`#card-${i}-button`).on('click', pointCard)
updateCard(i) updateCard(i)
} }
updatePhase(data['phase']) updatePhase(data['phase'])
events = data.events
for(var i = 0; i<events.length; i++) {
e = events[i]
addEvent(e)
}
if(data['current-hint']) {
setHint(data['current-hint'].word, data['current-hint'].wordCount)
}
} }
function selectPlayer(e) { function selectPlayer(e) {
@@ -117,10 +205,8 @@ function selectPlayer(e) {
socket = Stomp.over(new SockJS('/socket')); socket = Stomp.over(new SockJS('/socket'));
socket.connect({}, function (frame) { socket.connect({}, function (frame) {
socket.subscribe('/topic/submit-hint', onSubmitHint);
socket.subscribe('/topic/point-card', onPointCard);
socket.subscribe('/topic/end-guessing', onEndGuessing);
socket.subscribe('/topic/new-phase', onNewPhase); socket.subscribe('/topic/new-phase', onNewPhase);
socket.subscribe('/topic/new-event', onNewEvent);
socket.subscribe('/topic/update-card/' + player.toLowerCase(), onUpdateCard); socket.subscribe('/topic/update-card/' + player.toLowerCase(), onUpdateCard);
}); });
} }
@@ -142,34 +228,13 @@ function pointCard(e) {
function endGuessing(e) { function endGuessing(e) {
socket.send('/app/end-guessing', wsHeaders, "") socket.send('/app/end-guessing', wsHeaders, "")
} }
function onSubmitHint(m) {
data = JSON.parse(m.body)
setHint(data['hint'], data['hintWordCount'])
$("#event-log").append($(`
<li>
${data.player} proposed "${data['hint']}" in ${data['hintWordCount']} word(s)
</li>
`))
}
function onPointCard(m) {
data = JSON.parse(m.body)
i = data['cardIndex']
$("#event-log").append($(`
<li>
${data.player} pointed card ${cards[i].word} (${data.color})
</li>
`))
}
function onEndGuessing(m) {
$("#event-log").append($(`
<li>
Ended guessing
</li>
`))
}
function onNewPhase(m) { function onNewPhase(m) {
updatePhase(m.body) phase = JSON.parse(m.body)
updatePhase(phase)
}
function onNewEvent(m) {
e = JSON.parse(m.body)
addEvent(e)
} }
function onUpdateCard(m) { function onUpdateCard(m) {