World config and inventories

This commit is contained in:
Mysaa Java
2026-10-08 11:46:28 +02:00
parent 466da86969
commit 741f60bb3b
24 changed files with 976 additions and 352 deletions
+10
View File
@@ -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} = {
@@ -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";
}
@@ -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<CardColors, Integer> 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()));
@@ -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
@@ -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<WorldId, Set<WorldObserver>> 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<String, GridConfig> 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<World> 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<MutableOption, Object> updates = w.getConfig().updateConfig(grid.getConfig(), inv);
log.debug("Updating option for world {} : {}", w.getName(), updates);
for(Entry<MutableOption, Object> 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<World> 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<World> 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<World> 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<Integer> 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<Integer> newWordCounts = new HashSet<>(currentWordCounts);
newWordCounts.add(i);
unlockMutable(game, MutableOption.AVAILABLE_HINT_WORD_COUNT, newWordCounts);
gRepository.save(g);
}
@AllArgsConstructor
@@ -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<CardColors> origDistrib = gc.getWordDistributionAsList();
String[] words = dictionnaries.randomWords(gc.getColorDistribution().wordCount(), r);
List<CardColors> origDistrib = gc.getColorDistribution().asList();
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())
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);
}
@@ -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<CardColors, Integer> colors;
public List<CardColors> asList() {
List<CardColors> out = new ArrayList<>();
for(Entry<CardColors, Integer> e : this.colors.entrySet()) {
for(int i=0;i<e.getValue();i++) {
out.add(e.getKey());
}
}
return Collections.unmodifiableList(out);
}
public int anyGreenCount() {
return this.colors.get(CardColors.GG) +
this.colors.get(CardColors.WG) +
this.colors.get(CardColors.GW) +
this.colors.get(CardColors.GB) +
this.colors.get(CardColors.BG);
}
public int wordCount() {
return (int)this.colors.values().stream().collect(Collectors.summarizingInt(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<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);
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<ColorDistribution> {
public Deserializer() {
super(ColorDistribution.class);
}
@Override
public ColorDistribution deserialize(JsonParser p, DeserializationContext ctxt) throws JacksonException {
String s = p.getString();
return ColorDistribution.ofString(s);
}
}
}
@@ -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<World> worlds = null;
public Game(UUID id, String name, GameConfig config) {
this();
this.setId(id);
this.setName(name);
this.setConfig(config);
}
public static final Map<String, GridConfig> DEFAULT_WORLDS_CONFIG = Map.ofEntries(
Map.entry("1-1", GridConfig.zeroWithSize(GridConfig.CUSTOM6)),
Map.entry("1-2", GridConfig.zeroWithSize(GridConfig.CUSTOM9)),
Map.entry("1-3", GridConfig.zeroWithSize(GridConfig.CUSTOM9)),
Map.entry("2-1", GridConfig.zeroWithSize(GridConfig.CUSTOM12)),
Map.entry("2-2", GridConfig.zeroWithSize(GridConfig.CUSTOM16)),
Map.entry("2-3", GridConfig.zeroWithSize(GridConfig.CUSTOM16)),
Map.entry("3-1", GridConfig.zeroWithSize(GridConfig.CUSTOM20)),
Map.entry("3-2", GridConfig.zeroWithSize(GridConfig.CLASSICAL25)),
Map.entry("3-3", GridConfig.zeroWithSize(GridConfig.CLASSICAL25)),
Map.entry("4-1", GridConfig.zeroWithSize(GridConfig.CUSTOM30)),
Map.entry("4-2", GridConfig.zeroWithSize(GridConfig.CUSTOM36)),
Map.entry("4-3", GridConfig.zeroWithSize(GridConfig.CUSTOM36))
);
}
@@ -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<String, WorldConfig> 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))
));
}
@@ -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<List<GameEvent>, JsonNode> {
JsonNodeFactory jsn;
private final JsonNodeFactory jsn;
@Override
public JsonNode convertToDatabaseColumn(List<GameEvent> attribute) {
@@ -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<CardColors, Integer> CUSTOM6 =
makeDistribution(0, 0, 2, 1, 1, 0);
public static final Map<CardColors, Integer> CUSTOM9 =
makeDistribution(0, 1, 1, 2, 2, 1);
public static final Map<CardColors, Integer> CUSTOM12 =
makeDistribution(1, 1, 1, 2, 3, 1);
public static final Map<CardColors, Integer> CUSTOM16 =
makeDistribution(1, 1, 2, 2, 5, 1);
public static final Map<CardColors, Integer> CUSTOM20 =
makeDistribution(1, 1, 4, 2, 5, 1);
public static final Map<CardColors, Integer> CLASSICAL25 =
makeDistribution(1, 1, 5, 3, 7, 1);
public static final Map<CardColors, Integer> CUSTOM30 =
makeDistribution(2, 2, 5, 3, 7, 1);
public static final Map<CardColors, Integer> CUSTOM36 =
makeDistribution(2, 2, 6, 5, 9, 2);
public static final Set<Integer> 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<CardColors, Integer> 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<CardColors, Integer> 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<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 (int)this.getWordDistribution().values().stream().collect(Collectors.summarizingInt(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<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);
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<CardColors, Integer> 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<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
));
}
}
}
@@ -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<String> receivedItems = new ArrayList<>();
private int extraHintCount = 0;
private boolean extraStructuredGrid = false;
private boolean extraAnyoneStarts = false;
private Set<Integer> 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<String> unlockedWorlds = new HashSet<>();
public Inventory(List<String> 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<Inventory, JsonNode> {
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<String> items = new ArrayList<>(arr.size());
for(JsonNode itemNode : arr)
items.add(itemNode.stringValue());
return new Inventory(items);
}
}
}
@@ -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
@@ -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<Integer> baseHintWordCounts;
@JsonProperty("max_hint_word_counts")
private Set<Integer> 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<Integer> getHintWordCounts(Inventory i) {
HashSet<Integer> 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<MutableOption, Object> updateConfig(GridConfig b, Inventory i) {
Map<MutableOption, Object> out = new EnumMap<>(MutableOption.class);
int hintCount = this.getHintCount(i);
if(b.getMaxHint() != hintCount)
out.put(MutableOption.MAX_HINT, hintCount);
Set<Integer> 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<Integer> 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);
}
}
@@ -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<ColorDistribution, String> {
@Override
public String convertToDatabaseColumn(ColorDistribution attribute) {
return attribute.toString();
}
@Override
public ColorDistribution convertToEntityAttribute(String dbData) {
return ColorDistribution.ofString(dbData);
}
}
@@ -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<GameConfig, JsonNode> {
private final JsonNodeFactory jsn;
private final WorldConfigConverter wcc;
@Override
public JsonNode convertToDatabaseColumn(GameConfig gc) {
ObjectNode o = jsn.objectNode();
for(Entry<String,WorldConfig> e : gc.getWorlds().entrySet())
o.set(e.getKey(), wcc.convertToDatabaseColumn(e.getValue()));
return o;
}
@Override
public GameConfig convertToEntityAttribute(JsonNode json) {
Map<String, WorldConfig> configs = json.asObject().propertyStream()
.collect(Collectors.toMap(Entry::getKey, e -> wcc.convertToEntityAttribute(e.getValue())));
return new GameConfig(configs);
}
}
@@ -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<JsonNode, String> {
ObjectMapper om = new ObjectMapper();
@Override
public String convertToDatabaseColumn(JsonNode attribute) {
return om.writeValueAsString(attribute);
}
@Override
public JsonNode convertToEntityAttribute(String dbData) {
return om.readTree(dbData);
}
}
@@ -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<WorldConfig, JsonNode> {
private final JsonNodeFactory jsn;
private final ColorDistributionConverter cdwdc;
public JsonNode hintWordCountsToJson(Set<Integer> hintWordCounts) {
return hintWordCounts.stream().sorted().map(jsn::numberNode).collect(Utils.jsonNodeCollector(jsn));
}
public Set<Integer> 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()
);
}
}
@@ -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<JsonNode,ArrayNode,ArrayNode> jsonNodeCollector(JsonNodeFactory jsn) {
return new Collector<JsonNode,ArrayNode,ArrayNode>() {
@Override
public Supplier<ArrayNode> supplier() {
return jsn::arrayNode;
}
@Override
public BiConsumer<ArrayNode, JsonNode> accumulator() {
return ArrayNode::add;
}
@Override
public BinaryOperator<ArrayNode> combiner() {
return (a,b) -> {
a.addAll(b);
return a;
};
}
@Override
public Function<ArrayNode, ArrayNode> finisher() {
return Function.identity();
}
@Override
public Set<Characteristics> characteristics() {
return EnumSet.of(Characteristics.IDENTITY_FINISH);
}
};
}
}
@@ -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;
+4
View File
@@ -14,4 +14,8 @@ main{
font-size: 24px;
flex-direction: column;
margin-top: 115px;
}
main section {
padding: 1em;
}
+4
View File
@@ -17,6 +17,10 @@
<form method="GET" th:action="@{/create-game}">
<label for="create-game-name">Name : </label>
<input type="text" id="create-game-name" name="name"/>
<select name="type">
<option value="default">Default</option>
<option value="regular">Regular</option>
</select>
<input type="submit" value="Create game"/>
</form>
+20 -9
View File
@@ -10,15 +10,26 @@
</head>
<body>
<main>
<ul>
<li th:each="world : ${worlds}">
<!-- TODO remove this duplication of World span -->
<span th:if="${world.locked}"> World <span th:text="${world.name}"/> </span>
<a th:if="!${world.locked}" th:href="@{/game/{gid}/grid/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">World <span th:text="${world.name}"/></a>
<a th:if="!${world.locked}" th:href="@{/game/{gid}/reset/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">reset</a>
<span th:if="${world.won}">Gagné !</span>
</li>
</ul>
<section>
<h2>Worlds</h2><br/>
<ul>
<li th:each="world : ${worlds}">
<!-- TODO remove this duplication of World span -->
<span th:if="${world.locked}"> World <span th:text="${world.name}"/> </span>
<a th:if="!${world.locked}" th:href="@{/game/{gid}/grid/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">World <span th:text="${world.name}"/></a>
<a th:if="!${world.locked}" th:href="@{/game/{gid}/reset/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">reset</a>
<span th:if="${world.won}">Gagné !</span>
</li>
</ul>
</section>
<section>
<h2>Received Items</h2><br/>
<ul>
<li th:each="item : ${items}">
<span th:text="${item}"/>
</li>
</ul>
</section>
</main>
</body>
</html>
@@ -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);
}
}