Compare commits
9
Commits
44e5a01727
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d23aedb924 | ||
|
|
741f60bb3b | ||
|
|
466da86969 | ||
|
|
0bb880d434 | ||
|
|
fa82900770 | ||
|
|
89cac6a0c6 | ||
|
|
90f3f7afb0 | ||
|
|
217c98a94a | ||
|
|
6127508211 |
@@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
|
||||||
|
## License
|
||||||
|
Dictionaries taken from [this project](https://github.com/LandazuriPaul/Codenames)
|
||||||
@@ -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:_'
|
||||||
|
|||||||
@@ -15,12 +15,18 @@
|
|||||||
gradle = pkgs.gradle;
|
gradle = pkgs.gradle;
|
||||||
version = "1.0";
|
version = "1.0";
|
||||||
|
|
||||||
|
nodecames-apworld = pkgs.runCommand "nodecames.apworld" {} ''
|
||||||
|
cp -r ${./src/main/apworld} ./nodecames/
|
||||||
|
${pkgs.zip}/bin/zip -r $out ./nodecames/
|
||||||
|
'';
|
||||||
|
|
||||||
nodecames = pkgs.buildGradleArtifact {
|
nodecames = pkgs.buildGradleArtifact {
|
||||||
pname = "nodecames";
|
pname = "nodecames";
|
||||||
src = ./.;
|
src = ./.;
|
||||||
buildTask = "bootJar";
|
buildTask = "bootJar";
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
cp -r "build/libs/" "$out"
|
cp -r "build/libs/" "$out"
|
||||||
|
cp -r ./build/resources/ $out/resources/
|
||||||
'';
|
'';
|
||||||
inherit version jdk gradle;
|
inherit version jdk gradle;
|
||||||
};
|
};
|
||||||
@@ -33,20 +39,49 @@
|
|||||||
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;
|
||||||
inherit nodecames;
|
inherit nodecames nodecames-apworld;
|
||||||
};
|
};
|
||||||
|
|
||||||
apps.${system} = {
|
apps.${system} = {
|
||||||
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} = {
|
||||||
@@ -54,10 +89,12 @@
|
|||||||
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; [
|
||||||
redhat.java
|
redhat.java
|
||||||
|
ms-python.python
|
||||||
vscjava.vscode-java-debug
|
vscjava.vscode-java-debug
|
||||||
vscjava.vscode-java-test
|
vscjava.vscode-java-test
|
||||||
vscjava.vscode-gradle
|
vscjava.vscode-gradle
|
||||||
|
|||||||
@@ -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"/>
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
from .world import NodecamesWorld
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"game": "NodeCames",
|
||||||
|
"minimum_ap_version": "0.6.8",
|
||||||
|
"world_version": "1.0.0",
|
||||||
|
"authors": ["Mysaa"]
|
||||||
|
}
|
||||||
@@ -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)}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .world import NodecamesWorld
|
||||||
|
|
||||||
|
from BaseClasses import Item, ItemClassification
|
||||||
|
|
||||||
|
from . import const as c
|
||||||
|
|
||||||
|
class NodecamesItem(Item):
|
||||||
|
game = "Nodecames"
|
||||||
|
|
||||||
|
def get_random_filler_item_name(world: NodecamesWorld) -> str:
|
||||||
|
return c.filler_item
|
||||||
|
|
||||||
|
|
||||||
|
def create_item_with_correct_classification(
|
||||||
|
world: NodecamesWorld, name: str
|
||||||
|
) -> NodecamesItem:
|
||||||
|
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:
|
||||||
|
itempool: list[Item] = []
|
||||||
|
|
||||||
|
hintItemCount = world.options.max_hint_count - world.options.start_hint_count
|
||||||
|
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 += [
|
||||||
|
world.create_item("zero_hint"),
|
||||||
|
world.create_item("infinity_hint"),
|
||||||
|
world.create_item("one_more_guess"),
|
||||||
|
world.create_item("hint_max_length"),
|
||||||
|
world.create_item("end_guessing"),
|
||||||
|
world.create_item("only_one_green_green"),
|
||||||
|
world.create_item("sudden_death"),
|
||||||
|
world.create_item("structured_grid"),
|
||||||
|
world.create_item("hints_in_a_row"),
|
||||||
|
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)
|
||||||
|
|
||||||
|
print(to_complete,"items to fill")
|
||||||
|
|
||||||
|
itempool += [world.create_filler() for _ in range(to_complete)]
|
||||||
|
|
||||||
|
world.multiworld.itempool += itempool
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
|
from Options import Choice, OptionGroup, PerGameCommonOptions, Range, DefaultOnToggle, OptionDict
|
||||||
|
from schema import Schema, And
|
||||||
|
from . import const as c
|
||||||
|
|
||||||
|
class WorldsLocked(DefaultOnToggle):
|
||||||
|
"""
|
||||||
|
Access to worlds need to be unlocked by an item
|
||||||
|
"""
|
||||||
|
|
||||||
|
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):
|
||||||
|
"""
|
||||||
|
Hint count at the beginning of the game
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Start hint count"
|
||||||
|
|
||||||
|
range_start = 1
|
||||||
|
range_end = 20
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
class MaxHintCount(Range):
|
||||||
|
"""
|
||||||
|
Maximum hint count we can get
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Max hint count"
|
||||||
|
|
||||||
|
range_start = 1
|
||||||
|
range_end = 20
|
||||||
|
default = 10
|
||||||
|
|
||||||
|
|
||||||
|
class StartHintLength(Range):
|
||||||
|
"""
|
||||||
|
Maximal Hint length at the beginning of the game
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Start Hint Length"
|
||||||
|
|
||||||
|
range_start = 3
|
||||||
|
range_end = 30
|
||||||
|
default = 3
|
||||||
|
|
||||||
|
|
||||||
|
class MaxHintLength(Range):
|
||||||
|
"""
|
||||||
|
Maximal maximum hint length we can get. Once it is reached, maximal length is now infinity.
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Max Hint Length"
|
||||||
|
|
||||||
|
range_start = 3
|
||||||
|
range_end = 30
|
||||||
|
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):
|
||||||
|
option_Off = 0
|
||||||
|
option_Item = 1
|
||||||
|
option_On = 2
|
||||||
|
|
||||||
|
default = 1
|
||||||
|
|
||||||
|
|
||||||
|
class ZeroHint(OnOffDefault):
|
||||||
|
"""
|
||||||
|
Can give zero as a hint
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Hint Count 0"
|
||||||
|
|
||||||
|
|
||||||
|
class InfinityHint(OnOffDefault):
|
||||||
|
"""
|
||||||
|
Can give infinity as a hint
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Hint Count infty"
|
||||||
|
|
||||||
|
|
||||||
|
class OneMoreGuess(OnOffDefault):
|
||||||
|
"""
|
||||||
|
Can guess one word more than hint count
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "One More Guess"
|
||||||
|
|
||||||
|
|
||||||
|
class EndGuessing(OnOffDefault):
|
||||||
|
"""
|
||||||
|
Can end guessing before touching the given hint count
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "End Guessing"
|
||||||
|
|
||||||
|
|
||||||
|
class OnlyOneGreenGreen(OnOffDefault):
|
||||||
|
"""
|
||||||
|
Green-Green cards only have to be pointed by one of the players
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Only One Green Green"
|
||||||
|
|
||||||
|
|
||||||
|
class SuddenDeath(OnOffDefault):
|
||||||
|
"""
|
||||||
|
Sudden death triggered when no more hints can be given
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Sudden Death"
|
||||||
|
|
||||||
|
|
||||||
|
class StructuredGrid(OnOffDefault):
|
||||||
|
"""
|
||||||
|
Are front and back colors of the cards linked
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "Structured Grid"
|
||||||
|
|
||||||
|
|
||||||
|
class HintsInARow(OnOffDefault):
|
||||||
|
"""
|
||||||
|
Players can give multiple hints in a row
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "HintsInARow"
|
||||||
|
|
||||||
|
|
||||||
|
class AnyoneStarts(OnOffDefault):
|
||||||
|
"""
|
||||||
|
Both players can start the game
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "AnyoneStarts"
|
||||||
|
|
||||||
|
|
||||||
|
class WorldCount(Range):
|
||||||
|
"""
|
||||||
|
How many worlds to generate
|
||||||
|
"""
|
||||||
|
|
||||||
|
display_name = "World Count"
|
||||||
|
|
||||||
|
range_start = 1
|
||||||
|
range_end = 12
|
||||||
|
default = 12
|
||||||
|
|
||||||
|
|
||||||
|
class VictoryCondition(Choice):
|
||||||
|
"""
|
||||||
|
What is needed to complete game
|
||||||
|
"""
|
||||||
|
|
||||||
|
option_AllWorlds = 1
|
||||||
|
option_LastWorld = 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.
|
||||||
|
# This is in the format "option_name_in_snake_case: OptionClassName".
|
||||||
|
@dataclass
|
||||||
|
class NodecamesOptions(PerGameCommonOptions):
|
||||||
|
worlds_locked: WorldsLocked
|
||||||
|
hint_word_logic_ratio: HintWordLogicRatio
|
||||||
|
victory_condition: VictoryCondition
|
||||||
|
start_hint_count: StartHintCount
|
||||||
|
max_hint_count: MaxHintCount
|
||||||
|
start_hint_length: StartHintLength
|
||||||
|
max_hint_length: MaxHintLength
|
||||||
|
start_hint_word_count: StartHintWordCount
|
||||||
|
max_hint_word_count: MaxHintWordCount
|
||||||
|
zero_hint: ZeroHint
|
||||||
|
infinity_hint: InfinityHint
|
||||||
|
one_more_guess: OneMoreGuess
|
||||||
|
end_guessing: EndGuessing
|
||||||
|
only_one_green_green: OnlyOneGreenGreen
|
||||||
|
sudden_death: SuddenDeath
|
||||||
|
structured_grid: StructuredGrid
|
||||||
|
hints_in_a_row: HintsInARow
|
||||||
|
anyone_starts: AnyoneStarts
|
||||||
|
worlds: WorldsOption
|
||||||
|
|
||||||
|
|
||||||
|
# If we want to group our options by similar type, we can do so as well. This looks nice on the website.
|
||||||
|
option_groups = [
|
||||||
|
OptionGroup(
|
||||||
|
"General Options",
|
||||||
|
[WorldsLocked, VictoryCondition, HintWordLogicRatio],
|
||||||
|
),
|
||||||
|
OptionGroup(
|
||||||
|
"Grid Options",
|
||||||
|
[
|
||||||
|
StartHintCount,
|
||||||
|
MaxHintCount,
|
||||||
|
StartHintLength,
|
||||||
|
MaxHintLength,
|
||||||
|
ZeroHint,
|
||||||
|
InfinityHint,
|
||||||
|
OneMoreGuess,
|
||||||
|
EndGuessing,
|
||||||
|
OnlyOneGreenGreen,
|
||||||
|
SuddenDeath,
|
||||||
|
StructuredGrid,
|
||||||
|
HintsInARow,
|
||||||
|
AnyoneStarts,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
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".
|
||||||
|
option_presets = {
|
||||||
|
"default": {
|
||||||
|
"hint_word_logic_ratio": 25,
|
||||||
|
"worlds_locked": True,
|
||||||
|
"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,
|
||||||
|
"zero_hint": 1,
|
||||||
|
"infinity_hint": 1,
|
||||||
|
"one_more_guess": 1,
|
||||||
|
"end_guessing": 1,
|
||||||
|
"only_one_green_green": 1,
|
||||||
|
"sudden_death": 1,
|
||||||
|
"structured_grid": 1,
|
||||||
|
"hints_in_a_row": 1,
|
||||||
|
"anyone_starts": 1,
|
||||||
|
"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,
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .world import NodecamesWorld
|
||||||
|
|
||||||
|
|
||||||
|
from rule_builder.options import OptionFilter
|
||||||
|
from rule_builder.rules import Has, HasAll, Rule, True_
|
||||||
|
from BaseClasses import Region, Location
|
||||||
|
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)
|
||||||
|
|
||||||
|
class NodecamesLocation(Location):
|
||||||
|
game = "Nodecames"
|
||||||
|
|
||||||
|
def total_word_count(distribution):
|
||||||
|
return distribution['GB']+distribution['BG']+distribution['GW']+distribution['WG']+distribution['GG']
|
||||||
|
|
||||||
|
def can_access_word(options, word):
|
||||||
|
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 can_access_world(options, wn, wi):
|
||||||
|
if options.worlds[wn]['locked']:
|
||||||
|
return Has(f"unlock_world_{wi}")
|
||||||
|
else:
|
||||||
|
return True_()
|
||||||
|
|
||||||
|
def create_all_regions(world: NodecamesWorld) -> None:
|
||||||
|
#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
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
from collections.abc import Mapping
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from worlds.AutoWorld import World
|
||||||
|
|
||||||
|
from . import items, rules
|
||||||
|
from . import options as nodecames_options
|
||||||
|
from . import const as c
|
||||||
|
|
||||||
|
class NodecamesWorld(World):
|
||||||
|
"""
|
||||||
|
Nodecames is a word game, inspired by Code Names by Vlaada Chvátil
|
||||||
|
"""
|
||||||
|
|
||||||
|
game = "nodecames"
|
||||||
|
|
||||||
|
options_dataclass = nodecames_options.NodecamesOptions
|
||||||
|
options: nodecames_options.NodecamesOptions
|
||||||
|
|
||||||
|
location_name_to_id = c.LOCATION_NAME_TO_ID
|
||||||
|
item_name_to_id = c.ITEM_NAME_TO_ID
|
||||||
|
|
||||||
|
origin_region_name = "Map"
|
||||||
|
|
||||||
|
# Create regions, locations and rules
|
||||||
|
def create_regions(self) -> None:
|
||||||
|
rules.create_all_regions(self)
|
||||||
|
|
||||||
|
def create_items(self) -> None:
|
||||||
|
items.create_all_items(self)
|
||||||
|
|
||||||
|
# Create an item from name
|
||||||
|
def create_item(self, name: str) -> items.NodecamesItem:
|
||||||
|
return items.create_item_with_correct_classification(self, name)
|
||||||
|
|
||||||
|
# Create one filler item
|
||||||
|
def get_filler_item_name(self) -> str:
|
||||||
|
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
|
||||||
|
def fill_slot_data(self) -> Mapping[str, Any]:
|
||||||
|
return []
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
package com.bernard.nodecames;
|
package com.bernard.nodecames;
|
||||||
|
|
||||||
|
import java.net.URISyntaxException;
|
||||||
|
import java.util.Comparator;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map.Entry;
|
import java.util.Map.Entry;
|
||||||
|
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.ResponseEntity;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.validation.BindingResult;
|
import org.springframework.validation.BindingResult;
|
||||||
@@ -12,50 +22,85 @@ import org.springframework.web.bind.annotation.GetMapping;
|
|||||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.servlet.view.RedirectView;
|
import org.springframework.web.servlet.view.RedirectView;
|
||||||
|
|
||||||
import com.bernard.nodecames.frontend.GridConfigEdit;
|
import com.bernard.nodecames.frontend.GridConfigEdit;
|
||||||
|
import com.bernard.nodecames.game.ArchipelagoManager;
|
||||||
import com.bernard.nodecames.game.GameManager;
|
import com.bernard.nodecames.game.GameManager;
|
||||||
|
import com.bernard.nodecames.game.GridManager;
|
||||||
import com.bernard.nodecames.game.IllegalGameActionException;
|
import com.bernard.nodecames.game.IllegalGameActionException;
|
||||||
|
import com.bernard.nodecames.model.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;
|
||||||
|
import com.bernard.nodecames.model.World;
|
||||||
|
import com.bernard.nodecames.repository.GameRepository;
|
||||||
|
import com.bernard.nodecames.repository.GridRepository;
|
||||||
|
import com.bernard.nodecames.repository.WorldRepository;
|
||||||
|
|
||||||
|
import io.github.archipelagomw.network.ConnectionResult;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Controller
|
@Controller
|
||||||
public class HttpController {
|
public class HttpController {
|
||||||
|
|
||||||
private final GameManager gm;
|
private final GridManager gm;
|
||||||
|
private final GameManager wm;
|
||||||
|
private final ArchipelagoManager am;
|
||||||
|
private final GridRepository gridRepository;
|
||||||
|
private final GameRepository gRepository;
|
||||||
|
private final WorldRepository wRepository;
|
||||||
private final Random rand = new Random();
|
private final Random rand = new Random();
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/")
|
||||||
public String index() {
|
public String index(Model m) {
|
||||||
|
List<Game> games = wm.allGames().stream().sorted(Comparator.comparing(Game::getName)).toList();
|
||||||
|
m.addAttribute("games", games);
|
||||||
return "index.html";
|
return "index.html";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/room/{id}/grid")
|
@GetMapping("/room/{id}/grid")
|
||||||
public String grid(@PathVariable("id") String id) {
|
public Object grid(@PathVariable("id") String id, @RequestParam(value = "player", required = false) String player, Model m) {
|
||||||
|
if(player == null) {
|
||||||
|
m.addAttribute("redirect", "/room/"+id+"/grid");
|
||||||
|
return "player-select";
|
||||||
|
}
|
||||||
|
Optional<Grid> grid = gridRepository.findById(UUID.fromString(id));
|
||||||
|
if(!grid.isPresent()) {
|
||||||
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
m.addAttribute("player", player);
|
||||||
|
m.addAttribute("grid", grid.get());
|
||||||
return "grid";
|
return "grid";
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/room/{id}/config")
|
@GetMapping("/room/{id}/config")
|
||||||
public String gridConfig(@PathVariable("id") String id, Model model) {
|
public Object gridConfig(@PathVariable("id") String id, Model model) {
|
||||||
GridConfig gc = gm.findGrid(id).getConfig();
|
Optional<Grid> grid = gridRepository.findById(UUID.fromString(id));
|
||||||
|
if(!grid.isPresent()) {
|
||||||
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
GridConfig gc = grid.get().getConfig();
|
||||||
model.addAttribute("gridConfig", GridConfigEdit.of(gc));
|
model.addAttribute("gridConfig", GridConfigEdit.of(gc));
|
||||||
return "grid-config";
|
return "grid-config";
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/room/{id}/config")
|
@PostMapping("/room/{id}/config")
|
||||||
public String setGridConfig(@PathVariable("id") String id,
|
public Object setGridConfig(@PathVariable("id") String id,
|
||||||
@ModelAttribute("gridConfig") GridConfigEdit gce,
|
@ModelAttribute("gridConfig") GridConfigEdit gce,
|
||||||
BindingResult br,
|
BindingResult br,
|
||||||
Model model
|
Model model
|
||||||
) {
|
) {
|
||||||
Grid g = gm.findGrid(id);
|
Optional<Grid> grid = gridRepository.findById(UUID.fromString(id));
|
||||||
for(Entry<MutableOption, Object> e : gce.getModifications(g.getConfig()).entrySet()) {
|
if(!grid.isPresent()) {
|
||||||
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
for(Entry<MutableOption, Object> e : gce.getModifications(grid.get().getConfig()).entrySet()) {
|
||||||
try {
|
try {
|
||||||
gm.unlock(g, e.getKey(), e.getValue());
|
gm.unlock(grid.get(), e.getKey(), e.getValue());
|
||||||
} catch (IllegalGameActionException ige) {
|
} catch (IllegalGameActionException ige) {
|
||||||
br.addError(new ObjectError(e.getKey().name(), ige.getMessage()));
|
br.addError(new ObjectError(e.getKey().name(), ige.getMessage()));
|
||||||
}
|
}
|
||||||
@@ -64,12 +109,117 @@ public class HttpController {
|
|||||||
return "grid-config";
|
return "grid-config";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@GetMapping("/create-room")
|
@GetMapping("/create-room")
|
||||||
public RedirectView createRoom() {
|
public RedirectView createRoom(@RequestParam("type") String type) {
|
||||||
|
GridConfig cfg = switch(type) {
|
||||||
|
case "zero" -> GridConfig.ZERO;
|
||||||
|
case "base" -> GridConfig.REGULAR;
|
||||||
|
case "big" -> GridConfig.BIG;
|
||||||
|
default -> GridConfig.REGULAR;
|
||||||
|
};
|
||||||
Random r = new Random(rand.nextLong());
|
Random r = new Random(rand.nextLong());
|
||||||
UUID uuid = gm.newGrid(GridConfig.ZERO, r);
|
Grid g = gm.newGrid(null, cfg, r);
|
||||||
return new RedirectView("/room/"+uuid.toString()+"/grid");
|
return new RedirectView("/room/"+g.getId()+"/grid");
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/create-game")
|
||||||
|
public RedirectView createGame(@RequestParam("name") String name, @RequestParam("type") String type) {
|
||||||
|
GameConfig cfg = switch(type) {
|
||||||
|
case "default" -> GameConfig.DEFAULT_GAME_CONFIG;
|
||||||
|
case "regular" -> GameConfig.REGULAR_GAME_CONFIG;
|
||||||
|
default -> GameConfig.DEFAULT_GAME_CONFIG;
|
||||||
|
};
|
||||||
|
Game g = wm.createGame(name, cfg);
|
||||||
|
return new RedirectView("/game/"+g.getId().toString()+"/map");
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/game/{id}/map")
|
||||||
|
public Object getMap(@PathVariable("id") String id, @RequestParam(value = "player", required = false) String player, Model m) {
|
||||||
|
if(player == null) {
|
||||||
|
m.addAttribute("redirect", "/game/"+id+"/map");
|
||||||
|
return "player-select";
|
||||||
|
}
|
||||||
|
Optional<Game> g = gRepository.findById(UUID.fromString(id));
|
||||||
|
if(!g.isPresent()) {
|
||||||
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
List<World> worlds = wm.allWorlds(g.get())
|
||||||
|
.stream()
|
||||||
|
.sorted(Comparator.comparing(World::getName))
|
||||||
|
.toList();
|
||||||
|
m.addAttribute("game", g.get());
|
||||||
|
m.addAttribute("worlds", worlds);
|
||||||
|
m.addAttribute("items", g.get().getInventory().getReceivedItems());
|
||||||
|
m.addAttribute("player", player);
|
||||||
|
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}")
|
||||||
|
public Object gridRedirect(@PathVariable("id") String id, @PathVariable("world") String world, @RequestParam("player") String player) {
|
||||||
|
Optional<Game> g = gRepository.findById(UUID.fromString(id));
|
||||||
|
if(!g.isPresent()) {
|
||||||
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
Optional<World> w = wRepository.findByGameAndId_Name(g.get(), world);
|
||||||
|
if(!w.isPresent()) {
|
||||||
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
Grid grid;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/game/{id}/reset/{world}")
|
||||||
|
public Object resetWorld(@PathVariable("id") String id, @PathVariable("world") String world, @RequestParam("player") String player) {
|
||||||
|
Optional<Game> g = gRepository.findById(UUID.fromString(id));
|
||||||
|
if(!g.isPresent()) {
|
||||||
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
Optional<World> w = wRepository.findByGameAndId_Name(g.get(), world);
|
||||||
|
if(!w.isPresent()) {
|
||||||
|
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,9 @@ package com.bernard.nodecames;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
|
||||||
|
import tools.jackson.databind.node.JsonNodeFactory;
|
||||||
|
|
||||||
@SpringBootApplication
|
@SpringBootApplication
|
||||||
public class NodecamesApplication {
|
public class NodecamesApplication {
|
||||||
@@ -10,4 +13,9 @@ public class NodecamesApplication {
|
|||||||
SpringApplication.run(NodecamesApplication.class, args);
|
SpringApplication.run(NodecamesApplication.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JsonNodeFactory theJsonNodeFactory() {
|
||||||
|
return JsonNodeFactory.instance;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +1,31 @@
|
|||||||
package com.bernard.nodecames.frontend;
|
package com.bernard.nodecames.frontend;
|
||||||
|
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.stream.IntStream;
|
|
||||||
|
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import com.bernard.nodecames.model.Card;
|
import com.bernard.nodecames.model.Card;
|
||||||
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.GridConfig;
|
|
||||||
import com.bernard.nodecames.model.GridConfig.CardColors;
|
|
||||||
import com.bernard.nodecames.model.GridConfig.MutableOption;
|
|
||||||
import com.bernard.nodecames.model.Grid.Phase;
|
import com.bernard.nodecames.model.Grid.Phase;
|
||||||
import com.bernard.nodecames.model.Hint;
|
import com.bernard.nodecames.model.GridConfig;
|
||||||
|
import com.bernard.nodecames.model.GridConfig.MutableOption;
|
||||||
|
import com.bernard.nodecames.model.converter.ColorDistributionConverter;
|
||||||
|
import com.bernard.nodecames.repository.CardRepository;
|
||||||
|
|
||||||
|
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
|
||||||
|
@AllArgsConstructor
|
||||||
public class JsonDataService {
|
public class JsonDataService {
|
||||||
|
|
||||||
private static final JsonNodeFactory jsn = JsonNodeFactory.instance;
|
private final JsonNodeFactory jsn;
|
||||||
|
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();
|
||||||
@@ -40,14 +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(CardColors c : gc.getWordDistribution()) {
|
|
||||||
ObjectNode ccNode = jsn.objectNode();
|
|
||||||
ccNode.set("face-a", jsn.stringNode(c.getFaceA().name()));
|
|
||||||
ccNode.set("face-b", jsn.stringNode(c.getFaceB().name()));
|
|
||||||
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()));
|
||||||
@@ -102,35 +98,8 @@ public class JsonDataService {
|
|||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonNode eventData(Grid g, GameEvent ge) {
|
public JsonNode eventData(GameEvent ge) {
|
||||||
ObjectNode geNode = jsn.objectNode();
|
return GameEvent.toJsonNode(ge);
|
||||||
geNode.set("type", jsn.stringNode(ge.getType().name().toLowerCase()));
|
|
||||||
geNode.set("issuer", jsn.stringNode(Character.toString(ge.getIssuer())));
|
|
||||||
switch (ge.getType()) {
|
|
||||||
case GameEvent.Type.HINT:
|
|
||||||
Hint hint = (Hint)ge.getData();
|
|
||||||
geNode.set("word", jsn.stringNode(hint.getWord()));
|
|
||||||
geNode.set("wordCount", jsn.numberNode(hint.getWordCount()));
|
|
||||||
break;
|
|
||||||
case GameEvent.Type.GUESS:
|
|
||||||
Card c = (Card)ge.getData();
|
|
||||||
Integer pos = IntStream.range(0, g.getCardCount())
|
|
||||||
.filter(i -> c == g.getCards()[i])
|
|
||||||
.findFirst()
|
|
||||||
.getAsInt();
|
|
||||||
geNode.set("card-index", jsn.numberNode(pos));
|
|
||||||
geNode.set("color", jsn.stringNode(((ge.getIssuer()=='A')?c.getColorB():c.getColorA()).name().toLowerCase()));
|
|
||||||
break;
|
|
||||||
case GameEvent.Type.END_GUESSING:
|
|
||||||
geNode.set("manual", jsn.booleanNode((Boolean)ge.getData()));
|
|
||||||
break;
|
|
||||||
case GameEvent.Type.GAME_END:
|
|
||||||
geNode.set("win", jsn.booleanNode((Boolean)ge.getData()));
|
|
||||||
break;
|
|
||||||
case GameEvent.Type.SUDDEN_DEATH:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return geNode;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public JsonNode phaseData(Phase p) {
|
public JsonNode phaseData(Phase p) {
|
||||||
@@ -148,18 +117,19 @@ public class JsonDataService {
|
|||||||
|
|
||||||
public JsonNode gridData(Grid g, char player) {
|
public JsonNode gridData(Grid g, char player) {
|
||||||
|
|
||||||
ArrayNode cardsNode = jsn.arrayNode(g.getCardCount());
|
Card[] cards = cRepository.findByGridOrderById_Index(g);
|
||||||
for (int i = 0; i < g.getCardCount(); i++)
|
ArrayNode cardsNode = jsn.arrayNode(cards.length);
|
||||||
cardsNode.add(cardData(g.getCards()[i], player));
|
for (int i = 0; i < cards.length; i++)
|
||||||
|
cardsNode.add(cardData(cards[i], player));
|
||||||
|
|
||||||
ArrayNode eventsNode = jsn.arrayNode(g.getGameEvents().size());
|
ArrayNode eventsNode = jsn.arrayNode(g.getGameEvents().size());
|
||||||
for(GameEvent ge : g.getGameEvents())
|
for(GameEvent ge : g.getGameEvents())
|
||||||
eventsNode.add(eventData(g, ge));
|
eventsNode.add(eventData(ge));
|
||||||
|
|
||||||
ObjectNode currentHintNode = jsn.objectNode();
|
ObjectNode currentHintNode = jsn.objectNode();
|
||||||
if(g.getCurrentHint() != null) {
|
if(g.getCurrentHint() != null) {
|
||||||
currentHintNode.set("word", jsn.stringNode(g.getCurrentHint().getWord()));
|
currentHintNode.set("word", jsn.stringNode(g.getCurrentHint().getWord()));
|
||||||
currentHintNode.set("wordCount", jsn.numberNode(g.getCurrentHint().getWordCount()));
|
currentHintNode.set("word-count", jsn.numberNode(g.getCurrentHint().getWordCount()));
|
||||||
}
|
}
|
||||||
|
|
||||||
ObjectNode out = jsn.objectNode();
|
ObjectNode out = jsn.objectNode();
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
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.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
@@ -12,65 +16,43 @@ import org.springframework.stereotype.Controller;
|
|||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
|
||||||
import com.bernard.nodecames.game.GameManager;
|
import com.bernard.nodecames.game.GridManager;
|
||||||
import com.bernard.nodecames.game.IllegalGameActionException;
|
import com.bernard.nodecames.game.IllegalGameActionException;
|
||||||
import com.bernard.nodecames.model.Card;
|
import com.bernard.nodecames.model.Card;
|
||||||
|
import com.bernard.nodecames.model.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 lombok.AllArgsConstructor;
|
||||||
import tools.jackson.databind.JsonNode;
|
import tools.jackson.databind.JsonNode;
|
||||||
|
|
||||||
@Controller
|
@Controller
|
||||||
|
@AllArgsConstructor
|
||||||
public class WebSocketController {
|
public class WebSocketController {
|
||||||
|
|
||||||
private GameManager gm;
|
private final GridManager gm;
|
||||||
private SimpMessagingTemplate template;
|
private final SimpMessagingTemplate template;
|
||||||
private JsonDataService json;
|
private final JsonDataService json;
|
||||||
|
private final GridRepository gRepository;
|
||||||
|
|
||||||
public WebSocketController(GameManager gm, SimpMessagingTemplate template, JsonDataService json) {
|
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());
|
||||||
gm.setWsc(this);
|
gm.addObserver(g, new GObserver(g));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onNewEvent(Grid g, GameEvent ge) {
|
|
||||||
this.template.convertAndSend("/topic/new-event",
|
|
||||||
json.eventData(g, 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, int cardIndex, char player) {
|
|
||||||
Card c = g.getCards()[cardIndex];
|
|
||||||
this.template.convertAndSend(
|
|
||||||
"/topic/update-card/" + Character.toLowerCase(player),
|
|
||||||
json.cardData(c, player),
|
|
||||||
Map.of("cardIndex", cardIndex)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
public void publishCardUpdate(Grid g, int cardIndex) {
|
|
||||||
publishCardUpdateToPlayer(g, cardIndex, 'A');
|
|
||||||
publishCardUpdateToPlayer(g, cardIndex, '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;
|
||||||
@@ -81,20 +63,29 @@ public class WebSocketController {
|
|||||||
else
|
else
|
||||||
throw new IllegalArgumentException("Unknown player type");
|
throw new IllegalArgumentException("Unknown player type");
|
||||||
|
|
||||||
Grid g = gm.findGrid(gridId);
|
Optional<Grid> g = gRepository.findById(UUID.fromString(gridId));
|
||||||
|
if(g.isPresent()){
|
||||||
return new ResponseEntity<>(json.gridData(g, player), HttpStatus.OK);
|
Grid grid = g.get();
|
||||||
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@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();
|
||||||
|
|
||||||
Grid g = gm.findGrid(roomId);
|
Optional<Grid> g = gRepository.findById(UUID.fromString(roomId));
|
||||||
|
if(!g.isPresent())
|
||||||
|
// 404
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
gm.proposeHint(g, player.charAt(0), hint, wordCount);
|
gm.proposeHint(g.get(), player.charAt(0), hint, wordCount);
|
||||||
} catch (IllegalGameActionException e) {
|
} catch (IllegalGameActionException e) {
|
||||||
publishError(player.charAt(0), e);
|
publishError(player.charAt(0), e);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -105,10 +96,12 @@ public class WebSocketController {
|
|||||||
public void pointCard(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) {
|
public void pointCard(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) {
|
||||||
int cardIndex = content.asObject().get("cardIndex").intValue();
|
int cardIndex = content.asObject().get("cardIndex").intValue();
|
||||||
|
|
||||||
Grid g = gm.findGrid(roomId);
|
Optional<Grid> g = gRepository.findById(UUID.fromString(roomId));
|
||||||
|
if(!g.isPresent())
|
||||||
|
// 404
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
gm.pointCard(g, player.charAt(0), cardIndex);
|
gm.pointCard(g.get(), player.charAt(0), cardIndex);
|
||||||
publishCardUpdate(g, cardIndex);
|
|
||||||
} catch (IllegalGameActionException e) {
|
} catch (IllegalGameActionException e) {
|
||||||
publishError(player.charAt(0), e);
|
publishError(player.charAt(0), e);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -117,9 +110,12 @@ public class WebSocketController {
|
|||||||
|
|
||||||
@MessageMapping("end-guessing")
|
@MessageMapping("end-guessing")
|
||||||
public void endGuessing(@Header("room") String roomId, @Header("player") String player) {
|
public void endGuessing(@Header("room") String roomId, @Header("player") String player) {
|
||||||
Grid g = gm.findGrid(roomId);
|
Optional<Grid> g = gRepository.findById(UUID.fromString(roomId));
|
||||||
|
if(!g.isPresent())
|
||||||
|
// 404
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
gm.endGuessing(g, player.charAt(0));
|
gm.endGuessing(g.get(), player.charAt(0));
|
||||||
} catch (IllegalGameActionException e) {
|
} catch (IllegalGameActionException e) {
|
||||||
publishError(player.charAt(0), e);
|
publishError(player.charAt(0), e);
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
@@ -128,12 +124,67 @@ public class WebSocketController {
|
|||||||
|
|
||||||
@MessageMapping("enter-sudden-death")
|
@MessageMapping("enter-sudden-death")
|
||||||
public void enterSuddenDeath(@Header("room") String roomId, @Header("player") String player) {
|
public void enterSuddenDeath(@Header("room") String roomId, @Header("player") String player) {
|
||||||
Grid g = gm.findGrid(roomId);
|
Optional<Grid> g = gRepository.findById(UUID.fromString(roomId));
|
||||||
|
if(!g.isPresent())
|
||||||
|
// 404
|
||||||
|
return;
|
||||||
try {
|
try {
|
||||||
gm.enterSuddenDeath(g);
|
gm.enterSuddenDeath(g.get());
|
||||||
} catch (IllegalGameActionException e) {
|
} catch (IllegalGameActionException e) {
|
||||||
publishError(player.charAt(0), e);
|
publishError(player.charAt(0), e);
|
||||||
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) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,420 +1,164 @@
|
|||||||
package com.bernard.nodecames.game;
|
package com.bernard.nodecames.game;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.Collection;
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Optional;
|
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
import java.util.UUID;
|
import java.util.Map.Entry;
|
||||||
import java.util.stream.Collectors;
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
import java.util.stream.IntStream;
|
import java.util.function.Consumer;
|
||||||
|
|
||||||
import org.slf4j.Logger;
|
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.Card.Color;
|
import com.bernard.nodecames.model.Card.Color;
|
||||||
|
import com.bernard.nodecames.model.Game;
|
||||||
|
import com.bernard.nodecames.model.GameConfig;
|
||||||
import com.bernard.nodecames.model.GameEvent;
|
import com.bernard.nodecames.model.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.Inventory;
|
||||||
|
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.WorldRepository;
|
||||||
|
|
||||||
import lombok.RequiredArgsConstructor;
|
import jakarta.annotation.PostConstruct;
|
||||||
import lombok.Setter;
|
import jakarta.validation.constraints.NotNull;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@AllArgsConstructor
|
||||||
public class GameManager {
|
public class GameManager {
|
||||||
|
|
||||||
//TODO Remove static, make something more Spring Boot-y
|
private final GridManager gm;
|
||||||
private static Map<UUID, Grid> games = new HashMap<>();
|
private final GameRepository gRepository;
|
||||||
private static Map<UUID, Random> randoms = new HashMap<>();
|
private final WorldRepository wRepository;
|
||||||
|
|
||||||
@Setter
|
|
||||||
private WebSocketController wsc = null;
|
|
||||||
|
|
||||||
private final DictionnariesService dictionnaries;
|
|
||||||
|
|
||||||
private static final Logger log = LoggerFactory.getLogger(GameManager.class);
|
private static final Logger log = LoggerFactory.getLogger(GameManager.class);
|
||||||
|
|
||||||
private void newGameEvent(Grid g, GameEvent ge) {
|
//TODO try putting "World" here instead of WorldId
|
||||||
g.newGameEvent(ge);
|
private Map<WorldId, Set<WorldObserver>> observers = new HashMap<>();
|
||||||
log.debug("New game event : {}", ge);
|
|
||||||
wsc.onNewEvent(g, ge);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setPhase(Grid g, Phase phase) {
|
private void forEachObserver(World w, Consumer<WorldObserver> action) {
|
||||||
g.setPhase(phase);
|
observers.getOrDefault(w.getId(), Set.of()).forEach(action);
|
||||||
log.debug("New phase : {}", phase.name());
|
|
||||||
if(!phase.isAnyoneGuessing()) {
|
|
||||||
g.setCurrentHint(null);
|
|
||||||
}
|
|
||||||
wsc.onNewPhase(g);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void changeConfig(Grid g, MutableOption cc, Object data) {
|
/*
|
||||||
g.getConfig().set(cc, data);
|
* In case of server restart, registers to GridManager observers
|
||||||
log.debug("Changed config option: {} -> new value: {}", cc.name(), data);
|
|
||||||
wsc.onConfigChange(g, cc, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private char other(char player) {
|
|
||||||
return (player=='A')?'B':'A';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
@returns true iff all green cards of this player have been revealed either side
|
|
||||||
if onlyoneGreenGreen is false, checks only the side of this player
|
|
||||||
*/
|
*/
|
||||||
private boolean allGreenRevealed(Grid g, char player, boolean onlyOneGreenGreen) {
|
@PostConstruct
|
||||||
for(Card c : g.getCards()) {
|
private void registerRunningObservers() {
|
||||||
if ((c.getColor(player) == Color.GREEN && !c.isColorPublic(player)) &&
|
for(World w : wRepository.findAll()) {
|
||||||
(!onlyOneGreenGreen || (c.getColor(player) == Color.GREEN && !c.isColorPublic(player))))
|
if(w.getCurrentGrid() != null && w.getCurrentGrid().getWon() != null) {
|
||||||
return false;
|
registerObserver(w, w.getCurrentGrid());
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns true iff the game would have been won if the rule onlyOneGreenGreen was true
|
|
||||||
*/
|
|
||||||
private boolean onlyOneGreenGreenWouldWin(Grid g) {
|
|
||||||
return !g.getConfig().isOnlyOneGreenGreen() && allGreenRevealed(g, 'A', true) && allGreenRevealed(g, 'B', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Grid findGrid(String gridId) {
|
|
||||||
return games.get(UUID.fromString(gridId));
|
|
||||||
}
|
|
||||||
|
|
||||||
public UUID newGrid(GridConfig gc, Random r) {
|
|
||||||
UUID uuid = UUID.randomUUID();
|
|
||||||
randoms.put(uuid, r);
|
|
||||||
String[] words = dictionnaries.randomWords(gc.getWordCount(), r);
|
|
||||||
List<Card> cards = new ArrayList<>(gc.getWordCount());
|
|
||||||
List<CardColors> origDistrib = gc.getWordDistribution();
|
|
||||||
List<CardColors> distrib;
|
|
||||||
if(!gc.isStructuredGrid()) {
|
|
||||||
List<Color> bColors = origDistrib.stream().map(cc -> cc.getFaceB()).collect(Collectors.toList());
|
|
||||||
Collections.shuffle(bColors, r);
|
|
||||||
distrib = IntStream.range(0, gc.getWordCount())
|
|
||||||
.mapToObj(i -> CardColors.of(origDistrib.get(i).getFaceA(), bColors.get(i)))
|
|
||||||
.toList();
|
|
||||||
} else {
|
|
||||||
distrib = origDistrib;
|
|
||||||
}
|
|
||||||
for (int j = 0; j < distrib.size(); j++) {
|
|
||||||
cards.add(new Card(words[j], distrib.get(j).getFaceA(), distrib.get(j).getFaceB()));
|
|
||||||
}
|
|
||||||
Collections.shuffle(cards, r);
|
|
||||||
|
|
||||||
Phase startPhase;
|
|
||||||
if(gc.isAnyoneStarts()) {
|
|
||||||
startPhase = Phase.HINTING_BOTH;
|
|
||||||
} else {
|
|
||||||
if(r.nextBoolean())
|
|
||||||
startPhase = Phase.hinting('A');
|
|
||||||
else
|
|
||||||
startPhase = Phase.hinting('B');
|
|
||||||
}
|
|
||||||
|
|
||||||
Grid g = new Grid(uuid.toString(),
|
|
||||||
(Card[])cards.toArray(new Card[cards.size()]),
|
|
||||||
new ArrayList<>(),
|
|
||||||
null,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
0,
|
|
||||||
startPhase,
|
|
||||||
Optional.empty(),
|
|
||||||
gc);
|
|
||||||
|
|
||||||
games.put(uuid, g);
|
|
||||||
|
|
||||||
return uuid;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**********************************
|
|
||||||
* GAME ACTIONS *
|
|
||||||
**********************************/
|
|
||||||
private void endGame(Grid g, boolean win) {
|
|
||||||
g.setWon(Optional.of(win));
|
|
||||||
setPhase(g, Phase.GAME_ENDED);
|
|
||||||
newGameEvent(g, GameEvent.newGameEndEvent(win));
|
|
||||||
}
|
|
||||||
|
|
||||||
public void proposeHint(Grid g, char player, String hint, int wordCount) throws IllegalGameActionException {
|
|
||||||
if (!g.getPhase().canPlay() && !g.getPhase().isHinting(player)) {
|
|
||||||
throw new IllegalGameActionException(g, "This action is not allowed now");
|
|
||||||
}
|
|
||||||
if (!g.getConfig().getAvailableHintWordCount().contains(wordCount)) {
|
|
||||||
throw new IllegalGameActionException(g, "Unavailable word count");
|
|
||||||
}
|
|
||||||
if (hint.length() > g.getConfig().getHintMaxLength()) {
|
|
||||||
throw new IllegalGameActionException(g, "Hint is too long");
|
|
||||||
}
|
|
||||||
|
|
||||||
Hint h = new Hint(hint, wordCount);
|
|
||||||
g.incrementHintCount();
|
|
||||||
g.setCurrentHint(h);
|
|
||||||
g.setCurrentGuessCount(0);
|
|
||||||
newGameEvent(g, GameEvent.newHintEvent(h, player));
|
|
||||||
setPhase(g, Phase.guessing(other(player)));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns 'A', 'B', '_' depending on who should be the next ones to hint if we enter a hinting phase
|
|
||||||
*/
|
|
||||||
private char whoWouldHintNext(Grid g, char player) {
|
|
||||||
if(allGreenRevealed(g, player, g.getConfig().isOnlyOneGreenGreen())) {
|
|
||||||
// I can't hint anymore
|
|
||||||
return other(player);
|
|
||||||
} else {
|
|
||||||
if(g.getConfig().isHintsInARow() && !allGreenRevealed(g, other(player), g.getConfig().isOnlyOneGreenGreen())) {
|
|
||||||
return '_';
|
|
||||||
} else {
|
|
||||||
return player;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
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) {
|
||||||
* Enters the sudden death phase
|
Game g = cfg.newGame(gRepository, wRepository, name);
|
||||||
*/
|
|
||||||
private void enterSuddenDeathInternal(Grid g) {
|
return g;
|
||||||
newGameEvent(g, GameEvent.newSuddenDeath());
|
}
|
||||||
if(allGreenRevealed(g, 'A', g.getConfig().isOnlyOneGreenGreen())) {
|
|
||||||
// If all 'A''s greens are revealed, A is the only one to guess
|
public Collection<Game> allGames() {
|
||||||
setPhase(g, Phase.suddenDeathOnly('A'));
|
return gRepository.findAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public Set<World> allWorlds(@NotNull Game g) {
|
||||||
|
return wRepository.findByGame(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Grid newGrid(Game g, World w) throws IllegalGameActionException {
|
||||||
|
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());
|
||||||
|
|
||||||
|
w.setCurrentGrid(grid);
|
||||||
|
wRepository.save(w);
|
||||||
|
registerObserver(w, grid);
|
||||||
|
|
||||||
|
return grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Grid resetGrid(Game g, World world) throws IllegalGameActionException{
|
||||||
|
return newGrid(g, world);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Grid getGrid(Game g, World w) throws IllegalGameActionException{
|
||||||
|
if(w.getCurrentGrid() != null) {
|
||||||
|
return w.getCurrentGrid();
|
||||||
} else {
|
} else {
|
||||||
// Else, we are both guessing
|
return newGrid(g, w);
|
||||||
setPhase(g, Phase.SUDDEN_DEATH_BOTH);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
public void giveItem(Game g, String item) {
|
||||||
Sets the phase of the grid when player ends its guessing round.
|
g.getInventory().receiveItem(item);
|
||||||
@param g Must neither be ended neither be in a sudden death
|
Inventory inv = g.getInventory();
|
||||||
*/
|
for(World w : allWorlds(g)) {
|
||||||
private void endGuessingRound(Grid g, char player) {
|
if(w.getCurrentGrid() == null) continue;
|
||||||
// We're done with this round of guesses
|
Grid grid = w.getCurrentGrid();
|
||||||
if(g.getUsedHints() >= g.getConfig().getMaxHint()) {
|
Map<MutableOption, Object> updates = w.getConfig().updateConfig(grid.getConfig(), inv);
|
||||||
if(g.getUsedHints() >= g.getConfig().getMaxMaxHint()) {
|
log.debug("Updating option for world {} : {}", w.getName(), updates);
|
||||||
if(!g.getConfig().isSuddenDeath()) {
|
for(Entry<MutableOption, Object> e : updates.entrySet()) {
|
||||||
// We're entering sudden death
|
try {
|
||||||
enterSuddenDeathInternal(g);
|
gm.unlock(grid, e.getKey(), e.getValue());
|
||||||
} else {
|
} catch (IllegalGameActionException ex) {
|
||||||
// Only sudden death can get us out
|
throw new IllegalStateException("World Config sent a set of changes that got us out of game logic", ex);
|
||||||
setPhase(g, Phase.NEED_SUDDEN_DEATH);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// We are waiting for more hints
|
|
||||||
if(g.getConfig().isSuddenDeath()) {
|
|
||||||
setPhase(g, Phase.needMoreHints(whoWouldHintNext(g, player)));
|
|
||||||
} else {
|
|
||||||
setPhase(g, Phase.needMoreHintsOrSuddenDeath(whoWouldHintNext(g, player)));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
// Next regular hinting round
|
|
||||||
setPhase(g, Phase.hinting(whoWouldHintNext(g, player)));
|
|
||||||
}
|
}
|
||||||
|
gRepository.save(g);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Card.Color pointCard(Grid g, char player, int cardIndex) throws IllegalGameActionException {
|
@AllArgsConstructor
|
||||||
if (!g.getPhase().canPlay() || !g.getPhase().isGuessing(player)) {
|
private class GObserver implements GridObserver {
|
||||||
throw new IllegalGameActionException(g, "This action is not allowed now");
|
private Grid grid;
|
||||||
}
|
private World world;
|
||||||
if (cardIndex<0 || cardIndex>g.getCardCount()) {
|
|
||||||
throw new IllegalArgumentException("The given card index is invalid");
|
|
||||||
}
|
|
||||||
Card c = g.getCards()[cardIndex];
|
|
||||||
|
|
||||||
if(c.isColorPublic(other(player)) || (c.getColor(player) == Card.Color.GREEN && c.isColorPublic(player)))
|
@Override
|
||||||
throw new IllegalGameActionException(g, "Card has already been revealed");
|
public void onGridEnd(boolean win) {
|
||||||
c.setColorPublic(other(player), true);
|
if(win) {
|
||||||
Card.Color guessedColor = c.getColor(other(player));
|
world.setWon(true);
|
||||||
|
wRepository.save(world);
|
||||||
newGameEvent(g, GameEvent.newGuessEvent(c, player));
|
|
||||||
g.incrementCurrentGuessCount();
|
|
||||||
if(guessedColor == Color.BLACK){
|
|
||||||
if(g.getConfig().isOnlyOneGreenGreenPreventDeath() && onlyOneGreenGreenWouldWin(g)) {
|
|
||||||
setPhase(g, Phase.WIN_IF_GREEN_GREEN);
|
|
||||||
} else {
|
|
||||||
endGame(g, false);
|
|
||||||
}
|
}
|
||||||
} else if (g.getPhase().isSuddenDeath() && guessedColor == Color.WHITE) {
|
|
||||||
if(g.getConfig().isOnlyOneGreenGreenPreventDeath() && onlyOneGreenGreenWouldWin(g)) {
|
|
||||||
setPhase(g, Phase.WIN_IF_GREEN_GREEN);
|
|
||||||
} else {
|
|
||||||
endGame(g, false);
|
|
||||||
}
|
|
||||||
} else if (allGreenRevealed(g, other(player), g.getConfig().isOnlyOneGreenGreen()) && guessedColor == Color.GREEN) {
|
|
||||||
// i.e. i made my last guess
|
|
||||||
if (allGreenRevealed(g, player, g.getConfig().isOnlyOneGreenGreen())) {
|
|
||||||
endGame(g, true);
|
|
||||||
} else if (g.getPhase().isSuddenDeath()) {
|
|
||||||
// Sudden death with only the other
|
|
||||||
setPhase(g, Phase.suddenDeathOnly(other(player)));
|
|
||||||
} else {
|
|
||||||
endGuessingRound(g, player);
|
|
||||||
}
|
|
||||||
} else if (!allGreenRevealed(g, other(player), g.getConfig().isOnlyOneGreenGreen()) && guessedColor == Color.GREEN) {
|
|
||||||
// There is still cards i should find
|
|
||||||
if (!g.getPhase().isSuddenDeath() && !g.getCurrentHint().canStillGuess(g.getCurrentGuessCount(), g.getConfig().isOneMoreGuess())) {
|
|
||||||
if(!g.getConfig().isOneMoreGuess() && g.getCurrentHint().canStillGuess(g.getCurrentGuessCount(), true)) {
|
|
||||||
// Having one more guess could make us continue
|
|
||||||
setPhase(g, Phase.waitingForOneMore(player));
|
|
||||||
} else {
|
|
||||||
// We're done with this round of guesses anyway
|
|
||||||
endGuessingRound(g, player);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// I continue guessing cards in this round
|
|
||||||
}
|
|
||||||
} else if (guessedColor == Color.WHITE && !g.getPhase().isSuddenDeath()){
|
|
||||||
endGuessingRound(g, player);
|
|
||||||
} else {
|
|
||||||
throw new IllegalStateException("This should logically never happen");
|
|
||||||
}
|
}
|
||||||
if(!g.getPhase().isAnyoneGuessing() && !g.getPhase().isWaitingForOneMore())
|
|
||||||
// It means this pick ended the guessing turn
|
|
||||||
newGameEvent(g, GameEvent.newEndGuessingEvent(false, player));
|
|
||||||
return guessedColor;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void endGuessing(Grid g, char player) throws IllegalGameActionException {
|
@Override
|
||||||
if (!g.getPhase().canPlay() || (!g.getPhase().isGuessing(player) && !g.getPhase().isWaitingForOneMore())) {
|
public void onPointCard(char player, Card card, Color color) {}
|
||||||
throw new IllegalGameActionException(g, "This action is not allowed now");
|
@Override
|
||||||
}
|
public void onSubmitHint(char player, Hint hint) {}
|
||||||
if (!g.getConfig().isEndGuessing() && !g.getPhase().isWaitingForOneMore()) {
|
@Override
|
||||||
throw new IllegalGameActionException(g, "Cannot end guessing in this game");
|
public void onGameEvent(GameEvent ge) {}
|
||||||
}
|
@Override
|
||||||
endGuessingRound(g, player);
|
public void onNewPhase(Phase p) {}
|
||||||
newGameEvent(g, GameEvent.newEndGuessingEvent(true, player));
|
@Override
|
||||||
}
|
public void onConfigChange(MutableOption cc, Object data) {}
|
||||||
|
|
||||||
public void enterSuddenDeath(Grid g) throws IllegalGameActionException {
|
|
||||||
if(!g.getPhase().isNeedMoreHints()) {
|
|
||||||
throw new IllegalGameActionException(g, "You can only enter sudden death when being out of hints");
|
|
||||||
}
|
|
||||||
enterSuddenDeathInternal(g);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UPDATE GAME CONFIG
|
|
||||||
*/
|
|
||||||
public void unlock(Grid g, MutableOption opt, Object o) throws IllegalGameActionException {
|
|
||||||
switch(opt) {
|
|
||||||
case MutableOption.MAX_HINT:
|
|
||||||
unlockHintCount(g, (Integer)o);
|
|
||||||
break;
|
|
||||||
case MutableOption.AVAILABLE_HINT_WORD_COUNT:
|
|
||||||
unlockNewWordCounts(g, (Set<Integer>)o);
|
|
||||||
break;
|
|
||||||
case MutableOption.ONE_MORE_GUESS:
|
|
||||||
unlockOneMoreGuess(g);
|
|
||||||
break;
|
|
||||||
case MutableOption.HINT_MAX_LENGTH:
|
|
||||||
unlockHintMaxLength(g, (Integer)o);
|
|
||||||
break;
|
|
||||||
case MutableOption.END_GUESSING:
|
|
||||||
unlockEndGuessing(g);
|
|
||||||
break;
|
|
||||||
case MutableOption.ONLY_ONE_GREEN_GREEN:
|
|
||||||
unlockOnlyOneGreenGreen(g);
|
|
||||||
break;
|
|
||||||
case MutableOption.SUDDEN_DEATH:
|
|
||||||
unlockSuddenDeath(g);
|
|
||||||
break;
|
|
||||||
case MutableOption.HINTS_IN_A_ROW:
|
|
||||||
unlockHintsInARow(g);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new IllegalStateException("Unknown MutableOption to edit");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public void unlockHintCount(Grid g, int newHintCount) throws IllegalGameActionException {
|
|
||||||
if(newHintCount > g.getConfig().getMaxMaxHint()) {
|
|
||||||
throw new IllegalGameActionException(g, "Illegal hint count");
|
|
||||||
}
|
|
||||||
if(newHintCount <= g.getConfig().getMaxHint()) {
|
|
||||||
throw new IllegalGameActionException(g, "Cannot lower hint count");
|
|
||||||
}
|
|
||||||
changeConfig(g, MutableOption.MAX_HINT, newHintCount);
|
|
||||||
if(g.getPhase().isNeedMoreHints() || g.getPhase().isNeedMoreHintsOrSuddenDeath()) {
|
|
||||||
setPhase(g, Phase.hinting(g.getPhase().getHinting()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockNewWordCounts(Grid g, Set<Integer> newAvailableHintWordCounts) throws IllegalGameActionException {
|
|
||||||
if (!newAvailableHintWordCounts.containsAll(g.getConfig().getAvailableHintWordCount())
|
|
||||||
|| g.getConfig().getAvailableHintWordCount().containsAll(newAvailableHintWordCounts)) {
|
|
||||||
// i.e. If given data is not a strict superset
|
|
||||||
throw new IllegalGameActionException(g, "Can only add hint word options");
|
|
||||||
}
|
|
||||||
changeConfig(g, MutableOption.AVAILABLE_HINT_WORD_COUNT, newAvailableHintWordCounts);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockOneMoreGuess(Grid g) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().isOneMoreGuess())
|
|
||||||
throw new IllegalGameActionException(g, "Cannot unlock oneMoreGuess as it is already unlocked");
|
|
||||||
changeConfig(g, MutableOption.ONE_MORE_GUESS, true);
|
|
||||||
if(g.getPhase().isWaitingForOneMore()) {
|
|
||||||
setPhase(g, Phase.guessing(g.getPhase().getGuessing()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockHintMaxLength(Grid g, int newHintMaxLength) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().getHintMaxLength() >= newHintMaxLength) {
|
|
||||||
throw new IllegalGameActionException(g, "Can only raise max hint length");
|
|
||||||
}
|
|
||||||
changeConfig(g, MutableOption.HINT_MAX_LENGTH, newHintMaxLength);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockEndGuessing(Grid g) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().isEndGuessing())
|
|
||||||
throw new IllegalGameActionException(g, "Cannot unlock endGuessing as it is already unlocked");
|
|
||||||
changeConfig(g, MutableOption.END_GUESSING, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockOnlyOneGreenGreen(Grid g) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().isOnlyOneGreenGreen())
|
|
||||||
throw new IllegalGameActionException(g, "Cannot unlock onlyOneGreenGreen as it is already unlocked");
|
|
||||||
changeConfig(g, MutableOption.ONLY_ONE_GREEN_GREEN, true);
|
|
||||||
if(g.getPhase() == Phase.WIN_IF_GREEN_GREEN) {
|
|
||||||
endGame(g, true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockSuddenDeath(Grid g) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().isSuddenDeath())
|
|
||||||
throw new IllegalGameActionException(g, "Cannot unlock suddenDeath as it is already unlocked");
|
|
||||||
changeConfig(g, MutableOption.SUDDEN_DEATH,true);
|
|
||||||
if (g.getPhase().isNeedMoreHintsOrSuddenDeath()) {
|
|
||||||
setPhase(g, Phase.needMoreHints(g.getPhase().getHinting()));
|
|
||||||
} else if (g.getPhase() == Phase.NEED_SUDDEN_DEATH) {
|
|
||||||
// We're entering sudden death
|
|
||||||
enterSuddenDeathInternal(g);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void unlockHintsInARow(Grid g) throws IllegalGameActionException {
|
|
||||||
if(g.getConfig().isHintsInARow())
|
|
||||||
throw new IllegalGameActionException(g, "Cannot unlock hintsInARow as it is already unlocked");
|
|
||||||
changeConfig(g, MutableOption.HINTS_IN_A_ROW,true);
|
|
||||||
if ((g.getPhase() == Phase.HINTING_A || g.getPhase() == Phase.HINTING_B)
|
|
||||||
&& !allGreenRevealed(g, 'A', g.getConfig().isOnlyOneGreenGreen())
|
|
||||||
&& !allGreenRevealed(g, 'B', g.getConfig().isOnlyOneGreenGreen())) {
|
|
||||||
setPhase(g, Phase.HINTING_BOTH);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,464 @@
|
|||||||
|
package com.bernard.nodecames.game;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Random;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.CopyOnWriteArraySet;
|
||||||
|
import java.util.function.Consumer;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
import java.util.stream.IntStream;
|
||||||
|
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import com.bernard.nodecames.model.Card;
|
||||||
|
import com.bernard.nodecames.model.Card.Color;
|
||||||
|
import com.bernard.nodecames.model.ColorDistribution.CardColors;
|
||||||
|
import com.bernard.nodecames.model.GameEvent;
|
||||||
|
import com.bernard.nodecames.model.Grid;
|
||||||
|
import com.bernard.nodecames.model.Grid.Phase;
|
||||||
|
import com.bernard.nodecames.model.GridConfig;
|
||||||
|
import com.bernard.nodecames.model.GridConfig.MutableOption;
|
||||||
|
import com.bernard.nodecames.model.Hint;
|
||||||
|
import com.bernard.nodecames.model.World;
|
||||||
|
import com.bernard.nodecames.observer.GridObserver;
|
||||||
|
import com.bernard.nodecames.repository.CardRepository;
|
||||||
|
import com.bernard.nodecames.repository.GridRepository;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class GridManager {
|
||||||
|
|
||||||
|
private Map<UUID, Set<GridObserver>> observers = new HashMap<>();
|
||||||
|
|
||||||
|
private final GridRepository gRepository;
|
||||||
|
private final CardRepository cRepository;
|
||||||
|
|
||||||
|
private final DictionnariesService dictionnaries;
|
||||||
|
|
||||||
|
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) {
|
||||||
|
log.debug("New game event : {}", ge);
|
||||||
|
g.newGameEvent(ge);
|
||||||
|
gRepository.save(g);
|
||||||
|
forEachObserver(g, o -> o.onGameEvent(ge));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setPhase(Grid g, Phase phase) {
|
||||||
|
log.debug("New phase : {}", phase.name());
|
||||||
|
g.setPhase(phase);
|
||||||
|
if(!phase.isAnyoneGuessing()) {
|
||||||
|
g.setCurrentHint(null);
|
||||||
|
}
|
||||||
|
gRepository.save(g);
|
||||||
|
forEachObserver(g, o -> o.onNewPhase(phase));
|
||||||
|
}
|
||||||
|
private void setWon(Grid g, boolean won) {
|
||||||
|
log.debug("Game won: {}", won);
|
||||||
|
g.setWonOpt(Optional.of(won));
|
||||||
|
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 setCardPointed(Grid g, Card c, char player, Color col) {
|
||||||
|
log.debug("Card set public, index {}", c.getIndex());
|
||||||
|
c.setColorPublic(player, true);
|
||||||
|
cRepository.save(c);
|
||||||
|
forEachObserver(c.getGrid(), o -> o.onPointCard(player, c, col));
|
||||||
|
g.incrementCurrentGuessCount();
|
||||||
|
gRepository.save(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void newHint(Grid g, char player, Hint h) {
|
||||||
|
log.debug("New hint: {}", h);
|
||||||
|
g.incrementHintCount();
|
||||||
|
g.setCurrentHint(h);
|
||||||
|
g.setCurrentGuessCount(0);
|
||||||
|
gRepository.save(g);
|
||||||
|
forEachObserver(g, o -> o.onSubmitHint(player, h));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void changeConfig(Grid g, MutableOption cc, Object data) {
|
||||||
|
log.debug("Changed config option: {} -> new value: {}", cc.name(), data);
|
||||||
|
g.getConfig().set(cc, data);
|
||||||
|
gRepository.save(g);
|
||||||
|
forEachObserver(g, o -> o.onConfigChange(cc, data));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public int getCardCount(Grid g) {
|
||||||
|
return cRepository.countByGrid(g);
|
||||||
|
}
|
||||||
|
public Card[] getCards(Grid g) {
|
||||||
|
return cRepository.findByGridOrderById_Index(g);
|
||||||
|
}
|
||||||
|
public Card getCard(Grid g, int index) {
|
||||||
|
return cRepository.findByGridAndId_Index(g, index);
|
||||||
|
}
|
||||||
|
private char other(char player) {
|
||||||
|
return (player=='A')?'B':'A';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
@returns true iff all green cards of this player have been revealed either side
|
||||||
|
if onlyoneGreenGreen is false, checks only the side of this player
|
||||||
|
*/
|
||||||
|
private boolean allGreenRevealed(Grid g, char player, boolean onlyOneGreenGreen) {
|
||||||
|
for(Card c : getCards(g)) {
|
||||||
|
if ((c.getColor(player) == Color.GREEN && !c.isColorPublic(player)) &&
|
||||||
|
(!onlyOneGreenGreen || (c.getColor(player) == Color.GREEN && !c.isColorPublic(player))))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true iff the game would have been won if the rule onlyOneGreenGreen was true
|
||||||
|
*/
|
||||||
|
private boolean onlyOneGreenGreenWouldWin(Grid g) {
|
||||||
|
return !g.getConfig().isOnlyOneGreenGreen() && allGreenRevealed(g, 'A', true) && allGreenRevealed(g, 'B', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Grid newGrid(@Nullable World world, GridConfig gc, Random r) {
|
||||||
|
UUID uuid = UUID.randomUUID();
|
||||||
|
String[] words = dictionnaries.randomWords(gc.getColorDistribution().wordCount(), r);
|
||||||
|
List<CardColors> origDistrib = gc.getColorDistribution().asList();
|
||||||
|
List<CardColors> distrib;
|
||||||
|
if(!gc.isStructuredGrid()) {
|
||||||
|
List<Color> bColors = origDistrib.stream().map(cc -> cc.getFaceB()).collect(Collectors.toList());
|
||||||
|
Collections.shuffle(bColors, r);
|
||||||
|
distrib = IntStream.range(0, gc.getColorDistribution().wordCount())
|
||||||
|
.mapToObj(i -> CardColors.of(origDistrib.get(i).getFaceA(), bColors.get(i)))
|
||||||
|
.toList();
|
||||||
|
} else {
|
||||||
|
distrib = origDistrib;
|
||||||
|
}
|
||||||
|
distrib = new ArrayList<>(distrib);
|
||||||
|
Collections.shuffle(distrib, r);
|
||||||
|
|
||||||
|
Phase startPhase;
|
||||||
|
if(gc.isAnyoneStarts()) {
|
||||||
|
startPhase = Phase.HINTING_BOTH;
|
||||||
|
} else {
|
||||||
|
if(r.nextBoolean())
|
||||||
|
startPhase = Phase.hinting('A');
|
||||||
|
else
|
||||||
|
startPhase = Phase.hinting('B');
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid g = new Grid(uuid, world, startPhase, gc);
|
||||||
|
gRepository.save(g);
|
||||||
|
|
||||||
|
for (int j = 0; j < distrib.size(); j++) {
|
||||||
|
Card card = new Card(g, j, words[j], distrib.get(j).getFaceA(), distrib.get(j).getFaceB());
|
||||||
|
cRepository.save(card);
|
||||||
|
}
|
||||||
|
|
||||||
|
return g;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**********************************
|
||||||
|
* GAME ACTIONS *
|
||||||
|
**********************************/
|
||||||
|
private void endGame(Grid g, boolean win) {
|
||||||
|
setPhase(g, Phase.GAME_ENDED);
|
||||||
|
newGameEvent(g, GameEvent.newGameEndEvent(win));
|
||||||
|
// This must be the last step as it removes observers
|
||||||
|
setWon(g, win);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void proposeHint(Grid g, char player, String hint, int wordCount) throws IllegalGameActionException {
|
||||||
|
if (!g.getPhase().canPlay() && !g.getPhase().isHinting(player)) {
|
||||||
|
throw new IllegalGameActionException(g, "This action is not allowed now");
|
||||||
|
}
|
||||||
|
if (!g.getConfig().getAvailableHintWordCount().contains(wordCount)) {
|
||||||
|
throw new IllegalGameActionException(g, "Unavailable word count");
|
||||||
|
}
|
||||||
|
if (hint.length() > g.getConfig().getHintMaxLength()) {
|
||||||
|
throw new IllegalGameActionException(g, "Hint is too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
Hint h = new Hint(hint, wordCount);
|
||||||
|
newHint(g, player, h);
|
||||||
|
newGameEvent(g, GameEvent.newHintEvent(h, player));
|
||||||
|
setPhase(g, Phase.guessing(other(player)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns 'A', 'B', '_' depending on who should be the next ones to hint if we enter a hinting phase
|
||||||
|
*/
|
||||||
|
private char whoWouldHintNext(Grid g, char player) {
|
||||||
|
if(allGreenRevealed(g, player, g.getConfig().isOnlyOneGreenGreen())) {
|
||||||
|
// I can't hint anymore
|
||||||
|
return other(player);
|
||||||
|
} else {
|
||||||
|
if(g.getConfig().isHintsInARow() && !allGreenRevealed(g, other(player), g.getConfig().isOnlyOneGreenGreen())) {
|
||||||
|
return '_';
|
||||||
|
} else {
|
||||||
|
return player;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enters the sudden death phase
|
||||||
|
*/
|
||||||
|
private void enterSuddenDeathInternal(Grid g) {
|
||||||
|
newGameEvent(g, GameEvent.newSuddenDeath());
|
||||||
|
if(allGreenRevealed(g, 'A', g.getConfig().isOnlyOneGreenGreen())) {
|
||||||
|
// If all 'A''s greens are revealed, A is the only one to guess
|
||||||
|
setPhase(g, Phase.suddenDeathOnly('A'));
|
||||||
|
} else {
|
||||||
|
// Else, we are both guessing
|
||||||
|
setPhase(g, Phase.SUDDEN_DEATH_BOTH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
Sets the phase of the grid when player ends its guessing round.
|
||||||
|
@param g Must neither be ended neither be in a sudden death
|
||||||
|
*/
|
||||||
|
private void endGuessingRound(Grid g, char player) {
|
||||||
|
// We're done with this round of guesses
|
||||||
|
if(g.getUsedHints() >= g.getConfig().getMaxHint()) {
|
||||||
|
if(g.getUsedHints() >= g.getConfig().getMaxMaxHint()) {
|
||||||
|
if(!g.getConfig().isSuddenDeath()) {
|
||||||
|
// We're entering sudden death
|
||||||
|
enterSuddenDeathInternal(g);
|
||||||
|
} else {
|
||||||
|
// Only sudden death can get us out
|
||||||
|
setPhase(g, Phase.NEED_SUDDEN_DEATH);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// We are waiting for more hints
|
||||||
|
if(g.getConfig().isSuddenDeath()) {
|
||||||
|
setPhase(g, Phase.needMoreHints(whoWouldHintNext(g, player)));
|
||||||
|
} else {
|
||||||
|
setPhase(g, Phase.needMoreHintsOrSuddenDeath(whoWouldHintNext(g, player)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Next regular hinting round
|
||||||
|
setPhase(g, Phase.hinting(whoWouldHintNext(g, player)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Card.Color pointCard(Grid g, char player, int cardIndex) throws IllegalGameActionException {
|
||||||
|
if (!g.getPhase().canPlay() || !g.getPhase().isGuessing(player)) {
|
||||||
|
throw new IllegalGameActionException(g, "This action is not allowed now");
|
||||||
|
}
|
||||||
|
if (cardIndex<0 || cardIndex>getCardCount(g)) {
|
||||||
|
throw new IllegalArgumentException("The given card index is invalid");
|
||||||
|
}
|
||||||
|
Card c = getCard(g, cardIndex);
|
||||||
|
|
||||||
|
if(c.isColorPublic(other(player)) || (g.getConfig().isOnlyOneGreenGreen() && c.getColor(player) == Card.Color.GREEN && c.isColorPublic(player)))
|
||||||
|
throw new IllegalGameActionException(g, "Card has already been revealed");
|
||||||
|
Card.Color guessedColor = c.getColor(other(player));
|
||||||
|
setCardPointed(g, c, other(player), guessedColor);
|
||||||
|
|
||||||
|
newGameEvent(g, GameEvent.newGuessEvent(c.getIndex(), player, c.getColor(other(player))));
|
||||||
|
g.incrementCurrentGuessCount();
|
||||||
|
if(guessedColor == Color.BLACK){
|
||||||
|
if(g.getConfig().isOnlyOneGreenGreenPreventDeath() && onlyOneGreenGreenWouldWin(g)) {
|
||||||
|
setPhase(g, Phase.WIN_IF_GREEN_GREEN);
|
||||||
|
} else {
|
||||||
|
endGame(g, false);
|
||||||
|
}
|
||||||
|
} else if (g.getPhase().isSuddenDeath() && guessedColor == Color.WHITE) {
|
||||||
|
if(g.getConfig().isOnlyOneGreenGreenPreventDeath() && onlyOneGreenGreenWouldWin(g)) {
|
||||||
|
setPhase(g, Phase.WIN_IF_GREEN_GREEN);
|
||||||
|
} else {
|
||||||
|
endGame(g, false);
|
||||||
|
}
|
||||||
|
} else if (allGreenRevealed(g, other(player), g.getConfig().isOnlyOneGreenGreen()) && guessedColor == Color.GREEN) {
|
||||||
|
// i.e. i made my last guess
|
||||||
|
if (allGreenRevealed(g, player, g.getConfig().isOnlyOneGreenGreen())) {
|
||||||
|
endGame(g, true);
|
||||||
|
} else if (g.getPhase().isSuddenDeath()) {
|
||||||
|
// Sudden death with only the other
|
||||||
|
setPhase(g, Phase.suddenDeathOnly(other(player)));
|
||||||
|
} else {
|
||||||
|
endGuessingRound(g, player);
|
||||||
|
}
|
||||||
|
} else if (!allGreenRevealed(g, other(player), g.getConfig().isOnlyOneGreenGreen()) && guessedColor == Color.GREEN) {
|
||||||
|
// There is still cards i should find
|
||||||
|
if (!g.getPhase().isSuddenDeath() && !g.getCurrentHint().canStillGuess(g.getCurrentGuessCount(), g.getConfig().isOneMoreGuess())) {
|
||||||
|
if(!g.getConfig().isOneMoreGuess() && g.getCurrentHint().canStillGuess(g.getCurrentGuessCount(), true)) {
|
||||||
|
// Having one more guess could make us continue
|
||||||
|
setPhase(g, Phase.waitingForOneMore(player));
|
||||||
|
} else {
|
||||||
|
// We're done with this round of guesses anyway
|
||||||
|
endGuessingRound(g, player);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// I continue guessing cards in this round
|
||||||
|
}
|
||||||
|
} else if (guessedColor == Color.WHITE && !g.getPhase().isSuddenDeath()){
|
||||||
|
endGuessingRound(g, player);
|
||||||
|
} else {
|
||||||
|
throw new IllegalStateException("This should logically never happen");
|
||||||
|
}
|
||||||
|
if(!g.getPhase().isAnyoneGuessing() && !g.getPhase().isWaitingForOneMore())
|
||||||
|
// It means this pick ended the guessing turn
|
||||||
|
newGameEvent(g, GameEvent.newEndGuessingEvent(false, player));
|
||||||
|
return guessedColor;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void endGuessing(Grid g, char player) throws IllegalGameActionException {
|
||||||
|
if (!g.getPhase().canPlay() || (!g.getPhase().isGuessing(player) && !g.getPhase().isWaitingForOneMore())) {
|
||||||
|
throw new IllegalGameActionException(g, "This action is not allowed now");
|
||||||
|
}
|
||||||
|
if (!g.getConfig().isEndGuessing() && !g.getPhase().isWaitingForOneMore()) {
|
||||||
|
throw new IllegalGameActionException(g, "Cannot end guessing in this game");
|
||||||
|
}
|
||||||
|
endGuessingRound(g, player);
|
||||||
|
newGameEvent(g, GameEvent.newEndGuessingEvent(true, player));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void enterSuddenDeath(Grid g) throws IllegalGameActionException {
|
||||||
|
if(!g.getPhase().isNeedMoreHints()) {
|
||||||
|
throw new IllegalGameActionException(g, "You can only enter sudden death when being out of hints");
|
||||||
|
}
|
||||||
|
enterSuddenDeathInternal(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UPDATE GAME CONFIG
|
||||||
|
*/
|
||||||
|
public void unlock(Grid g, MutableOption opt, Object o) throws IllegalGameActionException {
|
||||||
|
switch(opt) {
|
||||||
|
case MutableOption.MAX_HINT:
|
||||||
|
unlockHintCount(g, (Integer)o);
|
||||||
|
break;
|
||||||
|
case MutableOption.AVAILABLE_HINT_WORD_COUNT:
|
||||||
|
unlockNewWordCounts(g, ((Set<?>)o).stream().map(i -> (Integer)i).collect(Collectors.toSet()));
|
||||||
|
break;
|
||||||
|
case MutableOption.ONE_MORE_GUESS:
|
||||||
|
unlockOneMoreGuess(g);
|
||||||
|
break;
|
||||||
|
case MutableOption.HINT_MAX_LENGTH:
|
||||||
|
unlockHintMaxLength(g, (Integer)o);
|
||||||
|
break;
|
||||||
|
case MutableOption.END_GUESSING:
|
||||||
|
unlockEndGuessing(g);
|
||||||
|
break;
|
||||||
|
case MutableOption.ONLY_ONE_GREEN_GREEN:
|
||||||
|
unlockOnlyOneGreenGreen(g);
|
||||||
|
break;
|
||||||
|
case MutableOption.SUDDEN_DEATH:
|
||||||
|
unlockSuddenDeath(g);
|
||||||
|
break;
|
||||||
|
case MutableOption.HINTS_IN_A_ROW:
|
||||||
|
unlockHintsInARow(g);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new IllegalStateException("Unknown MutableOption to edit");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public void unlockHintCount(Grid g, int newHintCount) throws IllegalGameActionException {
|
||||||
|
if(newHintCount > g.getConfig().getMaxMaxHint()) {
|
||||||
|
throw new IllegalGameActionException(g, "Hint count higher than highest hint count");
|
||||||
|
}
|
||||||
|
if(newHintCount <= g.getConfig().getMaxHint()) {
|
||||||
|
throw new IllegalGameActionException(g, "Cannot lower hint count");
|
||||||
|
}
|
||||||
|
changeConfig(g, MutableOption.MAX_HINT, newHintCount);
|
||||||
|
if(g.getPhase().isNeedMoreHints() || g.getPhase().isNeedMoreHintsOrSuddenDeath()) {
|
||||||
|
setPhase(g, Phase.hinting(g.getPhase().getHinting()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockNewWordCounts(Grid g, Set<Integer> newAvailableHintWordCounts) throws IllegalGameActionException {
|
||||||
|
if (!newAvailableHintWordCounts.containsAll(g.getConfig().getAvailableHintWordCount())
|
||||||
|
|| g.getConfig().getAvailableHintWordCount().containsAll(newAvailableHintWordCounts)) {
|
||||||
|
// i.e. If given data is not a strict superset
|
||||||
|
throw new IllegalGameActionException(g,
|
||||||
|
"Can only add hint word options (%s) is not a strict superset of (%s)"
|
||||||
|
.formatted(newAvailableHintWordCounts, g.getConfig().getAvailableHintWordCount())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
changeConfig(g, MutableOption.AVAILABLE_HINT_WORD_COUNT, newAvailableHintWordCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockOneMoreGuess(Grid g) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().isOneMoreGuess())
|
||||||
|
throw new IllegalGameActionException(g, "Cannot unlock oneMoreGuess as it is already unlocked");
|
||||||
|
changeConfig(g, MutableOption.ONE_MORE_GUESS, true);
|
||||||
|
if(g.getPhase().isWaitingForOneMore()) {
|
||||||
|
setPhase(g, Phase.guessing(g.getPhase().getGuessing()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockHintMaxLength(Grid g, int newHintMaxLength) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().getHintMaxLength() >= newHintMaxLength) {
|
||||||
|
throw new IllegalGameActionException(g, "Can only raise max hint length");
|
||||||
|
}
|
||||||
|
changeConfig(g, MutableOption.HINT_MAX_LENGTH, newHintMaxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockEndGuessing(Grid g) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().isEndGuessing())
|
||||||
|
throw new IllegalGameActionException(g, "Cannot unlock endGuessing as it is already unlocked");
|
||||||
|
changeConfig(g, MutableOption.END_GUESSING, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockOnlyOneGreenGreen(Grid g) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().isOnlyOneGreenGreen())
|
||||||
|
throw new IllegalGameActionException(g, "Cannot unlock onlyOneGreenGreen as it is already unlocked");
|
||||||
|
changeConfig(g, MutableOption.ONLY_ONE_GREEN_GREEN, true);
|
||||||
|
if(g.getPhase() == Phase.WIN_IF_GREEN_GREEN) {
|
||||||
|
endGame(g, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockSuddenDeath(Grid g) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().isSuddenDeath())
|
||||||
|
throw new IllegalGameActionException(g, "Cannot unlock suddenDeath as it is already unlocked");
|
||||||
|
changeConfig(g, MutableOption.SUDDEN_DEATH,true);
|
||||||
|
if (g.getPhase().isNeedMoreHintsOrSuddenDeath()) {
|
||||||
|
setPhase(g, Phase.needMoreHints(g.getPhase().getHinting()));
|
||||||
|
} else if (g.getPhase() == Phase.NEED_SUDDEN_DEATH) {
|
||||||
|
// We're entering sudden death
|
||||||
|
enterSuddenDeathInternal(g);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void unlockHintsInARow(Grid g) throws IllegalGameActionException {
|
||||||
|
if(g.getConfig().isHintsInARow())
|
||||||
|
throw new IllegalGameActionException(g, "Cannot unlock hintsInARow as it is already unlocked");
|
||||||
|
changeConfig(g, MutableOption.HINTS_IN_A_ROW,true);
|
||||||
|
if ((g.getPhase() == Phase.HINTING_A || g.getPhase() == Phase.HINTING_B)
|
||||||
|
&& !allGreenRevealed(g, 'A', g.getConfig().isOnlyOneGreenGreen())
|
||||||
|
&& !allGreenRevealed(g, 'B', g.getConfig().isOnlyOneGreenGreen())) {
|
||||||
|
setPhase(g, Phase.HINTING_BOTH);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,14 +1,31 @@
|
|||||||
package com.bernard.nodecames.model;
|
package com.bernard.nodecames.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import jakarta.persistence.EmbeddedId;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.MapsId;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
import lombok.Setter;
|
import lombok.Setter;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@Entity
|
||||||
public class Card {
|
public class Card {
|
||||||
|
|
||||||
|
@EmbeddedId
|
||||||
|
CardId id;
|
||||||
|
|
||||||
|
@MapsId("gridId")
|
||||||
|
@ManyToOne
|
||||||
|
Grid grid;
|
||||||
|
|
||||||
String word;
|
String word;
|
||||||
|
|
||||||
Color colorA;
|
Color colorA;
|
||||||
@@ -16,8 +33,12 @@ public class Card {
|
|||||||
boolean colorAPublic;
|
boolean colorAPublic;
|
||||||
boolean colorBPublic;
|
boolean colorBPublic;
|
||||||
|
|
||||||
public Card(String word, Color colorA, Color colorB) {
|
public Card(Grid g, int index, String word, Color colorA, Color colorB) {
|
||||||
this(word, colorA, colorB, false, false);
|
this(new CardId(g.getId(), index), g, word, colorA, colorB, false, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getIndex() {
|
||||||
|
return this.getId().getIndex();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Color getColor(char player) {
|
public Color getColor(char player) {
|
||||||
@@ -40,9 +61,29 @@ public class Card {
|
|||||||
isColorAPublic()?'O':'X', isColorBPublic()?'O':'X');
|
isColorAPublic()?'O':'X', isColorBPublic()?'O':'X');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
public enum Color {
|
public enum Color {
|
||||||
GREEN,
|
GREEN('G'),
|
||||||
WHITE,
|
WHITE('W'),
|
||||||
BLACK;
|
BLACK('B');
|
||||||
|
|
||||||
|
private char letter;
|
||||||
|
|
||||||
|
public static final Color findByLetter(char letter) {
|
||||||
|
for(Color c : Color.values()) {
|
||||||
|
if(c.letter == letter)
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class CardId {
|
||||||
|
UUID gridId;
|
||||||
|
int index;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,23 +1,43 @@
|
|||||||
package com.bernard.nodecames.model;
|
package com.bernard.nodecames.model;
|
||||||
|
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.hibernate.annotations.JdbcTypeCode;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.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;
|
||||||
|
|
||||||
@Entity
|
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@Entity
|
||||||
public class Game {
|
public class Game {
|
||||||
|
|
||||||
@Id
|
@Id
|
||||||
@GeneratedValue
|
private UUID id;
|
||||||
private Long id;
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
String name;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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))
|
||||||
|
));
|
||||||
|
}
|
||||||
@@ -1,49 +1,92 @@
|
|||||||
package com.bernard.nodecames.model;
|
package com.bernard.nodecames.model;
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
import com.bernard.nodecames.model.Card.Color;
|
||||||
import lombok.Getter;
|
|
||||||
|
|
||||||
@Getter
|
import tools.jackson.databind.JsonNode;
|
||||||
public class GameEvent {
|
import tools.jackson.databind.node.JsonNodeFactory;
|
||||||
|
import tools.jackson.databind.node.ObjectNode;
|
||||||
|
|
||||||
Type type;
|
public sealed interface GameEvent {
|
||||||
|
|
||||||
Object data;
|
|
||||||
|
|
||||||
// 'A' for player A, 'B' for player B, '_' for none
|
|
||||||
char issuer;
|
|
||||||
|
|
||||||
private GameEvent(Type type, Object data, char issuer){
|
|
||||||
this.data = data;
|
|
||||||
this.issuer = issuer;
|
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static GameEvent newHintEvent(Hint h, char issuer) {
|
public static GameEvent newHintEvent(Hint h, char issuer) {
|
||||||
return new GameEvent(Type.HINT, h, issuer);
|
return new HintEvent(issuer, h);
|
||||||
}
|
}
|
||||||
public static GameEvent newGuessEvent(Card c, char issuer) {
|
public static GameEvent newGuessEvent(int index, char issuer, Color color) {
|
||||||
return new GameEvent(Type.GUESS, c, issuer);
|
return new GuessEvent(issuer, index, color);
|
||||||
}
|
}
|
||||||
public static GameEvent newEndGuessingEvent(boolean manual, char issuer) {
|
public static GameEvent newEndGuessingEvent(boolean manual, char issuer) {
|
||||||
return new GameEvent(Type.END_GUESSING, manual, issuer);
|
return new EndGuessingEvent(issuer, manual);
|
||||||
}
|
}
|
||||||
public static GameEvent newGameEndEvent(boolean won) {
|
public static GameEvent newGameEndEvent(boolean won) {
|
||||||
return new GameEvent(Type.GAME_END, won, '_');
|
return new GameEndEvent(won);
|
||||||
}
|
}
|
||||||
public static GameEvent newSuddenDeath() {
|
public static GameEvent newSuddenDeath() {
|
||||||
return new GameEvent(Type.SUDDEN_DEATH, Object.class, '_');
|
return new EnterSuddenDeathEvent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
public static JsonNode toJsonNode(GameEvent ge) {
|
||||||
public enum Type {
|
JsonNodeFactory jsn = JsonNodeFactory.instance;
|
||||||
HINT(Hint.class),
|
ObjectNode o = jsn.objectNode();
|
||||||
GUESS(Card.class),
|
o.set("type", jsn.stringNode(ge.getClass().getSimpleName()));
|
||||||
END_GUESSING(Boolean.class),
|
switch (ge) {
|
||||||
GAME_END(Boolean.class),
|
case HintEvent(char issuer, Hint hint):
|
||||||
SUDDEN_DEATH(Object.class);
|
o.set("issuer", jsn.stringNode(Character.toString(issuer)));
|
||||||
|
o.set("word", jsn.stringNode(hint.getWord()));
|
||||||
Class<?> dataType;
|
o.set("word-count", jsn.numberNode(hint.getWordCount()));
|
||||||
|
break;
|
||||||
|
case GuessEvent(char issuer, int index, Color color):
|
||||||
|
o.set("issuer", jsn.stringNode(Character.toString(issuer)));
|
||||||
|
o.set("card-index", jsn.numberNode(index));
|
||||||
|
o.set("color", jsn.stringNode(color.name().toLowerCase()));
|
||||||
|
break;
|
||||||
|
case EndGuessingEvent(char issuer, boolean manual):
|
||||||
|
o.set("issuer", jsn.stringNode(Character.toString(issuer)));
|
||||||
|
o.set("manual", jsn.booleanNode(manual));
|
||||||
|
break;
|
||||||
|
case GameEndEvent(boolean win):
|
||||||
|
o.set("win", jsn.booleanNode(win));
|
||||||
|
break;
|
||||||
|
case EnterSuddenDeathEvent():
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
}
|
||||||
|
public static GameEvent fromJsonNode(JsonNode o) {
|
||||||
|
String type = o.get("type").stringValue();
|
||||||
|
return switch(type) {
|
||||||
|
case "HintEvent" ->
|
||||||
|
new HintEvent(
|
||||||
|
o.get("issuer").asString().charAt(0),
|
||||||
|
new Hint(
|
||||||
|
o.get("word").stringValue(),
|
||||||
|
o.get("word-count").intValue()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
case "GuessEvent" ->
|
||||||
|
new GuessEvent(
|
||||||
|
o.get("issuer").asString().charAt(0),
|
||||||
|
o.get("card-index").intValue(),
|
||||||
|
Color.valueOf(o.get("color").stringValue().toUpperCase())
|
||||||
|
);
|
||||||
|
case "EndGuessingEvent" ->
|
||||||
|
new EndGuessingEvent(
|
||||||
|
o.get("issuer").asString().charAt(0),
|
||||||
|
o.get("manual").booleanValue()
|
||||||
|
);
|
||||||
|
case "GameEndEvent" ->
|
||||||
|
new GameEndEvent(
|
||||||
|
o.get("win").booleanValue()
|
||||||
|
);
|
||||||
|
case "EnterSuddenDeathEvent" ->
|
||||||
|
new EnterSuddenDeathEvent();
|
||||||
|
default -> throw new IllegalArgumentException("Unknown event type : " + type);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
record HintEvent(char issuer, Hint hint) implements GameEvent {}
|
||||||
|
record GuessEvent(char issuer, int index, Color color) implements GameEvent {}
|
||||||
|
record EndGuessingEvent(char issuer, boolean manual) implements GameEvent {}
|
||||||
|
record GameEndEvent(boolean win) implements GameEvent {}
|
||||||
|
record EnterSuddenDeathEvent() implements GameEvent {}
|
||||||
@@ -1,24 +1,50 @@
|
|||||||
package com.bernard.nodecames.model;
|
package com.bernard.nodecames.model;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.hibernate.annotations.JdbcTypeCode;
|
||||||
|
import org.hibernate.type.SqlTypes;
|
||||||
|
|
||||||
|
import jakarta.annotation.Nullable;
|
||||||
|
import jakarta.persistence.AttributeConverter;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Convert;
|
||||||
|
import jakarta.persistence.Converter;
|
||||||
|
import jakarta.persistence.Embedded;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
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.node.ArrayNode;
|
||||||
|
import tools.jackson.databind.node.JsonNodeFactory;
|
||||||
|
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
@Setter
|
@Setter
|
||||||
|
@Entity
|
||||||
public class Grid {
|
public class Grid {
|
||||||
|
|
||||||
String id;
|
@Id
|
||||||
|
UUID id;
|
||||||
|
|
||||||
Card[] cards;
|
@ManyToOne(optional = true)
|
||||||
|
World w;
|
||||||
|
|
||||||
|
@Column(columnDefinition = "json")
|
||||||
|
@Convert(converter = GameEventListAdapter.class)
|
||||||
|
@JdbcTypeCode(SqlTypes.JSON)
|
||||||
List<GameEvent> gameEvents;
|
List<GameEvent> gameEvents;
|
||||||
|
|
||||||
|
@Embedded
|
||||||
Hint currentHint = null;
|
Hint currentHint = null;
|
||||||
int currentGuessCount;
|
int currentGuessCount;
|
||||||
|
|
||||||
@@ -26,10 +52,18 @@ public class Grid {
|
|||||||
int usedWhites;
|
int usedWhites;
|
||||||
|
|
||||||
Phase phase;
|
Phase phase;
|
||||||
Optional<Boolean> won;
|
|
||||||
|
|
||||||
|
//TODO make this parameter a real Optional<Boolean> with Jdbc understanding that optional means nullable
|
||||||
|
@Nullable
|
||||||
|
Boolean won;
|
||||||
|
|
||||||
|
@Embedded
|
||||||
GridConfig config;
|
GridConfig config;
|
||||||
|
|
||||||
|
public Grid(UUID id, World w, Phase phase, GridConfig config) {
|
||||||
|
this(id, w, List.of(), null, 0, 0, 0, phase, null, config);
|
||||||
|
}
|
||||||
|
|
||||||
public void incrementHintCount() {
|
public void incrementHintCount() {
|
||||||
this.setUsedHints(this.getUsedHints()+1);
|
this.setUsedHints(this.getUsedHints()+1);
|
||||||
}
|
}
|
||||||
@@ -40,10 +74,14 @@ public class Grid {
|
|||||||
this.setCurrentGuessCount(this.getCurrentGuessCount()+1);
|
this.setCurrentGuessCount(this.getCurrentGuessCount()+1);
|
||||||
}
|
}
|
||||||
public void newGameEvent(GameEvent event) {
|
public void newGameEvent(GameEvent event) {
|
||||||
|
this.gameEvents = new ArrayList<>(gameEvents);
|
||||||
this.gameEvents.add(event);
|
this.gameEvents.add(event);
|
||||||
}
|
}
|
||||||
public int getCardCount() {
|
public Optional<Boolean> isWonOpt() {
|
||||||
return this.getCards().length;
|
return Optional.ofNullable(this.won);
|
||||||
|
}
|
||||||
|
public void setWonOpt(Optional<Boolean> won) {
|
||||||
|
this.won = won.orElse(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
@@ -121,4 +159,25 @@ public class Grid {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Converter
|
||||||
|
public static class GameEventListAdapter implements AttributeConverter<List<GameEvent>, JsonNode> {
|
||||||
|
|
||||||
|
private final JsonNodeFactory jsn;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonNode convertToDatabaseColumn(List<GameEvent> attribute) {
|
||||||
|
ArrayNode out = jsn.arrayNode(attribute.size());
|
||||||
|
for(GameEvent ge : attribute) {
|
||||||
|
out.add(GameEvent.toJsonNode(ge));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<GameEvent> convertToEntityAttribute(JsonNode dbData) {
|
||||||
|
return dbData.asArray().valueStream().map(GameEvent::fromJsonNode).toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,62 +1,48 @@
|
|||||||
package com.bernard.nodecames.model;
|
package com.bernard.nodecames.model;
|
||||||
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.BB;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.BG;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.BW;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.GB;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.GG;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.GW;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.WB;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.WG;
|
|
||||||
import static com.bernard.nodecames.model.GridConfig.CardColors.WW;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
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.Convert;
|
||||||
|
import jakarta.persistence.Embeddable;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.Value;
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
@Getter
|
@Getter
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
|
@Embeddable
|
||||||
public class GridConfig {
|
public class GridConfig {
|
||||||
|
|
||||||
public static final List<CardColors> CLASSICAL25 = List.of(
|
public static final GridConfig ZERO =
|
||||||
BB,BW,WB,BG,GB,GG,GG,GG,WG,WG,WG,WG,WG,GW,GW,GW,GW,GW,WW,WW,WW,WW,WW,WW,WW
|
WorldConfig.zeroWithSize(ColorDistribution.CLASSICAL25).makeGridConfig(Inventory.empty());
|
||||||
);
|
public static final GridConfig REGULAR =
|
||||||
public static final Set<Integer> BASE_HINTWORDCOUNT = Set.of(
|
WorldConfig.classicalWithSize(ColorDistribution.CLASSICAL25, 9).makeGridConfig(Inventory.empty());
|
||||||
0,1,2,3,4,5,6,7,8,9,-1
|
public static final GridConfig BIG =
|
||||||
);
|
WorldConfig.classicalWithSize(ColorDistribution.CLASSICAL25, 9).makeGridConfig(Inventory.empty());
|
||||||
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);
|
|
||||||
|
|
||||||
|
|
||||||
// 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;
|
||||||
private List<CardColors> 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;
|
||||||
@@ -68,10 +54,6 @@ public class GridConfig {
|
|||||||
private boolean suddenDeath;
|
private boolean suddenDeath;
|
||||||
private boolean hintsInARow;
|
private boolean hintsInARow;
|
||||||
|
|
||||||
public int getWordCount() {
|
|
||||||
return wordDistribution.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void set(MutableOption opt, Object value) {
|
public void set(MutableOption opt, Object value) {
|
||||||
try{
|
try{
|
||||||
switch(opt) {
|
switch(opt) {
|
||||||
@@ -123,24 +105,9 @@ public class GridConfig {
|
|||||||
return 0 <= i || i == -1;
|
return 0 <= i || i == -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Value(staticConstructor = "of")
|
|
||||||
public static final class CardColors {
|
|
||||||
Color faceA;
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
@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),
|
||||||
|
|||||||
@@ -1,10 +1,16 @@
|
|||||||
package com.bernard.nodecames.model;
|
package com.bernard.nodecames.model;
|
||||||
|
|
||||||
|
import jakarta.persistence.Embeddable;
|
||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
@AllArgsConstructor
|
@AllArgsConstructor
|
||||||
|
@NoArgsConstructor
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Embeddable
|
||||||
public class Hint {
|
public class Hint {
|
||||||
|
|
||||||
String word;
|
String word;
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package com.bernard.nodecames.model;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import jakarta.persistence.Embeddable;
|
||||||
|
import jakarta.persistence.EmbeddedId;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.ManyToOne;
|
||||||
|
import jakarta.persistence.MapsId;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@Setter
|
||||||
|
@Entity
|
||||||
|
@NoArgsConstructor
|
||||||
|
public class World {
|
||||||
|
|
||||||
|
@EmbeddedId
|
||||||
|
WorldId id;
|
||||||
|
|
||||||
|
@MapsId("gameId")
|
||||||
|
@ManyToOne
|
||||||
|
Game game;
|
||||||
|
|
||||||
|
@ManyToOne(optional = true)
|
||||||
|
Grid currentGrid;
|
||||||
|
|
||||||
|
boolean won;
|
||||||
|
|
||||||
|
boolean locked;
|
||||||
|
|
||||||
|
public World(Game g, String name, Grid currentGrid, boolean won, boolean locked) {
|
||||||
|
this.id = new WorldId(g.getId(), name);
|
||||||
|
this.game = g;
|
||||||
|
this.currentGrid = currentGrid;
|
||||||
|
this.won = won;
|
||||||
|
this.locked = locked;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String 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
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Embeddable
|
||||||
|
public static class WorldId {
|
||||||
|
UUID gameId;
|
||||||
|
String name;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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,13 @@
|
|||||||
|
package com.bernard.nodecames.repository;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import com.bernard.nodecames.model.Card;
|
||||||
|
import com.bernard.nodecames.model.Grid;
|
||||||
|
import com.bernard.nodecames.model.Card.CardId;
|
||||||
|
|
||||||
|
public interface CardRepository extends JpaRepository<Card, CardId> {
|
||||||
|
public Card[] findByGridOrderById_Index(Grid g);
|
||||||
|
public Card findByGridAndId_Index(Grid g, int index);
|
||||||
|
public int countByGrid(Grid g);
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.bernard.nodecames.repository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import com.bernard.nodecames.model.Game;
|
||||||
|
|
||||||
|
public interface GameRepository extends JpaRepository<Game, UUID> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.bernard.nodecames.repository;
|
||||||
|
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import com.bernard.nodecames.model.Grid;
|
||||||
|
|
||||||
|
public interface GridRepository extends JpaRepository<Grid, UUID> {
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.bernard.nodecames.repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
|
||||||
|
import com.bernard.nodecames.model.Game;
|
||||||
|
import com.bernard.nodecames.model.World;
|
||||||
|
import com.bernard.nodecames.model.World.WorldId;
|
||||||
|
|
||||||
|
public interface WorldRepository extends JpaRepository<World, WorldId>{
|
||||||
|
public Optional<World> findByGameAndId_Name(Game g, String name);
|
||||||
|
public Set<World> findByGame(Game g);
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package com.bernard.nodecames.util;
|
||||||
|
|
||||||
|
import java.util.EnumSet;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.function.BiConsumer;
|
||||||
|
import java.util.function.BinaryOperator;
|
||||||
|
import java.util.function.Function;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
import java.util.stream.Collector;
|
||||||
|
|
||||||
|
import tools.jackson.databind.JsonNode;
|
||||||
|
import tools.jackson.databind.node.ArrayNode;
|
||||||
|
import tools.jackson.databind.node.JsonNodeFactory;
|
||||||
|
|
||||||
|
public class Utils {
|
||||||
|
|
||||||
|
private Utils() {}
|
||||||
|
|
||||||
|
public static final Collector<JsonNode,ArrayNode,ArrayNode> jsonNodeCollector(JsonNodeFactory jsn) {
|
||||||
|
return new Collector<JsonNode,ArrayNode,ArrayNode>() {
|
||||||
|
@Override
|
||||||
|
public Supplier<ArrayNode> supplier() {
|
||||||
|
return jsn::arrayNode;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public BiConsumer<ArrayNode, JsonNode> accumulator() {
|
||||||
|
return ArrayNode::add;
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public BinaryOperator<ArrayNode> combiner() {
|
||||||
|
return (a,b) -> {
|
||||||
|
a.addAll(b);
|
||||||
|
return a;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Function<ArrayNode, ArrayNode> finisher() {
|
||||||
|
return Function.identity();
|
||||||
|
}
|
||||||
|
@Override
|
||||||
|
public Set<Characteristics> characteristics() {
|
||||||
|
return EnumSet.of(Characteristics.IDENTITY_FINISH);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,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,8 @@
|
|||||||
|
create table card (index integer not null, colora smallint check ((colora between 0 and 2)), colorapublic boolean not null, colorb smallint check ((colorb between 0 and 2)), colorbpublic boolean not null, word varchar(255), grid_id uuid not null, primary key (grid_id, index));
|
||||||
|
create table game (id uuid not null, name varchar(255), primary key (id));
|
||||||
|
create table grid (id uuid not null, anyone_starts boolean, available_hint_word_count integer array, end_guessing boolean, hint_max_length integer, hints_inarow boolean, max_hint integer, max_max_hint integer, maybe_one_more_guess boolean, maybe_sudden_death boolean, one_more_guess boolean, only_one_green_green boolean, only_one_green_green_prevent_death boolean, structured_grid boolean, sudden_death boolean, word_distribution varchar(255), current_guess_count integer not null, word varchar(255), word_count integer, game_events json, phase smallint check ((phase between 0 and 16)), used_hints integer not null, used_whites integer not null, won boolean, w_game_id uuid, w_name varchar(255), primary key (id));
|
||||||
|
create table world (name varchar(255) not null, anyone_starts boolean, available_hint_word_count integer array, end_guessing boolean, hint_max_length integer, hints_inarow boolean, max_hint integer, max_max_hint integer, maybe_one_more_guess boolean, maybe_sudden_death boolean, one_more_guess boolean, only_one_green_green boolean, only_one_green_green_prevent_death boolean, structured_grid boolean, sudden_death boolean, word_distribution varchar(255), won boolean not null, game_id uuid not null, current_grid_id uuid, primary key (game_id, name));
|
||||||
|
alter table if exists card add constraint FK5x40wglgnltvcrme1tqa8ilsx foreign key (grid_id) references grid;
|
||||||
|
alter table if exists grid add constraint FK583ks7m95vxertpybyo2s5h6n foreign key (w_game_id, w_name) references world;
|
||||||
|
alter table if exists world add constraint FKo96aqpsvpf22bjeqk2w6l8jvx foreign key (current_grid_id) references grid;
|
||||||
|
alter table if exists world add constraint FKe9tajxpu9c9llbtdaldt6t18s foreign key (game_id) references game;
|
||||||
@@ -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;
|
||||||
@@ -15,3 +15,7 @@ main{
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
margin-top: 115px;
|
margin-top: 115px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
main section {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
var socket = null
|
var socket = null
|
||||||
var player = '_'
|
var player = '_'
|
||||||
var roomId = '_'
|
|
||||||
var wsHeaders = {}
|
var wsHeaders = {}
|
||||||
var cards = []
|
var cards = []
|
||||||
var gc = {}
|
var gc = {}
|
||||||
@@ -11,13 +10,6 @@ var phase = {}
|
|||||||
|
|
||||||
// This variable is here to check for updates to availableHintWordCount, as updating it is a bit expensive
|
// This variable is here to check for updates to availableHintWordCount, as updating it is a bit expensive
|
||||||
var registeredAvailableHintWordCountInvalid = true
|
var registeredAvailableHintWordCountInvalid = true
|
||||||
function getRoomId() {
|
|
||||||
const urlRegex = /\/room\/([a-f0-9-]{36})\/grid$/
|
|
||||||
const res = urlRegex.exec(window.location.href)
|
|
||||||
if (res == null)
|
|
||||||
console.log("Could not understand room id from url")
|
|
||||||
return res[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Update html to match state
|
* Update html to match state
|
||||||
@@ -30,12 +22,12 @@ function updatePhase() {
|
|||||||
$('#hint-counter-den').text(gc["max-hint"])
|
$('#hint-counter-den').text(gc["max-hint"])
|
||||||
|
|
||||||
if(currentHint) {
|
if(currentHint) {
|
||||||
$('#hint-text').text(currentHint['word']+" in "+currentHint['wordCount'])
|
$('#hint-text').text(currentHint['word']+" in "+currentHint['word-count'])
|
||||||
$('#guess-counter-num').text(currentGuessCount)
|
$('#guess-counter-num').text(currentGuessCount)
|
||||||
if(gc['one-more-guess']) {
|
if(gc['one-more-guess']) {
|
||||||
$('#guess-counter-den').text(currentHint['wordCount'] + "(+1)")
|
$('#guess-counter-den').text(currentHint['word-count'] + "(+1)")
|
||||||
} else {
|
} else {
|
||||||
$('#guess-counter-den').text(currentHint['wordCount'])
|
$('#guess-counter-den').text(currentHint['word-count'])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(registeredAvailableHintWordCountInvalid) {
|
if(registeredAvailableHintWordCountInvalid) {
|
||||||
@@ -64,7 +56,7 @@ function updatePhase() {
|
|||||||
}
|
}
|
||||||
if((phase['name'] == 'waiting_for_one_more_'+p) ||
|
if((phase['name'] == 'waiting_for_one_more_'+p) ||
|
||||||
(phase[p+'-guessing'] &&
|
(phase[p+'-guessing'] &&
|
||||||
(gc['end-guessing'] || (currentHint && currentGuessCount >= currentHint['wordCount'])))) {
|
(gc['end-guessing'] || (currentHint && currentGuessCount >= currentHint['word-count'])))) {
|
||||||
$('#end-guessing-button').show()
|
$('#end-guessing-button').show()
|
||||||
} else {
|
} else {
|
||||||
$('#end-guessing-button').hide()
|
$('#end-guessing-button').hide()
|
||||||
@@ -187,28 +179,28 @@ function addEndGameEvent(issuer, won) {
|
|||||||
|
|
||||||
function addEvent(e) {
|
function addEvent(e) {
|
||||||
switch(e.type) {
|
switch(e.type) {
|
||||||
case "hint":
|
case "HintEvent":
|
||||||
addHintEvent(e.issuer, e.word, e.wordCount)
|
addHintEvent(e.issuer, e.word, e['word-count'])
|
||||||
currentHint = {word: e.word, wordCount: e.wordCount}
|
currentHint = {word: e.word, 'word-count': e['word-count']}
|
||||||
currentHintCount += 1
|
currentHintCount += 1
|
||||||
currentGuessCount = 0
|
currentGuessCount = 0
|
||||||
updatePhase()
|
updatePhase()
|
||||||
break
|
break
|
||||||
case "guess":
|
case "GuessEvent":
|
||||||
addPointEvent(e.issuer, e['card-index'], e.color)
|
addPointEvent(e.issuer, e['card-index'], e.color)
|
||||||
currentGuessCount += 1
|
currentGuessCount += 1
|
||||||
break
|
break
|
||||||
case "end_guessing":
|
case "EndGuessingEvent":
|
||||||
addEndGuessingEvent(e.issuer, e.manual)
|
addEndGuessingEvent(e.issuer, e.manual)
|
||||||
currentHint = null
|
currentHint = null
|
||||||
currentGuessCount = 0
|
currentGuessCount = 0
|
||||||
break
|
break
|
||||||
case "game_end":
|
case "GameEndEvent":
|
||||||
addEndGameEvent(e.issuer, e.win)
|
addEndGameEvent(e.issuer, e.win)
|
||||||
currentHint = null
|
currentHint = null
|
||||||
currentGuessCount = 0
|
currentGuessCount = 0
|
||||||
break
|
break
|
||||||
case "sudden_death":
|
case "EnterSuddenDeathEvent":
|
||||||
addSuddenDeathEvent()
|
addSuddenDeathEvent()
|
||||||
currentHint = null
|
currentHint = null
|
||||||
currentGuessCount = 0
|
currentGuessCount = 0
|
||||||
@@ -245,8 +237,7 @@ function initGame(data) {
|
|||||||
updatePhase()
|
updatePhase()
|
||||||
}
|
}
|
||||||
|
|
||||||
function selectPlayer(e) {
|
function selectPlayer(player) {
|
||||||
player = (e.target.id == "select-player-a") ? 'A' : 'B'
|
|
||||||
wsHeaders = {
|
wsHeaders = {
|
||||||
"room": roomId,
|
"room": roomId,
|
||||||
"player": player
|
"player": player
|
||||||
@@ -265,10 +256,10 @@ function selectPlayer(e) {
|
|||||||
|
|
||||||
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)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,14 +327,10 @@ function onConfigChange(m) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initialize() {
|
function initialize() {
|
||||||
$('#select-player-a').on('click', selectPlayer)
|
|
||||||
$('#select-player-b').on('click', selectPlayer)
|
|
||||||
$('#submit-hint button').on('click', submitHint)
|
$('#submit-hint button').on('click', submitHint)
|
||||||
$('#end-guessing-button').on('click', endGuessing)
|
$('#end-guessing-button').on('click', endGuessing)
|
||||||
$('#enter-sudden-death-button').on('click', enterSuddenDeath)
|
$('#enter-sudden-death-button').on('click', enterSuddenDeath)
|
||||||
|
|
||||||
roomId = getRoomId()
|
|
||||||
|
|
||||||
$('#game-panel').hide()
|
$('#game-panel').hide()
|
||||||
$('#side-panel').hide()
|
$('#side-panel').hide()
|
||||||
}
|
}
|
||||||
@@ -9,11 +9,6 @@
|
|||||||
<script th:src="@{/js/grid.js}" type="text/javascript"></script>
|
<script th:src="@{/js/grid.js}" type="text/javascript"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="player-selector">
|
|
||||||
Which player are you ? <br/>
|
|
||||||
<button id="select-player-a">Player A</button>
|
|
||||||
<button id="select-player-b">Player B</button>
|
|
||||||
</div>
|
|
||||||
<main>
|
<main>
|
||||||
<div id="game-panel">
|
<div id="game-panel">
|
||||||
<ul id="cards-list">
|
<ul id="cards-list">
|
||||||
@@ -56,7 +51,12 @@
|
|||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<script>
|
<script>
|
||||||
|
/*<![CDATA[*/
|
||||||
|
var player = '[(${player})]';
|
||||||
|
var roomId = '[(${grid.id})]';
|
||||||
|
/*]]>*/
|
||||||
initialize()
|
initialize()
|
||||||
|
selectPlayer(player)
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
@@ -6,9 +6,34 @@
|
|||||||
|
|
||||||
<body>
|
<body>
|
||||||
<main>
|
<main>
|
||||||
Vous voulez jouer à Node Cames ? Ben c'est pas encore prêt :/
|
Vous voulez jouer à Node Cames ? Ben c'est là.
|
||||||
<br/>
|
<br/>
|
||||||
<a href="/create-room">(Bon, en vrai, vous pouvez créer une room en cliquant ici)</a>
|
<a href="/create-room?type=zero">Grille Zéro</a>
|
||||||
|
<a href="/create-room?type=base">Grille Normale</a>
|
||||||
|
<a href="/create-room?type=big">Grille Grosse</a>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<form method="GET" th:action="@{/create-game}">
|
||||||
|
<label for="create-game-name">Name : </label>
|
||||||
|
<input type="text" id="create-game-name" name="name"/>
|
||||||
|
<select name="type">
|
||||||
|
<option value="default">Default</option>
|
||||||
|
<option value="regular">Regular</option>
|
||||||
|
</select>
|
||||||
|
<input type="submit" value="Create game"/>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
|
||||||
|
<div>
|
||||||
|
Currently running games:
|
||||||
|
<ul>
|
||||||
|
<li th:each="game : ${games}">
|
||||||
|
<a th:href="@{/game/{gid}/map(gid=${game.id})}" th:text="${game.name}"></a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<div th:replace="~{html-head}"/>
|
||||||
|
<link rel="stylesheet" th:href="@{/css/grid.css}"/>
|
||||||
|
<script th:src="@{/js/jquery-4.0.0.min.js}" type="text/javascript"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js" integrity="sha512-1QvjE7BtotQjkq8PxLeF6P46gEpBRXuskzIVgjFpekzFVF4yjRgrQvTG1MTOJ3yQgvTteKAcO7DSZI92+u/yZw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js" integrity="sha512-iKDtgDyTHjAitUDdLljGhenhPwrbBfqTKWO1mkhSFH3A7blITC9MhYon6SjnMhp4o0rADGw9yAC6EW4t5a4K3g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script th:src="@{/js/grid.js}" type="text/javascript"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<main>
|
||||||
|
<section>
|
||||||
|
<h2>Worlds</h2><br/>
|
||||||
|
<ul>
|
||||||
|
<li th:each="world : ${worlds}">
|
||||||
|
<!-- TODO remove this duplication of World span -->
|
||||||
|
<span th:if="${world.locked}"> World <span th:text="${world.name}"/> </span>
|
||||||
|
<a th:if="!${world.locked}" th:href="@{/game/{gid}/grid/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">World <span th:text="${world.name}"/></a>
|
||||||
|
<a th:if="!${world.locked}" th:href="@{/game/{gid}/reset/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">reset</a>
|
||||||
|
<span th:if="${world.won}">Gagné !</span>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
<section>
|
||||||
|
<h2>Received Items</h2><br/>
|
||||||
|
<ul>
|
||||||
|
<li th:each="item : ${items}">
|
||||||
|
<span th:text="${item}"/>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="fr" dir="ltr">
|
||||||
|
<head>
|
||||||
|
<div th:replace="~{html-head}"/>
|
||||||
|
<link rel="stylesheet" th:href="@{/css/grid.css}"/>
|
||||||
|
<script th:src="@{/js/jquery-4.0.0.min.js}" type="text/javascript"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/sockjs-client/1.6.1/sockjs.min.js" integrity="sha512-1QvjE7BtotQjkq8PxLeF6P46gEpBRXuskzIVgjFpekzFVF4yjRgrQvTG1MTOJ3yQgvTteKAcO7DSZI92+u/yZw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/stomp.js/2.3.3/stomp.min.js" integrity="sha512-iKDtgDyTHjAitUDdLljGhenhPwrbBfqTKWO1mkhSFH3A7blITC9MhYon6SjnMhp4o0rADGw9yAC6EW4t5a4K3g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
||||||
|
<script th:src="@{/js/grid.js}" type="text/javascript"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div>
|
||||||
|
Which player are you ? <br/>
|
||||||
|
<a th:href="@{{redirect}?player=A(redirect=${redirect})}">Player A</a>
|
||||||
|
<a th:href="@{{redirect}?player=B(redirect=${redirect})}">Player B</a>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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,7 +2,10 @@ package com.bernard.nodecames;
|
|||||||
|
|
||||||
import org.springframework.boot.test.context.SpringBootTest;
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
|
||||||
@SpringBootTest
|
@SpringBootTest
|
||||||
|
@RequiredArgsConstructor
|
||||||
class NodeCamesApplicationTests {
|
class NodeCamesApplicationTests {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user