diff --git a/src/main/java/com/bernard/nodecames/frontend/WebSocketController.java b/src/main/java/com/bernard/nodecames/frontend/WebSocketController.java index 9aed68a..1545ef0 100644 --- a/src/main/java/com/bernard/nodecames/frontend/WebSocketController.java +++ b/src/main/java/com/bernard/nodecames/frontend/WebSocketController.java @@ -7,7 +7,6 @@ import org.springframework.http.ResponseEntity; 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.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; 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.model.Card; +import com.bernard.nodecames.model.GameEvent; import com.bernard.nodecames.model.Grid; -import com.bernard.nodecames.model.Card.Color; import com.bernard.nodecames.model.Grid.Phase; -import lombok.AllArgsConstructor; import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.JsonNodeFactory; -import tools.jackson.databind.node.ObjectNode; @Controller -@AllArgsConstructor public class WebSocketController { private static final JsonNodeFactory jsn = JsonNodeFactory.instance; @@ -33,12 +29,25 @@ public class WebSocketController { private GameManager gm; private SimpMessagingTemplate template; - public void publishNewPhase(Grid g) { - this.template.convertAndSend( - "/topic/new-phase", - g.getPhase().name().toLowerCase() + public WebSocketController(GameManager gm, SimpMessagingTemplate template) { + this.gm = gm; + this.template = template; + 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) { Card c = g.getCards()[cardIndex]; this.template.convertAndSend( @@ -68,42 +77,27 @@ public class WebSocketController { } @MessageMapping("submit-hint") - @SendTo("/topic/submit-hint") - public JsonNode submitHint(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) { + public void 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 = gm.findGrid(roomId); gm.proposeHint(g, player.charAt(0), hint, wordCount); - content.asObject().set("player", jsn.stringNode(player)); - this.publishNewPhase(g); - return content; } @MessageMapping("point-card") - @SendTo("/topic/point-card") - public JsonNode pointCard(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) { + public void pointCard(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) { int cardIndex = content.asObject().get("cardIndex").intValue(); Grid g = gm.findGrid(roomId); - Color c = 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); + gm.pointCard(g, player.charAt(0), cardIndex); publishCardUpdate(g, cardIndex); - return content; } @MessageMapping("end-guessing") - @SendTo("/topic/end-guessing") - public JsonNode endGuessing(@Header("room") String roomId, @Header("player") String player) { + public void endGuessing(@Header("room") String roomId, @Header("player") String player) { Grid g = gm.findGrid(roomId); gm.endGuessing(g, player.charAt(0)); - - ObjectNode out = jsn.objectNode(); - out.set("player", jsn.stringNode(player)); - return out; } } diff --git a/src/main/java/com/bernard/nodecames/game/GameManager.java b/src/main/java/com/bernard/nodecames/game/GameManager.java index 1fb21fb..04d4519 100644 --- a/src/main/java/com/bernard/nodecames/game/GameManager.java +++ b/src/main/java/com/bernard/nodecames/game/GameManager.java @@ -7,6 +7,7 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Random; import java.util.Scanner; import java.util.Set; @@ -17,12 +18,15 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.core.io.Resource; import org.springframework.stereotype.Service; +import com.bernard.nodecames.frontend.WebSocketController; import com.bernard.nodecames.model.Card; +import com.bernard.nodecames.model.Card.Color; import com.bernard.nodecames.model.GameEvent; import com.bernard.nodecames.model.Grid; +import com.bernard.nodecames.model.Grid.Phase; 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.node.ArrayNode; import tools.jackson.databind.node.JsonNodeFactory; @@ -33,12 +37,30 @@ public class GameManager { private static final JsonNodeFactory jsn = JsonNodeFactory.instance; + public static final int MAX_HINT_COUNT = 9; + //TODO Remove static, make something more Spring Boot-y private static Map games = new HashMap<>(); + @Setter + WebSocketController wsc; + @Value("classpath:dictionaries/fr.txt") 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 public List allWords() { try (Scanner wordsScanner = new Scanner(dictFr.getInputStream())) { @@ -70,73 +92,10 @@ public class GameManager { return out; } - 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.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; + private char other(char player) { + return (player=='A')?'B':'A'; } - - 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) { return games.get(UUID.fromString(gridId)); } @@ -169,27 +128,159 @@ public class GameManager { 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) { - if (!(((player == 'A') && (g.getPhase() == Grid.Phase.HINTING_A)) || - ((player == 'B') && (g.getPhase() == Grid.Phase.HINTING_B)) || - g.getPhase() == Grid.Phase.HINTING_ANY)) { + if (!g.getPhase().isHinting(player)) { 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); + 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) { - if (!(((player == 'A') && (g.getPhase() == Grid.Phase.GUESSING_A)) || - ((player == 'B') && (g.getPhase() == Grid.Phase.GUESSING_B)))) { + if (!g.getPhase().isGuessing(player)) { throw new RuntimeException("This action is not allowed now"); } if (cardIndex<0 || cardIndex>g.getCardCount()) { @@ -197,69 +288,52 @@ public class GameManager { } 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(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(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)); + newGameEvent(g, GameEvent.newGuessEvent(c, player)); + g.incrementCurrentGuessCount(); + if(guessedColor == Color.BLACK) + endGame(g, false); + else if (g.getPhase().isSuddenDeath() && guessedColor == Color.WHITE) + endGame(g, false); + else if (allGreenRevealed(g, other(player)) && guessedColor == Color.GREEN) { + // i.e. i made my last guess + if (allGreenRevealed(g, player)) { + endGame(g, true); + } else if (g.getPhase().isSuddenDeath()) { + // Sudden death with only the other + setPhase(g, Phase.suddenDeathOnly(other(player))); + } else { + endGuessingRound(g, player); + } + } 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"); } - return (player == 'A')?c.getColorB():c.getColorA(); + 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) { - if (!(((player == 'A') && (g.getPhase() == Grid.Phase.GUESSING_A)) || - ((player == 'B') && (g.getPhase() == Grid.Phase.GUESSING_B)))) { + if (!g.getPhase().isGuessing(player)) { 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)); + endGuessingRound(g, player); + newGameEvent(g, GameEvent.newEndGuessingEvent(true, player)); } } diff --git a/src/main/java/com/bernard/nodecames/model/Card.java b/src/main/java/com/bernard/nodecames/model/Card.java index 99501be..2c8036c 100644 --- a/src/main/java/com/bernard/nodecames/model/Card.java +++ b/src/main/java/com/bernard/nodecames/model/Card.java @@ -13,14 +13,33 @@ public class Card { Color colorA; Color colorB; - // revealedA is wether A knows B's color - boolean revealedA; - boolean revealedB; + boolean colorAPublic; + boolean colorBPublic; public Card(String word, Color colorA, Color colorB) { 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 { GREEN, WHITE, diff --git a/src/main/java/com/bernard/nodecames/model/Game.java b/src/main/java/com/bernard/nodecames/model/Game.java index 21f13be..86e8158 100644 --- a/src/main/java/com/bernard/nodecames/model/Game.java +++ b/src/main/java/com/bernard/nodecames/model/Game.java @@ -1,7 +1,5 @@ package com.bernard.nodecames.model; -import java.util.List; - import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; diff --git a/src/main/java/com/bernard/nodecames/model/GameEvent.java b/src/main/java/com/bernard/nodecames/model/GameEvent.java index a1f2601..1c2b1fa 100644 --- a/src/main/java/com/bernard/nodecames/model/GameEvent.java +++ b/src/main/java/com/bernard/nodecames/model/GameEvent.java @@ -28,8 +28,11 @@ public class GameEvent { 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, '_'); + public static GameEvent newGameEndEvent(boolean won) { + return new GameEvent(Type.GAME_END, won, '_'); + } + public static GameEvent newSuddenDeath() { + return new GameEvent(Type.SUDDEN_DEATH, Object.class, '_'); } @AllArgsConstructor @@ -37,7 +40,8 @@ public class GameEvent { HINT(Hint.class), GUESS(Card.class), END_GUESSING(Boolean.class), - START_GAME(Object.class); + GAME_END(Boolean.class), + SUDDEN_DEATH(Object.class); Class dataType; } diff --git a/src/main/java/com/bernard/nodecames/model/Grid.java b/src/main/java/com/bernard/nodecames/model/Grid.java index 034d475..bdf5250 100644 --- a/src/main/java/com/bernard/nodecames/model/Grid.java +++ b/src/main/java/com/bernard/nodecames/model/Grid.java @@ -2,11 +2,10 @@ package com.bernard.nodecames.model; import java.util.ArrayList; 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.EqualsAndHashCode; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @@ -29,9 +28,10 @@ public class Grid { int usedWhites; Phase phase; + Optional won; 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() { @@ -50,12 +50,59 @@ public class Grid { return this.getCards().length; } - public enum Phase { - HINTING_ANY, - HINTING_A, - HINTING_B, - GUESSING_A, - GUESSING_B; + @EqualsAndHashCode + @Getter + public static class Phase { + private boolean aHinting; + private boolean bHinting; + 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); + } } } diff --git a/src/main/resources/static/js/grid.js b/src/main/resources/static/js/grid.js index b7a42d8..244c99d 100644 --- a/src/main/resources/static/js/grid.js +++ b/src/main/resources/static/js/grid.js @@ -12,36 +12,34 @@ function getRoomId() { return res[1] } +/* + * Update html to match state + */ function updatePhase(phase) { - if(phase == "hinting_any" || (phase == "hinting_a" && player == 'A') || (phase == "hinting_b" && player == 'B')) { - // Hinting - $('.card-button').css('visibility', 'hidden') - $('#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 + p = player.toLowerCase() + o = (player=='A')?'b':'a' + if(phase[p+'-guessing']) { $('.card-button').css('visibility', 'visible') $('.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() + } else { + $('#hint-text').hide() + } + if(phase[p+'-hinting']) { + $('#submit-hint').show() + } else { $('#submit-hint').hide() + } + if(!phase[p+'-guessing'] & !phase[p+'-hinting']) { + $('#other-playing').show() + } else { $('#other-playing').hide() - $('#end-guessing').show() } } @@ -49,6 +47,20 @@ function setHint(hint, wordCount) { $('#hint-text').text(hint+" in "+wordCount) } +function createCard(i) { + html = ` +
  • + ${cards[i].word}
    +
    +
    A
    + +
    B
    +
    +
  • + ` + $('#cards-list').append($(html)) + $(`#card-${i}-button`).on('click', pointCard) +} function updateCard(i) { card = cards[i] if(card['revealed-a']) { @@ -76,26 +88,102 @@ function updateCard(i) { } } +function addHintEvent(issuer, hint, hintWordCount) { + $("#event-log").append($(` +
  • + ${issuer} proposed "${hint}" in ${hintWordCount} word(s) +
  • + `)) +} + +function addPointEvent(issuer, cardIndex, color) { + $("#event-log").append($(` +
  • + ${issuer} pointed card ${cards[cardIndex].word} (${color}) +
  • + `)) +} + +function addEndGuessingEvent(issuer, manual) { + if(manual) { + $("#event-log").append($(` +
  • + ${issuer} ended guessing +
  • + `)) + } else { + $("#event-log").append($(` +
  • + ${issuer} can't guess anymore +
  • + `)) + } +} +function addSuddenDeathEvent() { + $("#event-log").append($(` +
  • + Starting sudden death +
  • + `)) +} +function addEndGameEvent(issuer, won) { + if(won) { + $("#event-log").append($(` +
  • + You won the game ! +
  • + `)) + } else { + $("#event-log").append($(` +
  • + You lost the game ! +
  • + `)) + } +} + +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) { cards = data['cards'] $('#cards-list').empty() for(var i = 0; i - ${cards[i].word}
    -
    -
    A
    - -
    B
    -
    - - ` - $('#cards-list').append($(html)) - $(`#card-${i}-button`).on('click', pointCard) + createCard(i) updateCard(i) } - updatePhase(data['phase']) + events = data.events + for(var i = 0; i - ${data.player} proposed "${data['hint']}" in ${data['hintWordCount']} word(s) - - `)) -} -function onPointCard(m) { - data = JSON.parse(m.body) - i = data['cardIndex'] - $("#event-log").append($(` -
  • - ${data.player} pointed card ${cards[i].word} (${data.color}) -
  • - `)) -} -function onEndGuessing(m) { - $("#event-log").append($(` -
  • - Ended guessing -
  • - `)) -} 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) {