Added games
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
package com.bernard.nodecames;
|
package com.bernard.nodecames;
|
||||||
|
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
@@ -12,14 +14,18 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.servlet.view.RedirectView;
|
import org.springframework.web.servlet.view.RedirectView;
|
||||||
|
|
||||||
import com.bernard.nodecames.frontend.GridConfigEdit;
|
import com.bernard.nodecames.frontend.GridConfigEdit;
|
||||||
import com.bernard.nodecames.game.GameManager;
|
import com.bernard.nodecames.game.GridManager;
|
||||||
import com.bernard.nodecames.game.IllegalGameActionException;
|
import com.bernard.nodecames.game.IllegalGameActionException;
|
||||||
|
import com.bernard.nodecames.game.GameManager;
|
||||||
|
import com.bernard.nodecames.model.Game;
|
||||||
import com.bernard.nodecames.model.Grid;
|
import com.bernard.nodecames.model.Grid;
|
||||||
import com.bernard.nodecames.model.GridConfig;
|
import com.bernard.nodecames.model.GridConfig;
|
||||||
import com.bernard.nodecames.model.GridConfig.MutableOption;
|
import com.bernard.nodecames.model.GridConfig.MutableOption;
|
||||||
|
import com.bernard.nodecames.model.World;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@@ -27,16 +33,26 @@ import lombok.RequiredArgsConstructor;
|
|||||||
@Controller
|
@Controller
|
||||||
public class HttpController {
|
public class HttpController {
|
||||||
|
|
||||||
private final GameManager gm;
|
private final GridManager gm;
|
||||||
|
private final GameManager wm;
|
||||||
private final Random rand = new Random();
|
private final Random rand = new Random();
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public String index() {
|
public String index(Model m) {
|
||||||
|
List<Game> games = wm.allGames().stream().sorted(Comparator.comparing(Game::getName)).toList();
|
||||||
|
m.addAttribute("games", games);
|
||||||
return "index.html";
|
return "index.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/room/{id}/grid")
|
@GetMapping("/room/{id}/grid")
|
||||||
public String grid(@PathVariable("id") String id) {
|
public String grid(@PathVariable("id") String id, @RequestParam(value = "player", required = false) String player, Model m) {
|
||||||
|
if(player == null) {
|
||||||
|
m.addAttribute("redirect", "/room/"+id+"/grid");
|
||||||
|
return "player-select";
|
||||||
|
}
|
||||||
|
Grid grid = gm.findGrid(id);
|
||||||
|
m.addAttribute("player", player);
|
||||||
|
m.addAttribute("grid", grid);
|
||||||
return "grid";
|
return "grid";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +85,45 @@ public class HttpController {
|
|||||||
@GetMapping("/create-room")
|
@GetMapping("/create-room")
|
||||||
public RedirectView createRoom() {
|
public RedirectView createRoom() {
|
||||||
Random r = new Random(rand.nextLong());
|
Random r = new Random(rand.nextLong());
|
||||||
UUID uuid = gm.newGrid(GridConfig.ZERO, r);
|
Grid g = gm.newGrid(GridConfig.ZERO, r);
|
||||||
return new RedirectView("/room/"+uuid.toString()+"/grid");
|
return new RedirectView("/room/"+g.getId()+"/grid");
|
||||||
|
}
|
||||||
|
@GetMapping("/create-game")
|
||||||
|
public RedirectView createGame(@RequestParam("name") String name) {
|
||||||
|
UUID uuid = wm.createGame(name);
|
||||||
|
return new RedirectView("/game/"+uuid.toString()+"/map");
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/game/{id}/map")
|
||||||
|
public String getMap(@PathVariable("id") String id, @RequestParam(value = "player", required = false) String player, Model m) {
|
||||||
|
if(player == null) {
|
||||||
|
m.addAttribute("redirect", "/game/"+id+"/map");
|
||||||
|
return "player-select";
|
||||||
|
}
|
||||||
|
Game g = wm.getGame(id);
|
||||||
|
List<World> worlds = g.getWorlds()
|
||||||
|
.entrySet()
|
||||||
|
.stream()
|
||||||
|
.map(Entry::getValue)
|
||||||
|
.sorted(Comparator.comparing(World::getName))
|
||||||
|
.toList();
|
||||||
|
m.addAttribute("game", g);
|
||||||
|
m.addAttribute("worlds", worlds);
|
||||||
|
m.addAttribute("player", player);
|
||||||
|
return "map";
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/game/{id}/grid/{world}")
|
||||||
|
public Object gridRedirect(@PathVariable("id") String id, @PathVariable("world") String world, @RequestParam("player") String player) {
|
||||||
|
Game g = wm.getGame(id);
|
||||||
|
Grid grid = wm.getGrid(g, world);
|
||||||
|
return new RedirectView("/room/"+grid.getId()+"/grid" + "?player="+player);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/game/{id}/reset/{world}")
|
||||||
|
public Object resetWorld(@PathVariable("id") String id, @PathVariable("world") String world, @RequestParam("player") String player) {
|
||||||
|
Game g = wm.getGame(id);
|
||||||
|
Grid newGrid = wm.resetGrid(g, world);
|
||||||
|
return new RedirectView("/room/"+newGrid.getId()+"/grid" + "?player="+player);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
|
||||||
import com.bernard.nodecames.game.GameManager;
|
import com.bernard.nodecames.game.GridManager;
|
||||||
import com.bernard.nodecames.game.IllegalGameActionException;
|
import com.bernard.nodecames.game.IllegalGameActionException;
|
||||||
import com.bernard.nodecames.model.Card;
|
import com.bernard.nodecames.model.Card;
|
||||||
import com.bernard.nodecames.model.GameEvent;
|
import com.bernard.nodecames.model.GameEvent;
|
||||||
@@ -24,11 +24,11 @@ import tools.jackson.databind.JsonNode;
|
|||||||
@Controller
|
@Controller
|
||||||
public class WebSocketController {
|
public class WebSocketController {
|
||||||
|
|
||||||
private GameManager gm;
|
private GridManager gm;
|
||||||
private SimpMessagingTemplate template;
|
private SimpMessagingTemplate template;
|
||||||
private JsonDataService json;
|
private JsonDataService json;
|
||||||
|
|
||||||
public WebSocketController(GameManager gm, SimpMessagingTemplate template, JsonDataService json) {
|
public WebSocketController(GridManager gm, SimpMessagingTemplate template, JsonDataService json) {
|
||||||
this.gm = gm;
|
this.gm = gm;
|
||||||
this.template = template;
|
this.template = template;
|
||||||
this.json = json;
|
this.json = json;
|
||||||
|
|||||||
@@ -1,420 +1,92 @@
|
|||||||
package com.bernard.nodecames.game;
|
package com.bernard.nodecames.game;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
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.Set;
|
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
|
||||||
import org.slf4j.LoggerFactory;
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.bernard.nodecames.frontend.WebSocketController;
|
import com.bernard.nodecames.model.Game;
|
||||||
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;
|
||||||
import com.bernard.nodecames.model.Grid.Phase;
|
|
||||||
import com.bernard.nodecames.model.GridConfig;
|
import com.bernard.nodecames.model.GridConfig;
|
||||||
import com.bernard.nodecames.model.GridConfig.CardColors;
|
import com.bernard.nodecames.model.World;
|
||||||
import com.bernard.nodecames.model.GridConfig.MutableOption;
|
|
||||||
import com.bernard.nodecames.model.Hint;
|
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import lombok.Setter;
|
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class GameManager {
|
public class GameManager {
|
||||||
|
|
||||||
//TODO Remove static, make something more Spring Boot-y
|
Map<UUID, Game> games = new HashMap<>();
|
||||||
private static Map<UUID, Grid> games = new HashMap<>();
|
Map<UUID, Random> randoms = new HashMap<>();
|
||||||
private static Map<UUID, Random> randoms = new HashMap<>();
|
|
||||||
|
|
||||||
@Setter
|
// Reverse map allowing to pass grid events to the game
|
||||||
private WebSocketController wsc = null;
|
Map<UUID, World> currentGrids = new HashMap<>();
|
||||||
|
|
||||||
private final DictionnariesService dictionnaries;
|
private final GridManager gm;
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(GameManager.class);
|
public GameManager(GridManager gm) {
|
||||||
|
this.gm = gm;
|
||||||
private void newGameEvent(Grid g, GameEvent ge) {
|
gm.setWm(this);
|
||||||
g.newGameEvent(ge);
|
|
||||||
log.debug("New game event : {}", ge);
|
|
||||||
wsc.onNewEvent(g, ge);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPhase(Grid g, Phase phase) {
|
public UUID createGame(String name) {
|
||||||
g.setPhase(phase);
|
|
||||||
log.debug("New phase : {}", phase.name());
|
|
||||||
if(!phase.isAnyoneGuessing()) {
|
|
||||||
g.setCurrentHint(null);
|
|
||||||
}
|
|
||||||
wsc.onNewPhase(g);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void changeConfig(Grid g, MutableOption cc, Object data) {
|
|
||||||
g.getConfig().set(cc, data);
|
|
||||||
log.debug("Changed config option: {} -> new value: {}", cc.name(), data);
|
|
||||||
wsc.onConfigChange(g, cc, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private char other(char player) {
|
|
||||||
return (player=='A')?'B':'A';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
@returns true iff all green cards of this player have been revealed either side
|
|
||||||
if onlyoneGreenGreen is false, checks only the side of this player
|
|
||||||
*/
|
|
||||||
private boolean allGreenRevealed(Grid g, char player, boolean onlyOneGreenGreen) {
|
|
||||||
for(Card c : g.getCards()) {
|
|
||||||
if ((c.getColor(player) == Color.GREEN && !c.isColorPublic(player)) &&
|
|
||||||
(!onlyOneGreenGreen || (c.getColor(player) == Color.GREEN && !c.isColorPublic(player))))
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true iff the game would have been won if the rule onlyOneGreenGreen was true
|
|
||||||
*/
|
|
||||||
private boolean onlyOneGreenGreenWouldWin(Grid g) {
|
|
||||||
return !g.getConfig().isOnlyOneGreenGreen() && allGreenRevealed(g, 'A', true) && allGreenRevealed(g, 'B', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Grid findGrid(String gridId) {
|
|
||||||
return games.get(UUID.fromString(gridId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public UUID newGrid(GridConfig gc, Random r) {
|
|
||||||
UUID uuid = UUID.randomUUID();
|
UUID uuid = UUID.randomUUID();
|
||||||
randoms.put(uuid, r);
|
randoms.put(uuid, new Random());
|
||||||
String[] words = dictionnaries.randomWords(gc.getWordCount(), r);
|
|
||||||
List<Card> cards = new ArrayList<>(gc.getWordCount());
|
|
||||||
List<CardColors> origDistrib = gc.getWordDistribution();
|
|
||||||
List<CardColors> distrib;
|
|
||||||
if(!gc.isStructuredGrid()) {
|
|
||||||
List<Color> bColors = origDistrib.stream().map(cc -> cc.getFaceB()).collect(Collectors.toList());
|
|
||||||
Collections.shuffle(bColors, r);
|
|
||||||
distrib = IntStream.range(0, gc.getWordCount())
|
|
||||||
.mapToObj(i -> CardColors.of(origDistrib.get(i).getFaceA(), bColors.get(i)))
|
|
||||||
.toList();
|
|
||||||
} else {
|
|
||||||
distrib = origDistrib;
|
|
||||||
}
|
|
||||||
for (int j = 0; j < distrib.size(); j++) {
|
|
||||||
cards.add(new Card(words[j], distrib.get(j).getFaceA(), distrib.get(j).getFaceB()));
|
|
||||||
}
|
|
||||||
Collections.shuffle(cards, r);
|
|
||||||
|
|
||||||
Phase startPhase;
|
Game g = new Game(uuid, name, null);
|
||||||
if(gc.isAnyoneStarts()) {
|
Map<String, World> worlds = Game.DEFAULT_WORLDS_CONFIG
|
||||||
startPhase = Phase.HINTING_BOTH;
|
.entrySet()
|
||||||
} else {
|
.stream()
|
||||||
if(r.nextBoolean())
|
.collect(Collectors.toMap(
|
||||||
startPhase = Phase.hinting('A');
|
Map.Entry::getKey,
|
||||||
else
|
e -> new World(g, e.getKey(), e.getValue(), null, false)
|
||||||
startPhase = Phase.hinting('B');
|
));
|
||||||
}
|
g.setWorlds(worlds);
|
||||||
|
|
||||||
Grid g = new Grid(uuid.toString(),
|
|
||||||
(Card[])cards.toArray(new Card[cards.size()]),
|
|
||||||
new ArrayList<>(),
|
|
||||||
null,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
startPhase,
|
|
||||||
Optional.empty(),
|
|
||||||
gc);
|
|
||||||
|
|
||||||
games.put(uuid, g);
|
games.put(uuid, g);
|
||||||
|
|
||||||
return uuid;
|
return uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**********************************
|
public Collection<Game> allGames() {
|
||||||
* GAME ACTIONS *
|
return games.values();
|
||||||
**********************************/
|
|
||||||
private void endGame(Grid g, boolean win) {
|
|
||||||
g.setWon(Optional.of(win));
|
|
||||||
setPhase(g, Phase.GAME_ENDED);
|
|
||||||
newGameEvent(g, GameEvent.newGameEndEvent(win));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void proposeHint(Grid g, char player, String hint, int wordCount) throws IllegalGameActionException {
|
private Grid newGrid(Game g, String world) {
|
||||||
if (!g.getPhase().canPlay() && !g.getPhase().isHinting(player)) {
|
|
||||||
throw new IllegalGameActionException(g, "This action is not allowed now");
|
|
||||||
}
|
|
||||||
if (!g.getConfig().getAvailableHintWordCount().contains(wordCount)) {
|
|
||||||
throw new IllegalGameActionException(g, "Unavailable word count");
|
|
||||||
}
|
|
||||||
if (hint.length() > g.getConfig().getHintMaxLength()) {
|
|
||||||
throw new IllegalGameActionException(g, "Hint is too long");
|
|
||||||
}
|
|
||||||
|
|
||||||
Hint h = new Hint(hint, wordCount);
|
World w = g.getWorld(world);
|
||||||
g.incrementHintCount();
|
GridConfig gc = w.getInitialConfig();
|
||||||
g.setCurrentHint(h);
|
Grid grid = gm.newGrid(gc, new Random(randoms.get(g.getId()).nextLong()));
|
||||||
g.setCurrentGuessCount(0);
|
|
||||||
newGameEvent(g, GameEvent.newHintEvent(h, player));
|
w.setCurrentGrid(grid);
|
||||||
setPhase(g, Phase.guessing(other(player)));
|
currentGrids.put(grid.getId(), w);
|
||||||
|
|
||||||
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Grid resetGrid(Game g, String world) {
|
||||||
* Returns 'A', 'B', '_' depending on who should be the next ones to hint if we enter a hinting phase
|
return newGrid(g, world);
|
||||||
*/
|
}
|
||||||
private char whoWouldHintNext(Grid g, char player) {
|
|
||||||
if(allGreenRevealed(g, player, g.getConfig().isOnlyOneGreenGreen())) {
|
public Grid getGrid(Game g, String world) {
|
||||||
// I can't hint anymore
|
World w = g.getWorld(world);
|
||||||
return other(player);
|
if(w.getCurrentGrid() != null) {
|
||||||
|
return w.getCurrentGrid();
|
||||||
} else {
|
} else {
|
||||||
if(g.getConfig().isHintsInARow() && !allGreenRevealed(g, other(player), g.getConfig().isOnlyOneGreenGreen())) {
|
return newGrid(g, world);
|
||||||
return '_';
|
|
||||||
} else {
|
|
||||||
return player;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public Game getGame(String id) {
|
||||||
* Enters the sudden death phase
|
return games.get(UUID.fromString(id));
|
||||||
*/
|
|
||||||
private void enterSuddenDeathInternal(Grid g) {
|
|
||||||
newGameEvent(g, GameEvent.newSuddenDeath());
|
|
||||||
if(allGreenRevealed(g, 'A', g.getConfig().isOnlyOneGreenGreen())) {
|
|
||||||
// If all 'A''s greens are revealed, A is the only one to guess
|
|
||||||
setPhase(g, Phase.suddenDeathOnly('A'));
|
|
||||||
} else {
|
|
||||||
// Else, we are both guessing
|
|
||||||
setPhase(g, Phase.SUDDEN_DEATH_BOTH);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void onGameWon(Grid g) {
|
||||||
Sets the phase of the grid when player ends its guessing round.
|
World w = currentGrids.get(g.getId());
|
||||||
@param g Must neither be ended neither be in a sudden death
|
if(w != null) {
|
||||||
*/
|
w.setWon(true);
|
||||||
private void endGuessingRound(Grid g, char player) {
|
|
||||||
// We're done with this round of guesses
|
|
||||||
if(g.getUsedHints() >= g.getConfig().getMaxHint()) {
|
|
||||||
if(g.getUsedHints() >= g.getConfig().getMaxMaxHint()) {
|
|
||||||
if(!g.getConfig().isSuddenDeath()) {
|
|
||||||
// We're entering sudden death
|
|
||||||
enterSuddenDeathInternal(g);
|
|
||||||
} else {
|
|
||||||
// Only sudden death can get us out
|
|
||||||
setPhase(g, Phase.NEED_SUDDEN_DEATH);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We are waiting for more hints
|
|
||||||
if(g.getConfig().isSuddenDeath()) {
|
|
||||||
setPhase(g, Phase.needMoreHints(whoWouldHintNext(g, player)));
|
|
||||||
} else {
|
|
||||||
setPhase(g, Phase.needMoreHintsOrSuddenDeath(whoWouldHintNext(g, player)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Next regular hinting round
|
|
||||||
setPhase(g, Phase.hinting(whoWouldHintNext(g, player)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public Card.Color pointCard(Grid g, char player, int cardIndex) throws IllegalGameActionException {
|
|
||||||
if (!g.getPhase().canPlay() || !g.getPhase().isGuessing(player)) {
|
|
||||||
throw new IllegalGameActionException(g, "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];
|
|
||||||
|
|
||||||
if(c.isColorPublic(other(player)) || (c.getColor(player) == Card.Color.GREEN && c.isColorPublic(player)))
|
|
||||||
throw new IllegalGameActionException(g, "Card has already been revealed");
|
|
||||||
c.setColorPublic(other(player), true);
|
|
||||||
Card.Color guessedColor = c.getColor(other(player));
|
|
||||||
|
|
||||||
newGameEvent(g, GameEvent.newGuessEvent(c, player));
|
|
||||||
g.incrementCurrentGuessCount();
|
|
||||||
if(guessedColor == Color.BLACK){
|
|
||||||
if(g.getConfig().isOnlyOneGreenGreenPreventDeath() && onlyOneGreenGreenWouldWin(g)) {
|
|
||||||
setPhase(g, Phase.WIN_IF_GREEN_GREEN);
|
|
||||||
} else {
|
|
||||||
endGame(g, false);
|
|
||||||
}
|
|
||||||
} else if (g.getPhase().isSuddenDeath() && guessedColor == Color.WHITE) {
|
|
||||||
if(g.getConfig().isOnlyOneGreenGreenPreventDeath() && onlyOneGreenGreenWouldWin(g)) {
|
|
||||||
setPhase(g, Phase.WIN_IF_GREEN_GREEN);
|
|
||||||
} else {
|
|
||||||
endGame(g, false);
|
|
||||||
}
|
|
||||||
} else if (allGreenRevealed(g, other(player), g.getConfig().isOnlyOneGreenGreen()) && guessedColor == Color.GREEN) {
|
|
||||||
// i.e. i made my last guess
|
|
||||||
if (allGreenRevealed(g, player, g.getConfig().isOnlyOneGreenGreen())) {
|
|
||||||
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), g.getConfig().isOnlyOneGreenGreen()) && guessedColor == Color.GREEN) {
|
|
||||||
// There is still cards i should find
|
|
||||||
if (!g.getPhase().isSuddenDeath() && !g.getCurrentHint().canStillGuess(g.getCurrentGuessCount(), g.getConfig().isOneMoreGuess())) {
|
|
||||||
if(!g.getConfig().isOneMoreGuess() && g.getCurrentHint().canStillGuess(g.getCurrentGuessCount(), true)) {
|
|
||||||
// Having one more guess could make us continue
|
|
||||||
setPhase(g, Phase.waitingForOneMore(player));
|
|
||||||
} else {
|
|
||||||
// We're done with this round of guesses anyway
|
|
||||||
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() && !g.getPhase().isWaitingForOneMore())
|
|
||||||
// It means this pick ended the guessing turn
|
|
||||||
newGameEvent(g, GameEvent.newEndGuessingEvent(false, player));
|
|
||||||
return guessedColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void endGuessing(Grid g, char player) throws IllegalGameActionException {
|
|
||||||
if (!g.getPhase().canPlay() || (!g.getPhase().isGuessing(player) && !g.getPhase().isWaitingForOneMore())) {
|
|
||||||
throw new IllegalGameActionException(g, "This action is not allowed now");
|
|
||||||
}
|
|
||||||
if (!g.getConfig().isEndGuessing() && !g.getPhase().isWaitingForOneMore()) {
|
|
||||||
throw new IllegalGameActionException(g, "Cannot end guessing in this game");
|
|
||||||
}
|
|
||||||
endGuessingRound(g, player);
|
|
||||||
newGameEvent(g, GameEvent.newEndGuessingEvent(true, player));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void enterSuddenDeath(Grid g) throws IllegalGameActionException {
|
|
||||||
if(!g.getPhase().isNeedMoreHints()) {
|
|
||||||
throw new IllegalGameActionException(g, "You can only enter sudden death when being out of hints");
|
|
||||||
}
|
|
||||||
enterSuddenDeathInternal(g);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UPDATE GAME CONFIG
|
|
||||||
*/
|
|
||||||
public void unlock(Grid g, MutableOption opt, Object o) throws IllegalGameActionException {
|
|
||||||
switch(opt) {
|
|
||||||
case MutableOption.MAX_HINT:
|
|
||||||
unlockHintCount(g, (Integer)o);
|
|
||||||
break;
|
|
||||||
case MutableOption.AVAILABLE_HINT_WORD_COUNT:
|
|
||||||
unlockNewWordCounts(g, (Set<Integer>)o);
|
|
||||||
break;
|
|
||||||
case MutableOption.ONE_MORE_GUESS:
|
|
||||||
unlockOneMoreGuess(g);
|
|
||||||
break;
|
|
||||||
case MutableOption.HINT_MAX_LENGTH:
|
|
||||||
unlockHintMaxLength(g, (Integer)o);
|
|
||||||
break;
|
|
||||||
case MutableOption.END_GUESSING:
|
|
||||||
unlockEndGuessing(g);
|
|
||||||
break;
|
|
||||||
case MutableOption.ONLY_ONE_GREEN_GREEN:
|
|
||||||
unlockOnlyOneGreenGreen(g);
|
|
||||||
break;
|
|
||||||
case MutableOption.SUDDEN_DEATH:
|
|
||||||
unlockSuddenDeath(g);
|
|
||||||
break;
|
|
||||||
case MutableOption.HINTS_IN_A_ROW:
|
|
||||||
unlockHintsInARow(g);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Unknown MutableOption to edit");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public void unlockHintCount(Grid g, int newHintCount) throws IllegalGameActionException {
|
|
||||||
if(newHintCount > g.getConfig().getMaxMaxHint()) {
|
|
||||||
throw new IllegalGameActionException(g, "Illegal hint count");
|
|
||||||
}
|
|
||||||
if(newHintCount <= g.getConfig().getMaxHint()) {
|
|
||||||
throw new IllegalGameActionException(g, "Cannot lower hint count");
|
|
||||||
}
|
|
||||||
changeConfig(g, MutableOption.MAX_HINT, newHintCount);
|
|
||||||
if(g.getPhase().isNeedMoreHints() || g.getPhase().isNeedMoreHintsOrSuddenDeath()) {
|
|
||||||
setPhase(g, Phase.hinting(g.getPhase().getHinting()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockNewWordCounts(Grid g, Set<Integer> newAvailableHintWordCounts) throws IllegalGameActionException {
|
|
||||||
if (!newAvailableHintWordCounts.containsAll(g.getConfig().getAvailableHintWordCount())
|
|
||||||
|| g.getConfig().getAvailableHintWordCount().containsAll(newAvailableHintWordCounts)) {
|
|
||||||
// i.e. If given data is not a strict superset
|
|
||||||
throw new IllegalGameActionException(g, "Can only add hint word options");
|
|
||||||
}
|
|
||||||
changeConfig(g, MutableOption.AVAILABLE_HINT_WORD_COUNT, newAvailableHintWordCounts);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockOneMoreGuess(Grid g) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().isOneMoreGuess())
|
|
||||||
throw new IllegalGameActionException(g, "Cannot unlock oneMoreGuess as it is already unlocked");
|
|
||||||
changeConfig(g, MutableOption.ONE_MORE_GUESS, true);
|
|
||||||
if(g.getPhase().isWaitingForOneMore()) {
|
|
||||||
setPhase(g, Phase.guessing(g.getPhase().getGuessing()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockHintMaxLength(Grid g, int newHintMaxLength) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().getHintMaxLength() >= newHintMaxLength) {
|
|
||||||
throw new IllegalGameActionException(g, "Can only raise max hint length");
|
|
||||||
}
|
|
||||||
changeConfig(g, MutableOption.HINT_MAX_LENGTH, newHintMaxLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockEndGuessing(Grid g) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().isEndGuessing())
|
|
||||||
throw new IllegalGameActionException(g, "Cannot unlock endGuessing as it is already unlocked");
|
|
||||||
changeConfig(g, MutableOption.END_GUESSING, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockOnlyOneGreenGreen(Grid g) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().isOnlyOneGreenGreen())
|
|
||||||
throw new IllegalGameActionException(g, "Cannot unlock onlyOneGreenGreen as it is already unlocked");
|
|
||||||
changeConfig(g, MutableOption.ONLY_ONE_GREEN_GREEN, true);
|
|
||||||
if(g.getPhase() == Phase.WIN_IF_GREEN_GREEN) {
|
|
||||||
endGame(g, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockSuddenDeath(Grid g) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().isSuddenDeath())
|
|
||||||
throw new IllegalGameActionException(g, "Cannot unlock suddenDeath as it is already unlocked");
|
|
||||||
changeConfig(g, MutableOption.SUDDEN_DEATH,true);
|
|
||||||
if (g.getPhase().isNeedMoreHintsOrSuddenDeath()) {
|
|
||||||
setPhase(g, Phase.needMoreHints(g.getPhase().getHinting()));
|
|
||||||
} else if (g.getPhase() == Phase.NEED_SUDDEN_DEATH) {
|
|
||||||
// We're entering sudden death
|
|
||||||
enterSuddenDeathInternal(g);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockHintsInARow(Grid g) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().isHintsInARow())
|
|
||||||
throw new IllegalGameActionException(g, "Cannot unlock hintsInARow as it is already unlocked");
|
|
||||||
changeConfig(g, MutableOption.HINTS_IN_A_ROW,true);
|
|
||||||
if ((g.getPhase() == Phase.HINTING_A || g.getPhase() == Phase.HINTING_B)
|
|
||||||
&& !allGreenRevealed(g, 'A', g.getConfig().isOnlyOneGreenGreen())
|
|
||||||
&& !allGreenRevealed(g, 'B', g.getConfig().isOnlyOneGreenGreen())) {
|
|
||||||
setPhase(g, Phase.HINTING_BOTH);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,425 @@
|
|||||||
|
package com.bernard.nodecames.game;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
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.GridConfig;
|
||||||
|
import com.bernard.nodecames.model.GridConfig.CardColors;
|
||||||
|
import com.bernard.nodecames.model.GridConfig.MutableOption;
|
||||||
|
import com.bernard.nodecames.model.Hint;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GridManager {
|
||||||
|
|
||||||
|
//TODO Remove static, make something more Spring Boot-y
|
||||||
|
private static Map<UUID, Grid> games = new HashMap<>();
|
||||||
|
private static Map<UUID, Random> randoms = new HashMap<>();
|
||||||
|
|
||||||
|
@Setter
|
||||||
|
private WebSocketController wsc = null;
|
||||||
|
@Setter
|
||||||
|
private GameManager wm = null;
|
||||||
|
|
||||||
|
private final DictionnariesService dictionnaries;
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(GridManager.class);
|
||||||
|
|
||||||
|
private void newGameEvent(Grid g, GameEvent ge) {
|
||||||
|
g.newGameEvent(ge);
|
||||||
|
log.debug("New game event : {}", ge);
|
||||||
|
wsc.onNewEvent(g, ge);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPhase(Grid g, Phase phase) {
|
||||||
|
g.setPhase(phase);
|
||||||
|
log.debug("New phase : {}", phase.name());
|
||||||
|
if(!phase.isAnyoneGuessing()) {
|
||||||
|
g.setCurrentHint(null);
|
||||||
|
}
|
||||||
|
wsc.onNewPhase(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changeConfig(Grid g, MutableOption cc, Object data) {
|
||||||
|
g.getConfig().set(cc, data);
|
||||||
|
log.debug("Changed config option: {} -> new value: {}", cc.name(), data);
|
||||||
|
wsc.onConfigChange(g, cc, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private char other(char player) {
|
||||||
|
return (player=='A')?'B':'A';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns true iff all green cards of this player have been revealed either side
|
||||||
|
if onlyoneGreenGreen is false, checks only the side of this player
|
||||||
|
*/
|
||||||
|
private boolean allGreenRevealed(Grid g, char player, boolean onlyOneGreenGreen) {
|
||||||
|
for(Card c : g.getCards()) {
|
||||||
|
if ((c.getColor(player) == Color.GREEN && !c.isColorPublic(player)) &&
|
||||||
|
(!onlyOneGreenGreen || (c.getColor(player) == Color.GREEN && !c.isColorPublic(player))))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true iff the game would have been won if the rule onlyOneGreenGreen was true
|
||||||
|
*/
|
||||||
|
private boolean onlyOneGreenGreenWouldWin(Grid g) {
|
||||||
|
return !g.getConfig().isOnlyOneGreenGreen() && allGreenRevealed(g, 'A', true) && allGreenRevealed(g, 'B', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Grid findGrid(String gridId) {
|
||||||
|
return games.get(UUID.fromString(gridId));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Grid newGrid(GridConfig gc, Random r) {
|
||||||
|
UUID uuid = UUID.randomUUID();
|
||||||
|
randoms.put(uuid, r);
|
||||||
|
String[] words = dictionnaries.randomWords(gc.getWordCount(), r);
|
||||||
|
List<Card> cards = new ArrayList<>(gc.getWordCount());
|
||||||
|
List<CardColors> origDistrib = gc.getWordDistribution();
|
||||||
|
List<CardColors> distrib;
|
||||||
|
if(!gc.isStructuredGrid()) {
|
||||||
|
List<Color> bColors = origDistrib.stream().map(cc -> cc.getFaceB()).collect(Collectors.toList());
|
||||||
|
Collections.shuffle(bColors, r);
|
||||||
|
distrib = IntStream.range(0, gc.getWordCount())
|
||||||
|
.mapToObj(i -> CardColors.of(origDistrib.get(i).getFaceA(), bColors.get(i)))
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
distrib = origDistrib;
|
||||||
|
}
|
||||||
|
for (int j = 0; j < distrib.size(); j++) {
|
||||||
|
cards.add(new Card(words[j], distrib.get(j).getFaceA(), distrib.get(j).getFaceB()));
|
||||||
|
}
|
||||||
|
Collections.shuffle(cards, r);
|
||||||
|
|
||||||
|
Phase startPhase;
|
||||||
|
if(gc.isAnyoneStarts()) {
|
||||||
|
startPhase = Phase.HINTING_BOTH;
|
||||||
|
} else {
|
||||||
|
if(r.nextBoolean())
|
||||||
|
startPhase = Phase.hinting('A');
|
||||||
|
else
|
||||||
|
startPhase = Phase.hinting('B');
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid g = new Grid(uuid,
|
||||||
|
(Card[])cards.toArray(new Card[cards.size()]),
|
||||||
|
new ArrayList<>(),
|
||||||
|
null,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
startPhase,
|
||||||
|
Optional.empty(),
|
||||||
|
gc);
|
||||||
|
|
||||||
|
games.put(uuid, g);
|
||||||
|
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**********************************
|
||||||
|
* GAME ACTIONS *
|
||||||
|
**********************************/
|
||||||
|
private void endGame(Grid g, boolean win) {
|
||||||
|
g.setWon(Optional.of(win));
|
||||||
|
setPhase(g, Phase.GAME_ENDED);
|
||||||
|
newGameEvent(g, GameEvent.newGameEndEvent(win));
|
||||||
|
if(win) {
|
||||||
|
wm.onGameWon(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void proposeHint(Grid g, char player, String hint, int wordCount) throws IllegalGameActionException {
|
||||||
|
if (!g.getPhase().canPlay() && !g.getPhase().isHinting(player)) {
|
||||||
|
throw new IllegalGameActionException(g, "This action is not allowed now");
|
||||||
|
}
|
||||||
|
if (!g.getConfig().getAvailableHintWordCount().contains(wordCount)) {
|
||||||
|
throw new IllegalGameActionException(g, "Unavailable word count");
|
||||||
|
}
|
||||||
|
if (hint.length() > g.getConfig().getHintMaxLength()) {
|
||||||
|
throw new IllegalGameActionException(g, "Hint is too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
Hint h = new Hint(hint, wordCount);
|
||||||
|
g.incrementHintCount();
|
||||||
|
g.setCurrentHint(h);
|
||||||
|
g.setCurrentGuessCount(0);
|
||||||
|
newGameEvent(g, GameEvent.newHintEvent(h, player));
|
||||||
|
setPhase(g, Phase.guessing(other(player)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns 'A', 'B', '_' depending on who should be the next ones to hint if we enter a hinting phase
|
||||||
|
*/
|
||||||
|
private char whoWouldHintNext(Grid g, char player) {
|
||||||
|
if(allGreenRevealed(g, player, g.getConfig().isOnlyOneGreenGreen())) {
|
||||||
|
// I can't hint anymore
|
||||||
|
return other(player);
|
||||||
|
} else {
|
||||||
|
if(g.getConfig().isHintsInARow() && !allGreenRevealed(g, other(player), g.getConfig().isOnlyOneGreenGreen())) {
|
||||||
|
return '_';
|
||||||
|
} else {
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enters the sudden death phase
|
||||||
|
*/
|
||||||
|
private void enterSuddenDeathInternal(Grid g) {
|
||||||
|
newGameEvent(g, GameEvent.newSuddenDeath());
|
||||||
|
if(allGreenRevealed(g, 'A', g.getConfig().isOnlyOneGreenGreen())) {
|
||||||
|
// If all 'A''s greens are revealed, A is the only one to guess
|
||||||
|
setPhase(g, Phase.suddenDeathOnly('A'));
|
||||||
|
} else {
|
||||||
|
// Else, we are both guessing
|
||||||
|
setPhase(g, Phase.SUDDEN_DEATH_BOTH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
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() >= g.getConfig().getMaxHint()) {
|
||||||
|
if(g.getUsedHints() >= g.getConfig().getMaxMaxHint()) {
|
||||||
|
if(!g.getConfig().isSuddenDeath()) {
|
||||||
|
// We're entering sudden death
|
||||||
|
enterSuddenDeathInternal(g);
|
||||||
|
} else {
|
||||||
|
// Only sudden death can get us out
|
||||||
|
setPhase(g, Phase.NEED_SUDDEN_DEATH);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We are waiting for more hints
|
||||||
|
if(g.getConfig().isSuddenDeath()) {
|
||||||
|
setPhase(g, Phase.needMoreHints(whoWouldHintNext(g, player)));
|
||||||
|
} else {
|
||||||
|
setPhase(g, Phase.needMoreHintsOrSuddenDeath(whoWouldHintNext(g, player)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Next regular hinting round
|
||||||
|
setPhase(g, Phase.hinting(whoWouldHintNext(g, player)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Card.Color pointCard(Grid g, char player, int cardIndex) throws IllegalGameActionException {
|
||||||
|
if (!g.getPhase().canPlay() || !g.getPhase().isGuessing(player)) {
|
||||||
|
throw new IllegalGameActionException(g, "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];
|
||||||
|
|
||||||
|
if(c.isColorPublic(other(player)) || (g.getConfig().isOnlyOneGreenGreen() && c.getColor(player) == Card.Color.GREEN && c.isColorPublic(player)))
|
||||||
|
throw new IllegalGameActionException(g, "Card has already been revealed");
|
||||||
|
c.setColorPublic(other(player), true);
|
||||||
|
Card.Color guessedColor = c.getColor(other(player));
|
||||||
|
|
||||||
|
newGameEvent(g, GameEvent.newGuessEvent(c, player));
|
||||||
|
g.incrementCurrentGuessCount();
|
||||||
|
if(guessedColor == Color.BLACK){
|
||||||
|
if(g.getConfig().isOnlyOneGreenGreenPreventDeath() && onlyOneGreenGreenWouldWin(g)) {
|
||||||
|
setPhase(g, Phase.WIN_IF_GREEN_GREEN);
|
||||||
|
} else {
|
||||||
|
endGame(g, false);
|
||||||
|
}
|
||||||
|
} else if (g.getPhase().isSuddenDeath() && guessedColor == Color.WHITE) {
|
||||||
|
if(g.getConfig().isOnlyOneGreenGreenPreventDeath() && onlyOneGreenGreenWouldWin(g)) {
|
||||||
|
setPhase(g, Phase.WIN_IF_GREEN_GREEN);
|
||||||
|
} else {
|
||||||
|
endGame(g, false);
|
||||||
|
}
|
||||||
|
} else if (allGreenRevealed(g, other(player), g.getConfig().isOnlyOneGreenGreen()) && guessedColor == Color.GREEN) {
|
||||||
|
// i.e. i made my last guess
|
||||||
|
if (allGreenRevealed(g, player, g.getConfig().isOnlyOneGreenGreen())) {
|
||||||
|
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), g.getConfig().isOnlyOneGreenGreen()) && guessedColor == Color.GREEN) {
|
||||||
|
// There is still cards i should find
|
||||||
|
if (!g.getPhase().isSuddenDeath() && !g.getCurrentHint().canStillGuess(g.getCurrentGuessCount(), g.getConfig().isOneMoreGuess())) {
|
||||||
|
if(!g.getConfig().isOneMoreGuess() && g.getCurrentHint().canStillGuess(g.getCurrentGuessCount(), true)) {
|
||||||
|
// Having one more guess could make us continue
|
||||||
|
setPhase(g, Phase.waitingForOneMore(player));
|
||||||
|
} else {
|
||||||
|
// We're done with this round of guesses anyway
|
||||||
|
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() && !g.getPhase().isWaitingForOneMore())
|
||||||
|
// It means this pick ended the guessing turn
|
||||||
|
newGameEvent(g, GameEvent.newEndGuessingEvent(false, player));
|
||||||
|
return guessedColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endGuessing(Grid g, char player) throws IllegalGameActionException {
|
||||||
|
if (!g.getPhase().canPlay() || (!g.getPhase().isGuessing(player) && !g.getPhase().isWaitingForOneMore())) {
|
||||||
|
throw new IllegalGameActionException(g, "This action is not allowed now");
|
||||||
|
}
|
||||||
|
if (!g.getConfig().isEndGuessing() && !g.getPhase().isWaitingForOneMore()) {
|
||||||
|
throw new IllegalGameActionException(g, "Cannot end guessing in this game");
|
||||||
|
}
|
||||||
|
endGuessingRound(g, player);
|
||||||
|
newGameEvent(g, GameEvent.newEndGuessingEvent(true, player));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enterSuddenDeath(Grid g) throws IllegalGameActionException {
|
||||||
|
if(!g.getPhase().isNeedMoreHints()) {
|
||||||
|
throw new IllegalGameActionException(g, "You can only enter sudden death when being out of hints");
|
||||||
|
}
|
||||||
|
enterSuddenDeathInternal(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UPDATE GAME CONFIG
|
||||||
|
*/
|
||||||
|
public void unlock(Grid g, MutableOption opt, Object o) throws IllegalGameActionException {
|
||||||
|
switch(opt) {
|
||||||
|
case MutableOption.MAX_HINT:
|
||||||
|
unlockHintCount(g, (Integer)o);
|
||||||
|
break;
|
||||||
|
case MutableOption.AVAILABLE_HINT_WORD_COUNT:
|
||||||
|
unlockNewWordCounts(g, (Set<Integer>)o);
|
||||||
|
break;
|
||||||
|
case MutableOption.ONE_MORE_GUESS:
|
||||||
|
unlockOneMoreGuess(g);
|
||||||
|
break;
|
||||||
|
case MutableOption.HINT_MAX_LENGTH:
|
||||||
|
unlockHintMaxLength(g, (Integer)o);
|
||||||
|
break;
|
||||||
|
case MutableOption.END_GUESSING:
|
||||||
|
unlockEndGuessing(g);
|
||||||
|
break;
|
||||||
|
case MutableOption.ONLY_ONE_GREEN_GREEN:
|
||||||
|
unlockOnlyOneGreenGreen(g);
|
||||||
|
break;
|
||||||
|
case MutableOption.SUDDEN_DEATH:
|
||||||
|
unlockSuddenDeath(g);
|
||||||
|
break;
|
||||||
|
case MutableOption.HINTS_IN_A_ROW:
|
||||||
|
unlockHintsInARow(g);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unknown MutableOption to edit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void unlockHintCount(Grid g, int newHintCount) throws IllegalGameActionException {
|
||||||
|
if(newHintCount > g.getConfig().getMaxMaxHint()) {
|
||||||
|
throw new IllegalGameActionException(g, "Illegal hint count");
|
||||||
|
}
|
||||||
|
if(newHintCount <= g.getConfig().getMaxHint()) {
|
||||||
|
throw new IllegalGameActionException(g, "Cannot lower hint count");
|
||||||
|
}
|
||||||
|
changeConfig(g, MutableOption.MAX_HINT, newHintCount);
|
||||||
|
if(g.getPhase().isNeedMoreHints() || g.getPhase().isNeedMoreHintsOrSuddenDeath()) {
|
||||||
|
setPhase(g, Phase.hinting(g.getPhase().getHinting()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockNewWordCounts(Grid g, Set<Integer> newAvailableHintWordCounts) throws IllegalGameActionException {
|
||||||
|
if (!newAvailableHintWordCounts.containsAll(g.getConfig().getAvailableHintWordCount())
|
||||||
|
|| g.getConfig().getAvailableHintWordCount().containsAll(newAvailableHintWordCounts)) {
|
||||||
|
// i.e. If given data is not a strict superset
|
||||||
|
throw new IllegalGameActionException(g, "Can only add hint word options");
|
||||||
|
}
|
||||||
|
changeConfig(g, MutableOption.AVAILABLE_HINT_WORD_COUNT, newAvailableHintWordCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockOneMoreGuess(Grid g) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().isOneMoreGuess())
|
||||||
|
throw new IllegalGameActionException(g, "Cannot unlock oneMoreGuess as it is already unlocked");
|
||||||
|
changeConfig(g, MutableOption.ONE_MORE_GUESS, true);
|
||||||
|
if(g.getPhase().isWaitingForOneMore()) {
|
||||||
|
setPhase(g, Phase.guessing(g.getPhase().getGuessing()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockHintMaxLength(Grid g, int newHintMaxLength) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().getHintMaxLength() >= newHintMaxLength) {
|
||||||
|
throw new IllegalGameActionException(g, "Can only raise max hint length");
|
||||||
|
}
|
||||||
|
changeConfig(g, MutableOption.HINT_MAX_LENGTH, newHintMaxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockEndGuessing(Grid g) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().isEndGuessing())
|
||||||
|
throw new IllegalGameActionException(g, "Cannot unlock endGuessing as it is already unlocked");
|
||||||
|
changeConfig(g, MutableOption.END_GUESSING, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockOnlyOneGreenGreen(Grid g) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().isOnlyOneGreenGreen())
|
||||||
|
throw new IllegalGameActionException(g, "Cannot unlock onlyOneGreenGreen as it is already unlocked");
|
||||||
|
changeConfig(g, MutableOption.ONLY_ONE_GREEN_GREEN, true);
|
||||||
|
if(g.getPhase() == Phase.WIN_IF_GREEN_GREEN) {
|
||||||
|
endGame(g, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockSuddenDeath(Grid g) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().isSuddenDeath())
|
||||||
|
throw new IllegalGameActionException(g, "Cannot unlock suddenDeath as it is already unlocked");
|
||||||
|
changeConfig(g, MutableOption.SUDDEN_DEATH,true);
|
||||||
|
if (g.getPhase().isNeedMoreHintsOrSuddenDeath()) {
|
||||||
|
setPhase(g, Phase.needMoreHints(g.getPhase().getHinting()));
|
||||||
|
} else if (g.getPhase() == Phase.NEED_SUDDEN_DEATH) {
|
||||||
|
// We're entering sudden death
|
||||||
|
enterSuddenDeathInternal(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockHintsInARow(Grid g) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().isHintsInARow())
|
||||||
|
throw new IllegalGameActionException(g, "Cannot unlock hintsInARow as it is already unlocked");
|
||||||
|
changeConfig(g, MutableOption.HINTS_IN_A_ROW,true);
|
||||||
|
if ((g.getPhase() == Phase.HINTING_A || g.getPhase() == Phase.HINTING_B)
|
||||||
|
&& !allGreenRevealed(g, 'A', g.getConfig().isOnlyOneGreenGreen())
|
||||||
|
&& !allGreenRevealed(g, 'B', g.getConfig().isOnlyOneGreenGreen())) {
|
||||||
|
setPhase(g, Phase.HINTING_BOTH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.bernard.nodecames.model;
|
package com.bernard.nodecames.model;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import java.util.Map;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
@@ -8,7 +10,6 @@ import lombok.Getter;
|
|||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@Entity
|
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
@@ -17,7 +18,28 @@ public class Game {
|
|||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
@GeneratedValue
|
||||||
private Long id;
|
private UUID id;
|
||||||
|
|
||||||
String name;
|
String name;
|
||||||
|
|
||||||
|
Map<String, World> worlds;
|
||||||
|
|
||||||
|
public static final Map<String, GridConfig> DEFAULT_WORLDS_CONFIG = Map.ofEntries(
|
||||||
|
Map.entry("1-1", GridConfig.zeroWithSize(GridConfig.CUSTOM6)),
|
||||||
|
Map.entry("1-2", GridConfig.zeroWithSize(GridConfig.CUSTOM9)),
|
||||||
|
Map.entry("1-3", GridConfig.zeroWithSize(GridConfig.CUSTOM9)),
|
||||||
|
Map.entry("2-1", GridConfig.zeroWithSize(GridConfig.CUSTOM12)),
|
||||||
|
Map.entry("2-2", GridConfig.zeroWithSize(GridConfig.CUSTOM16)),
|
||||||
|
Map.entry("2-3", GridConfig.zeroWithSize(GridConfig.CUSTOM16)),
|
||||||
|
Map.entry("3-1", GridConfig.zeroWithSize(GridConfig.CUSTOM20)),
|
||||||
|
Map.entry("3-2", GridConfig.zeroWithSize(GridConfig.CLASSICAL25)),
|
||||||
|
Map.entry("3-3", GridConfig.zeroWithSize(GridConfig.CLASSICAL25)),
|
||||||
|
Map.entry("4-1", GridConfig.zeroWithSize(GridConfig.CUSTOM30)),
|
||||||
|
Map.entry("4-2", GridConfig.zeroWithSize(GridConfig.CUSTOM36)),
|
||||||
|
Map.entry("4-3", GridConfig.zeroWithSize(GridConfig.CUSTOM36))
|
||||||
|
);
|
||||||
|
|
||||||
|
public World getWorld(String world) {
|
||||||
|
return this.getWorlds().get(world);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.bernard.nodecames.model;
|
|||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import jakarta.persistence.Id;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
@@ -14,7 +16,8 @@ import lombok.Setter;
|
|||||||
@Setter
|
@Setter
|
||||||
public class Grid {
|
public class Grid {
|
||||||
|
|
||||||
String id;
|
@Id
|
||||||
|
UUID id;
|
||||||
|
|
||||||
Card[] cards;
|
Card[] cards;
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
package com.bernard.nodecames.model;
|
package com.bernard.nodecames.model;
|
||||||
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.BB;
|
import static com.bernard.nodecames.model.GridConfig.CardColors.makeDistribution;
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.BG;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.BW;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.GB;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.GG;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.GW;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.WB;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.WG;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.WW;
|
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
@@ -25,9 +19,23 @@ import lombok.Value;
|
|||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
public class GridConfig {
|
public class GridConfig {
|
||||||
|
|
||||||
public static final List<CardColors> CLASSICAL25 = List.of(
|
public static final List<CardColors> CUSTOM6 =
|
||||||
BB,BW,WB,BG,GB,GG,GG,GG,WG,WG,WG,WG,WG,GW,GW,GW,GW,GW,WW,WW,WW,WW,WW,WW,WW
|
makeDistribution(0, 0, 2, 1, 1, 0);
|
||||||
);
|
public static final List<CardColors> CUSTOM9 =
|
||||||
|
makeDistribution(0, 1, 1, 2, 2, 1);
|
||||||
|
public static final List<CardColors> CUSTOM12 =
|
||||||
|
makeDistribution(1, 1, 1, 2, 3, 1);
|
||||||
|
public static final List<CardColors> CUSTOM16 =
|
||||||
|
makeDistribution(1, 1, 2, 2, 5, 1);
|
||||||
|
public static final List<CardColors> CUSTOM20 =
|
||||||
|
makeDistribution(1, 1, 4, 2, 5, 1);
|
||||||
|
public static final List<CardColors> CLASSICAL25 =
|
||||||
|
makeDistribution(1, 1, 5, 3, 7, 1);
|
||||||
|
public static final List<CardColors> CUSTOM30 =
|
||||||
|
makeDistribution(2, 2, 5, 3, 7, 1);
|
||||||
|
public static final List<CardColors> CUSTOM36 =
|
||||||
|
makeDistribution(2, 2, 6, 5, 9, 2);
|
||||||
|
|
||||||
public static final Set<Integer> BASE_HINTWORDCOUNT = Set.of(
|
public static final Set<Integer> BASE_HINTWORDCOUNT = Set.of(
|
||||||
0,1,2,3,4,5,6,7,8,9,-1
|
0,1,2,3,4,5,6,7,8,9,-1
|
||||||
);
|
);
|
||||||
@@ -43,6 +51,13 @@ public class GridConfig {
|
|||||||
3, false, false,
|
3, false, false,
|
||||||
false, false);
|
false, false);
|
||||||
|
|
||||||
|
public static final GridConfig zeroWithSize(List<CardColors> colors) {
|
||||||
|
return new GridConfig(
|
||||||
|
9, false, false, colors, true,
|
||||||
|
true, false, 1, Set.of(1), false,
|
||||||
|
3, false, false,
|
||||||
|
false, false);
|
||||||
|
}
|
||||||
|
|
||||||
// Immutable options
|
// Immutable options
|
||||||
// Maximum of maximom number of hints
|
// Maximum of maximom number of hints
|
||||||
@@ -137,6 +152,22 @@ public class GridConfig {
|
|||||||
public static final CardColors GB = CardColors.of(Color.GREEN, Color.BLACK);
|
public static final CardColors GB = CardColors.of(Color.GREEN, Color.BLACK);
|
||||||
public static final CardColors BG = CardColors.of(Color.BLACK, Color.GREEN);
|
public static final CardColors BG = CardColors.of(Color.BLACK, Color.GREEN);
|
||||||
public static final CardColors BB = CardColors.of(Color.BLACK, Color.BLACK);
|
public static final CardColors BB = CardColors.of(Color.BLACK, Color.BLACK);
|
||||||
|
|
||||||
|
|
||||||
|
public static final List<CardColors> makeDistribution(
|
||||||
|
int wbbwCount, int bggbCount, int wggwCount,
|
||||||
|
int ggCount, int wwCount, int bbCount
|
||||||
|
) {
|
||||||
|
int total = (wbbwCount + bggbCount + wggwCount)*2 + ggCount + wwCount + bbCount;
|
||||||
|
List<CardColors> out = new ArrayList<>(total);
|
||||||
|
for(int i=0;i<wbbwCount;i++) { out.add(WB); out.add(BW); }
|
||||||
|
for(int i=0;i<bggbCount;i++) { out.add(GB); out.add(BG); }
|
||||||
|
for(int i=0;i<wggwCount;i++) { out.add(GW); out.add(WG); }
|
||||||
|
for(int i=0;i<ggCount;i++) out.add(GG);
|
||||||
|
for(int i=0;i<wwCount;i++) out.add(WW);
|
||||||
|
for(int i=0;i<bbCount;i++) out.add(BB);
|
||||||
|
return Collections.unmodifiableList(out);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.bernard.nodecames.model;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
public class World {
|
||||||
|
|
||||||
|
Game game;
|
||||||
|
|
||||||
|
String name;
|
||||||
|
|
||||||
|
// Config used when resetting this world
|
||||||
|
GridConfig initialConfig;
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
Grid currentGrid;
|
||||||
|
|
||||||
|
boolean won;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
var socket = null
|
var socket = null
|
||||||
var player = '_'
|
var player = '_'
|
||||||
var roomId = '_'
|
|
||||||
var wsHeaders = {}
|
var wsHeaders = {}
|
||||||
var cards = []
|
var cards = []
|
||||||
var gc = {}
|
var gc = {}
|
||||||
@@ -11,13 +10,6 @@ var phase = {}
|
|||||||
|
|
||||||
// This variable is here to check for updates to availableHintWordCount, as updating it is a bit expensive
|
// This variable is here to check for updates to availableHintWordCount, as updating it is a bit expensive
|
||||||
var registeredAvailableHintWordCountInvalid = true
|
var registeredAvailableHintWordCountInvalid = true
|
||||||
function getRoomId() {
|
|
||||||
const urlRegex = /\/room\/([a-f0-9-]{36})\/grid$/
|
|
||||||
const res = urlRegex.exec(window.location.href)
|
|
||||||
if (res == null)
|
|
||||||
console.log("Could not understand room id from url")
|
|
||||||
return res[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Update html to match state
|
* Update html to match state
|
||||||
@@ -245,8 +237,7 @@ function initGame(data) {
|
|||||||
updatePhase()
|
updatePhase()
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPlayer(e) {
|
function selectPlayer(player) {
|
||||||
player = (e.target.id == "select-player-a") ? 'A' : 'B'
|
|
||||||
wsHeaders = {
|
wsHeaders = {
|
||||||
"room": roomId,
|
"room": roomId,
|
||||||
"player": player
|
"player": player
|
||||||
@@ -336,14 +327,10 @@ function onConfigChange(m) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initialize() {
|
function initialize() {
|
||||||
$('#select-player-a').on('click', selectPlayer)
|
|
||||||
$('#select-player-b').on('click', selectPlayer)
|
|
||||||
$('#submit-hint button').on('click', submitHint)
|
$('#submit-hint button').on('click', submitHint)
|
||||||
$('#end-guessing-button').on('click', endGuessing)
|
$('#end-guessing-button').on('click', endGuessing)
|
||||||
$('#enter-sudden-death-button').on('click', enterSuddenDeath)
|
$('#enter-sudden-death-button').on('click', enterSuddenDeath)
|
||||||
|
|
||||||
roomId = getRoomId()
|
|
||||||
|
|
||||||
$('#game-panel').hide()
|
$('#game-panel').hide()
|
||||||
$('#side-panel').hide()
|
$('#side-panel').hide()
|
||||||
}
|
}
|
||||||
@@ -9,11 +9,6 @@
|
|||||||
<script th:src="@{/js/grid.js}" type="text/javascript"></script>
|
<script th:src="@{/js/grid.js}" type="text/javascript"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="player-selector">
|
|
||||||
Which player are you ? <br/>
|
|
||||||
<button id="select-player-a">Player A</button>
|
|
||||||
<button id="select-player-b">Player B</button>
|
|
||||||
</div>
|
|
||||||
<main>
|
<main>
|
||||||
<div id="game-panel">
|
<div id="game-panel">
|
||||||
<ul id="cards-list">
|
<ul id="cards-list">
|
||||||
@@ -56,7 +51,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
|
/*<![CDATA[*/
|
||||||
|
var player = '[(${player})]';
|
||||||
|
var roomId = '[(${grid.id})]';
|
||||||
|
/*]]>*/
|
||||||
initialize()
|
initialize()
|
||||||
|
selectPlayer(player)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -9,6 +9,25 @@
|
|||||||
Vous voulez jouer à Node Cames ? Ben c'est pas encore prêt :/
|
Vous voulez jouer à Node Cames ? Ben c'est pas encore prêt :/
|
||||||
<br/>
|
<br/>
|
||||||
<a href="/create-room">(Bon, en vrai, vous pouvez créer une room en cliquant ici)</a>
|
<a href="/create-room">(Bon, en vrai, vous pouvez créer une room en cliquant ici)</a>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<form method="GET" th:action="@{/create-game}">
|
||||||
|
<label for="create-game-name">Name : </label>
|
||||||
|
<input type="text" id="create-game-name" name="name"/>
|
||||||
|
<input type="submit" value="Create game"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Currently running games:
|
||||||
|
<ul>
|
||||||
|
<li th:each="game : ${games}">
|
||||||
|
<a th:href="@{/game/{gid}/map(gid=${game.id})}" th:text="${game.name}"></a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<div th:replace="~{html-head}"/>
|
||||||
|
<link rel="stylesheet" th:href="@{/css/grid.css}"/>
|
||||||
|
<script th:src="@{/js/jquery-4.0.0.min.js}" type="text/javascript"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js" integrity="sha512-1QvjE7BtotQjkq8PxLeF6P46gEpBRXuskzIVgjFpekzFVF4yjRgrQvTG1MTOJ3yQgvTteKAcO7DSZI92+u/yZw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js" integrity="sha512-iKDtgDyTHjAitUDdLljGhenhPwrbBfqTKWO1mkhSFH3A7blITC9MhYon6SjnMhp4o0rADGw9yAC6EW4t5a4K3g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script th:src="@{/js/grid.js}" type="text/javascript"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<ul>
|
||||||
|
<li th:each="world : ${worlds}">
|
||||||
|
<a th:href="@{/game/{gid}/grid/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">World <span th:text="${world.name}"/></a>
|
||||||
|
<a th:href="@{/game/{gid}/reset/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">reset</a>
|
||||||
|
<span th:if="${world.won}">Gagné !</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<div th:replace="~{html-head}"/>
|
||||||
|
<link rel="stylesheet" th:href="@{/css/grid.css}"/>
|
||||||
|
<script th:src="@{/js/jquery-4.0.0.min.js}" type="text/javascript"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js" integrity="sha512-1QvjE7BtotQjkq8PxLeF6P46gEpBRXuskzIVgjFpekzFVF4yjRgrQvTG1MTOJ3yQgvTteKAcO7DSZI92+u/yZw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js" integrity="sha512-iKDtgDyTHjAitUDdLljGhenhPwrbBfqTKWO1mkhSFH3A7blITC9MhYon6SjnMhp4o0rADGw9yAC6EW4t5a4K3g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script th:src="@{/js/grid.js}" type="text/javascript"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
Which player are you ? <br/>
|
||||||
|
<a th:href="@{{redirect}?player=A(redirect=${redirect})}">Player A</a>
|
||||||
|
<a th:href="@{{redirect}?player=B(redirect=${redirect})}">Player B</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user