diff --git a/README.md b/README.md
new file mode 100644
index 0000000..663b336
--- /dev/null
+++ b/README.md
@@ -0,0 +1,4 @@
+
+
+## License
+Dictionaries taken from [this project](https://github.com/LandazuriPaul/Codenames)
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 407c492..2d3fe06 100644
--- a/build.gradle
+++ b/build.gradle
@@ -31,6 +31,7 @@ dependencies {
implementation 'org.flywaydb:flyway-core:_'
implementation 'org.flywaydb:flyway-database-postgresql:_'
implementation 'jakarta.validation:jakarta.validation-api:_'
+ implementation 'io.github.archipelagomw:Java-Client:_'
//TODO use webjars
runtimeOnly 'org.webjars:jquery:_'
diff --git a/flake.nix b/flake.nix
index f1292c4..f28bf10 100644
--- a/flake.nix
+++ b/flake.nix
@@ -39,6 +39,8 @@
echo "Launching NodeCames"
${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" ''
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 &
@@ -61,6 +63,7 @@
packages = [
jdk
gradle
+ postgres
(pkgs.vscode-with-extensions.override {
vscode = pkgs.vscodium;
vscodeExtensions = with pkgs.vscode-extensions; [
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index fcaf524..f6be882 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -103,17 +103,30 @@
-
-
-
+
+
+
-
-
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -280,6 +293,17 @@
+
+
+
+
+
+
+
+
+
+
+
@@ -1044,6 +1068,14 @@
+
+
+
+
+
+
+
+
diff --git a/src/main/apworld/items.py b/src/main/apworld/items.py
index ffec7df..0980540 100644
--- a/src/main/apworld/items.py
+++ b/src/main/apworld/items.py
@@ -19,7 +19,9 @@ ITEM_NAME_TO_ID = {
"structured_grid" : 9,
"hints_in_a_row" : 10,
"anyone_starts" : 11,
- "filler": 12
+ "progressive_hint_word_count": 12,
+ "progressive_hint_word_length": 13,
+ "filler": 14
}
for w in range(1,12+1):
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(
world: NodecamesWorld, name: str
) -> 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:
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)]
+ 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"),
diff --git a/src/main/apworld/options.py b/src/main/apworld/options.py
index 8d8a904..4ee9633 100644
--- a/src/main/apworld/options.py
+++ b/src/main/apworld/options.py
@@ -58,6 +58,29 @@ class MaxHintLength(Range):
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
@@ -171,6 +194,8 @@ class NodecamesOptions(PerGameCommonOptions):
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
@@ -218,6 +243,8 @@ option_presets = {
"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,
diff --git a/src/main/apworld/world.py b/src/main/apworld/world.py
index 5b11c82..f033b0d 100644
--- a/src/main/apworld/world.py
+++ b/src/main/apworld/world.py
@@ -12,7 +12,7 @@ class NodecamesWorld(World):
Nodecames is a word game, inspired by Code Names by Vlaada Chvátil
"""
- game = "Nodecames"
+ game = "nodecames"
options_dataclass = nodecames_options.NodecamesOptions
options: nodecames_options.NodecamesOptions
@@ -40,6 +40,10 @@ class NodecamesWorld(World):
def get_filler_item_name(self) -> str:
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
def fill_slot_data(self) -> Mapping[str, Any]:
return self.options.as_dict(
diff --git a/src/main/java/com/bernard/nodecames/HttpController.java b/src/main/java/com/bernard/nodecames/HttpController.java
index 6249e8a..52bb45f 100644
--- a/src/main/java/com/bernard/nodecames/HttpController.java
+++ b/src/main/java/com/bernard/nodecames/HttpController.java
@@ -1,11 +1,16 @@
package com.bernard.nodecames;
+import java.net.URISyntaxException;
import java.util.Comparator;
import java.util.List;
import java.util.Map.Entry;
import java.util.Optional;
import java.util.Random;
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;
@@ -21,6 +26,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.view.RedirectView;
import com.bernard.nodecames.frontend.GridConfigEdit;
+import com.bernard.nodecames.game.ArchipelagoManager;
import com.bernard.nodecames.game.GameManager;
import com.bernard.nodecames.game.GridManager;
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.WorldRepository;
+import io.github.archipelagomw.network.ConnectionResult;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@@ -41,6 +48,7 @@ public class HttpController {
private final GridManager gm;
private final GameManager wm;
+ private final ArchipelagoManager am;
private final GridRepository gridRepository;
private final GameRepository gRepository;
private final WorldRepository wRepository;
@@ -78,6 +86,7 @@ public class HttpController {
model.addAttribute("gridConfig", GridConfigEdit.of(gc));
return "grid-config";
}
+
@PostMapping("/room/{id}/config")
public Object setGridConfig(@PathVariable("id") String id,
@ModelAttribute("gridConfig") GridConfigEdit gce,
@@ -138,6 +147,38 @@ public class HttpController {
return "map";
}
+ @GetMapping("/game/{id}/archipelago")
+ public Object gridConfig(@PathVariable("id") String id, @RequestParam("address") String address,
+ @RequestParam("slot") String slot, Model model) {
+
+ Optional g = gRepository.findById(UUID.fromString(id));
+ if(!g.isPresent())
+ return new ResponseEntity<>(HttpStatus.NOT_FOUND);
+
+ try {
+ CompletableFuture 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 g = gRepository.findById(UUID.fromString(id));
@@ -148,7 +189,12 @@ public class HttpController {
if(!w.isPresent()) {
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);
}
@@ -162,7 +208,11 @@ public class HttpController {
if(!w.isPresent()) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
- Grid newGrid = wm.resetGrid(g.get(), w.get());
- return new RedirectView("/room/"+newGrid.getId()+"/grid" + "?player="+player);
+ 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);
+ }
}
}
diff --git a/src/main/java/com/bernard/nodecames/frontend/WebSocketController.java b/src/main/java/com/bernard/nodecames/frontend/WebSocketController.java
index 6190572..57d08d0 100644
--- a/src/main/java/com/bernard/nodecames/frontend/WebSocketController.java
+++ b/src/main/java/com/bernard/nodecames/frontend/WebSocketController.java
@@ -1,7 +1,9 @@
package com.bernard.nodecames.frontend;
+import java.util.HashSet;
import java.util.Map;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
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.IllegalGameActionException;
import com.bernard.nodecames.model.Card;
+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.Phase;
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;
@Controller
+@AllArgsConstructor
public class WebSocketController {
private final GridManager gm;
@@ -32,49 +40,19 @@ public class WebSocketController {
private final JsonDataService json;
private final GridRepository gRepository;
- public WebSocketController(GridManager gm, SimpMessagingTemplate template, JsonDataService json, GridRepository gRepository) {
- this.gm = gm;
- this.template = template;
- this.json = json;
- this.gRepository = gRepository;
- 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');
+ Set observed = new HashSet<>();
+ private void registerObserver(Grid g) {
+ if(!observed.contains(g.getId())) {
+ observed.add(g.getId());
+ gm.addObserver(g, new GObserver(g));
+ }
}
public void publishError(char issuer, IllegalGameActionException e) {
//TODO open error channel
}
+ //TODO We should do this with websocket protocol
@GetMapping("/room/{id}/game/{player}")
public Object grid(@PathVariable("id") String gridId, @PathVariable("player") String playerStr) {
char player;
@@ -86,14 +64,19 @@ public class WebSocketController {
throw new IllegalArgumentException("Unknown player type");
Optional g = gRepository.findById(UUID.fromString(gridId));
- if(g.isPresent())
- return new ResponseEntity<>(json.gridData(g.get(), player), HttpStatus.OK);
- else
+ if(g.isPresent()){
+ 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")
- public void submitHint(@Header("room") String roomId, @Header("player") String player, @Payload JsonNode content) {
+ //TODO handle websockets per grid
+ @MessageMapping("/submit-hint")
+ public void submitHint(@Header("room") String roomId,
+ @Header("player") String player,
+ @Payload JsonNode content) {
String hint = content.asObject().get("hint").stringValue();
int wordCount = content.asObject().get("hintWordCount").intValue();
@@ -101,7 +84,6 @@ public class WebSocketController {
if(!g.isPresent())
// 404
return;
-
try {
gm.proposeHint(g.get(), player.charAt(0), hint, wordCount);
} catch (IllegalGameActionException e) {
@@ -118,10 +100,8 @@ public class WebSocketController {
if(!g.isPresent())
// 404
return;
-
try {
gm.pointCard(g.get(), player.charAt(0), cardIndex);
- publishCardUpdate(g.get(), gm.getCard(g.get(), cardIndex));
} catch (IllegalGameActionException e) {
publishError(player.charAt(0), e);
e.printStackTrace();
@@ -134,7 +114,6 @@ public class WebSocketController {
if(!g.isPresent())
// 404
return;
-
try {
gm.endGuessing(g.get(), player.charAt(0));
} catch (IllegalGameActionException e) {
@@ -156,4 +135,56 @@ public class WebSocketController {
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)
+ );
+ }
+ }
}
diff --git a/src/main/java/com/bernard/nodecames/game/ArchipelagoClient.java b/src/main/java/com/bernard/nodecames/game/ArchipelagoClient.java
new file mode 100644
index 0000000..eca80d0
--- /dev/null
+++ b/src/main/java/com/bernard/nodecames/game/ArchipelagoClient.java
@@ -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 = 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'");
+ }
+
+
+}
diff --git a/src/main/java/com/bernard/nodecames/game/ArchipelagoManager.java b/src/main/java/com/bernard/nodecames/game/ArchipelagoManager.java
new file mode 100644
index 0000000..5e0d103
--- /dev/null
+++ b/src/main/java/com/bernard/nodecames/game/ArchipelagoManager.java
@@ -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 clients = new HashMap<>();
+
+ GameManager gm;
+ CardRepository cRepository;
+ WorldRepository wRepository;
+
+ private static final Logger log = LoggerFactory.getLogger(ArchipelagoManager.class);
+
+ public CompletableFuture 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 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) {}
+ }
+
+}
diff --git a/src/main/java/com/bernard/nodecames/game/GameManager.java b/src/main/java/com/bernard/nodecames/game/GameManager.java
index 8b1ed33..a9a9a00 100644
--- a/src/main/java/com/bernard/nodecames/game/GameManager.java
+++ b/src/main/java/com/bernard/nodecames/game/GameManager.java
@@ -2,38 +2,75 @@ package com.bernard.nodecames.game;
import java.util.Collection;
import java.util.HashMap;
+import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
+import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.UUID;
+import java.util.concurrent.CopyOnWriteArraySet;
+import java.util.function.Consumer;
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;
+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.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 jakarta.annotation.PostConstruct;
import jakarta.validation.constraints.NotNull;
+import lombok.AllArgsConstructor;
@Service
+@AllArgsConstructor
public class GameManager {
- // Reverse map allowing to pass grid events to the game
- Map currentGrids = new HashMap<>();
-
private final GridManager gm;
private final GameRepository gRepository;
private final WorldRepository wRepository;
- public GameManager(GridManager gm, GameRepository gRepository, WorldRepository wRepository) {
- this.gm = gm;
- this.gRepository = gRepository;
- this.wRepository = wRepository;
- gm.setWm(this);
+ //TODO try putting "World" here instead of WorldId
+ private Map> observers = new HashMap<>();
+
+ public void addObserver(World w, WorldObserver observer) {
+ if(!observers.containsKey(w.getId()))
+ observers.put(w.getId(), new CopyOnWriteArraySet<>());
+ observers.get(w.getId()).add(observer);
+ if(w.getCurrentGrid() != null)
+ gm.addObserver(w.getCurrentGrid(), observer);
+ }
+
+ private void forEachObserver(World w, Consumer 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) {
@@ -42,10 +79,9 @@ public class GameManager {
Game g = new Game(uuid, name);
gRepository.save(g);
for (Entry wConfig : Game.DEFAULT_WORLDS_CONFIG.entrySet()) {
- World w = new World(g, wConfig.getKey(), wConfig.getValue(), null, false);
+ World w = new World(g, wConfig.getKey(), wConfig.getValue(), null, false, true);
wRepository.save(w);
}
-
return g;
}
@@ -57,22 +93,26 @@ public class GameManager {
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();
+ //TODO seed random
Grid grid = gm.newGrid(w, gc, new Random());
w.setCurrentGrid(grid);
wRepository.save(w);
- currentGrids.put(grid.getId(), w);
+ registerObserver(w, grid);
return grid;
}
- public Grid resetGrid(Game g, World world) {
+ public Grid resetGrid(Game g, World world) throws IllegalGameActionException{
return newGrid(g, world);
}
- public Grid getGrid(Game g, World w) {
+ public Grid getGrid(Game g, World w) throws IllegalGameActionException{
if(w.getCurrentGrid() != null) {
return w.getCurrentGrid();
} else {
@@ -80,11 +120,100 @@ public class GameManager {
}
}
- public void onGameWon(Grid g) {
- World w = currentGrids.get(g.getId());
- if(w != null) {
- w.setWon(true);
+ /**
+ * UPDATE GAME CONFIG
+ */
+ public void unlockMutable(Game g, MutableOption opt, Object o) throws IllegalGameActionException {
+ Set worlds = wRepository.findByGame(g);
+ for(World w : worlds) {
+ GridConfig initialConfig = w.getInitialConfig();
+ initialConfig.set(opt, o);
+ w.setInitialConfig(initialConfig);
+ wRepository.save(w);
+ Grid grid = w.getCurrentGrid();
+ if(grid != null) {
+ gm.unlock(grid, opt, o);
+ }
+ }
+ }
+
+ public void unlockStructuredGrid(Game g) {
+ Set worlds = wRepository.findByGame(g);
+ for(World w : worlds) {
+ GridConfig initialConfig = w.getInitialConfig();
+ initialConfig.setStructuredGrid(true);
+ w.setInitialConfig(initialConfig);
wRepository.save(w);
}
}
+ public void unlockAnyoneCanStart(Game g) {
+ Set worlds = wRepository.findByGame(g);
+ for(World w : worlds) {
+ GridConfig initialConfig = w.getInitialConfig();
+ initialConfig.setAnyoneStarts(true);
+ w.setInitialConfig(initialConfig);
+ wRepository.save(w);
+ }
+ }
+
+ public void unlockWorld(Game g, String name) throws IllegalGameActionException {
+ Optional ow = wRepository.findByGameAndId_Name(g, name);
+ if(!ow.isPresent())
+ throw new IllegalGameActionException(null, "Could not find world with name %s".formatted(name));
+ World w = ow.get();
+ w.setLocked(false);
+ wRepository.save(w);
+ }
+
+ public void unlockMoreHintCount(Game game) throws IllegalGameActionException {
+ //TODO Store current hint count elsewhere
+ int currentHintCount = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getMaxHint();
+ unlockMutable(game, MutableOption.MAX_HINT, currentHintCount+1);
+ }
+
+ public void unlockMoreHintWordLengthCount(Game game) throws IllegalGameActionException {
+ //TODO Store current hint word length elsewhere
+ int currentHintWordLength = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getHintMaxLength();
+ unlockMutable(game, MutableOption.HINT_MAX_LENGTH, currentHintWordLength+1);
+ }
+
+ public void unlockMoreHintWordCount(Game game) throws IllegalGameActionException {
+ //TODO Store current hint word count elsewhere
+ int currentMaxWordCount = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getAvailableHintWordCount().stream().mapToInt(i -> i).max().getAsInt();
+ unlockHintWordCount(game, currentMaxWordCount+1);
+ }
+
+ public void unlockHintWordCount(Game game, int i) throws IllegalGameActionException {
+ Set currentWordCounts = wRepository.findByGame(game).stream().findAny().get().getInitialConfig().getAvailableHintWordCount();
+ if(currentWordCounts.contains(i))
+ throw new IllegalGameActionException(null, "Word count already unlocked : %d".formatted(i));
+ Set newWordCounts = new HashSet<>(currentWordCounts);
+ newWordCounts.add(i);
+ unlockMutable(game, MutableOption.AVAILABLE_HINT_WORD_COUNT, newWordCounts);
+ }
+
+ @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) {}
+ }
}
diff --git a/src/main/java/com/bernard/nodecames/game/GridManager.java b/src/main/java/com/bernard/nodecames/game/GridManager.java
index db02368..450248b 100644
--- a/src/main/java/com/bernard/nodecames/game/GridManager.java
+++ b/src/main/java/com/bernard/nodecames/game/GridManager.java
@@ -2,11 +2,15 @@ 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;
@@ -14,10 +18,9 @@ import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
-import com.bernard.nodecames.frontend.WebSocketController;
import com.bernard.nodecames.model.Card;
-import com.bernard.nodecames.model.GameEvent;
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.Phase;
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.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;
-import lombok.Setter;
@Service
@RequiredArgsConstructor
public class GridManager {
- @Setter
- private WebSocketController wsc = null;
- @Setter
- private GameManager wm = null;
+ private Map> observers = new HashMap<>();
private final GridRepository gRepository;
private final CardRepository cRepository;
@@ -48,11 +48,22 @@ public class GridManager {
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 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);
- wsc.onNewEvent(g, ge);
+ forEachObserver(g, o -> o.onGameEvent(ge));
}
private void setPhase(Grid g, Phase phase) {
@@ -62,33 +73,40 @@ public class GridManager {
g.setCurrentHint(null);
}
gRepository.save(g);
- wsc.onNewPhase(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 setCardPublic(Card c, char player) {
+ 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, Hint h) {
+ 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);
- wsc.onConfigChange(g, cc, data);
+ forEachObserver(g, o -> o.onConfigChange(cc, data));
}
@@ -169,12 +187,10 @@ public class GridManager {
* GAME ACTIONS *
**********************************/
private void endGame(Grid g, boolean win) {
- setWon(g, win);
setPhase(g, Phase.GAME_ENDED);
newGameEvent(g, GameEvent.newGameEndEvent(win));
- if(win) {
- wm.onGameWon(g);
- }
+ // 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 {
@@ -189,7 +205,7 @@ public class GridManager {
}
Hint h = new Hint(hint, wordCount);
- newHint(g, h);
+ newHint(g, player, h);
newGameEvent(g, GameEvent.newHintEvent(h, player));
setPhase(g, Phase.guessing(other(player)));
}
@@ -264,9 +280,9 @@ public class GridManager {
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");
- setCardPublic(c, 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))));
g.incrementCurrentGuessCount();
if(guessedColor == Color.BLACK){
@@ -369,7 +385,7 @@ public class GridManager {
}
public void unlockHintCount(Grid g, int newHintCount) throws IllegalGameActionException {
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()) {
throw new IllegalGameActionException(g, "Cannot lower hint count");
diff --git a/src/main/java/com/bernard/nodecames/model/Grid.java b/src/main/java/com/bernard/nodecames/model/Grid.java
index 17de405..750e525 100644
--- a/src/main/java/com/bernard/nodecames/model/Grid.java
+++ b/src/main/java/com/bernard/nodecames/model/Grid.java
@@ -3,6 +3,7 @@ package com.bernard.nodecames.model;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
+import java.util.Set;
import java.util.UUID;
import org.hibernate.annotations.JdbcTypeCode;
@@ -10,12 +11,15 @@ import org.hibernate.type.SqlTypes;
import jakarta.annotation.Nullable;
import jakarta.persistence.AttributeConverter;
+import jakarta.persistence.CascadeType;
import jakarta.persistence.Column;
import jakarta.persistence.Convert;
import jakarta.persistence.Embedded;
import jakarta.persistence.Entity;
+import jakarta.persistence.FetchType;
import jakarta.persistence.Id;
import jakarta.persistence.ManyToOne;
+import jakarta.persistence.OneToMany;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
diff --git a/src/main/java/com/bernard/nodecames/model/GridConfig.java b/src/main/java/com/bernard/nodecames/model/GridConfig.java
index a44a639..c7d8965 100644
--- a/src/main/java/com/bernard/nodecames/model/GridConfig.java
+++ b/src/main/java/com/bernard/nodecames/model/GridConfig.java
@@ -21,6 +21,7 @@ import jakarta.persistence.Embeddable;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
+import lombok.Setter;
import lombok.Value;
@Getter
@@ -50,27 +51,24 @@ public class GridConfig {
0,1,2,3,4,5,6,7,8,9,-1
);
public static final GridConfig BIG_CLASSICAL = new GridConfig(
- 12, true, true, CUSTOM36, true,
- true, true, 12, BASE_HINTWORDCOUNT, true,
+ 12, CUSTOM36, true, true,
+ true, true, true, 12, BASE_HINTWORDCOUNT, true,
Integer.MAX_VALUE, true, true,
true, false);
public static final GridConfig CODENAMES_CLASSICAL = new GridConfig(
- 9, true, true, CLASSICAL25, true,
+ 9, CLASSICAL25, true, true, true,
true, true, 9, BASE_HINTWORDCOUNT, true,
Integer.MAX_VALUE, true, true,
true, false);
- public static final GridConfig ZERO = new GridConfig(
- 9, false, false, CLASSICAL25, true,
- true, false, 1, Set.of(1), false,
- 3, false, false,
- false, false);
+ public static final GridConfig ZERO = zeroWithSize(CLASSICAL25);
+ //TODO this 9 should be config
public static final GridConfig zeroWithSize(Map colors) {
return new GridConfig(
- 9, false, false, colors, true,
- true, false, 1, Set.of(1), false,
+ 9, colors, true, true,
+ false, false, false, 1, Set.of(1), false,
3, false, false,
false, false);
}
@@ -79,8 +77,6 @@ public class GridConfig {
// Maximum of maximom number of hints
// This is to trigger end of game
private int maxMaxHint;
- private boolean structuredGrid;
- private boolean anyoneStarts;
@Convert(converter = WordDistributionConverter.class)
private Map wordDistribution;
private boolean maybeSuddenDeath; //TODO
@@ -89,6 +85,11 @@ public class GridConfig {
// prevend death and enter WIN_IF_GREEN_GREEN state, else just die
private boolean onlyOneGreenGreenPreventDeath;
+ // Game mutable options
+ @Setter
+ private boolean structuredGrid;
+ @Setter
+ private boolean anyoneStarts;
// Mutable options
private int maxHint;
diff --git a/src/main/java/com/bernard/nodecames/model/World.java b/src/main/java/com/bernard/nodecames/model/World.java
index 0b4dea1..849683b 100644
--- a/src/main/java/com/bernard/nodecames/model/World.java
+++ b/src/main/java/com/bernard/nodecames/model/World.java
@@ -34,12 +34,15 @@ public class World {
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.game = g;
this.initialConfig = initConfig;
this.currentGrid = currentGrid;
this.won = won;
+ this.locked = locked;
}
public String getName() {
diff --git a/src/main/java/com/bernard/nodecames/observer/GridObserver.java b/src/main/java/com/bernard/nodecames/observer/GridObserver.java
new file mode 100644
index 0000000..e435bf8
--- /dev/null
+++ b/src/main/java/com/bernard/nodecames/observer/GridObserver.java
@@ -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);
+
+}
diff --git a/src/main/java/com/bernard/nodecames/observer/WorldObserver.java b/src/main/java/com/bernard/nodecames/observer/WorldObserver.java
new file mode 100644
index 0000000..f33d33e
--- /dev/null
+++ b/src/main/java/com/bernard/nodecames/observer/WorldObserver.java
@@ -0,0 +1,5 @@
+package com.bernard.nodecames.observer;
+
+public interface WorldObserver extends GridObserver {
+
+}
diff --git a/src/main/resources/db/migration/V1_1_0__world_locking.sql b/src/main/resources/db/migration/V1_1_0__world_locking.sql
new file mode 100644
index 0000000..0191a9b
--- /dev/null
+++ b/src/main/resources/db/migration/V1_1_0__world_locking.sql
@@ -0,0 +1 @@
+alter table if exists world add column locked boolean not null default false;
diff --git a/src/main/resources/static/js/grid.js b/src/main/resources/static/js/grid.js
index 60cec55..61d86ab 100644
--- a/src/main/resources/static/js/grid.js
+++ b/src/main/resources/static/js/grid.js
@@ -256,10 +256,10 @@ function selectPlayer(player) {
socket = Stomp.over(new SockJS('/socket'));
socket.connect({}, function (frame) {
- socket.subscribe('/topic/new-phase', onNewPhase);
- socket.subscribe('/topic/new-event', onNewEvent);
- socket.subscribe('/topic/update-card/' + player.toLowerCase(), onUpdateCard);
- socket.subscribe('/topic/config-change', onConfigChange)
+ socket.subscribe('/topic/'+roomId+'/new-phase', onNewPhase);
+ socket.subscribe('/topic/'+roomId+'/new-event', onNewEvent);
+ socket.subscribe('/topic/'+roomId+'/update-card/' + player.toLowerCase(), onUpdateCard);
+ socket.subscribe('/topic/'+roomId+'/config-change', onConfigChange)
});
}
diff --git a/src/main/resources/templates/map.html b/src/main/resources/templates/map.html
index 0467ca7..59c63e4 100644
--- a/src/main/resources/templates/map.html
+++ b/src/main/resources/templates/map.html
@@ -12,8 +12,10 @@
diff --git a/versions.properties b/versions.properties
index 06110f9..04ea12a 100644
--- a/versions.properties
+++ b/versions.properties
@@ -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.io.github.archipelagomw..Java-Client=0.2.1
+
version.jakarta.validation..jakarta.validation-api=3.1.1
## # available=4.0.0-M1