Observer pattern & Archipelago

This commit is contained in:
Mysaa Java
2026-09-29 21:32:22 +02:00
parent 0bb880d434
commit 466da86969
22 changed files with 695 additions and 118 deletions
+4
View File
@@ -0,0 +1,4 @@
## License
Dictionaries taken from [this project](https://github.com/LandazuriPaul/Codenames)
+1
View File
@@ -31,6 +31,7 @@ dependencies {
implementation 'org.flywaydb:flyway-core:_' implementation 'org.flywaydb:flyway-core:_'
implementation 'org.flywaydb:flyway-database-postgresql:_' implementation 'org.flywaydb:flyway-database-postgresql:_'
implementation 'jakarta.validation:jakarta.validation-api:_' implementation 'jakarta.validation:jakarta.validation-api:_'
implementation 'io.github.archipelagomw:Java-Client:_'
//TODO use webjars //TODO use webjars
runtimeOnly 'org.webjars:jquery:_' runtimeOnly 'org.webjars:jquery:_'
+3
View File
@@ -39,6 +39,8 @@
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 &
@@ -61,6 +63,7 @@
packages = [ packages = [
jdk jdk
gradle gradle
postgres
(pkgs.vscode-with-extensions.override { (pkgs.vscode-with-extensions.override {
vscode = pkgs.vscodium; vscode = pkgs.vscodium;
vscodeExtensions = with pkgs.vscode-extensions; [ vscodeExtensions = with pkgs.vscode-extensions; [
+40 -8
View File
@@ -103,17 +103,30 @@
<sha256 value="19889dbdf1b254b2601a5ee645b8147a974644882297684c798afe5d63d78dfe" origin="Generated by Gradle"/> <sha256 value="19889dbdf1b254b2601a5ee645b8147a974644882297684c798afe5d63d78dfe" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.38.0"> <component group="com.google.code.gson" name="gson" version="2.13.2">
<artifact name="error_prone_annotations-2.38.0.jar"> <artifact name="gson-2.13.2.jar">
<sha256 value="6661d5335090a5fc61dd869d2095bc6c1e2156e3aa47a6e4ababdf64c99a7889" origin="Generated by Gradle"/> <sha256 value="dd0ce1b55a3ed2080cb70f9c655850cda86c206862310009dcb5e5c95265a5e0" origin="Generated by Gradle"/>
</artifact> </artifact>
<artifact name="error_prone_annotations-2.38.0.pom"> <artifact name="gson-2.13.2.pom">
<sha256 value="3007bef8aff3ae8ea12d81c3feacb4f15979b2cf5c3e38fc9189a98dea141167" origin="Generated by Gradle"/> <sha256 value="3aa06aa7c0f9af092961a42d09578e4324be146348a0ee6ed47857f7c2677b76" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.38.0"> <component group="com.google.code.gson" name="gson-parent" version="2.13.2">
<artifact name="error_prone_parent-2.38.0.pom"> <artifact name="gson-parent-2.13.2.pom">
<sha256 value="e62458a6a3e63081bc7c57b3c0fac9f04f76ce32f606532db69fe2b3d47b934c" origin="Generated by Gradle"/> <sha256 value="83ab528a9d50fd76aeb8ad6f727b3ee9cb766586255774ed16ca8c4c76d9dacd" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_annotations" version="2.41.0">
<artifact name="error_prone_annotations-2.41.0.jar">
<sha256 value="a56e782b5b50811ac204073a355a21d915a2107fce13ec711331ad036f660fcc" origin="Generated by Gradle"/>
</artifact>
<artifact name="error_prone_annotations-2.41.0.pom">
<sha256 value="a151df1e2e0b48618d8b06a180748a29b3abb39b1b2396f6a1c879a727488c6e" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="com.google.errorprone" name="error_prone_parent" version="2.41.0">
<artifact name="error_prone_parent-2.41.0.pom">
<sha256 value="c538388d760a5c1c98dcf06f6ed3cfe5f11a651827db5cbd2ed8288c795cad42" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="com.google.protobuf" name="protobuf-bom" version="4.35.1"> <component group="com.google.protobuf" name="protobuf-bom" version="4.35.1">
@@ -280,6 +293,17 @@
<sha256 value="7c490839bb11df9ae7c37f9c31994a4b423bd826245d34abe6c864f5e61d46e9" origin="Generated by Gradle"/> <sha256 value="7c490839bb11df9ae7c37f9c31994a4b423bd826245d34abe6c864f5e61d46e9" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="io.github.archipelagomw" name="Java-Client" version="0.2.1">
<artifact name="Java-Client-0.2.1.jar">
<sha256 value="9f3ff0a1a3cd99cbc9090e819c00d3890b775e17cbac47201677972953337a33" origin="Generated by Gradle"/>
</artifact>
<artifact name="Java-Client-0.2.1.module">
<sha256 value="c4b35dff4ed88b82e4a9133efdfbc6efb6a23bca3f9124e1b606a37b10240c4f" origin="Generated by Gradle"/>
</artifact>
<artifact name="Java-Client-0.2.1.pom">
<sha256 value="812e1984b148cb856afb290029c224de7dfb99a11a28dd9b6a402d2ed761dd06" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="io.grpc" name="grpc-bom" version="1.83.1"> <component group="io.grpc" name="grpc-bom" version="1.83.1">
<artifact name="grpc-bom-1.83.1.pom"> <artifact name="grpc-bom-1.83.1.pom">
<sha256 value="58cc1c0c0e6dc3aa28a6b8f6c8cc1cbdbdc47843cfba282d07f694c2b1af4f8b" origin="Generated by Gradle"/> <sha256 value="58cc1c0c0e6dc3aa28a6b8f6c8cc1cbdbdc47843cfba282d07f694c2b1af4f8b" origin="Generated by Gradle"/>
@@ -1044,6 +1068,14 @@
<sha256 value="aeabde04eca66e1ac30f9df3cc36c9b07a25a37c52043b33418e274ce5a23616" origin="Generated by Gradle"/> <sha256 value="aeabde04eca66e1ac30f9df3cc36c9b07a25a37c52043b33418e274ce5a23616" origin="Generated by Gradle"/>
</artifact> </artifact>
</component> </component>
<component group="org.java-websocket" name="Java-WebSocket" version="1.6.0">
<artifact name="Java-WebSocket-1.6.0.jar">
<sha256 value="eae29213e4f16515639c28957200f011b3967fffcada1962cf0255d24919c22f" origin="Generated by Gradle"/>
</artifact>
<artifact name="Java-WebSocket-1.6.0.pom">
<sha256 value="a8e84d553c34d793e2ea7e4a54b073f44ddb0bb442f4dd6fad0cecd755d1ceac" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.jboss" name="jboss-parent" version="52"> <component group="org.jboss" name="jboss-parent" version="52">
<artifact name="jboss-parent-52.pom"> <artifact name="jboss-parent-52.pom">
<sha256 value="0615b429e3a21a0d6bd925d09ede2937892034272b4cd04bb9ab617d57e72b42" origin="Generated by Gradle"/> <sha256 value="0615b429e3a21a0d6bd925d09ede2937892034272b4cd04bb9ab617d57e72b42" origin="Generated by Gradle"/>
+12 -3
View File
@@ -19,7 +19,9 @@ ITEM_NAME_TO_ID = {
"structured_grid" : 9, "structured_grid" : 9,
"hints_in_a_row" : 10, "hints_in_a_row" : 10,
"anyone_starts" : 11, "anyone_starts" : 11,
"filler": 12 "progressive_hint_word_count": 12,
"progressive_hint_word_length": 13,
"filler": 14
} }
for w in range(1,12+1): for w in range(1,12+1):
ITEM_NAME_TO_ID[f"unlock_world_{w}"] = 100+w ITEM_NAME_TO_ID[f"unlock_world_{w}"] = 100+w
@@ -35,14 +37,21 @@ def get_random_filler_item_name(world: NodecamesWorld) -> str:
def create_item_with_correct_classification( def create_item_with_correct_classification(
world: NodecamesWorld, name: str world: NodecamesWorld, name: str
) -> NodecamesItem: ) -> NodecamesItem:
return NodecamesItem(name, ItemClassification.progression, ITEM_NAME_TO_ID[name], world.player) classification = ItemClassification.progression
if name == "filler":
classification = ItemClassification.filler
return NodecamesItem(name, classification, ITEM_NAME_TO_ID[name], world.player)
def create_all_items(world: NodecamesWorld) -> None: def create_all_items(world: NodecamesWorld) -> None:
itempool: list[Item] = [] itempool: list[Item] = []
hintItemCount = world.options.max_hint_count - world.options.max_hint_length hintItemCount = world.options.max_hint_count - world.options.start_hint_count
itempool += [world.create_item("progressive_hint_count") for _ in range(hintItemCount)] itempool += [world.create_item("progressive_hint_count") for _ in range(hintItemCount)]
hintWordLengthItemCount = world.options.max_hint_length - world.options.start_hint_length
itempool += [world.create_item("progressive_hint_word_length") for _ in range(hintWordLengthItemCount)]
hintWordCountItemCount = world.options.max_hint_word_count - world.options.start_hint_word_count
itempool += [world.create_item("progressive_hint_word_count") for _ in range(hintWordCountItemCount)]
itempool += [ itempool += [
world.create_item("zero_hint"), world.create_item("zero_hint"),
world.create_item("infinity_hint"), world.create_item("infinity_hint"),
+27
View File
@@ -58,6 +58,29 @@ class MaxHintLength(Range):
range_end = 30 range_end = 30
default = 10 default = 10
class StartHintWordCount(Range):
"""
Hint Word count available at the beginning of the game
"""
display_name = "Start Hint Word Count"
range_start = 1
range_end = 16
default = 1
class MaxHintWordCount(Range):
"""
Maximal hint word count we can get.
"""
display_name = "Max Hint Word Count"
range_start = 1
range_end = 16
default = 16
class OnOffDefault(Choice): class OnOffDefault(Choice):
option_Off = 0 option_Off = 0
@@ -171,6 +194,8 @@ class NodecamesOptions(PerGameCommonOptions):
max_hint_count: MaxHintCount max_hint_count: MaxHintCount
start_hint_length: StartHintLength start_hint_length: StartHintLength
max_hint_length: MaxHintLength max_hint_length: MaxHintLength
start_hint_word_count: StartHintWordCount
max_hint_word_count: MaxHintWordCount
zero_hint: ZeroHint zero_hint: ZeroHint
infinity_hint: InfinityHint infinity_hint: InfinityHint
one_more_guess: OneMoreGuess one_more_guess: OneMoreGuess
@@ -218,6 +243,8 @@ option_presets = {
"max_hint_count": 10, "max_hint_count": 10,
"start_hint_length": 3, "start_hint_length": 3,
"max_hint_length": 12, "max_hint_length": 12,
"start_hint_word_count": 1,
"max_hint_word_count": 16,
"zero_hint": 1, "zero_hint": 1,
"infinity_hint": 1, "infinity_hint": 1,
"one_more_guess": 1, "one_more_guess": 1,
+5 -1
View File
@@ -12,7 +12,7 @@ class NodecamesWorld(World):
Nodecames is a word game, inspired by Code Names by Vlaada Chvátil Nodecames is a word game, inspired by Code Names by Vlaada Chvátil
""" """
game = "Nodecames" game = "nodecames"
options_dataclass = nodecames_options.NodecamesOptions options_dataclass = nodecames_options.NodecamesOptions
options: nodecames_options.NodecamesOptions options: nodecames_options.NodecamesOptions
@@ -40,6 +40,10 @@ class NodecamesWorld(World):
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
return items.get_random_filler_item_name(self) return items.get_random_filler_item_name(self)
def generate_output(self, outdir : str) -> None:
# Generates the output file
pass
# Data sent to the client # Data sent to the client
def fill_slot_data(self) -> Mapping[str, Any]: def fill_slot_data(self) -> Mapping[str, Any]:
return self.options.as_dict( return self.options.as_dict(
@@ -1,11 +1,16 @@
package com.bernard.nodecames; package com.bernard.nodecames;
import java.net.URISyntaxException;
import java.util.Comparator; import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Map.Entry; import java.util.Map.Entry;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -21,6 +26,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.view.RedirectView; import org.springframework.web.servlet.view.RedirectView;
import com.bernard.nodecames.frontend.GridConfigEdit; import com.bernard.nodecames.frontend.GridConfigEdit;
import com.bernard.nodecames.game.ArchipelagoManager;
import com.bernard.nodecames.game.GameManager; import com.bernard.nodecames.game.GameManager;
import com.bernard.nodecames.game.GridManager; import com.bernard.nodecames.game.GridManager;
import com.bernard.nodecames.game.IllegalGameActionException; import com.bernard.nodecames.game.IllegalGameActionException;
@@ -33,6 +39,7 @@ import com.bernard.nodecames.repository.GameRepository;
import com.bernard.nodecames.repository.GridRepository; import com.bernard.nodecames.repository.GridRepository;
import com.bernard.nodecames.repository.WorldRepository; import com.bernard.nodecames.repository.WorldRepository;
import io.github.archipelagomw.network.ConnectionResult;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -41,6 +48,7 @@ public class HttpController {
private final GridManager gm; private final GridManager gm;
private final GameManager wm; private final GameManager wm;
private final ArchipelagoManager am;
private final GridRepository gridRepository; private final GridRepository gridRepository;
private final GameRepository gRepository; private final GameRepository gRepository;
private final WorldRepository wRepository; private final WorldRepository wRepository;
@@ -78,6 +86,7 @@ public class HttpController {
model.addAttribute("gridConfig", GridConfigEdit.of(gc)); model.addAttribute("gridConfig", GridConfigEdit.of(gc));
return "grid-config"; return "grid-config";
} }
@PostMapping("/room/{id}/config") @PostMapping("/room/{id}/config")
public Object setGridConfig(@PathVariable("id") String id, public Object setGridConfig(@PathVariable("id") String id,
@ModelAttribute("gridConfig") GridConfigEdit gce, @ModelAttribute("gridConfig") GridConfigEdit gce,
@@ -138,6 +147,38 @@ public class HttpController {
return "map"; return "map";
} }
@GetMapping("/game/{id}/archipelago")
public Object gridConfig(@PathVariable("id") String id, @RequestParam("address") String address,
@RequestParam("slot") String slot, Model model) {
Optional<Game> g = gRepository.findById(UUID.fromString(id));
if(!g.isPresent())
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
try {
CompletableFuture<ConnectionResult> connectionResultFuture = am.openClient(g.get(), address, slot);
ConnectionResult connectionResult = connectionResultFuture.get(5, TimeUnit.SECONDS);
if (connectionResult != ConnectionResult.Success) {
return new ResponseEntity<>("Could not connect to archipelago server: "+connectionResult.name(), HttpStatus.NOT_FOUND);
} else {
return new ResponseEntity<>("Connection successful", HttpStatus.OK);
}
} catch (TimeoutException e) {
return new ResponseEntity<>("Timed out connecting to archipelago", HttpStatus.REQUEST_TIMEOUT);
} catch (URISyntaxException e) {
return new ResponseEntity<>("URI malformed : "+e.getMessage(), HttpStatus.BAD_REQUEST);
} catch (InterruptedException e) {
e.printStackTrace();
Thread.currentThread().interrupt();
return new ResponseEntity<>("Interrupted", HttpStatus.INTERNAL_SERVER_ERROR);
} catch (ExecutionException e) {
e.printStackTrace();
return new ResponseEntity<>("ExecutionException while connecting to Archipelago : "+e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@GetMapping("/game/{id}/grid/{world}") @GetMapping("/game/{id}/grid/{world}")
public Object gridRedirect(@PathVariable("id") String id, @PathVariable("world") String world, @RequestParam("player") String player) { public Object gridRedirect(@PathVariable("id") String id, @PathVariable("world") String world, @RequestParam("player") String player) {
Optional<Game> g = gRepository.findById(UUID.fromString(id)); Optional<Game> g = gRepository.findById(UUID.fromString(id));
@@ -148,7 +189,12 @@ public class HttpController {
if(!w.isPresent()) { if(!w.isPresent()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND); return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} }
Grid grid = wm.getGrid(g.get(), w.get()); Grid grid;
try {
grid = wm.getGrid(g.get(), w.get());
} catch (IllegalGameActionException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
return new RedirectView("/room/"+grid.getId()+"/grid" + "?player="+player); return new RedirectView("/room/"+grid.getId()+"/grid" + "?player="+player);
} }
@@ -162,7 +208,11 @@ public class HttpController {
if(!w.isPresent()) { if(!w.isPresent()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND); return new ResponseEntity<>(HttpStatus.NOT_FOUND);
} }
try {
Grid newGrid = wm.resetGrid(g.get(), w.get()); Grid newGrid = wm.resetGrid(g.get(), w.get());
return new RedirectView("/room/"+newGrid.getId()+"/grid" + "?player="+player); return new RedirectView("/room/"+newGrid.getId()+"/grid" + "?player="+player);
} catch (IllegalGameActionException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
}
} }
} }
@@ -1,7 +1,9 @@
package com.bernard.nodecames.frontend; package com.bernard.nodecames.frontend;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
@@ -17,14 +19,20 @@ import org.springframework.web.bind.annotation.PathVariable;
import com.bernard.nodecames.game.GridManager; import com.bernard.nodecames.game.GridManager;
import com.bernard.nodecames.game.IllegalGameActionException; import com.bernard.nodecames.game.IllegalGameActionException;
import com.bernard.nodecames.model.Card; import com.bernard.nodecames.model.Card;
import com.bernard.nodecames.model.Card.Color;
import com.bernard.nodecames.model.GameEvent; import com.bernard.nodecames.model.GameEvent;
import com.bernard.nodecames.model.Grid; import com.bernard.nodecames.model.Grid;
import com.bernard.nodecames.model.Grid.Phase;
import com.bernard.nodecames.model.GridConfig.MutableOption; import com.bernard.nodecames.model.GridConfig.MutableOption;
import com.bernard.nodecames.model.Hint;
import com.bernard.nodecames.observer.GridObserver;
import com.bernard.nodecames.repository.GridRepository; import com.bernard.nodecames.repository.GridRepository;
import lombok.AllArgsConstructor;
import tools.jackson.databind.JsonNode; import tools.jackson.databind.JsonNode;
@Controller @Controller
@AllArgsConstructor
public class WebSocketController { public class WebSocketController {
private final GridManager gm; private final GridManager gm;
@@ -32,49 +40,19 @@ public class WebSocketController {
private final JsonDataService json; private final JsonDataService json;
private final GridRepository gRepository; private final GridRepository gRepository;
public WebSocketController(GridManager gm, SimpMessagingTemplate template, JsonDataService json, GridRepository gRepository) { Set<UUID> observed = new HashSet<>();
this.gm = gm; private void registerObserver(Grid g) {
this.template = template; if(!observed.contains(g.getId())) {
this.json = json; observed.add(g.getId());
this.gRepository = gRepository; gm.addObserver(g, new GObserver(g));
gm.setWsc(this);
} }
public void onNewEvent(Grid g, GameEvent ge) {
this.template.convertAndSend("/topic/new-event",
json.eventData(ge)
);
}
public void onNewPhase(Grid g) {
this.template.convertAndSend(
"/topic/new-phase",
json.phaseData(g.getPhase())
);
}
public void onConfigChange(Grid g, MutableOption cc, Object data) {
this.template.convertAndSend("/topic/config-change",
json.gridConfigUpdateData(cc, data)
);
}
public void publishCardUpdateToPlayer(Grid g, Card c, char player) {
this.template.convertAndSend(
"/topic/update-card/" + Character.toLowerCase(player),
json.cardData(c, player),
Map.of("cardIndex", c.getIndex())
);
}
public void publishCardUpdate(Grid g, Card card) {
publishCardUpdateToPlayer(g, card, 'A');
publishCardUpdateToPlayer(g, card, 'B');
} }
public void publishError(char issuer, IllegalGameActionException e) { public void publishError(char issuer, IllegalGameActionException e) {
//TODO open error channel //TODO open error channel
} }
//TODO We should do this with websocket protocol
@GetMapping("/room/{id}/game/{player}") @GetMapping("/room/{id}/game/{player}")
public Object grid(@PathVariable("id") String gridId, @PathVariable("player") String playerStr) { public Object grid(@PathVariable("id") String gridId, @PathVariable("player") String playerStr) {
char player; char player;
@@ -86,14 +64,19 @@ public class WebSocketController {
throw new IllegalArgumentException("Unknown player type"); throw new IllegalArgumentException("Unknown player type");
Optional<Grid> g = gRepository.findById(UUID.fromString(gridId)); Optional<Grid> g = gRepository.findById(UUID.fromString(gridId));
if(g.isPresent()) if(g.isPresent()){
return new ResponseEntity<>(json.gridData(g.get(), player), HttpStatus.OK); Grid grid = g.get();
else registerObserver(grid);
return new ResponseEntity<>(json.gridData(grid, player), HttpStatus.OK);
} else
return new ResponseEntity<>("Could not find grid with given ID",HttpStatus.NOT_FOUND); return new ResponseEntity<>("Could not find grid with given ID",HttpStatus.NOT_FOUND);
} }
@MessageMapping("submit-hint") //TODO handle websockets per grid
public void submitHint(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) { @MessageMapping("/submit-hint")
public void submitHint(@Header("room") String roomId,
@Header("player") String player,
@Payload JsonNode content) {
String hint = content.asObject().get("hint").stringValue(); String hint = content.asObject().get("hint").stringValue();
int wordCount = content.asObject().get("hintWordCount").intValue(); int wordCount = content.asObject().get("hintWordCount").intValue();
@@ -101,7 +84,6 @@ public class WebSocketController {
if(!g.isPresent()) if(!g.isPresent())
// 404 // 404
return; return;
try { try {
gm.proposeHint(g.get(), player.charAt(0), hint, wordCount); gm.proposeHint(g.get(), player.charAt(0), hint, wordCount);
} catch (IllegalGameActionException e) { } catch (IllegalGameActionException e) {
@@ -118,10 +100,8 @@ public class WebSocketController {
if(!g.isPresent()) if(!g.isPresent())
// 404 // 404
return; return;
try { try {
gm.pointCard(g.get(), player.charAt(0), cardIndex); gm.pointCard(g.get(), player.charAt(0), cardIndex);
publishCardUpdate(g.get(), gm.getCard(g.get(), cardIndex));
} catch (IllegalGameActionException e) { } catch (IllegalGameActionException e) {
publishError(player.charAt(0), e); publishError(player.charAt(0), e);
e.printStackTrace(); e.printStackTrace();
@@ -134,7 +114,6 @@ public class WebSocketController {
if(!g.isPresent()) if(!g.isPresent())
// 404 // 404
return; return;
try { try {
gm.endGuessing(g.get(), player.charAt(0)); gm.endGuessing(g.get(), player.charAt(0));
} catch (IllegalGameActionException e) { } catch (IllegalGameActionException e) {
@@ -156,4 +135,56 @@ public class WebSocketController {
e.printStackTrace(); e.printStackTrace();
} }
} }
@AllArgsConstructor
private class GObserver implements GridObserver {
Grid grid;
public void publishCardUpdateToPlayer(Card c, char player) {
template.convertAndSend(
"/topic/%s/update-card/%s".formatted(grid.getId(), Character.toLowerCase(player)),
json.cardData(c, player),
Map.of("cardIndex", c.getIndex())
);
}
public void publishCardUpdate(Card card) {
publishCardUpdateToPlayer(card, 'A');
publishCardUpdateToPlayer(card, 'B');
}
@Override
public void onPointCard(char player, Card card, Color color) {
publishCardUpdate(card);
}
@Override
public void onSubmitHint(char player, Hint hint) {}
@Override
public void onGridEnd(boolean won) {}
@Override
public void onGameEvent(GameEvent ge) {
template.convertAndSend(
"/topic/%s/new-event".formatted(grid.getId()),
json.eventData(ge)
);
}
@Override
public void onNewPhase(Phase p) {
template.convertAndSend(
"/topic/%s/new-phase".formatted(grid.getId()),
json.phaseData(p)
);
}
@Override
public void onConfigChange(MutableOption cc, Object data) {
template.convertAndSend(
"/topic/%s/config-change".formatted(grid.getId()),
json.gridConfigUpdateData(cc, data)
);
}
}
} }
@@ -0,0 +1,141 @@
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 com.bernard.nodecames.model.GridConfig.MutableOption;
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) throws IllegalGameActionException {
log.debug("Received item : {}", e.getItemName());
switch(e.getItemName()) {
case "progressive_hint_count":
gm.unlockMoreHintCount(this.game);
break;
case "progressive_hint_word_length":
gm.unlockMoreHintWordLengthCount(this.game);
break;
case "progressive_hint_word_count":
gm.unlockMoreHintWordCount(this.game);
break;
case "zero_hint":
gm.unlockHintWordCount(this.game, 0);
break;
case "infinity_hint":
gm.unlockHintWordCount(this.game, -1);
break;
case "one_more_guess":
gm.unlockMutable(game, MutableOption.ONE_MORE_GUESS, true);
break;
case "end_guessing":
gm.unlockMutable(game, MutableOption.END_GUESSING, true);
break;
case "only_one_green_green":
gm.unlockMutable(game, MutableOption.ONLY_ONE_GREEN_GREEN, true);
break;
case "sudden_death":
gm.unlockMutable(game, MutableOption.SUDDEN_DEATH, true);
break;
case "structured_grid":
gm.unlockStructuredGrid(game);
break;
case "hints_in_a_row":
gm.unlockMutable(game, MutableOption.HINTS_IN_A_ROW, true);
break;
case "anyone_starts":
gm.unlockAnyoneCanStart(game);
break;
case "filler":
// We do nothing if we receive a filler item
break;
default:
if (e.getItemName().startsWith("unlock_world_")) {
int worldIndex = Integer.parseInt(e.getItemName().substring("unlock_world_".length()))-1;
String worldName = "%d-%d".formatted((worldIndex/3)+1, (worldIndex%3)+1);
gm.unlockWorld(game, worldName);
} else {
throw new IllegalArgumentException("Received unknown item : "+e.getItemName());
}
}
}
@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) {}
}
}
@@ -2,38 +2,75 @@ package com.bernard.nodecames.game;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Map; import java.util.Map;
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.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.bernard.nodecames.model.Card;
import com.bernard.nodecames.model.Card.Color;
import com.bernard.nodecames.model.Game; import com.bernard.nodecames.model.Game;
import com.bernard.nodecames.model.GameEvent;
import com.bernard.nodecames.model.Grid; import com.bernard.nodecames.model.Grid;
import com.bernard.nodecames.model.Grid.Phase;
import com.bernard.nodecames.model.GridConfig; import com.bernard.nodecames.model.GridConfig;
import com.bernard.nodecames.model.GridConfig.MutableOption;
import com.bernard.nodecames.model.Hint;
import com.bernard.nodecames.model.World; import com.bernard.nodecames.model.World;
import com.bernard.nodecames.model.World.WorldId;
import com.bernard.nodecames.observer.GridObserver;
import com.bernard.nodecames.observer.WorldObserver;
import com.bernard.nodecames.repository.GameRepository; import com.bernard.nodecames.repository.GameRepository;
import com.bernard.nodecames.repository.WorldRepository; import com.bernard.nodecames.repository.WorldRepository;
import jakarta.annotation.PostConstruct;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
@Service @Service
@AllArgsConstructor
public class GameManager { public class GameManager {
// Reverse map allowing to pass grid events to the game
Map<UUID, World> currentGrids = new HashMap<>();
private final GridManager gm; private final GridManager gm;
private final GameRepository gRepository; private final GameRepository gRepository;
private final WorldRepository wRepository; private final WorldRepository wRepository;
public GameManager(GridManager gm, GameRepository gRepository, WorldRepository wRepository) { //TODO try putting "World" here instead of WorldId
this.gm = gm; private Map<WorldId, Set<WorldObserver>> observers = new HashMap<>();
this.gRepository = gRepository;
this.wRepository = wRepository; public void addObserver(World w, WorldObserver observer) {
gm.setWm(this); 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 forEachObserver(World w, Consumer<WorldObserver> action) {
observers.getOrDefault(w.getId(), Set.of()).forEach(action);
}
/*
* In case of server restart, registers to GridManager observers
*/
@PostConstruct
private void registerRunningObservers() {
for(World w : wRepository.findAll()) {
if(w.getCurrentGrid() != null && w.getCurrentGrid().getWon() != null) {
registerObserver(w, w.getCurrentGrid());
}
}
}
private void registerObserver(World w, Grid g) {
gm.addObserver(g, new GObserver(g, w));
forEachObserver(w, o -> gm.addObserver(g, o));
} }
public Game createGame(String name) { public Game createGame(String name) {
@@ -42,10 +79,9 @@ public class GameManager {
Game g = new Game(uuid, name); Game g = new Game(uuid, name);
gRepository.save(g); gRepository.save(g);
for (Entry<String, GridConfig> wConfig : Game.DEFAULT_WORLDS_CONFIG.entrySet()) { for (Entry<String, GridConfig> wConfig : Game.DEFAULT_WORLDS_CONFIG.entrySet()) {
World w = new World(g, wConfig.getKey(), wConfig.getValue(), null, false); World w = new World(g, wConfig.getKey(), wConfig.getValue(), null, false, true);
wRepository.save(w); wRepository.save(w);
} }
return g; return g;
} }
@@ -57,22 +93,26 @@ public class GameManager {
return wRepository.findByGame(g); return wRepository.findByGame(g);
} }
private Grid newGrid(Game g, World w) { private Grid newGrid(Game g, World w) throws IllegalGameActionException {
if(w.isLocked())
//TODO separate IllegaleGameAction and IllegalGridAction
throw new IllegalGameActionException(null, "Cannot create a grid in a locked world");
GridConfig gc = w.getInitialConfig(); GridConfig gc = w.getInitialConfig();
//TODO seed random
Grid grid = gm.newGrid(w, gc, new Random()); Grid grid = gm.newGrid(w, gc, new Random());
w.setCurrentGrid(grid); w.setCurrentGrid(grid);
wRepository.save(w); wRepository.save(w);
currentGrids.put(grid.getId(), w); registerObserver(w, grid);
return grid; return grid;
} }
public Grid resetGrid(Game g, World world) { public Grid resetGrid(Game g, World world) throws IllegalGameActionException{
return newGrid(g, world); return newGrid(g, world);
} }
public Grid getGrid(Game g, World w) { public Grid getGrid(Game g, World w) throws IllegalGameActionException{
if(w.getCurrentGrid() != null) { if(w.getCurrentGrid() != null) {
return w.getCurrentGrid(); return w.getCurrentGrid();
} else { } else {
@@ -80,11 +120,100 @@ public class GameManager {
} }
} }
public void onGameWon(Grid g) { /**
World w = currentGrids.get(g.getId()); * UPDATE GAME CONFIG
if(w != null) { */
w.setWon(true); public void unlockMutable(Game g, MutableOption opt, Object o) throws IllegalGameActionException {
Set<World> worlds = wRepository.findByGame(g);
for(World w : worlds) {
GridConfig initialConfig = w.getInitialConfig();
initialConfig.set(opt, o);
w.setInitialConfig(initialConfig);
wRepository.save(w);
Grid grid = w.getCurrentGrid();
if(grid != null) {
gm.unlock(grid, opt, o);
}
}
}
public void unlockStructuredGrid(Game g) {
Set<World> worlds = wRepository.findByGame(g);
for(World w : worlds) {
GridConfig initialConfig = w.getInitialConfig();
initialConfig.setStructuredGrid(true);
w.setInitialConfig(initialConfig);
wRepository.save(w); wRepository.save(w);
} }
} }
public void unlockAnyoneCanStart(Game g) {
Set<World> worlds = wRepository.findByGame(g);
for(World w : worlds) {
GridConfig initialConfig = w.getInitialConfig();
initialConfig.setAnyoneStarts(true);
w.setInitialConfig(initialConfig);
wRepository.save(w);
}
}
public void unlockWorld(Game g, String name) throws IllegalGameActionException {
Optional<World> ow = wRepository.findByGameAndId_Name(g, name);
if(!ow.isPresent())
throw new IllegalGameActionException(null, "Could not find world with name %s".formatted(name));
World w = ow.get();
w.setLocked(false);
wRepository.save(w);
}
public void unlockMoreHintCount(Game game) throws IllegalGameActionException {
//TODO Store current hint count elsewhere
int currentHintCount = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getMaxHint();
unlockMutable(game, MutableOption.MAX_HINT, currentHintCount+1);
}
public void unlockMoreHintWordLengthCount(Game game) throws IllegalGameActionException {
//TODO Store current hint word length elsewhere
int currentHintWordLength = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getHintMaxLength();
unlockMutable(game, MutableOption.HINT_MAX_LENGTH, currentHintWordLength+1);
}
public void unlockMoreHintWordCount(Game game) throws IllegalGameActionException {
//TODO Store current hint word count elsewhere
int currentMaxWordCount = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getAvailableHintWordCount().stream().mapToInt(i -> i).max().getAsInt();
unlockHintWordCount(game, currentMaxWordCount+1);
}
public void unlockHintWordCount(Game game, int i) throws IllegalGameActionException {
Set<Integer> currentWordCounts = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getAvailableHintWordCount();
if(currentWordCounts.contains(i))
throw new IllegalGameActionException(null, "Word count already unlocked : %d".formatted(i));
Set<Integer> newWordCounts = new HashSet<>(currentWordCounts);
newWordCounts.add(i);
unlockMutable(game, MutableOption.AVAILABLE_HINT_WORD_COUNT, newWordCounts);
}
@AllArgsConstructor
private class GObserver implements GridObserver {
private Grid grid;
private World world;
@Override
public void onGridEnd(boolean win) {
if(win) {
world.setWon(true);
wRepository.save(world);
}
}
@Override
public void onPointCard(char player, Card card, Color color) {}
@Override
public void onSubmitHint(char player, Hint hint) {}
@Override
public void onGameEvent(GameEvent ge) {}
@Override
public void onNewPhase(Phase p) {}
@Override
public void onConfigChange(MutableOption cc, Object data) {}
}
} }
@@ -2,11 +2,15 @@ package com.bernard.nodecames.game;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.CopyOnWriteArraySet;
import java.util.function.Consumer;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import java.util.stream.IntStream; import java.util.stream.IntStream;
@@ -14,10 +18,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import com.bernard.nodecames.frontend.WebSocketController;
import com.bernard.nodecames.model.Card; import com.bernard.nodecames.model.Card;
import com.bernard.nodecames.model.GameEvent;
import com.bernard.nodecames.model.Card.Color; import com.bernard.nodecames.model.Card.Color;
import com.bernard.nodecames.model.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;
@@ -25,21 +28,18 @@ import com.bernard.nodecames.model.GridConfig.CardColors;
import com.bernard.nodecames.model.GridConfig.MutableOption; import com.bernard.nodecames.model.GridConfig.MutableOption;
import com.bernard.nodecames.model.Hint; import com.bernard.nodecames.model.Hint;
import com.bernard.nodecames.model.World; import com.bernard.nodecames.model.World;
import com.bernard.nodecames.observer.GridObserver;
import com.bernard.nodecames.repository.CardRepository; import com.bernard.nodecames.repository.CardRepository;
import com.bernard.nodecames.repository.GridRepository; import com.bernard.nodecames.repository.GridRepository;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class GridManager { public class GridManager {
@Setter private Map<UUID, Set<GridObserver>> observers = new HashMap<>();
private WebSocketController wsc = null;
@Setter
private GameManager wm = null;
private final GridRepository gRepository; private final GridRepository gRepository;
private final CardRepository cRepository; private final CardRepository cRepository;
@@ -48,11 +48,22 @@ public class GridManager {
private static final Logger log = LoggerFactory.getLogger(GridManager.class); private static final Logger log = LoggerFactory.getLogger(GridManager.class);
public void addObserver(Grid g, GridObserver observer) {
if(!observers.containsKey(g.getId()))
observers.put(g.getId(), new CopyOnWriteArraySet<>());
observers.get(g.getId()).add(observer);
}
private void forEachObserver(Grid g, Consumer<GridObserver> action) {
log.debug("The current observers are : {}", observers.get(g.getId()));
observers.getOrDefault(g.getId(), Set.of()).forEach(action);
}
private void newGameEvent(Grid g, GameEvent ge) { private void newGameEvent(Grid g, GameEvent ge) {
log.debug("New game event : {}", ge); log.debug("New game event : {}", ge);
g.newGameEvent(ge); g.newGameEvent(ge);
gRepository.save(g); gRepository.save(g);
wsc.onNewEvent(g, ge); forEachObserver(g, o -> o.onGameEvent(ge));
} }
private void setPhase(Grid g, Phase phase) { private void setPhase(Grid g, Phase phase) {
@@ -62,33 +73,40 @@ public class GridManager {
g.setCurrentHint(null); g.setCurrentHint(null);
} }
gRepository.save(g); gRepository.save(g);
wsc.onNewPhase(g); forEachObserver(g, o -> o.onNewPhase(phase));
} }
private void setWon(Grid g, boolean won) { private void setWon(Grid g, boolean won) {
log.debug("Game won: {}", won); log.debug("Game won: {}", won);
g.setWonOpt(Optional.of(won)); g.setWonOpt(Optional.of(won));
gRepository.save(g); gRepository.save(g);
forEachObserver(g, o -> o.onGridEnd(won));
// Grid will no longer be updated, therefore we stop observing it
observers.remove(g.getId());
} }
private void setCardPublic(Card c, char player) { private void setCardPointed(Grid g, Card c, char player, Color col) {
log.debug("Card set public, index {}", c.getIndex()); log.debug("Card set public, index {}", c.getIndex());
c.setColorPublic(player, true); c.setColorPublic(player, true);
cRepository.save(c); cRepository.save(c);
forEachObserver(c.getGrid(), o -> o.onPointCard(player, c, col));
g.incrementCurrentGuessCount();
gRepository.save(g);
} }
private void newHint(Grid g, Hint h) { private void newHint(Grid g, char player, Hint h) {
log.debug("New hint: {}", h); log.debug("New hint: {}", h);
g.incrementHintCount(); g.incrementHintCount();
g.setCurrentHint(h); g.setCurrentHint(h);
g.setCurrentGuessCount(0); g.setCurrentGuessCount(0);
gRepository.save(g); gRepository.save(g);
forEachObserver(g, o -> o.onSubmitHint(player, h));
} }
private void changeConfig(Grid g, MutableOption cc, Object data) { private void changeConfig(Grid g, MutableOption cc, Object data) {
log.debug("Changed config option: {} -> new value: {}", cc.name(), data); log.debug("Changed config option: {} -> new value: {}", cc.name(), data);
g.getConfig().set(cc, data); g.getConfig().set(cc, data);
gRepository.save(g); gRepository.save(g);
wsc.onConfigChange(g, cc, data); forEachObserver(g, o -> o.onConfigChange(cc, data));
} }
@@ -169,12 +187,10 @@ public class GridManager {
* GAME ACTIONS * * GAME ACTIONS *
**********************************/ **********************************/
private void endGame(Grid g, boolean win) { private void endGame(Grid g, boolean win) {
setWon(g, win);
setPhase(g, Phase.GAME_ENDED); setPhase(g, Phase.GAME_ENDED);
newGameEvent(g, GameEvent.newGameEndEvent(win)); newGameEvent(g, GameEvent.newGameEndEvent(win));
if(win) { // This must be the last step as it removes observers
wm.onGameWon(g); setWon(g, win);
}
} }
public void proposeHint(Grid g, char player, String hint, int wordCount) throws IllegalGameActionException { public void proposeHint(Grid g, char player, String hint, int wordCount) throws IllegalGameActionException {
@@ -189,7 +205,7 @@ public class GridManager {
} }
Hint h = new Hint(hint, wordCount); Hint h = new Hint(hint, wordCount);
newHint(g, h); newHint(g, player, h);
newGameEvent(g, GameEvent.newHintEvent(h, player)); newGameEvent(g, GameEvent.newHintEvent(h, player));
setPhase(g, Phase.guessing(other(player))); setPhase(g, Phase.guessing(other(player)));
} }
@@ -264,8 +280,8 @@ public class GridManager {
if(c.isColorPublic(other(player)) || (g.getConfig().isOnlyOneGreenGreen() && c.getColor(player) == Card.Color.GREEN && c.isColorPublic(player))) if(c.isColorPublic(other(player)) || (g.getConfig().isOnlyOneGreenGreen() && c.getColor(player) == Card.Color.GREEN && c.isColorPublic(player)))
throw new IllegalGameActionException(g, "Card has already been revealed"); throw new IllegalGameActionException(g, "Card has already been revealed");
setCardPublic(c, other(player));
Card.Color guessedColor = c.getColor(other(player)); Card.Color guessedColor = c.getColor(other(player));
setCardPointed(g, c, other(player), guessedColor);
newGameEvent(g, GameEvent.newGuessEvent(c.getIndex(), player, c.getColor(other(player)))); newGameEvent(g, GameEvent.newGuessEvent(c.getIndex(), player, c.getColor(other(player))));
g.incrementCurrentGuessCount(); g.incrementCurrentGuessCount();
@@ -369,7 +385,7 @@ public class GridManager {
} }
public void unlockHintCount(Grid g, int newHintCount) throws IllegalGameActionException { public void unlockHintCount(Grid g, int newHintCount) throws IllegalGameActionException {
if(newHintCount > g.getConfig().getMaxMaxHint()) { if(newHintCount > g.getConfig().getMaxMaxHint()) {
throw new IllegalGameActionException(g, "Illegal hint count"); throw new IllegalGameActionException(g, "Hint count higher than highest hint count");
} }
if(newHintCount <= g.getConfig().getMaxHint()) { if(newHintCount <= g.getConfig().getMaxHint()) {
throw new IllegalGameActionException(g, "Cannot lower hint count"); throw new IllegalGameActionException(g, "Cannot lower hint count");
@@ -3,6 +3,7 @@ package com.bernard.nodecames.model;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Optional; import java.util.Optional;
import java.util.Set;
import java.util.UUID; import java.util.UUID;
import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.annotations.JdbcTypeCode;
@@ -10,12 +11,15 @@ import org.hibernate.type.SqlTypes;
import jakarta.annotation.Nullable; import jakarta.annotation.Nullable;
import jakarta.persistence.AttributeConverter; import jakarta.persistence.AttributeConverter;
import jakarta.persistence.CascadeType;
import jakarta.persistence.Column; import jakarta.persistence.Column;
import jakarta.persistence.Convert; import jakarta.persistence.Convert;
import jakarta.persistence.Embedded; import jakarta.persistence.Embedded;
import jakarta.persistence.Entity; import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.Id; import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne; import jakarta.persistence.ManyToOne;
import jakarta.persistence.OneToMany;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
@@ -21,6 +21,7 @@ import jakarta.persistence.Embeddable;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Getter; import lombok.Getter;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import lombok.Setter;
import lombok.Value; import lombok.Value;
@Getter @Getter
@@ -50,27 +51,24 @@ public class GridConfig {
0,1,2,3,4,5,6,7,8,9,-1 0,1,2,3,4,5,6,7,8,9,-1
); );
public static final GridConfig BIG_CLASSICAL = new GridConfig( public static final GridConfig BIG_CLASSICAL = new GridConfig(
12, true, true, CUSTOM36, true, 12, CUSTOM36, true, true,
true, true, 12, BASE_HINTWORDCOUNT, true, true, true, true, 12, BASE_HINTWORDCOUNT, true,
Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, true, true,
true, false); true, false);
public static final GridConfig CODENAMES_CLASSICAL = new GridConfig( public static final GridConfig CODENAMES_CLASSICAL = new GridConfig(
9, true, true, CLASSICAL25, true, 9, CLASSICAL25, true, true, true,
true, true, 9, BASE_HINTWORDCOUNT, true, true, true, 9, BASE_HINTWORDCOUNT, true,
Integer.MAX_VALUE, true, true, Integer.MAX_VALUE, true, true,
true, false); true, false);
public static final GridConfig ZERO = new GridConfig( public static final GridConfig ZERO = zeroWithSize(CLASSICAL25);
9, false, false, CLASSICAL25, true,
true, false, 1, Set.of(1), false,
3, false, false,
false, false);
//TODO this 9 should be config
public static final GridConfig zeroWithSize(Map<CardColors, Integer> colors) { public static final GridConfig zeroWithSize(Map<CardColors, Integer> colors) {
return new GridConfig( return new GridConfig(
9, false, false, colors, true, 9, colors, true, true,
true, false, 1, Set.of(1), false, false, false, false, 1, Set.of(1), false,
3, false, false, 3, false, false,
false, false); false, false);
} }
@@ -79,8 +77,6 @@ public class GridConfig {
// 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;
private boolean anyoneStarts;
@Convert(converter = WordDistributionConverter.class) @Convert(converter = WordDistributionConverter.class)
private Map<CardColors, Integer> wordDistribution; private Map<CardColors, Integer> wordDistribution;
private boolean maybeSuddenDeath; //TODO private boolean maybeSuddenDeath; //TODO
@@ -89,6 +85,11 @@ public class GridConfig {
// 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;
@@ -34,12 +34,15 @@ public class World {
boolean won; boolean won;
public World(Game g, String name, GridConfig initConfig, Grid currentGrid, boolean won) { boolean locked;
public World(Game g, String name, GridConfig initConfig, Grid currentGrid, boolean won, boolean locked) {
this.id = new WorldId(g.getId(), name); this.id = new WorldId(g.getId(), name);
this.game = g; this.game = g;
this.initialConfig = initConfig; this.initialConfig = initConfig;
this.currentGrid = currentGrid; this.currentGrid = currentGrid;
this.won = won; this.won = won;
this.locked = locked;
} }
public String getName() { public String getName() {
@@ -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 @@
alter table if exists world add column locked boolean not null default false;
+4 -4
View File
@@ -256,10 +256,10 @@ function selectPlayer(player) {
socket = Stomp.over(new SockJS('/socket')); socket = Stomp.over(new SockJS('/socket'));
socket.connect({}, function (frame) { socket.connect({}, function (frame) {
socket.subscribe('/topic/new-phase', onNewPhase); socket.subscribe('/topic/'+roomId+'/new-phase', onNewPhase);
socket.subscribe('/topic/new-event', onNewEvent); socket.subscribe('/topic/'+roomId+'/new-event', onNewEvent);
socket.subscribe('/topic/update-card/' + player.toLowerCase(), onUpdateCard); socket.subscribe('/topic/'+roomId+'/update-card/' + player.toLowerCase(), onUpdateCard);
socket.subscribe('/topic/config-change', onConfigChange) socket.subscribe('/topic/'+roomId+'/config-change', onConfigChange)
}); });
} }
+4 -2
View File
@@ -12,8 +12,10 @@
<main> <main>
<ul> <ul>
<li th:each="world : ${worlds}"> <li th:each="world : ${worlds}">
<a th:href="@{/game/{gid}/grid/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">World <span th:text="${world.name}"/></a> <!-- TODO remove this duplication of World span -->
<a th:href="@{/game/{gid}/reset/{wname}?player={player}(gid=${game.id},wname=${world.name},player=${player})}">reset</a> <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> <span th:if="${world.won}">Gagné !</span>
</li> </li>
</ul> </ul>
+2
View File
@@ -11,6 +11,8 @@ version.com.fasterxml.jackson.core..jackson-databind=2.22.2
version.com.fasterxml.jackson.datatype..jackson-datatype-hibernate5=2.22.2 version.com.fasterxml.jackson.datatype..jackson-datatype-hibernate5=2.22.2
version.io.github.archipelagomw..Java-Client=0.2.1
version.jakarta.validation..jakarta.validation-api=3.1.1 version.jakarta.validation..jakarta.validation-api=3.1.1
## # available=4.0.0-M1 ## # available=4.0.0-M1