Moved to Hibernate
This commit is contained in:
@@ -3,9 +3,12 @@ package com.bernard.nodecames;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.validation.BindingResult;
|
||||
@@ -18,14 +21,17 @@ import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.servlet.view.RedirectView;
|
||||
|
||||
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.GameManager;
|
||||
import com.bernard.nodecames.model.Game;
|
||||
import com.bernard.nodecames.model.Grid;
|
||||
import com.bernard.nodecames.model.GridConfig;
|
||||
import com.bernard.nodecames.model.GridConfig.MutableOption;
|
||||
import com.bernard.nodecames.model.World;
|
||||
import com.bernard.nodecames.repository.GameRepository;
|
||||
import com.bernard.nodecames.repository.GridRepository;
|
||||
import com.bernard.nodecames.repository.WorldRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -35,6 +41,9 @@ public class HttpController {
|
||||
|
||||
private final GridManager gm;
|
||||
private final GameManager wm;
|
||||
private final GridRepository gridRepository;
|
||||
private final GameRepository gRepository;
|
||||
private final WorldRepository wRepository;
|
||||
private final Random rand = new Random();
|
||||
|
||||
@GetMapping("/")
|
||||
@@ -45,33 +54,43 @@ public class HttpController {
|
||||
}
|
||||
|
||||
@GetMapping("/room/{id}/grid")
|
||||
public String grid(@PathVariable("id") String id, @RequestParam(value = "player", required = false) String player, Model m) {
|
||||
public Object 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);
|
||||
Optional<Grid> grid = gridRepository.findById(UUID.fromString(id));
|
||||
if(!grid.isPresent()) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
m.addAttribute("player", player);
|
||||
m.addAttribute("grid", grid);
|
||||
m.addAttribute("grid", grid.get());
|
||||
return "grid";
|
||||
}
|
||||
|
||||
@GetMapping("/room/{id}/config")
|
||||
public String gridConfig(@PathVariable("id") String id, Model model) {
|
||||
GridConfig gc = gm.findGrid(id).getConfig();
|
||||
public Object gridConfig(@PathVariable("id") String id, Model model) {
|
||||
Optional<Grid> grid = gridRepository.findById(UUID.fromString(id));
|
||||
if(!grid.isPresent()) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
GridConfig gc = grid.get().getConfig();
|
||||
model.addAttribute("gridConfig", GridConfigEdit.of(gc));
|
||||
return "grid-config";
|
||||
}
|
||||
@PostMapping("/room/{id}/config")
|
||||
public String setGridConfig(@PathVariable("id") String id,
|
||||
public Object setGridConfig(@PathVariable("id") String id,
|
||||
@ModelAttribute("gridConfig") GridConfigEdit gce,
|
||||
BindingResult br,
|
||||
Model model
|
||||
) {
|
||||
Grid g = gm.findGrid(id);
|
||||
for(Entry<MutableOption, Object> e : gce.getModifications(g.getConfig()).entrySet()) {
|
||||
Optional<Grid> grid = gridRepository.findById(UUID.fromString(id));
|
||||
if(!grid.isPresent()) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
for(Entry<MutableOption, Object> e : gce.getModifications(grid.get().getConfig()).entrySet()) {
|
||||
try {
|
||||
gm.unlock(g, e.getKey(), e.getValue());
|
||||
gm.unlock(grid.get(), e.getKey(), e.getValue());
|
||||
} catch (IllegalGameActionException ige) {
|
||||
br.addError(new ObjectError(e.getKey().name(), ige.getMessage()));
|
||||
}
|
||||
@@ -80,34 +99,34 @@ public class HttpController {
|
||||
return "grid-config";
|
||||
}
|
||||
|
||||
|
||||
|
||||
@GetMapping("/create-room")
|
||||
public RedirectView createRoom() {
|
||||
Random r = new Random(rand.nextLong());
|
||||
Grid g = gm.newGrid(GridConfig.ZERO, r);
|
||||
Grid g = gm.newGrid(null, GridConfig.ZERO, r);
|
||||
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");
|
||||
Game g = wm.createGame(name);
|
||||
return new RedirectView("/game/"+g.getId().toString()+"/map");
|
||||
}
|
||||
|
||||
@GetMapping("/game/{id}/map")
|
||||
public String getMap(@PathVariable("id") String id, @RequestParam(value = "player", required = false) String player, Model m) {
|
||||
public Object 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()
|
||||
Optional<Game> g = gRepository.findById(UUID.fromString(id));
|
||||
if(!g.isPresent()) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
List<World> worlds = wm.allWorlds(g.get())
|
||||
.stream()
|
||||
.map(Entry::getValue)
|
||||
.sorted(Comparator.comparing(World::getName))
|
||||
.toList();
|
||||
m.addAttribute("game", g);
|
||||
m.addAttribute("game", g.get());
|
||||
m.addAttribute("worlds", worlds);
|
||||
m.addAttribute("player", player);
|
||||
return "map";
|
||||
@@ -115,15 +134,29 @@ public class HttpController {
|
||||
|
||||
@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);
|
||||
Optional<Game> g = gRepository.findById(UUID.fromString(id));
|
||||
if(!g.isPresent()) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
Optional<World> w = wRepository.findByGameAndId_Name(g.get(), world);
|
||||
if(!w.isPresent()) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
Grid grid = wm.getGrid(g.get(), w.get());
|
||||
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);
|
||||
Optional<Game> g = gRepository.findById(UUID.fromString(id));
|
||||
if(!g.isPresent()) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
Optional<World> w = wRepository.findByGameAndId_Name(g.get(), world);
|
||||
if(!w.isPresent()) {
|
||||
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||
}
|
||||
Grid newGrid = wm.resetGrid(g.get(), w.get());
|
||||
return new RedirectView("/room/"+newGrid.getId()+"/grid" + "?player="+player);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,9 @@ package com.bernard.nodecames;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
|
||||
import tools.jackson.databind.node.JsonNodeFactory;
|
||||
|
||||
@SpringBootApplication
|
||||
public class NodecamesApplication {
|
||||
@@ -10,4 +13,9 @@ public class NodecamesApplication {
|
||||
SpringApplication.run(NodecamesApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public JsonNodeFactory theJsonNodeFactory() {
|
||||
return JsonNodeFactory.instance;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,28 +1,31 @@
|
||||
package com.bernard.nodecames.frontend;
|
||||
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import com.bernard.nodecames.model.Card;
|
||||
import com.bernard.nodecames.model.GameEvent;
|
||||
import com.bernard.nodecames.model.Grid;
|
||||
import com.bernard.nodecames.model.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.Grid.Phase;
|
||||
import com.bernard.nodecames.model.Hint;
|
||||
import com.bernard.nodecames.repository.CardRepository;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
import tools.jackson.databind.node.JsonNodeFactory;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class JsonDataService {
|
||||
|
||||
private static final JsonNodeFactory jsn = JsonNodeFactory.instance;
|
||||
private final JsonNodeFactory jsn;
|
||||
private final CardRepository cRepository;
|
||||
|
||||
public JsonNode cardData(Card c, char player) {
|
||||
ObjectNode cardNode = jsn.objectNode();
|
||||
@@ -41,10 +44,11 @@ public class JsonDataService {
|
||||
gcNode.set("structured-grid", jsn.booleanNode(gc.isStructuredGrid()));
|
||||
gcNode.set("anyone-starts", jsn.booleanNode(gc.isAnyoneStarts()));
|
||||
ArrayNode wordDistributionNode = jsn.arrayNode(gc.getWordDistribution().size());
|
||||
for(CardColors c : gc.getWordDistribution()) {
|
||||
for(Entry<CardColors, Integer> e : gc.getWordDistribution().entrySet()) {
|
||||
ObjectNode ccNode = jsn.objectNode();
|
||||
ccNode.set("face-a", jsn.stringNode(c.getFaceA().name()));
|
||||
ccNode.set("face-b", jsn.stringNode(c.getFaceB().name()));
|
||||
ccNode.set("face-a", jsn.stringNode(e.getKey().getFaceA().name()));
|
||||
ccNode.set("face-b", jsn.stringNode(e.getKey().getFaceB().name()));
|
||||
ccNode.set("count", jsn.numberNode(e.getValue()));
|
||||
wordDistributionNode.add(ccNode);
|
||||
}
|
||||
gcNode.set("word-distribution", wordDistributionNode);
|
||||
@@ -102,35 +106,8 @@ public class JsonDataService {
|
||||
return config;
|
||||
}
|
||||
|
||||
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 eventData(GameEvent ge) {
|
||||
return GameEvent.toJsonNode(ge);
|
||||
}
|
||||
|
||||
public JsonNode phaseData(Phase p) {
|
||||
@@ -148,18 +125,19 @@ public class JsonDataService {
|
||||
|
||||
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));
|
||||
Card[] cards = cRepository.findByGridOrderById_Index(g);
|
||||
ArrayNode cardsNode = jsn.arrayNode(cards.length);
|
||||
for (int i = 0; i < cards.length; i++)
|
||||
cardsNode.add(cardData(cards[i], player));
|
||||
|
||||
ArrayNode eventsNode = jsn.arrayNode(g.getGameEvents().size());
|
||||
for(GameEvent ge : g.getGameEvents())
|
||||
eventsNode.add(eventData(g, ge));
|
||||
eventsNode.add(eventData(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()));
|
||||
currentHintNode.set("word-count", jsn.numberNode(g.getCurrentHint().getWordCount()));
|
||||
}
|
||||
|
||||
ObjectNode out = jsn.objectNode();
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.bernard.nodecames.frontend;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
@@ -18,26 +20,29 @@ import com.bernard.nodecames.model.Card;
|
||||
import com.bernard.nodecames.model.GameEvent;
|
||||
import com.bernard.nodecames.model.Grid;
|
||||
import com.bernard.nodecames.model.GridConfig.MutableOption;
|
||||
import com.bernard.nodecames.repository.GridRepository;
|
||||
|
||||
import tools.jackson.databind.JsonNode;
|
||||
|
||||
@Controller
|
||||
public class WebSocketController {
|
||||
|
||||
private GridManager gm;
|
||||
private SimpMessagingTemplate template;
|
||||
private JsonDataService json;
|
||||
private final GridManager gm;
|
||||
private final SimpMessagingTemplate template;
|
||||
private final JsonDataService json;
|
||||
private final GridRepository gRepository;
|
||||
|
||||
public WebSocketController(GridManager gm, SimpMessagingTemplate template, JsonDataService json) {
|
||||
public WebSocketController(GridManager gm, SimpMessagingTemplate template, JsonDataService json, GridRepository gRepository) {
|
||||
this.gm = gm;
|
||||
this.template = template;
|
||||
this.json = json;
|
||||
this.gRepository = gRepository;
|
||||
gm.setWsc(this);
|
||||
}
|
||||
|
||||
public void onNewEvent(Grid g, GameEvent ge) {
|
||||
this.template.convertAndSend("/topic/new-event",
|
||||
json.eventData(g, ge)
|
||||
json.eventData(ge)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -54,17 +59,16 @@ public class WebSocketController {
|
||||
);
|
||||
}
|
||||
|
||||
public void publishCardUpdateToPlayer(Grid g, int cardIndex, char player) {
|
||||
Card c = g.getCards()[cardIndex];
|
||||
public void publishCardUpdateToPlayer(Grid g, Card c, char player) {
|
||||
this.template.convertAndSend(
|
||||
"/topic/update-card/" + Character.toLowerCase(player),
|
||||
json.cardData(c, player),
|
||||
Map.of("cardIndex", cardIndex)
|
||||
Map.of("cardIndex", c.getIndex())
|
||||
);
|
||||
}
|
||||
public void publishCardUpdate(Grid g, int cardIndex) {
|
||||
publishCardUpdateToPlayer(g, cardIndex, 'A');
|
||||
publishCardUpdateToPlayer(g, cardIndex, 'B');
|
||||
public void publishCardUpdate(Grid g, Card card) {
|
||||
publishCardUpdateToPlayer(g, card, 'A');
|
||||
publishCardUpdateToPlayer(g, card, 'B');
|
||||
}
|
||||
|
||||
public void publishError(char issuer, IllegalGameActionException e) {
|
||||
@@ -81,20 +85,25 @@ public class WebSocketController {
|
||||
else
|
||||
throw new IllegalArgumentException("Unknown player type");
|
||||
|
||||
Grid g = gm.findGrid(gridId);
|
||||
|
||||
return new ResponseEntity<>(json.gridData(g, player), HttpStatus.OK);
|
||||
Optional<Grid> g = gRepository.findById(UUID.fromString(gridId));
|
||||
if(g.isPresent())
|
||||
return new ResponseEntity<>(json.gridData(g.get(), player), HttpStatus.OK);
|
||||
else
|
||||
return new ResponseEntity<>("Could not find grid with given ID",HttpStatus.NOT_FOUND);
|
||||
}
|
||||
|
||||
@MessageMapping("submit-hint")
|
||||
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);
|
||||
|
||||
Optional<Grid> g = gRepository.findById(UUID.fromString(roomId));
|
||||
if(!g.isPresent())
|
||||
// 404
|
||||
return;
|
||||
|
||||
try {
|
||||
gm.proposeHint(g, player.charAt(0), hint, wordCount);
|
||||
gm.proposeHint(g.get(), player.charAt(0), hint, wordCount);
|
||||
} catch (IllegalGameActionException e) {
|
||||
publishError(player.charAt(0), e);
|
||||
e.printStackTrace();
|
||||
@@ -104,11 +113,15 @@ public class WebSocketController {
|
||||
@MessageMapping("point-card")
|
||||
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);
|
||||
|
||||
Optional<Grid> g = gRepository.findById(UUID.fromString(roomId));
|
||||
if(!g.isPresent())
|
||||
// 404
|
||||
return;
|
||||
|
||||
try {
|
||||
gm.pointCard(g, player.charAt(0), cardIndex);
|
||||
publishCardUpdate(g, cardIndex);
|
||||
gm.pointCard(g.get(), player.charAt(0), cardIndex);
|
||||
publishCardUpdate(g.get(), gm.getCard(g.get(), cardIndex));
|
||||
} catch (IllegalGameActionException e) {
|
||||
publishError(player.charAt(0), e);
|
||||
e.printStackTrace();
|
||||
@@ -117,9 +130,13 @@ public class WebSocketController {
|
||||
|
||||
@MessageMapping("end-guessing")
|
||||
public void endGuessing(@Header("room") String roomId, @Header("player") String player) {
|
||||
Grid g = gm.findGrid(roomId);
|
||||
Optional<Grid> g = gRepository.findById(UUID.fromString(roomId));
|
||||
if(!g.isPresent())
|
||||
// 404
|
||||
return;
|
||||
|
||||
try {
|
||||
gm.endGuessing(g, player.charAt(0));
|
||||
gm.endGuessing(g.get(), player.charAt(0));
|
||||
} catch (IllegalGameActionException e) {
|
||||
publishError(player.charAt(0), e);
|
||||
e.printStackTrace();
|
||||
@@ -128,9 +145,12 @@ public class WebSocketController {
|
||||
|
||||
@MessageMapping("enter-sudden-death")
|
||||
public void enterSuddenDeath(@Header("room") String roomId, @Header("player") String player) {
|
||||
Grid g = gm.findGrid(roomId);
|
||||
Optional<Grid> g = gRepository.findById(UUID.fromString(roomId));
|
||||
if(!g.isPresent())
|
||||
// 404
|
||||
return;
|
||||
try {
|
||||
gm.enterSuddenDeath(g);
|
||||
gm.enterSuddenDeath(g.get());
|
||||
} catch (IllegalGameActionException e) {
|
||||
publishError(player.charAt(0), e);
|
||||
e.printStackTrace();
|
||||
|
||||
@@ -3,9 +3,10 @@ package com.bernard.nodecames.game;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Random;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@@ -13,80 +14,77 @@ import com.bernard.nodecames.model.Game;
|
||||
import com.bernard.nodecames.model.Grid;
|
||||
import com.bernard.nodecames.model.GridConfig;
|
||||
import com.bernard.nodecames.model.World;
|
||||
import com.bernard.nodecames.repository.GameRepository;
|
||||
import com.bernard.nodecames.repository.WorldRepository;
|
||||
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
|
||||
@Service
|
||||
public class GameManager {
|
||||
|
||||
Map<UUID, Game> games = new HashMap<>();
|
||||
Map<UUID, Random> randoms = new HashMap<>();
|
||||
|
||||
// Reverse map allowing to pass grid events to the game
|
||||
Map<UUID, World> currentGrids = new HashMap<>();
|
||||
|
||||
private final GridManager gm;
|
||||
private final GameRepository gRepository;
|
||||
private final WorldRepository wRepository;
|
||||
|
||||
public GameManager(GridManager gm) {
|
||||
public GameManager(GridManager gm, GameRepository gRepository, WorldRepository wRepository) {
|
||||
this.gm = gm;
|
||||
this.gRepository = gRepository;
|
||||
this.wRepository = wRepository;
|
||||
gm.setWm(this);
|
||||
}
|
||||
|
||||
public UUID createGame(String name) {
|
||||
public Game createGame(String name) {
|
||||
UUID uuid = UUID.randomUUID();
|
||||
randoms.put(uuid, new Random());
|
||||
|
||||
Game g = new Game(uuid, name, null);
|
||||
Map<String, World> worlds = Game.DEFAULT_WORLDS_CONFIG
|
||||
.entrySet()
|
||||
.stream()
|
||||
.collect(Collectors.toMap(
|
||||
Map.Entry::getKey,
|
||||
e -> new World(g, e.getKey(), e.getValue(), null, false)
|
||||
));
|
||||
g.setWorlds(worlds);
|
||||
Game g = new Game(uuid, name);
|
||||
gRepository.save(g);
|
||||
for (Entry<String, GridConfig> wConfig : Game.DEFAULT_WORLDS_CONFIG.entrySet()) {
|
||||
World w = new World(g, wConfig.getKey(), wConfig.getValue(), null, false);
|
||||
wRepository.save(w);
|
||||
}
|
||||
|
||||
|
||||
games.put(uuid, g);
|
||||
|
||||
return uuid;
|
||||
return g;
|
||||
}
|
||||
|
||||
public Collection<Game> allGames() {
|
||||
return games.values();
|
||||
return gRepository.findAll();
|
||||
}
|
||||
|
||||
public Set<World> allWorlds(@NotNull Game g) {
|
||||
return wRepository.findByGame(g);
|
||||
}
|
||||
|
||||
private Grid newGrid(Game g, String world) {
|
||||
|
||||
World w = g.getWorld(world);
|
||||
private Grid newGrid(Game g, World w) {
|
||||
GridConfig gc = w.getInitialConfig();
|
||||
Grid grid = gm.newGrid(gc, new Random(randoms.get(g.getId()).nextLong()));
|
||||
Grid grid = gm.newGrid(w, gc, new Random());
|
||||
|
||||
w.setCurrentGrid(grid);
|
||||
wRepository.save(w);
|
||||
currentGrids.put(grid.getId(), w);
|
||||
|
||||
return grid;
|
||||
}
|
||||
|
||||
public Grid resetGrid(Game g, String world) {
|
||||
public Grid resetGrid(Game g, World world) {
|
||||
return newGrid(g, world);
|
||||
}
|
||||
|
||||
public Grid getGrid(Game g, String world) {
|
||||
World w = g.getWorld(world);
|
||||
public Grid getGrid(Game g, World w) {
|
||||
if(w.getCurrentGrid() != null) {
|
||||
return w.getCurrentGrid();
|
||||
} else {
|
||||
return newGrid(g, world);
|
||||
return newGrid(g, w);
|
||||
}
|
||||
}
|
||||
|
||||
public Game getGame(String id) {
|
||||
return games.get(UUID.fromString(id));
|
||||
}
|
||||
|
||||
public void onGameWon(Grid g) {
|
||||
World w = currentGrids.get(g.getId());
|
||||
if(w != null) {
|
||||
w.setWon(true);
|
||||
wRepository.save(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,7 @@ 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;
|
||||
@@ -18,15 +16,19 @@ 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.Card.Color;
|
||||
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 com.bernard.nodecames.model.World;
|
||||
import com.bernard.nodecames.repository.CardRepository;
|
||||
import com.bernard.nodecames.repository.GridRepository;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@@ -34,40 +36,67 @@ import lombok.Setter;
|
||||
@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 GridRepository gRepository;
|
||||
private final CardRepository cRepository;
|
||||
|
||||
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);
|
||||
g.newGameEvent(ge);
|
||||
gRepository.save(g);
|
||||
wsc.onNewEvent(g, ge);
|
||||
}
|
||||
|
||||
private void setPhase(Grid g, Phase phase) {
|
||||
g.setPhase(phase);
|
||||
log.debug("New phase : {}", phase.name());
|
||||
g.setPhase(phase);
|
||||
if(!phase.isAnyoneGuessing()) {
|
||||
g.setCurrentHint(null);
|
||||
}
|
||||
gRepository.save(g);
|
||||
wsc.onNewPhase(g);
|
||||
}
|
||||
private void setWon(Grid g, boolean won) {
|
||||
log.debug("Game won: {}", won);
|
||||
g.setWonOpt(Optional.of(won));
|
||||
gRepository.save(g);
|
||||
}
|
||||
|
||||
private void newHint(Grid g, Hint h) {
|
||||
log.debug("New hint: {}", h);
|
||||
g.incrementHintCount();
|
||||
g.setCurrentHint(h);
|
||||
g.setCurrentGuessCount(0);
|
||||
gRepository.save(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);
|
||||
g.getConfig().set(cc, data);
|
||||
gRepository.save(g);
|
||||
wsc.onConfigChange(g, cc, data);
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
public int getCardCount(Grid g) {
|
||||
return cRepository.countByGrid(g);
|
||||
}
|
||||
public Card[] getCards(Grid g) {
|
||||
return cRepository.findByGridOrderById_Index(g);
|
||||
}
|
||||
public Card getCard(Grid g, int index) {
|
||||
return cRepository.findByGridAndId_Index(g, index);
|
||||
}
|
||||
private char other(char player) {
|
||||
return (player=='A')?'B':'A';
|
||||
}
|
||||
@@ -77,7 +106,7 @@ public class GridManager {
|
||||
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()) {
|
||||
for(Card c : getCards(g)) {
|
||||
if ((c.getColor(player) == Color.GREEN && !c.isColorPublic(player)) &&
|
||||
(!onlyOneGreenGreen || (c.getColor(player) == Color.GREEN && !c.isColorPublic(player))))
|
||||
return false;
|
||||
@@ -92,31 +121,23 @@ public class GridManager {
|
||||
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) {
|
||||
public Grid newGrid(@Nullable World world, 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> origDistrib = gc.getWordDistributionAsList();
|
||||
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();
|
||||
.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);
|
||||
|
||||
distrib = new ArrayList<>(distrib);
|
||||
Collections.shuffle(distrib, r);
|
||||
|
||||
Phase startPhase;
|
||||
if(gc.isAnyoneStarts()) {
|
||||
startPhase = Phase.HINTING_BOTH;
|
||||
@@ -126,19 +147,14 @@ public class GridManager {
|
||||
else
|
||||
startPhase = Phase.hinting('B');
|
||||
}
|
||||
|
||||
Grid g = new Grid(uuid, world, startPhase, gc);
|
||||
gRepository.save(g);
|
||||
|
||||
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);
|
||||
for (int j = 0; j < distrib.size(); j++) {
|
||||
Card card = new Card(g, j, words[j], distrib.get(j).getFaceA(), distrib.get(j).getFaceB());
|
||||
cRepository.save(card);
|
||||
}
|
||||
|
||||
return g;
|
||||
}
|
||||
@@ -147,7 +163,7 @@ public class GridManager {
|
||||
* GAME ACTIONS *
|
||||
**********************************/
|
||||
private void endGame(Grid g, boolean win) {
|
||||
g.setWon(Optional.of(win));
|
||||
setWon(g, win);
|
||||
setPhase(g, Phase.GAME_ENDED);
|
||||
newGameEvent(g, GameEvent.newGameEndEvent(win));
|
||||
if(win) {
|
||||
@@ -167,9 +183,7 @@ public class GridManager {
|
||||
}
|
||||
|
||||
Hint h = new Hint(hint, wordCount);
|
||||
g.incrementHintCount();
|
||||
g.setCurrentHint(h);
|
||||
g.setCurrentGuessCount(0);
|
||||
newHint(g, h);
|
||||
newGameEvent(g, GameEvent.newHintEvent(h, player));
|
||||
setPhase(g, Phase.guessing(other(player)));
|
||||
}
|
||||
@@ -237,17 +251,17 @@ public class GridManager {
|
||||
if (!g.getPhase().canPlay() || !g.getPhase().isGuessing(player)) {
|
||||
throw new IllegalGameActionException(g, "This action is not allowed now");
|
||||
}
|
||||
if (cardIndex<0 || cardIndex>g.getCardCount()) {
|
||||
if (cardIndex<0 || cardIndex>getCardCount(g)) {
|
||||
throw new IllegalArgumentException("The given card index is invalid");
|
||||
}
|
||||
Card c = g.getCards()[cardIndex];
|
||||
Card c = getCard(g, 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);
|
||||
c.setColorPublic(other(player), true);//TODO XXX BDD
|
||||
Card.Color guessedColor = c.getColor(other(player));
|
||||
|
||||
newGameEvent(g, GameEvent.newGuessEvent(c, player));
|
||||
newGameEvent(g, GameEvent.newGuessEvent(c.getIndex(), player, c.getColor(other(player))));
|
||||
g.incrementCurrentGuessCount();
|
||||
if(guessedColor == Color.BLACK){
|
||||
if(g.getConfig().isOnlyOneGreenGreenPreventDeath() && onlyOneGreenGreenWouldWin(g)) {
|
||||
@@ -323,7 +337,7 @@ public class GridManager {
|
||||
unlockHintCount(g, (Integer)o);
|
||||
break;
|
||||
case MutableOption.AVAILABLE_HINT_WORD_COUNT:
|
||||
unlockNewWordCounts(g, (Set<Integer>)o);
|
||||
unlockNewWordCounts(g, ((Set<?>)o).stream().map(i -> (Integer)i).collect(Collectors.toSet()));
|
||||
break;
|
||||
case MutableOption.ONE_MORE_GUESS:
|
||||
unlockOneMoreGuess(g);
|
||||
|
||||
@@ -1,14 +1,31 @@
|
||||
package com.bernard.nodecames.model;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.persistence.EmbeddedId;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MapsId;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
public class Card {
|
||||
|
||||
@EmbeddedId
|
||||
CardId id;
|
||||
|
||||
@MapsId("gridId")
|
||||
@ManyToOne
|
||||
Grid grid;
|
||||
|
||||
String word;
|
||||
|
||||
Color colorA;
|
||||
@@ -16,8 +33,12 @@ public class Card {
|
||||
boolean colorAPublic;
|
||||
boolean colorBPublic;
|
||||
|
||||
public Card(String word, Color colorA, Color colorB) {
|
||||
this(word, colorA, colorB, false, false);
|
||||
public Card(Grid g, int index, String word, Color colorA, Color colorB) {
|
||||
this(new CardId(g.getId(), index), g, word, colorA, colorB, false, false);
|
||||
}
|
||||
|
||||
public int getIndex() {
|
||||
return this.getId().getIndex();
|
||||
}
|
||||
|
||||
public Color getColor(char player) {
|
||||
@@ -40,9 +61,29 @@ public class Card {
|
||||
isColorAPublic()?'O':'X', isColorBPublic()?'O':'X');
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
public enum Color {
|
||||
GREEN,
|
||||
WHITE,
|
||||
BLACK;
|
||||
GREEN('G'),
|
||||
WHITE('W'),
|
||||
BLACK('B');
|
||||
|
||||
private char letter;
|
||||
|
||||
public static final Color findByLetter(char letter) {
|
||||
for(Color c : Color.values()) {
|
||||
if(c.letter == letter)
|
||||
return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public static class CardId {
|
||||
UUID gridId;
|
||||
int index;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.bernard.nodecames.model;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.Id;
|
||||
import lombok.AllArgsConstructor;
|
||||
@@ -14,16 +15,14 @@ import lombok.Setter;
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
public class Game {
|
||||
|
||||
@Id
|
||||
@GeneratedValue
|
||||
private UUID id;
|
||||
|
||||
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)),
|
||||
@@ -38,8 +37,4 @@ public class Game {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,49 +1,92 @@
|
||||
package com.bernard.nodecames.model;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import com.bernard.nodecames.model.Card.Color;
|
||||
|
||||
@Getter
|
||||
public class GameEvent {
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.node.JsonNodeFactory;
|
||||
import tools.jackson.databind.node.ObjectNode;
|
||||
|
||||
Type type;
|
||||
|
||||
Object data;
|
||||
|
||||
// 'A' for player A, 'B' for player B, '_' for none
|
||||
char issuer;
|
||||
|
||||
private GameEvent(Type type, Object data, char issuer){
|
||||
this.data = data;
|
||||
this.issuer = issuer;
|
||||
this.type = type;
|
||||
}
|
||||
public sealed interface GameEvent {
|
||||
|
||||
public static GameEvent newHintEvent(Hint h, char issuer) {
|
||||
return new GameEvent(Type.HINT, h, issuer);
|
||||
return new HintEvent(issuer, h);
|
||||
}
|
||||
public static GameEvent newGuessEvent(Card c, char issuer) {
|
||||
return new GameEvent(Type.GUESS, c, issuer);
|
||||
public static GameEvent newGuessEvent(int index, char issuer, Color color) {
|
||||
return new GuessEvent(issuer, index, color);
|
||||
}
|
||||
public static GameEvent newEndGuessingEvent(boolean manual, char issuer) {
|
||||
return new GameEvent(Type.END_GUESSING, manual, issuer);
|
||||
return new EndGuessingEvent(issuer, manual);
|
||||
}
|
||||
public static GameEvent newGameEndEvent(boolean won) {
|
||||
return new GameEvent(Type.GAME_END, won, '_');
|
||||
return new GameEndEvent(won);
|
||||
}
|
||||
public static GameEvent newSuddenDeath() {
|
||||
return new GameEvent(Type.SUDDEN_DEATH, Object.class, '_');
|
||||
return new EnterSuddenDeathEvent();
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
public enum Type {
|
||||
HINT(Hint.class),
|
||||
GUESS(Card.class),
|
||||
END_GUESSING(Boolean.class),
|
||||
GAME_END(Boolean.class),
|
||||
SUDDEN_DEATH(Object.class);
|
||||
|
||||
Class<?> dataType;
|
||||
public static JsonNode toJsonNode(GameEvent ge) {
|
||||
JsonNodeFactory jsn = JsonNodeFactory.instance;
|
||||
ObjectNode o = jsn.objectNode();
|
||||
o.set("type", jsn.stringNode(ge.getClass().getSimpleName()));
|
||||
switch (ge) {
|
||||
case HintEvent(char issuer, Hint hint):
|
||||
o.set("issuer", jsn.stringNode(Character.toString(issuer)));
|
||||
o.set("word", jsn.stringNode(hint.getWord()));
|
||||
o.set("word-count", jsn.numberNode(hint.getWordCount()));
|
||||
break;
|
||||
case GuessEvent(char issuer, int index, Color color):
|
||||
o.set("issuer", jsn.stringNode(Character.toString(issuer)));
|
||||
o.set("card-index", jsn.numberNode(index));
|
||||
o.set("color", jsn.stringNode(color.name().toLowerCase()));
|
||||
break;
|
||||
case EndGuessingEvent(char issuer, boolean manual):
|
||||
o.set("issuer", jsn.stringNode(Character.toString(issuer)));
|
||||
o.set("manual", jsn.booleanNode(manual));
|
||||
break;
|
||||
case GameEndEvent(boolean win):
|
||||
o.set("win", jsn.booleanNode(win));
|
||||
break;
|
||||
case EnterSuddenDeathEvent():
|
||||
break;
|
||||
}
|
||||
return o;
|
||||
}
|
||||
|
||||
public static GameEvent fromJsonNode(JsonNode o) {
|
||||
String type = o.get("type").stringValue();
|
||||
return switch(type) {
|
||||
case "HintEvent" ->
|
||||
new HintEvent(
|
||||
o.get("issuer").asString().charAt(0),
|
||||
new Hint(
|
||||
o.get("word").stringValue(),
|
||||
o.get("word-count").intValue()
|
||||
)
|
||||
);
|
||||
case "GuessEvent" ->
|
||||
new GuessEvent(
|
||||
o.get("issuer").asString().charAt(0),
|
||||
o.get("card-index").intValue(),
|
||||
Color.valueOf(o.get("color").stringValue().toUpperCase())
|
||||
);
|
||||
case "EndGuessingEvent" ->
|
||||
new EndGuessingEvent(
|
||||
o.get("issuer").asString().charAt(0),
|
||||
o.get("manual").booleanValue()
|
||||
);
|
||||
case "GameEndEvent" ->
|
||||
new GameEndEvent(
|
||||
o.get("win").booleanValue()
|
||||
);
|
||||
case "EnterSuddenDeathEvent" ->
|
||||
new EnterSuddenDeathEvent();
|
||||
default -> throw new IllegalArgumentException("Unknown event type : " + type);
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
record HintEvent(char issuer, Hint hint) implements GameEvent {}
|
||||
record GuessEvent(char issuer, int index, Color color) implements GameEvent {}
|
||||
record EndGuessingEvent(char issuer, boolean manual) implements GameEvent {}
|
||||
record GameEndEvent(boolean win) implements GameEvent {}
|
||||
record EnterSuddenDeathEvent() implements GameEvent {}
|
||||
@@ -1,27 +1,49 @@
|
||||
package com.bernard.nodecames.model;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.UUID;
|
||||
|
||||
import org.hibernate.annotations.JdbcTypeCode;
|
||||
import org.hibernate.type.SqlTypes;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.Embedded;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
import tools.jackson.databind.JsonNode;
|
||||
import tools.jackson.databind.node.ArrayNode;
|
||||
import tools.jackson.databind.node.JsonNodeFactory;
|
||||
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
public class Grid {
|
||||
|
||||
@Id
|
||||
UUID id;
|
||||
|
||||
Card[] cards;
|
||||
@ManyToOne(optional = true)
|
||||
World w;
|
||||
|
||||
//@Type(JsonType.class)
|
||||
@Column(columnDefinition = "json")
|
||||
@Convert(converter = GameEventListAdapter.class)
|
||||
@JdbcTypeCode(SqlTypes.JSON)
|
||||
List<GameEvent> gameEvents;
|
||||
|
||||
@Embedded
|
||||
Hint currentHint = null;
|
||||
int currentGuessCount;
|
||||
|
||||
@@ -29,10 +51,18 @@ public class Grid {
|
||||
int usedWhites;
|
||||
|
||||
Phase phase;
|
||||
Optional<Boolean> won;
|
||||
|
||||
//TODO make this parameter a real Optional<Boolean> with Jdbc understanding that optional means nullable
|
||||
@Nullable
|
||||
Boolean won;
|
||||
|
||||
@Embedded
|
||||
GridConfig config;
|
||||
|
||||
public Grid(UUID id, World w, Phase phase, GridConfig config) {
|
||||
this(id, w, List.of(), null, 0, 0, 0, phase, null, config);
|
||||
}
|
||||
|
||||
public void incrementHintCount() {
|
||||
this.setUsedHints(this.getUsedHints()+1);
|
||||
}
|
||||
@@ -43,10 +73,14 @@ public class Grid {
|
||||
this.setCurrentGuessCount(this.getCurrentGuessCount()+1);
|
||||
}
|
||||
public void newGameEvent(GameEvent event) {
|
||||
this.gameEvents = new ArrayList<>(gameEvents);
|
||||
this.gameEvents.add(event);
|
||||
}
|
||||
public int getCardCount() {
|
||||
return this.getCards().length;
|
||||
public Optional<Boolean> isWonOpt() {
|
||||
return Optional.ofNullable(this.won);
|
||||
}
|
||||
public void setWonOpt(Optional<Boolean> won) {
|
||||
this.won = won.orElse(null);
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
@@ -124,4 +158,24 @@ public class Grid {
|
||||
}
|
||||
}
|
||||
|
||||
@AllArgsConstructor
|
||||
public static class GameEventListAdapter implements AttributeConverter<List<GameEvent>, JsonNode> {
|
||||
|
||||
JsonNodeFactory jsn;
|
||||
|
||||
@Override
|
||||
public JsonNode convertToDatabaseColumn(List<GameEvent> attribute) {
|
||||
ArrayNode out = jsn.arrayNode(attribute.size());
|
||||
for(GameEvent ge : attribute) {
|
||||
out.add(GameEvent.toJsonNode(ge));
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<GameEvent> convertToEntityAttribute(JsonNode dbData) {
|
||||
return dbData.asArray().valueStream().map(GameEvent::fromJsonNode).toList();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,38 +2,48 @@ package com.bernard.nodecames.model;
|
||||
|
||||
import static com.bernard.nodecames.model.GridConfig.CardColors.makeDistribution;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import com.bernard.nodecames.model.Card.Color;
|
||||
|
||||
import jakarta.persistence.AttributeConverter;
|
||||
import jakarta.persistence.Convert;
|
||||
import jakarta.persistence.Converter;
|
||||
import jakarta.persistence.Embeddable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Value;
|
||||
|
||||
@Getter
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Embeddable
|
||||
public class GridConfig {
|
||||
|
||||
public static final List<CardColors> CUSTOM6 =
|
||||
public static final Map<CardColors, Integer> CUSTOM6 =
|
||||
makeDistribution(0, 0, 2, 1, 1, 0);
|
||||
public static final List<CardColors> CUSTOM9 =
|
||||
public static final Map<CardColors, Integer> CUSTOM9 =
|
||||
makeDistribution(0, 1, 1, 2, 2, 1);
|
||||
public static final List<CardColors> CUSTOM12 =
|
||||
public static final Map<CardColors, Integer> CUSTOM12 =
|
||||
makeDistribution(1, 1, 1, 2, 3, 1);
|
||||
public static final List<CardColors> CUSTOM16 =
|
||||
public static final Map<CardColors, Integer> CUSTOM16 =
|
||||
makeDistribution(1, 1, 2, 2, 5, 1);
|
||||
public static final List<CardColors> CUSTOM20 =
|
||||
public static final Map<CardColors, Integer> CUSTOM20 =
|
||||
makeDistribution(1, 1, 4, 2, 5, 1);
|
||||
public static final List<CardColors> CLASSICAL25 =
|
||||
public static final Map<CardColors, Integer> CLASSICAL25 =
|
||||
makeDistribution(1, 1, 5, 3, 7, 1);
|
||||
public static final List<CardColors> CUSTOM30 =
|
||||
public static final Map<CardColors, Integer> CUSTOM30 =
|
||||
makeDistribution(2, 2, 5, 3, 7, 1);
|
||||
public static final List<CardColors> CUSTOM36 =
|
||||
public static final Map<CardColors, Integer> CUSTOM36 =
|
||||
makeDistribution(2, 2, 6, 5, 9, 2);
|
||||
|
||||
public static final Set<Integer> BASE_HINTWORDCOUNT = Set.of(
|
||||
@@ -51,7 +61,7 @@ public class GridConfig {
|
||||
3, false, false,
|
||||
false, false);
|
||||
|
||||
public static final GridConfig zeroWithSize(List<CardColors> colors) {
|
||||
public static final GridConfig zeroWithSize(Map<CardColors, Integer> colors) {
|
||||
return new GridConfig(
|
||||
9, false, false, colors, true,
|
||||
true, false, 1, Set.of(1), false,
|
||||
@@ -65,7 +75,8 @@ public class GridConfig {
|
||||
private int maxMaxHint;
|
||||
private boolean structuredGrid;
|
||||
private boolean anyoneStarts;
|
||||
private List<CardColors> wordDistribution;
|
||||
@Convert(converter = WordDistributionConverter.class)
|
||||
private Map<CardColors, Integer> wordDistribution;
|
||||
private boolean maybeSuddenDeath; //TODO
|
||||
private boolean maybeOneMoreGuess; //TODO
|
||||
// If onlyOneGreenGreen is false and having it true would have meant we won,
|
||||
@@ -83,8 +94,18 @@ public class GridConfig {
|
||||
private boolean suddenDeath;
|
||||
private boolean hintsInARow;
|
||||
|
||||
public List<CardColors> getWordDistributionAsList() {
|
||||
List<CardColors> out = new ArrayList<>();
|
||||
for(Entry<CardColors, Integer> e : this.getWordDistribution().entrySet()) {
|
||||
for(int i=0;i<e.getValue();i++) {
|
||||
out.add(e.getKey());
|
||||
}
|
||||
}
|
||||
return Collections.unmodifiableList(out);
|
||||
}
|
||||
|
||||
public int getWordCount() {
|
||||
return wordDistribution.size();
|
||||
return (int)this.getWordDistribution().values().stream().collect(Collectors.summarizingInt(i -> i)).getSum();
|
||||
}
|
||||
|
||||
public void set(MutableOption opt, Object value) {
|
||||
@@ -139,9 +160,9 @@ public class GridConfig {
|
||||
}
|
||||
|
||||
@Value(staticConstructor = "of")
|
||||
public static final class CardColors {
|
||||
Color faceA;
|
||||
Color faceB;
|
||||
public static final class CardColors implements Serializable, Comparable<CardColors> {
|
||||
private Color faceA;
|
||||
private Color faceB;
|
||||
|
||||
public static final CardColors WW = CardColors.of(Color.WHITE, Color.WHITE);
|
||||
public static final CardColors WB = CardColors.of(Color.WHITE, Color.BLACK);
|
||||
@@ -154,19 +175,27 @@ public class GridConfig {
|
||||
public static final CardColors BB = CardColors.of(Color.BLACK, Color.BLACK);
|
||||
|
||||
|
||||
public static final List<CardColors> makeDistribution(
|
||||
public static final Map<CardColors, Integer> 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);
|
||||
return Map.of(
|
||||
BB, bbCount,
|
||||
WW, wwCount,
|
||||
GG, ggCount,
|
||||
WB, wbbwCount,
|
||||
BW, wbbwCount,
|
||||
GB, bggbCount,
|
||||
BG, bggbCount,
|
||||
GW, wggwCount,
|
||||
WG, wggwCount
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int compareTo(CardColors o) {
|
||||
int i = this.getFaceA().compareTo(o.getFaceA());
|
||||
return i!=0?i:this.getFaceB().compareTo(o.getFaceB());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,4 +213,52 @@ public class GridConfig {
|
||||
|
||||
Class<?> dataClass;
|
||||
}
|
||||
|
||||
@NoArgsConstructor
|
||||
@Converter
|
||||
public static class WordDistributionConverter implements AttributeConverter<Map<CardColors, Integer>, String> {
|
||||
|
||||
private String cardColorsToString(CardColors cc, Integer i) {
|
||||
return "%s%s%d".formatted(cc.getFaceA().getLetter(), cc.getFaceB().getLetter(), i);
|
||||
}
|
||||
|
||||
private CardColors parseCardColors(String s) {
|
||||
if(s.length()<3)
|
||||
throw new IllegalArgumentException("Malformed string");
|
||||
char lA = s.charAt(0);
|
||||
char lB = s.charAt(1);
|
||||
Color cA = Color.findByLetter(lA);
|
||||
Color cB = Color.findByLetter(lB);
|
||||
if (cA == null)
|
||||
throw new IllegalArgumentException("Could not find color corresponding to '"+lA+"' in string '" + s + "'");
|
||||
if (cB == null)
|
||||
throw new IllegalArgumentException("Could not find color corresponding to '"+lB+"'");
|
||||
return CardColors.of(cA, cB);
|
||||
}
|
||||
|
||||
private Integer parseCount(String s) {
|
||||
if(s.length()<3)
|
||||
throw new IllegalArgumentException("Malformed string");
|
||||
String number = s.substring(2);
|
||||
return Integer.parseInt(number, 10);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String convertToDatabaseColumn(Map<CardColors, Integer> attribute) {
|
||||
return attribute.entrySet().stream()
|
||||
.sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey()))
|
||||
.map(e -> cardColorsToString(e.getKey(), e.getValue()))
|
||||
.collect(Collectors.joining(";"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<CardColors, Integer> convertToEntityAttribute(String dbData) {
|
||||
return Arrays.stream(dbData.split(";"))
|
||||
.collect(Collectors.toMap(
|
||||
this::parseCardColors,
|
||||
this::parseCount
|
||||
));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
package com.bernard.nodecames.model;
|
||||
|
||||
import jakarta.persistence.Embeddable;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@Embeddable
|
||||
public class Hint {
|
||||
|
||||
String word;
|
||||
|
||||
@@ -1,27 +1,58 @@
|
||||
package com.bernard.nodecames.model;
|
||||
|
||||
import jakarta.annotation.Nullable;
|
||||
import java.util.UUID;
|
||||
|
||||
import jakarta.persistence.Embeddable;
|
||||
import jakarta.persistence.EmbeddedId;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.ManyToOne;
|
||||
import jakarta.persistence.MapsId;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.Setter;
|
||||
|
||||
@AllArgsConstructor
|
||||
@Getter
|
||||
@Setter
|
||||
@Entity
|
||||
@NoArgsConstructor
|
||||
public class World {
|
||||
|
||||
@EmbeddedId
|
||||
WorldId id;
|
||||
|
||||
@MapsId("gameId")
|
||||
@ManyToOne
|
||||
Game game;
|
||||
|
||||
String name;
|
||||
|
||||
// Config used when resetting this world
|
||||
GridConfig initialConfig;
|
||||
|
||||
@Nullable
|
||||
@ManyToOne(optional = true)
|
||||
Grid currentGrid;
|
||||
|
||||
boolean won;
|
||||
|
||||
public World(Game g, String name, GridConfig initConfig, Grid currentGrid, boolean won) {
|
||||
this.id = new WorldId(g.getId(), name);
|
||||
this.game = g;
|
||||
this.initialConfig = initConfig;
|
||||
this.currentGrid = currentGrid;
|
||||
this.won = won;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.getId().getName();
|
||||
}
|
||||
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Embeddable
|
||||
public static class WorldId {
|
||||
UUID gameId;
|
||||
String name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.bernard.nodecames.repository;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.bernard.nodecames.model.Card;
|
||||
import com.bernard.nodecames.model.Grid;
|
||||
import com.bernard.nodecames.model.Card.CardId;
|
||||
|
||||
public interface CardRepository extends JpaRepository<Card, CardId> {
|
||||
public Card[] findByGridOrderById_Index(Grid g);
|
||||
public Card findByGridAndId_Index(Grid g, int index);
|
||||
public int countByGrid(Grid g);
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.bernard.nodecames.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.bernard.nodecames.model.Game;
|
||||
|
||||
public interface GameRepository extends JpaRepository<Game, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.bernard.nodecames.repository;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.bernard.nodecames.model.Grid;
|
||||
|
||||
public interface GridRepository extends JpaRepository<Grid, UUID> {
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.bernard.nodecames.repository;
|
||||
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import com.bernard.nodecames.model.Game;
|
||||
import com.bernard.nodecames.model.World;
|
||||
import com.bernard.nodecames.model.World.WorldId;
|
||||
|
||||
public interface WorldRepository extends JpaRepository<World, WorldId>{
|
||||
public Optional<World> findByGameAndId_Name(Game g, String name);
|
||||
public Set<World> findByGame(Game g);
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
create table card (index integer not null, colora smallint check ((colora between 0 and 2)), colorapublic boolean not null, colorb smallint check ((colorb between 0 and 2)), colorbpublic boolean not null, word varchar(255), grid_id uuid not null, primary key (grid_id, index));
|
||||
create table game (id uuid not null, name varchar(255), primary key (id));
|
||||
create table grid (id uuid not null, anyone_starts boolean, available_hint_word_count integer array, end_guessing boolean, hint_max_length integer, hints_inarow boolean, max_hint integer, max_max_hint integer, maybe_one_more_guess boolean, maybe_sudden_death boolean, one_more_guess boolean, only_one_green_green boolean, only_one_green_green_prevent_death boolean, structured_grid boolean, sudden_death boolean, word_distribution varchar(255), current_guess_count integer not null, word varchar(255), word_count integer, game_events json, phase smallint check ((phase between 0 and 16)), used_hints integer not null, used_whites integer not null, won boolean, w_game_id uuid, w_name varchar(255), primary key (id));
|
||||
create table world (name varchar(255) not null, anyone_starts boolean, available_hint_word_count integer array, end_guessing boolean, hint_max_length integer, hints_inarow boolean, max_hint integer, max_max_hint integer, maybe_one_more_guess boolean, maybe_sudden_death boolean, one_more_guess boolean, only_one_green_green boolean, only_one_green_green_prevent_death boolean, structured_grid boolean, sudden_death boolean, word_distribution varchar(255), won boolean not null, game_id uuid not null, current_grid_id uuid, primary key (game_id, name));
|
||||
alter table if exists card add constraint FK5x40wglgnltvcrme1tqa8ilsx foreign key (grid_id) references grid;
|
||||
alter table if exists grid add constraint FK583ks7m95vxertpybyo2s5h6n foreign key (w_game_id, w_name) references world;
|
||||
alter table if exists world add constraint FKo96aqpsvpf22bjeqk2w6l8jvx foreign key (current_grid_id) references grid;
|
||||
alter table if exists world add constraint FKe9tajxpu9c9llbtdaldt6t18s foreign key (game_id) references game;
|
||||
@@ -22,12 +22,12 @@ function updatePhase() {
|
||||
$('#hint-counter-den').text(gc["max-hint"])
|
||||
|
||||
if(currentHint) {
|
||||
$('#hint-text').text(currentHint['word']+" in "+currentHint['wordCount'])
|
||||
$('#hint-text').text(currentHint['word']+" in "+currentHint['word-count'])
|
||||
$('#guess-counter-num').text(currentGuessCount)
|
||||
if(gc['one-more-guess']) {
|
||||
$('#guess-counter-den').text(currentHint['wordCount'] + "(+1)")
|
||||
$('#guess-counter-den').text(currentHint['word-count'] + "(+1)")
|
||||
} else {
|
||||
$('#guess-counter-den').text(currentHint['wordCount'])
|
||||
$('#guess-counter-den').text(currentHint['word-count'])
|
||||
}
|
||||
}
|
||||
if(registeredAvailableHintWordCountInvalid) {
|
||||
@@ -56,7 +56,7 @@ function updatePhase() {
|
||||
}
|
||||
if((phase['name'] == 'waiting_for_one_more_'+p) ||
|
||||
(phase[p+'-guessing'] &&
|
||||
(gc['end-guessing'] || (currentHint && currentGuessCount >= currentHint['wordCount'])))) {
|
||||
(gc['end-guessing'] || (currentHint && currentGuessCount >= currentHint['word-count'])))) {
|
||||
$('#end-guessing-button').show()
|
||||
} else {
|
||||
$('#end-guessing-button').hide()
|
||||
@@ -179,28 +179,28 @@ function addEndGameEvent(issuer, won) {
|
||||
|
||||
function addEvent(e) {
|
||||
switch(e.type) {
|
||||
case "hint":
|
||||
addHintEvent(e.issuer, e.word, e.wordCount)
|
||||
currentHint = {word: e.word, wordCount: e.wordCount}
|
||||
case "HintEvent":
|
||||
addHintEvent(e.issuer, e.word, e['word-count'])
|
||||
currentHint = {word: e.word, 'word-count': e['word-count']}
|
||||
currentHintCount += 1
|
||||
currentGuessCount = 0
|
||||
updatePhase()
|
||||
break
|
||||
case "guess":
|
||||
case "GuessEvent":
|
||||
addPointEvent(e.issuer, e['card-index'], e.color)
|
||||
currentGuessCount += 1
|
||||
break
|
||||
case "end_guessing":
|
||||
case "EndGuessingEvent":
|
||||
addEndGuessingEvent(e.issuer, e.manual)
|
||||
currentHint = null
|
||||
currentGuessCount = 0
|
||||
break
|
||||
case "game_end":
|
||||
case "GameEndEvent":
|
||||
addEndGameEvent(e.issuer, e.win)
|
||||
currentHint = null
|
||||
currentGuessCount = 0
|
||||
break
|
||||
case "sudden_death":
|
||||
case "EnterSuddenDeathEvent":
|
||||
addSuddenDeathEvent()
|
||||
currentHint = null
|
||||
currentGuessCount = 0
|
||||
|
||||
@@ -2,7 +2,10 @@ package com.bernard.nodecames;
|
||||
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@SpringBootTest
|
||||
@RequiredArgsConstructor
|
||||
class NodeCamesApplicationTests {
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user