From 741f60bb3b51241e33e45e05b0bb24f7716879d9 Mon Sep 17 00:00:00 2001 From: Mysaa Java Date: Thu, 8 Oct 2026 11:46:28 +0200 Subject: [PATCH] World config and inventories --- flake.nix | 10 + .../com/bernard/nodecames/HttpController.java | 17 +- .../nodecames/frontend/JsonDataService.java | 18 +- .../nodecames/game/ArchipelagoClient.java | 55 +---- .../bernard/nodecames/game/GameManager.java | 105 +++------ .../bernard/nodecames/game/GridManager.java | 13 +- .../nodecames/model/ColorDistribution.java | 167 ++++++++++++++ .../com/bernard/nodecames/model/Game.java | 41 ++-- .../bernard/nodecames/model/GameConfig.java | 57 +++++ .../com/bernard/nodecames/model/Grid.java | 12 +- .../bernard/nodecames/model/GridConfig.java | 168 +------------- .../bernard/nodecames/model/Inventory.java | 131 +++++++++++ .../com/bernard/nodecames/model/World.java | 15 +- .../bernard/nodecames/model/WorldConfig.java | 205 ++++++++++++++++++ .../converter/ColorDistributionConverter.java | 22 ++ .../model/converter/GameConfigConverter.java | 42 ++++ .../model/converter/JsonNodeConverter.java | 25 +++ .../model/converter/WorldConfigConverter.java | 92 ++++++++ .../com/bernard/nodecames/util/Utils.java | 47 ++++ .../db/migration/V1_2_0__add_game_config.sql | 19 ++ src/main/resources/static/css/style.css | 4 + src/main/resources/templates/index.html | 4 + src/main/resources/templates/map.html | 29 ++- .../java/com/bernard/nodecames/JsonTests.java | 30 +++ 24 files changed, 976 insertions(+), 352 deletions(-) create mode 100644 src/main/java/com/bernard/nodecames/model/ColorDistribution.java create mode 100644 src/main/java/com/bernard/nodecames/model/GameConfig.java create mode 100644 src/main/java/com/bernard/nodecames/model/Inventory.java create mode 100644 src/main/java/com/bernard/nodecames/model/WorldConfig.java create mode 100644 src/main/java/com/bernard/nodecames/model/converter/ColorDistributionConverter.java create mode 100644 src/main/java/com/bernard/nodecames/model/converter/GameConfigConverter.java create mode 100644 src/main/java/com/bernard/nodecames/model/converter/JsonNodeConverter.java create mode 100644 src/main/java/com/bernard/nodecames/model/converter/WorldConfigConverter.java create mode 100644 src/main/java/com/bernard/nodecames/util/Utils.java create mode 100644 src/main/resources/db/migration/V1_2_0__add_game_config.sql create mode 100644 src/test/java/com/bernard/nodecames/JsonTests.java diff --git a/flake.nix b/flake.nix index f28bf10..77b2580 100644 --- a/flake.nix +++ b/flake.nix @@ -46,6 +46,15 @@ ${pkgs.podman}/bin/podman run -e POSTGRES_USER=nodecames -e POSTGRES_PASSWORD=nodecames-dev -e POSTGRES_DB=nodecames --volume ./.postgres:/var/lib/postgresql -p 127.0.0.1:5432:5432 postgres:18.4 &> .postgres/postgres.log & ''; + test-archipelago = pkgs.writeShellApplication { + name = "archipelago"; + text = '' + TMP_HOME=$(mktemp -d) + HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago --update_settings + ln -s ${nodecames-apworld} "$TMP_HOME/.local/share/Archipelago/worlds/nodecames.apworld" + HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago + ''; + }; in { packages.${system} = { default = self.packages.${system}.nodecames; @@ -56,6 +65,7 @@ default = self.apps.${system}.nodecames; nodecames = { type = "app"; program = "${nodecames-launcher}/bin/nodecames"; }; postgres-server = { type = "app"; program = "${postgres-server}/bin/postgres-server"; }; + archipelago = { type = "app"; program = "${test-archipelago}/bin/archipelago"; }; }; devShells.${system} = { diff --git a/src/main/java/com/bernard/nodecames/HttpController.java b/src/main/java/com/bernard/nodecames/HttpController.java index 52bb45f..835a34e 100644 --- a/src/main/java/com/bernard/nodecames/HttpController.java +++ b/src/main/java/com/bernard/nodecames/HttpController.java @@ -31,6 +31,7 @@ import com.bernard.nodecames.game.GameManager; import com.bernard.nodecames.game.GridManager; import com.bernard.nodecames.game.IllegalGameActionException; import com.bernard.nodecames.model.Game; +import com.bernard.nodecames.model.GameConfig; import com.bernard.nodecames.model.Grid; import com.bernard.nodecames.model.GridConfig; import com.bernard.nodecames.model.GridConfig.MutableOption; @@ -112,9 +113,9 @@ public class HttpController { public RedirectView createRoom(@RequestParam("type") String type) { GridConfig cfg = switch(type) { case "zero" -> GridConfig.ZERO; - case "base" -> GridConfig.CODENAMES_CLASSICAL; - case "big" -> GridConfig.BIG_CLASSICAL; - default -> GridConfig.CODENAMES_CLASSICAL; + case "base" -> GridConfig.REGULAR; + case "big" -> GridConfig.BIG; + default -> GridConfig.REGULAR; }; Random r = new Random(rand.nextLong()); Grid g = gm.newGrid(null, cfg, r); @@ -122,8 +123,13 @@ public class HttpController { } @GetMapping("/create-game") - public RedirectView createGame(@RequestParam("name") String name) { - Game g = wm.createGame(name); + public RedirectView createGame(@RequestParam("name") String name, @RequestParam("type") String type) { + GameConfig cfg = switch(type) { + case "default" -> GameConfig.DEFAULT_GAME_CONFIG; + case "regular" -> GameConfig.REGULAR_GAME_CONFIG; + default -> GameConfig.DEFAULT_GAME_CONFIG; + }; + Game g = wm.createGame(name, cfg); return new RedirectView("/game/"+g.getId().toString()+"/map"); } @@ -143,6 +149,7 @@ public class HttpController { .toList(); m.addAttribute("game", g.get()); m.addAttribute("worlds", worlds); + m.addAttribute("items", g.get().getInventory().getReceivedItems()); m.addAttribute("player", player); return "map"; } diff --git a/src/main/java/com/bernard/nodecames/frontend/JsonDataService.java b/src/main/java/com/bernard/nodecames/frontend/JsonDataService.java index d2916de..b6c9a58 100644 --- a/src/main/java/com/bernard/nodecames/frontend/JsonDataService.java +++ b/src/main/java/com/bernard/nodecames/frontend/JsonDataService.java @@ -1,6 +1,5 @@ package com.bernard.nodecames.frontend; -import java.util.Map.Entry; import java.util.Set; import org.springframework.stereotype.Service; @@ -10,22 +9,23 @@ 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.converter.ColorDistributionConverter; import com.bernard.nodecames.repository.CardRepository; -import lombok.RequiredArgsConstructor; +import lombok.AllArgsConstructor; 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 +@AllArgsConstructor public class JsonDataService { private final JsonNodeFactory jsn; private final CardRepository cRepository; + private final ColorDistributionConverter wdc; public JsonNode cardData(Card c, char player) { ObjectNode cardNode = jsn.objectNode(); @@ -43,15 +43,7 @@ public class JsonDataService { gcNode.set("max-max-hint", jsn.numberNode(gc.getMaxMaxHint())); gcNode.set("structured-grid", jsn.booleanNode(gc.isStructuredGrid())); gcNode.set("anyone-starts", jsn.booleanNode(gc.isAnyoneStarts())); - ArrayNode wordDistributionNode = jsn.arrayNode(gc.getWordDistribution().size()); - for(Entry e : gc.getWordDistribution().entrySet()) { - ObjectNode ccNode = jsn.objectNode(); - 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); + gcNode.set("color-distribution", jsn.stringNode(wdc.convertToDatabaseColumn(gc.getColorDistribution()))); gcNode.set("maybe-sudden-death", jsn.booleanNode(gc.isMaybeSuddenDeath())); gcNode.set("maybe-one-more-guess", jsn.booleanNode(gc.isMaybeOneMoreGuess())); gcNode.set("only-one-green-green-prevent-death", jsn.booleanNode(gc.isOnlyOneGreenGreenPreventDeath())); diff --git a/src/main/java/com/bernard/nodecames/game/ArchipelagoClient.java b/src/main/java/com/bernard/nodecames/game/ArchipelagoClient.java index eca80d0..f52116b 100644 --- a/src/main/java/com/bernard/nodecames/game/ArchipelagoClient.java +++ b/src/main/java/com/bernard/nodecames/game/ArchipelagoClient.java @@ -8,7 +8,6 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.bernard.nodecames.model.Game; -import com.bernard.nodecames.model.GridConfig.MutableOption; import io.github.archipelagomw.APResult.ResultCode; import io.github.archipelagomw.Client; @@ -37,57 +36,11 @@ public class ArchipelagoClient extends Client { } @ArchipelagoEventListener - public void onReceiveItem(ReceiveItemEvent e) throws IllegalGameActionException { + public void onReceiveItem(ReceiveItemEvent e) { log.debug("Received item : {}", e.getItemName()); - switch(e.getItemName()) { - case "progressive_hint_count": - gm.unlockMoreHintCount(this.game); - break; - case "progressive_hint_word_length": - gm.unlockMoreHintWordLengthCount(this.game); - break; - case "progressive_hint_word_count": - gm.unlockMoreHintWordCount(this.game); - break; - case "zero_hint": - gm.unlockHintWordCount(this.game, 0); - break; - case "infinity_hint": - gm.unlockHintWordCount(this.game, -1); - break; - case "one_more_guess": - gm.unlockMutable(game, MutableOption.ONE_MORE_GUESS, true); - break; - case "end_guessing": - gm.unlockMutable(game, MutableOption.END_GUESSING, true); - break; - case "only_one_green_green": - gm.unlockMutable(game, MutableOption.ONLY_ONE_GREEN_GREEN, true); - break; - case "sudden_death": - gm.unlockMutable(game, MutableOption.SUDDEN_DEATH, true); - break; - case "structured_grid": - gm.unlockStructuredGrid(game); - break; - case "hints_in_a_row": - gm.unlockMutable(game, MutableOption.HINTS_IN_A_ROW, true); - break; - case "anyone_starts": - gm.unlockAnyoneCanStart(game); - break; - case "filler": - // We do nothing if we receive a filler item - break; - default: - if (e.getItemName().startsWith("unlock_world_")) { - int worldIndex = Integer.parseInt(e.getItemName().substring("unlock_world_".length()))-1; - String worldName = "%d-%d".formatted((worldIndex/3)+1, (worldIndex%3)+1); - gm.unlockWorld(game, worldName); - } else { - throw new IllegalArgumentException("Received unknown item : "+e.getItemName()); - } - } + if(e.getIndex() > game.getInventory().getReceivedItems().size()) + gm.giveItem(game, e.getItemName()); + // Else, the item has already been received } @Getter diff --git a/src/main/java/com/bernard/nodecames/game/GameManager.java b/src/main/java/com/bernard/nodecames/game/GameManager.java index a9a9a00..2aed232 100644 --- a/src/main/java/com/bernard/nodecames/game/GameManager.java +++ b/src/main/java/com/bernard/nodecames/game/GameManager.java @@ -2,27 +2,28 @@ package com.bernard.nodecames.game; import java.util.Collection; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; -import java.util.Map.Entry; -import java.util.Optional; import java.util.Random; import java.util.Set; -import java.util.UUID; +import java.util.Map.Entry; import java.util.concurrent.CopyOnWriteArraySet; import java.util.function.Consumer; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Service; import com.bernard.nodecames.model.Card; import com.bernard.nodecames.model.Card.Color; import com.bernard.nodecames.model.Game; +import com.bernard.nodecames.model.GameConfig; 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.MutableOption; import com.bernard.nodecames.model.Hint; +import com.bernard.nodecames.model.Inventory; import com.bernard.nodecames.model.World; import com.bernard.nodecames.model.World.WorldId; import com.bernard.nodecames.observer.GridObserver; @@ -42,6 +43,9 @@ public class GameManager { private final GameRepository gRepository; private final WorldRepository wRepository; + + private static final Logger log = LoggerFactory.getLogger(GameManager.class); + //TODO try putting "World" here instead of WorldId private Map> observers = new HashMap<>(); @@ -73,15 +77,9 @@ public class GameManager { forEachObserver(w, o -> gm.addObserver(g, o)); } - public Game createGame(String name) { - UUID uuid = UUID.randomUUID(); + public Game createGame(String name, GameConfig cfg) { + Game g = cfg.newGame(gRepository, wRepository, name); - Game g = new Game(uuid, name); - gRepository.save(g); - for (Entry wConfig : Game.DEFAULT_WORLDS_CONFIG.entrySet()) { - World w = new World(g, wConfig.getKey(), wConfig.getValue(), null, false, true); - wRepository.save(w); - } return g; } @@ -97,7 +95,7 @@ public class GameManager { if(w.isLocked()) //TODO separate IllegaleGameAction and IllegalGridAction throw new IllegalGameActionException(null, "Cannot create a grid in a locked world"); - GridConfig gc = w.getInitialConfig(); + GridConfig gc = w.makeGridConfig(); //TODO seed random Grid grid = gm.newGrid(w, gc, new Random()); @@ -120,76 +118,23 @@ public class GameManager { } } - /** - * UPDATE GAME CONFIG - */ - public void unlockMutable(Game g, MutableOption opt, Object o) throws IllegalGameActionException { - Set worlds = wRepository.findByGame(g); - for(World w : worlds) { - GridConfig initialConfig = w.getInitialConfig(); - initialConfig.set(opt, o); - w.setInitialConfig(initialConfig); - wRepository.save(w); + public void giveItem(Game g, String item) { + g.getInventory().receiveItem(item); + Inventory inv = g.getInventory(); + for(World w : allWorlds(g)) { + if(w.getCurrentGrid() == null) continue; Grid grid = w.getCurrentGrid(); - if(grid != null) { - gm.unlock(grid, opt, o); + Map updates = w.getConfig().updateConfig(grid.getConfig(), inv); + log.debug("Updating option for world {} : {}", w.getName(), updates); + for(Entry e : updates.entrySet()) { + try { + gm.unlock(grid, e.getKey(), e.getValue()); + } catch (IllegalGameActionException ex) { + throw new IllegalStateException("World Config sent a set of changes that got us out of game logic", ex); + } } } - } - - public void unlockStructuredGrid(Game g) { - Set worlds = wRepository.findByGame(g); - for(World w : worlds) { - GridConfig initialConfig = w.getInitialConfig(); - initialConfig.setStructuredGrid(true); - w.setInitialConfig(initialConfig); - wRepository.save(w); - } - } - public void unlockAnyoneCanStart(Game g) { - Set worlds = wRepository.findByGame(g); - for(World w : worlds) { - GridConfig initialConfig = w.getInitialConfig(); - initialConfig.setAnyoneStarts(true); - w.setInitialConfig(initialConfig); - wRepository.save(w); - } - } - - public void unlockWorld(Game g, String name) throws IllegalGameActionException { - Optional ow = wRepository.findByGameAndId_Name(g, name); - if(!ow.isPresent()) - throw new IllegalGameActionException(null, "Could not find world with name %s".formatted(name)); - World w = ow.get(); - w.setLocked(false); - wRepository.save(w); - } - - public void unlockMoreHintCount(Game game) throws IllegalGameActionException { - //TODO Store current hint count elsewhere - int currentHintCount = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getMaxHint(); - unlockMutable(game, MutableOption.MAX_HINT, currentHintCount+1); - } - - public void unlockMoreHintWordLengthCount(Game game) throws IllegalGameActionException { - //TODO Store current hint word length elsewhere - int currentHintWordLength = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getHintMaxLength(); - unlockMutable(game, MutableOption.HINT_MAX_LENGTH, currentHintWordLength+1); - } - - public void unlockMoreHintWordCount(Game game) throws IllegalGameActionException { - //TODO Store current hint word count elsewhere - int currentMaxWordCount = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getAvailableHintWordCount().stream().mapToInt(i -> i).max().getAsInt(); - unlockHintWordCount(game, currentMaxWordCount+1); - } - - public void unlockHintWordCount(Game game, int i) throws IllegalGameActionException { - Set currentWordCounts = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getAvailableHintWordCount(); - if(currentWordCounts.contains(i)) - throw new IllegalGameActionException(null, "Word count already unlocked : %d".formatted(i)); - Set newWordCounts = new HashSet<>(currentWordCounts); - newWordCounts.add(i); - unlockMutable(game, MutableOption.AVAILABLE_HINT_WORD_COUNT, newWordCounts); + gRepository.save(g); } @AllArgsConstructor diff --git a/src/main/java/com/bernard/nodecames/game/GridManager.java b/src/main/java/com/bernard/nodecames/game/GridManager.java index 450248b..61f61a7 100644 --- a/src/main/java/com/bernard/nodecames/game/GridManager.java +++ b/src/main/java/com/bernard/nodecames/game/GridManager.java @@ -20,11 +20,11 @@ import org.springframework.stereotype.Service; import com.bernard.nodecames.model.Card; import com.bernard.nodecames.model.Card.Color; +import com.bernard.nodecames.model.ColorDistribution.CardColors; 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 com.bernard.nodecames.model.World; @@ -147,13 +147,13 @@ public class GridManager { public Grid newGrid(@Nullable World world, GridConfig gc, Random r) { UUID uuid = UUID.randomUUID(); - String[] words = dictionnaries.randomWords(gc.getWordCount(), r); - List origDistrib = gc.getWordDistributionAsList(); + String[] words = dictionnaries.randomWords(gc.getColorDistribution().wordCount(), r); + List origDistrib = gc.getColorDistribution().asList(); List distrib; if(!gc.isStructuredGrid()) { List bColors = origDistrib.stream().map(cc -> cc.getFaceB()).collect(Collectors.toList()); Collections.shuffle(bColors, r); - distrib = IntStream.range(0, gc.getWordCount()) + distrib = IntStream.range(0, gc.getColorDistribution().wordCount()) .mapToObj(i -> CardColors.of(origDistrib.get(i).getFaceA(), bColors.get(i))) .toList(); } else { @@ -400,7 +400,10 @@ public class GridManager { 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"); + throw new IllegalGameActionException(g, + "Can only add hint word options (%s) is not a strict superset of (%s)" + .formatted(newAvailableHintWordCounts, g.getConfig().getAvailableHintWordCount()) + ); } changeConfig(g, MutableOption.AVAILABLE_HINT_WORD_COUNT, newAvailableHintWordCounts); } diff --git a/src/main/java/com/bernard/nodecames/model/ColorDistribution.java b/src/main/java/com/bernard/nodecames/model/ColorDistribution.java new file mode 100644 index 0000000..ffd4ed6 --- /dev/null +++ b/src/main/java/com/bernard/nodecames/model/ColorDistribution.java @@ -0,0 +1,167 @@ +package com.bernard.nodecames.model; + +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.stream.Collectors; + +import com.bernard.nodecames.model.Card.Color; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +import lombok.Value; +import tools.jackson.core.JacksonException; +import tools.jackson.core.JsonParser; +import tools.jackson.databind.DeserializationContext; +import tools.jackson.databind.annotation.JsonDeserialize; +import tools.jackson.databind.deser.std.StdDeserializer; + +@Value(staticConstructor = "of") +@JsonDeserialize(using = ColorDistribution.Deserializer.class) +public class ColorDistribution { + + public static final ColorDistribution CUSTOM6 = + makeDistribution(0, 0, 2, 1, 1, 0); + public static final ColorDistribution CUSTOM9 = + makeDistribution(0, 1, 1, 2, 2, 1); + public static final ColorDistribution CUSTOM12 = + makeDistribution(1, 1, 1, 2, 3, 1); + public static final ColorDistribution CUSTOM16 = + makeDistribution(1, 1, 2, 2, 5, 1); + public static final ColorDistribution CUSTOM20 = + makeDistribution(1, 1, 4, 2, 5, 1); + public static final ColorDistribution CLASSICAL25 = + makeDistribution(1, 1, 5, 3, 7, 1); + public static final ColorDistribution CUSTOM30 = + makeDistribution(2, 2, 5, 3, 7, 1); + public static final ColorDistribution CUSTOM36 = + makeDistribution(2, 2, 6, 5, 9, 2); + + private final Map colors; + + public List asList() { + List out = new ArrayList<>(); + for(Entry e : this.colors.entrySet()) { + for(int i=0;i i)).getSum(); + } + + public static final ColorDistribution makeDistribution( + int wbbwCount, int bggbCount, int wggwCount, + int ggCount, int wwCount, int bbCount + ) { + return new ColorDistribution(Map.of( + CardColors.BB, bbCount, + CardColors.WW, wwCount, + CardColors.GG, ggCount, + CardColors.WB, wbbwCount, + CardColors.BW, wbbwCount, + CardColors.GB, bggbCount, + CardColors.BG, bggbCount, + CardColors.GW, wggwCount, + CardColors.WG, wggwCount + )); + } + + @Value(staticConstructor = "of") + public static final class CardColors implements Comparable { + 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); + public static final CardColors BW = CardColors.of(Color.BLACK, Color.WHITE); + public static final CardColors WG = CardColors.of(Color.WHITE, Color.GREEN); + public static final CardColors GW = CardColors.of(Color.GREEN, Color.WHITE); + public static final CardColors GG = CardColors.of(Color.GREEN, Color.GREEN); + 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 BB = CardColors.of(Color.BLACK, Color.BLACK); + + @Override + public int compareTo(CardColors o) { + int i = this.getFaceA().compareTo(o.getFaceA()); + return i!=0?i:this.getFaceB().compareTo(o.getFaceB()); + } + + } + + + /* + * JSON conversion + */ + + private static String cardColorsToString(CardColors cc, Integer i) { + return "%s%s%d".formatted(cc.getFaceA().getLetter(), cc.getFaceB().getLetter(), i); + } + + private static 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 static Integer parseCount(String s) { + if(s.length()<3) + throw new IllegalArgumentException("Malformed string"); + String number = s.substring(2); + return Integer.parseInt(number, 10); + } + + @JsonValue + @Override + public String toString() { + return this.getColors().entrySet().stream() + .sorted((e1, e2) -> e1.getKey().compareTo(e2.getKey())) + .map(e -> cardColorsToString(e.getKey(), e.getValue())) + .collect(Collectors.joining(";")); + } + + @JsonCreator + public static ColorDistribution ofString(String str) { + return ColorDistribution.of(Arrays.stream(str.split(";")) + .collect(Collectors.toMap( + ColorDistribution::parseCardColors, + ColorDistribution::parseCount + ))); + } + + public static class Deserializer extends StdDeserializer { + public Deserializer() { + super(ColorDistribution.class); + } + + @Override + public ColorDistribution deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException { + String s = p.getString(); + return ColorDistribution.ofString(s); + } + + } + +} diff --git a/src/main/java/com/bernard/nodecames/model/Game.java b/src/main/java/com/bernard/nodecames/model/Game.java index 25b33c9..be8aac2 100644 --- a/src/main/java/com/bernard/nodecames/model/Game.java +++ b/src/main/java/com/bernard/nodecames/model/Game.java @@ -1,18 +1,19 @@ package com.bernard.nodecames.model; -import java.util.Map; +import java.util.Set; import java.util.UUID; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.type.SqlTypes; + import jakarta.persistence.Entity; -import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; -import lombok.AllArgsConstructor; +import jakarta.persistence.OneToMany; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; @NoArgsConstructor -@AllArgsConstructor @Getter @Setter @Entity @@ -21,20 +22,22 @@ public class Game { @Id private UUID id; - String name; + private String name; + + @JdbcTypeCode(SqlTypes.JSON) + private GameConfig config; + + @JdbcTypeCode(SqlTypes.JSON) + private Inventory inventory = Inventory.empty(); + + @OneToMany(mappedBy = "game") + Set worlds = null; + + public Game(UUID id, String name, GameConfig config) { + this(); + this.setId(id); + this.setName(name); + this.setConfig(config); + } - public static final Map 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)) - ); } diff --git a/src/main/java/com/bernard/nodecames/model/GameConfig.java b/src/main/java/com/bernard/nodecames/model/GameConfig.java new file mode 100644 index 0000000..5313a04 --- /dev/null +++ b/src/main/java/com/bernard/nodecames/model/GameConfig.java @@ -0,0 +1,57 @@ +package com.bernard.nodecames.model; + +import java.util.Map; +import java.util.UUID; + +import com.bernard.nodecames.repository.GameRepository; +import com.bernard.nodecames.repository.WorldRepository; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Value; + +@Value +@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) +@AllArgsConstructor +public class GameConfig { + + @JsonProperty("worlds") + private final Map worlds; + + //TODO we might have a solution without passing repositories as parameters + public Game newGame(GameRepository gRepository, WorldRepository wRepository, String name) { + UUID id = UUID.randomUUID(); + Game g = new Game(id, name, this); + gRepository.save(g); + for(String wName : worlds.keySet()) { + WorldConfig wc = worlds.get(wName); + World w = wc.newWorld(g, wName); + wRepository.save(w); + } + return g; + } + + public static final GameConfig DEFAULT_GAME_CONFIG = new GameConfig(Map.ofEntries( + Map.entry("1-1", WorldConfig.zeroWithSize(ColorDistribution.CUSTOM6)), + Map.entry("1-2", WorldConfig.zeroWithSize(ColorDistribution.CUSTOM9)), + Map.entry("1-3", WorldConfig.zeroWithSize(ColorDistribution.CUSTOM9)), + Map.entry("2-1", WorldConfig.zeroWithSize(ColorDistribution.CUSTOM12)), + Map.entry("2-2", WorldConfig.zeroWithSize(ColorDistribution.CUSTOM16)), + Map.entry("2-3", WorldConfig.zeroWithSize(ColorDistribution.CUSTOM16)), + Map.entry("3-1", WorldConfig.zeroWithSize(ColorDistribution.CUSTOM20)), + Map.entry("3-2", WorldConfig.zeroWithSize(ColorDistribution.CLASSICAL25)), + Map.entry("3-3", WorldConfig.zeroWithSize(ColorDistribution.CLASSICAL25)), + Map.entry("4-1", WorldConfig.zeroWithSize(ColorDistribution.CUSTOM30)), + Map.entry("4-2", WorldConfig.zeroWithSize(ColorDistribution.CUSTOM36)), + Map.entry("4-3", WorldConfig.zeroWithSize(ColorDistribution.CUSTOM36)) + )); + + public static final GameConfig REGULAR_GAME_CONFIG = new GameConfig(Map.ofEntries( + Map.entry("1", WorldConfig.classicalWithSize(ColorDistribution.CUSTOM9, 4)), + Map.entry("2", WorldConfig.classicalWithSize(ColorDistribution.CUSTOM16, 7)), + Map.entry("3", WorldConfig.classicalWithSize(ColorDistribution.CLASSICAL25, 9)), + Map.entry("4", WorldConfig.classicalWithSize(ColorDistribution.CUSTOM36, 15)) + )); +} diff --git a/src/main/java/com/bernard/nodecames/model/Grid.java b/src/main/java/com/bernard/nodecames/model/Grid.java index 750e525..e50eced 100644 --- a/src/main/java/com/bernard/nodecames/model/Grid.java +++ b/src/main/java/com/bernard/nodecames/model/Grid.java @@ -3,7 +3,6 @@ package com.bernard.nodecames.model; import java.util.ArrayList; import java.util.List; import java.util.Optional; -import java.util.Set; import java.util.UUID; import org.hibernate.annotations.JdbcTypeCode; @@ -11,18 +10,17 @@ import org.hibernate.type.SqlTypes; import jakarta.annotation.Nullable; import jakarta.persistence.AttributeConverter; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Convert; +import jakarta.persistence.Converter; import jakarta.persistence.Embedded; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; import lombok.Setter; import tools.jackson.databind.JsonNode; import tools.jackson.databind.node.ArrayNode; @@ -41,7 +39,6 @@ public class Grid { @ManyToOne(optional = true) World w; - //@Type(JsonType.class) @Column(columnDefinition = "json") @Convert(converter = GameEventListAdapter.class) @JdbcTypeCode(SqlTypes.JSON) @@ -162,10 +159,11 @@ public class Grid { } } - @AllArgsConstructor + @RequiredArgsConstructor + @Converter public static class GameEventListAdapter implements AttributeConverter, JsonNode> { - JsonNodeFactory jsn; + private final JsonNodeFactory jsn; @Override public JsonNode convertToDatabaseColumn(List attribute) { diff --git a/src/main/java/com/bernard/nodecames/model/GridConfig.java b/src/main/java/com/bernard/nodecames/model/GridConfig.java index c7d8965..05f58ea 100644 --- a/src/main/java/com/bernard/nodecames/model/GridConfig.java +++ b/src/main/java/com/bernard/nodecames/model/GridConfig.java @@ -1,28 +1,17 @@ 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 com.bernard.nodecames.model.converter.ColorDistributionConverter; -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.Setter; -import lombok.Value; @Getter @AllArgsConstructor @@ -30,55 +19,19 @@ import lombok.Value; @Embeddable public class GridConfig { - public static final Map CUSTOM6 = - makeDistribution(0, 0, 2, 1, 1, 0); - public static final Map CUSTOM9 = - makeDistribution(0, 1, 1, 2, 2, 1); - public static final Map CUSTOM12 = - makeDistribution(1, 1, 1, 2, 3, 1); - public static final Map CUSTOM16 = - makeDistribution(1, 1, 2, 2, 5, 1); - public static final Map CUSTOM20 = - makeDistribution(1, 1, 4, 2, 5, 1); - public static final Map CLASSICAL25 = - makeDistribution(1, 1, 5, 3, 7, 1); - public static final Map CUSTOM30 = - makeDistribution(2, 2, 5, 3, 7, 1); - public static final Map CUSTOM36 = - makeDistribution(2, 2, 6, 5, 9, 2); - - public static final Set BASE_HINTWORDCOUNT = Set.of( - 0,1,2,3,4,5,6,7,8,9,-1 - ); - public static final GridConfig BIG_CLASSICAL = new GridConfig( - 12, CUSTOM36, true, true, - true, true, true, 12, BASE_HINTWORDCOUNT, true, - Integer.MAX_VALUE, true, true, - true, false); - - public static final GridConfig CODENAMES_CLASSICAL = new GridConfig( - 9, CLASSICAL25, true, true, true, - true, true, 9, BASE_HINTWORDCOUNT, true, - Integer.MAX_VALUE, true, true, - true, false); - - public static final GridConfig ZERO = zeroWithSize(CLASSICAL25); + public static final GridConfig ZERO = + WorldConfig.zeroWithSize(ColorDistribution.CLASSICAL25).makeGridConfig(Inventory.empty()); + public static final GridConfig REGULAR = + WorldConfig.classicalWithSize(ColorDistribution.CLASSICAL25, 9).makeGridConfig(Inventory.empty()); + public static final GridConfig BIG = + WorldConfig.classicalWithSize(ColorDistribution.CLASSICAL25, 9).makeGridConfig(Inventory.empty()); - //TODO this 9 should be config - public static final GridConfig zeroWithSize(Map colors) { - return new GridConfig( - 9, colors, true, true, - false, false, false, 1, Set.of(1), false, - 3, false, false, - false, false); - } - // Immutable options // Maximum of maximom number of hints // This is to trigger end of game private int maxMaxHint; - @Convert(converter = WordDistributionConverter.class) - private Map wordDistribution; + @Convert(converter = ColorDistributionConverter.class) + private ColorDistribution colorDistribution; private boolean maybeSuddenDeath; //TODO private boolean maybeOneMoreGuess; //TODO // If onlyOneGreenGreen is false and having it true would have meant we won, @@ -101,20 +54,6 @@ public class GridConfig { private boolean suddenDeath; private boolean hintsInARow; - public List getWordDistributionAsList() { - List out = new ArrayList<>(); - for(Entry e : this.getWordDistribution().entrySet()) { - for(int i=0;i i)).getSum(); - } - public void set(MutableOption opt, Object value) { try{ switch(opt) { @@ -166,48 +105,9 @@ public class GridConfig { return 0 <= i || i == -1; } - @Value(staticConstructor = "of") - public static final class CardColors implements Serializable, Comparable { - 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); - public static final CardColors BW = CardColors.of(Color.BLACK, Color.WHITE); - public static final CardColors WG = CardColors.of(Color.WHITE, Color.GREEN); - public static final CardColors GW = CardColors.of(Color.GREEN, Color.WHITE); - public static final CardColors GG = CardColors.of(Color.GREEN, Color.GREEN); - 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 BB = CardColors.of(Color.BLACK, Color.BLACK); - - - public static final Map makeDistribution( - int wbbwCount, int bggbCount, int wggwCount, - int ggCount, int wwCount, int bbCount - ) { - 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()); - } - } - @AllArgsConstructor @Getter + //TODO do record type public enum MutableOption { MAX_HINT(Integer.class), AVAILABLE_HINT_WORD_COUNT(Set.class), @@ -220,52 +120,4 @@ public class GridConfig { Class dataClass; } - - @NoArgsConstructor - @Converter - public static class WordDistributionConverter implements AttributeConverter, 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 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 convertToEntityAttribute(String dbData) { - return Arrays.stream(dbData.split(";")) - .collect(Collectors.toMap( - this::parseCardColors, - this::parseCount - )); - } - - } } diff --git a/src/main/java/com/bernard/nodecames/model/Inventory.java b/src/main/java/com/bernard/nodecames/model/Inventory.java new file mode 100644 index 0000000..a092b79 --- /dev/null +++ b/src/main/java/com/bernard/nodecames/model/Inventory.java @@ -0,0 +1,131 @@ +package com.bernard.nodecames.model; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.RequiredArgsConstructor; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.JsonNodeFactory; + +@Getter +public class Inventory { + + // List of items ids in reception order + private List receivedItems = new ArrayList<>(); + + private int extraHintCount = 0; + + private boolean extraStructuredGrid = false; + private boolean extraAnyoneStarts = false; + + private Set extraHintWordCounts = new HashSet<>(); + private boolean extraOneMoreGuess = false; + private int extraHintMaxLength = 0; + private boolean extraEndGuessing = false; + private boolean extraOnlyOneGreenGreen = false; + private boolean extraSuddenDeath = false; + private boolean extraHintsInARow = false; + + private Set unlockedWorlds = new HashSet<>(); + + public Inventory(List items) { + receivedItems = new ArrayList<>(items); + for(String item : items) + receiveItem(item); + } + + private Inventory() {} + + public static Inventory empty() { + return new Inventory(); + } + + private static final String UNLOCK_WORLD_ITEM_PREFIX = "unlock_world_"; + + public void receiveItem(String item) { + receivedItems.add(item); + switch(item) { + case "progressive_hint_count": + extraHintCount++; + break; + case "progressive_hint_word_length": + extraHintMaxLength++; + break; + case "progressive_hint_word_count": + //TODO get default value from game config + int currentMaxHintWordCount = extraHintWordCounts.stream().mapToInt(i -> i).max().orElse(1); + extraHintWordCounts.add(currentMaxHintWordCount+1); + break; + case "zero_hint": + extraHintWordCounts.add(0); + break; + case "infinity_hint": + extraHintWordCounts.add(-1); + break; + case "one_more_guess": + extraOneMoreGuess = true; + break; + case "end_guessing": + extraEndGuessing = true; + break; + case "only_one_green_green": + extraOnlyOneGreenGreen = true; + break; + case "sudden_death": + extraSuddenDeath = true; + break; + case "structured_grid": + extraStructuredGrid = true; + break; + case "hints_in_a_row": + extraHintsInARow = true; + break; + case "anyone_starts": + extraAnyoneStarts = true; + break; + case "filler": + // We do nothing if we receive a filler item + break; + default: + if (item.startsWith(UNLOCK_WORLD_ITEM_PREFIX)) { + String worldName = item.substring(UNLOCK_WORLD_ITEM_PREFIX.length()); + this.unlockedWorlds.add(worldName); + } else { + throw new IllegalArgumentException("Received unknown item : "+item); + } + } + } + + @RequiredArgsConstructor + @Converter + public static class InventoryConverter implements AttributeConverter { + + private final JsonNodeFactory jsn; + + @Override + public JsonNode convertToDatabaseColumn(Inventory inv) { + ArrayNode out = jsn.arrayNode(); + for(String s : inv.receivedItems) { + out.add(jsn.stringNode(s)); + } + return out; + } + + @Override + public Inventory convertToEntityAttribute(JsonNode json) { + ArrayNode arr = json.asArray(); + List items = new ArrayList<>(arr.size()); + for(JsonNode itemNode : arr) + items.add(itemNode.stringValue()); + return new Inventory(items); + } + + } +} diff --git a/src/main/java/com/bernard/nodecames/model/World.java b/src/main/java/com/bernard/nodecames/model/World.java index 849683b..571886a 100644 --- a/src/main/java/com/bernard/nodecames/model/World.java +++ b/src/main/java/com/bernard/nodecames/model/World.java @@ -26,9 +26,6 @@ public class World { @ManyToOne Game game; - // Config used when resetting this world - GridConfig initialConfig; - @ManyToOne(optional = true) Grid currentGrid; @@ -36,10 +33,9 @@ public class World { boolean locked; - public World(Game g, String name, GridConfig initConfig, Grid currentGrid, boolean won, boolean locked) { + public World(Game g, String name, Grid currentGrid, boolean won, boolean locked) { this.id = new WorldId(g.getId(), name); this.game = g; - this.initialConfig = initConfig; this.currentGrid = currentGrid; this.won = won; this.locked = locked; @@ -49,6 +45,15 @@ public class World { return this.getId().getName(); } + public WorldConfig getConfig() { + return this.getGame().getConfig().getWorlds().get(this.getName()); + } + + public GridConfig makeGridConfig() { + WorldConfig wc = getConfig(); + return wc.makeGridConfig(this.getGame().getInventory()); + } + @Data @NoArgsConstructor @AllArgsConstructor diff --git a/src/main/java/com/bernard/nodecames/model/WorldConfig.java b/src/main/java/com/bernard/nodecames/model/WorldConfig.java new file mode 100644 index 0000000..2b1620b --- /dev/null +++ b/src/main/java/com/bernard/nodecames/model/WorldConfig.java @@ -0,0 +1,205 @@ +package com.bernard.nodecames.model; + +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.IntStream; + +import com.bernard.nodecames.model.GridConfig.MutableOption; +import com.fasterxml.jackson.annotation.JsonProperty; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.NoArgsConstructor; +import lombok.Value; + +@Value +@NoArgsConstructor(force = true, access = AccessLevel.PRIVATE) +@AllArgsConstructor +public class WorldConfig { + + @JsonProperty("color_distribution") + private ColorDistribution colorDistribution; + + // Base is with no received items + // Max is either a cap when items are received + + @JsonProperty("base_hint_count") + private int baseHintCount; + @JsonProperty("max_hint_count") + private int maxHintCount; + + @JsonProperty("base_structured_grid") + private boolean baseStructuredGrid; + @JsonProperty("max_structured_grid") + private boolean maxStructuredGrid; + @JsonProperty("base_anyone_starts") + private boolean baseAnyoneStarts; + @JsonProperty("max_anyone_starts") + private boolean maxAnyoneStarts; + + @JsonProperty("base_hint_word_counts") + private Set baseHintWordCounts; + @JsonProperty("max_hint_word_counts") + private Set maxHintWordCounts; + @JsonProperty("base_one_more_guess") + private boolean baseOneMoreGuess; + @JsonProperty("max_one_more_guess") + private boolean maxOneMoreGuess; + @JsonProperty("base_hint_max_length") + private int baseHintMaxLength; + @JsonProperty("max_hint_max_length") + private int maxHintMaxLength; + @JsonProperty("base_end_guessing") + private boolean baseEndGuessing; + @JsonProperty("max_end_guessing") + private boolean maxEndGuessing; + @JsonProperty("base_only_one_green_green") + private boolean baseOnlyOneGreenGreen; + @JsonProperty("max_only_one_green_green") + private boolean maxOnlyOneGreenGreen; + @JsonProperty("base_sudden_death") + private boolean baseSuddenDeath; + @JsonProperty("max_sudden_death") + private boolean maxSuddenDeath; + @JsonProperty("base_hints_in_a_row") + private boolean baseHintsInARow; + @JsonProperty("max_hints_in_a_row") + private boolean maxHintsInARow; + + @JsonProperty("base_unlocked") + private boolean baseUnlocked; + + @JsonProperty("only_one_green_green_prevents_death") + private boolean onlyOneGreenGreenPreventsDeath; + + public boolean isStructuredGrid(Inventory i) { + return this.baseStructuredGrid || this.maxStructuredGrid && i.isExtraStructuredGrid(); + } + public boolean isAnyoneStarts(Inventory i) { + return this.baseAnyoneStarts || this.maxAnyoneStarts && i.isExtraAnyoneStarts(); + } + public boolean isEndGuessing(Inventory i) { + return this.baseEndGuessing || this.maxEndGuessing && i.isExtraEndGuessing(); + } + public boolean isOnlyOneGreenGreen(Inventory i) { + return this.baseOnlyOneGreenGreen || this.maxOnlyOneGreenGreen && i.isExtraOnlyOneGreenGreen(); + } + public boolean isSuddenDeath(Inventory i) { + return this.baseSuddenDeath || this.maxSuddenDeath && i.isExtraSuddenDeath(); + } + public boolean isOneMoreGuess(Inventory i) { + return this.baseOneMoreGuess || this.maxOneMoreGuess && i.isExtraOneMoreGuess(); + } + public boolean isHintsInARow(Inventory i) { + return this.baseHintsInARow || this.maxHintsInARow && i.isExtraHintsInARow(); + } + public int getHintCount(Inventory i) { + return Math.min(this.baseHintCount + i.getExtraHintCount(), this.maxHintCount); + } + public int getHintMaxLength(Inventory i) { + return Math.min(this.baseHintMaxLength + i.getExtraHintMaxLength(), this.maxHintMaxLength); + } + public Set getHintWordCounts(Inventory i) { + HashSet out = new HashSet<>(); + out.addAll(baseHintWordCounts); + out.addAll(i.getExtraHintWordCounts()); + out.retainAll(maxHintWordCounts); + return Collections.unmodifiableSet(out); + } + + /** + * Makes a GridConfig for the given inventory + */ + public GridConfig makeGridConfig(Inventory inv) { + return new GridConfig( + maxHintCount, + colorDistribution, + maxSuddenDeath, + maxOneMoreGuess, + onlyOneGreenGreenPreventsDeath, + isStructuredGrid(inv), + isAnyoneStarts(inv), + getHintCount(inv), + getHintWordCounts(inv), + isOneMoreGuess(inv), + getHintMaxLength(inv), + isEndGuessing(inv), + isOnlyOneGreenGreen(inv), + isSuddenDeath(inv), + isHintsInARow(inv) + ); + } + + /** + * Returns the set of mutable options to be modified according to this new inventory + */ + public Map updateConfig(GridConfig b, Inventory i) { + Map out = new EnumMap<>(MutableOption.class); + int hintCount = this.getHintCount(i); + if(b.getMaxHint() != hintCount) + out.put(MutableOption.MAX_HINT, hintCount); + Set availableHintWordCount = this.getHintWordCounts(i); + if(!b.getAvailableHintWordCount().containsAll(availableHintWordCount)) + out.put(MutableOption.AVAILABLE_HINT_WORD_COUNT, availableHintWordCount); + int hintMaxLength = this.getHintMaxLength(i); + if(b.getHintMaxLength() != hintMaxLength) + out.put(MutableOption.HINT_MAX_LENGTH, hintMaxLength); + boolean oneMoreGuess = this.isOneMoreGuess(i); + if(b.isOneMoreGuess() != oneMoreGuess) + out.put(MutableOption.ONE_MORE_GUESS, oneMoreGuess); + boolean endGuessing = this.isEndGuessing(i); + if(b.isEndGuessing() != endGuessing) + out.put(MutableOption.END_GUESSING, endGuessing); + boolean onlyOneGreenGreen = this.isOnlyOneGreenGreen(i); + if(b.isOnlyOneGreenGreen() != onlyOneGreenGreen) + out.put(MutableOption.ONLY_ONE_GREEN_GREEN, onlyOneGreenGreen); + boolean suddenDeath = this.isSuddenDeath(i); + if(b.isSuddenDeath() != suddenDeath) + out.put(MutableOption.SUDDEN_DEATH, suddenDeath); + boolean hintsInARow = this.isHintsInARow(i); + if(b.isHintsInARow() != hintsInARow) + out.put(MutableOption.HINTS_IN_A_ROW, hintsInARow); + return out; + } + + public static final WorldConfig zeroWithSize(ColorDistribution colors) { + return new WorldConfig( + colors, + 1, 15, + false, true, + false, true, + Set.of(1), IntStream.range(-1, colors.anyGreenCount()+1).mapToObj(i->i).collect(Collectors.toSet()), + false, true, + 3, 20, + false, true, + false, true, + false, true, + false, true, + true, false); + } + public static final WorldConfig classicalWithSize(ColorDistribution colors, int hintCount) { + int cardsToFind = colors.anyGreenCount(); + Set hintWordCounts = IntStream.range(-1, cardsToFind+1).mapToObj(i -> i).collect(Collectors.toSet()); + return new WorldConfig( + colors, + hintCount, hintCount, + true, true, + true, true, + hintWordCounts, hintWordCounts, + true, true, + 100, 100, + true, true, + true, true, + true, true, + false, false, + true, false); + } + + public World newWorld(Game g, String name) { + return new World(g, name, null, false, !baseUnlocked); + } +} diff --git a/src/main/java/com/bernard/nodecames/model/converter/ColorDistributionConverter.java b/src/main/java/com/bernard/nodecames/model/converter/ColorDistributionConverter.java new file mode 100644 index 0000000..0871db0 --- /dev/null +++ b/src/main/java/com/bernard/nodecames/model/converter/ColorDistributionConverter.java @@ -0,0 +1,22 @@ +package com.bernard.nodecames.model.converter; + +import org.springframework.stereotype.Service; + +import com.bernard.nodecames.model.ColorDistribution; + +import jakarta.persistence.AttributeConverter; + +@Service +public class ColorDistributionConverter implements AttributeConverter { + + @Override + public String convertToDatabaseColumn(ColorDistribution attribute) { + return attribute.toString(); + } + + @Override + public ColorDistribution convertToEntityAttribute(String dbData) { + return ColorDistribution.ofString(dbData); + } + +} diff --git a/src/main/java/com/bernard/nodecames/model/converter/GameConfigConverter.java b/src/main/java/com/bernard/nodecames/model/converter/GameConfigConverter.java new file mode 100644 index 0000000..61302e3 --- /dev/null +++ b/src/main/java/com/bernard/nodecames/model/converter/GameConfigConverter.java @@ -0,0 +1,42 @@ +package com.bernard.nodecames.model.converter; + +import java.util.Map; +import java.util.Map.Entry; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import com.bernard.nodecames.model.GameConfig; +import com.bernard.nodecames.model.WorldConfig; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.AllArgsConstructor; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; + +@AllArgsConstructor +@Converter +@Service +public class GameConfigConverter implements AttributeConverter { + + private final JsonNodeFactory jsn; + private final WorldConfigConverter wcc; + + @Override + public JsonNode convertToDatabaseColumn(GameConfig gc) { + ObjectNode o = jsn.objectNode(); + for(Entry e : gc.getWorlds().entrySet()) + o.set(e.getKey(), wcc.convertToDatabaseColumn(e.getValue())); + return o; + } + + @Override + public GameConfig convertToEntityAttribute(JsonNode json) { + Map configs = json.asObject().propertyStream() + .collect(Collectors.toMap(Entry::getKey, e -> wcc.convertToEntityAttribute(e.getValue()))); + return new GameConfig(configs); + } + +} \ No newline at end of file diff --git a/src/main/java/com/bernard/nodecames/model/converter/JsonNodeConverter.java b/src/main/java/com/bernard/nodecames/model/converter/JsonNodeConverter.java new file mode 100644 index 0000000..27014f3 --- /dev/null +++ b/src/main/java/com/bernard/nodecames/model/converter/JsonNodeConverter.java @@ -0,0 +1,25 @@ +package com.bernard.nodecames.model.converter; + +import org.springframework.stereotype.Service; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.ObjectMapper; + +@Service +@Converter +public class JsonNodeConverter implements AttributeConverter { + + ObjectMapper om = new ObjectMapper(); + + @Override + public String convertToDatabaseColumn(JsonNode attribute) { + return om.writeValueAsString(attribute); + } + + @Override + public JsonNode convertToEntityAttribute(String dbData) { + return om.readTree(dbData); + } +} diff --git a/src/main/java/com/bernard/nodecames/model/converter/WorldConfigConverter.java b/src/main/java/com/bernard/nodecames/model/converter/WorldConfigConverter.java new file mode 100644 index 0000000..052c45e --- /dev/null +++ b/src/main/java/com/bernard/nodecames/model/converter/WorldConfigConverter.java @@ -0,0 +1,92 @@ +package com.bernard.nodecames.model.converter; + +import java.util.Set; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Service; + +import com.bernard.nodecames.model.WorldConfig; +import com.bernard.nodecames.util.Utils; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Converter; +import lombok.AllArgsConstructor; +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.JsonNodeFactory; +import tools.jackson.databind.node.ObjectNode; + +@AllArgsConstructor +@Converter +@Service +public class WorldConfigConverter implements AttributeConverter { + private final JsonNodeFactory jsn; + private final ColorDistributionConverter cdwdc; + + public JsonNode hintWordCountsToJson(Set hintWordCounts) { + return hintWordCounts.stream().sorted().map(jsn::numberNode).collect(Utils.jsonNodeCollector(jsn)); + } + public Set hintWordCountsOfJson(JsonNode json) { + return json.asArray().valueStream().map(JsonNode::intValue).collect(Collectors.toSet()); + } + + @Override + public JsonNode convertToDatabaseColumn(WorldConfig wc) { + ObjectNode out = jsn.objectNode(); + + out.set("color_distribution", jsn.stringNode(cdwdc.convertToDatabaseColumn(wc.getColorDistribution()))); + out.set("base_hint_count", jsn.numberNode(wc.getBaseHintCount())); + out.set("max_hint_count", jsn.numberNode(wc.getMaxHintCount())); + out.set("base_structured_grid", jsn.booleanNode(wc.isBaseStructuredGrid())); + out.set("max_structured_grid", jsn.booleanNode(wc.isMaxStructuredGrid())); + out.set("base_anyone_starts", jsn.booleanNode(wc.isBaseAnyoneStarts())); + out.set("max_anyone_starts", jsn.booleanNode(wc.isMaxAnyoneStarts())); + out.set("base_hint_word_counts", hintWordCountsToJson(wc.getBaseHintWordCounts())); + out.set("max_hint_word_counts", hintWordCountsToJson(wc.getMaxHintWordCounts())); + out.set("base_one_more_guess", jsn.booleanNode(wc.isBaseOneMoreGuess())); + out.set("max_one_more_guess", jsn.booleanNode(wc.isMaxOneMoreGuess())); + out.set("base_hint_max_length", jsn.numberNode(wc.getBaseHintMaxLength())); + out.set("max_hint_max_length", jsn.numberNode(wc.getMaxHintMaxLength())); + out.set("base_end_guessing", jsn.booleanNode(wc.isBaseEndGuessing())); + out.set("max_end_guessing", jsn.booleanNode(wc.isMaxEndGuessing())); + out.set("base_only_one_green_green", jsn.booleanNode(wc.isBaseOnlyOneGreenGreen())); + out.set("max_only_one_green_green", jsn.booleanNode(wc.isMaxOnlyOneGreenGreen())); + out.set("base_sudden_death", jsn.booleanNode(wc.isBaseSuddenDeath())); + out.set("max_sudden_death", jsn.booleanNode(wc.isMaxSuddenDeath())); + out.set("base_hints_in_a_row", jsn.booleanNode(wc.isBaseHintsInARow())); + out.set("max_hints_in_a_row", jsn.booleanNode(wc.isMaxHintsInARow())); + out.set("base_unlocked", jsn.booleanNode(wc.isBaseUnlocked())); + out.set("only_one_green_green_prevents_death", jsn.booleanNode(wc.isOnlyOneGreenGreenPreventsDeath())); + + return out; + } + + @Override + public WorldConfig convertToEntityAttribute(JsonNode json) { + ObjectNode o = json.asObject(); + return new WorldConfig( + cdwdc.convertToEntityAttribute(o.get("color_distribution").stringValue()), + o.get("base_hint_count").intValue(), + o.get("max_hint_count").intValue(), + o.get("base_structured_grid").booleanValue(), + o.get("max_structured_grid").booleanValue(), + o.get("base_anyone starts").booleanValue(), + o.get("max_anyone_starts").booleanValue(), + hintWordCountsOfJson(o.get("base_hint_word_counts")), + hintWordCountsOfJson(o.get("max_hint_word_counts")), + o.get("base_one_more_guess").booleanValue(), + o.get("max_one_more_guess").booleanValue(), + o.get("base_hint_max_length").intValue(), + o.get("max_hint_max_length").intValue(), + o.get("base_end_guessing").booleanValue(), + o.get("max_end_guessing").booleanValue(), + o.get("base_only_one_green_green").booleanValue(), + o.get("max_only_one_green_green").booleanValue(), + o.get("base_sudden_death").booleanValue(), + o.get("max_sudden_death").booleanValue(), + o.get("base_hints_in_a_row").booleanValue(), + o.get("max_hints_in_a_row").booleanValue(), + o.get("base_unlocked").booleanValue(), + o.get("only_one_green_green_prevents_death").booleanValue() + ); + } +} \ No newline at end of file diff --git a/src/main/java/com/bernard/nodecames/util/Utils.java b/src/main/java/com/bernard/nodecames/util/Utils.java new file mode 100644 index 0000000..f2204a6 --- /dev/null +++ b/src/main/java/com/bernard/nodecames/util/Utils.java @@ -0,0 +1,47 @@ +package com.bernard.nodecames.util; + +import java.util.EnumSet; +import java.util.Set; +import java.util.function.BiConsumer; +import java.util.function.BinaryOperator; +import java.util.function.Function; +import java.util.function.Supplier; +import java.util.stream.Collector; + +import tools.jackson.databind.JsonNode; +import tools.jackson.databind.node.ArrayNode; +import tools.jackson.databind.node.JsonNodeFactory; + +public class Utils { + + private Utils() {} + + public static final Collector jsonNodeCollector(JsonNodeFactory jsn) { + return new Collector() { + @Override + public Supplier supplier() { + return jsn::arrayNode; + } + @Override + public BiConsumer accumulator() { + return ArrayNode::add; + } + @Override + public BinaryOperator combiner() { + return (a,b) -> { + a.addAll(b); + return a; + }; + } + @Override + public Function finisher() { + return Function.identity(); + } + @Override + public Set characteristics() { + return EnumSet.of(Characteristics.IDENTITY_FINISH); + } + }; + } + +} diff --git a/src/main/resources/db/migration/V1_2_0__add_game_config.sql b/src/main/resources/db/migration/V1_2_0__add_game_config.sql new file mode 100644 index 0000000..c0c49ea --- /dev/null +++ b/src/main/resources/db/migration/V1_2_0__add_game_config.sql @@ -0,0 +1,19 @@ +alter table if exists game add column config json; +alter table if exists game add column inventory json; +alter table if exists grid rename column word_distribution to color_distribution; + +alter table if exists world drop column anyone_starts; +alter table if exists world drop column available_hint_word_count; +alter table if exists world drop column end_guessing; +alter table if exists world drop column hint_max_length; +alter table if exists world drop column hints_inarow; +alter table if exists world drop column max_hint; +alter table if exists world drop column max_max_hint; +alter table if exists world drop column maybe_one_more_guess; +alter table if exists world drop column maybe_sudden_death; +alter table if exists world drop column one_more_guess; +alter table if exists world drop column only_one_green_green; +alter table if exists world drop column only_one_green_green_prevent_death; +alter table if exists world drop column structured_grid; +alter table if exists world drop column sudden_death; +alter table if exists world drop column word_distribution; diff --git a/src/main/resources/static/css/style.css b/src/main/resources/static/css/style.css index d83738f..aa282a0 100644 --- a/src/main/resources/static/css/style.css +++ b/src/main/resources/static/css/style.css @@ -14,4 +14,8 @@ main{ font-size: 24px; flex-direction: column; margin-top: 115px; +} + +main section { + padding: 1em; } \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html index a0f5d72..1d82228 100644 --- a/src/main/resources/templates/index.html +++ b/src/main/resources/templates/index.html @@ -17,6 +17,10 @@
+
diff --git a/src/main/resources/templates/map.html b/src/main/resources/templates/map.html index 59c63e4..9304b2d 100644 --- a/src/main/resources/templates/map.html +++ b/src/main/resources/templates/map.html @@ -10,15 +10,26 @@
- +
+

Worlds


+ +
+
+

Received Items


+
    +
  • + +
  • +
+
\ No newline at end of file diff --git a/src/test/java/com/bernard/nodecames/JsonTests.java b/src/test/java/com/bernard/nodecames/JsonTests.java new file mode 100644 index 0000000..8465a05 --- /dev/null +++ b/src/test/java/com/bernard/nodecames/JsonTests.java @@ -0,0 +1,30 @@ +package com.bernard.nodecames; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.api.Test; + +import com.bernard.nodecames.model.GameConfig; + +import lombok.RequiredArgsConstructor; +import tools.jackson.databind.ObjectMapper; + +@RequiredArgsConstructor +class JsonTests { + + ObjectMapper om = new ObjectMapper(); + + @Test + void serializeGameConfig() { + GameConfig g = GameConfig.DEFAULT_GAME_CONFIG; + + String json = om.writeValueAsString(g); + + System.out.println("Got json : "+json); + + GameConfig g2 = om.readValue(json, GameConfig.class); + + assertEquals(g, g2); + } + +}