Compare commits

..
3 Commits
Author SHA1 Message Date
Mysaa Java d23aedb924 Added logic, can still be locked 2026-10-09 14:37:08 +02:00
Mysaa Java 741f60bb3b World config and inventories 2026-10-08 11:46:28 +02:00
Mysaa Java 466da86969 Observer pattern & Archipelago 2026-09-29 21:32:22 +02:00
42 changed files with 1826 additions and 491 deletions
+4
View File
@@ -0,0 +1,4 @@
## License
Dictionaries taken from [this project](https://github.com/LandazuriPaul/Codenames)
+1
View File
@@ -31,6 +31,7 @@ dependencies {
implementation 'org.flywaydb:flyway-core:_' implementation 'org.flywaydb:flyway-core:_'
implementation 'org.flywaydb:flyway-database-postgresql:_' implementation 'org.flywaydb:flyway-database-postgresql:_'
implementation 'jakarta.validation:jakarta.validation-api:_' implementation 'jakarta.validation:jakarta.validation-api:_'
implementation 'io.github.archipelagomw:Java-Client:_'
//TODO use webjars //TODO use webjars
runtimeOnly 'org.webjars:jquery:_' runtimeOnly 'org.webjars:jquery:_'
+29
View File
@@ -39,11 +39,37 @@
echo "Launching NodeCames" echo "Launching NodeCames"
${jdk}/bin/java -jar ${nodecames}/nodecames-${version}.jar --spring.datasource.url=$NODECAMES_DATABASE --spring.datasource.password=$NODECAMES_PASSWORD "$@" ${jdk}/bin/java -jar ${nodecames}/nodecames-${version}.jar --spring.datasource.url=$NODECAMES_DATABASE --spring.datasource.password=$NODECAMES_PASSWORD "$@"
''; '';
postgres = pkgs.postgresql_18;
# TODO make his more nix
postgres-server = pkgs.writeShellScriptBin "postgres-server" '' postgres-server = pkgs.writeShellScriptBin "postgres-server" ''
mkdir -p .postgres mkdir -p .postgres
${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 & ${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"
chmod +w "$TMP_HOME/.local/share/Archipelago/Players/"
ln -s ${./src/main/resources/apworld/test.yml} "$TMP_HOME/.local/share/Archipelago/Players/Tester.yaml"
HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago "$@"
'';
};
archipelago-host = pkgs.writeShellApplication {
name = "archipelago-host";
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"
chmod +w "$TMP_HOME/.local/share/Archipelago/Players/"
ln -s ${./src/main/resources/apworld/test.yml} "$TMP_HOME/.local/share/Archipelago/Players/Tester.yaml"
HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago Generate
GENFILE=$(find "$TMP_HOME/.local/share/Archipelago/output/" -name "*.zip")
HOME=$TMP_HOME ${pkgs.archipelago}/bin/archipelago Host "$GENFILE"
'';
};
in { in {
packages.${system} = { packages.${system} = {
default = self.packages.${system}.nodecames; default = self.packages.${system}.nodecames;
@@ -54,6 +80,8 @@
default = self.apps.${system}.nodecames; default = self.apps.${system}.nodecames;
nodecames = { type = "app"; program = "${nodecames-launcher}/bin/nodecames"; }; nodecames = { type = "app"; program = "${nodecames-launcher}/bin/nodecames"; };
postgres-server = { type = "app"; program = "${postgres-server}/bin/postgres-server"; }; postgres-server = { type = "app"; program = "${postgres-server}/bin/postgres-server"; };
archipelago = { type = "app"; program = "${test-archipelago}/bin/archipelago"; };
archipelago-host = { type = "app"; program = "${archipelago-host}/bin/archipelago-host"; };
}; };
devShells.${system} = { devShells.${system} = {
@@ -61,6 +89,7 @@
packages = [ packages = [
jdk jdk
gradle gradle
postgres
(pkgs.vscode-with-extensions.override { (pkgs.vscode-with-extensions.override {
vscode = pkgs.vscodium; vscode = pkgs.vscodium;
vscodeExtensions = with pkgs.vscode-extensions; [ vscodeExtensions = with pkgs.vscode-extensions; [
+40 -8
View File
@@ -103,17 +103,30 @@
<sha256 value="19889dbdf1b254b2601a5ee645b8147a974644882297684c798afe5d63d78dfe" origin="Generated by Gradle"/> <sha256 value="19889dbdf1b254b2601a5ee645b8147a974644882297684c798afe5d63d78dfe" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.38.0"> <component group="com.google.code.gson" name="gson" version="2.13.2">
<artifact name="error_prone_annotations-2.38.0.jar"> <artifact name="gson-2.13.2.jar">
<sha256 value="6661d5335090a5fc61dd869d2095bc6c1e2156e3aa47a6e4ababdf64c99a7889" origin="Generated by Gradle"/> <sha256 value="dd0ce1b55a3ed2080cb70f9c655850cda86c206862310009dcb5e5c95265a5e0" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="error_prone_annotations-2.38.0.pom"> <artifact name="gson-2.13.2.pom">
<sha256 value="3007bef8aff3ae8ea12d81c3feacb4f15979b2cf5c3e38fc9189a98dea141167" origin="Generated by Gradle"/> <sha256 value="3aa06aa7c0f9af092961a42d09578e4324be146348a0ee6ed47857f7c2677b76" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.38.0"> <component group="com.google.code.gson" name="gson-parent" version="2.13.2">
<artifact name="error_prone_parent-2.38.0.pom"> <artifact name="gson-parent-2.13.2.pom">
<sha256 value="e62458a6a3e63081bc7c57b3c0fac9f04f76ce32f606532db69fe2b3d47b934c" origin="Generated by Gradle"/> <sha256 value="83ab528a9d50fd76aeb8ad6f727b3ee9cb766586255774ed16ca8c4c76d9dacd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.41.0">
<artifact name="error_prone_annotations-2.41.0.jar">
<sha256 value="a56e782b5b50811ac204073a355a21d915a2107fce13ec711331ad036f660fcc" origin="Generated by Gradle"/>
</artifact>
<artifact name="error_prone_annotations-2.41.0.pom">
<sha256 value="a151df1e2e0b48618d8b06a180748a29b3abb39b1b2396f6a1c879a727488c6e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.41.0">
<artifact name="error_prone_parent-2.41.0.pom">
<sha256 value="c538388d760a5c1c98dcf06f6ed3cfe5f11a651827db5cbd2ed8288c795cad42" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="com.google.protobuf" name="protobuf-bom" version="4.35.1"> <component group="com.google.protobuf" name="protobuf-bom" version="4.35.1">
@@ -280,6 +293,17 @@
<sha256 value="7c490839bb11df9ae7c37f9c31994a4b423bd826245d34abe6c864f5e61d46e9" origin="Generated by Gradle"/> <sha256 value="7c490839bb11df9ae7c37f9c31994a4b423bd826245d34abe6c864f5e61d46e9" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="io.github.archipelagomw" name="Java-Client" version="0.2.1">
<artifact name="Java-Client-0.2.1.jar">
<sha256 value="9f3ff0a1a3cd99cbc9090e819c00d3890b775e17cbac47201677972953337a33" origin="Generated by Gradle"/>
</artifact>
<artifact name="Java-Client-0.2.1.module">
<sha256 value="c4b35dff4ed88b82e4a9133efdfbc6efb6a23bca3f9124e1b606a37b10240c4f" origin="Generated by Gradle"/>
</artifact>
<artifact name="Java-Client-0.2.1.pom">
<sha256 value="812e1984b148cb856afb290029c224de7dfb99a11a28dd9b6a402d2ed761dd06" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-bom" version="1.83.1"> <component group="io.grpc" name="grpc-bom" version="1.83.1">
<artifact name="grpc-bom-1.83.1.pom"> <artifact name="grpc-bom-1.83.1.pom">
<sha256 value="58cc1c0c0e6dc3aa28a6b8f6c8cc1cbdbdc47843cfba282d07f694c2b1af4f8b" origin="Generated by Gradle"/> <sha256 value="58cc1c0c0e6dc3aa28a6b8f6c8cc1cbdbdc47843cfba282d07f694c2b1af4f8b" origin="Generated by Gradle"/>
@@ -1044,6 +1068,14 @@
<sha256 value="aeabde04eca66e1ac30f9df3cc36c9b07a25a37c52043b33418e274ce5a23616" origin="Generated by Gradle"/> <sha256 value="aeabde04eca66e1ac30f9df3cc36c9b07a25a37c52043b33418e274ce5a23616" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.java-websocket" name="Java-WebSocket" version="1.6.0">
<artifact name="Java-WebSocket-1.6.0.jar">
<sha256 value="eae29213e4f16515639c28957200f011b3967fffcada1962cf0255d24919c22f" origin="Generated by Gradle"/>
</artifact>
<artifact name="Java-WebSocket-1.6.0.pom">
<sha256 value="a8e84d553c34d793e2ea7e4a54b073f44ddb0bb442f4dd6fad0cecd755d1ceac" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jboss" name="jboss-parent" version="52"> <component group="org.jboss" name="jboss-parent" version="52">
<artifact name="jboss-parent-52.pom"> <artifact name="jboss-parent-52.pom">
<sha256 value="0615b429e3a21a0d6bd925d09ede2937892034272b4cd04bb9ab617d57e72b42" origin="Generated by Gradle"/> <sha256 value="0615b429e3a21a0d6bd925d09ede2937892034272b4cd04bb9ab617d57e72b42" origin="Generated by Gradle"/>
+51
View File
@@ -0,0 +1,51 @@
import re
MAX_WORLD_COUNT=100
MAX_WORD_PER_WORLD=99
def parseWordDistribution(s):
components = s.split(';')
out = {}
for comp in components:
p = re.match("^([GWB])([GWB])([0-9]+)$", comp)
if not p:
raise ValueError("Invalid component of word distribution")
colora = p.group(1)
colorb = p.group(2)
count = int(p.group(3))
if (colora + colorb in out):
raise ValueError(f"Component {colora}{colorb} defined twice")
out[colora + colorb] = count
return out
filler_item = "filler"
ITEM_NAME_TO_ID = {
"progressive_hint_count" : 1,
"zero_hint" : 2,
"infinity_hint" : 3,
"one_more_guess" : 4,
"hint_max_length" : 5,
"end_guessing" : 6,
"only_one_green_green" : 7,
"sudden_death" : 8,
"structured_grid" : 9,
"hints_in_a_row" : 10,
"anyone_starts" : 11,
"progressive_hint_word_count": 12,
"progressive_hint_word_length": 13,
"filler": 14
}
for w in range(MAX_WORLD_COUNT):
ITEM_NAME_TO_ID[f"unlock_world_{w+1}"] = 100+w
def location_id_of_world_and_word(world, word):
return 10000 + (world-1)*(MAX_WORD_PER_WORLD+1) + word
def world_and_word_of_location(id):
world = (id - 10000)//(MAX_WORD_PER_WORLD+1) + 1
word = (id - 10000) % (MAX_WORD_PER_WORLD+1)
return (world, word)
def location_name_of_world_and_word(world, word):
return f"World {world} word {word}"
LOCATION_NAME_TO_ID = {location_name_of_world_and_word(world,word) : location_id_of_world_and_word(world,word) for world in range(1,MAX_WORLD_COUNT+1) for word in range(0,MAX_WORD_PER_WORLD+1)}
+19 -22
View File
@@ -1,48 +1,38 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from BaseClasses import Item, ItemClassification
if TYPE_CHECKING: if TYPE_CHECKING:
from .world import NodecamesWorld from .world import NodecamesWorld
ITEM_NAME_TO_ID = { from BaseClasses import Item, ItemClassification
"progressive_hint_count" : 1,
"zero_hint" : 2,
"infinity_hint" : 3,
"one_more_guess" : 4,
"hint_max_length" : 5,
"end_guessing" : 6,
"only_one_green_green" : 7,
"sudden_death" : 8,
"structured_grid" : 9,
"hints_in_a_row" : 10,
"anyone_starts" : 11,
"filler": 12
}
for w in range(1,12+1):
ITEM_NAME_TO_ID[f"unlock_world_{w}"] = 100+w
from . import const as c
class NodecamesItem(Item): class NodecamesItem(Item):
game = "Nodecames" game = "Nodecames"
def get_random_filler_item_name(world: NodecamesWorld) -> str: def get_random_filler_item_name(world: NodecamesWorld) -> str:
return "filler" return c.filler_item
def create_item_with_correct_classification( def create_item_with_correct_classification(
world: NodecamesWorld, name: str world: NodecamesWorld, name: str
) -> NodecamesItem: ) -> NodecamesItem:
return NodecamesItem(name, ItemClassification.progression, ITEM_NAME_TO_ID[name], world.player) classification = ItemClassification.progression
if name == c.filler_item:
classification = ItemClassification.filler
return NodecamesItem(name, classification, c.ITEM_NAME_TO_ID[name], world.player)
def create_all_items(world: NodecamesWorld) -> None: def create_all_items(world: NodecamesWorld) -> None:
itempool: list[Item] = [] itempool: list[Item] = []
hintItemCount = world.options.max_hint_count - world.options.max_hint_length hintItemCount = world.options.max_hint_count - world.options.start_hint_count
itempool += [world.create_item("progressive_hint_count") for _ in range(hintItemCount)] itempool += [world.create_item("progressive_hint_count") for _ in range(hintItemCount)]
hintWordLengthItemCount = world.options.max_hint_length - world.options.start_hint_length
itempool += [world.create_item("progressive_hint_word_length") for _ in range(hintWordLengthItemCount)]
hintWordCountItemCount = world.options.max_hint_word_count - world.options.start_hint_word_count
itempool += [world.create_item("progressive_hint_word_count") for _ in range(hintWordCountItemCount)]
itempool += [ itempool += [
world.create_item("zero_hint"), world.create_item("zero_hint"),
world.create_item("infinity_hint"), world.create_item("infinity_hint"),
@@ -55,9 +45,16 @@ def create_all_items(world: NodecamesWorld) -> None:
world.create_item("hints_in_a_row"), world.create_item("hints_in_a_row"),
world.create_item("anyone_starts"), world.create_item("anyone_starts"),
] ]
for wi,wn in enumerate(world.options.worlds.keys()):
w = world.options.worlds[wn]
if w['locked']:
itempool.append(world.create_item(f"unlock_world_{wi}"))
to_complete = len(world.multiworld.get_unfilled_locations(world.player)) - len(itempool) to_complete = len(world.multiworld.get_unfilled_locations(world.player)) - len(itempool)
print(to_complete,"items to fill")
itempool += [world.create_filler() for _ in range(to_complete)] itempool += [world.create_filler() for _ in range(to_complete)]
world.multiworld.itempool += itempool world.multiworld.itempool += itempool
-67
View File
@@ -1,67 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from BaseClasses import ItemClassification, Location
from . import items
if TYPE_CHECKING:
from .world import NodecamesWorld
GREEN_COUNT_PER_WORLD = [6, 9, 9, 12, 16, 16, 20, 25, 25, 30, 36, 36]
def worldName(index):
return f"{(index//3)+1}-{(index%3)+1}"
LOCATION_NAME_TO_ID = {}
LOCATION_NAME_FROM_WORLD = {}
id = 0
for w in range(len(GREEN_COUNT_PER_WORLD)):
LOCATION_NAME_FROM_WORLD[w] = []
for i in range(1, GREEN_COUNT_PER_WORLD[w]):
name = f"World {worldName(w)} word {i}"
id += 1
LOCATION_NAME_FROM_WORLD[w].append(name)
LOCATION_NAME_TO_ID[name] = id
name = f"Complete {worldName(w)}"
id += 1
LOCATION_NAME_FROM_WORLD[w].append(name)
LOCATION_NAME_TO_ID[name] = id
class NodecamesLocation(Location):
game = "Nodecames"
def create_all_locations(world: NodecamesWorld) -> None:
create_world_locations(world)
create_events(world)
def create_world_locations(world: NodecamesWorld) -> None:
overworld = world.get_region("Overworld")
for w in LOCATION_NAME_FROM_WORLD:
for l in range(len(LOCATION_NAME_FROM_WORLD[w])-1):
name = LOCATION_NAME_FROM_WORLD[w][l]
loc = NodecamesLocation(
world.player,
name,
LOCATION_NAME_TO_ID[name],
overworld,
)
overworld.locations.append(loc)
def all_completion_items(world: NodecamesWorld) -> list[str]:
return [LOCATION_NAME_FROM_WORLD[w][-1]+" item" for w in LOCATION_NAME_FROM_WORLD]
def create_events(world: NodecamesWorld) -> None:
overworld = world.get_region("Overworld")
for w in LOCATION_NAME_FROM_WORLD:
overworld.add_event(
LOCATION_NAME_FROM_WORLD[w][-1],
LOCATION_NAME_FROM_WORLD[w][-1]+" item",
location_type = NodecamesLocation,
item_type = items.NodecamesItem)
+111 -6
View File
@@ -1,7 +1,8 @@
from dataclasses import dataclass from dataclasses import dataclass
from Options import Choice, OptionGroup, PerGameCommonOptions, Range, DefaultOnToggle from Options import Choice, OptionGroup, PerGameCommonOptions, Range, DefaultOnToggle, OptionDict
from schema import Schema, And
from . import const as c
class WorldsLocked(DefaultOnToggle): class WorldsLocked(DefaultOnToggle):
""" """
@@ -10,6 +11,16 @@ class WorldsLocked(DefaultOnToggle):
display_name = "Worlds locked" display_name = "Worlds locked"
class HintWordLogicRatio(Range):
"""
How many hints per word count in the grid is needed as a minimum.
Used to compute the logic
Divide by 10
"""
range_start = 10
range_end = 80
display_name = "Hint Word Logic Ration"
class StartHintCount(Range): class StartHintCount(Range):
""" """
@@ -58,6 +69,29 @@ class MaxHintLength(Range):
range_end = 30 range_end = 30
default = 10 default = 10
class StartHintWordCount(Range):
"""
Hint Word count available at the beginning of the game
"""
display_name = "Start Hint Word Count"
range_start = 1
range_end = 16
default = 1
class MaxHintWordCount(Range):
"""
Maximal hint word count we can get.
"""
display_name = "Max Hint Word Count"
range_start = 1
range_end = 16
default = 16
class OnOffDefault(Choice): class OnOffDefault(Choice):
option_Off = 0 option_Off = 0
@@ -160,17 +194,36 @@ class VictoryCondition(Choice):
option_LastWorld = 2 option_LastWorld = 2
default = 2 default = 2
class WorldsOption(OptionDict):
"""
Which nodecames worlds will be in the game and their options
It takes the following parameters:
locked: wether the world is unlocked by default, or needs an item to be unlocked
word_distribution: a string describing the distributions of colors on the grid
"""
display_name = "Worlds"
schema = Schema({
str: {
"locked": bool,
"word_distribution": And(str, c.parseWordDistribution),
"required": bool
}})
# We must now define a dataclass inheriting from PerGameCommonOptions that we put all our options in. # We must now define a dataclass inheriting from PerGameCommonOptions that we put all our options in.
# This is in the format "option_name_in_snake_case: OptionClassName". # This is in the format "option_name_in_snake_case: OptionClassName".
@dataclass @dataclass
class NodecamesOptions(PerGameCommonOptions): class NodecamesOptions(PerGameCommonOptions):
worlds_locked: WorldsLocked worlds_locked: WorldsLocked
hint_word_logic_ratio: HintWordLogicRatio
victory_condition: VictoryCondition victory_condition: VictoryCondition
start_hint_count: StartHintCount start_hint_count: StartHintCount
max_hint_count: MaxHintCount max_hint_count: MaxHintCount
start_hint_length: StartHintLength start_hint_length: StartHintLength
max_hint_length: MaxHintLength max_hint_length: MaxHintLength
start_hint_word_count: StartHintWordCount
max_hint_word_count: MaxHintWordCount
zero_hint: ZeroHint zero_hint: ZeroHint
infinity_hint: InfinityHint infinity_hint: InfinityHint
one_more_guess: OneMoreGuess one_more_guess: OneMoreGuess
@@ -180,14 +233,14 @@ class NodecamesOptions(PerGameCommonOptions):
structured_grid: StructuredGrid structured_grid: StructuredGrid
hints_in_a_row: HintsInARow hints_in_a_row: HintsInARow
anyone_starts: AnyoneStarts anyone_starts: AnyoneStarts
world_count: WorldCount worlds: WorldsOption
# If we want to group our options by similar type, we can do so as well. This looks nice on the website. # If we want to group our options by similar type, we can do so as well. This looks nice on the website.
option_groups = [ option_groups = [
OptionGroup( OptionGroup(
"General Options", "General Options",
[WorldsLocked, VictoryCondition], [WorldsLocked, VictoryCondition, HintWordLogicRatio],
), ),
OptionGroup( OptionGroup(
"Grid Options", "Grid Options",
@@ -207,17 +260,20 @@ option_groups = [
AnyoneStarts, AnyoneStarts,
], ],
), ),
OptionGroup("Worlds Options", [WorldCount]), OptionGroup("Worlds Options", [WorldsOption]),
] ]
# Finally, we can define some option presets if we want the player to be able to quickly choose a specific "mode". # Finally, we can define some option presets if we want the player to be able to quickly choose a specific "mode".
option_presets = { option_presets = {
"default": { "default": {
"hint_word_logic_ratio": 25,
"worlds_locked": True, "worlds_locked": True,
"start_hint_count": 1, "start_hint_count": 1,
"max_hint_count": 10, "max_hint_count": 10,
"start_hint_length": 3, "start_hint_length": 3,
"max_hint_length": 12, "max_hint_length": 12,
"start_hint_word_count": 1,
"max_hint_word_count": 16,
"zero_hint": 1, "zero_hint": 1,
"infinity_hint": 1, "infinity_hint": 1,
"one_more_guess": 1, "one_more_guess": 1,
@@ -227,7 +283,56 @@ option_presets = {
"structured_grid": 1, "structured_grid": 1,
"hints_in_a_row": 1, "hints_in_a_row": 1,
"anyone_starts": 1, "anyone_starts": 1,
"world_count": 12, "worlds": {
"1-1": {
"locked": False,
"word_distribution": "GG1;GW2;GB0;WG2;WW1;WB0;BG0;BW0;BB0"
},
"1-2": {
"locked": True,
"word_distribution": "GG2;GW1;GB1;WG1;WW2;WB0;BG1;BW0;BB1"
},
"1-3": {
"locked": True,
"word_distribution": "GG2;GW1;GB1;WG1;WW2;WB0;BG1;BW0;BB1"
},
"2-1": {
"locked": True,
"word_distribution": "GG2;GW1;GB1;WG1;WW3;WB1;BG1;BW1;BB1"
},
"2-2": {
"locked": True,
"word_distribution": "GG2;GW2;GB1;WG2;WW5;WB1;BG1;BW1;BB1"
},
"2-3": {
"locked": True,
"word_distribution": "GG2;GW2;GB1;WG2;WW5;WB1;BG1;BW1;BB1"
},
"3-1": {
"locked": True,
"word_distribution": "GG2;GW4;GB1;WG4;WW5;WB1;BG1;BW1;BB1"
},
"3-2": {
"locked": True,
"word_distribution": "GG3;GW5;GB1;WG5;WW7;WB1;BG1;BW1;BB1"
},
"3-3": {
"locked": True,
"word_distribution": "GG3;GW5;GB1;WG5;WW7;WB1;BG1;BW1;BB1"
},
"4-1": {
"locked": True,
"word_distribution": "GG3;GW5;GB2;WG5;WW7;WB2;BG2;BW2;BB1"
},
"4-2": {
"locked": True,
"word_distribution": "GG5;GW6;GB2;WG6;WW9;WB2;BG2;BW2;BB2"
},
"4-3": {
"locked": True,
"word_distribution": "GG5;GW6;GB2;WG6;WW9;WB2;BG2;BW2;BB2"
}
},
"victory_condition": 2, "victory_condition": 2,
}, },
} }
-22
View File
@@ -1,22 +0,0 @@
from __future__ import annotations
from typing import TYPE_CHECKING
from BaseClasses import Entrance, Region
if TYPE_CHECKING:
from .world import NodecamesWorld
def create_and_connect_regions(world: NodecamesWorld) -> None:
create_all_regions(world)
connect_regions(world)
def create_all_regions(world: NodecamesWorld) -> None:
overworld = Region("Overworld", world.player, world.multiworld)
world.multiworld.regions += [overworld]
def connect_regions(world: NodecamesWorld) -> None:
overworld = world.get_region("Overworld")
+64 -17
View File
@@ -1,31 +1,78 @@
from __future__ import annotations from __future__ import annotations
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from rule_builder.options import OptionFilter
from rule_builder.rules import Has, HasAll, Rule
from . import locations
if TYPE_CHECKING: if TYPE_CHECKING:
from .world import NodecamesWorld from .world import NodecamesWorld
def set_all_rules(world: NodecamesWorld) -> None: from rule_builder.options import OptionFilter
set_all_entrance_rules(world) from rule_builder.rules import Has, HasAll, Rule, True_
set_all_location_rules(world) from BaseClasses import Region, Location
set_completion_condition(world) from . import const as c
import math
from . import items
def print_graph(world : NodecamesWorld):
for region in world.multiworld.regions:
print(region)
for exit in region.exits:
print("->",exit,"--",exit.access_rule)
def set_all_entrance_rules(world: NodecamesWorld) -> None: class NodecamesLocation(Location):
pass game = "Nodecames"
def total_word_count(distribution):
return distribution['GB']+distribution['BG']+distribution['GW']+distribution['WG']+distribution['GG']
def set_all_location_rules(world: NodecamesWorld) -> None: def can_access_word(options, word):
pass ratio = options.hint_word_logic_ratio/10
min_hint_count = math.ceil(word/ratio)
min_hint_word_count = math.ceil(word/min_hint_count) # min_hint_count * min_hint_word_count >= word
return Has("progressive_hint_word_count", count=max(min_hint_word_count-options.start_hint_word_count,0)) & Has("progressive_hint_count", count=max(min_hint_count-options.start_hint_count,0))
def set_completion_condition(world: NodecamesWorld) -> None: def can_access_world(options, wn, wi):
if options.worlds[wn]['locked']:
return Has(f"unlock_world_{wi}")
else:
return True_()
worldLocations = locations.all_completion_items(world) def create_all_regions(world: NodecamesWorld) -> None:
world.set_completion_rule(HasAll(*worldLocations)) #TODO make a helper to add new regions
owmap = Region("Map", world.player, world.multiworld)
wregions = dict()
toComplete = list()
for wi,wn in enumerate(world.options.worlds.keys()):
w = world.options.worlds[wn]
distrib = c.parseWordDistribution(w['word_distribution'])
wregions[wn] = list()
for i in range(total_word_count(distrib)+1):
name = c.location_name_of_world_and_word(wi+1,i)
wregions[wn].append(Region(name, world.player, world.multiworld))
if i>0:
wregions[wn][i-1].connect(wregions[wn][i], rule = can_access_word(world.options, i))
else:
owmap.connect(wregions[wn][i], rule = can_access_world(world.options, wn, wi))
loc = NodecamesLocation(
world.player,
name,
c.LOCATION_NAME_TO_ID[name],
wregions[wn][i]
)
wregions[wn][i].locations.append(loc)
wregions[wn][-1].add_event(
f"World {wn} complete",
f"World {wn} completion item",
location_type = NodecamesLocation,
item_type = items.NodecamesItem)
if w['required']:
toComplete.append(f"World {wn} completion item")
world.set_completion_rule(HasAll(*toComplete))
allregions = [owmap] + [wrxv for wrv in wregions.values() for wrxv in wrv]
print(f"Generated {len(allregions)} regions")
print(f"Generated {sum([len(reg.locations) for reg in allregions])} locations")
world.multiworld.regions += allregions
+13 -29
View File
@@ -3,31 +3,28 @@ from typing import Any
from worlds.AutoWorld import World from worlds.AutoWorld import World
from . import items, locations, regions, rules from . import items, rules
from . import options as nodecames_options from . import options as nodecames_options
from . import const as c
class NodecamesWorld(World): class NodecamesWorld(World):
""" """
Nodecames is a word game, inspired by Code Names by Vlaada Chvátil Nodecames is a word game, inspired by Code Names by Vlaada Chvátil
""" """
game = "Nodecames" game = "nodecames"
options_dataclass = nodecames_options.NodecamesOptions options_dataclass = nodecames_options.NodecamesOptions
options: nodecames_options.NodecamesOptions options: nodecames_options.NodecamesOptions
location_name_to_id = locations.LOCATION_NAME_TO_ID location_name_to_id = c.LOCATION_NAME_TO_ID
item_name_to_id = items.ITEM_NAME_TO_ID item_name_to_id = c.ITEM_NAME_TO_ID
origin_region_name = "Overworld" origin_region_name = "Map"
# Create regions, locations and rules
def create_regions(self) -> None: def create_regions(self) -> None:
regions.create_and_connect_regions(self) rules.create_all_regions(self)
locations.create_all_locations(self)
def set_rules(self) -> None:
rules.set_all_rules(self)
def create_items(self) -> None: def create_items(self) -> None:
items.create_all_items(self) items.create_all_items(self)
@@ -40,23 +37,10 @@ class NodecamesWorld(World):
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
return items.get_random_filler_item_name(self) return items.get_random_filler_item_name(self)
# Generates the output file
def generate_output(self, outdir : str) -> None:
pass
# Data sent to the client # Data sent to the client
def fill_slot_data(self) -> Mapping[str, Any]: def fill_slot_data(self) -> Mapping[str, Any]:
return self.options.as_dict( return []
"worlds_locked",
"start_hint_count",
"max_hint_count",
"start_hint_length",
"max_hint_length",
"zero_hint",
"infinity_hint",
"one_more_guess",
"end_guessing",
"only_one_green_green",
"sudden_death",
"structured_grid",
"hints_in_a_row",
"anyone_starts",
"world_count",
"victory_condition",
)
@@ -1,11 +1,16 @@
package com.bernard.nodecames; package com.bernard.nodecames;
import java.net.URISyntaxException;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -21,10 +26,12 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.view.RedirectView; import org.springframework.web.servlet.view.RedirectView;
import com.bernard.nodecames.frontend.GridConfigEdit; import com.bernard.nodecames.frontend.GridConfigEdit;
import com.bernard.nodecames.game.ArchipelagoManager;
import com.bernard.nodecames.game.GameManager; import com.bernard.nodecames.game.GameManager;
import com.bernard.nodecames.game.GridManager; import com.bernard.nodecames.game.GridManager;
import com.bernard.nodecames.game.IllegalGameActionException; import com.bernard.nodecames.game.IllegalGameActionException;
import com.bernard.nodecames.model.Game; import com.bernard.nodecames.model.Game;
import com.bernard.nodecames.model.GameConfig;
import com.bernard.nodecames.model.Grid; import com.bernard.nodecames.model.Grid;
import com.bernard.nodecames.model.GridConfig; import com.bernard.nodecames.model.GridConfig;
import com.bernard.nodecames.model.GridConfig.MutableOption; import com.bernard.nodecames.model.GridConfig.MutableOption;
@@ -33,6 +40,7 @@ import com.bernard.nodecames.repository.GameRepository;
import com.bernard.nodecames.repository.GridRepository; import com.bernard.nodecames.repository.GridRepository;
import com.bernard.nodecames.repository.WorldRepository; import com.bernard.nodecames.repository.WorldRepository;
import io.github.archipelagomw.network.ConnectionResult;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -41,6 +49,7 @@ public class HttpController {
private final GridManager gm; private final GridManager gm;
private final GameManager wm; private final GameManager wm;
private final ArchipelagoManager am;
private final GridRepository gridRepository; private final GridRepository gridRepository;
private final GameRepository gRepository; private final GameRepository gRepository;
private final WorldRepository wRepository; private final WorldRepository wRepository;
@@ -78,6 +87,7 @@ public class HttpController {
model.addAttribute("gridConfig", GridConfigEdit.of(gc)); model.addAttribute("gridConfig", GridConfigEdit.of(gc));
return "grid-config"; return "grid-config";
} }
@PostMapping("/room/{id}/config") @PostMapping("/room/{id}/config")
public Object setGridConfig(@PathVariable("id") String id, public Object setGridConfig(@PathVariable("id") String id,
@ModelAttribute("gridConfig") GridConfigEdit gce, @ModelAttribute("gridConfig") GridConfigEdit gce,
@@ -103,9 +113,9 @@ public class HttpController {
public RedirectView createRoom(@RequestParam("type") String type) { public RedirectView createRoom(@RequestParam("type") String type) {
GridConfig cfg = switch(type) { GridConfig cfg = switch(type) {
case "zero" -> GridConfig.ZERO; case "zero" -> GridConfig.ZERO;
case "base" -> GridConfig.CODENAMES_CLASSICAL; case "base" -> GridConfig.REGULAR;
case "big" -> GridConfig.BIG_CLASSICAL; case "big" -> GridConfig.BIG;
default -> GridConfig.CODENAMES_CLASSICAL; default -> GridConfig.REGULAR;
}; };
Random r = new Random(rand.nextLong()); Random r = new Random(rand.nextLong());
Grid g = gm.newGrid(null, cfg, r); Grid g = gm.newGrid(null, cfg, r);
@@ -113,8 +123,13 @@ public class HttpController {
} }
@GetMapping("/create-game") @GetMapping("/create-game")
public RedirectView createGame(@RequestParam("name") String name) { public RedirectView createGame(@RequestParam("name") String name, @RequestParam("type") String type) {
Game g = wm.createGame(name); 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"); return new RedirectView("/game/"+g.getId().toString()+"/map");
} }
@@ -134,10 +149,43 @@ public class HttpController {
.toList(); .toList();
m.addAttribute("game", g.get()); m.addAttribute("game", g.get());
m.addAttribute("worlds", worlds); m.addAttribute("worlds", worlds);
m.addAttribute("items", g.get().getInventory().getReceivedItems());
m.addAttribute("player", player); m.addAttribute("player", player);
return "map"; return "map";
} }
@GetMapping("/game/{id}/archipelago")
public Object gridConfig(@PathVariable("id") String id, @RequestParam("address") String address,
@RequestParam("slot") String slot, Model model) {
Optional<Game> g = gRepository.findById(UUID.fromString(id));
if(!g.isPresent())
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
try {
CompletableFuture<ConnectionResult> connectionResultFuture = am.openClient(g.get(), address, slot);
ConnectionResult connectionResult = connectionResultFuture.get(5, TimeUnit.SECONDS);
if (connectionResult != ConnectionResult.Success) {
return new ResponseEntity<>("Could not connect to archipelago server: "+connectionResult.name(), HttpStatus.NOT_FOUND);
} else {
return new ResponseEntity<>("Connection successful", HttpStatus.OK);
}
} catch (TimeoutException e) {
return new ResponseEntity<>("Timed out connecting to archipelago", HttpStatus.REQUEST_TIMEOUT);
} catch (URISyntaxException e) {
return new ResponseEntity<>("URI malformed : "+e.getMessage(), HttpStatus.BAD_REQUEST);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
return new ResponseEntity<>("Interrupted", HttpStatus.INTERNAL_SERVER_ERROR);
} catch (ExecutionException e) {
e.printStackTrace();
return new ResponseEntity<>("ExecutionException while connecting to Archipelago : "+e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/game/{id}/grid/{world}") @GetMapping("/game/{id}/grid/{world}")
public Object gridRedirect(@PathVariable("id") String id, @PathVariable("world") String world, @RequestParam("player") String player) { public Object gridRedirect(@PathVariable("id") String id, @PathVariable("world") String world, @RequestParam("player") String player) {
Optional<Game> g = gRepository.findById(UUID.fromString(id)); Optional<Game> g = gRepository.findById(UUID.fromString(id));
@@ -148,7 +196,12 @@ public class HttpController {
if(!w.isPresent()) { if(!w.isPresent()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND); return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} }
Grid grid = wm.getGrid(g.get(), w.get()); Grid grid;
try {
grid = wm.getGrid(g.get(), w.get());
} catch (IllegalGameActionException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
return new RedirectView("/room/"+grid.getId()+"/grid" + "?player="+player); return new RedirectView("/room/"+grid.getId()+"/grid" + "?player="+player);
} }
@@ -162,7 +215,11 @@ public class HttpController {
if(!w.isPresent()) { if(!w.isPresent()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND); return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} }
Grid newGrid = wm.resetGrid(g.get(), w.get()); try {
return new RedirectView("/room/"+newGrid.getId()+"/grid" + "?player="+player); Grid newGrid = wm.resetGrid(g.get(), w.get());
return new RedirectView("/room/"+newGrid.getId()+"/grid" + "?player="+player);
} catch (IllegalGameActionException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
} }
} }
@@ -1,6 +1,5 @@
package com.bernard.nodecames.frontend; package com.bernard.nodecames.frontend;
import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import org.springframework.stereotype.Service; 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;
import com.bernard.nodecames.model.Grid.Phase; import com.bernard.nodecames.model.Grid.Phase;
import com.bernard.nodecames.model.GridConfig; import com.bernard.nodecames.model.GridConfig;
import com.bernard.nodecames.model.GridConfig.CardColors;
import com.bernard.nodecames.model.GridConfig.MutableOption; import com.bernard.nodecames.model.GridConfig.MutableOption;
import com.bernard.nodecames.model.converter.ColorDistributionConverter;
import com.bernard.nodecames.repository.CardRepository; import com.bernard.nodecames.repository.CardRepository;
import lombok.RequiredArgsConstructor; import lombok.AllArgsConstructor;
import tools.jackson.databind.JsonNode; import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ArrayNode;
import tools.jackson.databind.node.JsonNodeFactory; import tools.jackson.databind.node.JsonNodeFactory;
import tools.jackson.databind.node.ObjectNode; import tools.jackson.databind.node.ObjectNode;
@Service @Service
@RequiredArgsConstructor @AllArgsConstructor
public class JsonDataService { public class JsonDataService {
private final JsonNodeFactory jsn; private final JsonNodeFactory jsn;
private final CardRepository cRepository; private final CardRepository cRepository;
private final ColorDistributionConverter wdc;
public JsonNode cardData(Card c, char player) { public JsonNode cardData(Card c, char player) {
ObjectNode cardNode = jsn.objectNode(); ObjectNode cardNode = jsn.objectNode();
@@ -43,15 +43,7 @@ public class JsonDataService {
gcNode.set("max-max-hint", jsn.numberNode(gc.getMaxMaxHint())); gcNode.set("max-max-hint", jsn.numberNode(gc.getMaxMaxHint()));
gcNode.set("structured-grid", jsn.booleanNode(gc.isStructuredGrid())); gcNode.set("structured-grid", jsn.booleanNode(gc.isStructuredGrid()));
gcNode.set("anyone-starts", jsn.booleanNode(gc.isAnyoneStarts())); gcNode.set("anyone-starts", jsn.booleanNode(gc.isAnyoneStarts()));
ArrayNode wordDistributionNode = jsn.arrayNode(gc.getWordDistribution().size()); gcNode.set("color-distribution", jsn.stringNode(wdc.convertToDatabaseColumn(gc.getColorDistribution())));
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("maybe-sudden-death", jsn.booleanNode(gc.isMaybeSuddenDeath())); gcNode.set("maybe-sudden-death", jsn.booleanNode(gc.isMaybeSuddenDeath()));
gcNode.set("maybe-one-more-guess", jsn.booleanNode(gc.isMaybeOneMoreGuess())); gcNode.set("maybe-one-more-guess", jsn.booleanNode(gc.isMaybeOneMoreGuess()));
gcNode.set("only-one-green-green-prevent-death", jsn.booleanNode(gc.isOnlyOneGreenGreenPreventDeath())); gcNode.set("only-one-green-green-prevent-death", jsn.booleanNode(gc.isOnlyOneGreenGreenPreventDeath()));
@@ -1,7 +1,9 @@
package com.bernard.nodecames.frontend; package com.bernard.nodecames.frontend;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -17,14 +19,20 @@ import org.springframework.web.bind.annotation.PathVariable;
import com.bernard.nodecames.game.GridManager; import com.bernard.nodecames.game.GridManager;
import com.bernard.nodecames.game.IllegalGameActionException; import com.bernard.nodecames.game.IllegalGameActionException;
import com.bernard.nodecames.model.Card; import com.bernard.nodecames.model.Card;
import com.bernard.nodecames.model.Card.Color;
import com.bernard.nodecames.model.GameEvent; import com.bernard.nodecames.model.GameEvent;
import com.bernard.nodecames.model.Grid; import com.bernard.nodecames.model.Grid;
import com.bernard.nodecames.model.Grid.Phase;
import com.bernard.nodecames.model.GridConfig.MutableOption; import com.bernard.nodecames.model.GridConfig.MutableOption;
import com.bernard.nodecames.model.Hint;
import com.bernard.nodecames.observer.GridObserver;
import com.bernard.nodecames.repository.GridRepository; import com.bernard.nodecames.repository.GridRepository;
import lombok.AllArgsConstructor;
import tools.jackson.databind.JsonNode; import tools.jackson.databind.JsonNode;
@Controller @Controller
@AllArgsConstructor
public class WebSocketController { public class WebSocketController {
private final GridManager gm; private final GridManager gm;
@@ -32,49 +40,19 @@ public class WebSocketController {
private final JsonDataService json; private final JsonDataService json;
private final GridRepository gRepository; private final GridRepository gRepository;
public WebSocketController(GridManager gm, SimpMessagingTemplate template, JsonDataService json, GridRepository gRepository) { Set<UUID> observed = new HashSet<>();
this.gm = gm; private void registerObserver(Grid g) {
this.template = template; if(!observed.contains(g.getId())) {
this.json = json; observed.add(g.getId());
this.gRepository = gRepository; gm.addObserver(g, new GObserver(g));
gm.setWsc(this); }
}
public void onNewEvent(Grid g, GameEvent ge) {
this.template.convertAndSend("/topic/new-event",
json.eventData(ge)
);
}
public void onNewPhase(Grid g) {
this.template.convertAndSend(
"/topic/new-phase",
json.phaseData(g.getPhase())
);
}
public void onConfigChange(Grid g, MutableOption cc, Object data) {
this.template.convertAndSend("/topic/config-change",
json.gridConfigUpdateData(cc, data)
);
}
public void publishCardUpdateToPlayer(Grid g, Card c, char player) {
this.template.convertAndSend(
"/topic/update-card/" + Character.toLowerCase(player),
json.cardData(c, player),
Map.of("cardIndex", c.getIndex())
);
}
public void publishCardUpdate(Grid g, Card card) {
publishCardUpdateToPlayer(g, card, 'A');
publishCardUpdateToPlayer(g, card, 'B');
} }
public void publishError(char issuer, IllegalGameActionException e) { public void publishError(char issuer, IllegalGameActionException e) {
//TODO open error channel //TODO open error channel
} }
//TODO We should do this with websocket protocol
@GetMapping("/room/{id}/game/{player}") @GetMapping("/room/{id}/game/{player}")
public Object grid(@PathVariable("id") String gridId, @PathVariable("player") String playerStr) { public Object grid(@PathVariable("id") String gridId, @PathVariable("player") String playerStr) {
char player; char player;
@@ -86,14 +64,19 @@ public class WebSocketController {
throw new IllegalArgumentException("Unknown player type"); throw new IllegalArgumentException("Unknown player type");
Optional<Grid> g = gRepository.findById(UUID.fromString(gridId)); Optional<Grid> g = gRepository.findById(UUID.fromString(gridId));
if(g.isPresent()) if(g.isPresent()){
return new ResponseEntity<>(json.gridData(g.get(), player), HttpStatus.OK); Grid grid = g.get();
else registerObserver(grid);
return new ResponseEntity<>(json.gridData(grid, player), HttpStatus.OK);
} else
return new ResponseEntity<>("Could not find grid with given ID",HttpStatus.NOT_FOUND); return new ResponseEntity<>("Could not find grid with given ID",HttpStatus.NOT_FOUND);
} }
@MessageMapping("submit-hint") //TODO handle websockets per grid
public void submitHint(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) { @MessageMapping("/submit-hint")
public void submitHint(@Header("room") String roomId,
@Header("player") String player,
@Payload JsonNode content) {
String hint = content.asObject().get("hint").stringValue(); String hint = content.asObject().get("hint").stringValue();
int wordCount = content.asObject().get("hintWordCount").intValue(); int wordCount = content.asObject().get("hintWordCount").intValue();
@@ -101,7 +84,6 @@ public class WebSocketController {
if(!g.isPresent()) if(!g.isPresent())
// 404 // 404
return; return;
try { try {
gm.proposeHint(g.get(), player.charAt(0), hint, wordCount); gm.proposeHint(g.get(), player.charAt(0), hint, wordCount);
} catch (IllegalGameActionException e) { } catch (IllegalGameActionException e) {
@@ -118,10 +100,8 @@ public class WebSocketController {
if(!g.isPresent()) if(!g.isPresent())
// 404 // 404
return; return;
try { try {
gm.pointCard(g.get(), player.charAt(0), cardIndex); gm.pointCard(g.get(), player.charAt(0), cardIndex);
publishCardUpdate(g.get(), gm.getCard(g.get(), cardIndex));
} catch (IllegalGameActionException e) { } catch (IllegalGameActionException e) {
publishError(player.charAt(0), e); publishError(player.charAt(0), e);
e.printStackTrace(); e.printStackTrace();
@@ -134,7 +114,6 @@ public class WebSocketController {
if(!g.isPresent()) if(!g.isPresent())
// 404 // 404
return; return;
try { try {
gm.endGuessing(g.get(), player.charAt(0)); gm.endGuessing(g.get(), player.charAt(0));
} catch (IllegalGameActionException e) { } catch (IllegalGameActionException e) {
@@ -156,4 +135,56 @@ public class WebSocketController {
e.printStackTrace(); e.printStackTrace();
} }
} }
@AllArgsConstructor
private class GObserver implements GridObserver {
Grid grid;
public void publishCardUpdateToPlayer(Card c, char player) {
template.convertAndSend(
"/topic/%s/update-card/%s".formatted(grid.getId(), Character.toLowerCase(player)),
json.cardData(c, player),
Map.of("cardIndex", c.getIndex())
);
}
public void publishCardUpdate(Card card) {
publishCardUpdateToPlayer(card, 'A');
publishCardUpdateToPlayer(card, 'B');
}
@Override
public void onPointCard(char player, Card card, Color color) {
publishCardUpdate(card);
}
@Override
public void onSubmitHint(char player, Hint hint) {}
@Override
public void onGridEnd(boolean won) {}
@Override
public void onGameEvent(GameEvent ge) {
template.convertAndSend(
"/topic/%s/new-event".formatted(grid.getId()),
json.eventData(ge)
);
}
@Override
public void onNewPhase(Phase p) {
template.convertAndSend(
"/topic/%s/new-phase".formatted(grid.getId()),
json.phaseData(p)
);
}
@Override
public void onConfigChange(MutableOption cc, Object data) {
template.convertAndSend(
"/topic/%s/config-change".formatted(grid.getId()),
json.gridConfigUpdateData(cc, data)
);
}
}
} }
@@ -0,0 +1,94 @@
package com.bernard.nodecames.game;
import java.util.Arrays;
import java.util.concurrent.CompletableFuture;
import java.util.stream.IntStream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.bernard.nodecames.model.Game;
import io.github.archipelagomw.APResult.ResultCode;
import io.github.archipelagomw.Client;
import io.github.archipelagomw.ClientStatus;
import io.github.archipelagomw.events.ArchipelagoEventListener;
import io.github.archipelagomw.events.ConnectionResultEvent;
import io.github.archipelagomw.events.ReceiveItemEvent;
import io.github.archipelagomw.flags.ItemsHandling;
import io.github.archipelagomw.network.ConnectionResult;
import lombok.Getter;
public class ArchipelagoClient extends Client {
Game game;
GameManager gm;
private static final Logger log = LoggerFactory.getLogger(ArchipelagoClient.class);
public ArchipelagoClient(Game game, GameManager gm, String slot) {
super();
this.gm = gm;
this.game = game;
this.setGame("nodecames");
this.setName(slot);
client.setItemsHandlingFlags(ItemsHandling.SEND_ITEMS | ItemsHandling.SEND_OWN_ITEMS);
this.getEventManager().registerListener(this);
}
@ArchipelagoEventListener
public void onReceiveItem(ReceiveItemEvent e) {
log.debug("Received item : {}", e.getItemName());
if(e.getIndex() > game.getInventory().getReceivedItems().size())
gm.giveItem(game, e.getItemName());
// Else, the item has already been received
}
@Getter
private CompletableFuture<ConnectionResult> connectionResult = new CompletableFuture<>();
@ArchipelagoEventListener
public void onConnectResult(ConnectionResultEvent e) {
connectionResult.complete(e.getResult());
if(e.getResult() != ConnectionResult.Success) {
throw new IllegalStateException("Could not connect to archipelago : %s".formatted(e.getResult().name()));
} else {
if(this.setGameState(ClientStatus.CLIENT_PLAYING).getCode() != ResultCode.SUCCESS)
throw new IllegalStateException("Colud not set state to playing");
else {
client.sendChat("Hello, here is Nodecames AP client, nice to meet you");
}
}
}
public static int[] GREEN_COUNT_PER_WORLD = new int[]{6, 9, 9, 12, 16, 16, 20, 25, 25, 30, 36, 36};
public static String[] WORLDS = new String[]{"1-1","1-2","1-3","2-1","2-2","2-3","3-1","3-2","3-3","4-1","4-2","4-3"};
public void checkWordInWorld(String worldName, int wordIndex) {
//TODO binarySearch is okay for now, but we'll have to remove this at some point
int worldIndex = Arrays.binarySearch(WORLDS, 0, WORLDS.length, worldName);
if(wordIndex>GREEN_COUNT_PER_WORLD[wordIndex]-1)return;//We don't have that many checks
//TODO this should be stored, not computed, in archipelagoconfig
long locationId = IntStream.range(0, worldIndex).map(w -> GREEN_COUNT_PER_WORLD[w]).sum()+1+wordIndex;
this.checkLocation(locationId);
}
public void checkWorldComplete(String worldName) {
//TODO binarySearch is okay for now, but we'll have to remove this at some point
int worldIndex = Arrays.binarySearch(WORLDS, 0, WORLDS.length, worldName);
long locationId = IntStream.range(0, worldIndex+1).map(w -> GREEN_COUNT_PER_WORLD[w]).sum()+1;
this.checkLocation(locationId);
}
@Override
public void onError(Exception e) {
//TODO make a real handler
throw new IllegalStateException("Archipelago error :", e);
}
@Override
public void onClose(String Reason, int attemptingReconnect) {
// TODO Auto-generated method stub
throw new UnsupportedOperationException("Unimplemented method 'onClose'");
}
}
@@ -0,0 +1,92 @@
package com.bernard.nodecames.game;
import java.net.URISyntaxException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
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.GameEvent;
import com.bernard.nodecames.model.Grid.Phase;
import com.bernard.nodecames.model.GridConfig.MutableOption;
import com.bernard.nodecames.model.Hint;
import com.bernard.nodecames.model.World;
import com.bernard.nodecames.observer.WorldObserver;
import com.bernard.nodecames.repository.CardRepository;
import com.bernard.nodecames.repository.WorldRepository;
import io.github.archipelagomw.network.ConnectionResult;
import lombok.AllArgsConstructor;
@Service
@AllArgsConstructor
public class ArchipelagoManager {
Map<UUID, ArchipelagoClient> clients = new HashMap<>();
GameManager gm;
CardRepository cRepository;
WorldRepository wRepository;
private static final Logger log = LoggerFactory.getLogger(ArchipelagoManager.class);
public CompletableFuture<ConnectionResult> openClient(Game g, String address, String slot) throws URISyntaxException{
ArchipelagoClient client = new ArchipelagoClient(g, gm, slot);
client.connect(address);
clients.put(g.getId(), client);
Set<World> worlds = wRepository.findByGame(g);
log.debug("Registering observer for {} worlds", worlds.size());
for(World w : worlds) {
gm.addObserver(w, new WObserver(w, client));
}
return client.getConnectionResult();
}
@AllArgsConstructor
private class WObserver implements WorldObserver {
private World world;
ArchipelagoClient client;
@Override
public void onPointCard(char player, Card card, Color color) {
// We compute the count of green cards
//TODO right now, we only do the strategy of green green cards once
//TODO decide wether we or they should have the logic of wether to send the ith check
Card[] cards = cRepository.findByGridOrderById_Index(world.getCurrentGrid());
int greenCount = (int)Arrays.stream(cards).filter(c ->
(c.isColorAPublic() && c.getColorA() == Color.GREEN) ||
(c.isColorBPublic() && c.getColorB() == Color.GREEN)
).count();
log.info("Pointed card, {}th", greenCount);
client.checkWordInWorld(world.getName(), greenCount);
}
@Override
public void onGridEnd(boolean win) {
if(win) {
client.checkWorldComplete(world.getName());
}
}
@Override
public void onSubmitHint(char player, Hint hint) {}
@Override
public void onGameEvent(GameEvent ge) {}
@Override
public void onNewPhase(Phase p) {}
@Override
public void onConfigChange(MutableOption cc, Object data) {}
}
}
@@ -3,48 +3,82 @@ package com.bernard.nodecames.game;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import java.util.Map.Entry;
import java.util.Random; import java.util.Random;
import java.util.Set; 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 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.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;
import com.bernard.nodecames.model.Grid.Phase;
import com.bernard.nodecames.model.GridConfig; import com.bernard.nodecames.model.GridConfig;
import com.bernard.nodecames.model.GridConfig.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;
import com.bernard.nodecames.model.World.WorldId;
import com.bernard.nodecames.observer.GridObserver;
import com.bernard.nodecames.observer.WorldObserver;
import com.bernard.nodecames.repository.GameRepository; import com.bernard.nodecames.repository.GameRepository;
import com.bernard.nodecames.repository.WorldRepository; import com.bernard.nodecames.repository.WorldRepository;
import jakarta.annotation.PostConstruct;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
@Service @Service
@AllArgsConstructor
public class GameManager { public class GameManager {
// Reverse map allowing to pass grid events to the game
Map<UUID, World> currentGrids = new HashMap<>();
private final GridManager gm; private final GridManager gm;
private final GameRepository gRepository; private final GameRepository gRepository;
private final WorldRepository wRepository; private final WorldRepository wRepository;
public GameManager(GridManager gm, GameRepository gRepository, WorldRepository wRepository) {
this.gm = gm; private static final Logger log = LoggerFactory.getLogger(GameManager.class);
this.gRepository = gRepository;
this.wRepository = wRepository; //TODO try putting "World" here instead of WorldId
gm.setWm(this); private Map<WorldId, Set<WorldObserver>> observers = new HashMap<>();
public void addObserver(World w, WorldObserver observer) {
if(!observers.containsKey(w.getId()))
observers.put(w.getId(), new CopyOnWriteArraySet<>());
observers.get(w.getId()).add(observer);
if(w.getCurrentGrid() != null)
gm.addObserver(w.getCurrentGrid(), observer);
} }
public Game createGame(String name) { private void forEachObserver(World w, Consumer<WorldObserver> action) {
UUID uuid = UUID.randomUUID(); observers.getOrDefault(w.getId(), Set.of()).forEach(action);
}
Game g = new Game(uuid, name); /*
gRepository.save(g); * In case of server restart, registers to GridManager observers
for (Entry<String, GridConfig> wConfig : Game.DEFAULT_WORLDS_CONFIG.entrySet()) { */
World w = new World(g, wConfig.getKey(), wConfig.getValue(), null, false); @PostConstruct
wRepository.save(w); private void registerRunningObservers() {
for(World w : wRepository.findAll()) {
if(w.getCurrentGrid() != null && w.getCurrentGrid().getWon() != null) {
registerObserver(w, w.getCurrentGrid());
}
} }
}
private void registerObserver(World w, Grid g) {
gm.addObserver(g, new GObserver(g, w));
forEachObserver(w, o -> gm.addObserver(g, o));
}
public Game createGame(String name, GameConfig cfg) {
Game g = cfg.newGame(gRepository, wRepository, name);
return g; return g;
} }
@@ -57,22 +91,26 @@ public class GameManager {
return wRepository.findByGame(g); return wRepository.findByGame(g);
} }
private Grid newGrid(Game g, World w) { private Grid newGrid(Game g, World w) throws IllegalGameActionException {
GridConfig gc = w.getInitialConfig(); if(w.isLocked())
//TODO separate IllegaleGameAction and IllegalGridAction
throw new IllegalGameActionException(null, "Cannot create a grid in a locked world");
GridConfig gc = w.makeGridConfig();
//TODO seed random
Grid grid = gm.newGrid(w, gc, new Random()); Grid grid = gm.newGrid(w, gc, new Random());
w.setCurrentGrid(grid); w.setCurrentGrid(grid);
wRepository.save(w); wRepository.save(w);
currentGrids.put(grid.getId(), w); registerObserver(w, grid);
return grid; return grid;
} }
public Grid resetGrid(Game g, World world) { public Grid resetGrid(Game g, World world) throws IllegalGameActionException{
return newGrid(g, world); return newGrid(g, world);
} }
public Grid getGrid(Game g, World w) { public Grid getGrid(Game g, World w) throws IllegalGameActionException{
if(w.getCurrentGrid() != null) { if(w.getCurrentGrid() != null) {
return w.getCurrentGrid(); return w.getCurrentGrid();
} else { } else {
@@ -80,11 +118,47 @@ public class GameManager {
} }
} }
public void onGameWon(Grid g) { public void giveItem(Game g, String item) {
World w = currentGrids.get(g.getId()); g.getInventory().receiveItem(item);
if(w != null) { Inventory inv = g.getInventory();
w.setWon(true); for(World w : allWorlds(g)) {
wRepository.save(w); if(w.getCurrentGrid() == null) continue;
Grid grid = w.getCurrentGrid();
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);
}
}
} }
gRepository.save(g);
}
@AllArgsConstructor
private class GObserver implements GridObserver {
private Grid grid;
private World world;
@Override
public void onGridEnd(boolean win) {
if(win) {
world.setWon(true);
wRepository.save(world);
}
}
@Override
public void onPointCard(char player, Card card, Color color) {}
@Override
public void onSubmitHint(char player, Hint hint) {}
@Override
public void onGameEvent(GameEvent ge) {}
@Override
public void onNewPhase(Phase p) {}
@Override
public void onConfigChange(MutableOption cc, Object data) {}
} }
} }
@@ -2,11 +2,15 @@ package com.bernard.nodecames.game;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@@ -14,32 +18,28 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.bernard.nodecames.frontend.WebSocketController;
import com.bernard.nodecames.model.Card; import com.bernard.nodecames.model.Card;
import com.bernard.nodecames.model.GameEvent;
import com.bernard.nodecames.model.Card.Color; 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;
import com.bernard.nodecames.model.Grid.Phase; import com.bernard.nodecames.model.Grid.Phase;
import com.bernard.nodecames.model.GridConfig; import com.bernard.nodecames.model.GridConfig;
import com.bernard.nodecames.model.GridConfig.CardColors;
import com.bernard.nodecames.model.GridConfig.MutableOption; import com.bernard.nodecames.model.GridConfig.MutableOption;
import com.bernard.nodecames.model.Hint; import com.bernard.nodecames.model.Hint;
import com.bernard.nodecames.model.World; import com.bernard.nodecames.model.World;
import com.bernard.nodecames.observer.GridObserver;
import com.bernard.nodecames.repository.CardRepository; import com.bernard.nodecames.repository.CardRepository;
import com.bernard.nodecames.repository.GridRepository; import com.bernard.nodecames.repository.GridRepository;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class GridManager { public class GridManager {
@Setter private Map<UUID, Set<GridObserver>> observers = new HashMap<>();
private WebSocketController wsc = null;
@Setter
private GameManager wm = null;
private final GridRepository gRepository; private final GridRepository gRepository;
private final CardRepository cRepository; private final CardRepository cRepository;
@@ -48,11 +48,22 @@ public class GridManager {
private static final Logger log = LoggerFactory.getLogger(GridManager.class); private static final Logger log = LoggerFactory.getLogger(GridManager.class);
public void addObserver(Grid g, GridObserver observer) {
if(!observers.containsKey(g.getId()))
observers.put(g.getId(), new CopyOnWriteArraySet<>());
observers.get(g.getId()).add(observer);
}
private void forEachObserver(Grid g, Consumer<GridObserver> action) {
log.debug("The current observers are : {}", observers.get(g.getId()));
observers.getOrDefault(g.getId(), Set.of()).forEach(action);
}
private void newGameEvent(Grid g, GameEvent ge) { private void newGameEvent(Grid g, GameEvent ge) {
log.debug("New game event : {}", ge); log.debug("New game event : {}", ge);
g.newGameEvent(ge); g.newGameEvent(ge);
gRepository.save(g); gRepository.save(g);
wsc.onNewEvent(g, ge); forEachObserver(g, o -> o.onGameEvent(ge));
} }
private void setPhase(Grid g, Phase phase) { private void setPhase(Grid g, Phase phase) {
@@ -62,33 +73,40 @@ public class GridManager {
g.setCurrentHint(null); g.setCurrentHint(null);
} }
gRepository.save(g); gRepository.save(g);
wsc.onNewPhase(g); forEachObserver(g, o -> o.onNewPhase(phase));
} }
private void setWon(Grid g, boolean won) { private void setWon(Grid g, boolean won) {
log.debug("Game won: {}", won); log.debug("Game won: {}", won);
g.setWonOpt(Optional.of(won)); g.setWonOpt(Optional.of(won));
gRepository.save(g); gRepository.save(g);
forEachObserver(g, o -> o.onGridEnd(won));
// Grid will no longer be updated, therefore we stop observing it
observers.remove(g.getId());
} }
private void setCardPublic(Card c, char player) { private void setCardPointed(Grid g, Card c, char player, Color col) {
log.debug("Card set public, index {}", c.getIndex()); log.debug("Card set public, index {}", c.getIndex());
c.setColorPublic(player, true); c.setColorPublic(player, true);
cRepository.save(c); cRepository.save(c);
forEachObserver(c.getGrid(), o -> o.onPointCard(player, c, col));
g.incrementCurrentGuessCount();
gRepository.save(g);
} }
private void newHint(Grid g, Hint h) { private void newHint(Grid g, char player, Hint h) {
log.debug("New hint: {}", h); log.debug("New hint: {}", h);
g.incrementHintCount(); g.incrementHintCount();
g.setCurrentHint(h); g.setCurrentHint(h);
g.setCurrentGuessCount(0); g.setCurrentGuessCount(0);
gRepository.save(g); gRepository.save(g);
forEachObserver(g, o -> o.onSubmitHint(player, h));
} }
private void changeConfig(Grid g, MutableOption cc, Object data) { private void changeConfig(Grid g, MutableOption cc, Object data) {
log.debug("Changed config option: {} -> new value: {}", cc.name(), data); log.debug("Changed config option: {} -> new value: {}", cc.name(), data);
g.getConfig().set(cc, data); g.getConfig().set(cc, data);
gRepository.save(g); gRepository.save(g);
wsc.onConfigChange(g, cc, data); forEachObserver(g, o -> o.onConfigChange(cc, data));
} }
@@ -129,13 +147,13 @@ public class GridManager {
public Grid newGrid(@Nullable World world, GridConfig gc, Random r) { public Grid newGrid(@Nullable World world, GridConfig gc, Random r) {
UUID uuid = UUID.randomUUID(); UUID uuid = UUID.randomUUID();
String[] words = dictionnaries.randomWords(gc.getWordCount(), r); String[] words = dictionnaries.randomWords(gc.getColorDistribution().wordCount(), r);
List<CardColors> origDistrib = gc.getWordDistributionAsList(); List<CardColors> origDistrib = gc.getColorDistribution().asList();
List<CardColors> distrib; List<CardColors> distrib;
if(!gc.isStructuredGrid()) { if(!gc.isStructuredGrid()) {
List<Color> bColors = origDistrib.stream().map(cc -> cc.getFaceB()).collect(Collectors.toList()); List<Color> bColors = origDistrib.stream().map(cc -> cc.getFaceB()).collect(Collectors.toList());
Collections.shuffle(bColors, r); 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))) .mapToObj(i -> CardColors.of(origDistrib.get(i).getFaceA(), bColors.get(i)))
.toList(); .toList();
} else { } else {
@@ -169,12 +187,10 @@ public class GridManager {
* GAME ACTIONS * * GAME ACTIONS *
**********************************/ **********************************/
private void endGame(Grid g, boolean win) { private void endGame(Grid g, boolean win) {
setWon(g, win);
setPhase(g, Phase.GAME_ENDED); setPhase(g, Phase.GAME_ENDED);
newGameEvent(g, GameEvent.newGameEndEvent(win)); newGameEvent(g, GameEvent.newGameEndEvent(win));
if(win) { // This must be the last step as it removes observers
wm.onGameWon(g); setWon(g, win);
}
} }
public void proposeHint(Grid g, char player, String hint, int wordCount) throws IllegalGameActionException { public void proposeHint(Grid g, char player, String hint, int wordCount) throws IllegalGameActionException {
@@ -189,7 +205,7 @@ public class GridManager {
} }
Hint h = new Hint(hint, wordCount); Hint h = new Hint(hint, wordCount);
newHint(g, h); newHint(g, player, h);
newGameEvent(g, GameEvent.newHintEvent(h, player)); newGameEvent(g, GameEvent.newHintEvent(h, player));
setPhase(g, Phase.guessing(other(player))); setPhase(g, Phase.guessing(other(player)));
} }
@@ -264,8 +280,8 @@ public class GridManager {
if(c.isColorPublic(other(player)) || (g.getConfig().isOnlyOneGreenGreen() && c.getColor(player) == Card.Color.GREEN && c.isColorPublic(player))) if(c.isColorPublic(other(player)) || (g.getConfig().isOnlyOneGreenGreen() && c.getColor(player) == Card.Color.GREEN && c.isColorPublic(player)))
throw new IllegalGameActionException(g, "Card has already been revealed"); throw new IllegalGameActionException(g, "Card has already been revealed");
setCardPublic(c, other(player));
Card.Color guessedColor = c.getColor(other(player)); Card.Color guessedColor = c.getColor(other(player));
setCardPointed(g, c, other(player), guessedColor);
newGameEvent(g, GameEvent.newGuessEvent(c.getIndex(), player, c.getColor(other(player)))); newGameEvent(g, GameEvent.newGuessEvent(c.getIndex(), player, c.getColor(other(player))));
g.incrementCurrentGuessCount(); g.incrementCurrentGuessCount();
@@ -369,7 +385,7 @@ public class GridManager {
} }
public void unlockHintCount(Grid g, int newHintCount) throws IllegalGameActionException { public void unlockHintCount(Grid g, int newHintCount) throws IllegalGameActionException {
if(newHintCount > g.getConfig().getMaxMaxHint()) { if(newHintCount > g.getConfig().getMaxMaxHint()) {
throw new IllegalGameActionException(g, "Illegal hint count"); throw new IllegalGameActionException(g, "Hint count higher than highest hint count");
} }
if(newHintCount <= g.getConfig().getMaxHint()) { if(newHintCount <= g.getConfig().getMaxHint()) {
throw new IllegalGameActionException(g, "Cannot lower hint count"); throw new IllegalGameActionException(g, "Cannot lower hint count");
@@ -384,7 +400,10 @@ public class GridManager {
if (!newAvailableHintWordCounts.containsAll(g.getConfig().getAvailableHintWordCount()) if (!newAvailableHintWordCounts.containsAll(g.getConfig().getAvailableHintWordCount())
|| g.getConfig().getAvailableHintWordCount().containsAll(newAvailableHintWordCounts)) { || g.getConfig().getAvailableHintWordCount().containsAll(newAvailableHintWordCounts)) {
// i.e. If given data is not a strict superset // 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); 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; package com.bernard.nodecames.model;
import java.util.Map; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import lombok.AllArgsConstructor; import jakarta.persistence.OneToMany;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter; import lombok.Setter;
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor
@Getter @Getter
@Setter @Setter
@Entity @Entity
@@ -21,20 +22,22 @@ public class Game {
@Id @Id
private UUID 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))
));
}
@@ -12,6 +12,7 @@ import jakarta.annotation.Nullable;
import jakarta.persistence.AttributeConverter; import jakarta.persistence.AttributeConverter;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Convert; import jakarta.persistence.Convert;
import jakarta.persistence.Converter;
import jakarta.persistence.Embedded; import jakarta.persistence.Embedded;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.Id; import jakarta.persistence.Id;
@@ -19,6 +20,7 @@ import jakarta.persistence.ManyToOne;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.Setter; import lombok.Setter;
import tools.jackson.databind.JsonNode; import tools.jackson.databind.JsonNode;
import tools.jackson.databind.node.ArrayNode; import tools.jackson.databind.node.ArrayNode;
@@ -37,7 +39,6 @@ public class Grid {
@ManyToOne(optional = true) @ManyToOne(optional = true)
World w; World w;
//@Type(JsonType.class)
@Column(columnDefinition = "json") @Column(columnDefinition = "json")
@Convert(converter = GameEventListAdapter.class) @Convert(converter = GameEventListAdapter.class)
@JdbcTypeCode(SqlTypes.JSON) @JdbcTypeCode(SqlTypes.JSON)
@@ -158,10 +159,11 @@ public class Grid {
} }
} }
@AllArgsConstructor @RequiredArgsConstructor
@Converter
public static class GameEventListAdapter implements AttributeConverter<List<GameEvent>, JsonNode> { public static class GameEventListAdapter implements AttributeConverter<List<GameEvent>, JsonNode> {
JsonNodeFactory jsn; private final JsonNodeFactory jsn;
@Override @Override
public JsonNode convertToDatabaseColumn(List<GameEvent> attribute) { public JsonNode convertToDatabaseColumn(List<GameEvent> attribute) {
@@ -1,27 +1,17 @@
package com.bernard.nodecames.model; 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;
import java.util.Map.Entry;
import java.util.Set; import java.util.Set;
import java.util.stream.Collectors; 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.Convert;
import jakarta.persistence.Converter;
import jakarta.persistence.Embeddable; import jakarta.persistence.Embeddable;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Value; import lombok.Setter;
@Getter @Getter
@AllArgsConstructor @AllArgsConstructor
@@ -29,66 +19,30 @@ import lombok.Value;
@Embeddable @Embeddable
public class GridConfig { public class GridConfig {
public static final Map<CardColors, Integer> CUSTOM6 = public static final GridConfig ZERO =
makeDistribution(0, 0, 2, 1, 1, 0); WorldConfig.zeroWithSize(ColorDistribution.CLASSICAL25).makeGridConfig(Inventory.empty());
public static final Map<CardColors, Integer> CUSTOM9 = public static final GridConfig REGULAR =
makeDistribution(0, 1, 1, 2, 2, 1); WorldConfig.classicalWithSize(ColorDistribution.CLASSICAL25, 9).makeGridConfig(Inventory.empty());
public static final Map<CardColors, Integer> CUSTOM12 = public static final GridConfig BIG =
makeDistribution(1, 1, 1, 2, 3, 1); WorldConfig.classicalWithSize(ColorDistribution.CLASSICAL25, 9).makeGridConfig(Inventory.empty());
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, true, true, CUSTOM36, true,
true, true, 12, BASE_HINTWORDCOUNT, true,
Integer.MAX_VALUE, true, true,
true, false);
public static final GridConfig CODENAMES_CLASSICAL = new GridConfig(
9, true, true, CLASSICAL25, true,
true, true, 9, BASE_HINTWORDCOUNT, true,
Integer.MAX_VALUE, true, true,
true, false);
public static final GridConfig ZERO = new GridConfig(
9, false, false, CLASSICAL25, true,
true, false, 1, Set.of(1), false,
3, false, false,
false, false);
public static final GridConfig zeroWithSize(Map<CardColors, Integer> colors) {
return new GridConfig(
9, false, false, colors, true,
true, false, 1, Set.of(1), false,
3, false, false,
false, false);
}
// Immutable options // Immutable options
// Maximum of maximom number of hints // Maximum of maximom number of hints
// This is to trigger end of game // This is to trigger end of game
private int maxMaxHint; private int maxMaxHint;
private boolean structuredGrid; @Convert(converter = ColorDistributionConverter.class)
private boolean anyoneStarts; private ColorDistribution colorDistribution;
@Convert(converter = WordDistributionConverter.class)
private Map<CardColors, Integer> wordDistribution;
private boolean maybeSuddenDeath; //TODO private boolean maybeSuddenDeath; //TODO
private boolean maybeOneMoreGuess; //TODO private boolean maybeOneMoreGuess; //TODO
// If onlyOneGreenGreen is false and having it true would have meant we won, // If onlyOneGreenGreen is false and having it true would have meant we won,
// prevend death and enter WIN_IF_GREEN_GREEN state, else just die // prevend death and enter WIN_IF_GREEN_GREEN state, else just die
private boolean onlyOneGreenGreenPreventDeath; private boolean onlyOneGreenGreenPreventDeath;
// Game mutable options
@Setter
private boolean structuredGrid;
@Setter
private boolean anyoneStarts;
// Mutable options // Mutable options
private int maxHint; private int maxHint;
@@ -100,20 +54,6 @@ public class GridConfig {
private boolean suddenDeath; private boolean suddenDeath;
private boolean hintsInARow; 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) { public void set(MutableOption opt, Object value) {
try{ try{
switch(opt) { switch(opt) {
@@ -165,48 +105,9 @@ public class GridConfig {
return 0 <= i || i == -1; 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 @AllArgsConstructor
@Getter @Getter
//TODO do record type
public enum MutableOption { public enum MutableOption {
MAX_HINT(Integer.class), MAX_HINT(Integer.class),
AVAILABLE_HINT_WORD_COUNT(Set.class), AVAILABLE_HINT_WORD_COUNT(Set.class),
@@ -219,52 +120,4 @@ public class GridConfig {
Class<?> dataClass; 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,26 +26,34 @@ public class World {
@ManyToOne @ManyToOne
Game game; Game game;
// Config used when resetting this world
GridConfig initialConfig;
@ManyToOne(optional = true) @ManyToOne(optional = true)
Grid currentGrid; Grid currentGrid;
boolean won; boolean won;
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.id = new WorldId(g.getId(), name);
this.game = g; this.game = g;
this.initialConfig = initConfig;
this.currentGrid = currentGrid; this.currentGrid = currentGrid;
this.won = won; this.won = won;
this.locked = locked;
} }
public String getName() { public String getName() {
return this.getId().getName(); 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 @Data
@NoArgsConstructor @NoArgsConstructor
@AllArgsConstructor @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,20 @@
package com.bernard.nodecames.observer;
import com.bernard.nodecames.model.Card;
import com.bernard.nodecames.model.GameEvent;
import com.bernard.nodecames.model.Hint;
import com.bernard.nodecames.model.Card.Color;
import com.bernard.nodecames.model.Grid.Phase;
import com.bernard.nodecames.model.GridConfig.MutableOption;
public interface GridObserver {
public void onPointCard(char player, Card card, Color color);
public void onSubmitHint(char player, Hint hint);
public void onGridEnd(boolean won);
public void onGameEvent(GameEvent ge);
public void onNewPhase(Phase p);
public void onConfigChange(MutableOption cc, Object data);
}
@@ -0,0 +1,5 @@
package com.bernard.nodecames.observer;
public interface WorldObserver extends GridObserver {
}
@@ -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);
}
};
}
}
+63
View File
@@ -0,0 +1,63 @@
name: Tester
game: nodecames
nodecames:
progression_balancing: normal
accessibility: full
hint_word_logic_ratio: 25
start_hint_count: 1
max_hint_count: 10
start_hint_length: 3
max_hint_length: 12
start_hint_word_count: 1
max_hint_word_count: 16
worlds:
1-1:
locked: false
word_distribution: "GG1;GW2;GB0;WG2;WW1;WB0;BG0;BW0;BB0"
required: false
1-2:
locked: true
word_distribution: "GG2;GW1;GB1;WG1;WW2;WB0;BG1;BW0;BB1"
required: false
1-3:
locked: true
word_distribution: "GG2;GW1;GB1;WG1;WW2;WB0;BG1;BW0;BB1"
required: true
2-1:
locked: true
word_distribution: "GG2;GW1;GB1;WG1;WW3;WB1;BG1;BW1;BB1"
required: false
2-2:
locked: true
word_distribution: "GG2;GW2;GB1;WG2;WW5;WB1;BG1;BW1;BB1"
required: false
2-3:
locked: true
word_distribution: "GG2;GW2;GB1;WG2;WW5;WB1;BG1;BW1;BB1"
required: true
3-1:
locked: true
word_distribution: "GG2;GW4;GB1;WG4;WW5;WB1;BG1;BW1;BB1"
required: false
3-2:
locked: true
word_distribution: "GG3;GW5;GB1;WG5;WW7;WB1;BG1;BW1;BB1"
required: false
3-3:
locked: true
word_distribution: "GG3;GW5;GB1;WG5;WW7;WB1;BG1;BW1;BB1"
required: true
4-1:
locked: true
word_distribution: "GG3;GW5;GB2;WG5;WW7;WB2;BG2;BW2;BB1"
required: false
4-2:
locked: true
word_distribution: "GG5;GW6;GB2;WG6;WW9;WB2;BG2;BW2;BB2"
required: false
4-3:
locked: true
word_distribution: "GG5;GW6;GB2;WG6;WW9;WB2;BG2;BW2;BB2"
required: true
@@ -0,0 +1 @@
alter table if exists world add column locked boolean not null default false;
@@ -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
@@ -15,3 +15,7 @@ main{
flex-direction: column; flex-direction: column;
margin-top: 115px; margin-top: 115px;
} }
main section {
padding: 1em;
}
+4 -4
View File
@@ -256,10 +256,10 @@ function selectPlayer(player) {
socket = Stomp.over(new SockJS('/socket')); socket = Stomp.over(new SockJS('/socket'));
socket.connect({}, function (frame) { socket.connect({}, function (frame) {
socket.subscribe('/topic/new-phase', onNewPhase); socket.subscribe('/topic/'+roomId+'/new-phase', onNewPhase);
socket.subscribe('/topic/new-event', onNewEvent); socket.subscribe('/topic/'+roomId+'/new-event', onNewEvent);
socket.subscribe('/topic/update-card/' + player.toLowerCase(), onUpdateCard); socket.subscribe('/topic/'+roomId+'/update-card/' + player.toLowerCase(), onUpdateCard);
socket.subscribe('/topic/config-change', onConfigChange) socket.subscribe('/topic/'+roomId+'/config-change', onConfigChange)
}); });
} }
+4
View File
@@ -17,6 +17,10 @@
<form method="GET" th:action="@{/create-game}"> <form method="GET" th:action="@{/create-game}">
<label for="create-game-name">Name : </label> <label for="create-game-name">Name : </label>
<input type="text" id="create-game-name" name="name"/> <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"/> <input type="submit" value="Create game"/>
</form> </form>
+20 -7
View File
@@ -10,13 +10,26 @@
</head> </head>
<body> <body>
<main> <main>
<ul> <section>
<li th:each="world : ${worlds}"> <h2>Worlds</h2><br/>
<a th:href="@{/game/{gid}/grid/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">World <span th:text="${world.name}"/></a> <ul>
<a th:href="@{/game/{gid}/reset/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">reset</a> <li th:each="world : ${worlds}">
<span th:if="${world.won}">Gagné !</span> <!-- TODO remove this duplication of World span -->
</li> <span th:if="${world.locked}"> World <span th:text="${world.name}"/> </span>
</ul> <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> </main>
</body> </body>
</html> </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);
}
}
+2
View File
@@ -11,6 +11,8 @@ version.com.fasterxml.jackson.core..jackson-databind=2.22.2
version.com.fasterxml.jackson.datatype..jackson-datatype-hibernate5=2.22.2 version.com.fasterxml.jackson.datatype..jackson-datatype-hibernate5=2.22.2
version.io.github.archipelagomw..Java-Client=0.2.1
version.jakarta.validation..jakarta.validation-api=3.1.1 version.jakarta.validation..jakarta.validation-api=3.1.1
## # available=4.0.0-M1 ## # available=4.0.0-M1