From d82409a411071f683a1657b384ca12c5c0fa3116 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 18:32:01 +0200 Subject: [PATCH 001/207] feat: add start game websocket dto --- .../codenames/backend/game/dto/StartGameMessage.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java new file mode 100644 index 00000000..4a688a86 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java @@ -0,0 +1,10 @@ +package com.codenames.codenames.backend.game.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class StartGameMessage { + private String lobbyCode; +} From d268f3779bd1a70745770af4d353da2c2dda1524 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 18:34:52 +0200 Subject: [PATCH 002/207] feat: add lobby getter for websocket gameplay --- .../backend/game/dto/StartGameMessage.java | 2 +- .../backend/lobby/services/LobbyService.java | 22 +++++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java index 4a688a86..eb88ca4b 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java @@ -6,5 +6,5 @@ @Getter @Setter public class StartGameMessage { - private String lobbyCode; + private String lobbyCode; } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 0af52802..62b858cc 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -12,8 +12,8 @@ /** * Service responsible for managing lobbies and player interactions. * - *

Handles creation of lobbies, player joins/leaves, and retrieval of lobby data. - * Ensures uniqueness of lobby codes and thread-safe access to lobby storage. + *

Handles creation of lobbies, player joins/leaves, and retrieval of lobby data. Ensures + * uniqueness of lobby codes and thread-safe access to lobby storage. */ @Service public class LobbyService { @@ -50,7 +50,7 @@ public String createLobby(String username) { /** * Adds a player to an existing lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby * @return {@code true} if the player successfully joined, {@code false} otherwise */ @@ -65,7 +65,7 @@ public boolean joinLobby(String username, String lobbyCode) { /** * Removes a player from a lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby * @return {@code true} if the player was removed, {@code false} if the lobby does not exist */ @@ -81,10 +81,10 @@ public boolean leaveLobby(String username, String lobbyCode) { /** * Assigns a team and role to a player in a lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby - * @param team the selected team - * @param role the selected role + * @param team the selected team + * @param role the selected role * @return {@code true} if the position was assigned, {@code false} otherwise */ public boolean selectPosition(String username, String lobbyCode, Team team, Role role) { @@ -117,9 +117,9 @@ public List getPlayers(String lobbyCode) { /** * Checks whether a spymaster is already assigned for the given team in the lobby. * - * @param lobby the lobby to inspect + * @param lobby the lobby to inspect * @param username the username requesting the role - * @param team the team to inspect + * @param team the team to inspect * @return {@code true} if a different player is already the spymaster for that team */ private boolean isSpymasterAlreadyAssigned(Lobby lobby, String username, Team team) { @@ -183,4 +183,8 @@ public Role getPlayerRole(String username, String lobbyCode) { } return null; } + + public Lobby getLobby(String lobbyCode) { + return lobbyList.get(lobbyCode); + } } From f94dba43453da384fec86d88fd2ed8f80cf70759 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 18:40:09 +0200 Subject: [PATCH 003/207] refactor: inject gameplay websocket dependencies --- .../game/controller/GameSocketController.java | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java new file mode 100644 index 00000000..73fab3e1 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -0,0 +1,25 @@ +package com.codenames.codenames.backend.game.controller; + +import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.playingfield.CardGenerator; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +public class GameSocketController { + + private final LobbyService lobbyService; + private final SimpMessagingTemplate messagingTemplate; + private final CardGenerator cardGenerator; + private final ClueValidationService clueValidationService; + + public GameSocketController( + LobbyService lobbyService, + SimpMessagingTemplate messagingTemplate, + CardGenerator cardGenerator, + ClueValidationService clueValidationService) { + this.lobbyService = lobbyService; + this.messagingTemplate = messagingTemplate; + this.cardGenerator = cardGenerator; + this.clueValidationService = clueValidationService; + } +} From 3fb4b692f03c95a82076f626ab380c5538f9a738 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 18:53:31 +0200 Subject: [PATCH 004/207] feat: add in memory game session storage --- .../backend/game/controller/GameSocketController.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 73fab3e1..28849047 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -3,8 +3,12 @@ import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.CardGenerator; +import com.codenames.codenames.backend.playingfield.GameManager; import org.springframework.messaging.simp.SimpMessagingTemplate; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + public class GameSocketController { private final LobbyService lobbyService; @@ -12,6 +16,8 @@ public class GameSocketController { private final CardGenerator cardGenerator; private final ClueValidationService clueValidationService; + private final Map gameSessions = new ConcurrentHashMap<>(); + public GameSocketController( LobbyService lobbyService, SimpMessagingTemplate messagingTemplate, From 8a4ede18e428f07bbe172355c747553d35d0d52c Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 19:02:32 +0200 Subject: [PATCH 005/207] refactor: encapsulate starting team selection in lobby service --- .../codenames/backend/lobby/services/LobbyService.java | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 62b858cc..aeb828fa 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -184,7 +184,13 @@ public Role getPlayerRole(String username, String lobbyCode) { return null; } - public Lobby getLobby(String lobbyCode) { - return lobbyList.get(lobbyCode); + public Team decideStartingTeam(String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + + if (lobby == null) { + return null; + } + + return lobby.decideStartingTeam(); } } From 06ae58bc19062ec7ade35867629a8a11a170635f Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 19:03:59 +0200 Subject: [PATCH 006/207] feat: add websocket endpoint for starting games --- .../game/controller/GameSocketController.java | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 28849047..d6f9d32a 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -1,13 +1,15 @@ package com.codenames.codenames.backend.game.controller; import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.CardGenerator; import com.codenames.codenames.backend.playingfield.GameManager; -import org.springframework.messaging.simp.SimpMessagingTemplate; - +import com.codenames.codenames.backend.utility.Team; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.messaging.handler.annotation.MessageMapping; +import org.springframework.messaging.simp.SimpMessagingTemplate; public class GameSocketController { @@ -28,4 +30,17 @@ public GameSocketController( this.cardGenerator = cardGenerator; this.clueValidationService = clueValidationService; } + + @MessageMapping("/start-game") + public void startGame(StartGameMessage message) { + + Team startingTeam = lobbyService.decideStartingTeam(message.getLobbyCode()); + + GameManager gameManager = new GameManager(startingTeam, cardGenerator, clueValidationService); + + gameSessions.put(message.getLobbyCode(), gameManager); + + messagingTemplate.convertAndSend( + "/topic/game/" + message.getLobbyCode(), gameManager.getCardList()); + } } From 3dd55fe7eca922e50c285a218ff75f0a03c60c9e Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 19:39:58 +0200 Subject: [PATCH 007/207] feat: add reveal card websocket dto --- .../backend/game/dto/RevealCardMessage.java | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java new file mode 100644 index 00000000..21a30fb1 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java @@ -0,0 +1,13 @@ +package com.codenames.codenames.backend.game.dto; + +import com.codenames.codenames.backend.utility.Color; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class RevealCardMessage { + private String lobbyCode; + private int position; + private Color currentTurn; +} From d3fcd88c3697b3c5a60b86b30a5e758f8c4186d9 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 19:41:33 +0200 Subject: [PATCH 008/207] feat: add websocket endpoint for revealing cards --- .../game/controller/GameSocketController.java | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index d6f9d32a..93b8f6bd 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.game.controller; import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.CardGenerator; @@ -43,4 +44,19 @@ public void startGame(StartGameMessage message) { messagingTemplate.convertAndSend( "/topic/game/" + message.getLobbyCode(), gameManager.getCardList()); } + + @MessageMapping("/reveal-card") + public void revealCard(RevealCardMessage message) { + + GameManager gameManager = gameSessions.get(message.getLobbyCode()); + + if (gameManager == null) { + return; + } + + gameManager.flipCard(message.getPosition(), message.getCurrentTurn()); + + messagingTemplate.convertAndSend( + "/topic/game/" + message.getLobbyCode(), gameManager.getCardList()); + } } From 11b88f94c133f164873d8c79b5c601c091e96477 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 22:58:19 +0200 Subject: [PATCH 009/207] feat: add clue websocket dto --- .../codenames/backend/game/dto/ClueMessage.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java new file mode 100644 index 00000000..7b1e8113 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java @@ -0,0 +1,12 @@ +package com.codenames.codenames.backend.game.dto; + +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ClueMessage { + private String lobbyCode; + private String word; + private int guessAmount; +} From 4ce3354ea24538523f28996bb3a14061edaf2a08 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 23:02:00 +0200 Subject: [PATCH 010/207] feat: add websocket endpoint for clue submission --- .../game/controller/GameSocketController.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 93b8f6bd..d535871f 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -1,6 +1,8 @@ package com.codenames.codenames.backend.game.controller; +import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.game.dto.ClueMessage; import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.lobby.services.LobbyService; @@ -59,4 +61,20 @@ public void revealCard(RevealCardMessage message) { messagingTemplate.convertAndSend( "/topic/game/" + message.getLobbyCode(), gameManager.getCardList()); } + + @MessageMapping("/submit-clue") + public void submitClue(ClueMessage message) { + + GameManager gameManager = gameSessions.get(message.getLobbyCode()); + + if (gameManager == null) { + return; + } + + Clue clue = new Clue(message.getWord(), message.getGuessAmount()); + + gameManager.submitClue(clue); + + messagingTemplate.convertAndSend("/topic/game/" + message.getLobbyCode(), gameManager); + } } From b216d7b18e2e711b212eeadedc4398cd4c179672 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 10 May 2026 23:10:24 +0200 Subject: [PATCH 011/207] test: add game websocket controller tests --- .../controller/GameSocketControllerTest.java | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java new file mode 100644 index 00000000..1bb2dfc9 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -0,0 +1,30 @@ +package com.codenames.codenames.backend.game.controller; + +import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.playingfield.CardGenerator; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.messaging.simp.SimpMessagingTemplate; + +@ExtendWith(MockitoExtension.class) +class GameSocketControllerTest { + @Mock private LobbyService lobbyService; + + @Mock private SimpMessagingTemplate messagingTemplate; + + @Mock private CardGenerator cardGenerator; + + @Mock private ClueValidationService clueValidationService; + + private GameSocketController controller; + + @BeforeEach + void setUp() { + controller = + new GameSocketController( + lobbyService, messagingTemplate, cardGenerator, clueValidationService); + } +} From 06384b05159b892f34d91cc4e7f8b495e9d631cc Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 11 May 2026 00:51:55 +0200 Subject: [PATCH 012/207] test: verify websocket gameplay interactions --- .../controller/GameSocketControllerTest.java | 103 ++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 1bb2dfc9..74306cd1 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -1,14 +1,32 @@ package com.codenames.codenames.backend.game.controller; import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.game.dto.ClueMessage; +import com.codenames.codenames.backend.game.dto.RevealCardMessage; +import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.playingfield.CardGenerator; +import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.utility.Team; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.messaging.simp.SimpMessagingTemplate; +import java.util.List; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + @ExtendWith(MockitoExtension.class) class GameSocketControllerTest { @Mock private LobbyService lobbyService; @@ -27,4 +45,89 @@ void setUp() { new GameSocketController( lobbyService, messagingTemplate, cardGenerator, clueValidationService); } + + @Test + void startGameShouldBroadcastBoard() { + + when(lobbyService.decideStartingTeam("ABCDE")).thenReturn(Team.RED); + + StartGameMessage message = new StartGameMessage(); + + message.setLobbyCode("ABCDE"); + + controller.startGame(message); + + verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); + } + + @Test + void revealCardShouldBroadcastBoardUpdate() { + + when(lobbyService.decideStartingTeam("ABCDE")).thenReturn(Team.RED); + + when(cardGenerator.generateCards(anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) + .thenReturn(List.of(new Card("Test", Color.RED))); + + StartGameMessage startMessage = new StartGameMessage(); + + startMessage.setLobbyCode("ABCDE"); + + when(clueValidationService.validateWord(any(), anyString())).thenReturn(true); + + controller.startGame(startMessage); + + ClueMessage clueMessage = new ClueMessage(); + + clueMessage.setLobbyCode("ABCDE"); + clueMessage.setWord("animal"); + clueMessage.setGuessAmount(1); + + controller.submitClue(clueMessage); + + RevealCardMessage revealMessage = new RevealCardMessage(); + + revealMessage.setLobbyCode("ABCDE"); + revealMessage.setPosition(0); + revealMessage.setCurrentTurn(Color.RED); + + controller.revealCard(revealMessage); + + verify(messagingTemplate, times(3)).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); + } + + @Test + void revealCardShouldReturnWhenGameSessionMissing() { + + RevealCardMessage message = new RevealCardMessage(); + + message.setLobbyCode("UNKNOWN"); + + controller.revealCard(message); + + verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); + } + + @Test + void submitClueShouldBroadcastGameUpdate() { + + when(lobbyService.decideStartingTeam("ABCDE")).thenReturn(Team.RED); + + StartGameMessage startMessage = new StartGameMessage(); + + startMessage.setLobbyCode("ABCDE"); + + controller.startGame(startMessage); + + ClueMessage clueMessage = new ClueMessage(); + + clueMessage.setLobbyCode("ABCDE"); + clueMessage.setWord("animal"); + clueMessage.setGuessAmount(2); + + when(clueValidationService.validateWord(any(), anyString())).thenReturn(true); + + controller.submitClue(clueMessage); + + verify(messagingTemplate, times(2)).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); + } } From 26bde4a918c5cd9e5dc563d383f7e8c11669b804 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 11 May 2026 00:53:53 +0200 Subject: [PATCH 013/207] test: verify submit clue ignores missing game session --- .../game/controller/GameSocketControllerTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 74306cd1..4e753eb8 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -130,4 +130,18 @@ void submitClueShouldBroadcastGameUpdate() { verify(messagingTemplate, times(2)).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } + + @Test + void submitClueShouldReturnWhenGameSessionMissing() { + + ClueMessage clueMessage = new ClueMessage(); + + clueMessage.setLobbyCode("UNKNOWN"); + clueMessage.setWord("animal"); + clueMessage.setGuessAmount(2); + + controller.submitClue(clueMessage); + + verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); + } } From fe1e897e0eec26dda0968ed56e886a92cf6f5968 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 11 May 2026 00:56:08 +0200 Subject: [PATCH 014/207] test: verify starting team selection --- .../lobby/services/LobbyServiceTest.java | 22 +++++++++++-------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index 43059b19..f915ef14 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -20,7 +20,6 @@ * *

Validates lobby creation, joining, leaving, and player management behavior. */ - class LobbyServiceTest { private LobbyService lobbyService; @@ -87,10 +86,7 @@ void leaveLobbyReturnFalseLobbyNotExists() { @Test void createLobbyShouldGenerateNewCodeIfDuplicateExists() { - when(generator.generateLobbyCode()) - .thenReturn("ABCDE") - .thenReturn("ABCDE") - .thenReturn("FGHIJ"); + when(generator.generateLobbyCode()).thenReturn("ABCDE").thenReturn("ABCDE").thenReturn("FGHIJ"); lobbyService.createLobby("Host1"); String code2 = lobbyService.createLobby("Host2"); @@ -179,9 +175,7 @@ void joinLobbyShouldReturnFalseWhenPlayerAlreadyExists() { List players = lobbyService.getPlayers("ABCDE"); - long count = players.stream() - .filter(p -> p.getUsername().equals("Max")) - .count(); + long count = players.stream().filter(p -> p.getUsername().equals("Max")).count(); assertEquals(1, count); } @@ -245,4 +239,14 @@ void getPlayerRole_nonExistentPlayer() { assertNull(lobbyService.getPlayerRole("nonExistentPlayer", lobbyCode)); } -} \ No newline at end of file + + @Test + void decideStartingTeamShouldReturnTeam() { + + lobbyService.createLobby("Host"); + + Team result = lobbyService.decideStartingTeam("ABCDE"); + + assertNotNull(result); + } +} From d695459fe38e36754d0c2956a72119fce6c445bf Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 11 May 2026 00:58:48 +0200 Subject: [PATCH 015/207] test: verify starting team selection returns null for missing lobby --- .../backend/lobby/services/LobbyServiceTest.java | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index f915ef14..f4e4f2d0 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -249,4 +249,12 @@ void decideStartingTeamShouldReturnTeam() { assertNotNull(result); } + + @Test + void decideStartingTeamShouldReturnNullWhenLobbyDoesNotExist() { + + Team result = lobbyService.decideStartingTeam("UNKNOWN"); + + assertNull(result); + } } From 9b127f597d82310f57f9ba51380231b9ffa1225e Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 11 May 2026 01:19:17 +0200 Subject: [PATCH 016/207] docs: add javadocs --- .../game/controller/GameSocketController.java | 35 +++++++++++++++++++ .../backend/game/dto/ClueMessage.java | 5 +++ .../backend/game/dto/RevealCardMessage.java | 5 +++ .../backend/game/dto/StartGameMessage.java | 5 +++ .../backend/lobby/services/LobbyService.java | 6 ++++ 5 files changed, 56 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index d535871f..4bd27b5e 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -14,6 +14,12 @@ import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; +/** + * WebSocket controller for real-time gameplay interactions. + * + *

Handles game-related WebSocket requests such as starting games, submitting clues, and + * revealing cards. Broadcasts updated game state information to subscribed clients. + */ public class GameSocketController { private final LobbyService lobbyService; @@ -23,6 +29,14 @@ public class GameSocketController { private final Map gameSessions = new ConcurrentHashMap<>(); + /** + * Creates a new {@code GameSocketController}. + * + * @param lobbyService service for lobby management + * @param messagingTemplate template used for broadcasting messages + * @param cardGenerator utility for generating game cards + * @param clueValidationService service for validating clues + */ public GameSocketController( LobbyService lobbyService, SimpMessagingTemplate messagingTemplate, @@ -34,6 +48,13 @@ public GameSocketController( this.clueValidationService = clueValidationService; } + /** + * Starts a new game session for a lobby. + * + *

Creates a new game manager and broadcasts the initial board state to all subscribed players. + * + * @param message the start game request + */ @MessageMapping("/start-game") public void startGame(StartGameMessage message) { @@ -47,6 +68,13 @@ public void startGame(StartGameMessage message) { "/topic/game/" + message.getLobbyCode(), gameManager.getCardList()); } + /** + * Reveals a card on the board. + * + *

Updates the game state and broadcasts the updated board to all subscribed players. + * + * @param message the reveal card request + */ @MessageMapping("/reveal-card") public void revealCard(RevealCardMessage message) { @@ -62,6 +90,13 @@ public void revealCard(RevealCardMessage message) { "/topic/game/" + message.getLobbyCode(), gameManager.getCardList()); } + /** + * Submits a clue for the current turn. + * + *

Updates the current clue and broadcasts the updated game state to all subscribed players. + * + * @param message the clue submission request + */ @MessageMapping("/submit-clue") public void submitClue(ClueMessage message) { diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java index 7b1e8113..466c95c7 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java @@ -3,6 +3,11 @@ import lombok.Getter; import lombok.Setter; +/** + * WebSocket message for submitting a clue. + * + *

Contains the clue word and the allowed amount of guesses. + */ @Getter @Setter public class ClueMessage { diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java index 21a30fb1..d31b1729 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java @@ -4,6 +4,11 @@ import lombok.Getter; import lombok.Setter; +/** + * WebSocket message for revealing a card on the board. + * + *

Contains the lobby code, selected card position, and the current team's turn color. + */ @Getter @Setter public class RevealCardMessage { diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java index eb88ca4b..e8d846aa 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/dto/StartGameMessage.java @@ -3,6 +3,11 @@ import lombok.Getter; import lombok.Setter; +/** + * WebSocket message for starting a game session. + * + *

Contains the lobby code of the game that should be started. + */ @Getter @Setter public class StartGameMessage { diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index aeb828fa..6894550c 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -184,6 +184,12 @@ public Role getPlayerRole(String username, String lobbyCode) { return null; } + /** + * Determines the starting team for a lobby. + * + * @param lobbyCode the lobby code + * @return the randomly selected starting team, or null if the lobby does not exist + */ public Team decideStartingTeam(String lobbyCode) { Lobby lobby = lobbyList.get(lobbyCode); From 80ee9a45e0e484ac2e2fcef42060f7cba2ca646e Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 11 May 2026 01:22:17 +0200 Subject: [PATCH 017/207] style: fix imports --- .../controller/GameSocketControllerTest.java | 21 +++++++++---------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 4e753eb8..84d23c92 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -1,5 +1,14 @@ package com.codenames.codenames.backend.game.controller; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.game.dto.ClueMessage; import com.codenames.codenames.backend.game.dto.RevealCardMessage; @@ -9,6 +18,7 @@ import com.codenames.codenames.backend.playingfield.CardGenerator; import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Team; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -16,17 +26,6 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.messaging.simp.SimpMessagingTemplate; -import java.util.List; - -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - @ExtendWith(MockitoExtension.class) class GameSocketControllerTest { @Mock private LobbyService lobbyService; From dfdfff6892970027630655a50cd67f45462f0339 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 11 May 2026 18:58:23 +0200 Subject: [PATCH 018/207] refactor: extract websocket game topic constant --- .../backend/game/controller/GameSocketController.java | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 4bd27b5e..f26c192b 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -29,6 +29,8 @@ public class GameSocketController { private final Map gameSessions = new ConcurrentHashMap<>(); + private static final String GAME_TOPIC_PREFIX = "/topic/game/"; + /** * Creates a new {@code GameSocketController}. * @@ -65,7 +67,7 @@ public void startGame(StartGameMessage message) { gameSessions.put(message.getLobbyCode(), gameManager); messagingTemplate.convertAndSend( - "/topic/game/" + message.getLobbyCode(), gameManager.getCardList()); + GAME_TOPIC_PREFIX + message.getLobbyCode(), gameManager.getCardList()); } /** @@ -87,7 +89,7 @@ public void revealCard(RevealCardMessage message) { gameManager.flipCard(message.getPosition(), message.getCurrentTurn()); messagingTemplate.convertAndSend( - "/topic/game/" + message.getLobbyCode(), gameManager.getCardList()); + GAME_TOPIC_PREFIX + message.getLobbyCode(), gameManager.getCardList()); } /** @@ -110,6 +112,6 @@ public void submitClue(ClueMessage message) { gameManager.submitClue(clue); - messagingTemplate.convertAndSend("/topic/game/" + message.getLobbyCode(), gameManager); + messagingTemplate.convertAndSend(GAME_TOPIC_PREFIX + message.getLobbyCode(), gameManager); } } From 869a89f1f1717525a408a129af9b7f7b935b0e23 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 11 May 2026 19:01:04 +0200 Subject: [PATCH 019/207] refactor: clean up websocket gameplay controller --- .../codenames/backend/game/controller/GameSocketController.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index f26c192b..96140d20 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -13,6 +13,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.stereotype.Controller; /** * WebSocket controller for real-time gameplay interactions. @@ -20,6 +21,7 @@ *

Handles game-related WebSocket requests such as starting games, submitting clues, and * revealing cards. Broadcasts updated game state information to subscribed clients. */ +@Controller public class GameSocketController { private final LobbyService lobbyService; From 62a7fe3dc8f8f4923373dfbaadaef478766b90de Mon Sep 17 00:00:00 2001 From: XtophB Date: Tue, 12 May 2026 22:53:22 +0200 Subject: [PATCH 020/207] refactor: change incorrect use of enum for turn and winner --- .../backend/playingfield/GameManager.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index db103ea2..6b84ca3c 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -24,7 +24,7 @@ public class GameManager { private final Board board; @Getter private int currentRedFound = 0; @Getter private int currentBlueFound = 0; - private Color winner; + private Team winner; private final ClueValidationService clueValidationService; @Getter private Clue currentClue; @@ -79,7 +79,7 @@ public Color checkColor(int position) { * @param cardColor the color of the card * @param currentTurn the current team's turn */ - private void updateScore(Color cardColor, Color currentTurn) { + private void updateScore(Color cardColor, Team currentTurn) { switch (cardColor) { case RED: currentRedFound++; @@ -88,10 +88,10 @@ private void updateScore(Color cardColor, Color currentTurn) { currentBlueFound++; break; case BLACK: - if (currentTurn == Color.RED) { - this.winner = Color.BLUE; + if (currentTurn == Team.RED) { + this.winner = Team.BLUE; } else { - this.winner = Color.RED; + this.winner = Team.RED; } break; default: @@ -104,15 +104,15 @@ private void updateScore(Color cardColor, Color currentTurn) { * * @return the winning color is returned or null if no team has won */ - public Color getWinner() { + public Team getWinner() { if (this.winner != null) { return this.winner; } if (currentRedFound >= redCards) { - return Color.RED; + return Team.RED; } if (currentBlueFound >= blueCards) { - return Color.BLUE; + return Team.BLUE; } return null; } @@ -124,7 +124,7 @@ public Color getWinner() { * @param currentTurn the team whose turn it currently is * @throws IllegalStateException if game over, card already flipped, no more guesses */ - public void flipCard(int position, Color currentTurn) { + public void flipCard(int position, Team currentTurn) { if (getWinner() != null) { throw new IllegalStateException("Winner is already set"); } From 5142885a50a6a9f959c47404de71df58d3de7c3f Mon Sep 17 00:00:00 2001 From: XtophB Date: Tue, 12 May 2026 23:08:14 +0200 Subject: [PATCH 021/207] refactor: change incorrect use of enum for turn and winner in test class --- .../backend/playingfield/GameManagerTest.java | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 7ad244f5..bda9c086 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -28,6 +28,8 @@ class GameManagerTest { private static final int SECOND_TEAM_CARDS = 8; private static final int WHITE_CARDS = 7; private static final int BLACK_CARDS = 1; + private static final Team redTeam = Team.RED; + private static final Team blueTeam = Team.BLUE; private GameManager gameManager; private CardGenerator mockCardGenerator; private ClueValidationService mockClueValidationService; @@ -39,7 +41,7 @@ void setUp() { mockCardGenerator = mock(CardGenerator.class); mockClueValidationService = mock(ClueValidationService.class); mockCardGeneration(List.of(new Card("Test", Color.RED))); - gameManager = new GameManager(Team.RED, mockCardGenerator, mockClueValidationService); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); when(mockClueValidationService.validateWord(any(), anyString())).thenReturn(true); } @@ -58,7 +60,7 @@ void testConstructorRedStarts() { @Test void testConstructorBlueStarts() { - new GameManager(Team.BLUE, mockCardGenerator, mockClueValidationService); + new GameManager(blueTeam, mockCardGenerator, mockClueValidationService); verify(mockCardGenerator, times(1)) .generateCards( TOTAL_CARDS, SECOND_TEAM_CARDS, STARTING_TEAM_CARDS, WHITE_CARDS, BLACK_CARDS); @@ -106,85 +108,85 @@ private void helperMethodSubmitClue(GameManager gameManager, int guessAmount) { @Test void testGetWinner_redStartsRedWins() { - gameManager = helperMethodGenerateFullCardList(Color.RED, Team.RED); + gameManager = helperMethodGenerateFullCardList(Color.RED, redTeam); for (int i = 0; i < 9; i++) { - gameManager.flipCard(i, Color.RED); + gameManager.flipCard(i, redTeam); } - assertEquals(Color.RED, gameManager.getWinner()); + assertEquals(redTeam, gameManager.getWinner()); } @Test void testGetWinner_redStartsBlueWins() { - gameManager = helperMethodGenerateFullCardList(Color.BLUE, Team.RED); + gameManager = helperMethodGenerateFullCardList(Color.BLUE, redTeam); for (int i = 0; i < 8; i++) { - gameManager.flipCard(i, Color.BLUE); + gameManager.flipCard(i, blueTeam); } - assertEquals(Color.BLUE, gameManager.getWinner()); + assertEquals(blueTeam, gameManager.getWinner()); } @Test void testGetWinner_blueStartsRedWins() { - gameManager = helperMethodGenerateFullCardList(Color.RED, Team.BLUE); + gameManager = helperMethodGenerateFullCardList(Color.RED, blueTeam); for (int i = 0; i < 8; i++) { - gameManager.flipCard(i, Color.RED); + gameManager.flipCard(i, redTeam); } - assertEquals(Color.RED, gameManager.getWinner()); + assertEquals(redTeam, gameManager.getWinner()); } @Test void testGetWinner_blueStartsBlueWins() { - gameManager = helperMethodGenerateFullCardList(Color.BLUE, Team.BLUE); + gameManager = helperMethodGenerateFullCardList(Color.BLUE, blueTeam); for (int i = 0; i < 9; i++) { - gameManager.flipCard(i, Color.BLUE); + gameManager.flipCard(i, blueTeam); } - assertEquals(Color.BLUE, gameManager.getWinner()); + assertEquals(blueTeam, gameManager.getWinner()); } @Test void testGetWinner_redFoundBlackCardFound() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); - gameManager = new GameManager(Team.RED, mockCardGenerator, mockClueValidationService); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1); - gameManager.flipCard(0, Color.RED); - assertEquals(Color.BLUE, gameManager.getWinner()); + gameManager.flipCard(0, redTeam); + assertEquals(blueTeam, gameManager.getWinner()); } @Test void testGetWinner_blueFoundBlackCardFound() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); - gameManager = new GameManager(Team.RED, mockCardGenerator, mockClueValidationService); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1); - gameManager.flipCard(0, Color.BLUE); - assertEquals(Color.RED, gameManager.getWinner()); + gameManager.flipCard(0, blueTeam); + assertEquals(redTeam, gameManager.getWinner()); } @Test void testFlipWhiteCard() { mockCardGeneration(List.of(new Card("Test", Color.WHITE))); - gameManager = new GameManager(Team.RED, mockCardGenerator, mockClueValidationService); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1); - gameManager.flipCard(0, Color.BLUE); + gameManager.flipCard(0, blueTeam); assertNull(gameManager.getWinner()); } @Test void testFlipCard_cardAlreadyFlipped() { helperMethodSubmitClue(gameManager, 1); - gameManager.flipCard(0, Color.RED); - assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, Color.RED)); + gameManager.flipCard(0, redTeam); + assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, redTeam)); } @Test void testFlipCard_winnerAlreadyDetermined() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); - gameManager = new GameManager(Team.RED, mockCardGenerator, mockClueValidationService); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1); - gameManager.flipCard(0, Color.BLUE); - assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, Color.RED)); + gameManager.flipCard(0, blueTeam); + assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, redTeam)); } @Test @@ -210,10 +212,10 @@ void testSubmitClue() { @Test void testOutOfGuesses() { mockCardGeneration(List.of(new Card("Test", Color.RED), new Card("Test2", Color.RED))); - gameManager = new GameManager(Team.RED, mockCardGenerator, mockClueValidationService); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 0); - gameManager.flipCard(0, Color.RED); - assertThrows(IllegalStateException.class, () -> gameManager.flipCard(1, Color.RED)); + gameManager.flipCard(0, redTeam); + assertThrows(IllegalStateException.class, () -> gameManager.flipCard(1, redTeam)); } @Test From c15de8cca3a57d42acae1206c791c9f33d4cd2bf Mon Sep 17 00:00:00 2001 From: XtophB Date: Tue, 12 May 2026 23:08:38 +0200 Subject: [PATCH 022/207] refactor: change parameter type to Team --- .../backend/serialization/DataTransferObjectService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java index 1cbec083..aec55410 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java @@ -3,6 +3,7 @@ import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.utility.Role; +import com.codenames.codenames.backend.utility.Team; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Service; @@ -38,7 +39,7 @@ private CardDataTransferObject createCardDataTransferObject(Card card, Role role * @return a DTO of the current game state */ public GameStateDataTransferObject createGameStateDataTransferObject( - GameManager gameManager, Role role, String currentTurn) { + GameManager gameManager, Role role, Team currentTurn) { List cardList = gameManager.getCardList(); List cardDataTransferObject = new ArrayList<>(); @@ -53,7 +54,7 @@ public GameStateDataTransferObject createGameStateDataTransferObject( } return new GameStateDataTransferObject( winner, - currentTurn, + currentTurn.toString(), gameManager.getCurrentRedFound(), gameManager.getCurrentBlueFound(), gameManager.getCurrentClueWord(), From b276bcccfe3a94435a68f57a1e2a2f0809c0e884 Mon Sep 17 00:00:00 2001 From: XtophB Date: Tue, 12 May 2026 23:09:34 +0200 Subject: [PATCH 023/207] refactor: change String to Team enum --- .../DataTransferObjectServiceTest.java | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 389988a8..653f725c 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -9,6 +9,7 @@ import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Role; +import com.codenames.codenames.backend.utility.Team; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -19,6 +20,8 @@ class DataTransferObjectServiceTest { GameManager mockGameManager; DataTransferObjectService service; GameStateDataTransferObject gameStateDto; + private static final Team redTeam = Team.RED; + // private static final Team blueTeam = Team.BLUE; @BeforeEach void setUp() { @@ -29,36 +32,33 @@ void setUp() { mockGameManager = mock(GameManager.class); service = new DataTransferObjectService(); when(mockGameManager.getCardList()).thenReturn(List.of(cardHidden, cardGuessed)); - when(mockGameManager.getWinner()).thenReturn(Color.RED); + when(mockGameManager.getWinner()).thenReturn(Team.RED); when(mockGameManager.getCurrentRedFound()).thenReturn(0); when(mockGameManager.getCurrentBlueFound()).thenReturn(0); + + gameStateDto = + service.createGameStateDataTransferObject(mockGameManager, Role.OPERATIVE, redTeam); } @Test void testSpymasterVisibility() { gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.SPYMASTER, "RED"); + service.createGameStateDataTransferObject(mockGameManager, Role.SPYMASTER, redTeam); assertEquals("RED", gameStateDto.cardList().get(0).color()); } @Test void testOperatorVisibility_hidden() { - gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.OPERATIVE, "RED"); assertEquals("HIDDEN", gameStateDto.cardList().get(0).color()); } @Test void testOperatorVisibility_isGuessed() { - gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.OPERATIVE, "RED"); assertEquals("RED", gameStateDto.cardList().get(1).color()); } @Test void testGetWinner_exists() { - gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.OPERATIVE, "RED"); assertEquals("RED", gameStateDto.winner()); } @@ -66,7 +66,7 @@ void testGetWinner_exists() { void testGetWinner_null() { when(mockGameManager.getWinner()).thenReturn(null); gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.OPERATIVE, "RED"); + service.createGameStateDataTransferObject(mockGameManager, Role.OPERATIVE, redTeam); assertNull(gameStateDto.winner()); } } From 6bd03c370154eb97b9654560150f965f97dcb3f9 Mon Sep 17 00:00:00 2001 From: XtophB Date: Tue, 12 May 2026 23:23:55 +0200 Subject: [PATCH 024/207] refactor: remove unused variable --- .../backend/serialization/DataTransferObjectServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 653f725c..50001326 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -21,7 +21,6 @@ class DataTransferObjectServiceTest { DataTransferObjectService service; GameStateDataTransferObject gameStateDto; private static final Team redTeam = Team.RED; - // private static final Team blueTeam = Team.BLUE; @BeforeEach void setUp() { From a306ffec0a92c7914a980136cb555ccef008e910 Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 13 May 2026 13:23:07 +0200 Subject: [PATCH 025/207] change lobby response to have a parameter playerList for frontend communication, add boolean isHost to Player class --- .../codenames/backend/lobby/Lobby.java | 28 +++++++++++++------ .../lobby/controller/LobbyController.java | 20 +++++++------ .../backend/lobby/dto/LobbyResponse.java | 17 +++++------ .../backend/lobby/services/LobbyService.java | 12 ++++---- .../backend/websocket/GameController.java | 2 +- .../codenames/backend/websocket/Player.java | 10 ++----- .../websocket/WebSocketEventListener.java | 4 +-- src/main/resources/application.yaml | 2 +- .../codenames/backend/lobby/LobbyTest.java | 8 +++--- .../lobby/services/LobbyServiceTest.java | 6 ++-- .../backend/websocket/GameControllerTest.java | 4 +-- .../backend/websocket/PlayerTest.java | 4 +-- 12 files changed, 62 insertions(+), 55 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java index 21d885b4..96b64bc7 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java @@ -11,9 +11,9 @@ import lombok.Getter; /** - * Represents a game lobby containing a limited number of players. + * Represents a game lobby containing a limited number of playerList. * - *

Supports adding and removing players while enforcing constraints such as maximum player count + *

Supports adding and removing playerList while enforcing constraints such as maximum player count * and unique usernames. */ @Getter @@ -44,33 +44,45 @@ public Lobby(String lobbyCode, String username) { this.lobbyCode = lobbyCode; this.playerTeams = new HashMap<>(); this.playerRoles = new HashMap<>(); - this.addPlayer(username); + this.addPlayer(username, true); } /** * Adds a player to the lobby if capacity allows and the username is unique. * * @param username the username of the player + * @param isHost whether the player is the host of the lobby * @return {@code true} if the player was added, {@code false} otherwise */ - public boolean addPlayer(String username) { - boolean alreadyExists = playerList.stream().anyMatch(p -> p.getUsername().equals(username)); + public boolean addPlayer(String username, boolean isHost) { + boolean alreadyExists = playerList.stream().anyMatch(p -> p.username().equals(username)); if (alreadyExists || playerList.size() >= MAX_PLAYERS) { return false; } - playerList.add(new Player(username)); + playerList.add(new Player(username, isHost)); return true; } + /** + * Adds a player to the lobby if capacity allows and the username is unique. + * + * @param username the username of the player + * @calls {@link #addPlayer(String, boolean)} with {@code false} as the second argument + * @return {@code true} if the player was added, {@code false} otherwise + */ + public boolean addPlayer(String username) { + return addPlayer(username, false); + } + /** * Removes a player from the lobby. * * @param username the username of the player to remove */ public void removePlayer(String username) { - playerList.removeIf(p -> p.getUsername().equals(username)); + playerList.removeIf(p -> p.username().equals(username)); this.playerTeams.remove(username); this.playerRoles.remove(username); } @@ -82,7 +94,7 @@ public void removePlayer(String username) { * @return {@code true} if the player exists in the lobby, {@code false} otherwise */ public boolean hasPlayer(String username) { - return playerList.stream().anyMatch(p -> p.getUsername().equals(username)); + return playerList.stream().anyMatch(p -> p.username().equals(username)); } /** diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 201c3ffd..19672bb1 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -3,6 +3,7 @@ import com.codenames.codenames.backend.lobby.dto.LobbyResponse; import com.codenames.codenames.backend.lobby.dto.PositionSelectMessage; import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.websocket.Player; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -10,6 +11,8 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import java.util.List; + /** * REST controller for handling lobby management operations. * @@ -44,9 +47,10 @@ public ResponseEntity createLobby(@RequestParam String username) String lobbyCode = service.createLobby(username); if (lobbyCode == null || lobbyCode.isBlank()) { return ResponseEntity.internalServerError() - .body(new LobbyResponse("Error while creating lobby.", "")); + .body(new LobbyResponse("Error while creating lobby.", "", null)); } else { - return ResponseEntity.ok(new LobbyResponse("Successfully created Lobby.", lobbyCode)); + List players = service.getPlayers(lobbyCode); + return ResponseEntity.ok(new LobbyResponse("Successfully created Lobby.", lobbyCode, players)); } } @@ -62,10 +66,10 @@ public ResponseEntity joinLobby( @RequestParam String username, @RequestParam String lobbyCode) { boolean joined = service.joinLobby(username, lobbyCode); if (joined) { - return ResponseEntity.ok(new LobbyResponse("Joined Lobby successfully.", lobbyCode)); + return ResponseEntity.ok(new LobbyResponse("Joined Lobby successfully.", lobbyCode, service.getPlayers(lobbyCode))); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse("Could not find lobby.", lobbyCode)); + .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); } } @@ -81,10 +85,10 @@ public ResponseEntity leaveLobby( @RequestParam String username, @RequestParam String lobbyCode) { boolean left = service.leaveLobby(username, lobbyCode); if (left) { - return ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode)); + return ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode, service.getPlayers(lobbyCode))); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse("Could not find lobby.", lobbyCode)); + .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); } } @@ -107,11 +111,11 @@ public ResponseEntity selectPosition( if (updated) { return ResponseEntity.ok( - new LobbyResponse("Position selected successfully.", request.getLobbyCode()) + new LobbyResponse("Position selected successfully.", request.getLobbyCode(), service.getPlayers(request.getLobbyCode())) ); } else { return ResponseEntity.badRequest().body( - new LobbyResponse("Could not assign selected team/role.", request.getLobbyCode()) + new LobbyResponse("Could not assign selected team/role.", request.getLobbyCode(), service.getPlayers(request.getLobbyCode())) ); } } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java b/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java index 1f8feb30..e4118c35 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java @@ -1,25 +1,22 @@ package com.codenames.codenames.backend.lobby.dto; -import lombok.Getter; +import com.codenames.codenames.backend.websocket.Player; + +import java.util.List; /** * Data transfer object representing the result of a lobby operation. * *

Contains a message describing the outcome and the associated lobby code. */ -@Getter -public class LobbyResponse { - private final String message; - private final String lobbyCode; - +public record LobbyResponse(String message, String lobbyCode, List playerList) { /** * Creates a new lobby response. * - * @param message the message describing the result of the operation + * @param message the message describing the result of the operation * @param lobbyCode the associated lobby code + * @param playerList the list of playerList currently in the lobby */ - public LobbyResponse(String message, String lobbyCode) { - this.lobbyCode = lobbyCode; - this.message = message; + public LobbyResponse { } } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 0af52802..d39b0b3a 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -4,6 +4,7 @@ import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; + import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -41,7 +42,6 @@ public String createLobby(String username) { if (lobbyCode == null || lobbyCode.isBlank()) { return null; } - Lobby lobby = new Lobby(lobbyCode, username); lobbyList.put(lobbyCode, lobby); return lobbyCode; @@ -104,10 +104,10 @@ public boolean selectPosition(String username, String lobbyCode, Team team, Role } /** - * Retrieves all players in the specified lobby. + * Retrieves all playerList in the specified lobby. * * @param lobbyCode the lobby code identifying the lobby - * @return a list of players, or an empty list if the lobby does not exist + * @return a list of playerList, or an empty list if the lobby does not exist */ public List getPlayers(String lobbyCode) { Lobby lobby = lobbyList.get(lobbyCode); @@ -124,9 +124,9 @@ public List getPlayers(String lobbyCode) { */ private boolean isSpymasterAlreadyAssigned(Lobby lobby, String username, Team team) { for (Player player : lobby.getPlayerList()) { - if (!player.getUsername().equals(username) - && lobby.getPlayerTeam(player.getUsername()) == team - && lobby.getPlayerRole(player.getUsername()) == Role.SPYMASTER) { + if (!player.username().equals(username) + && lobby.getPlayerTeam(player.username()) == team + && lobby.getPlayerRole(player.username()) == Role.SPYMASTER) { return true; } } diff --git a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java index 6fe3fc9a..d2b8c535 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java @@ -76,7 +76,7 @@ public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) * @param code the lobby code identifying the lobby */ private void sendPlayerUpdate(String code) { - List players = lobbyService.getPlayers(code).stream().map(Player::getUsername).toList(); + List players = lobbyService.getPlayers(code).stream().map(Player::username).toList(); messagingTemplate.convertAndSend("/topic/lobby/" + code, players); } diff --git a/src/main/java/com/codenames/codenames/backend/websocket/Player.java b/src/main/java/com/codenames/codenames/backend/websocket/Player.java index 5acedb38..ddce6bc8 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/Player.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/Player.java @@ -1,22 +1,16 @@ package com.codenames.codenames.backend.websocket; -import lombok.Getter; - /** * Represents a player connected to the system. * *

A player is identified by a username and may be associated with a WebSocket session. */ -@Getter -public class Player { - private final String username; - +public record Player(String username, boolean isHost) { /** * Creates a new player. * * @param username the player's username */ - public Player(String username) { - this.username = username; + public Player { } } diff --git a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java b/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java index 1389ae21..496d6135 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java @@ -10,7 +10,7 @@ /** * Listener for WebSocket lifecycle events. * - *

Handles client disconnections by removing players from lobbies, cleaning up session mappings, + *

Handles client disconnections by removing playerList from lobbies, cleaning up session mappings, * and notifying remaining clients. */ @Component @@ -59,7 +59,7 @@ public void handleDisconnect(SessionDisconnectEvent event) { sessionRegistry.remove(sessionId); List players = - lobbyService.getPlayers(lobbyCode).stream().map(Player::getUsername).toList(); + lobbyService.getPlayers(lobbyCode).stream().map(Player::username).toList(); messagingTemplate.convertAndSend("/topic/lobby/" + lobbyCode, players); } diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index 391e93fe..d1f130a3 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -3,4 +3,4 @@ spring: name: Codenames_Backend app: - allowed-origins: "http://localhost:8080,http://10.0.2.2:8080" \ No newline at end of file + allowed-origins: "*" \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/lobby/LobbyTest.java b/src/test/java/com/codenames/codenames/backend/lobby/LobbyTest.java index e5f36d19..7c28cc89 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/LobbyTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/LobbyTest.java @@ -23,7 +23,7 @@ void constructorShouldInitializeLobbyCorrectly() { assertEquals("ABCDE", lobby.getLobbyCode()); assertEquals(1, lobby.getPlayerList().size()); - assertTrue(lobby.getPlayerList().stream().anyMatch(p -> p.getUsername().equals("Host"))); + assertTrue(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("Host"))); } @Test @@ -33,7 +33,7 @@ void addPlayerShouldAddPlayer() { lobby.addPlayer("P1"); assertEquals(2, lobby.getPlayerList().size()); - assertTrue(lobby.getPlayerList().stream().anyMatch(p -> p.getUsername().equals("P1"))); + assertTrue(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("P1"))); } @Test @@ -55,7 +55,7 @@ void removePlayerShouldRemovePlayer() { lobby.addPlayer("P1"); lobby.removePlayer("P1"); - assertFalse(lobby.getPlayerList().stream().anyMatch(p -> p.getUsername().equals("P1"))); + assertFalse(lobby.getPlayerList().stream().anyMatch(p -> p.username().equals("P1"))); } @Test @@ -77,7 +77,7 @@ void addPlayerShouldNotAddDuplicatePlayer() { assertTrue(first); assertFalse(second); - long count = lobby.getPlayerList().stream().filter(p -> p.getUsername().equals("Max")).count(); + long count = lobby.getPlayerList().stream().filter(p -> p.username().equals("Max")).count(); assertEquals(1, count); } diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index 43059b19..ef61e579 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -41,7 +41,7 @@ void createLobbyReturnLobbyCode() { assertTrue(result); List players = lobbyService.getPlayers("ABCDE"); - assertTrue(players.stream().anyMatch(p -> p.getUsername().equals("TestUser"))); + assertTrue(players.stream().anyMatch(p -> p.username().equals("TestUser"))); } @Test @@ -76,7 +76,7 @@ void leaveLobbyReturnTrueLobbyExists() { List players = lobbyService.getPlayers("ABCDE"); - assertFalse(players.stream().anyMatch(p -> p.getUsername().equals("Host"))); + assertFalse(players.stream().anyMatch(p -> p.username().equals("Host"))); } @Test @@ -180,7 +180,7 @@ void joinLobbyShouldReturnFalseWhenPlayerAlreadyExists() { List players = lobbyService.getPlayers("ABCDE"); long count = players.stream() - .filter(p -> p.getUsername().equals("Max")) + .filter(p -> p.username().equals("Max")) .count(); assertEquals(1, count); diff --git a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java index 3818cfe2..a9ffcca8 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java @@ -53,7 +53,7 @@ void shouldRegisterJoinAndRegisterSession() { when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(true); - when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max"))); + when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); controller.join(msg, accessor); @@ -116,7 +116,7 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { accessor.setSessionAttributes(attrs); when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(true); - when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max"))); + when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); controller.join(msg, accessor); diff --git a/src/test/java/com/codenames/codenames/backend/websocket/PlayerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/PlayerTest.java index 1fad9b66..33f1b002 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/PlayerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/PlayerTest.java @@ -8,8 +8,8 @@ class PlayerTest { @Test void shouldReturnUsername() { - Player player = new Player("Max"); + Player player = new Player("Max", false); - assertEquals("Max", player.getUsername()); + assertEquals("Max", player.username()); } } From 5f75c21f02d53a5dcebd3c9ec540d1b834b8eff0 Mon Sep 17 00:00:00 2001 From: XtophB Date: Wed, 13 May 2026 14:06:09 +0200 Subject: [PATCH 026/207] refactor: use Team datatype instead of string in DTO --- .../backend/serialization/DataTransferObjectService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java index aec55410..df3e594a 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java @@ -46,15 +46,15 @@ public GameStateDataTransferObject createGameStateDataTransferObject( for (Card card : cardList) { cardDataTransferObject.add(createCardDataTransferObject(card, role)); } - String winner; + Team winner; if (gameManager.getWinner() == null) { winner = null; } else { - winner = gameManager.getWinner().toString(); + winner = gameManager.getWinner(); } return new GameStateDataTransferObject( winner, - currentTurn.toString(), + currentTurn, gameManager.getCurrentRedFound(), gameManager.getCurrentBlueFound(), gameManager.getCurrentClueWord(), From 59eb6a7a6db84925452e1c4e8953ae450adb3891 Mon Sep 17 00:00:00 2001 From: XtophB Date: Wed, 13 May 2026 14:06:52 +0200 Subject: [PATCH 027/207] refactor: use Team data type instead of String as payload --- .../backend/serialization/GameStateDataTransferObject.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java index 16b03f3f..3353415a 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.serialization; +import com.codenames.codenames.backend.utility.Team; import java.util.List; /** @@ -14,8 +15,8 @@ * @param cardList the cards on the board */ public record GameStateDataTransferObject( - String winner, - String currentTurn, + Team winner, + Team currentTurn, int currentRedFound, int currentBlueFound, String currentClue, From e6ec8474300131b4bd1847ac3f12bfbef115e1ff Mon Sep 17 00:00:00 2001 From: XtophB Date: Wed, 13 May 2026 14:07:43 +0200 Subject: [PATCH 028/207] refactor: change fix mismatched test - use Team instead of String --- .../backend/serialization/DataTransferObjectServiceTest.java | 2 +- .../codenames/backend/serialization/SerializationJsonTest.java | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 50001326..57e5003a 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -58,7 +58,7 @@ void testOperatorVisibility_isGuessed() { @Test void testGetWinner_exists() { - assertEquals("RED", gameStateDto.winner()); + assertEquals(redTeam, gameStateDto.winner()); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index 4d096039..003b61b6 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -8,6 +8,7 @@ import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.utility.Team; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.List; @@ -28,7 +29,7 @@ void setUp() { serializer = new SerializationJson(mapper); dummyList = List.of(new CardDataTransferObject("TEST", "HIDDEN", false)); - dummyGameState = new GameStateDataTransferObject("RED", "RED", 0, 0, "Test", 1, dummyList); + dummyGameState = new GameStateDataTransferObject(Team.RED, Team.RED, 0, 0, "Test", 1, dummyList); } @Test From a58a119db8916b9799e2ab379a063b8f87afc69d Mon Sep 17 00:00:00 2001 From: XtophB Date: Wed, 13 May 2026 14:08:30 +0200 Subject: [PATCH 029/207] refactor: split line - too long for google code style --- .../codenames/backend/serialization/SerializationJsonTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index 003b61b6..286ea635 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -29,7 +29,8 @@ void setUp() { serializer = new SerializationJson(mapper); dummyList = List.of(new CardDataTransferObject("TEST", "HIDDEN", false)); - dummyGameState = new GameStateDataTransferObject(Team.RED, Team.RED, 0, 0, "Test", 1, dummyList); + dummyGameState = + new GameStateDataTransferObject(Team.RED, Team.RED, 0, 0, "Test", 1, dummyList); } @Test From 93ab424d37773473de0c351b45efed987623855a Mon Sep 17 00:00:00 2001 From: XtophB Date: Wed, 13 May 2026 18:50:42 +0200 Subject: [PATCH 030/207] feat: add method to advance the current turn and role --- .../backend/playingfield/GameManager.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index 6b84ca3c..5c26588c 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -3,6 +3,7 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.List; import lombok.Getter; @@ -30,6 +31,9 @@ public class GameManager { @Getter private Clue currentClue; @Getter private int remainingGuesses; + @Getter private Team currentTurn; + @Getter private Role currentPhase; + /** * Constructor for a new GameManager and initializes the playing board. * @@ -42,7 +46,11 @@ public GameManager( if (startingTeam == null) { throw new IllegalArgumentException("startingTeam cannot be null"); } + this.currentTurn = startingTeam; + this.currentPhase = Role.SPYMASTER; // we hard code spymaster since game has to start with them + this.clueValidationService = clueValidationService; + if (startingTeam == Team.RED) { this.redCards = 9; this.blueCards = 8; @@ -50,6 +58,7 @@ public GameManager( this.redCards = 8; this.blueCards = 9; } + this.board = new Board(cardGenerator, TOTAL_CARDS, redCards, blueCards, WHITE_CARDS, BLACK_CARDS); } @@ -173,4 +182,24 @@ public String getCurrentClueWord() { } return currentClue.word(); } + + public Team nextTeamColor(Team current) { + if (current == Team.RED) { + return Team.BLUE; + } else { + return Team.RED; + } + } + + // we do not change the team color when we advance after being spymaster as the same team + // operatives are now at turn, only after operatives are done we clear clue and change team color. + public void advanceTurn() { + if (currentPhase == Role.SPYMASTER) { + currentPhase = Role.OPERATIVE; + } else { + currentPhase = Role.SPYMASTER; + currentTurn = nextTeamColor(currentTurn); + clearClue(); + } + } } From 55666ac45bdfa028cd58dbeb3b5db690e205eb9b Mon Sep 17 00:00:00 2001 From: XtophB Date: Wed, 13 May 2026 19:06:22 +0200 Subject: [PATCH 031/207] feat: add a check for correct turn or role --- .../codenames/backend/playingfield/GameManager.java | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index 5c26588c..ad3dab5d 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -130,10 +130,9 @@ public Team getWinner() { * Changes the guessed state of a card and updates the score if necessary. * * @param position the position of the card that is selected by the player - * @param currentTurn the team whose turn it currently is * @throws IllegalStateException if game over, card already flipped, no more guesses */ - public void flipCard(int position, Team currentTurn) { + public void flipCard(int position) { if (getWinner() != null) { throw new IllegalStateException("Winner is already set"); } @@ -144,6 +143,7 @@ public void flipCard(int position, Team currentTurn) { clearClue(); throw new IllegalStateException("No more guesses."); } + checkCorrectTurn(currentTurn, currentPhase); this.remainingGuesses--; this.board.setGuessed(position); Color currentColor = this.board.checkColor(position); @@ -157,6 +157,7 @@ public void flipCard(int position, Team currentTurn) { * @throws IllegalArgumentException if clue is: null, empty, spaces, or word is on the board */ public void submitClue(Clue clue) { + checkCorrectTurn(currentTurn, currentPhase); if (clueValidationService.validateWord(this.board, clue.word())) { this.currentClue = clue; this.remainingGuesses = clue.guessAmount(); @@ -202,4 +203,10 @@ public void advanceTurn() { clearClue(); } } + + private void checkCorrectTurn(Team team, Role role) { + if (team != currentTurn || role != currentPhase) { + throw new IllegalStateException("Not your turn/ role"); + } + } } From 124dc3b5b138643a69b616bf2c7aa1db0b5f26e5 Mon Sep 17 00:00:00 2001 From: XtophB Date: Wed, 13 May 2026 19:06:42 +0200 Subject: [PATCH 032/207] add methods to be called by endpoints to access GameManager methods --- .../backend/playingfield/GameService.java | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/playingfield/GameService.java diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java new file mode 100644 index 00000000..45719077 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -0,0 +1,23 @@ +package com.codenames.codenames.backend.playingfield; + +import com.codenames.codenames.backend.clue.Clue; + +public class GameService { + + public void submitClue(GameManager gm, Clue clue) { + gm.submitClue(clue); + gm.advanceTurn(); + } + + public void flipCard(GameManager gm, int position) { + gm.flipCard(position); + if (gm.getRemainingGuesses() == 0) { + gm.advanceTurn(); + } + } + + public void endTurn(GameManager gm) { + gm.advanceTurn(); + } + +} From 3d37b21d1248e74368dab06ad0b8976012e9bc6f Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 13 May 2026 20:01:31 +0200 Subject: [PATCH 033/207] added endpoint for lobby updates and new dto for player list transfer with roles included --- .../lobby/controller/LobbyController.java | 49 +++++++++++-------- .../backend/lobby/dto/LobbyResponse.java | 4 +- .../backend/lobby/dto/PlayerDto.java | 7 +++ .../lobby/dto/PositionSelectMessage.java | 21 -------- .../backend/lobby/services/LobbyService.java | 22 +++++++++ 5 files changed, 59 insertions(+), 44 deletions(-) create mode 100644 src/main/java/com/codenames/codenames/backend/lobby/dto/PlayerDto.java delete mode 100644 src/main/java/com/codenames/codenames/backend/lobby/dto/PositionSelectMessage.java diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 19672bb1..88837b8d 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -1,15 +1,10 @@ package com.codenames.codenames.backend.lobby.controller; import com.codenames.codenames.backend.lobby.dto.LobbyResponse; -import com.codenames.codenames.backend.lobby.dto.PositionSelectMessage; +import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.websocket.Player; import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.PostMapping; -import org.springframework.web.bind.annotation.RequestBody; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.bind.annotation.*; import java.util.List; @@ -49,7 +44,7 @@ public ResponseEntity createLobby(@RequestParam String username) return ResponseEntity.internalServerError() .body(new LobbyResponse("Error while creating lobby.", "", null)); } else { - List players = service.getPlayers(lobbyCode); + List players = service.getPlayersDto(lobbyCode); return ResponseEntity.ok(new LobbyResponse("Successfully created Lobby.", lobbyCode, players)); } } @@ -66,7 +61,7 @@ public ResponseEntity joinLobby( @RequestParam String username, @RequestParam String lobbyCode) { boolean joined = service.joinLobby(username, lobbyCode); if (joined) { - return ResponseEntity.ok(new LobbyResponse("Joined Lobby successfully.", lobbyCode, service.getPlayers(lobbyCode))); + return ResponseEntity.ok(new LobbyResponse("Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); } else { return ResponseEntity.badRequest() .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); @@ -80,12 +75,26 @@ public ResponseEntity joinLobby( * @param lobbyCode the lobby code identifying the lobby * @return a response indicating whether the operation was successful */ - @PostMapping("/leave") + @PostMapping("/{lobbyCode}/leave") public ResponseEntity leaveLobby( - @RequestParam String username, @RequestParam String lobbyCode) { + @RequestParam String username, + @PathVariable String lobbyCode) { boolean left = service.leaveLobby(username, lobbyCode); if (left) { - return ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode, service.getPlayers(lobbyCode))); + return ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); + } else { + return ResponseEntity.badRequest() + .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); + } + } + + @GetMapping("/{lobbyCode}") + public ResponseEntity getLobbyInfo( + @PathVariable String lobbyCode + ) { + List players = service.getPlayersDto(lobbyCode); + if (players != null) { + return ResponseEntity.ok(new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players)); } else { return ResponseEntity.badRequest() .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); @@ -98,24 +107,24 @@ public ResponseEntity leaveLobby( * @param request the position selection request containing username, lobby code, team, and role * @return a response indicating whether the selection was successful */ - @PostMapping("/select-position") + @PostMapping("/{lobbyCode}/select-position") public ResponseEntity selectPosition( - @RequestBody PositionSelectMessage request + @PathVariable String lobbyCode, @RequestBody PlayerDto request ) { boolean updated = service.selectPosition( - request.getUsername(), - request.getLobbyCode(), - request.getTeam(), - request.getRole() + request.username(), + lobbyCode, + request.team(), + request.role() ); if (updated) { return ResponseEntity.ok( - new LobbyResponse("Position selected successfully.", request.getLobbyCode(), service.getPlayers(request.getLobbyCode())) + new LobbyResponse("Position selected successfully.", lobbyCode, service.getPlayersDto(lobbyCode)) ); } else { return ResponseEntity.badRequest().body( - new LobbyResponse("Could not assign selected team/role.", request.getLobbyCode(), service.getPlayers(request.getLobbyCode())) + new LobbyResponse("Could not assign selected team/role.", lobbyCode, service.getPlayersDto(lobbyCode)) ); } } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java b/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java index e4118c35..009b2890 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java @@ -1,7 +1,5 @@ package com.codenames.codenames.backend.lobby.dto; -import com.codenames.codenames.backend.websocket.Player; - import java.util.List; /** @@ -9,7 +7,7 @@ * *

Contains a message describing the outcome and the associated lobby code. */ -public record LobbyResponse(String message, String lobbyCode, List playerList) { +public record LobbyResponse(String message, String lobbyCode, List playerList) { /** * Creates a new lobby response. * diff --git a/src/main/java/com/codenames/codenames/backend/lobby/dto/PlayerDto.java b/src/main/java/com/codenames/codenames/backend/lobby/dto/PlayerDto.java new file mode 100644 index 00000000..55b25c19 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/lobby/dto/PlayerDto.java @@ -0,0 +1,7 @@ +package com.codenames.codenames.backend.lobby.dto; + +import com.codenames.codenames.backend.utility.Role; +import com.codenames.codenames.backend.utility.Team; + +public record PlayerDto(String username, Team team, Role role, boolean isHost) { +} diff --git a/src/main/java/com/codenames/codenames/backend/lobby/dto/PositionSelectMessage.java b/src/main/java/com/codenames/codenames/backend/lobby/dto/PositionSelectMessage.java deleted file mode 100644 index 8cb795eb..00000000 --- a/src/main/java/com/codenames/codenames/backend/lobby/dto/PositionSelectMessage.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.codenames.codenames.backend.lobby.dto; - -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; - -/** - * DTO for selecting a position (team and role) in a lobby. - */ -@Getter -@Setter -@NoArgsConstructor -public class PositionSelectMessage { - - private String username; - private String lobbyCode; - private Team team; - private Role role; -} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index d39b0b3a..b4d267f7 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.lobby.services; import com.codenames.codenames.backend.lobby.Lobby; +import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; @@ -114,6 +115,27 @@ public List getPlayers(String lobbyCode) { return lobby != null ? lobby.getPlayerList() : List.of(); } + /** + * Retrieves all playerList in the specified lobby as PlayerDto objects. + * + * @param lobbyCode the lobby code identifying the lobby + * @return a list of PlayerDto objects, or an empty list if the lobby does not exist + */ + + public List getPlayersDto(String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + return lobby.getPlayerList().stream() + .map(player -> new PlayerDto( + player.username(), + lobby.getPlayerTeam(player.username()) != null ? lobby.getPlayerTeam(player.username()) : null, + lobby.getPlayerRole(player.username()) != null ? lobby.getPlayerRole(player.username()) : null, + player.isHost())) + .toList(); + } + return List.of(); + } + /** * Checks whether a spymaster is already assigned for the given team in the lobby. * From 3ba5426b2d26caecf3a25e267123007f6feb7d26 Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 13 May 2026 21:02:15 +0200 Subject: [PATCH 034/207] minor changes to leave flow --- .../codenames/backend/lobby/controller/LobbyController.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 88837b8d..86c95c51 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -77,9 +77,11 @@ public ResponseEntity joinLobby( */ @PostMapping("/{lobbyCode}/leave") public ResponseEntity leaveLobby( - @RequestParam String username, - @PathVariable String lobbyCode) { + @PathVariable String lobbyCode, + @RequestParam String username) { boolean left = service.leaveLobby(username, lobbyCode); + System.out.println("LobbyCode: " + lobbyCode); + System.out.println("Username: " + username); if (left) { return ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); } else { From 5f828fb43aef25dede79d5bb6bef7aac8c4fdb19 Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 13 May 2026 21:30:23 +0200 Subject: [PATCH 035/207] lobbies are now removed when they are empty --- .../backend/lobby/controller/LobbyController.java | 4 +++- .../backend/lobby/services/LobbyService.java | 11 +++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 86c95c51..3057e896 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -83,7 +83,9 @@ public ResponseEntity leaveLobby( System.out.println("LobbyCode: " + lobbyCode); System.out.println("Username: " + username); if (left) { - return ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); + ResponseEntity response = ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); + service.checkLobbyStillHasPlayers(lobbyCode); + return response; } else { return ResponseEntity.badRequest() .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index b4d267f7..5c16b600 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -103,6 +103,17 @@ public boolean selectPosition(String username, String lobbyCode, Team team, Role lobby.setPlayerRole(username, role); return true; } + /** + * Checks if the lobby still has players after a player leaves and removes the lobby if it is empty. + * + * @param lobbyCode the lobby code identifying the lobby + */ + public void checkLobbyStillHasPlayers(String lobbyCode){ + Lobby lobby = lobbyList.get(lobbyCode); + if(lobby.getPlayerList().isEmpty()){ + lobbyList.remove(lobbyCode); + } + } /** * Retrieves all playerList in the specified lobby. From c040d6745d51104138873c7d4f5011c0a4c9611e Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 13 May 2026 21:59:49 +0200 Subject: [PATCH 036/207] test: add and fix test for lobby controller and service --- .../backend/lobby/services/LobbyService.java | 3 ++ .../lobby/controller/LobbyControllerTest.java | 42 +++++++++++++------ .../lobby/services/LobbyServiceTest.java | 34 +++++++++++++++ 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 5c16b600..85332ebf 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -9,6 +9,8 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; + +import lombok.Getter; import org.springframework.stereotype.Service; /** @@ -20,6 +22,7 @@ @Service public class LobbyService { + @Getter private final Map lobbyList = new ConcurrentHashMap<>(); private final LobbyCodeGenerator generator; diff --git a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java index 5c5971ce..250a0425 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java @@ -1,10 +1,12 @@ package com.codenames.codenames.backend.lobby.controller; import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; +import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; @@ -15,6 +17,8 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import java.util.List; + @WebMvcTest(LobbyController.class) class LobbyControllerTest { @@ -71,10 +75,9 @@ void joinLobbyShouldReturn400_whenNotFound() throws Exception { @Test void leaveLobbyShouldReturn200_whenSuccess() throws Exception { when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(true); - - mockMvc.perform(post("/lobby/leave") - .param("username", "TestUser") - .param("lobbyCode", "ABCDE")) + String url = "/lobby/ABCDE/leave"; + mockMvc.perform(post(url) + .param("username", "TestUser")) .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("Left lobby successfully.")); } @@ -82,10 +85,9 @@ void leaveLobbyShouldReturn200_whenSuccess() throws Exception { @Test void leaveLobbyNoSuccess() throws Exception { when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(false); - - mockMvc.perform(post("/lobby/leave") - .param("username", "TestUser") - .param("lobbyCode", "ABCDE")) + String url = "/lobby/ABCDE/leave"; + mockMvc.perform(post(url) + .param("username", "TestUser")) .andExpect(status().isBadRequest()) .andExpect(jsonPath("$.message").value("Could not find lobby.")); } @@ -93,8 +95,8 @@ void leaveLobbyNoSuccess() throws Exception { @Test void selectPositionShouldReturn200whenSuccess() throws Exception { when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(true); - - mockMvc.perform(post("/lobby/select-position") + String url = "/lobby/ABCDE/select-position"; + mockMvc.perform(post(url) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -113,8 +115,8 @@ void selectPositionShouldReturn200whenSuccess() throws Exception { @Test void selectPositionShouldReturn400whenAssignmentFails() throws Exception { when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(false); - - mockMvc.perform(post("/lobby/select-position") + String url = "/lobby/ABCDE/select-position"; + mockMvc.perform(post(url) .contentType(MediaType.APPLICATION_JSON) .content( """ @@ -129,4 +131,20 @@ void selectPositionShouldReturn400whenAssignmentFails() throws Exception { .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); } + + @Test + void getLobbyInfoShouldReturn200() throws Exception { + when(service.getPlayersDto("ABCDE")).thenReturn(List.of(new PlayerDto("test", null, null, true))); + String url = "/lobby/ABCDE"; + mockMvc.perform(get(url)) + .andExpect(status().isOk()); + } + + @Test + void getLobbyInfoShouldReturn404() throws Exception { + when(service.getPlayersDto("XXXXX")).thenReturn(null); + String url = "/lobby/XXXXX"; + mockMvc.perform(get(url)) + .andExpect(status().isBadRequest()); + } } \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index ef61e579..7c4edad8 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; @@ -245,4 +246,37 @@ void getPlayerRole_nonExistentPlayer() { assertNull(lobbyService.getPlayerRole("nonExistentPlayer", lobbyCode)); } + + @Test + void testLobbyIsRemovedWhenItIsEmpty() { + lobbyService.createLobby("Host"); + lobbyService.leaveLobby("Host", "ABCDE"); + lobbyService.checkLobbyStillHasPlayers("ABCDE"); + assertFalse(lobbyService.getLobbyList().containsKey("ABCDE")); + } + + @Test + void testLobbyIsNotRemovedWhenItHasPlayers() { + lobbyService.createLobby("Host"); + lobbyService.joinLobby("Player1", "ABCDE"); + lobbyService.checkLobbyStillHasPlayers("ABCDE"); + assertTrue(lobbyService.getLobbyList().containsKey("ABCDE")); + } + + @Test + void testGetPlayersDto(){ + lobbyService.createLobby("Host"); + + List players = lobbyService.getPlayersDto("ABCDE"); + PlayerDto player = players.get(0); + assertEquals("Host", player.username()); + assertTrue(player.isHost()); + } + + @Test + void testGetPlayersDto_lobbyNotExists(){ + List players = lobbyService.getPlayersDto("UNKNOWN"); + assertNotNull(players); + assertTrue(players.isEmpty()); + } } \ No newline at end of file From 9b23f1658f70a8c4901eae626ec2f3c4a3cfc3e4 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 16:11:24 +0200 Subject: [PATCH 037/207] refactor: remove parameter since current team color is accessible --- .../codenames/backend/playingfield/GameManager.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index ad3dab5d..fe1f96b9 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -184,11 +184,11 @@ public String getCurrentClueWord() { return currentClue.word(); } - public Team nextTeamColor(Team current) { - if (current == Team.RED) { - return Team.BLUE; + public void nextTeamColor() { + if (currentTurn == Team.RED) { + currentTurn = Team.BLUE; } else { - return Team.RED; + currentTurn = Team.RED; } } @@ -199,7 +199,7 @@ public void advanceTurn() { currentPhase = Role.OPERATIVE; } else { currentPhase = Role.SPYMASTER; - currentTurn = nextTeamColor(currentTurn); + nextTeamColor(); clearClue(); } } From 6b5dadfdfd447b54745c0f6e82011ed6164f1773 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 17:07:42 +0200 Subject: [PATCH 038/207] feat: add method to end turn early --- .../codenames/backend/playingfield/GameManager.java | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index fe1f96b9..8b74b8d5 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -204,6 +204,11 @@ public void advanceTurn() { } } + public void passTurn(Team callingTeam){ + checkCorrectTurn(callingTeam, Role.OPERATIVE); + advanceTurn(); + } + private void checkCorrectTurn(Team team, Role role) { if (team != currentTurn || role != currentPhase) { throw new IllegalStateException("Not your turn/ role"); From 753cbd30f21de6a0fd9175797d8f0a973ce14b60 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 17:08:27 +0200 Subject: [PATCH 039/207] feat: add check to end turn when wrong card is flipped or out of guesses --- .../codenames/backend/playingfield/GameManager.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index 8b74b8d5..eb9495a4 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -148,6 +148,13 @@ public void flipCard(int position) { this.board.setGuessed(position); Color currentColor = this.board.checkColor(position); updateScore(currentColor, currentTurn); + boolean opponentOrWhiteCard = + (currentTurn == Team.RED && currentColor != Color.RED) + || (currentTurn == Team.BLUE && currentColor != Color.BLUE); + + if (opponentOrWhiteCard || this.remainingGuesses == 0) { + advanceTurn(); + } } /** From 5284f717cf5e41dfec4295ec32506d34561a2d95 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 17:09:26 +0200 Subject: [PATCH 040/207] refactor: have method read from class attribute instead of passing it --- .../codenames/backend/playingfield/GameManager.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index eb9495a4..4dc724e1 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -86,9 +86,8 @@ public Color checkColor(int position) { * Updates the score based on the color passed. If black card is found, opposing team wins. * * @param cardColor the color of the card - * @param currentTurn the current team's turn */ - private void updateScore(Color cardColor, Team currentTurn) { + private void updateScore(Color cardColor) { switch (cardColor) { case RED: currentRedFound++; @@ -97,7 +96,7 @@ private void updateScore(Color cardColor, Team currentTurn) { currentBlueFound++; break; case BLACK: - if (currentTurn == Team.RED) { + if (this.currentTurn == Team.RED) { this.winner = Team.BLUE; } else { this.winner = Team.RED; From 7a0369d6e2fac717b872b2c4f12e75a272976e8f Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Thu, 14 May 2026 17:11:23 +0200 Subject: [PATCH 041/207] refactor: replace reveal card color turn state with team model --- .../codenames/backend/game/dto/RevealCardMessage.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java index d31b1729..940d4676 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/dto/RevealCardMessage.java @@ -1,6 +1,6 @@ package com.codenames.codenames.backend.game.dto; -import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.utility.Team; import lombok.Getter; import lombok.Setter; @@ -14,5 +14,5 @@ public class RevealCardMessage { private String lobbyCode; private int position; - private Color currentTurn; + private Team currentTurn; } From a071b50d0f7ba4608e72d93b7a3f6a62d58e708c Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 17:13:09 +0200 Subject: [PATCH 042/207] fix: change parameter for checkCorrectTurn Originally passing wrong parameters that would evaluate to true erroneously. Now frontend should pass the current team that is calling the method to check if that team is allowed to make a move or not. we also hard code the proper role that is allowed for that method, as frontend does not need to do this. only spymaster can submit clues, only operatives can flip cards --- .../codenames/backend/playingfield/GameManager.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index 4dc724e1..87d34abd 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -131,7 +131,7 @@ public Team getWinner() { * @param position the position of the card that is selected by the player * @throws IllegalStateException if game over, card already flipped, no more guesses */ - public void flipCard(int position) { + public void flipCard(int position, Team callingTeam) { if (getWinner() != null) { throw new IllegalStateException("Winner is already set"); } @@ -142,11 +142,14 @@ public void flipCard(int position) { clearClue(); throw new IllegalStateException("No more guesses."); } - checkCorrectTurn(currentTurn, currentPhase); + + checkCorrectTurn(callingTeam, Role.OPERATIVE); + this.remainingGuesses--; this.board.setGuessed(position); Color currentColor = this.board.checkColor(position); - updateScore(currentColor, currentTurn); + updateScore(currentColor); + boolean opponentOrWhiteCard = (currentTurn == Team.RED && currentColor != Color.RED) || (currentTurn == Team.BLUE && currentColor != Color.BLUE); @@ -162,8 +165,8 @@ public void flipCard(int position) { * @param clue the clue object containing word and guess amount * @throws IllegalArgumentException if clue is: null, empty, spaces, or word is on the board */ - public void submitClue(Clue clue) { - checkCorrectTurn(currentTurn, currentPhase); + public void submitClue(Clue clue, Team callingTeam) { + checkCorrectTurn(callingTeam, Role.SPYMASTER); if (clueValidationService.validateWord(this.board, clue.word())) { this.currentClue = clue; this.remainingGuesses = clue.guessAmount(); From 6b5ae258a7c85d67b34f47bbd81bb163d109e645 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Thu, 14 May 2026 17:16:32 +0200 Subject: [PATCH 043/207] test: align websocket gameplay tests with team turn handling --- .../backend/game/controller/GameSocketControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 84d23c92..934de4b3 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -87,7 +87,7 @@ void revealCardShouldBroadcastBoardUpdate() { revealMessage.setLobbyCode("ABCDE"); revealMessage.setPosition(0); - revealMessage.setCurrentTurn(Color.RED); + revealMessage.setCurrentTurn(Team.RED); controller.revealCard(revealMessage); From 1a9efb0804439ab0697fdd6d1511ca5b5659e8ac Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 17:18:06 +0200 Subject: [PATCH 044/207] fix: add callingTeam parameter that should be passed by frontend We want frontend/ caller to pass their team to ensure that they are allowed to invoke the method or not --- .../backend/playingfield/GameService.java | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 45719077..14e7e09b 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -1,23 +1,22 @@ package com.codenames.codenames.backend.playingfield; import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.utility.Team; +import org.springframework.stereotype.Service; +@Service public class GameService { - public void submitClue(GameManager gm, Clue clue) { - gm.submitClue(clue); + public void submitClue(GameManager gm, Clue clue, Team callingTeam) { + gm.submitClue(clue, callingTeam); gm.advanceTurn(); } - public void flipCard(GameManager gm, int position) { - gm.flipCard(position); - if (gm.getRemainingGuesses() == 0) { - gm.advanceTurn(); - } + public void flipCard(GameManager gm, int position, Team callingTeam) { + gm.flipCard(position, callingTeam); } - public void endTurn(GameManager gm) { - gm.advanceTurn(); + public void passTurn(GameManager gm, Team callingTeam){ + gm.passTurn(callingTeam); } - } From 0cf8e11f7cb093ade899be1b052e91cdc0905f7a Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Thu, 14 May 2026 17:24:35 +0200 Subject: [PATCH 045/207] feat: add gameplay state dto --- .../backend/game/dto/GameStateDto.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java b/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java new file mode 100644 index 00000000..cd86d0cc --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java @@ -0,0 +1,32 @@ +package com.codenames.codenames.backend.game.dto; + +import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.playingfield.Card; +import com.codenames.codenames.backend.utility.Team; +import java.util.List; +import lombok.Getter; + +/** DTO representing the current game state. */ +@Getter +public class GameStateDto { + private final List cards; + private final Clue currentClue; + private final int remainingGuesses; + private final Team winner; + + /** + * Creates a new game state DTO. + * + * @param cards current board cards + * @param currentClue current clue + * @param remainingGuesses remaining guesses + * @param winner winning team or null + */ + public GameStateDto(List cards, Clue currentClue, int remainingGuesses, Team winner) { + + this.cards = cards; + this.currentClue = currentClue; + this.remainingGuesses = remainingGuesses; + this.winner = winner; + } +} From feca412944e2b33ac75fa87d9789e0f0bc7e898e Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Thu, 14 May 2026 17:31:25 +0200 Subject: [PATCH 046/207] refactor: add gameplay state mapping helper --- .../backend/game/controller/GameSocketController.java | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 96140d20..b6e8ec94 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -3,6 +3,7 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.game.dto.ClueMessage; +import com.codenames.codenames.backend.game.dto.GameStateDto; import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.lobby.services.LobbyService; @@ -33,6 +34,15 @@ public class GameSocketController { private static final String GAME_TOPIC_PREFIX = "/topic/game/"; + private GameStateDto mapGameState(GameManager gameManager) { + + return new GameStateDto( + gameManager.getCardList(), + gameManager.getCurrentClue(), + gameManager.getRemainingGuesses(), + gameManager.getWinner()); + } + /** * Creates a new {@code GameSocketController}. * From e15434b7c5a535976442f561546cae99621e91a4 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Thu, 14 May 2026 17:37:54 +0200 Subject: [PATCH 047/207] refactor: unify websocket gameplay response payloads --- .../backend/game/controller/GameSocketController.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index b6e8ec94..a0dbc809 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -79,7 +79,7 @@ public void startGame(StartGameMessage message) { gameSessions.put(message.getLobbyCode(), gameManager); messagingTemplate.convertAndSend( - GAME_TOPIC_PREFIX + message.getLobbyCode(), gameManager.getCardList()); + GAME_TOPIC_PREFIX + message.getLobbyCode(), mapGameState(gameManager)); } /** @@ -101,7 +101,7 @@ public void revealCard(RevealCardMessage message) { gameManager.flipCard(message.getPosition(), message.getCurrentTurn()); messagingTemplate.convertAndSend( - GAME_TOPIC_PREFIX + message.getLobbyCode(), gameManager.getCardList()); + GAME_TOPIC_PREFIX + message.getLobbyCode(), mapGameState(gameManager)); } /** @@ -124,6 +124,6 @@ public void submitClue(ClueMessage message) { gameManager.submitClue(clue); - messagingTemplate.convertAndSend(GAME_TOPIC_PREFIX + message.getLobbyCode(), gameManager); + messagingTemplate.convertAndSend(GAME_TOPIC_PREFIX + message.getLobbyCode(), mapGameState(gameManager)); } } From b0f3991e8ac66dabb72d8a7366388bf7a7669615 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Thu, 14 May 2026 17:39:22 +0200 Subject: [PATCH 048/207] style: fix checkstyle line length violation --- .../backend/game/controller/GameSocketController.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index a0dbc809..ddb3f911 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -124,6 +124,7 @@ public void submitClue(ClueMessage message) { gameManager.submitClue(clue); - messagingTemplate.convertAndSend(GAME_TOPIC_PREFIX + message.getLobbyCode(), mapGameState(gameManager)); + messagingTemplate.convertAndSend( + GAME_TOPIC_PREFIX + message.getLobbyCode(), mapGameState(gameManager)); } } From 23ff4f59ec8fb0ff0206451ec980c839e4c0994f Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 17:54:19 +0200 Subject: [PATCH 049/207] feat: add creating and storing GameMmanager for lobbies Turn system is complete but had no way of being used. This commit includes the storing and creating of GameManagers. This way we can reference a certain GameManager of a lobby to call the methods needed to advance the turns --- .../backend/playingfield/GameService.java | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 14e7e09b..065391c6 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -1,13 +1,39 @@ package com.codenames.codenames.backend.playingfield; import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.utility.Team; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Service; @Service public class GameService { public void submitClue(GameManager gm, Clue clue, Team callingTeam) { + private final CardGenerator cardGenerator; + private final ClueValidationService clueValidationService; + + public GameService(CardGenerator cardGenerator, ClueValidationService clueValidationService) { + this.cardGenerator = cardGenerator; + this.clueValidationService = clueValidationService; + } + + public void createGameManager(String lobbyCode, Team startingTeam) { + games.computeIfAbsent( + lobbyCode, game -> new GameManager(startingTeam, cardGenerator, clueValidationService)); + } + + public void removeGame(String lobbyCode) { + games.remove(lobbyCode); + } + + public GameManager getGame(String lobbyCode) { + if (games.get(lobbyCode) == null) { + throw new IllegalStateException("GameManager does not exist for lobby: " + lobbyCode); + } + return games.get(lobbyCode); + } gm.submitClue(clue, callingTeam); gm.advanceTurn(); } From f95fa186a303dd591483a2d4c0f6bf6d74057d06 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 17:54:44 +0200 Subject: [PATCH 050/207] fix: remove GameManager parameter as frontend should not know what gm is --- .../codenames/backend/playingfield/GameService.java | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 065391c6..1b113924 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -10,7 +10,7 @@ @Service public class GameService { - public void submitClue(GameManager gm, Clue clue, Team callingTeam) { + private final Map games = new ConcurrentHashMap<>(); private final CardGenerator cardGenerator; private final ClueValidationService clueValidationService; @@ -34,15 +34,20 @@ public GameManager getGame(String lobbyCode) { } return games.get(lobbyCode); } + + public void submitClue(String lobbyCode, Clue clue, Team callingTeam) { + GameManager gm = getGame(lobbyCode); gm.submitClue(clue, callingTeam); gm.advanceTurn(); } - public void flipCard(GameManager gm, int position, Team callingTeam) { + public void flipCard(String lobbyCode, int position, Team callingTeam) { + GameManager gm = getGame(lobbyCode); gm.flipCard(position, callingTeam); } - public void passTurn(GameManager gm, Team callingTeam){ + public void passTurn(String lobbyCode, Team callingTeam) { + GameManager gm = getGame(lobbyCode); gm.passTurn(callingTeam); } } From 770b81c9035e3ed754379e1be9cd7ce0f9616c5f Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Thu, 14 May 2026 18:21:31 +0200 Subject: [PATCH 051/207] feat: add health endpoint for deployment monitoring --- .../backend/controller/HealthController.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/controller/HealthController.java diff --git a/src/main/java/com/codenames/codenames/backend/controller/HealthController.java b/src/main/java/com/codenames/codenames/backend/controller/HealthController.java new file mode 100644 index 00000000..3d6f749b --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/controller/HealthController.java @@ -0,0 +1,14 @@ +package com.codenames.codenames.backend.controller; + +import java.util.Map; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class HealthController { + + @GetMapping("/health") + public Map health() { + return Map.of("status", "UP"); + } +} From e38bbd6abe786f54edea2af28d034c7539a6aedb Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Thu, 14 May 2026 18:31:57 +0200 Subject: [PATCH 052/207] docs: add JavaDocs for health endpoint --- .../codenames/backend/controller/HealthController.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/controller/HealthController.java b/src/main/java/com/codenames/codenames/backend/controller/HealthController.java index 3d6f749b..b19a3425 100644 --- a/src/main/java/com/codenames/codenames/backend/controller/HealthController.java +++ b/src/main/java/com/codenames/codenames/backend/controller/HealthController.java @@ -4,9 +4,18 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; +/** + * Controller providing a simple health check endpoint to verify that the backend application is + * running. + */ @RestController public class HealthController { + /** + * Returns the current health status of the backend. + * + * @return a map containing the application status + */ @GetMapping("/health") public Map health() { return Map.of("status", "UP"); From dff6b2176fb4ec6491d4479f31ad4ec89f932b4a Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 18:32:41 +0200 Subject: [PATCH 053/207] feat: create GameService and GameManager when a lobby is created --- .../codenames/backend/lobby/services/LobbyService.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 0af52802..bbba0393 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.lobby.services; import com.codenames.codenames.backend.lobby.Lobby; +import com.codenames.codenames.backend.playingfield.GameService; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; @@ -20,14 +21,16 @@ public class LobbyService { private final Map lobbyList = new ConcurrentHashMap<>(); private final LobbyCodeGenerator generator; + private final GameService gameService; /** * Creates a new {@code LobbyService}. * * @param generator the lobby code generator used to create unique lobby codes */ - public LobbyService(LobbyCodeGenerator generator) { + public LobbyService(LobbyCodeGenerator generator, GameService gameService) { this.generator = generator; + this.gameService = gameService; } /** @@ -44,6 +47,9 @@ public String createLobby(String username) { Lobby lobby = new Lobby(lobbyCode, username); lobbyList.put(lobbyCode, lobby); + + Team start = lobby.decideStartingTeam(); + gameService.createGameManager(lobbyCode, start); return lobbyCode; } From beeba51459f4fbb2863c4d671abc34432f049a0e Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 19:12:52 +0200 Subject: [PATCH 054/207] refactor: extract creation of GM for a lobby --- .../backend/lobby/services/LobbyService.java | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index bbba0393..2020011a 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -44,13 +44,21 @@ public String createLobby(String username) { if (lobbyCode == null || lobbyCode.isBlank()) { return null; } - Lobby lobby = new Lobby(lobbyCode, username); lobbyList.put(lobbyCode, lobby); + addGameManagerForLobby(lobby, lobbyCode); + return lobbyCode; + } + /** + * Helper method to add the GameManager once a lobby is created. + * + * @param lobby the lobby object to determine the starting team + * @param lobbyCode the ID for the lobby which the GameManager is responsible for + */ + private void addGameManagerForLobby(Lobby lobby, String lobbyCode) { Team start = lobby.decideStartingTeam(); gameService.createGameManager(lobbyCode, start); - return lobbyCode; } /** From 213bac9ebc7c57a83e1d59e23cd87a0ed1c9259c Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 19:13:17 +0200 Subject: [PATCH 055/207] fix: add gameService mock to class test --- .../codenames/backend/lobby/services/LobbyServiceTest.java | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index 43059b19..a3a5eaa0 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -8,6 +8,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.codenames.codenames.backend.playingfield.GameService; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; @@ -25,11 +26,13 @@ class LobbyServiceTest { private LobbyService lobbyService; private LobbyCodeGenerator generator; + private GameService gameService; @BeforeEach void setup() { generator = mock(LobbyCodeGenerator.class); - lobbyService = new LobbyService(generator); + gameService = mock(GameService.class); + lobbyService = new LobbyService(generator, gameService); when(generator.generateLobbyCode()).thenReturn("ABCDE"); } From a51844407d9893ac6678571e02629dd77ab5fa0c Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 19:23:45 +0200 Subject: [PATCH 056/207] docs: add java docs --- .../backend/lobby/services/LobbyService.java | 19 +++---- .../backend/playingfield/GameManager.java | 32 +++++++++--- .../backend/playingfield/GameService.java | 51 ++++++++++++++++++- 3 files changed, 84 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 2020011a..34c9f956 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -13,8 +13,8 @@ /** * Service responsible for managing lobbies and player interactions. * - *

Handles creation of lobbies, player joins/leaves, and retrieval of lobby data. - * Ensures uniqueness of lobby codes and thread-safe access to lobby storage. + *

Handles creation of lobbies, player joins/leaves, and retrieval of lobby data. Ensures + * uniqueness of lobby codes and thread-safe access to lobby storage. */ @Service public class LobbyService { @@ -27,6 +27,7 @@ public class LobbyService { * Creates a new {@code LobbyService}. * * @param generator the lobby code generator used to create unique lobby codes + * @param gameService the service used for creating a GameManager for each lobby */ public LobbyService(LobbyCodeGenerator generator, GameService gameService) { this.generator = generator; @@ -64,7 +65,7 @@ private void addGameManagerForLobby(Lobby lobby, String lobbyCode) { /** * Adds a player to an existing lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby * @return {@code true} if the player successfully joined, {@code false} otherwise */ @@ -79,7 +80,7 @@ public boolean joinLobby(String username, String lobbyCode) { /** * Removes a player from a lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby * @return {@code true} if the player was removed, {@code false} if the lobby does not exist */ @@ -95,10 +96,10 @@ public boolean leaveLobby(String username, String lobbyCode) { /** * Assigns a team and role to a player in a lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby - * @param team the selected team - * @param role the selected role + * @param team the selected team + * @param role the selected role * @return {@code true} if the position was assigned, {@code false} otherwise */ public boolean selectPosition(String username, String lobbyCode, Team team, Role role) { @@ -131,9 +132,9 @@ public List getPlayers(String lobbyCode) { /** * Checks whether a spymaster is already assigned for the given team in the lobby. * - * @param lobby the lobby to inspect + * @param lobby the lobby to inspect * @param username the username requesting the role - * @param team the team to inspect + * @param team the team to inspect * @return {@code true} if a different player is already the spymaster for that team */ private boolean isSpymasterAlreadyAssigned(Lobby lobby, String username, Team team) { diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index 87d34abd..3911ef84 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -9,10 +9,9 @@ import lombok.Getter; /** - * Manages state and initialization of a game. - * - *

This class handles the setup of the board and interaction, by providing methods to interact - * with the game's state. + * This class handles the setup of the board and interaction, by providing methods to interact with + * the game's state. Additionally, it keeps track of the points, turn and handles the early + * determining the winner */ public class GameManager { @@ -193,7 +192,8 @@ public String getCurrentClueWord() { return currentClue.word(); } - public void nextTeamColor() { + /** Changes the color of what team is at turn. */ + private void nextTeamColor() { if (currentTurn == Team.RED) { currentTurn = Team.BLUE; } else { @@ -201,8 +201,13 @@ public void nextTeamColor() { } } - // we do not change the team color when we advance after being spymaster as the same team - // operatives are now at turn, only after operatives are done we clear clue and change team color. + /** + * Is called by relevant turn based methods. Class holds the current Team and Phase, when we call + * this method, based on the current turn, we swap to the opposite turn/ phase. Since after + * spymaster is done, the same team color is still at turn, and we can simply switch to the + * operative phase. If we are Operative, we have to additionally switch the next team color and + * clear the clue from our spymaster. + */ public void advanceTurn() { if (currentPhase == Role.SPYMASTER) { currentPhase = Role.OPERATIVE; @@ -213,11 +218,22 @@ public void advanceTurn() { } } - public void passTurn(Team callingTeam){ + /** + * Voluntarily pass turn early before all guesses are used up. + * + * @param callingTeam the current team calling the method. + */ + public void passTurn(Team callingTeam) { checkCorrectTurn(callingTeam, Role.OPERATIVE); advanceTurn(); } + /*** + * Helper method to check if the current team calling a method is allowed to do so. + * + * @param team the team of who is calling the method + * @param role the role of who is calling the method + */ private void checkCorrectTurn(Team team, Role role) { if (team != currentTurn || role != currentPhase) { throw new IllegalStateException("Not your turn/ role"); diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 1b113924..b2e08516 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -7,6 +7,11 @@ import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Service; +/** + * Service class for the game. The class handles the creation and storing of a GameManager for each + * lobby. This also exposes the methods of the GameManager, so that the websocket controllers can + * have message mappings to allow frontend to interact with the backend. + */ @Service public class GameService { @@ -14,38 +19,82 @@ public class GameService { private final CardGenerator cardGenerator; private final ClueValidationService clueValidationService; + /** + * Constructor for a GameService object. + * + * @param cardGenerator the card generator required for creating the cards in the manager class + * @param clueValidationService the service required for validating clues in the manager class + */ public GameService(CardGenerator cardGenerator, ClueValidationService clueValidationService) { this.cardGenerator = cardGenerator; this.clueValidationService = clueValidationService; } + /** + * We have a has map with lobbyID as the key. This method adds a GM if the key does not already + * exist. + * + * @param lobbyCode the lobbyID that serves as the key + * @param startingTeam the starting team required to initialize a GM + */ public void createGameManager(String lobbyCode, Team startingTeam) { games.computeIfAbsent( lobbyCode, game -> new GameManager(startingTeam, cardGenerator, clueValidationService)); } + /** + * This method serves as a deletion of an entry in the hashmap once a lobby is no longer needed. + * + * @param lobbyCode the lobbyID that serves as a key to identify which GM to delete + */ public void removeGame(String lobbyCode) { games.remove(lobbyCode); } - public GameManager getGame(String lobbyCode) { + /** + * Helper method to retrieve a GM object from the hash map. + * + * @param lobbyCode the lobbyID that serves as a key to identify which GM to access + * @return the GM specified by the lobbyID + */ + private GameManager getGame(String lobbyCode) { if (games.get(lobbyCode) == null) { throw new IllegalStateException("GameManager does not exist for lobby: " + lobbyCode); } return games.get(lobbyCode); } + /** + * The exposed clue submission method from GM that is accessed by frontend via websockets + * + * @param lobbyCode the lobbyID that serves as a key to identify which GM to access + * @param clue the clue object that needs to be validated and added to GM + * @param callingTeam the team who called to ensure the calling team is at turn + */ public void submitClue(String lobbyCode, Clue clue, Team callingTeam) { GameManager gm = getGame(lobbyCode); gm.submitClue(clue, callingTeam); gm.advanceTurn(); } + /** + * The exposed card flipping method from GM that is accessed by frontend via websockets + * + * @param lobbyCode the lobbyID that serves as a key to identify which GM to access + * @param position the position of which card is supposed to flipped on the board + * @param callingTeam the team who called to ensure the calling team is at turn + */ public void flipCard(String lobbyCode, int position, Team callingTeam) { GameManager gm = getGame(lobbyCode); gm.flipCard(position, callingTeam); } + /** + * The exposed early turn ending method from GM that is accessed by frontend via websockets + * + * @param lobbyCode the lobbyID that serves as a key to identify which GM to access + * @param callingTeam the team who called to ensure the calling team is at turn + */ public void passTurn(String lobbyCode, Team callingTeam) { GameManager gm = getGame(lobbyCode); gm.passTurn(callingTeam); From ca28397110521e7227fa7444d4ebce4c65e54a74 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 19:24:54 +0200 Subject: [PATCH 057/207] docs: add missing punctuation for checkstyle --- .../codenames/backend/playingfield/GameService.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index b2e08516..14349f80 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -65,7 +65,7 @@ private GameManager getGame(String lobbyCode) { } /** - * The exposed clue submission method from GM that is accessed by frontend via websockets + * The exposed clue submission method from GM that is accessed by frontend via websockets. * * @param lobbyCode the lobbyID that serves as a key to identify which GM to access * @param clue the clue object that needs to be validated and added to GM @@ -78,7 +78,7 @@ public void submitClue(String lobbyCode, Clue clue, Team callingTeam) { } /** - * The exposed card flipping method from GM that is accessed by frontend via websockets + * The exposed card flipping method from GM that is accessed by frontend via websockets. * * @param lobbyCode the lobbyID that serves as a key to identify which GM to access * @param position the position of which card is supposed to flipped on the board @@ -90,7 +90,7 @@ public void flipCard(String lobbyCode, int position, Team callingTeam) { } /** - * The exposed early turn ending method from GM that is accessed by frontend via websockets + * The exposed early turn ending method from GM that is accessed by frontend via websockets. * * @param lobbyCode the lobbyID that serves as a key to identify which GM to access * @param callingTeam the team who called to ensure the calling team is at turn From 9caa3f6d46e0743bd4231d01e34f163829d9749e Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 20:19:12 +0200 Subject: [PATCH 058/207] test: add test for verification of addGameManagerForLobby is invvoked --- .../backend/lobby/services/LobbyServiceTest.java | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index a3a5eaa0..0b818e9e 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -6,6 +6,8 @@ import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.codenames.codenames.backend.playingfield.GameService; @@ -218,12 +220,12 @@ void testGetPlayerTeam() { } @Test - void getPlayerTeam_wrongCode() { + void testGetPlayerTeam_wrongCode() { assertNull(lobbyService.getPlayerTeam("Host", "invalidCode")); } @Test - void getPlayerTeam_nonExistentPlayer() { + void testGetPlayerTeam_nonExistentPlayer() { String lobbyCode = lobbyService.createLobby("Host"); assertNull(lobbyService.getPlayerTeam("nonExistentPlayer", lobbyCode)); @@ -238,14 +240,20 @@ void testGetPlayerRole() { } @Test - void getPlayerRole_wrongCode() { + void testGetPlayerRole_wrongCode() { assertNull(lobbyService.getPlayerRole("Host", "test")); } @Test - void getPlayerRole_nonExistentPlayer() { + void testGetPlayerRole_nonExistentPlayer() { String lobbyCode = lobbyService.createLobby("Host"); assertNull(lobbyService.getPlayerRole("nonExistentPlayer", lobbyCode)); } + + @Test + void testAddGameManagerForLobby() { + lobbyService.createLobby("Host"); + verify(gameService, times(1)).createGameManager("ABCDE", Team.RED); + } } \ No newline at end of file From 6c2e3ac1b0671fd250d0c3ba3cc80c48e07f417c Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 20:19:33 +0200 Subject: [PATCH 059/207] docs: fix check style --- .../codenames/backend/playingfield/GameManager.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index 3911ef84..8ce543a0 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -10,8 +10,9 @@ /** * This class handles the setup of the board and interaction, by providing methods to interact with - * the game's state. Additionally, it keeps track of the points, turn and handles the early - * determining the winner + * the game's state. + * + *

Additionally, it keeps track of the points, turn and handles the early determining the winner */ public class GameManager { @@ -228,7 +229,7 @@ public void passTurn(Team callingTeam) { advanceTurn(); } - /*** + /** * Helper method to check if the current team calling a method is allowed to do so. * * @param team the team of who is calling the method From ec74ca4f9bc3747715c5ddd60a6303c8b553d5f4 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 20:31:03 +0200 Subject: [PATCH 060/207] docs: add missing param annotation in docs --- .../codenames/codenames/backend/playingfield/GameManager.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index 8ce543a0..a673b491 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -39,6 +39,7 @@ public class GameManager { * * @param startingTeam the team that goes first will get an extra card * @param cardGenerator the utility used to generate the cards for the game + * @param clueValidationService the utility used to validate clues * @throws IllegalArgumentException if team is null, white or black */ public GameManager( @@ -129,6 +130,7 @@ public Team getWinner() { * Changes the guessed state of a card and updates the score if necessary. * * @param position the position of the card that is selected by the player + * @param callingTeam the team that called this method * @throws IllegalStateException if game over, card already flipped, no more guesses */ public void flipCard(int position, Team callingTeam) { @@ -163,6 +165,7 @@ public void flipCard(int position, Team callingTeam) { * Submits a clue and updates remaining guesses. * * @param clue the clue object containing word and guess amount + * @param callingTeam the team that called this method * @throws IllegalArgumentException if clue is: null, empty, spaces, or word is on the board */ public void submitClue(Clue clue, Team callingTeam) { From aaa165f04f52c5066d0d1d2303468fd86b5eb3e0 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 20:31:24 +0200 Subject: [PATCH 061/207] fix: add missing parameters as source code changed --- .../backend/playingfield/GameManagerTest.java | 58 +++++++++---------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index bda9c086..8174177c 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -51,6 +51,24 @@ private void mockCardGeneration(List cardList) { .thenReturn(cardList); } + private void helperMethodSubmitClue(GameManager gameManager, int guessAmount, Team callingTeam) { + gameManager.submitClue(new Clue("Test", guessAmount), callingTeam); + } + + // Helper method for testing permutation of getWinner() + private @NonNull GameManager helperMethodGenerateFullCardList( + Color cardColor, Team startingTeam) { + List cardList = new ArrayList<>(); + for (int i = 0; i < 25; i++) { + cardList.add(new Card("Test" + i, cardColor)); + } + mockCardGeneration(cardList); + GameManager fullListGameManager = + new GameManager(startingTeam, mockCardGenerator, mockClueValidationService); + helperMethodSubmitClue(fullListGameManager, 9, startingTeam); + return fullListGameManager; + } + @Test void testConstructorRedStarts() { verify(mockCardGenerator, times(1)) @@ -88,24 +106,6 @@ void testGetWinner_null() { assertNull(gameManager.getWinner()); } - private void helperMethodSubmitClue(GameManager gameManager, int guessAmount) { - gameManager.submitClue(new Clue("Test", guessAmount)); - } - - // Helper method for testing permutation of getWinner() - private @NonNull GameManager helperMethodGenerateFullCardList( - Color cardColor, Team startingTeam) { - List cardList = new ArrayList<>(); - for (int i = 0; i < 25; i++) { - cardList.add(new Card("Test" + i, cardColor)); - } - mockCardGeneration(cardList); - GameManager fullListGameManager = - new GameManager(startingTeam, mockCardGenerator, mockClueValidationService); - helperMethodSubmitClue(fullListGameManager, 9); - return fullListGameManager; - } - @Test void testGetWinner_redStartsRedWins() { gameManager = helperMethodGenerateFullCardList(Color.RED, redTeam); @@ -150,7 +150,7 @@ void testGetWinner_blueStartsBlueWins() { void testGetWinner_redFoundBlackCardFound() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); - helperMethodSubmitClue(gameManager, 1); + helperMethodSubmitClue(gameManager, 1, blueTeam); gameManager.flipCard(0, redTeam); assertEquals(blueTeam, gameManager.getWinner()); } @@ -159,7 +159,7 @@ void testGetWinner_redFoundBlackCardFound() { void testGetWinner_blueFoundBlackCardFound() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); - helperMethodSubmitClue(gameManager, 1); + helperMethodSubmitClue(gameManager, 1, blueTeam); gameManager.flipCard(0, blueTeam); assertEquals(redTeam, gameManager.getWinner()); } @@ -168,14 +168,14 @@ void testGetWinner_blueFoundBlackCardFound() { void testFlipWhiteCard() { mockCardGeneration(List.of(new Card("Test", Color.WHITE))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); - helperMethodSubmitClue(gameManager, 1); - gameManager.flipCard(0, blueTeam); + helperMethodSubmitClue(gameManager, 1, redTeam); + gameManager.flipCard(0, redTeam); assertNull(gameManager.getWinner()); } @Test void testFlipCard_cardAlreadyFlipped() { - helperMethodSubmitClue(gameManager, 1); + helperMethodSubmitClue(gameManager, 1, redTeam); gameManager.flipCard(0, redTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, redTeam)); } @@ -184,7 +184,7 @@ void testFlipCard_cardAlreadyFlipped() { void testFlipCard_winnerAlreadyDetermined() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); - helperMethodSubmitClue(gameManager, 1); + helperMethodSubmitClue(gameManager, 1, blueTeam); gameManager.flipCard(0, blueTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, redTeam)); } @@ -204,7 +204,7 @@ void testGetCurrentBlueFoundCards() { @Test void testSubmitClue() { Clue validClue = new Clue("Test", 2); - gameManager.submitClue(validClue); + gameManager.submitClue(validClue, redTeam); assertEquals(validClue, gameManager.getCurrentClue()); assertEquals(3, gameManager.getRemainingGuesses()); } @@ -213,20 +213,20 @@ void testSubmitClue() { void testOutOfGuesses() { mockCardGeneration(List.of(new Card("Test", Color.RED), new Card("Test2", Color.RED))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); - helperMethodSubmitClue(gameManager, 0); + helperMethodSubmitClue(gameManager, 0, redTeam); gameManager.flipCard(0, redTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(1, redTeam)); } @Test void testGetCurrentClueWord() { - helperMethodSubmitClue(gameManager, 1); + helperMethodSubmitClue(gameManager, 1, redTeam); assertEquals("Test", gameManager.getCurrentClueWord()); } @Test void testGetRemainingGuesses() { - helperMethodSubmitClue(gameManager, 1); + helperMethodSubmitClue(gameManager, 1, redTeam); assertEquals(2, gameManager.getRemainingGuesses()); } @@ -234,7 +234,7 @@ void testGetRemainingGuesses() { void testSubmitClue_invalidClue() { when(mockClueValidationService.validateWord(any(), anyString())).thenReturn(false); Clue invalidClue = new Clue("InvalidClue", 1); - assertThrows(IllegalArgumentException.class, () -> gameManager.submitClue((invalidClue))); + assertThrows(IllegalArgumentException.class, () -> gameManager.submitClue((invalidClue), redTeam)); } @Test From 5cfc33fe973fac3d80c51024ffd4d05539e38b62 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 20:47:38 +0200 Subject: [PATCH 062/207] test: add test to ensure we start with correct team and role --- .../backend/playingfield/GameManagerTest.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 8174177c..b68429b2 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -241,4 +241,15 @@ void testSubmitClue_invalidClue() { void testGetCurrentClueWordNullUponInitialization() { assertNull(gameManager.getCurrentClueWord()); } + + @Test + void testCorrectStart_redTeam() { + assertEquals(redTeam, gameManager.getCurrentTurn()); + } + + @Test + void testCorrectStart_spymaster() { + assertEquals(Role.SPYMASTER, gameManager.getCurrentPhase()); + } + } From 47a1d896aa0b9259afe43bc1284d999f820890fb Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 20:48:08 +0200 Subject: [PATCH 063/207] test: add test to ensure correct state after advancing once --- .../backend/playingfield/GameManagerTest.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index b68429b2..27eb8569 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -252,4 +252,16 @@ void testCorrectStart_spymaster() { assertEquals(Role.SPYMASTER, gameManager.getCurrentPhase()); } + @Test + void testAdvanceTurn_spymasterToOperative() { + gameManager.advanceTurn(); + assertEquals(Role.OPERATIVE, gameManager.getCurrentPhase()); + } + + @Test + void testAdvanceTurn_spymasterToOperative_sameTeam() { + gameManager.advanceTurn(); + assertEquals(redTeam, gameManager.getCurrentTurn()); + } + } From 2cf04a12dbb316e2bba8cf5511a623563eebcf37 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 20:50:14 +0200 Subject: [PATCH 064/207] test: add test for advancing turn twice --- .../backend/playingfield/GameManagerTest.java | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 27eb8569..53647641 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -264,4 +264,25 @@ void testAdvanceTurn_spymasterToOperative_sameTeam() { assertEquals(redTeam, gameManager.getCurrentTurn()); } + @Test + void testAdvanceTurnTwice_operativeToSpymaster() { + gameManager.advanceTurn(); + gameManager.advanceTurn(); + assertEquals(Role.SPYMASTER, gameManager.getCurrentPhase()); + } + + @Test + void testAdvanceTurnTwice_redTeamToBlueTeam() { + gameManager.advanceTurn(); + gameManager.advanceTurn(); + assertEquals(blueTeam, gameManager.getCurrentTurn()); + } + + @Test + void testAdvanceTurnTwice_wipeClue() { + gameManager.advanceTurn(); + gameManager.advanceTurn(); + assertNull(gameManager.getCurrentClue()); + } + } From 50a3e34c3123d0735352c26db6c773d86dc501ba Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 20:51:28 +0200 Subject: [PATCH 065/207] test: add tests for passing turn voluntarily --- .../backend/playingfield/GameManagerTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 53647641..15e8233f 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -285,4 +285,18 @@ void testAdvanceTurnTwice_wipeClue() { assertNull(gameManager.getCurrentClue()); } + @Test + void testPassTurn_correctTeam() { + gameManager.advanceTurn(); + gameManager.passTurn(redTeam); + assertEquals(blueTeam, gameManager.getCurrentTurn()); + } + + @Test + void testPassTurn_correctPhase() { + gameManager.advanceTurn(); + gameManager.passTurn(redTeam); + assertEquals(Role.SPYMASTER, gameManager.getCurrentPhase()); + } + } From 048803aba2b7bcdb7abebc9a2988fea6213ccadc Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 20:52:13 +0200 Subject: [PATCH 066/207] test: add test for checking turn and role --- .../backend/playingfield/GameManagerTest.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 15e8233f..5ff1a5b6 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -14,6 +14,7 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.ArrayList; import java.util.List; @@ -234,7 +235,8 @@ void testGetRemainingGuesses() { void testSubmitClue_invalidClue() { when(mockClueValidationService.validateWord(any(), anyString())).thenReturn(false); Clue invalidClue = new Clue("InvalidClue", 1); - assertThrows(IllegalArgumentException.class, () -> gameManager.submitClue((invalidClue), redTeam)); + assertThrows( + IllegalArgumentException.class, () -> gameManager.submitClue((invalidClue), redTeam)); } @Test @@ -299,4 +301,19 @@ void testPassTurn_correctPhase() { assertEquals(Role.SPYMASTER, gameManager.getCurrentPhase()); } + @Test + void testCheckCorrectTurn_throwsWhenWrongRole() { + assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, redTeam)); + } + + @Test + void testCheckCorrectTurn_throwsWhenWrongTeam() { + Clue clue = new Clue("Test", 1); + assertThrows(IllegalStateException.class, () -> gameManager.submitClue(clue, blueTeam)); + } + + @Test + void testPassTurn_throwsWhenSpymaster() { + assertThrows(IllegalStateException.class, () -> gameManager.passTurn(redTeam)); + } } From 74366d1a8e666a2bd596cd718e87f33b98a11c1a Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 21:22:37 +0200 Subject: [PATCH 067/207] test: add test class for GameService --- .../codenames/backend/playingfield/GameSeviceTest.java | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 src/test/java/com/codenames/codenames/backend/playingfield/GameSeviceTest.java diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameSeviceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameSeviceTest.java new file mode 100644 index 00000000..5213178a --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameSeviceTest.java @@ -0,0 +1,4 @@ +package com.codenames.codenames.backend.playingfield; + +public class GameSeviceTest { +} From 24217b8082a648df487a45feab20a1d4eb11c652 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 21:24:00 +0200 Subject: [PATCH 068/207] fix errors in test due to incorrect turn and game phase After introducing a turn system to GameManager, the class became a state machine that had checks to ensure that only the correct team can call the methods when they are actually at turn. This broke the test class as it was previously built on a stateless version of GameManager. --- .../backend/playingfield/GameManagerTest.java | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 5ff1a5b6..00097a89 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -18,7 +18,6 @@ import com.codenames.codenames.backend.utility.Team; import java.util.ArrayList; import java.util.List; -import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -57,7 +56,7 @@ private void helperMethodSubmitClue(GameManager gameManager, int guessAmount, Te } // Helper method for testing permutation of getWinner() - private @NonNull GameManager helperMethodGenerateFullCardList( + private GameManager helperMethodGenerateFullCardList( Color cardColor, Team startingTeam) { List cardList = new ArrayList<>(); for (int i = 0; i < 25; i++) { @@ -111,6 +110,7 @@ void testGetWinner_null() { void testGetWinner_redStartsRedWins() { gameManager = helperMethodGenerateFullCardList(Color.RED, redTeam); + gameManager.advanceTurn(); for (int i = 0; i < 9; i++) { gameManager.flipCard(i, redTeam); } @@ -121,6 +121,11 @@ void testGetWinner_redStartsRedWins() { void testGetWinner_redStartsBlueWins() { gameManager = helperMethodGenerateFullCardList(Color.BLUE, redTeam); + gameManager.advanceTurn(); // red operative + gameManager.advanceTurn(); // blue spymaster + helperMethodSubmitClue(gameManager, 8, blueTeam); + gameManager.advanceTurn(); // blue operative + for (int i = 0; i < 8; i++) { gameManager.flipCard(i, blueTeam); } @@ -130,6 +135,10 @@ void testGetWinner_redStartsBlueWins() { @Test void testGetWinner_blueStartsRedWins() { gameManager = helperMethodGenerateFullCardList(Color.RED, blueTeam); + gameManager.advanceTurn(); // blue operative + gameManager.advanceTurn(); // red spymaster + helperMethodSubmitClue(gameManager, 8, redTeam); + gameManager.advanceTurn(); // red operative for (int i = 0; i < 8; i++) { gameManager.flipCard(i, redTeam); @@ -141,6 +150,7 @@ void testGetWinner_blueStartsRedWins() { void testGetWinner_blueStartsBlueWins() { gameManager = helperMethodGenerateFullCardList(Color.BLUE, blueTeam); + gameManager.advanceTurn(); for (int i = 0; i < 9; i++) { gameManager.flipCard(i, blueTeam); } @@ -150,8 +160,13 @@ void testGetWinner_blueStartsBlueWins() { @Test void testGetWinner_redFoundBlackCardFound() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); - gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); + gameManager = new GameManager(blueTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, blueTeam); + gameManager.advanceTurn(); // blue operative + gameManager.advanceTurn(); // red spymaster + helperMethodSubmitClue(gameManager, 1, redTeam); + gameManager.advanceTurn(); // red operative + gameManager.flipCard(0, redTeam); assertEquals(blueTeam, gameManager.getWinner()); } @@ -160,7 +175,10 @@ void testGetWinner_redFoundBlackCardFound() { void testGetWinner_blueFoundBlackCardFound() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); + gameManager.advanceTurn(); + gameManager.advanceTurn(); helperMethodSubmitClue(gameManager, 1, blueTeam); + gameManager.advanceTurn(); gameManager.flipCard(0, blueTeam); assertEquals(redTeam, gameManager.getWinner()); } @@ -170,6 +188,7 @@ void testFlipWhiteCard() { mockCardGeneration(List.of(new Card("Test", Color.WHITE))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, redTeam); + gameManager.advanceTurn(); gameManager.flipCard(0, redTeam); assertNull(gameManager.getWinner()); } @@ -177,6 +196,7 @@ void testFlipWhiteCard() { @Test void testFlipCard_cardAlreadyFlipped() { helperMethodSubmitClue(gameManager, 1, redTeam); + gameManager.advanceTurn(); gameManager.flipCard(0, redTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, redTeam)); } @@ -184,10 +204,11 @@ void testFlipCard_cardAlreadyFlipped() { @Test void testFlipCard_winnerAlreadyDetermined() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); - gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); + gameManager = new GameManager(blueTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, blueTeam); + gameManager.advanceTurn(); // red operative gameManager.flipCard(0, blueTeam); - assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, redTeam)); + assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, blueTeam)); } @Test @@ -215,6 +236,7 @@ void testOutOfGuesses() { mockCardGeneration(List.of(new Card("Test", Color.RED), new Card("Test2", Color.RED))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 0, redTeam); + gameManager.advanceTurn(); gameManager.flipCard(0, redTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(1, redTeam)); } From 8fbbf92eb2a6194307cb6f2a40af697dc31f6c1e Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 22:03:58 +0200 Subject: [PATCH 069/207] feat: extract GameManager creation out of GameService into Factory --- .../playingfield/GameManagerFactory.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java new file mode 100644 index 00000000..37e10322 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java @@ -0,0 +1,34 @@ +package com.codenames.codenames.backend.playingfield; + +import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.utility.Team; +import org.springframework.stereotype.Component; + +/** Generates GameManager instances to be used by GameService. */ +@Component +public class GameManagerFactory { + private final CardGenerator cardGenerator; + private final ClueValidationService clueValidationService; + + /** + * Initialized the factory with utility services injected via Spring. + * + * @param cardGenerator utility service to generate cards + * @param clueValidationService utility service to validate clues + */ + public GameManagerFactory( + CardGenerator cardGenerator, ClueValidationService clueValidationService) { + this.cardGenerator = cardGenerator; + this.clueValidationService = clueValidationService; + } + + /** + * Creates a GameManager object that is used in GameService. + * + * @param startingTeam the team that starts the game + * @return the GameManager object to be used in GameService + */ + public GameManager create(Team startingTeam) { + return new GameManager(startingTeam, cardGenerator, clueValidationService); + } +} From 1f3f72c29846dbd608f5de0f2f53b5aca39ffce1 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 22:07:21 +0200 Subject: [PATCH 070/207] refactor: use factory instead of creating GameService was responsible for creating and managing all the instances of GameManager. To avoid violating SRP, the creation of the GameManager was extracted into a factory class. --- .../backend/playingfield/GameService.java | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 14349f80..716dd0c8 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -8,7 +8,7 @@ import org.springframework.stereotype.Service; /** - * Service class for the game. The class handles the creation and storing of a GameManager for each + * Service class for the game. The class stores an instance of GameManager for each * lobby. This also exposes the methods of the GameManager, so that the websocket controllers can * have message mappings to allow frontend to interact with the backend. */ @@ -16,18 +16,15 @@ public class GameService { private final Map games = new ConcurrentHashMap<>(); - private final CardGenerator cardGenerator; - private final ClueValidationService clueValidationService; + private final GameManagerFactory gameManagerFactory; /** * Constructor for a GameService object. * - * @param cardGenerator the card generator required for creating the cards in the manager class - * @param clueValidationService the service required for validating clues in the manager class + * @param gameManagerFactory the factory responsible for generating GameManagers */ - public GameService(CardGenerator cardGenerator, ClueValidationService clueValidationService) { - this.cardGenerator = cardGenerator; - this.clueValidationService = clueValidationService; + public GameService(GameManagerFactory gameManagerFactory) { + this.gameManagerFactory = gameManagerFactory; } /** @@ -39,7 +36,7 @@ public GameService(CardGenerator cardGenerator, ClueValidationService clueValida */ public void createGameManager(String lobbyCode, Team startingTeam) { games.computeIfAbsent( - lobbyCode, game -> new GameManager(startingTeam, cardGenerator, clueValidationService)); + lobbyCode, key -> gameManagerFactory.create(startingTeam)); } /** From 26e78e14938bacae220b58adc3f11dcd541b3fdb Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 22:37:10 +0200 Subject: [PATCH 071/207] refactor: delete unused import --- .../codenames/codenames/backend/playingfield/GameService.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 716dd0c8..fe190a1a 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -1,7 +1,6 @@ package com.codenames.codenames.backend.playingfield; import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.utility.Team; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; From a1753a79b3ff8e3c1413a0db354988931be2830e Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 22:45:09 +0200 Subject: [PATCH 072/207] test: fix InvalidUseOfMatchers error --- .../codenames/backend/lobby/services/LobbyServiceTest.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index 0b818e9e..fa402268 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -5,6 +5,8 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -254,6 +256,6 @@ void testGetPlayerRole_nonExistentPlayer() { @Test void testAddGameManagerForLobby() { lobbyService.createLobby("Host"); - verify(gameService, times(1)).createGameManager("ABCDE", Team.RED); + verify(gameService, times(1)).createGameManager(eq("ABCDE"), any(Team.class)); } -} \ No newline at end of file +} From a731794699f681b5defd65d4cbede1f9ff189d82 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 22:45:25 +0200 Subject: [PATCH 073/207] refactor: rename class --- .../codenames/backend/playingfield/GameSeviceTest.java | 4 ---- 1 file changed, 4 deletions(-) delete mode 100644 src/test/java/com/codenames/codenames/backend/playingfield/GameSeviceTest.java diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameSeviceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameSeviceTest.java deleted file mode 100644 index 5213178a..00000000 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameSeviceTest.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.codenames.codenames.backend.playingfield; - -public class GameSeviceTest { -} From fc8a12185d8f1aeea1bc39a4df28c72fe3cd4a15 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 22:45:45 +0200 Subject: [PATCH 074/207] test: add tests for GameService --- .../backend/playingfield/GameServiceTest.java | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java new file mode 100644 index 00000000..0b5116e0 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -0,0 +1,73 @@ +package com.codenames.codenames.backend.playingfield; + +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.utility.Team; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GameServiceTest { + private GameService gameService; + private GameManager mockGameManager; + private GameManagerFactory mockGameManagerFactory; + + private final String lobbyCode = "ABCDE"; + + @BeforeEach + void setup() { + mockGameManagerFactory = mock(GameManagerFactory.class); + mockGameManager = mock(GameManager.class); + + gameService = new GameService(mockGameManagerFactory); + when(mockGameManagerFactory.create(Team.RED)).thenReturn(mockGameManager); + + gameService.createGameManager(lobbyCode, Team.RED); + } + + @Test + void testCreateGameManager_oneInvocation() { + verify(mockGameManagerFactory, times(1)).create(Team.RED); + } + + @Test + void testCreateGameManager_twoInvocations_noDuplicates() { + gameService.createGameManager(lobbyCode, Team.RED); + gameService.createGameManager(lobbyCode, Team.RED); + verify(mockGameManagerFactory, times(1)).create(Team.RED); + } + + @Test + void testRemoveGame() { + gameService.removeGame(lobbyCode); + + assertThrows(IllegalStateException.class, () -> gameService.flipCard(lobbyCode, 0, Team.RED)); + } + + @Test + void testSubmitClue() { + Clue mockClue = mock(Clue.class); + + gameService.submitClue(lobbyCode, mockClue, Team.RED); + verify(mockGameManager, times(1)).submitClue(mockClue, Team.RED); + verify(mockGameManager, times(1)).advanceTurn(); + } + + @Test + void testFlipCard() { + gameService.flipCard(lobbyCode, 0, Team.RED); + + verify(mockGameManager, times(1)).flipCard(0, Team.RED); + } + + @Test + void testPassTurn() { + gameService.passTurn(lobbyCode, Team.RED); + + verify(mockGameManager, times(1)).passTurn(Team.RED); + } +} From 9da49388a2322351e14680de77ec1e9258766036 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 22:54:19 +0200 Subject: [PATCH 075/207] test: add test class for GameManagerFactory --- .../playingfield/GameManagerFactoryTest.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java new file mode 100644 index 00000000..48574950 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java @@ -0,0 +1,29 @@ +package com.codenames.codenames.backend.playingfield; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mock; + +import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.utility.Team; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class GameManagerFactoryTest { + + private GameManagerFactory gameManagerFactory; + + @BeforeEach + void setup() { + CardGenerator mockCardGenerator = mock(CardGenerator.class); + ClueValidationService mockClueValidationService = mock(ClueValidationService.class); + + gameManagerFactory = new GameManagerFactory(mockCardGenerator, mockClueValidationService); + } + + @Test + void testCreate() { + GameManager gameManager = gameManagerFactory.create(Team.RED); + + assertNotNull(gameManager); + } +} From ab8c491b4b0bede0039bc8b188c226f96fd21420 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 22:57:14 +0200 Subject: [PATCH 076/207] refactor: define variables for maintenance --- .../backend/playingfield/GameServiceTest.java | 28 ++++++++++--------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index 0b5116e0..69b83080 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -17,6 +17,7 @@ class GameServiceTest { private GameManagerFactory mockGameManagerFactory; private final String lobbyCode = "ABCDE"; + private final Team redTeam = Team.RED; @BeforeEach void setup() { @@ -24,50 +25,51 @@ void setup() { mockGameManager = mock(GameManager.class); gameService = new GameService(mockGameManagerFactory); - when(mockGameManagerFactory.create(Team.RED)).thenReturn(mockGameManager); + when(mockGameManagerFactory.create(redTeam)).thenReturn(mockGameManager); - gameService.createGameManager(lobbyCode, Team.RED); + gameService.createGameManager(lobbyCode, redTeam); } @Test void testCreateGameManager_oneInvocation() { - verify(mockGameManagerFactory, times(1)).create(Team.RED); + verify(mockGameManagerFactory, times(1)).create(redTeam); } @Test void testCreateGameManager_twoInvocations_noDuplicates() { - gameService.createGameManager(lobbyCode, Team.RED); - gameService.createGameManager(lobbyCode, Team.RED); - verify(mockGameManagerFactory, times(1)).create(Team.RED); + gameService.createGameManager(lobbyCode, redTeam); + gameService.createGameManager(lobbyCode, redTeam); + verify(mockGameManagerFactory, times(1)).create(redTeam); } @Test void testRemoveGame() { gameService.removeGame(lobbyCode); - assertThrows(IllegalStateException.class, () -> gameService.flipCard(lobbyCode, 0, Team.RED)); + assertThrows(IllegalStateException.class, () -> gameService.flipCard(lobbyCode, 0, redTeam)); } @Test void testSubmitClue() { Clue mockClue = mock(Clue.class); - gameService.submitClue(lobbyCode, mockClue, Team.RED); - verify(mockGameManager, times(1)).submitClue(mockClue, Team.RED); + gameService.submitClue(lobbyCode, mockClue, redTeam); + verify(mockGameManager, times(1)).submitClue(mockClue, redTeam); verify(mockGameManager, times(1)).advanceTurn(); } @Test void testFlipCard() { - gameService.flipCard(lobbyCode, 0, Team.RED); + int firstCard = 0; + gameService.flipCard(lobbyCode, firstCard, redTeam); - verify(mockGameManager, times(1)).flipCard(0, Team.RED); + verify(mockGameManager, times(1)).flipCard(firstCard, redTeam); } @Test void testPassTurn() { - gameService.passTurn(lobbyCode, Team.RED); + gameService.passTurn(lobbyCode, redTeam); - verify(mockGameManager, times(1)).passTurn(Team.RED); + verify(mockGameManager, times(1)).passTurn(redTeam); } } From 85bde15ddff5bd8965ff7d0b92e4cbb61071eb7d Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 23:06:01 +0200 Subject: [PATCH 077/207] refactor: remove smells and SOLID violations --- .../backend/playingfield/GameManagerTest.java | 38 ++++++++++--------- 1 file changed, 20 insertions(+), 18 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 00097a89..9c175395 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -30,6 +30,8 @@ class GameManagerTest { private static final int BLACK_CARDS = 1; private static final Team redTeam = Team.RED; private static final Team blueTeam = Team.BLUE; + private static final Color redColor = Color.RED; + private static final Color blueColor = Color.BLUE; private GameManager gameManager; private CardGenerator mockCardGenerator; private ClueValidationService mockClueValidationService; @@ -40,7 +42,7 @@ class GameManagerTest { void setUp() { mockCardGenerator = mock(CardGenerator.class); mockClueValidationService = mock(ClueValidationService.class); - mockCardGeneration(List.of(new Card("Test", Color.RED))); + mockCardGeneration(List.of(new Card("Test", redColor))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); when(mockClueValidationService.validateWord(any(), anyString())).thenReturn(true); } @@ -59,13 +61,13 @@ private void helperMethodSubmitClue(GameManager gameManager, int guessAmount, Te private GameManager helperMethodGenerateFullCardList( Color cardColor, Team startingTeam) { List cardList = new ArrayList<>(); - for (int i = 0; i < 25; i++) { + for (int i = 0; i < TOTAL_CARDS; i++) { cardList.add(new Card("Test" + i, cardColor)); } mockCardGeneration(cardList); GameManager fullListGameManager = new GameManager(startingTeam, mockCardGenerator, mockClueValidationService); - helperMethodSubmitClue(fullListGameManager, 9, startingTeam); + helperMethodSubmitClue(fullListGameManager, STARTING_TEAM_CARDS, startingTeam); return fullListGameManager; } @@ -98,7 +100,7 @@ void testGetCardList() { @Test void testCheckColor() { - assertEquals(Color.RED, gameManager.checkColor(0)); + assertEquals(redColor, gameManager.checkColor(0)); } @Test @@ -108,10 +110,10 @@ void testGetWinner_null() { @Test void testGetWinner_redStartsRedWins() { - gameManager = helperMethodGenerateFullCardList(Color.RED, redTeam); + gameManager = helperMethodGenerateFullCardList(redColor, redTeam); gameManager.advanceTurn(); - for (int i = 0; i < 9; i++) { + for (int i = 0; i < STARTING_TEAM_CARDS; i++) { gameManager.flipCard(i, redTeam); } assertEquals(redTeam, gameManager.getWinner()); @@ -119,14 +121,14 @@ void testGetWinner_redStartsRedWins() { @Test void testGetWinner_redStartsBlueWins() { - gameManager = helperMethodGenerateFullCardList(Color.BLUE, redTeam); + gameManager = helperMethodGenerateFullCardList(blueColor, redTeam); gameManager.advanceTurn(); // red operative gameManager.advanceTurn(); // blue spymaster - helperMethodSubmitClue(gameManager, 8, blueTeam); + helperMethodSubmitClue(gameManager, SECOND_TEAM_CARDS, blueTeam); gameManager.advanceTurn(); // blue operative - for (int i = 0; i < 8; i++) { + for (int i = 0; i < SECOND_TEAM_CARDS; i++) { gameManager.flipCard(i, blueTeam); } assertEquals(blueTeam, gameManager.getWinner()); @@ -134,13 +136,13 @@ void testGetWinner_redStartsBlueWins() { @Test void testGetWinner_blueStartsRedWins() { - gameManager = helperMethodGenerateFullCardList(Color.RED, blueTeam); + gameManager = helperMethodGenerateFullCardList(redColor, blueTeam); gameManager.advanceTurn(); // blue operative gameManager.advanceTurn(); // red spymaster helperMethodSubmitClue(gameManager, 8, redTeam); gameManager.advanceTurn(); // red operative - for (int i = 0; i < 8; i++) { + for (int i = 0; i < SECOND_TEAM_CARDS; i++) { gameManager.flipCard(i, redTeam); } assertEquals(redTeam, gameManager.getWinner()); @@ -148,10 +150,10 @@ void testGetWinner_blueStartsRedWins() { @Test void testGetWinner_blueStartsBlueWins() { - gameManager = helperMethodGenerateFullCardList(Color.BLUE, blueTeam); + gameManager = helperMethodGenerateFullCardList(blueColor, blueTeam); gameManager.advanceTurn(); - for (int i = 0; i < 9; i++) { + for (int i = 0; i < STARTING_TEAM_CARDS; i++) { gameManager.flipCard(i, blueTeam); } assertEquals(blueTeam, gameManager.getWinner()); @@ -225,15 +227,14 @@ void testGetCurrentBlueFoundCards() { @Test void testSubmitClue() { - Clue validClue = new Clue("Test", 2); + Clue validClue = new Clue("Test", 1); gameManager.submitClue(validClue, redTeam); assertEquals(validClue, gameManager.getCurrentClue()); - assertEquals(3, gameManager.getRemainingGuesses()); } @Test void testOutOfGuesses() { - mockCardGeneration(List.of(new Card("Test", Color.RED), new Card("Test2", Color.RED))); + mockCardGeneration(List.of(new Card("Test", redColor), new Card("Test2", redColor))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 0, redTeam); gameManager.advanceTurn(); @@ -249,8 +250,9 @@ void testGetCurrentClueWord() { @Test void testGetRemainingGuesses() { - helperMethodSubmitClue(gameManager, 1, redTeam); - assertEquals(2, gameManager.getRemainingGuesses()); + int guessAmount = 2; // According to game rules, players have guessAmount + 1 + helperMethodSubmitClue(gameManager, guessAmount, redTeam); + assertEquals(guessAmount + 1, gameManager.getRemainingGuesses()); } @Test From aec73774add26e475bbb905b8e60b21804442805 Mon Sep 17 00:00:00 2001 From: XtophB Date: Thu, 14 May 2026 23:29:27 +0200 Subject: [PATCH 078/207] refactor: replace DRY violations with helper methods Originally gameManager.advancecTurn(); was repeatedly used on intention to reduce overhead when reading code to understand the turn skipping. After weighing pro and cons, decided to introduce helper method to remove DRY violations and left comments to improve code legibility. --- .../backend/playingfield/GameManagerTest.java | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 9c175395..8bbc2125 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -58,8 +58,7 @@ private void helperMethodSubmitClue(GameManager gameManager, int guessAmount, Te } // Helper method for testing permutation of getWinner() - private GameManager helperMethodGenerateFullCardList( - Color cardColor, Team startingTeam) { + private GameManager helperMethodGenerateFullCardList(Color cardColor, Team startingTeam) { List cardList = new ArrayList<>(); for (int i = 0; i < TOTAL_CARDS; i++) { cardList.add(new Card("Test" + i, cardColor)); @@ -71,6 +70,12 @@ private GameManager helperMethodGenerateFullCardList( return fullListGameManager; } + private void helperMethodAdvanceTurns(int advanceAmount) { + for (int i = 0; i < advanceAmount; i++) { + gameManager.advanceTurn(); + } + } + @Test void testConstructorRedStarts() { verify(mockCardGenerator, times(1)) @@ -112,7 +117,7 @@ void testGetWinner_null() { void testGetWinner_redStartsRedWins() { gameManager = helperMethodGenerateFullCardList(redColor, redTeam); - gameManager.advanceTurn(); + helperMethodAdvanceTurns(1); for (int i = 0; i < STARTING_TEAM_CARDS; i++) { gameManager.flipCard(i, redTeam); } @@ -123,10 +128,9 @@ void testGetWinner_redStartsRedWins() { void testGetWinner_redStartsBlueWins() { gameManager = helperMethodGenerateFullCardList(blueColor, redTeam); - gameManager.advanceTurn(); // red operative - gameManager.advanceTurn(); // blue spymaster + helperMethodAdvanceTurns(2); // blue spymaster helperMethodSubmitClue(gameManager, SECOND_TEAM_CARDS, blueTeam); - gameManager.advanceTurn(); // blue operative + helperMethodAdvanceTurns(1); // blue operative for (int i = 0; i < SECOND_TEAM_CARDS; i++) { gameManager.flipCard(i, blueTeam); @@ -137,10 +141,9 @@ void testGetWinner_redStartsBlueWins() { @Test void testGetWinner_blueStartsRedWins() { gameManager = helperMethodGenerateFullCardList(redColor, blueTeam); - gameManager.advanceTurn(); // blue operative - gameManager.advanceTurn(); // red spymaster + helperMethodAdvanceTurns(2); // red spymaster helperMethodSubmitClue(gameManager, 8, redTeam); - gameManager.advanceTurn(); // red operative + helperMethodAdvanceTurns(1); // red operative for (int i = 0; i < SECOND_TEAM_CARDS; i++) { gameManager.flipCard(i, redTeam); @@ -152,7 +155,7 @@ void testGetWinner_blueStartsRedWins() { void testGetWinner_blueStartsBlueWins() { gameManager = helperMethodGenerateFullCardList(blueColor, blueTeam); - gameManager.advanceTurn(); + helperMethodAdvanceTurns(1); for (int i = 0; i < STARTING_TEAM_CARDS; i++) { gameManager.flipCard(i, blueTeam); } @@ -164,10 +167,9 @@ void testGetWinner_redFoundBlackCardFound() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); gameManager = new GameManager(blueTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, blueTeam); - gameManager.advanceTurn(); // blue operative - gameManager.advanceTurn(); // red spymaster + helperMethodAdvanceTurns(2); // red spymaster helperMethodSubmitClue(gameManager, 1, redTeam); - gameManager.advanceTurn(); // red operative + helperMethodAdvanceTurns(1); // red operative gameManager.flipCard(0, redTeam); assertEquals(blueTeam, gameManager.getWinner()); @@ -177,10 +179,9 @@ void testGetWinner_redFoundBlackCardFound() { void testGetWinner_blueFoundBlackCardFound() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); - gameManager.advanceTurn(); - gameManager.advanceTurn(); + helperMethodAdvanceTurns(2); // blue spymaster helperMethodSubmitClue(gameManager, 1, blueTeam); - gameManager.advanceTurn(); + helperMethodAdvanceTurns(1); // blue operative gameManager.flipCard(0, blueTeam); assertEquals(redTeam, gameManager.getWinner()); } @@ -190,7 +191,7 @@ void testFlipWhiteCard() { mockCardGeneration(List.of(new Card("Test", Color.WHITE))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, redTeam); - gameManager.advanceTurn(); + helperMethodAdvanceTurns(1); gameManager.flipCard(0, redTeam); assertNull(gameManager.getWinner()); } @@ -198,7 +199,7 @@ void testFlipWhiteCard() { @Test void testFlipCard_cardAlreadyFlipped() { helperMethodSubmitClue(gameManager, 1, redTeam); - gameManager.advanceTurn(); + helperMethodAdvanceTurns(1); gameManager.flipCard(0, redTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, redTeam)); } @@ -208,7 +209,7 @@ void testFlipCard_winnerAlreadyDetermined() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); gameManager = new GameManager(blueTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, blueTeam); - gameManager.advanceTurn(); // red operative + helperMethodAdvanceTurns(1); // red operative gameManager.flipCard(0, blueTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, blueTeam)); } @@ -237,7 +238,7 @@ void testOutOfGuesses() { mockCardGeneration(List.of(new Card("Test", redColor), new Card("Test2", redColor))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 0, redTeam); - gameManager.advanceTurn(); + helperMethodAdvanceTurns(1); gameManager.flipCard(0, redTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(1, redTeam)); } @@ -280,47 +281,44 @@ void testCorrectStart_spymaster() { @Test void testAdvanceTurn_spymasterToOperative() { - gameManager.advanceTurn(); + helperMethodAdvanceTurns(1); assertEquals(Role.OPERATIVE, gameManager.getCurrentPhase()); } @Test void testAdvanceTurn_spymasterToOperative_sameTeam() { - gameManager.advanceTurn(); + helperMethodAdvanceTurns(1); assertEquals(redTeam, gameManager.getCurrentTurn()); } @Test void testAdvanceTurnTwice_operativeToSpymaster() { - gameManager.advanceTurn(); - gameManager.advanceTurn(); + helperMethodAdvanceTurns(2); assertEquals(Role.SPYMASTER, gameManager.getCurrentPhase()); } @Test void testAdvanceTurnTwice_redTeamToBlueTeam() { - gameManager.advanceTurn(); - gameManager.advanceTurn(); + helperMethodAdvanceTurns(2); assertEquals(blueTeam, gameManager.getCurrentTurn()); } @Test void testAdvanceTurnTwice_wipeClue() { - gameManager.advanceTurn(); - gameManager.advanceTurn(); + helperMethodAdvanceTurns(2); assertNull(gameManager.getCurrentClue()); } @Test void testPassTurn_correctTeam() { - gameManager.advanceTurn(); + helperMethodAdvanceTurns(1); gameManager.passTurn(redTeam); assertEquals(blueTeam, gameManager.getCurrentTurn()); } @Test void testPassTurn_correctPhase() { - gameManager.advanceTurn(); + helperMethodAdvanceTurns(1); gameManager.passTurn(redTeam); assertEquals(Role.SPYMASTER, gameManager.getCurrentPhase()); } From 77803906a27408355854f09ab593632be33685a4 Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 14:00:14 +0200 Subject: [PATCH 079/207] change lobby controller to use proper http methods --- .../lobby/controller/LobbyController.java | 196 +++++++++--------- 1 file changed, 98 insertions(+), 98 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 86c95c51..9d5673e0 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -19,115 +19,115 @@ @RequestMapping("/lobby") public class LobbyController { - private final LobbyService service; + private final LobbyService service; - /** - * Creates a new {@code LobbyController}. - * - * @param service the lobby service used to handle business logic - */ - public LobbyController(LobbyService service) { - this.service = service; - } + /** + * Creates a new {@code LobbyController}. + * + * @param service the lobby service used to handle business logic + */ + public LobbyController(LobbyService service) { + this.service = service; + } - /** - * Handles a request to create a new lobby. - * - * @param username the username of the requesting user - * @return a response containing the result and the generated lobby code - */ + /** + * Handles a request to create a new lobby. + * + * @param username the username of the requesting user + * @return a response containing the result and the generated lobby code + */ - @PostMapping("/create") - public ResponseEntity createLobby(@RequestParam String username) { - String lobbyCode = service.createLobby(username); - if (lobbyCode == null || lobbyCode.isBlank()) { - return ResponseEntity.internalServerError() - .body(new LobbyResponse("Error while creating lobby.", "", null)); - } else { - List players = service.getPlayersDto(lobbyCode); - return ResponseEntity.ok(new LobbyResponse("Successfully created Lobby.", lobbyCode, players)); + @GetMapping("/create") + public ResponseEntity createLobby(@RequestParam String username) { + String lobbyCode = service.createLobby(username); + if (lobbyCode == null || lobbyCode.isBlank()) { + return ResponseEntity.internalServerError() + .body(new LobbyResponse("Error while creating lobby.", "", null)); + } else { + List players = service.getPlayersDto(lobbyCode); + return ResponseEntity.ok(new LobbyResponse("Successfully created Lobby.", lobbyCode, players)); + } } - } - /** - * Handles a request to join an existing lobby. - * - * @param username the username of the player - * @param lobbyCode the lobby code identifying the lobby - * @return a response indicating whether the join was successful - */ - @PostMapping("/join") - public ResponseEntity joinLobby( - @RequestParam String username, @RequestParam String lobbyCode) { - boolean joined = service.joinLobby(username, lobbyCode); - if (joined) { - return ResponseEntity.ok(new LobbyResponse("Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); - } else { - return ResponseEntity.badRequest() - .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); + /** + * Handles a request to join an existing lobby. + * + * @param username the username of the player + * @param lobbyCode the lobby code identifying the lobby + * @return a response indicating whether the join was successful + */ + @GetMapping("/{lobbyCode}/join") + public ResponseEntity joinLobby( + @RequestParam String username, @PathVariable String lobbyCode) { + boolean joined = service.joinLobby(username, lobbyCode); + if (joined) { + return ResponseEntity.ok(new LobbyResponse("Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); + } else { + return ResponseEntity.badRequest() + .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); + } } - } - /** - * Handles a request to leave a lobby. - * - * @param username the username of the player - * @param lobbyCode the lobby code identifying the lobby - * @return a response indicating whether the operation was successful - */ - @PostMapping("/{lobbyCode}/leave") - public ResponseEntity leaveLobby( - @PathVariable String lobbyCode, - @RequestParam String username) { - boolean left = service.leaveLobby(username, lobbyCode); - System.out.println("LobbyCode: " + lobbyCode); - System.out.println("Username: " + username); - if (left) { - return ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); - } else { - return ResponseEntity.badRequest() - .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); + /** + * Handles a request to leave a lobby. + * + * @param username the username of the player + * @param lobbyCode the lobby code identifying the lobby + * @return a response indicating whether the operation was successful + */ + @GetMapping("/{lobbyCode}/leave") + public ResponseEntity leaveLobby( + @PathVariable String lobbyCode, + @RequestParam String username) { + boolean left = service.leaveLobby(username, lobbyCode); + System.out.println("LobbyCode: " + lobbyCode); + System.out.println("Username: " + username); + if (left) { + return ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); + } else { + return ResponseEntity.badRequest() + .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); + } } - } - @GetMapping("/{lobbyCode}") - public ResponseEntity getLobbyInfo( - @PathVariable String lobbyCode - ) { - List players = service.getPlayersDto(lobbyCode); - if (players != null) { - return ResponseEntity.ok(new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players)); - } else { - return ResponseEntity.badRequest() - .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); + @GetMapping("/{lobbyCode}") + public ResponseEntity getLobbyInfo( + @PathVariable String lobbyCode + ) { + List players = service.getPlayersDto(lobbyCode); + if (players != null) { + return ResponseEntity.ok(new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players)); + } else { + return ResponseEntity.badRequest() + .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); + } } - } - /** - * Handles a request to select a team and role for a player. - * - * @param request the position selection request containing username, lobby code, team, and role - * @return a response indicating whether the selection was successful - */ - @PostMapping("/{lobbyCode}/select-position") - public ResponseEntity selectPosition( - @PathVariable String lobbyCode, @RequestBody PlayerDto request - ) { - boolean updated = service.selectPosition( - request.username(), - lobbyCode, - request.team(), - request.role() - ); + /** + * Handles a request to select a team and role for a player. + * + * @param request the position selection request containing username, lobby code, team, and role + * @return a response indicating whether the selection was successful + */ + @PostMapping("/{lobbyCode}/select-position") + public ResponseEntity selectPosition( + @PathVariable String lobbyCode, @RequestBody PlayerDto request + ) { + boolean updated = service.selectPosition( + request.username(), + lobbyCode, + request.team(), + request.role() + ); - if (updated) { - return ResponseEntity.ok( - new LobbyResponse("Position selected successfully.", lobbyCode, service.getPlayersDto(lobbyCode)) - ); - } else { - return ResponseEntity.badRequest().body( - new LobbyResponse("Could not assign selected team/role.", lobbyCode, service.getPlayersDto(lobbyCode)) - ); + if (updated) { + return ResponseEntity.ok( + new LobbyResponse("Position selected successfully.", lobbyCode, service.getPlayersDto(lobbyCode)) + ); + } else { + return ResponseEntity.badRequest().body( + new LobbyResponse("Could not assign selected team/role.", lobbyCode, service.getPlayersDto(lobbyCode)) + ); + } } - } } From 11a41c076798cd565f686831c65195cd903ca0b8 Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 16:12:10 +0200 Subject: [PATCH 080/207] added logger to lobby service --- .../lobby/controller/LobbyController.java | 9 +- .../backend/lobby/services/LobbyService.java | 356 +++++++++--------- 2 files changed, 188 insertions(+), 177 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 9d5673e0..f557ee08 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -20,6 +20,7 @@ public class LobbyController { private final LobbyService service; + private static final String LOBBY_NOT_FOUND = "Could not find lobby"; /** * Creates a new {@code LobbyController}. @@ -64,7 +65,7 @@ public ResponseEntity joinLobby( return ResponseEntity.ok(new LobbyResponse("Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); } } @@ -80,13 +81,11 @@ public ResponseEntity leaveLobby( @PathVariable String lobbyCode, @RequestParam String username) { boolean left = service.leaveLobby(username, lobbyCode); - System.out.println("LobbyCode: " + lobbyCode); - System.out.println("Username: " + username); if (left) { return ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); } } @@ -99,7 +98,7 @@ public ResponseEntity getLobbyInfo( return ResponseEntity.ok(new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players)); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse("Could not find lobby.", lobbyCode, null)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); } } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index b4d267f7..86a15359 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -5,11 +5,12 @@ import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.stereotype.Service; /** * Service responsible for managing lobbies and player interactions. @@ -17,192 +18,203 @@ *

Handles creation of lobbies, player joins/leaves, and retrieval of lobby data. * Ensures uniqueness of lobby codes and thread-safe access to lobby storage. */ + +@Slf4j @Service public class LobbyService { - private final Map lobbyList = new ConcurrentHashMap<>(); - private final LobbyCodeGenerator generator; - - /** - * Creates a new {@code LobbyService}. - * - * @param generator the lobby code generator used to create unique lobby codes - */ - public LobbyService(LobbyCodeGenerator generator) { - this.generator = generator; - } - - /** - * Creates a new lobby and adds the given user as the first player. - * - * @param username the username of the player creating the lobby - * @return the generated lobby code, or {@code null} if creation fails - */ - public String createLobby(String username) { - String lobbyCode = generateLobbyCode(); - if (lobbyCode == null || lobbyCode.isBlank()) { - return null; + private final Map lobbyList = new ConcurrentHashMap<>(); + private final LobbyCodeGenerator generator; + + /** + * Creates a new {@code LobbyService}. + * + * @param generator the lobby code generator used to create unique lobby codes + */ + public LobbyService(LobbyCodeGenerator generator) { + this.generator = generator; } - Lobby lobby = new Lobby(lobbyCode, username); - lobbyList.put(lobbyCode, lobby); - return lobbyCode; - } - - /** - * Adds a player to an existing lobby. - * - * @param username the username of the player - * @param lobbyCode the lobby code identifying the lobby - * @return {@code true} if the player successfully joined, {@code false} otherwise - */ - public boolean joinLobby(String username, String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - return lobby.addPlayer(username); + + /** + * Creates a new lobby and adds the given user as the first player. + * + * @param username the username of the player creating the lobby + * @return the generated lobby code, or {@code null} if creation fails + */ + public String createLobby(String username) { + String lobbyCode = generateLobbyCode(); + if (lobbyCode == null || lobbyCode.isBlank()) { + log.error("ERROR: there was an error when generating a lobby code"); + return null; + } + Lobby lobby = new Lobby(lobbyCode, username); + lobbyList.put(lobbyCode, lobby); + log.info("{}: a lobby has been created", lobbyCode); + return lobbyCode; + } + + /** + * Adds a player to an existing lobby. + * + * @param username the username of the player + * @param lobbyCode the lobby code identifying the lobby + * @return {@code true} if the player successfully joined, {@code false} otherwise + */ + public boolean joinLobby(String username, String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + log.info("{}: a player has joined", lobbyCode); + return lobby.addPlayer(username); + } + log.error("{}: an error occurred when joining lobby", lobbyCode); + return false; } - return false; - } - - /** - * Removes a player from a lobby. - * - * @param username the username of the player - * @param lobbyCode the lobby code identifying the lobby - * @return {@code true} if the player was removed, {@code false} if the lobby does not exist - */ - public boolean leaveLobby(String username, String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - lobby.removePlayer(username); - return true; + + /** + * Removes a player from a lobby. + * + * @param username the username of the player + * @param lobbyCode the lobby code identifying the lobby + * @return {@code true} if the player was removed, {@code false} if the lobby does not exist + */ + public boolean leaveLobby(String username, String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + lobby.removePlayer(username); + log.info("{}: a player left", lobbyCode); + return true; + } + log.error("{}: an error occurred when leaving", lobbyCode); + return false; } - return false; - } - - /** - * Assigns a team and role to a player in a lobby. - * - * @param username the username of the player - * @param lobbyCode the lobby code identifying the lobby - * @param team the selected team - * @param role the selected role - * @return {@code true} if the position was assigned, {@code false} otherwise - */ - public boolean selectPosition(String username, String lobbyCode, Team team, Role role) { - Lobby lobby = lobbyList.get(lobbyCode); - - if (lobby == null || !lobby.hasPlayer(username) || team == null || role == null) { - return false; + + /** + * Assigns a team and role to a player in a lobby. + * + * @param username the username of the player + * @param lobbyCode the lobby code identifying the lobby + * @param team the selected team + * @param role the selected role + * @return {@code true} if the position was assigned, {@code false} otherwise + */ + public boolean selectPosition(String username, String lobbyCode, Team team, Role role) { + Lobby lobby = lobbyList.get(lobbyCode); + + if (lobby == null || !lobby.hasPlayer(username) || team == null || role == null) { + log.error("{}: position selection error occurred", lobbyCode); + return false; + } + + if (role == Role.SPYMASTER && isSpymasterAlreadyAssigned(lobby, username, team)) { + log.error("{}: position selection error occurred, spymaster is already assigned.", lobbyCode); + return false; + } + + lobby.setPlayerTeam(username, team); + lobby.setPlayerRole(username, role); + log.info("{}: new role was assigned to player", lobbyCode); + return true; } - if (role == Role.SPYMASTER && isSpymasterAlreadyAssigned(lobby, username, team)) { - return false; + /** + * Retrieves all playerList in the specified lobby. + * + * @param lobbyCode the lobby code identifying the lobby + * @return a list of playerList, or an empty list if the lobby does not exist + */ + public List getPlayers(String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + return lobby != null ? lobby.getPlayerList() : List.of(); } - lobby.setPlayerTeam(username, team); - lobby.setPlayerRole(username, role); - return true; - } - - /** - * Retrieves all playerList in the specified lobby. - * - * @param lobbyCode the lobby code identifying the lobby - * @return a list of playerList, or an empty list if the lobby does not exist - */ - public List getPlayers(String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - return lobby != null ? lobby.getPlayerList() : List.of(); - } - - /** - * Retrieves all playerList in the specified lobby as PlayerDto objects. - * - * @param lobbyCode the lobby code identifying the lobby - * @return a list of PlayerDto objects, or an empty list if the lobby does not exist - */ - - public List getPlayersDto(String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - return lobby.getPlayerList().stream() - .map(player -> new PlayerDto( - player.username(), - lobby.getPlayerTeam(player.username()) != null ? lobby.getPlayerTeam(player.username()) : null, - lobby.getPlayerRole(player.username()) != null ? lobby.getPlayerRole(player.username()) : null, - player.isHost())) - .toList(); + /** + * Retrieves all playerList in the specified lobby as PlayerDto objects. + * + * @param lobbyCode the lobby code identifying the lobby + * @return a list of PlayerDto objects, or an empty list if the lobby does not exist + */ + + public List getPlayersDto(String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + return lobby.getPlayerList().stream() + .map(player -> new PlayerDto( + player.username(), + lobby.getPlayerTeam(player.username()) != null ? lobby.getPlayerTeam(player.username()) : null, + lobby.getPlayerRole(player.username()) != null ? lobby.getPlayerRole(player.username()) : null, + player.isHost())) + .toList(); + } + return List.of(); } - return List.of(); - } - - /** - * Checks whether a spymaster is already assigned for the given team in the lobby. - * - * @param lobby the lobby to inspect - * @param username the username requesting the role - * @param team the team to inspect - * @return {@code true} if a different player is already the spymaster for that team - */ - private boolean isSpymasterAlreadyAssigned(Lobby lobby, String username, Team team) { - for (Player player : lobby.getPlayerList()) { - if (!player.username().equals(username) - && lobby.getPlayerTeam(player.username()) == team - && lobby.getPlayerRole(player.username()) == Role.SPYMASTER) { - return true; - } + + /** + * Checks whether a spymaster is already assigned for the given team in the lobby. + * + * @param lobby the lobby to inspect + * @param username the username requesting the role + * @param team the team to inspect + * @return {@code true} if a different player is already the spymaster for that team + */ + private boolean isSpymasterAlreadyAssigned(Lobby lobby, String username, Team team) { + for (Player player : lobby.getPlayerList()) { + if (!player.username().equals(username) + && lobby.getPlayerTeam(player.username()) == team + && lobby.getPlayerRole(player.username()) == Role.SPYMASTER) { + return true; + } + } + return false; } - return false; - } - - /** - * Generates a unique lobby code. - * - * @return a unique lobby code, or {@code null} if no valid code could be generated - */ - private String generateLobbyCode() { - String code = generator.generateLobbyCode(); - - if (code == null || code.isBlank()) { - return null; + + /** + * Generates a unique lobby code. + * + * @return a unique lobby code, or {@code null} if no valid code could be generated + */ + private String generateLobbyCode() { + String code = generator.generateLobbyCode(); + + if (code == null || code.isBlank()) { + return null; + } + + while (lobbyList.containsKey(code)) { + code = generator.generateLobbyCode(); + if (code == null || code.isBlank()) { + return null; + } + } + return code; } - while (lobbyList.containsKey(code)) { - code = generator.generateLobbyCode(); - if (code == null || code.isBlank()) { + /** + * Retrieves the team of a player in a lobby. + * + * @param username the username of a player + * @param lobbyCode the lobby code of the lobby + * @return the team of the player, or {@code null} if the lobby or player does not exist + */ + public Team getPlayerTeam(String username, String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + return lobby.getPlayerTeam(username); + } return null; - } - } - return code; - } - - /** - * Retrieves the team of a player in a lobby. - * - * @param username the username of a player - * @param lobbyCode the lobby code of the lobby - * @return the team of the player, or {@code null} if the lobby or player does not exist - */ - public Team getPlayerTeam(String username, String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - return lobby.getPlayerTeam(username); } - return null; - } - - /** - * Retrieves the role of a player in a lobby. - * - * @param username the username of a player - * @param lobbyCode the lobby code of the lobby - * @return the role of the player, or {@code null} if the lobby or player does not exist - */ - public Role getPlayerRole(String username, String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - return lobby.getPlayerRole(username); + + /** + * Retrieves the role of a player in a lobby. + * + * @param username the username of a player + * @param lobbyCode the lobby code of the lobby + * @return the role of the player, or {@code null} if the lobby or player does not exist + */ + public Role getPlayerRole(String username, String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + return lobby.getPlayerRole(username); + } + return null; } - return null; - } } From 9403127b80d3d693d5eccf99163c7c87ffe4b62a Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 16:41:18 +0200 Subject: [PATCH 081/207] test: update tests for lobby classes --- .../lobby/controller/LobbyController.java | 2 +- .../backend/lobby/services/LobbyService.java | 4 +- .../lobby/controller/LobbyControllerTest.java | 277 +++++++------ .../lobby/services/LobbyServiceTest.java | 367 ++++++++++-------- 4 files changed, 359 insertions(+), 291 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index f557ee08..8fcd79c3 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -20,7 +20,7 @@ public class LobbyController { private final LobbyService service; - private static final String LOBBY_NOT_FOUND = "Could not find lobby"; + private static final String LOBBY_NOT_FOUND = "Could not find lobby."; /** * Creates a new {@code LobbyController}. diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 86a15359..8144223c 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -140,8 +140,8 @@ public List getPlayersDto(String lobbyCode) { return lobby.getPlayerList().stream() .map(player -> new PlayerDto( player.username(), - lobby.getPlayerTeam(player.username()) != null ? lobby.getPlayerTeam(player.username()) : null, - lobby.getPlayerRole(player.username()) != null ? lobby.getPlayerRole(player.username()) : null, + lobby.getPlayerTeam(player.username()), + lobby.getPlayerRole(player.username()), player.isHost())) .toList(); } diff --git a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java index 5c5971ce..e62144f9 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java @@ -1,10 +1,6 @@ package com.codenames.codenames.backend.lobby.controller; -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - +import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; @@ -15,118 +11,167 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import java.util.List; + +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + @WebMvcTest(LobbyController.class) class LobbyControllerTest { - @Autowired - private MockMvc mockMvc; - - @MockBean - private LobbyService service; - - @Test - void createLobbyShouldReturn200() throws Exception { - when(service.createLobby("TestUser")).thenReturn("ABCDE"); - - mockMvc.perform(post("/lobby/create") - .param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); - } - - @Test - void createLobbyBlankLobbyCode() throws Exception { - when(service.createLobby("TestUser")).thenReturn(""); - - mockMvc.perform(post("/lobby/create") - .param("username", "TestUser")) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.message").value("Error while creating lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("")); - } - - @Test - void joinLobbyShouldReturn200_whenSuccess() throws Exception { - when(service.joinLobby("TestUser", "ABCDE")).thenReturn(true); - - mockMvc.perform(post("/lobby/join") - .param("username", "TestUser") - .param("lobbyCode", "ABCDE")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); - } - - @Test - void joinLobbyShouldReturn400_whenNotFound() throws Exception { - when(service.joinLobby("TestUser", "XXXXX")).thenReturn(false); - - mockMvc.perform(post("/lobby/join") - .param("username", "TestUser") - .param("lobbyCode", "XXXXX")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not find lobby.")); - } - - @Test - void leaveLobbyShouldReturn200_whenSuccess() throws Exception { - when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(true); - - mockMvc.perform(post("/lobby/leave") - .param("username", "TestUser") - .param("lobbyCode", "ABCDE")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Left lobby successfully.")); - } - - @Test - void leaveLobbyNoSuccess() throws Exception { - when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(false); - - mockMvc.perform(post("/lobby/leave") - .param("username", "TestUser") - .param("lobbyCode", "ABCDE")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not find lobby.")); - } - - @Test - void selectPositionShouldReturn200whenSuccess() throws Exception { - when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(true); - - mockMvc.perform(post("/lobby/select-position") - .contentType(MediaType.APPLICATION_JSON) - .content( - """ - { - "username": "TestUser", - "lobbyCode": "ABCDE", - "team": "RED", - "role": "SPYMASTER" - } - """)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Position selected successfully.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); - } - - @Test - void selectPositionShouldReturn400whenAssignmentFails() throws Exception { - when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(false); - - mockMvc.perform(post("/lobby/select-position") - .contentType(MediaType.APPLICATION_JSON) - .content( - """ - { - "username": "TestUser", - "lobbyCode": "ABCDE", - "team": "RED", - "role": "SPYMASTER" - } - """)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); - } + @Autowired + private MockMvc mockMvc; + + @MockBean + private LobbyService service; + + @Test + void createLobbyShouldReturn200() throws Exception { + when(service.createLobby("TestUser")).thenReturn("ABCDE"); + + mockMvc.perform(get("/lobby/create") + .param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + } + + @Test + void createLobbyBlankLobbyCode() throws Exception { + when(service.createLobby("TestUser")).thenReturn(""); + + mockMvc.perform(get("/lobby/create") + .param("username", "TestUser")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Error while creating lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("")); + } + + @Test + void createLobbyNullLobbyCode() throws Exception { + when(service.createLobby("TestUser")).thenReturn(null); + + mockMvc.perform(get("/lobby/create") + .param("username", "TestUser")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Error while creating lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("")); + } + + @Test + void joinLobbyShouldReturn200_whenSuccess() throws Exception { + when(service.joinLobby("TestUser", "ABCDE")).thenReturn(true); + + mockMvc.perform(get("/lobby/ABCDE/join") + .param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); + } + + @Test + void joinLobbyShouldReturn400_whenNotFound() throws Exception { + when(service.joinLobby("TestUser", "XXXXX")).thenReturn(false); + + mockMvc.perform(get("/lobby/XXXXX/join") + .param("username", "TestUser")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not find lobby.")); + } + + @Test + void leaveLobbyShouldReturn200_whenSuccess() throws Exception { + when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(true); + + mockMvc.perform(get("/lobby/ABCDE/leave") + .param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Left lobby successfully.")); + } + + @Test + void leaveLobbyNoSuccess() throws Exception { + when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(false); + + mockMvc.perform(get("/lobby/ABCDE/leave") + .param("username", "TestUser") + .param("lobbyCode", "ABCDE")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not find lobby.")); + } + + @Test + void selectPositionShouldReturn200whenSuccess() throws Exception { + when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(true); + + mockMvc.perform(post("/lobby/ABCDE/select-position") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "TestUser", + "team": "RED", + "role": "SPYMASTER", + "isHost": "true" + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Position selected successfully.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + } + + @Test + void selectPositionShouldReturn400whenAssignmentFails() throws Exception { + when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(false); + + mockMvc.perform(post("/lobby/ABCDE/select-position") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "TestUser", + "team": "RED", + "role": "SPYMASTER", + "isHost": "true" + } + """)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + } + + @Test + void getLobbyInfoShouldReturn200_whenLobbyExists() throws Exception { + List players = List.of( + new PlayerDto("Alice", null, null, true), + new PlayerDto("Bob", null, null, false) + ); + + when(service.getPlayersDto("ABCDE")).thenReturn(players); + + mockMvc.perform(get("/lobby/ABCDE")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message") + .value("Lobby info retrieved successfully.")) + .andExpect(jsonPath("$.lobbyCode") + .value("ABCDE")) + .andExpect(jsonPath("$.playerList[0].username") + .value("Alice")) + .andExpect(jsonPath("$.playerList[1].username") + .value("Bob")); + } + + @Test + void getLobbyInfoShouldReturn400_whenLobbyDoesNotExist() throws Exception { + when(service.getPlayersDto("ABCDE")).thenReturn(null); + + mockMvc.perform(get("/lobby/ABCDE")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message") + .value("Could not find lobby.")) + .andExpect(jsonPath("$.lobbyCode") + .value("ABCDE")); + } } \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index ef61e579..b8bf6018 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -1,20 +1,18 @@ package com.codenames.codenames.backend.lobby.services; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - +import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + /** * Tests for {@link LobbyService}. * @@ -23,226 +21,251 @@ class LobbyServiceTest { - private LobbyService lobbyService; - private LobbyCodeGenerator generator; + private LobbyService lobbyService; + private LobbyCodeGenerator generator; + + @BeforeEach + void setup() { + generator = mock(LobbyCodeGenerator.class); + lobbyService = new LobbyService(generator); + when(generator.generateLobbyCode()).thenReturn("ABCDE"); + } + + @Test + void createLobbyReturnLobbyCode() { + lobbyService.createLobby("Host"); + boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); + + assertTrue(result); + + List players = lobbyService.getPlayers("ABCDE"); + assertTrue(players.stream().anyMatch(p -> p.username().equals("TestUser"))); + } + + @Test + void createLobbyLobbyCodeIsNull() { + when(generator.generateLobbyCode()).thenReturn(null); + String result = lobbyService.createLobby("Host"); + + assertNull(result); + } + + @Test + void createLobbyLobbyCodeIsBlank() { + when(generator.generateLobbyCode()).thenReturn(""); + String result = lobbyService.createLobby("Host"); - @BeforeEach - void setup() { - generator = mock(LobbyCodeGenerator.class); - lobbyService = new LobbyService(generator); - when(generator.generateLobbyCode()).thenReturn("ABCDE"); - } + assertNull(result); + } - @Test - void createLobbyReturnLobbyCode() { - lobbyService.createLobby("Host"); - boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); + @Test + void joinLobbyReturnFalseLobbyNotExists() { + boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); + assertFalse(result); + } - assertTrue(result); + @Test + void leaveLobbyReturnTrueLobbyExists() { + lobbyService.createLobby("Host"); - List players = lobbyService.getPlayers("ABCDE"); - assertTrue(players.stream().anyMatch(p -> p.username().equals("TestUser"))); - } + boolean result = lobbyService.leaveLobby("Host", "ABCDE"); - @Test - void createLobbyLobbyCodeIsNull() { - when(generator.generateLobbyCode()).thenReturn(null); - String result = lobbyService.createLobby("Host"); + assertTrue(result); - assertNull(result); - } + List players = lobbyService.getPlayers("ABCDE"); - @Test - void createLobbyLobbyCodeIsBlank() { - when(generator.generateLobbyCode()).thenReturn(""); - String result = lobbyService.createLobby("Host"); + assertFalse(players.stream().anyMatch(p -> p.username().equals("Host"))); + } - assertNull(result); - } + @Test + void leaveLobbyReturnFalseLobbyNotExists() { + boolean result = lobbyService.leaveLobby("Host", "ABCDE"); + assertFalse(result); + } - @Test - void joinLobbyReturnFalseLobbyNotExists() { - boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); - assertFalse(result); - } + @Test + void createLobbyShouldGenerateNewCodeIfDuplicateExists() { + when(generator.generateLobbyCode()) + .thenReturn("ABCDE") + .thenReturn("ABCDE") + .thenReturn("FGHIJ"); - @Test - void leaveLobbyReturnTrueLobbyExists() { - lobbyService.createLobby("Host"); + lobbyService.createLobby("Host1"); + String code2 = lobbyService.createLobby("Host2"); - boolean result = lobbyService.leaveLobby("Host", "ABCDE"); + assertEquals("FGHIJ", code2); + } - assertTrue(result); + @Test + void selectPositionShouldReturnTrueWhenPlayerChoosesTeamAndRole() { + lobbyService.createLobby("Host"); - List players = lobbyService.getPlayers("ABCDE"); + boolean result = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - assertFalse(players.stream().anyMatch(p -> p.username().equals("Host"))); - } + assertTrue(result); + } - @Test - void leaveLobbyReturnFalseLobbyNotExists() { - boolean result = lobbyService.leaveLobby("Host", "ABCDE"); - assertFalse(result); - } + @Test + void selectPositionShouldReturnFalseWhenLobbyDoesNotExist() { + boolean result = lobbyService.selectPosition("Host", "XXXXX", Team.RED, Role.SPYMASTER); - @Test - void createLobbyShouldGenerateNewCodeIfDuplicateExists() { - when(generator.generateLobbyCode()) - .thenReturn("ABCDE") - .thenReturn("ABCDE") - .thenReturn("FGHIJ"); + assertFalse(result); + } - lobbyService.createLobby("Host1"); - String code2 = lobbyService.createLobby("Host2"); + @Test + void selectPositionShouldReturnFalseWhenPlayerIsNotInLobby() { + lobbyService.createLobby("Host"); - assertEquals("FGHIJ", code2); - } + boolean result = lobbyService.selectPosition("Ghost", "ABCDE", Team.RED, Role.SPYMASTER); - @Test - void selectPositionShouldReturnTrueWhenPlayerChoosesTeamAndRole() { - lobbyService.createLobby("Host"); + assertFalse(result); + } - boolean result = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + @Test + void selectPositionShouldReturnFalseWhenSecondSpymasterChoosesSameTeam() { + lobbyService.createLobby("Host"); + lobbyService.joinLobby("P1", "ABCDE"); - assertTrue(result); - } + boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.SPYMASTER); - @Test - void selectPositionShouldReturnFalseWhenLobbyDoesNotExist() { - boolean result = lobbyService.selectPosition("Host", "XXXXX", Team.RED, Role.SPYMASTER); + assertTrue(firstResult); + assertFalse(secondResult); + } - assertFalse(result); - } + @Test + void selectPositionShouldReturnTrueWhenSpymastersChooseDifferentTeams() { + lobbyService.createLobby("Host"); + lobbyService.joinLobby("P1", "ABCDE"); - @Test - void selectPositionShouldReturnFalseWhenPlayerIsNotInLobby() { - lobbyService.createLobby("Host"); + boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.BLUE, Role.SPYMASTER); - boolean result = lobbyService.selectPosition("Ghost", "ABCDE", Team.RED, Role.SPYMASTER); + assertTrue(firstResult); + assertTrue(secondResult); + } - assertFalse(result); - } + @Test + void selectPositionShouldReturnTrueWhenMultipleOperativesChooseSameTeam() { + lobbyService.createLobby("Host"); + lobbyService.joinLobby("P1", "ABCDE"); - @Test - void selectPositionShouldReturnFalseWhenSecondSpymasterChoosesSameTeam() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); + boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); + boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.OPERATIVE); - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.SPYMASTER); + assertTrue(firstResult); + assertTrue(secondResult); + } - assertTrue(firstResult); - assertFalse(secondResult); - } + @Test + void getPlayersShouldReturnEmptyListWhenLobbyDoesNotExist() { + List players = lobbyService.getPlayers("UNKNOWN"); - @Test - void selectPositionShouldReturnTrueWhenSpymastersChooseDifferentTeams() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); + assertNotNull(players); + assertTrue(players.isEmpty()); + } - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.BLUE, Role.SPYMASTER); + @Test + void joinLobbyShouldReturnFalseWhenPlayerAlreadyExists() { + lobbyService.createLobby("Host"); - assertTrue(firstResult); - assertTrue(secondResult); - } + boolean first = lobbyService.joinLobby("Max", "ABCDE"); + boolean second = lobbyService.joinLobby("Max", "ABCDE"); - @Test - void selectPositionShouldReturnTrueWhenMultipleOperativesChooseSameTeam() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); + assertTrue(first); + assertFalse(second); - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.OPERATIVE); + List players = lobbyService.getPlayers("ABCDE"); - assertTrue(firstResult); - assertTrue(secondResult); - } + long count = players.stream() + .filter(p -> p.username().equals("Max")) + .count(); - @Test - void getPlayersShouldReturnEmptyListWhenLobbyDoesNotExist() { - List players = lobbyService.getPlayers("UNKNOWN"); + assertEquals(1, count); + } - assertNotNull(players); - assertTrue(players.isEmpty()); - } + @Test + void selectPositionShouldReturnFalseIfTeamIsNull() { + when(generator.generateLobbyCode()).thenReturn("ABCDE"); + String lobbyCode = lobbyService.createLobby("Host"); - @Test - void joinLobbyShouldReturnFalseWhenPlayerAlreadyExists() { - lobbyService.createLobby("Host"); + boolean result = lobbyService.selectPosition("Host", lobbyCode, null, Role.OPERATIVE); - boolean first = lobbyService.joinLobby("Max", "ABCDE"); - boolean second = lobbyService.joinLobby("Max", "ABCDE"); + assertFalse(result); + } - assertTrue(first); - assertFalse(second); + @Test + void selectPositionShouldReturnFalseIfRoleIsNull() { + when(generator.generateLobbyCode()).thenReturn("ABCDE"); + String lobbyCode = lobbyService.createLobby("Host"); - List players = lobbyService.getPlayers("ABCDE"); + boolean result = lobbyService.selectPosition("Host", lobbyCode, Team.RED, null); - long count = players.stream() - .filter(p -> p.username().equals("Max")) - .count(); + assertFalse(result); + } - assertEquals(1, count); - } + @Test + void testGetPlayerTeam() { + lobbyService.createLobby("Host"); + lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - @Test - void selectPositionShouldReturnFalseIfTeamIsNull() { - when(generator.generateLobbyCode()).thenReturn("ABCDE"); - String lobbyCode = lobbyService.createLobby("Host"); + assertEquals(Team.RED, lobbyService.getPlayerTeam("Host", "ABCDE")); + } - boolean result = lobbyService.selectPosition("Host", lobbyCode, null, Role.OPERATIVE); + @Test + void getPlayerTeam_wrongCode() { + assertNull(lobbyService.getPlayerTeam("Host", "invalidCode")); + } - assertFalse(result); - } + @Test + void getPlayerTeam_nonExistentPlayer() { + String lobbyCode = lobbyService.createLobby("Host"); - @Test - void selectPositionShouldReturnFalseIfRoleIsNull() { - when(generator.generateLobbyCode()).thenReturn("ABCDE"); - String lobbyCode = lobbyService.createLobby("Host"); + assertNull(lobbyService.getPlayerTeam("nonExistentPlayer", lobbyCode)); + } - boolean result = lobbyService.selectPosition("Host", lobbyCode, Team.RED, null); + @Test + void testGetPlayerRole() { + lobbyService.createLobby("Host"); + lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); - assertFalse(result); - } + assertEquals(Role.OPERATIVE, lobbyService.getPlayerRole("Host", "ABCDE")); + } - @Test - void testGetPlayerTeam() { - lobbyService.createLobby("Host"); - lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + @Test + void getPlayerRole_wrongCode() { + assertNull(lobbyService.getPlayerRole("Host", "test")); + } - assertEquals(Team.RED, lobbyService.getPlayerTeam("Host", "ABCDE")); - } + @Test + void getPlayerRole_nonExistentPlayer() { + String lobbyCode = lobbyService.createLobby("Host"); - @Test - void getPlayerTeam_wrongCode() { - assertNull(lobbyService.getPlayerTeam("Host", "invalidCode")); - } + assertNull(lobbyService.getPlayerRole("nonExistentPlayer", lobbyCode)); + } - @Test - void getPlayerTeam_nonExistentPlayer() { - String lobbyCode = lobbyService.createLobby("Host"); + @Test + void getPlayersDtoShouldReturnPlayerDtos_whenLobbyExists() { + lobbyService.createLobby("Host"); - assertNull(lobbyService.getPlayerTeam("nonExistentPlayer", lobbyCode)); - } + List result = lobbyService.getPlayersDto("ABCDE"); - @Test - void testGetPlayerRole() { - lobbyService.createLobby("Host"); - lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); + assertNotNull(result); + assertEquals(1, result.size()); - assertEquals(Role.OPERATIVE, lobbyService.getPlayerRole("Host", "ABCDE")); - } + PlayerDto player = result.get(0); - @Test - void getPlayerRole_wrongCode() { - assertNull(lobbyService.getPlayerRole("Host", "test")); - } + assertEquals("Host", player.username()); + assertNull(player.team()); + assertNull(player.role()); + assertTrue(player.isHost()); + } - @Test - void getPlayerRole_nonExistentPlayer() { - String lobbyCode = lobbyService.createLobby("Host"); + @Test + void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { + List result = lobbyService.getPlayersDto("ABCDE"); - assertNull(lobbyService.getPlayerRole("nonExistentPlayer", lobbyCode)); - } + assertNotNull(result); + assertTrue(result.isEmpty()); + } } \ No newline at end of file From b37010b683ad5fd93e9b1c48c03db643219c7d3d Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 16:58:26 +0200 Subject: [PATCH 082/207] style: added some java docs, checkstyle check ignored 1 wildcard import (is automatically turned into wildcard when importing all methods) ignored lexical order, intelliJ orders imports automatically like this --- .../codenames/backend/lobby/Lobby.java | 12 +- .../lobby/controller/LobbyController.java | 225 ++++++----- .../backend/lobby/dto/LobbyResponse.java | 9 - .../backend/lobby/dto/PlayerDto.java | 8 + .../lobby/services/LobbyCodeGenerator.java | 4 +- .../backend/lobby/services/LobbyService.java | 360 +++++++++--------- 6 files changed, 325 insertions(+), 293 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java index 96b64bc7..7263bfb3 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java @@ -3,17 +3,19 @@ import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; +import lombok.Getter; + import java.security.SecureRandom; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; -import lombok.Getter; /** * Represents a game lobby containing a limited number of playerList. * - *

Supports adding and removing playerList while enforcing constraints such as maximum player count + *

Supports adding and removing playerList + * while enforcing constraints such as maximum player count * and unique usernames. */ @Getter @@ -67,9 +69,9 @@ public boolean addPlayer(String username, boolean isHost) { /** * Adds a player to the lobby if capacity allows and the username is unique. + * Calls {@link #addPlayer(String, boolean)} with {@code false} as the second argument * * @param username the username of the player - * @calls {@link #addPlayer(String, boolean)} with {@code false} as the second argument * @return {@code true} if the player was added, {@code false} otherwise */ public boolean addPlayer(String username) { @@ -101,7 +103,7 @@ public boolean hasPlayer(String username) { * Sets the team for a player. * * @param username the username of the player - * @param team the team to assign + * @param team the team to assign */ public void setPlayerTeam(String username, Team team) { playerTeams.put(username, team); @@ -111,7 +113,7 @@ public void setPlayerTeam(String username, Team team) { * Sets the role for a player. * * @param username the username of the player - * @param role the role to assign + * @param role the role to assign */ public void setPlayerRole(String username, Role role) { playerRoles.put(username, role); diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 8fcd79c3..0c14a448 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -19,114 +19,145 @@ @RequestMapping("/lobby") public class LobbyController { - private final LobbyService service; - private static final String LOBBY_NOT_FOUND = "Could not find lobby."; + private final LobbyService service; + private static final String LOBBY_NOT_FOUND = "Could not find lobby."; - /** - * Creates a new {@code LobbyController}. - * - * @param service the lobby service used to handle business logic - */ - public LobbyController(LobbyService service) { - this.service = service; - } + /** + * Creates a new {@code LobbyController}. + * + * @param service the lobby service used to handle business logic + */ + public LobbyController(LobbyService service) { + this.service = service; + } - /** - * Handles a request to create a new lobby. - * - * @param username the username of the requesting user - * @return a response containing the result and the generated lobby code - */ + /** + * Handles a request to create a new lobby. + * + * @param username the username of the requesting user + * @return a response containing the result and the generated lobby code + */ - @GetMapping("/create") - public ResponseEntity createLobby(@RequestParam String username) { - String lobbyCode = service.createLobby(username); - if (lobbyCode == null || lobbyCode.isBlank()) { - return ResponseEntity.internalServerError() - .body(new LobbyResponse("Error while creating lobby.", "", null)); - } else { - List players = service.getPlayersDto(lobbyCode); - return ResponseEntity.ok(new LobbyResponse("Successfully created Lobby.", lobbyCode, players)); - } + @GetMapping("/create") + public ResponseEntity createLobby(@RequestParam String username) { + String lobbyCode = service.createLobby(username); + if (lobbyCode == null || lobbyCode.isBlank()) { + return ResponseEntity.internalServerError() + .body(new LobbyResponse("Error while creating lobby.", "", null)); + } else { + List players = service.getPlayersDto(lobbyCode); + return ResponseEntity.ok( + new LobbyResponse("Successfully created Lobby.", lobbyCode, players) + ); } + } - /** - * Handles a request to join an existing lobby. - * - * @param username the username of the player - * @param lobbyCode the lobby code identifying the lobby - * @return a response indicating whether the join was successful - */ - @GetMapping("/{lobbyCode}/join") - public ResponseEntity joinLobby( - @RequestParam String username, @PathVariable String lobbyCode) { - boolean joined = service.joinLobby(username, lobbyCode); - if (joined) { - return ResponseEntity.ok(new LobbyResponse("Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); - } else { - return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); - } + /** + * Handles a request to join an existing lobby. + * + * @param username the username of the player + * @param lobbyCode the lobby code identifying the lobby + * @return a response indicating whether the join was successful + */ + @GetMapping("/{lobbyCode}/join") + public ResponseEntity joinLobby( + @RequestParam String username, @PathVariable String lobbyCode) { + boolean joined = service.joinLobby(username, lobbyCode); + if (joined) { + return ResponseEntity.ok( + new LobbyResponse( + "Joined Lobby successfully.", + lobbyCode, + service.getPlayersDto(lobbyCode) + ) + ); + } else { + return ResponseEntity.badRequest() + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); } + } - /** - * Handles a request to leave a lobby. - * - * @param username the username of the player - * @param lobbyCode the lobby code identifying the lobby - * @return a response indicating whether the operation was successful - */ - @GetMapping("/{lobbyCode}/leave") - public ResponseEntity leaveLobby( - @PathVariable String lobbyCode, - @RequestParam String username) { - boolean left = service.leaveLobby(username, lobbyCode); - if (left) { - return ResponseEntity.ok(new LobbyResponse("Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode))); - } else { - return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); - } + /** + * Handles a request to leave a lobby. + * + * @param username the username of the player + * @param lobbyCode the lobby code identifying the lobby + * @return a response indicating whether the operation was successful + */ + @GetMapping("/{lobbyCode}/leave") + public ResponseEntity leaveLobby( + @PathVariable String lobbyCode, + @RequestParam String username) { + boolean left = service.leaveLobby(username, lobbyCode); + if (left) { + return ResponseEntity.ok( + new LobbyResponse( + "Left lobby successfully.", + lobbyCode, + service.getPlayersDto(lobbyCode) + ) + ); + } else { + return ResponseEntity.badRequest() + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); } + } + + /** + * An endpoint for retrieving all lobby-specific info, used during polling in lobby-state. + * + * @param lobbyCode unique lobby code + * @return a response entity with the http code 200 for ok and 400 for bad request, if an error occurred + */ - @GetMapping("/{lobbyCode}") - public ResponseEntity getLobbyInfo( - @PathVariable String lobbyCode - ) { - List players = service.getPlayersDto(lobbyCode); - if (players != null) { - return ResponseEntity.ok(new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players)); - } else { - return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); - } + @GetMapping("/{lobbyCode}") + public ResponseEntity getLobbyInfo( + @PathVariable String lobbyCode + ) { + List players = service.getPlayersDto(lobbyCode); + if (players != null) { + return ResponseEntity.ok( + new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players) + ); + } else { + return ResponseEntity.badRequest() + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); } + } - /** - * Handles a request to select a team and role for a player. - * - * @param request the position selection request containing username, lobby code, team, and role - * @return a response indicating whether the selection was successful - */ - @PostMapping("/{lobbyCode}/select-position") - public ResponseEntity selectPosition( - @PathVariable String lobbyCode, @RequestBody PlayerDto request - ) { - boolean updated = service.selectPosition( - request.username(), - lobbyCode, - request.team(), - request.role() - ); + /** + * Handles a request to select a team and role for a player. + * + * @param request the position selection request containing username, lobby code, team, and role + * @return a response indicating whether the selection was successful + */ + @PostMapping("/{lobbyCode}/select-position") + public ResponseEntity selectPosition( + @PathVariable String lobbyCode, @RequestBody PlayerDto request + ) { + boolean updated = service.selectPosition( + request.username(), + lobbyCode, + request.team(), + request.role() + ); - if (updated) { - return ResponseEntity.ok( - new LobbyResponse("Position selected successfully.", lobbyCode, service.getPlayersDto(lobbyCode)) - ); - } else { - return ResponseEntity.badRequest().body( - new LobbyResponse("Could not assign selected team/role.", lobbyCode, service.getPlayersDto(lobbyCode)) - ); - } + if (updated) { + return ResponseEntity.ok( + new LobbyResponse( + "Position selected successfully.", + lobbyCode, + service.getPlayersDto(lobbyCode) + ) + ); + } else { + return ResponseEntity.badRequest().body( + new LobbyResponse( + "Could not assign selected team/role.", + lobbyCode, + service.getPlayersDto(lobbyCode) + ) + ); } + } } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java b/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java index 009b2890..02269fe5 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java @@ -8,13 +8,4 @@ *

Contains a message describing the outcome and the associated lobby code. */ public record LobbyResponse(String message, String lobbyCode, List playerList) { - /** - * Creates a new lobby response. - * - * @param message the message describing the result of the operation - * @param lobbyCode the associated lobby code - * @param playerList the list of playerList currently in the lobby - */ - public LobbyResponse { - } } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/dto/PlayerDto.java b/src/main/java/com/codenames/codenames/backend/lobby/dto/PlayerDto.java index 55b25c19..8132847c 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/dto/PlayerDto.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/dto/PlayerDto.java @@ -3,5 +3,13 @@ import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; +/** + * A data transfer object for communicating with the frontend, holds user-specific information. + * + * @param username the name of the user + * @param team the user's current team, can be null + * @param role the user's current role, can be null + * @param isHost whether the user is host + */ public record PlayerDto(String username, Team team, Role role, boolean isHost) { } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java index 8953e58e..682e4393 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java @@ -1,8 +1,9 @@ package com.codenames.codenames.backend.lobby.services; -import java.security.SecureRandom; import org.springframework.stereotype.Service; +import java.security.SecureRandom; + /** * Utility service for generating unique lobby codes. * @@ -12,7 +13,6 @@ @Service public class LobbyCodeGenerator { - @SuppressWarnings("SpellCheckingInspection") private static final String CHARACTERS = "ABCDEFGHJKLMNPQRSTUVWXYZ23456789"; private static final int CODE_LENGTH = 5; diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 8144223c..cab7e13d 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -23,198 +23,198 @@ @Service public class LobbyService { - private final Map lobbyList = new ConcurrentHashMap<>(); - private final LobbyCodeGenerator generator; - - /** - * Creates a new {@code LobbyService}. - * - * @param generator the lobby code generator used to create unique lobby codes - */ - public LobbyService(LobbyCodeGenerator generator) { - this.generator = generator; + private final Map lobbyList = new ConcurrentHashMap<>(); + private final LobbyCodeGenerator generator; + + /** + * Creates a new {@code LobbyService}. + * + * @param generator the lobby code generator used to create unique lobby codes + */ + public LobbyService(LobbyCodeGenerator generator) { + this.generator = generator; + } + + /** + * Creates a new lobby and adds the given user as the first player. + * + * @param username the username of the player creating the lobby + * @return the generated lobby code, or {@code null} if creation fails + */ + public String createLobby(String username) { + String lobbyCode = generateLobbyCode(); + if (lobbyCode == null || lobbyCode.isBlank()) { + log.error("ERROR: there was an error when generating a lobby code"); + return null; } - - /** - * Creates a new lobby and adds the given user as the first player. - * - * @param username the username of the player creating the lobby - * @return the generated lobby code, or {@code null} if creation fails - */ - public String createLobby(String username) { - String lobbyCode = generateLobbyCode(); - if (lobbyCode == null || lobbyCode.isBlank()) { - log.error("ERROR: there was an error when generating a lobby code"); - return null; - } - Lobby lobby = new Lobby(lobbyCode, username); - lobbyList.put(lobbyCode, lobby); - log.info("{}: a lobby has been created", lobbyCode); - return lobbyCode; - } - - /** - * Adds a player to an existing lobby. - * - * @param username the username of the player - * @param lobbyCode the lobby code identifying the lobby - * @return {@code true} if the player successfully joined, {@code false} otherwise - */ - public boolean joinLobby(String username, String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - log.info("{}: a player has joined", lobbyCode); - return lobby.addPlayer(username); - } - log.error("{}: an error occurred when joining lobby", lobbyCode); - return false; + Lobby lobby = new Lobby(lobbyCode, username); + lobbyList.put(lobbyCode, lobby); + log.info("{}: a lobby has been created", lobbyCode); + return lobbyCode; + } + + /** + * Adds a player to an existing lobby. + * + * @param username the username of the player + * @param lobbyCode the lobby code identifying the lobby + * @return {@code true} if the player successfully joined, {@code false} otherwise + */ + public boolean joinLobby(String username, String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + log.info("{}: a player has joined", lobbyCode); + return lobby.addPlayer(username); } - - /** - * Removes a player from a lobby. - * - * @param username the username of the player - * @param lobbyCode the lobby code identifying the lobby - * @return {@code true} if the player was removed, {@code false} if the lobby does not exist - */ - public boolean leaveLobby(String username, String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - lobby.removePlayer(username); - log.info("{}: a player left", lobbyCode); - return true; - } - log.error("{}: an error occurred when leaving", lobbyCode); - return false; + log.error("{}: an error occurred when joining lobby", lobbyCode); + return false; + } + + /** + * Removes a player from a lobby. + * + * @param username the username of the player + * @param lobbyCode the lobby code identifying the lobby + * @return {@code true} if the player was removed, {@code false} if the lobby does not exist + */ + public boolean leaveLobby(String username, String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + lobby.removePlayer(username); + log.info("{}: a player left", lobbyCode); + return true; } - - /** - * Assigns a team and role to a player in a lobby. - * - * @param username the username of the player - * @param lobbyCode the lobby code identifying the lobby - * @param team the selected team - * @param role the selected role - * @return {@code true} if the position was assigned, {@code false} otherwise - */ - public boolean selectPosition(String username, String lobbyCode, Team team, Role role) { - Lobby lobby = lobbyList.get(lobbyCode); - - if (lobby == null || !lobby.hasPlayer(username) || team == null || role == null) { - log.error("{}: position selection error occurred", lobbyCode); - return false; - } - - if (role == Role.SPYMASTER && isSpymasterAlreadyAssigned(lobby, username, team)) { - log.error("{}: position selection error occurred, spymaster is already assigned.", lobbyCode); - return false; - } - - lobby.setPlayerTeam(username, team); - lobby.setPlayerRole(username, role); - log.info("{}: new role was assigned to player", lobbyCode); - return true; + log.error("{}: an error occurred when leaving", lobbyCode); + return false; + } + + /** + * Assigns a team and role to a player in a lobby. + * + * @param username the username of the player + * @param lobbyCode the lobby code identifying the lobby + * @param team the selected team + * @param role the selected role + * @return {@code true} if the position was assigned, {@code false} otherwise + */ + public boolean selectPosition(String username, String lobbyCode, Team team, Role role) { + Lobby lobby = lobbyList.get(lobbyCode); + + if (lobby == null || !lobby.hasPlayer(username) || team == null || role == null) { + log.error("{}: position selection error occurred", lobbyCode); + return false; } - /** - * Retrieves all playerList in the specified lobby. - * - * @param lobbyCode the lobby code identifying the lobby - * @return a list of playerList, or an empty list if the lobby does not exist - */ - public List getPlayers(String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - return lobby != null ? lobby.getPlayerList() : List.of(); + if (role == Role.SPYMASTER && isSpymasterAlreadyAssigned(lobby, username, team)) { + log.error("{}: position selection error occurred, spymaster is already assigned.", lobbyCode); + return false; } - /** - * Retrieves all playerList in the specified lobby as PlayerDto objects. - * - * @param lobbyCode the lobby code identifying the lobby - * @return a list of PlayerDto objects, or an empty list if the lobby does not exist - */ - - public List getPlayersDto(String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - return lobby.getPlayerList().stream() - .map(player -> new PlayerDto( - player.username(), - lobby.getPlayerTeam(player.username()), - lobby.getPlayerRole(player.username()), - player.isHost())) - .toList(); - } - return List.of(); + lobby.setPlayerTeam(username, team); + lobby.setPlayerRole(username, role); + log.info("{}: new role was assigned to player", lobbyCode); + return true; + } + + /** + * Retrieves all playerList in the specified lobby. + * + * @param lobbyCode the lobby code identifying the lobby + * @return a list of playerList, or an empty list if the lobby does not exist + */ + public List getPlayers(String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + return lobby != null ? lobby.getPlayerList() : List.of(); + } + + /** + * Retrieves all playerList in the specified lobby as PlayerDto objects. + * + * @param lobbyCode the lobby code identifying the lobby + * @return a list of PlayerDto objects, or an empty list if the lobby does not exist + */ + + public List getPlayersDto(String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + return lobby.getPlayerList().stream() + .map(player -> new PlayerDto( + player.username(), + lobby.getPlayerTeam(player.username()), + lobby.getPlayerRole(player.username()), + player.isHost())) + .toList(); } - - /** - * Checks whether a spymaster is already assigned for the given team in the lobby. - * - * @param lobby the lobby to inspect - * @param username the username requesting the role - * @param team the team to inspect - * @return {@code true} if a different player is already the spymaster for that team - */ - private boolean isSpymasterAlreadyAssigned(Lobby lobby, String username, Team team) { - for (Player player : lobby.getPlayerList()) { - if (!player.username().equals(username) - && lobby.getPlayerTeam(player.username()) == team - && lobby.getPlayerRole(player.username()) == Role.SPYMASTER) { - return true; - } - } - return false; + return List.of(); + } + + /** + * Checks whether a spymaster is already assigned for the given team in the lobby. + * + * @param lobby the lobby to inspect + * @param username the username requesting the role + * @param team the team to inspect + * @return {@code true} if a different player is already the spymaster for that team + */ + private boolean isSpymasterAlreadyAssigned(Lobby lobby, String username, Team team) { + for (Player player : lobby.getPlayerList()) { + if (!player.username().equals(username) + && lobby.getPlayerTeam(player.username()) == team + && lobby.getPlayerRole(player.username()) == Role.SPYMASTER) { + return true; + } } - - /** - * Generates a unique lobby code. - * - * @return a unique lobby code, or {@code null} if no valid code could be generated - */ - private String generateLobbyCode() { - String code = generator.generateLobbyCode(); - - if (code == null || code.isBlank()) { - return null; - } - - while (lobbyList.containsKey(code)) { - code = generator.generateLobbyCode(); - if (code == null || code.isBlank()) { - return null; - } - } - return code; + return false; + } + + /** + * Generates a unique lobby code. + * + * @return a unique lobby code, or {@code null} if no valid code could be generated + */ + private String generateLobbyCode() { + String code = generator.generateLobbyCode(); + + if (code == null || code.isBlank()) { + return null; } - /** - * Retrieves the team of a player in a lobby. - * - * @param username the username of a player - * @param lobbyCode the lobby code of the lobby - * @return the team of the player, or {@code null} if the lobby or player does not exist - */ - public Team getPlayerTeam(String username, String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - return lobby.getPlayerTeam(username); - } + while (lobbyList.containsKey(code)) { + code = generator.generateLobbyCode(); + if (code == null || code.isBlank()) { return null; + } } - - /** - * Retrieves the role of a player in a lobby. - * - * @param username the username of a player - * @param lobbyCode the lobby code of the lobby - * @return the role of the player, or {@code null} if the lobby or player does not exist - */ - public Role getPlayerRole(String username, String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - if (lobby != null) { - return lobby.getPlayerRole(username); - } - return null; + return code; + } + + /** + * Retrieves the team of a player in a lobby. + * + * @param username the username of a player + * @param lobbyCode the lobby code of the lobby + * @return the team of the player, or {@code null} if the lobby or player does not exist + */ + public Team getPlayerTeam(String username, String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + return lobby.getPlayerTeam(username); + } + return null; + } + + /** + * Retrieves the role of a player in a lobby. + * + * @param username the username of a player + * @param lobbyCode the lobby code of the lobby + * @return the role of the player, or {@code null} if the lobby or player does not exist + */ + public Role getPlayerRole(String username, String lobbyCode) { + Lobby lobby = lobbyList.get(lobbyCode); + if (lobby != null) { + return lobby.getPlayerRole(username); } + return null; + } } From 5a0f6bf78b231cbde472baccab765aed2715d4cf Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 17:15:35 +0200 Subject: [PATCH 083/207] style: further refactoring plus merge with other device, forgotten commits --- .../lobby/controller/LobbyController.java | 3 +- .../backend/lobby/services/LobbyService.java | 12 +- .../lobby/controller/LobbyControllerTest.java | 332 ++++++--------- .../lobby/services/LobbyServiceTest.java | 378 +++++++++--------- 4 files changed, 319 insertions(+), 406 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index bdac6edb..2086b975 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -109,7 +109,8 @@ public ResponseEntity leaveLobby( * An endpoint for retrieving all lobby-specific info used during polling in lobby-state. * * @param lobbyCode unique lobby code - * @return a response entity with the http code 200 for ok and 400 for bad request, if an error occurred + * @return a response entity with the http code 200 for ok and + * 400 for bad request, if an error occurred */ @GetMapping("/{lobbyCode}") diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 6353729a..4c712a6a 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -117,14 +117,16 @@ public boolean selectPosition(String username, String lobbyCode, Team team, Role log.info("{}: new role was assigned to player", lobbyCode); return true; } + /** - * Checks if the lobby still has players after a player leaves and removes the lobby if it is empty. + * Checks if the lobby still has players + * after a player leaves and removes the lobby if it is empty. * * @param lobbyCode the lobby code identifying the lobby */ - public void checkLobbyStillHasPlayers(String lobbyCode){ + public void checkLobbyStillHasPlayers(String lobbyCode) { Lobby lobby = lobbyList.get(lobbyCode); - if(lobby.getPlayerList().isEmpty()){ + if (lobby.getPlayerList().isEmpty()) { lobbyList.remove(lobbyCode); } } @@ -204,7 +206,7 @@ private String generateLobbyCode() { /** * Retrieves the team of a player in a lobby. * - * @param username the username of a player + * @param username the username of a player * @param lobbyCode the lobby code of the lobby * @return the team of the player, or {@code null} if the lobby or player does not exist */ @@ -219,7 +221,7 @@ public Team getPlayerTeam(String username, String lobbyCode) { /** * Retrieves the role of a player in a lobby. * - * @param username the username of a player + * @param username the username of a player * @param lobbyCode the lobby code of the lobby * @return the role of the player, or {@code null} if the lobby or player does not exist */ diff --git a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java index 2e64de50..57c1e736 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java @@ -1,14 +1,5 @@ package com.codenames.codenames.backend.lobby.controller; -<<<<<<< HEAD -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - -======= ->>>>>>> b37010b683ad5fd93e9b1c48c03db643219c7d3d import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.utility.Role; @@ -22,126 +13,62 @@ import java.util.List; -<<<<<<< HEAD -======= import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; ->>>>>>> b37010b683ad5fd93e9b1c48c03db643219c7d3d @WebMvcTest(LobbyController.class) class LobbyControllerTest { - @Autowired - private MockMvc mockMvc; - - @MockBean - private LobbyService service; - - @Test - void createLobbyShouldReturn200() throws Exception { - when(service.createLobby("TestUser")).thenReturn("ABCDE"); - - mockMvc.perform(get("/lobby/create") - .param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); - } - - @Test - void createLobbyBlankLobbyCode() throws Exception { - when(service.createLobby("TestUser")).thenReturn(""); - - mockMvc.perform(get("/lobby/create") - .param("username", "TestUser")) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.message").value("Error while creating lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("")); - } - - @Test - void createLobbyNullLobbyCode() throws Exception { - when(service.createLobby("TestUser")).thenReturn(null); + @Autowired + private MockMvc mockMvc; - mockMvc.perform(get("/lobby/create") - .param("username", "TestUser")) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.message").value("Error while creating lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("")); - } + @MockBean + private LobbyService service; - @Test - void joinLobbyShouldReturn200_whenSuccess() throws Exception { - when(service.joinLobby("TestUser", "ABCDE")).thenReturn(true); - - mockMvc.perform(get("/lobby/ABCDE/join") - .param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); - } - -<<<<<<< HEAD @Test - void leaveLobbyShouldReturn200_whenSuccess() throws Exception { - when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(true); - String url = "/lobby/ABCDE/leave"; - mockMvc.perform(post(url) - .param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Left lobby successfully.")); + void createLobbyShouldReturn200() throws Exception { + when(service.createLobby("TestUser")).thenReturn("ABCDE"); + + mockMvc.perform(get("/lobby/create") + .param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); } @Test - void leaveLobbyNoSuccess() throws Exception { - when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(false); - String url = "/lobby/ABCDE/leave"; - mockMvc.perform(post(url) - .param("username", "TestUser")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not find lobby.")); + void createLobbyBlankLobbyCode() throws Exception { + when(service.createLobby("TestUser")).thenReturn(""); + + mockMvc.perform(get("/lobby/create") + .param("username", "TestUser")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Error while creating lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("")); } @Test - void selectPositionShouldReturn200whenSuccess() throws Exception { - when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(true); - String url = "/lobby/ABCDE/select-position"; - mockMvc.perform(post(url) - .contentType(MediaType.APPLICATION_JSON) - .content( - """ - { - "username": "TestUser", - "lobbyCode": "ABCDE", - "team": "RED", - "role": "SPYMASTER" - } - """)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Position selected successfully.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + void createLobbyNullLobbyCode() throws Exception { + when(service.createLobby("TestUser")).thenReturn(null); + + mockMvc.perform(get("/lobby/create") + .param("username", "TestUser")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Error while creating lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("")); } @Test - void selectPositionShouldReturn400whenAssignmentFails() throws Exception { - when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(false); - String url = "/lobby/ABCDE/select-position"; - mockMvc.perform(post(url) - .contentType(MediaType.APPLICATION_JSON) - .content( - """ - { - "username": "TestUser", - "lobbyCode": "ABCDE", - "team": "RED", - "role": "SPYMASTER" - } - """)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + void joinLobbyShouldReturn200_whenSuccess() throws Exception { + when(service.joinLobby("TestUser", "ABCDE")).thenReturn(true); + + mockMvc.perform(get("/lobby/ABCDE/join") + .param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); } @Test @@ -149,7 +76,7 @@ void getLobbyInfoShouldReturn200() throws Exception { when(service.getPlayersDto("ABCDE")).thenReturn(List.of(new PlayerDto("test", null, null, true))); String url = "/lobby/ABCDE"; mockMvc.perform(get(url)) - .andExpect(status().isOk()); + .andExpect(status().isOk()); } @Test @@ -157,111 +84,110 @@ void getLobbyInfoShouldReturn404() throws Exception { when(service.getPlayersDto("XXXXX")).thenReturn(null); String url = "/lobby/XXXXX"; mockMvc.perform(get(url)) - .andExpect(status().isBadRequest()); + .andExpect(status().isBadRequest()); } -======= - @Test - void joinLobbyShouldReturn400_whenNotFound() throws Exception { - when(service.joinLobby("TestUser", "XXXXX")).thenReturn(false); - - mockMvc.perform(get("/lobby/XXXXX/join") - .param("username", "TestUser")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not find lobby.")); - } - @Test - void leaveLobbyShouldReturn200_whenSuccess() throws Exception { - when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(true); - - mockMvc.perform(get("/lobby/ABCDE/leave") - .param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Left lobby successfully.")); - } + @Test + void joinLobbyShouldReturn400_whenNotFound() throws Exception { + when(service.joinLobby("TestUser", "XXXXX")).thenReturn(false); - @Test - void leaveLobbyNoSuccess() throws Exception { - when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(false); + mockMvc.perform(get("/lobby/XXXXX/join") + .param("username", "TestUser")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not find lobby.")); + } - mockMvc.perform(get("/lobby/ABCDE/leave") - .param("username", "TestUser") - .param("lobbyCode", "ABCDE")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not find lobby.")); - } + @Test + void leaveLobbyShouldReturn200_whenSuccess() throws Exception { + when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(true); - @Test - void selectPositionShouldReturn200whenSuccess() throws Exception { - when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(true); + mockMvc.perform(get("/lobby/ABCDE/leave") + .param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Left lobby successfully.")); + } - mockMvc.perform(post("/lobby/ABCDE/select-position") - .contentType(MediaType.APPLICATION_JSON) - .content( - """ - { - "username": "TestUser", - "team": "RED", - "role": "SPYMASTER", - "isHost": "true" - } - """)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Position selected successfully.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); - } + @Test + void leaveLobbyNoSuccess() throws Exception { + when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(false); - @Test - void selectPositionShouldReturn400whenAssignmentFails() throws Exception { - when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(false); + mockMvc.perform(get("/lobby/ABCDE/leave") + .param("username", "TestUser") + .param("lobbyCode", "ABCDE")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not find lobby.")); + } - mockMvc.perform(post("/lobby/ABCDE/select-position") - .contentType(MediaType.APPLICATION_JSON) - .content( - """ - { - "username": "TestUser", - "team": "RED", - "role": "SPYMASTER", - "isHost": "true" - } - """)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); - } + @Test + void selectPositionShouldReturn200whenSuccess() throws Exception { + when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(true); - @Test - void getLobbyInfoShouldReturn200_whenLobbyExists() throws Exception { - List players = List.of( - new PlayerDto("Alice", null, null, true), - new PlayerDto("Bob", null, null, false) - ); + mockMvc.perform(post("/lobby/ABCDE/select-position") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "TestUser", + "team": "RED", + "role": "SPYMASTER", + "isHost": "true" + } + """)) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Position selected successfully.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + } - when(service.getPlayersDto("ABCDE")).thenReturn(players); + @Test + void selectPositionShouldReturn400whenAssignmentFails() throws Exception { + when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(false); - mockMvc.perform(get("/lobby/ABCDE")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message") - .value("Lobby info retrieved successfully.")) - .andExpect(jsonPath("$.lobbyCode") - .value("ABCDE")) - .andExpect(jsonPath("$.playerList[0].username") - .value("Alice")) - .andExpect(jsonPath("$.playerList[1].username") - .value("Bob")); - } + mockMvc.perform(post("/lobby/ABCDE/select-position") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ + { + "username": "TestUser", + "team": "RED", + "role": "SPYMASTER", + "isHost": "true" + } + """)) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + } - @Test - void getLobbyInfoShouldReturn400_whenLobbyDoesNotExist() throws Exception { - when(service.getPlayersDto("ABCDE")).thenReturn(null); + @Test + void getLobbyInfoShouldReturn200_whenLobbyExists() throws Exception { + List players = List.of( + new PlayerDto("Alice", null, null, true), + new PlayerDto("Bob", null, null, false) + ); + + when(service.getPlayersDto("ABCDE")).thenReturn(players); + + mockMvc.perform(get("/lobby/ABCDE")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message") + .value("Lobby info retrieved successfully.")) + .andExpect(jsonPath("$.lobbyCode") + .value("ABCDE")) + .andExpect(jsonPath("$.playerList[0].username") + .value("Alice")) + .andExpect(jsonPath("$.playerList[1].username") + .value("Bob")); + } - mockMvc.perform(get("/lobby/ABCDE")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message") - .value("Could not find lobby.")) - .andExpect(jsonPath("$.lobbyCode") - .value("ABCDE")); - } ->>>>>>> b37010b683ad5fd93e9b1c48c03db643219c7d3d + @Test + void getLobbyInfoShouldReturn400_whenLobbyDoesNotExist() throws Exception { + when(service.getPlayersDto("ABCDE")).thenReturn(null); + + mockMvc.perform(get("/lobby/ABCDE")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message") + .value("Could not find lobby.")) + .andExpect(jsonPath("$.lobbyCode") + .value("ABCDE")); + } } \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index f09d3859..b486170a 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -1,16 +1,5 @@ package com.codenames.codenames.backend.lobby.services; -<<<<<<< HEAD -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -======= ->>>>>>> b37010b683ad5fd93e9b1c48c03db643219c7d3d import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; @@ -32,227 +21,226 @@ class LobbyServiceTest { - private LobbyService lobbyService; - private LobbyCodeGenerator generator; + private LobbyService lobbyService; + private LobbyCodeGenerator generator; - @BeforeEach - void setup() { - generator = mock(LobbyCodeGenerator.class); - lobbyService = new LobbyService(generator); - when(generator.generateLobbyCode()).thenReturn("ABCDE"); - } + @BeforeEach + void setup() { + generator = mock(LobbyCodeGenerator.class); + lobbyService = new LobbyService(generator); + when(generator.generateLobbyCode()).thenReturn("ABCDE"); + } - @Test - void createLobbyReturnLobbyCode() { - lobbyService.createLobby("Host"); - boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); + @Test + void createLobbyReturnLobbyCode() { + lobbyService.createLobby("Host"); + boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); - assertTrue(result); + assertTrue(result); - List players = lobbyService.getPlayers("ABCDE"); - assertTrue(players.stream().anyMatch(p -> p.username().equals("TestUser"))); - } + List players = lobbyService.getPlayers("ABCDE"); + assertTrue(players.stream().anyMatch(p -> p.username().equals("TestUser"))); + } - @Test - void createLobbyLobbyCodeIsNull() { - when(generator.generateLobbyCode()).thenReturn(null); - String result = lobbyService.createLobby("Host"); + @Test + void createLobbyLobbyCodeIsNull() { + when(generator.generateLobbyCode()).thenReturn(null); + String result = lobbyService.createLobby("Host"); - assertNull(result); - } + assertNull(result); + } - @Test - void createLobbyLobbyCodeIsBlank() { - when(generator.generateLobbyCode()).thenReturn(""); - String result = lobbyService.createLobby("Host"); + @Test + void createLobbyLobbyCodeIsBlank() { + when(generator.generateLobbyCode()).thenReturn(""); + String result = lobbyService.createLobby("Host"); - assertNull(result); - } + assertNull(result); + } - @Test - void joinLobbyReturnFalseLobbyNotExists() { - boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); - assertFalse(result); - } + @Test + void joinLobbyReturnFalseLobbyNotExists() { + boolean result = lobbyService.joinLobby("TestUser", "ABCDE"); + assertFalse(result); + } - @Test - void leaveLobbyReturnTrueLobbyExists() { - lobbyService.createLobby("Host"); + @Test + void leaveLobbyReturnTrueLobbyExists() { + lobbyService.createLobby("Host"); - boolean result = lobbyService.leaveLobby("Host", "ABCDE"); + boolean result = lobbyService.leaveLobby("Host", "ABCDE"); - assertTrue(result); + assertTrue(result); - List players = lobbyService.getPlayers("ABCDE"); + List players = lobbyService.getPlayers("ABCDE"); - assertFalse(players.stream().anyMatch(p -> p.username().equals("Host"))); - } + assertFalse(players.stream().anyMatch(p -> p.username().equals("Host"))); + } - @Test - void leaveLobbyReturnFalseLobbyNotExists() { - boolean result = lobbyService.leaveLobby("Host", "ABCDE"); - assertFalse(result); - } + @Test + void leaveLobbyReturnFalseLobbyNotExists() { + boolean result = lobbyService.leaveLobby("Host", "ABCDE"); + assertFalse(result); + } - @Test - void createLobbyShouldGenerateNewCodeIfDuplicateExists() { - when(generator.generateLobbyCode()) - .thenReturn("ABCDE") - .thenReturn("ABCDE") - .thenReturn("FGHIJ"); + @Test + void createLobbyShouldGenerateNewCodeIfDuplicateExists() { + when(generator.generateLobbyCode()) + .thenReturn("ABCDE") + .thenReturn("ABCDE") + .thenReturn("FGHIJ"); - lobbyService.createLobby("Host1"); - String code2 = lobbyService.createLobby("Host2"); + lobbyService.createLobby("Host1"); + String code2 = lobbyService.createLobby("Host2"); - assertEquals("FGHIJ", code2); - } + assertEquals("FGHIJ", code2); + } - @Test - void selectPositionShouldReturnTrueWhenPlayerChoosesTeamAndRole() { - lobbyService.createLobby("Host"); + @Test + void selectPositionShouldReturnTrueWhenPlayerChoosesTeamAndRole() { + lobbyService.createLobby("Host"); - boolean result = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + boolean result = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - assertTrue(result); - } + assertTrue(result); + } - @Test - void selectPositionShouldReturnFalseWhenLobbyDoesNotExist() { - boolean result = lobbyService.selectPosition("Host", "XXXXX", Team.RED, Role.SPYMASTER); + @Test + void selectPositionShouldReturnFalseWhenLobbyDoesNotExist() { + boolean result = lobbyService.selectPosition("Host", "XXXXX", Team.RED, Role.SPYMASTER); - assertFalse(result); - } + assertFalse(result); + } - @Test - void selectPositionShouldReturnFalseWhenPlayerIsNotInLobby() { - lobbyService.createLobby("Host"); + @Test + void selectPositionShouldReturnFalseWhenPlayerIsNotInLobby() { + lobbyService.createLobby("Host"); - boolean result = lobbyService.selectPosition("Ghost", "ABCDE", Team.RED, Role.SPYMASTER); + boolean result = lobbyService.selectPosition("Ghost", "ABCDE", Team.RED, Role.SPYMASTER); - assertFalse(result); - } + assertFalse(result); + } - @Test - void selectPositionShouldReturnFalseWhenSecondSpymasterChoosesSameTeam() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); + @Test + void selectPositionShouldReturnFalseWhenSecondSpymasterChoosesSameTeam() { + lobbyService.createLobby("Host"); + lobbyService.joinLobby("P1", "ABCDE"); - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.SPYMASTER); + boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.SPYMASTER); - assertTrue(firstResult); - assertFalse(secondResult); - } + assertTrue(firstResult); + assertFalse(secondResult); + } - @Test - void selectPositionShouldReturnTrueWhenSpymastersChooseDifferentTeams() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); + @Test + void selectPositionShouldReturnTrueWhenSpymastersChooseDifferentTeams() { + lobbyService.createLobby("Host"); + lobbyService.joinLobby("P1", "ABCDE"); - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.BLUE, Role.SPYMASTER); + boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.BLUE, Role.SPYMASTER); - assertTrue(firstResult); - assertTrue(secondResult); - } + assertTrue(firstResult); + assertTrue(secondResult); + } - @Test - void selectPositionShouldReturnTrueWhenMultipleOperativesChooseSameTeam() { - lobbyService.createLobby("Host"); - lobbyService.joinLobby("P1", "ABCDE"); + @Test + void selectPositionShouldReturnTrueWhenMultipleOperativesChooseSameTeam() { + lobbyService.createLobby("Host"); + lobbyService.joinLobby("P1", "ABCDE"); - boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); - boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.OPERATIVE); + boolean firstResult = lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); + boolean secondResult = lobbyService.selectPosition("P1", "ABCDE", Team.RED, Role.OPERATIVE); - assertTrue(firstResult); - assertTrue(secondResult); - } + assertTrue(firstResult); + assertTrue(secondResult); + } - @Test - void getPlayersShouldReturnEmptyListWhenLobbyDoesNotExist() { - List players = lobbyService.getPlayers("UNKNOWN"); + @Test + void getPlayersShouldReturnEmptyListWhenLobbyDoesNotExist() { + List players = lobbyService.getPlayers("UNKNOWN"); - assertNotNull(players); - assertTrue(players.isEmpty()); - } + assertNotNull(players); + assertTrue(players.isEmpty()); + } - @Test - void joinLobbyShouldReturnFalseWhenPlayerAlreadyExists() { - lobbyService.createLobby("Host"); + @Test + void joinLobbyShouldReturnFalseWhenPlayerAlreadyExists() { + lobbyService.createLobby("Host"); - boolean first = lobbyService.joinLobby("Max", "ABCDE"); - boolean second = lobbyService.joinLobby("Max", "ABCDE"); + boolean first = lobbyService.joinLobby("Max", "ABCDE"); + boolean second = lobbyService.joinLobby("Max", "ABCDE"); - assertTrue(first); - assertFalse(second); + assertTrue(first); + assertFalse(second); - List players = lobbyService.getPlayers("ABCDE"); + List players = lobbyService.getPlayers("ABCDE"); - long count = players.stream() - .filter(p -> p.username().equals("Max")) - .count(); + long count = players.stream() + .filter(p -> p.username().equals("Max")) + .count(); - assertEquals(1, count); - } + assertEquals(1, count); + } - @Test - void selectPositionShouldReturnFalseIfTeamIsNull() { - when(generator.generateLobbyCode()).thenReturn("ABCDE"); - String lobbyCode = lobbyService.createLobby("Host"); + @Test + void selectPositionShouldReturnFalseIfTeamIsNull() { + when(generator.generateLobbyCode()).thenReturn("ABCDE"); + String lobbyCode = lobbyService.createLobby("Host"); - boolean result = lobbyService.selectPosition("Host", lobbyCode, null, Role.OPERATIVE); + boolean result = lobbyService.selectPosition("Host", lobbyCode, null, Role.OPERATIVE); - assertFalse(result); - } + assertFalse(result); + } - @Test - void selectPositionShouldReturnFalseIfRoleIsNull() { - when(generator.generateLobbyCode()).thenReturn("ABCDE"); - String lobbyCode = lobbyService.createLobby("Host"); + @Test + void selectPositionShouldReturnFalseIfRoleIsNull() { + when(generator.generateLobbyCode()).thenReturn("ABCDE"); + String lobbyCode = lobbyService.createLobby("Host"); - boolean result = lobbyService.selectPosition("Host", lobbyCode, Team.RED, null); + boolean result = lobbyService.selectPosition("Host", lobbyCode, Team.RED, null); - assertFalse(result); - } + assertFalse(result); + } - @Test - void testGetPlayerTeam() { - lobbyService.createLobby("Host"); - lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + @Test + void testGetPlayerTeam() { + lobbyService.createLobby("Host"); + lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); - assertEquals(Team.RED, lobbyService.getPlayerTeam("Host", "ABCDE")); - } + assertEquals(Team.RED, lobbyService.getPlayerTeam("Host", "ABCDE")); + } - @Test - void getPlayerTeam_wrongCode() { - assertNull(lobbyService.getPlayerTeam("Host", "invalidCode")); - } + @Test + void getPlayerTeam_wrongCode() { + assertNull(lobbyService.getPlayerTeam("Host", "invalidCode")); + } - @Test - void getPlayerTeam_nonExistentPlayer() { - String lobbyCode = lobbyService.createLobby("Host"); + @Test + void getPlayerTeam_nonExistentPlayer() { + String lobbyCode = lobbyService.createLobby("Host"); - assertNull(lobbyService.getPlayerTeam("nonExistentPlayer", lobbyCode)); - } + assertNull(lobbyService.getPlayerTeam("nonExistentPlayer", lobbyCode)); + } - @Test - void testGetPlayerRole() { - lobbyService.createLobby("Host"); - lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); + @Test + void testGetPlayerRole() { + lobbyService.createLobby("Host"); + lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.OPERATIVE); - assertEquals(Role.OPERATIVE, lobbyService.getPlayerRole("Host", "ABCDE")); - } + assertEquals(Role.OPERATIVE, lobbyService.getPlayerRole("Host", "ABCDE")); + } - @Test - void getPlayerRole_wrongCode() { - assertNull(lobbyService.getPlayerRole("Host", "test")); - } + @Test + void getPlayerRole_wrongCode() { + assertNull(lobbyService.getPlayerRole("Host", "test")); + } - @Test - void getPlayerRole_nonExistentPlayer() { - String lobbyCode = lobbyService.createLobby("Host"); + @Test + void getPlayerRole_nonExistentPlayer() { + String lobbyCode = lobbyService.createLobby("Host"); -<<<<<<< HEAD assertNull(lobbyService.getPlayerRole("nonExistentPlayer", lobbyCode)); } @@ -273,7 +261,7 @@ void testLobbyIsNotRemovedWhenItHasPlayers() { } @Test - void testGetPlayersDto(){ + void testGetPlayersDto() { lobbyService.createLobby("Host"); List players = lobbyService.getPlayersDto("ABCDE"); @@ -283,38 +271,34 @@ void testGetPlayersDto(){ } @Test - void testGetPlayersDto_lobbyNotExists(){ + void testGetPlayersDto_lobbyNotExists() { List players = lobbyService.getPlayersDto("UNKNOWN"); assertNotNull(players); assertTrue(players.isEmpty()); } -======= - assertNull(lobbyService.getPlayerRole("nonExistentPlayer", lobbyCode)); - } - @Test - void getPlayersDtoShouldReturnPlayerDtos_whenLobbyExists() { - lobbyService.createLobby("Host"); + @Test + void getPlayersDtoShouldReturnPlayerDTOs_whenLobbyExists() { + lobbyService.createLobby("Host"); - List result = lobbyService.getPlayersDto("ABCDE"); + List result = lobbyService.getPlayersDto("ABCDE"); - assertNotNull(result); - assertEquals(1, result.size()); + assertNotNull(result); + assertEquals(1, result.size()); - PlayerDto player = result.get(0); + PlayerDto player = result.get(0); - assertEquals("Host", player.username()); - assertNull(player.team()); - assertNull(player.role()); - assertTrue(player.isHost()); - } + assertEquals("Host", player.username()); + assertNull(player.team()); + assertNull(player.role()); + assertTrue(player.isHost()); + } - @Test - void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { - List result = lobbyService.getPlayersDto("ABCDE"); + @Test + void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { + List result = lobbyService.getPlayersDto("ABCDE"); - assertNotNull(result); - assertTrue(result.isEmpty()); - } ->>>>>>> b37010b683ad5fd93e9b1c48c03db643219c7d3d + assertNotNull(result); + assertTrue(result.isEmpty()); + } } \ No newline at end of file From efe60ffa5a8b775ca23c932742ce9f3399631790 Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 17:33:50 +0200 Subject: [PATCH 084/207] fix: when lobby is empty, also the chat history is deleted --- .../codenames/backend/lobby/services/LobbyService.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 4c712a6a..1a11c52a 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.lobby.services; +import com.codenames.codenames.backend.chat.ChatService; import com.codenames.codenames.backend.lobby.Lobby; import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.utility.Role; @@ -27,14 +28,16 @@ public class LobbyService { @Getter private final Map lobbyList = new ConcurrentHashMap<>(); private final LobbyCodeGenerator generator; + private final ChatService chatService; /** * Creates a new {@code LobbyService}. * * @param generator the lobby code generator used to create unique lobby codes */ - public LobbyService(LobbyCodeGenerator generator) { + public LobbyService(LobbyCodeGenerator generator, ChatService chatService) { this.generator = generator; + this.chatService = chatService; } /** @@ -128,6 +131,7 @@ public void checkLobbyStillHasPlayers(String lobbyCode) { Lobby lobby = lobbyList.get(lobbyCode); if (lobby.getPlayerList().isEmpty()) { lobbyList.remove(lobbyCode); + chatService.clearLobbyHistory(lobbyCode); } } From b055978c805d3b1bab74c9415438446b97eac837 Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 17:45:04 +0200 Subject: [PATCH 085/207] fix: lobby is now also removed from gameService lists when it is empty --- .../codenames/codenames/backend/lobby/services/LobbyService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 8aa1752b..df5293e6 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -151,6 +151,7 @@ public void checkLobbyStillHasPlayers(String lobbyCode) { if (lobby.getPlayerList().isEmpty()) { lobbyList.remove(lobbyCode); chatService.clearLobbyHistory(lobbyCode); + gameService.removeGame(lobbyCode); } } From 831e9b48657bab2e4f8382eb62f6925468ed4669 Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 17:55:15 +0200 Subject: [PATCH 086/207] log: added log output --- .../codenames/codenames/backend/lobby/services/LobbyService.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index df5293e6..3fb7dea0 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -152,6 +152,7 @@ public void checkLobbyStillHasPlayers(String lobbyCode) { lobbyList.remove(lobbyCode); chatService.clearLobbyHistory(lobbyCode); gameService.removeGame(lobbyCode); + log.info("{}: Lobby is empty, was removed from list.", lobbyCode); } } From babf312708ba17741dad26a7e97cd92f2adaceb9 Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 18:12:27 +0200 Subject: [PATCH 087/207] sonar: removed empty constructor of record class --- .../com/codenames/codenames/backend/websocket/Player.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/websocket/Player.java b/src/main/java/com/codenames/codenames/backend/websocket/Player.java index ddce6bc8..38b6cd83 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/Player.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/Player.java @@ -6,11 +6,4 @@ *

A player is identified by a username and may be associated with a WebSocket session. */ public record Player(String username, boolean isHost) { - /** - * Creates a new player. - * - * @param username the player's username - */ - public Player { - } } From 8e054d6d39ea2cfaa5faffc9947d32c588aaaf3b Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 21:49:07 +0200 Subject: [PATCH 088/207] fix: minor fixes, see requested changes --- .../backend/lobby/controller/LobbyController.java | 11 +++-------- src/main/resources/application.yaml | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 2086b975..c23afd14 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -118,14 +118,9 @@ public ResponseEntity getLobbyInfo( @PathVariable String lobbyCode ) { List players = service.getPlayersDto(lobbyCode); - if (players != null) { - return ResponseEntity.ok( - new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players) - ); - } else { - return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); - } + return ResponseEntity.ok( + new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players) + ); } /** diff --git a/src/main/resources/application.yaml b/src/main/resources/application.yaml index d1f130a3..67237b60 100644 --- a/src/main/resources/application.yaml +++ b/src/main/resources/application.yaml @@ -3,4 +3,4 @@ spring: name: Codenames_Backend app: - allowed-origins: "*" \ No newline at end of file + allowed-origins: "http://localhost:8080,http://10.0.2.2:8080" \ No newline at end of file From 78988e27a521c27f233d91450acb4fc6fccadf0f Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 22:13:18 +0200 Subject: [PATCH 089/207] style: reordered imports to fit checkstyles wishes --- .../codenames/codenames/backend/lobby/Lobby.java | 3 +-- .../backend/lobby/controller/LobbyController.java | 13 +++++++++---- .../backend/lobby/services/LobbyCodeGenerator.java | 3 +-- .../backend/lobby/services/LobbyService.java | 7 +++---- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java b/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java index 7263bfb3..05303511 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/Lobby.java @@ -3,13 +3,12 @@ import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; -import lombok.Getter; - import java.security.SecureRandom; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.CopyOnWriteArrayList; +import lombok.Getter; /** * Represents a game lobby containing a limited number of playerList. diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index c23afd14..68eb9f78 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -3,10 +3,15 @@ import com.codenames.codenames.backend.lobby.dto.LobbyResponse; import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyService; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - import java.util.List; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; /** * REST controller for handling lobby management operations. @@ -110,7 +115,7 @@ public ResponseEntity leaveLobby( * * @param lobbyCode unique lobby code * @return a response entity with the http code 200 for ok and - * 400 for bad request, if an error occurred + * 400 for bad request, if an error occurred */ @GetMapping("/{lobbyCode}") diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java index 682e4393..c878bcf2 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyCodeGenerator.java @@ -1,8 +1,7 @@ package com.codenames.codenames.backend.lobby.services; -import org.springframework.stereotype.Service; - import java.security.SecureRandom; +import org.springframework.stereotype.Service; /** * Utility service for generating unique lobby codes. diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 3fb7dea0..17a18199 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -7,13 +7,12 @@ import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Service; - import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; /** * Service responsible for managing lobbies and player interactions. From ee1c2dd2adf08a917aadf9e37024277884cb1ee8 Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Fri, 15 May 2026 22:16:06 +0200 Subject: [PATCH 090/207] test: removed tests that tested the unreachable if-else-branch --- .../lobby/controller/LobbyControllerTest.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java index 57c1e736..874b3036 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java @@ -79,14 +79,6 @@ void getLobbyInfoShouldReturn200() throws Exception { .andExpect(status().isOk()); } - @Test - void getLobbyInfoShouldReturn404() throws Exception { - when(service.getPlayersDto("XXXXX")).thenReturn(null); - String url = "/lobby/XXXXX"; - mockMvc.perform(get(url)) - .andExpect(status().isBadRequest()); - } - @Test void joinLobbyShouldReturn400_whenNotFound() throws Exception { when(service.joinLobby("TestUser", "XXXXX")).thenReturn(false); @@ -178,16 +170,4 @@ void getLobbyInfoShouldReturn200_whenLobbyExists() throws Exception { .andExpect(jsonPath("$.playerList[1].username") .value("Bob")); } - - @Test - void getLobbyInfoShouldReturn400_whenLobbyDoesNotExist() throws Exception { - when(service.getPlayersDto("ABCDE")).thenReturn(null); - - mockMvc.perform(get("/lobby/ABCDE")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message") - .value("Could not find lobby.")) - .andExpect(jsonPath("$.lobbyCode") - .value("ABCDE")); - } } \ No newline at end of file From d970836adb92ef992996a28ea5b24817cf35b9e7 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 13:31:06 +0200 Subject: [PATCH 091/207] refactor: inject game service into websocket controller --- .../backend/game/controller/GameSocketController.java | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index ddb3f911..20859eba 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -9,9 +9,8 @@ import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.CardGenerator; import com.codenames.codenames.backend.playingfield.GameManager; +import com.codenames.codenames.backend.playingfield.GameService; import com.codenames.codenames.backend.utility.Team; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; @@ -25,12 +24,9 @@ @Controller public class GameSocketController { - private final LobbyService lobbyService; - private final SimpMessagingTemplate messagingTemplate; - private final CardGenerator cardGenerator; - private final ClueValidationService clueValidationService; + private final GameService gameService; - private final Map gameSessions = new ConcurrentHashMap<>(); + private final SimpMessagingTemplate messagingTemplate; private static final String GAME_TOPIC_PREFIX = "/topic/game/"; From 1ca9086cccc9a2d51a390644010c50e55eb14bb0 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 13:32:19 +0200 Subject: [PATCH 092/207] refactor: simplify websocket controller dependencies --- .../backend/game/controller/GameSocketController.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 20859eba..2c771194 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -47,15 +47,10 @@ private GameStateDto mapGameState(GameManager gameManager) { * @param cardGenerator utility for generating game cards * @param clueValidationService service for validating clues */ - public GameSocketController( - LobbyService lobbyService, - SimpMessagingTemplate messagingTemplate, - CardGenerator cardGenerator, - ClueValidationService clueValidationService) { - this.lobbyService = lobbyService; + public GameSocketController(GameService gameService, SimpMessagingTemplate messagingTemplate) { + + this.gameService = gameService; this.messagingTemplate = messagingTemplate; - this.cardGenerator = cardGenerator; - this.clueValidationService = clueValidationService; } /** From 0adbd6e44bb208572b0155f1baa758a06da88b9f Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 13:56:41 +0200 Subject: [PATCH 093/207] feat: expose game state retrieval in game service --- .../codenames/backend/playingfield/GameService.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index fe190a1a..10bab15f 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -7,9 +7,9 @@ import org.springframework.stereotype.Service; /** - * Service class for the game. The class stores an instance of GameManager for each - * lobby. This also exposes the methods of the GameManager, so that the websocket controllers can - * have message mappings to allow frontend to interact with the backend. + * Service class for the game. The class stores an instance of GameManager for each lobby. This also + * exposes the methods of the GameManager, so that the websocket controllers can have message + * mappings to allow frontend to interact with the backend. */ @Service public class GameService { @@ -34,8 +34,7 @@ public GameService(GameManagerFactory gameManagerFactory) { * @param startingTeam the starting team required to initialize a GM */ public void createGameManager(String lobbyCode, Team startingTeam) { - games.computeIfAbsent( - lobbyCode, key -> gameManagerFactory.create(startingTeam)); + games.computeIfAbsent(lobbyCode, key -> gameManagerFactory.create(startingTeam)); } /** @@ -60,6 +59,10 @@ private GameManager getGame(String lobbyCode) { return games.get(lobbyCode); } + public GameManager getGameState(String lobbyCode) { + return getGame(lobbyCode); + } + /** * The exposed clue submission method from GM that is accessed by frontend via websockets. * From 8da55381a1504d29f510929441660b5ac4628ea0 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 14:00:12 +0200 Subject: [PATCH 094/207] refactor: use game service for start game state updates --- .../backend/game/controller/GameSocketController.java | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 2c771194..19e33f54 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -63,14 +63,10 @@ public GameSocketController(GameService gameService, SimpMessagingTemplate messa @MessageMapping("/start-game") public void startGame(StartGameMessage message) { - Team startingTeam = lobbyService.decideStartingTeam(message.getLobbyCode()); - - GameManager gameManager = new GameManager(startingTeam, cardGenerator, clueValidationService); - - gameSessions.put(message.getLobbyCode(), gameManager); + GameManager game = gameService.getGameState(message.getLobbyCode()); messagingTemplate.convertAndSend( - GAME_TOPIC_PREFIX + message.getLobbyCode(), mapGameState(gameManager)); + GAME_TOPIC_PREFIX + message.getLobbyCode(), mapGameState(game)); } /** From d96177287c32ccdf611ff341ac7a40f90b1ce44e Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 14:03:47 +0200 Subject: [PATCH 095/207] refactor: delegate reveal card handling to game service --- .../backend/game/controller/GameSocketController.java | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 19e33f54..bfa95da2 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -79,16 +79,11 @@ public void startGame(StartGameMessage message) { @MessageMapping("/reveal-card") public void revealCard(RevealCardMessage message) { - GameManager gameManager = gameSessions.get(message.getLobbyCode()); - - if (gameManager == null) { - return; - } - - gameManager.flipCard(message.getPosition(), message.getCurrentTurn()); + gameService.flipCard(message.getLobbyCode(), message.getPosition(), message.getCurrentTurn()); messagingTemplate.convertAndSend( - GAME_TOPIC_PREFIX + message.getLobbyCode(), mapGameState(gameManager)); + GAME_TOPIC_PREFIX + message.getLobbyCode(), + mapGameState(gameService.getGameState(message.getLobbyCode()))); } /** From 9dacbfc36b05a5f9de20b66f233d37dd32734d9c Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 14:04:28 +0200 Subject: [PATCH 096/207] refactor: add calling team to clue websocket message --- .../com/codenames/codenames/backend/game/dto/ClueMessage.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java index 466c95c7..6b120c95 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/dto/ClueMessage.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.game.dto; +import com.codenames.codenames.backend.utility.Team; import lombok.Getter; import lombok.Setter; @@ -14,4 +15,5 @@ public class ClueMessage { private String lobbyCode; private String word; private int guessAmount; + private Team currentTurn; } From b0bf540b740c3ac96ba5c772097feafe1015f2dc Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 14:08:49 +0200 Subject: [PATCH 097/207] refactor: delegate clue submission to game service --- .../game/controller/GameSocketController.java | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index bfa95da2..16548a16 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -96,17 +96,13 @@ public void revealCard(RevealCardMessage message) { @MessageMapping("/submit-clue") public void submitClue(ClueMessage message) { - GameManager gameManager = gameSessions.get(message.getLobbyCode()); - - if (gameManager == null) { - return; - } - - Clue clue = new Clue(message.getWord(), message.getGuessAmount()); - - gameManager.submitClue(clue); + gameService.submitClue( + message.getLobbyCode(), + new Clue(message.getWord(), message.getGuessAmount()), + message.getCurrentTurn()); messagingTemplate.convertAndSend( - GAME_TOPIC_PREFIX + message.getLobbyCode(), mapGameState(gameManager)); + GAME_TOPIC_PREFIX + message.getLobbyCode(), + mapGameState(gameService.getGameState(message.getLobbyCode()))); } } From 213c7077cd85d4cbe705674ffa9645ac1a44a4d3 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 15:10:10 +0200 Subject: [PATCH 098/207] feat: add game state dto creation to game service --- .../codenames/backend/playingfield/GameService.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 10bab15f..08d4e4e4 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.playingfield; import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.game.dto.GameStateDto; import com.codenames.codenames.backend.utility.Team; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -98,4 +99,12 @@ public void passTurn(String lobbyCode, Team callingTeam) { GameManager gm = getGame(lobbyCode); gm.passTurn(callingTeam); } + + public GameStateDto createGameStateDto(String lobbyCode) { + + GameManager gm = getGame(lobbyCode); + + return new GameStateDto( + gm.getCardList(), gm.getCurrentClue(), gm.getRemainingGuesses(), gm.getWinner()); + } } From d182d0ce6ce1d0b89acad914b14a9585a67677b1 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 15:11:22 +0200 Subject: [PATCH 099/207] refactor: remove game state mapping from websocket controller --- .../backend/game/controller/GameSocketController.java | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 16548a16..689ffabf 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -30,15 +30,6 @@ public class GameSocketController { private static final String GAME_TOPIC_PREFIX = "/topic/game/"; - private GameStateDto mapGameState(GameManager gameManager) { - - return new GameStateDto( - gameManager.getCardList(), - gameManager.getCurrentClue(), - gameManager.getRemainingGuesses(), - gameManager.getWinner()); - } - /** * Creates a new {@code GameSocketController}. * From d4c689947bd705f45f3eb6ebdcf44671d775c10f Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 15:30:26 +0200 Subject: [PATCH 100/207] refactor: use game service for websocket response mapping --- .../backend/game/controller/GameSocketController.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 689ffabf..76cb0c11 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -57,7 +57,8 @@ public void startGame(StartGameMessage message) { GameManager game = gameService.getGameState(message.getLobbyCode()); messagingTemplate.convertAndSend( - GAME_TOPIC_PREFIX + message.getLobbyCode(), mapGameState(game)); + GAME_TOPIC_PREFIX + message.getLobbyCode(), + gameService.createGameStateDto(message.getLobbyCode())); } /** @@ -74,7 +75,7 @@ public void revealCard(RevealCardMessage message) { messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), - mapGameState(gameService.getGameState(message.getLobbyCode()))); + gameService.createGameStateDto(message.getLobbyCode())); } /** @@ -94,6 +95,6 @@ public void submitClue(ClueMessage message) { messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), - mapGameState(gameService.getGameState(message.getLobbyCode()))); + gameService.createGameStateDto(message.getLobbyCode())); } } From d34d5f850ab26c3b9fe2719b5d6602c9292dc9e9 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 16:07:49 +0200 Subject: [PATCH 101/207] refactor: remove unused game state retrieval in websocket controller --- .../codenames/backend/game/controller/GameSocketController.java | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 76cb0c11..c2b11e51 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -54,8 +54,6 @@ public GameSocketController(GameService gameService, SimpMessagingTemplate messa @MessageMapping("/start-game") public void startGame(StartGameMessage message) { - GameManager game = gameService.getGameState(message.getLobbyCode()); - messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), gameService.createGameStateDto(message.getLobbyCode())); From a41f1a024727491486af885e3154fc8ee756efee Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 16:08:32 +0200 Subject: [PATCH 102/207] style: remove unused imports from websocket controller --- .../backend/game/controller/GameSocketController.java | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index c2b11e51..18a4d371 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -1,16 +1,10 @@ package com.codenames.codenames.backend.game.controller; import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.game.dto.ClueMessage; -import com.codenames.codenames.backend.game.dto.GameStateDto; import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.playingfield.CardGenerator; -import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.utility.Team; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; From 063e82a6bb4b938fbf7eb3180825e289ef5eff84 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 16:25:04 +0200 Subject: [PATCH 103/207] test: align websocket controller tests with game service architecture --- .../controller/GameSocketControllerTest.java | 119 +++++------------- 1 file changed, 30 insertions(+), 89 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 934de4b3..b6c820d2 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -1,24 +1,17 @@ package com.codenames.codenames.backend.game.controller; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.times; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.game.dto.ClueMessage; +import com.codenames.codenames.backend.game.dto.GameStateDto; import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import com.codenames.codenames.backend.playingfield.Card; -import com.codenames.codenames.backend.playingfield.CardGenerator; -import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.playingfield.GameService; import com.codenames.codenames.backend.utility.Team; -import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -28,119 +21,67 @@ @ExtendWith(MockitoExtension.class) class GameSocketControllerTest { - @Mock private LobbyService lobbyService; - @Mock private SimpMessagingTemplate messagingTemplate; - - @Mock private CardGenerator cardGenerator; + @Mock private GameService gameService; - @Mock private ClueValidationService clueValidationService; + @Mock private SimpMessagingTemplate messagingTemplate; private GameSocketController controller; @BeforeEach void setUp() { - controller = - new GameSocketController( - lobbyService, messagingTemplate, cardGenerator, clueValidationService); + + controller = new GameSocketController(gameService, messagingTemplate); } @Test - void startGameShouldBroadcastBoard() { - - when(lobbyService.decideStartingTeam("ABCDE")).thenReturn(Team.RED); + void startGameShouldBroadcastState() { StartGameMessage message = new StartGameMessage(); message.setLobbyCode("ABCDE"); - controller.startGame(message); - - verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); - } - - @Test - void revealCardShouldBroadcastBoardUpdate() { - - when(lobbyService.decideStartingTeam("ABCDE")).thenReturn(Team.RED); - - when(cardGenerator.generateCards(anyInt(), anyInt(), anyInt(), anyInt(), anyInt())) - .thenReturn(List.of(new Card("Test", Color.RED))); - - StartGameMessage startMessage = new StartGameMessage(); - - startMessage.setLobbyCode("ABCDE"); - - when(clueValidationService.validateWord(any(), anyString())).thenReturn(true); - - controller.startGame(startMessage); - - ClueMessage clueMessage = new ClueMessage(); - - clueMessage.setLobbyCode("ABCDE"); - clueMessage.setWord("animal"); - clueMessage.setGuessAmount(1); + when(gameService.createGameStateDto("ABCDE")).thenReturn(mock(GameStateDto.class)); - controller.submitClue(clueMessage); - - RevealCardMessage revealMessage = new RevealCardMessage(); - - revealMessage.setLobbyCode("ABCDE"); - revealMessage.setPosition(0); - revealMessage.setCurrentTurn(Team.RED); - - controller.revealCard(revealMessage); + controller.startGame(message); - verify(messagingTemplate, times(3)).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); + verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } @Test - void revealCardShouldReturnWhenGameSessionMissing() { + void revealCardShouldBroadcastState() { RevealCardMessage message = new RevealCardMessage(); - message.setLobbyCode("UNKNOWN"); + message.setLobbyCode("ABCDE"); + message.setPosition(0); + message.setCurrentTurn(Team.RED); + + when(gameService.createGameStateDto("ABCDE")).thenReturn(mock(GameStateDto.class)); controller.revealCard(message); - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); + verify(gameService).flipCard("ABCDE", 0, Team.RED); + + verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } @Test - void submitClueShouldBroadcastGameUpdate() { - - when(lobbyService.decideStartingTeam("ABCDE")).thenReturn(Team.RED); - - StartGameMessage startMessage = new StartGameMessage(); + void submitClueShouldBroadcastState() { - startMessage.setLobbyCode("ABCDE"); + ClueMessage message = new ClueMessage(); - controller.startGame(startMessage); - - ClueMessage clueMessage = new ClueMessage(); - - clueMessage.setLobbyCode("ABCDE"); - clueMessage.setWord("animal"); - clueMessage.setGuessAmount(2); - - when(clueValidationService.validateWord(any(), anyString())).thenReturn(true); - - controller.submitClue(clueMessage); - - verify(messagingTemplate, times(2)).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); - } - - @Test - void submitClueShouldReturnWhenGameSessionMissing() { + message.setLobbyCode("ABCDE"); + message.setWord("animal"); + message.setGuessAmount(2); + message.setCurrentTurn(Team.RED); - ClueMessage clueMessage = new ClueMessage(); + when(gameService.createGameStateDto("ABCDE")).thenReturn(mock(GameStateDto.class)); - clueMessage.setLobbyCode("UNKNOWN"); - clueMessage.setWord("animal"); - clueMessage.setGuessAmount(2); + controller.submitClue(message); - controller.submitClue(clueMessage); + verify(gameService).submitClue(anyString(), any(), any()); - verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class)); + verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } } From 5a843b33758af686cd9e344d1689da4c2e75fcc0 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 16:28:21 +0200 Subject: [PATCH 104/207] test: verify game state retrieval from service --- .../codenames/backend/playingfield/GameServiceTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index 69b83080..9f97b9d0 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.playingfield; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -72,4 +73,12 @@ void testPassTurn() { verify(mockGameManager, times(1)).passTurn(redTeam); } + + @Test + void testGetGameState() { + + GameManager result = gameService.getGameState(lobbyCode); + + assertEquals(mockGameManager, result); + } } From 032600a86c92e072cfde9912bc0abe3e56667560 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 16:29:42 +0200 Subject: [PATCH 105/207] test: verify game state dto creation --- .../backend/playingfield/GameServiceTest.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index 9f97b9d0..ad4c8b8c 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.playingfield; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; @@ -8,10 +9,13 @@ import static org.mockito.Mockito.when; import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.game.dto.GameStateDto; import com.codenames.codenames.backend.utility.Team; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import java.util.List; + class GameServiceTest { private GameService gameService; private GameManager mockGameManager; @@ -81,4 +85,14 @@ void testGetGameState() { assertEquals(mockGameManager, result); } + + @Test + void testCreateGameStateDto() { + + when(mockGameManager.getCardList()).thenReturn(List.of()); + + GameStateDto dto = gameService.createGameStateDto(lobbyCode); + + assertNotNull(dto); + } } From 8b379ac090168dd9192d289041478478d3cb146d Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 16:38:07 +0200 Subject: [PATCH 106/207] docs: add/modify javadocs --- .../game/controller/GameSocketController.java | 12 ++++-------- .../codenames/backend/playingfield/GameService.java | 12 ++++++++++++ .../game/controller/GameSocketControllerTest.java | 1 + .../backend/playingfield/GameServiceTest.java | 4 ++-- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 18a4d371..06da4d16 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -27,10 +27,8 @@ public class GameSocketController { /** * Creates a new {@code GameSocketController}. * - * @param lobbyService service for lobby management - * @param messagingTemplate template used for broadcasting messages - * @param cardGenerator utility for generating game cards - * @param clueValidationService service for validating clues + * @param gameService service responsible for gameplay logic + * @param messagingTemplate template used for broadcasting websocket messages */ public GameSocketController(GameService gameService, SimpMessagingTemplate messagingTemplate) { @@ -39,11 +37,9 @@ public GameSocketController(GameService gameService, SimpMessagingTemplate messa } /** - * Starts a new game session for a lobby. + * Sends the current game state to subscribed players. * - *

Creates a new game manager and broadcasts the initial board state to all subscribed players. - * - * @param message the start game request + * @param message contains the lobby code */ @MessageMapping("/start-game") public void startGame(StartGameMessage message) { diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 08d4e4e4..e1d83e20 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -60,6 +60,12 @@ private GameManager getGame(String lobbyCode) { return games.get(lobbyCode); } + /** + * Retrieves the current GameManager for a lobby. + * + * @param lobbyCode lobby identifier + * @return the active GameManager + */ public GameManager getGameState(String lobbyCode) { return getGame(lobbyCode); } @@ -100,6 +106,12 @@ public void passTurn(String lobbyCode, Team callingTeam) { gm.passTurn(callingTeam); } + /** + * Creates a DTO representing the current game state. + * + * @param lobbyCode lobby identifier + * @return DTO containing board and turn information + */ public GameStateDto createGameStateDto(String lobbyCode) { GameManager gm = getGame(lobbyCode); diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index b6c820d2..0b3842b9 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -19,6 +19,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import org.springframework.messaging.simp.SimpMessagingTemplate; +/** Tests websocket gameplay controller interactions. */ @ExtendWith(MockitoExtension.class) class GameSocketControllerTest { diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index ad4c8b8c..cfa8725b 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -11,11 +11,11 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.game.dto.GameStateDto; import com.codenames.codenames.backend.utility.Team; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.List; - +/** Tests the functionality of GameService. */ class GameServiceTest { private GameService gameService; private GameManager mockGameManager; From 1d2dfa26c71aaf0ac4af49261cc9e61272f956ea Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 16:42:09 +0200 Subject: [PATCH 107/207] refactor: remove unused lobby starting team retrieval --- .../backend/lobby/services/LobbyService.java | 63 +++++++------------ .../lobby/services/LobbyServiceTest.java | 29 +-------- 2 files changed, 25 insertions(+), 67 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 0d6ae6a5..8f5859ac 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -17,16 +17,14 @@ /** * Service responsible for managing lobbies and player interactions. * - *

Handles creation of lobbies, player joins/leaves, and retrieval of lobby data. - * Ensures uniqueness of lobby codes and thread-safe access to lobby storage. + *

Handles creation of lobbies, player joins/leaves, and retrieval of lobby data. Ensures + * uniqueness of lobby codes and thread-safe access to lobby storage. */ - @Slf4j @Service public class LobbyService { - @Getter - private final Map lobbyList = new ConcurrentHashMap<>(); + @Getter private final Map lobbyList = new ConcurrentHashMap<>(); private final LobbyCodeGenerator generator; private final GameService gameService; private final ChatService chatService; @@ -37,9 +35,7 @@ public class LobbyService { * @param generator the lobby code generator used to create unique lobby codes */ public LobbyService( - LobbyCodeGenerator generator, - ChatService chatService, - GameService gameService) { + LobbyCodeGenerator generator, ChatService chatService, GameService gameService) { this.generator = generator; this.chatService = chatService; this.gameService = gameService; @@ -68,7 +64,7 @@ public String createLobby(String username) { /** * Helper method to add the GameManager once a lobby is created. * - * @param lobby the lobby object to determine the starting team + * @param lobby the lobby object to determine the starting team * @param lobbyCode the ID for the lobby which the GameManager is responsible for */ private void addGameManagerForLobby(Lobby lobby, String lobbyCode) { @@ -79,7 +75,7 @@ private void addGameManagerForLobby(Lobby lobby, String lobbyCode) { /** * Adds a player to an existing lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby * @return {@code true} if the player successfully joined, {@code false} otherwise */ @@ -96,7 +92,7 @@ public boolean joinLobby(String username, String lobbyCode) { /** * Removes a player from a lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby * @return {@code true} if the player was removed, {@code false} if the lobby does not exist */ @@ -114,10 +110,10 @@ public boolean leaveLobby(String username, String lobbyCode) { /** * Assigns a team and role to a player in a lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby - * @param team the selected team - * @param role the selected role + * @param team the selected team + * @param role the selected role * @return {@code true} if the position was assigned, {@code false} otherwise */ public boolean selectPosition(String username, String lobbyCode, Team team, Role role) { @@ -140,8 +136,8 @@ public boolean selectPosition(String username, String lobbyCode, Team team, Role } /** - * Checks if the lobby still has players - * after a player leaves and removes the lobby if it is empty. + * Checks if the lobby still has players after a player leaves and removes the lobby if it is + * empty. * * @param lobbyCode the lobby code identifying the lobby */ @@ -172,17 +168,18 @@ public List getPlayers(String lobbyCode) { * @param lobbyCode the lobby code identifying the lobby * @return a list of PlayerDto objects, or an empty list if the lobby does not exist */ - public List getPlayersDto(String lobbyCode) { Lobby lobby = lobbyList.get(lobbyCode); if (lobby != null) { return lobby.getPlayerList().stream() - .map(player -> new PlayerDto( + .map( + player -> + new PlayerDto( player.username(), lobby.getPlayerTeam(player.username()), lobby.getPlayerRole(player.username()), player.isHost())) - .toList(); + .toList(); } return List.of(); } @@ -190,16 +187,16 @@ public List getPlayersDto(String lobbyCode) { /** * Checks whether a spymaster is already assigned for the given team in the lobby. * - * @param lobby the lobby to inspect + * @param lobby the lobby to inspect * @param username the username requesting the role - * @param team the team to inspect + * @param team the team to inspect * @return {@code true} if a different player is already the spymaster for that team */ private boolean isSpymasterAlreadyAssigned(Lobby lobby, String username, Team team) { for (Player player : lobby.getPlayerList()) { if (!player.username().equals(username) - && lobby.getPlayerTeam(player.username()) == team - && lobby.getPlayerRole(player.username()) == Role.SPYMASTER) { + && lobby.getPlayerTeam(player.username()) == team + && lobby.getPlayerRole(player.username()) == Role.SPYMASTER) { return true; } } @@ -230,7 +227,7 @@ private String generateLobbyCode() { /** * Retrieves the team of a player in a lobby. * - * @param username the username of a player + * @param username the username of a player * @param lobbyCode the lobby code of the lobby * @return the team of the player, or {@code null} if the lobby or player does not exist */ @@ -245,7 +242,7 @@ public Team getPlayerTeam(String username, String lobbyCode) { /** * Retrieves the role of a player in a lobby. * - * @param username the username of a player + * @param username the username of a player * @param lobbyCode the lobby code of the lobby * @return the role of the player, or {@code null} if the lobby or player does not exist */ @@ -256,20 +253,4 @@ public Role getPlayerRole(String username, String lobbyCode) { } return null; } - - /** - * Determines the starting team for a lobby. - * - * @param lobbyCode the lobby code - * @return the randomly selected starting team, or null if the lobby does not exist - */ - public Team decideStartingTeam(String lobbyCode) { - Lobby lobby = lobbyList.get(lobbyCode); - - if (lobby == null) { - return null; - } - - return lobby.decideStartingTeam(); - } } diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index d9ac3799..fb970be7 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -91,10 +91,7 @@ void leaveLobbyReturnFalseLobbyNotExists() { @Test void createLobbyShouldGenerateNewCodeIfDuplicateExists() { - when(generator.generateLobbyCode()) - .thenReturn("ABCDE") - .thenReturn("ABCDE") - .thenReturn("FGHIJ"); + when(generator.generateLobbyCode()).thenReturn("ABCDE").thenReturn("ABCDE").thenReturn("FGHIJ"); lobbyService.createLobby("Host1"); String code2 = lobbyService.createLobby("Host2"); @@ -183,9 +180,7 @@ void joinLobbyShouldReturnFalseWhenPlayerAlreadyExists() { List players = lobbyService.getPlayers("ABCDE"); - long count = players.stream() - .filter(p -> p.username().equals("Max")) - .count(); + long count = players.stream().filter(p -> p.username().equals("Max")).count(); assertEquals(1, count); } @@ -308,27 +303,9 @@ void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { assertTrue(result.isEmpty()); } - @Test - void decideStartingTeamShouldReturnTeam() { - - lobbyService.createLobby("Host"); - - Team result = lobbyService.decideStartingTeam("ABCDE"); - - assertNotNull(result); - } - - @Test - void decideStartingTeamShouldReturnNullWhenLobbyDoesNotExist() { - - Team result = lobbyService.decideStartingTeam("UNKNOWN"); - - assertNull(result); - } - @Test void testAddGameManagerForLobby() { lobbyService.createLobby("Host"); verify(gameService, times(1)).createGameManager(eq("ABCDE"), any(Team.class)); } -} \ No newline at end of file +} From 1e9550647a6b0320f971783ad6fe24541b1add6a Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 16:45:29 +0200 Subject: [PATCH 108/207] style: fix imports --- .../lobby/services/LobbyServiceTest.java | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index fb970be7..ac9c971b 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -1,21 +1,27 @@ package com.codenames.codenames.backend.lobby.services; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + import com.codenames.codenames.backend.chat.ChatService; import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.playingfield.GameService; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.*; - /** * Tests for {@link LobbyService}. * From a60e49f15957bb7fbe2621caa39567e84a3b6785 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 16:47:04 +0200 Subject: [PATCH 109/207] style: fix test method naming to satisfy checkstyle --- .../codenames/backend/lobby/services/LobbyServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index ac9c971b..8fcfdab5 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -285,7 +285,7 @@ void testGetPlayersDto_lobbyNotExists() { } @Test - void getPlayersDtoShouldReturnPlayerDTOs_whenLobbyExists() { + void getPlayersDtoShouldReturnPlayerDtos_whenLobbyExists() { lobbyService.createLobby("Host"); List result = lobbyService.getPlayersDto("ABCDE"); From ef0f088fa395ef7c4e861f5d16b335138c43789f Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 17:10:53 +0200 Subject: [PATCH 110/207] feat: include current turn and phase in game state dto --- .../codenames/backend/game/dto/GameStateDto.java | 13 ++++++++++++- .../codenames/backend/playingfield/GameService.java | 7 ++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java b/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java index cd86d0cc..e7a03286 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java +++ b/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java @@ -2,6 +2,7 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.playingfield.Card; +import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.List; import lombok.Getter; @@ -13,6 +14,8 @@ public class GameStateDto { private final Clue currentClue; private final int remainingGuesses; private final Team winner; + private final Team currentTurn; + private final Role currentPhase; /** * Creates a new game state DTO. @@ -22,11 +25,19 @@ public class GameStateDto { * @param remainingGuesses remaining guesses * @param winner winning team or null */ - public GameStateDto(List cards, Clue currentClue, int remainingGuesses, Team winner) { + public GameStateDto( + List cards, + Clue currentClue, + int remainingGuesses, + Team winner, + Team currentTurn, + Role currentPhase) { this.cards = cards; this.currentClue = currentClue; this.remainingGuesses = remainingGuesses; this.winner = winner; + this.currentTurn = currentTurn; + this.currentPhase = currentPhase; } } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index e1d83e20..ae180344 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -117,6 +117,11 @@ public GameStateDto createGameStateDto(String lobbyCode) { GameManager gm = getGame(lobbyCode); return new GameStateDto( - gm.getCardList(), gm.getCurrentClue(), gm.getRemainingGuesses(), gm.getWinner()); + gm.getCardList(), + gm.getCurrentClue(), + gm.getRemainingGuesses(), + gm.getWinner(), + gm.getCurrentTurn(), + gm.getCurrentPhase()); } } From 8da1fca63cfdd3ddf594af7b77cccb27bde00316 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 17:26:47 +0200 Subject: [PATCH 111/207] style: add line-break --- .../codenames/backend/lobby/services/LobbyService.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 8f5859ac..5cb7b3be 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -24,7 +24,8 @@ @Service public class LobbyService { - @Getter private final Map lobbyList = new ConcurrentHashMap<>(); + @Getter + private final Map lobbyList = new ConcurrentHashMap<>(); private final LobbyCodeGenerator generator; private final GameService gameService; private final ChatService chatService; From 4d5e67afcd8dcf20314b7e2b8d94d64ba876d27b Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 21:25:18 +0200 Subject: [PATCH 112/207] feat: add websocket message for turn passing --- .../codenames/backend/game/dto/PassTurnMessage.java | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java new file mode 100644 index 00000000..ef2a9f73 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java @@ -0,0 +1,12 @@ +package com.codenames.codenames.backend.game.dto; + +import com.codenames.codenames.backend.utility.Team; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class PassTurnMessage { + private String lobbyCode; + private Team currentTurn; +} From 5b8678f21ac020ed81395ec2e5673297fa131733 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sat, 16 May 2026 21:28:27 +0200 Subject: [PATCH 113/207] feat: add current phase of the game to allow frontend show game loop --- .../backend/serialization/DataTransferObjectService.java | 3 ++- .../backend/serialization/GameStateDataTransferObject.java | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java index df3e594a..5926080c 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java @@ -39,7 +39,7 @@ private CardDataTransferObject createCardDataTransferObject(Card card, Role role * @return a DTO of the current game state */ public GameStateDataTransferObject createGameStateDataTransferObject( - GameManager gameManager, Role role, Team currentTurn) { + GameManager gameManager, Role role, Team currentTurn, Role currentPhase) { List cardList = gameManager.getCardList(); List cardDataTransferObject = new ArrayList<>(); @@ -55,6 +55,7 @@ public GameStateDataTransferObject createGameStateDataTransferObject( return new GameStateDataTransferObject( winner, currentTurn, + currentPhase, gameManager.getCurrentRedFound(), gameManager.getCurrentBlueFound(), gameManager.getCurrentClueWord(), diff --git a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java index 3353415a..c17708c5 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.serialization; +import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.List; @@ -17,6 +18,7 @@ public record GameStateDataTransferObject( Team winner, Team currentTurn, + Role currentPhase, int currentRedFound, int currentBlueFound, String currentClue, From 55a62926093a024aa6105acef6f146c32c2146e2 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 21:29:23 +0200 Subject: [PATCH 114/207] feat: add websocket endpoint for passing turns --- .../backend/game/controller/GameSocketController.java | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index 06da4d16..e1744b1c 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -2,6 +2,7 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.game.dto.ClueMessage; +import com.codenames.codenames.backend.game.dto.PassTurnMessage; import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.playingfield.GameService; @@ -85,4 +86,14 @@ public void submitClue(ClueMessage message) { GAME_TOPIC_PREFIX + message.getLobbyCode(), gameService.createGameStateDto(message.getLobbyCode())); } + + @MessageMapping("/pass-turn") + public void passTurn(PassTurnMessage message) { + + gameService.passTurn(message.getLobbyCode(), message.getCurrentTurn()); + + messagingTemplate.convertAndSend( + GAME_TOPIC_PREFIX + message.getLobbyCode(), + gameService.createGameStateDto(message.getLobbyCode())); + } } From 03d821a2f9112e1cb1d4f1deac5cef06064d74f9 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sat, 16 May 2026 21:30:34 +0200 Subject: [PATCH 115/207] refactor: move advance turn method into gameManager --- .../codenames/codenames/backend/playingfield/GameManager.java | 1 + .../codenames/codenames/backend/playingfield/GameService.java | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index a673b491..b435b97d 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -173,6 +173,7 @@ public void submitClue(Clue clue, Team callingTeam) { if (clueValidationService.validateWord(this.board, clue.word())) { this.currentClue = clue; this.remainingGuesses = clue.guessAmount(); + advanceTurn(); } else { throw new IllegalArgumentException("Clue is invalid, cannot be a word that is on the board!"); } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index fe190a1a..4ab3e7da 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -70,7 +70,6 @@ private GameManager getGame(String lobbyCode) { public void submitClue(String lobbyCode, Clue clue, Team callingTeam) { GameManager gm = getGame(lobbyCode); gm.submitClue(clue, callingTeam); - gm.advanceTurn(); } /** From bd4365cc2ce2e2c415d5cc5f84dd7c38fbb33d5d Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 21:32:36 +0200 Subject: [PATCH 116/207] test: add coverage for websocket pass turn endpoint --- .../controller/GameSocketControllerTest.java | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 0b3842b9..79bb6b22 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -8,6 +8,7 @@ import com.codenames.codenames.backend.game.dto.ClueMessage; import com.codenames.codenames.backend.game.dto.GameStateDto; +import com.codenames.codenames.backend.game.dto.PassTurnMessage; import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.playingfield.GameService; @@ -85,4 +86,21 @@ void submitClueShouldBroadcastState() { verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } + + @Test + void passTurnShouldBroadcastUpdatedState() { + + PassTurnMessage message = new PassTurnMessage(); + + message.setLobbyCode("ABCDE"); + message.setCurrentTurn(Team.RED); + + when(gameService.createGameStateDto("ABCDE")).thenReturn(mock(GameStateDto.class)); + + controller.passTurn(message); + + verify(gameService).passTurn("ABCDE", Team.RED); + + verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); + } } From 78737b538b075f987926ec4f3027191923f4405a Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sat, 16 May 2026 21:34:07 +0200 Subject: [PATCH 117/207] docs: add Javadocs --- .../backend/game/controller/GameSocketController.java | 5 +++++ .../codenames/backend/game/dto/PassTurnMessage.java | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index e1744b1c..eeac7aad 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -87,6 +87,11 @@ public void submitClue(ClueMessage message) { gameService.createGameStateDto(message.getLobbyCode())); } + /** + * Ends the current turn early and broadcasts the updated game state. + * + * @param message contains lobby and team information + */ @MessageMapping("/pass-turn") public void passTurn(PassTurnMessage message) { diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java b/src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java index ef2a9f73..10812d83 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java +++ b/src/main/java/com/codenames/codenames/backend/game/dto/PassTurnMessage.java @@ -4,6 +4,11 @@ import lombok.Getter; import lombok.Setter; +/** + * Message used for requesting an early turn pass. + * + *

Contains the lobby code and team initiating the action. + */ @Getter @Setter public class PassTurnMessage { From 6c21e7f01af6a904cdd4108dec7d3b465ed2a0c6 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sat, 16 May 2026 21:35:04 +0200 Subject: [PATCH 118/207] fix: update test to take current phase parameter --- .../serialization/DataTransferObjectServiceTest.java | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 57e5003a..eaade5ff 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import com.codenames.codenames.backend.playingfield.Card; @@ -21,6 +22,8 @@ class DataTransferObjectServiceTest { DataTransferObjectService service; GameStateDataTransferObject gameStateDto; private static final Team redTeam = Team.RED; + private static final Role spymaster = Role.SPYMASTER; + private static final Role operative = Role.OPERATIVE; @BeforeEach void setUp() { @@ -36,13 +39,13 @@ void setUp() { when(mockGameManager.getCurrentBlueFound()).thenReturn(0); gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.OPERATIVE, redTeam); + service.createGameStateDataTransferObject(mockGameManager, operative, redTeam, spymaster); } @Test void testSpymasterVisibility() { gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.SPYMASTER, redTeam); + service.createGameStateDataTransferObject(mockGameManager, spymaster, redTeam, spymaster); assertEquals("RED", gameStateDto.cardList().get(0).color()); } @@ -65,7 +68,7 @@ void testGetWinner_exists() { void testGetWinner_null() { when(mockGameManager.getWinner()).thenReturn(null); gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.OPERATIVE, redTeam); + service.createGameStateDataTransferObject(mockGameManager, operative, redTeam, spymaster); assertNull(gameStateDto.winner()); } } From 6e5f469651495f6ed7ed5e613accc2ac1ec7c953 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sat, 16 May 2026 22:01:16 +0200 Subject: [PATCH 119/207] fix: add current phase to the expected value --- .../backend/serialization/SerializationJsonTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index 286ea635..8112bb94 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -8,6 +8,7 @@ import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; @@ -22,6 +23,8 @@ class SerializationJsonTest { List dummyList; GameStateDataTransferObject dummyGameState; ObjectMapper mapper = new ObjectMapper(); + private static final Team redTeam = Team.RED; + private static final Role spymaster = Role.SPYMASTER; @BeforeEach void setUp() { @@ -30,15 +33,16 @@ void setUp() { dummyList = List.of(new CardDataTransferObject("TEST", "HIDDEN", false)); dummyGameState = - new GameStateDataTransferObject(Team.RED, Team.RED, 0, 0, "Test", 1, dummyList); + new GameStateDataTransferObject(redTeam, redTeam, spymaster, 0, 0, "Test", 1, dummyList); } @Test void testSerialize_pass() { String expectedResult = - "{\"winner\":\"RED\",\"currentTurn\":\"RED\",\"currentRedFound\":0,\"currentBlueFound\":0" - + ",\"currentClue\":\"Test\",\"remainingGuesses\":1,\"cardList\":[{\"word\":\"TEST\"," - + "\"color\":\"HIDDEN\",\"isGuessed\":false}]}"; + "{\"winner\":\"RED\",\"currentTurn\":\"RED\",\"currentPhase\":\"SPYMASTER\"," + + "\"currentRedFound\":0,\"currentBlueFound\":0,\"currentClue\":\"Test\"," + + "\"remainingGuesses\":1,\"cardList\":[{\"word\":\"TEST\",\"color\":\"HIDDEN\"," + + "\"isGuessed\":false}]}"; String result = serializer.serialize(dummyGameState); assertEquals(expectedResult, result); } From 8be964cae3c9a0dc563dba226c8c4b2d87472ef5 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sat, 16 May 2026 22:02:01 +0200 Subject: [PATCH 120/207] fix: change spymaster to operative in winning phase --- .../backend/serialization/DataTransferObjectServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index eaade5ff..3256091e 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -68,7 +68,7 @@ void testGetWinner_exists() { void testGetWinner_null() { when(mockGameManager.getWinner()).thenReturn(null); gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, operative, redTeam, spymaster); + service.createGameStateDataTransferObject(mockGameManager, operative, redTeam, operative); assertNull(gameStateDto.winner()); } } From c4272be60c42ae16497ab2fe69ac7446d3bc2476 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sat, 16 May 2026 22:02:37 +0200 Subject: [PATCH 121/207] fix: service test should no longer verify advance turn being called --- .../codenames/backend/playingfield/GameServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index 69b83080..da1af220 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -55,7 +55,6 @@ void testSubmitClue() { gameService.submitClue(lobbyCode, mockClue, redTeam); verify(mockGameManager, times(1)).submitClue(mockClue, redTeam); - verify(mockGameManager, times(1)).advanceTurn(); } @Test From ecfeb778a41e0f7e4fc40fa488c6a6bd6a36d520 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sat, 16 May 2026 22:11:53 +0200 Subject: [PATCH 122/207] fix: old tests were still assuming that submitClue did not advance turn --- .../backend/playingfield/GameManagerTest.java | 34 +++++++------------ 1 file changed, 12 insertions(+), 22 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 8bbc2125..01378fea 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -70,7 +70,7 @@ private GameManager helperMethodGenerateFullCardList(Color cardColor, Team start return fullListGameManager; } - private void helperMethodAdvanceTurns(int advanceAmount) { + private void helperMethodAdvanceTurns(GameManager gameManager, int advanceAmount) { for (int i = 0; i < advanceAmount; i++) { gameManager.advanceTurn(); } @@ -117,7 +117,6 @@ void testGetWinner_null() { void testGetWinner_redStartsRedWins() { gameManager = helperMethodGenerateFullCardList(redColor, redTeam); - helperMethodAdvanceTurns(1); for (int i = 0; i < STARTING_TEAM_CARDS; i++) { gameManager.flipCard(i, redTeam); } @@ -128,9 +127,8 @@ void testGetWinner_redStartsRedWins() { void testGetWinner_redStartsBlueWins() { gameManager = helperMethodGenerateFullCardList(blueColor, redTeam); - helperMethodAdvanceTurns(2); // blue spymaster + helperMethodAdvanceTurns(gameManager, 1); // blue spymaster helperMethodSubmitClue(gameManager, SECOND_TEAM_CARDS, blueTeam); - helperMethodAdvanceTurns(1); // blue operative for (int i = 0; i < SECOND_TEAM_CARDS; i++) { gameManager.flipCard(i, blueTeam); @@ -141,9 +139,8 @@ void testGetWinner_redStartsBlueWins() { @Test void testGetWinner_blueStartsRedWins() { gameManager = helperMethodGenerateFullCardList(redColor, blueTeam); - helperMethodAdvanceTurns(2); // red spymaster + helperMethodAdvanceTurns(gameManager, 1); // red spymaster helperMethodSubmitClue(gameManager, 8, redTeam); - helperMethodAdvanceTurns(1); // red operative for (int i = 0; i < SECOND_TEAM_CARDS; i++) { gameManager.flipCard(i, redTeam); @@ -155,7 +152,6 @@ void testGetWinner_blueStartsRedWins() { void testGetWinner_blueStartsBlueWins() { gameManager = helperMethodGenerateFullCardList(blueColor, blueTeam); - helperMethodAdvanceTurns(1); for (int i = 0; i < STARTING_TEAM_CARDS; i++) { gameManager.flipCard(i, blueTeam); } @@ -167,9 +163,8 @@ void testGetWinner_redFoundBlackCardFound() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); gameManager = new GameManager(blueTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, blueTeam); - helperMethodAdvanceTurns(2); // red spymaster + helperMethodAdvanceTurns(gameManager, 1); // red spymaster helperMethodSubmitClue(gameManager, 1, redTeam); - helperMethodAdvanceTurns(1); // red operative gameManager.flipCard(0, redTeam); assertEquals(blueTeam, gameManager.getWinner()); @@ -179,9 +174,8 @@ void testGetWinner_redFoundBlackCardFound() { void testGetWinner_blueFoundBlackCardFound() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); - helperMethodAdvanceTurns(2); // blue spymaster + helperMethodAdvanceTurns(gameManager, 2); // blue spymaster helperMethodSubmitClue(gameManager, 1, blueTeam); - helperMethodAdvanceTurns(1); // blue operative gameManager.flipCard(0, blueTeam); assertEquals(redTeam, gameManager.getWinner()); } @@ -191,7 +185,6 @@ void testFlipWhiteCard() { mockCardGeneration(List.of(new Card("Test", Color.WHITE))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, redTeam); - helperMethodAdvanceTurns(1); gameManager.flipCard(0, redTeam); assertNull(gameManager.getWinner()); } @@ -199,7 +192,6 @@ void testFlipWhiteCard() { @Test void testFlipCard_cardAlreadyFlipped() { helperMethodSubmitClue(gameManager, 1, redTeam); - helperMethodAdvanceTurns(1); gameManager.flipCard(0, redTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, redTeam)); } @@ -209,7 +201,6 @@ void testFlipCard_winnerAlreadyDetermined() { mockCardGeneration(List.of(new Card("Test", Color.BLACK))); gameManager = new GameManager(blueTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, blueTeam); - helperMethodAdvanceTurns(1); // red operative gameManager.flipCard(0, blueTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(0, blueTeam)); } @@ -238,7 +229,6 @@ void testOutOfGuesses() { mockCardGeneration(List.of(new Card("Test", redColor), new Card("Test2", redColor))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 0, redTeam); - helperMethodAdvanceTurns(1); gameManager.flipCard(0, redTeam); assertThrows(IllegalStateException.class, () -> gameManager.flipCard(1, redTeam)); } @@ -281,44 +271,44 @@ void testCorrectStart_spymaster() { @Test void testAdvanceTurn_spymasterToOperative() { - helperMethodAdvanceTurns(1); + helperMethodAdvanceTurns(gameManager, 1); assertEquals(Role.OPERATIVE, gameManager.getCurrentPhase()); } @Test void testAdvanceTurn_spymasterToOperative_sameTeam() { - helperMethodAdvanceTurns(1); + helperMethodAdvanceTurns(gameManager, 1); assertEquals(redTeam, gameManager.getCurrentTurn()); } @Test void testAdvanceTurnTwice_operativeToSpymaster() { - helperMethodAdvanceTurns(2); + helperMethodAdvanceTurns(gameManager, 2); assertEquals(Role.SPYMASTER, gameManager.getCurrentPhase()); } @Test void testAdvanceTurnTwice_redTeamToBlueTeam() { - helperMethodAdvanceTurns(2); + helperMethodAdvanceTurns(gameManager, 2); assertEquals(blueTeam, gameManager.getCurrentTurn()); } @Test void testAdvanceTurnTwice_wipeClue() { - helperMethodAdvanceTurns(2); + helperMethodAdvanceTurns(gameManager, 2); assertNull(gameManager.getCurrentClue()); } @Test void testPassTurn_correctTeam() { - helperMethodAdvanceTurns(1); + helperMethodAdvanceTurns(gameManager, 1); gameManager.passTurn(redTeam); assertEquals(blueTeam, gameManager.getCurrentTurn()); } @Test void testPassTurn_correctPhase() { - helperMethodAdvanceTurns(1); + helperMethodAdvanceTurns(gameManager, 1); gameManager.passTurn(redTeam); assertEquals(Role.SPYMASTER, gameManager.getCurrentPhase()); } From d2ad75bea9b39f920892943a43089c93ca1d2d88 Mon Sep 17 00:00:00 2001 From: XtophB Date: Sat, 16 May 2026 22:37:40 +0200 Subject: [PATCH 123/207] refactor: remove accidentally added import --- .../backend/serialization/DataTransferObjectServiceTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 3256091e..4abb1a26 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -3,7 +3,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; import static org.mockito.Mockito.when; import com.codenames.codenames.backend.playingfield.Card; From 8537f2ca6be918219f3b23bd2820937e757521b9 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 17 May 2026 13:40:04 +0200 Subject: [PATCH 124/207] test: add test for health endpoint --- .../controller/HealthControllerTest.java | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 src/test/java/com/codenames/codenames/backend/controller/HealthControllerTest.java diff --git a/src/test/java/com/codenames/codenames/backend/controller/HealthControllerTest.java b/src/test/java/com/codenames/codenames/backend/controller/HealthControllerTest.java new file mode 100644 index 00000000..b0d093c9 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/controller/HealthControllerTest.java @@ -0,0 +1,24 @@ +package com.codenames.codenames.backend.controller; + +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; +import org.springframework.test.web.servlet.MockMvc; + +@WebMvcTest(HealthController.class) +class HealthControllerTest { + + @Autowired private MockMvc mockMvc; + + @Test + void healthShouldReturnStatusUp() throws Exception { + mockMvc + .perform(get("/health")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.status").value("UP")); + } +} From 5194f41319ba67e8e8306a43480333bcd3f21490 Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Sun, 17 May 2026 13:47:53 +0200 Subject: [PATCH 125/207] add game start endpoint to lobby service --- .../lobby/controller/LobbyController.java | 24 +++++++++++++++++++ .../backend/lobby/dto/GameStartResponse.java | 15 ++++++++++++ .../backend/lobby/services/LobbyService.java | 21 +++++++++++++++- 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/codenames/codenames/backend/lobby/dto/GameStartResponse.java diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 68eb9f78..0bbc9d17 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.lobby.controller; +import com.codenames.codenames.backend.lobby.dto.GameStartResponse; import com.codenames.codenames.backend.lobby.dto.LobbyResponse; import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyService; @@ -163,4 +164,27 @@ public ResponseEntity selectPosition( ); } } + + @GetMapping("/{lobbyCode}/start-game") + public ResponseEntity startGame( + @PathVariable String lobbyCode, @RequestParam String username + ) { + boolean isStarted = service.startGame(lobbyCode, username); + + if(isStarted) return ResponseEntity.ok( + new GameStartResponse( + "Game is starting now.", + lobbyCode, + service.getPlayersDto(lobbyCode), + true + ) + ); + return ResponseEntity.badRequest().body( + new GameStartResponse("Could not start the game.", + username, + service.getPlayersDto(lobbyCode), + false + ) + ); + } } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/dto/GameStartResponse.java b/src/main/java/com/codenames/codenames/backend/lobby/dto/GameStartResponse.java new file mode 100644 index 00000000..8c827889 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/lobby/dto/GameStartResponse.java @@ -0,0 +1,15 @@ +package com.codenames.codenames.backend.lobby.dto; + +import java.util.List; + +/** + * This response is transferred, when the host of a lobby requests a game start. + * It is used as a broadcast message type which all players receive simultaneously. + * + * @param message the message that is displayed + * @param lobbyCode the lobbyCode of the starting lobby + * @param playerList list of all players including their roles + * @param isGameStarted if the game was started successfully + */ +public record GameStartResponse(String message, String lobbyCode, List playerList, boolean isGameStarted) { +} diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 17a18199..8972139a 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -9,6 +9,7 @@ import com.codenames.codenames.backend.websocket.Player; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -60,7 +61,6 @@ public String createLobby(String username) { Lobby lobby = new Lobby(lobbyCode, username); lobbyList.put(lobbyCode, lobby); - addGameManagerForLobby(lobby, lobbyCode); log.info("{}: a lobby has been created", lobbyCode); return lobbyCode; } @@ -256,4 +256,23 @@ public Role getPlayerRole(String username, String lobbyCode) { } return null; } + + public boolean startGame(String lobbyCode, String username) { + boolean isStarted = !lobbyCode.isBlank() && !username.isBlank() && Objects.equals(getHost(lobbyCode), username); + Lobby lobby = lobbyList.get(lobbyCode); + addGameManagerForLobby(lobby, lobbyCode); + + log.info("{}: Game start requested", lobbyCode); + return isStarted; + } + + private String getHost(String lobbyCode) { + List players = getPlayers(lobbyCode); + for(Player p : players) { + if(p.isHost()) { + return p.username(); + } + } + return ""; + } } From dd6f74db16f5b499553ae6e28001ce84c01bbf16 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 17 May 2026 14:53:37 +0200 Subject: [PATCH 126/207] refactor: keep lobby players on websocket disconnect --- .../websocket/WebSocketEventListener.java | 23 ++----------------- 1 file changed, 2 insertions(+), 21 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java b/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java index 496d6135..35ad6f4e 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java @@ -1,9 +1,6 @@ package com.codenames.codenames.backend.websocket; -import com.codenames.codenames.backend.lobby.services.LobbyService; -import java.util.List; import org.springframework.context.event.EventListener; -import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Component; import org.springframework.web.socket.messaging.SessionDisconnectEvent; @@ -16,8 +13,6 @@ @Component public class WebSocketEventListener { private final SessionRegistry sessionRegistry; - private final LobbyService lobbyService; - private final SimpMessagingTemplate messagingTemplate; /** * Creates a new {@code WebSocketEventListener}. @@ -26,13 +21,8 @@ public class WebSocketEventListener { * @param lobbyService the service handling lobby operations * @param messagingTemplate the messaging template used for broadcasting updates */ - public WebSocketEventListener( - SessionRegistry sessionRegistry, - LobbyService lobbyService, - SimpMessagingTemplate messagingTemplate) { + public WebSocketEventListener(SessionRegistry sessionRegistry) { this.sessionRegistry = sessionRegistry; - this.lobbyService = lobbyService; - this.messagingTemplate = messagingTemplate; } /** @@ -48,19 +38,10 @@ public void handleDisconnect(SessionDisconnectEvent event) { String sessionId = event.getSessionId(); - String username = sessionRegistry.getUser(sessionId); - String lobbyCode = sessionRegistry.getLobby(sessionId); - - if (username == null || lobbyCode == null) { + if (sessionRegistry.getUser(sessionId) == null || sessionRegistry.getLobby(sessionId) == null) { return; } - lobbyService.leaveLobby(username, lobbyCode); sessionRegistry.remove(sessionId); - - List players = - lobbyService.getPlayers(lobbyCode).stream().map(Player::username).toList(); - - messagingTemplate.convertAndSend("/topic/lobby/" + lobbyCode, players); } } From ad4be462766db092347df0e28721707cb9f97298 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 17 May 2026 14:59:27 +0200 Subject: [PATCH 127/207] feat: send current game state on websocket join reconnect --- .../backend/websocket/GameController.java | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java index d2b8c535..39793ef3 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.websocket; import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.playingfield.GameService; import java.util.List; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; @@ -17,6 +18,7 @@ public class GameController { private final LobbyService lobbyService; + private final GameService gameService; private final SimpMessagingTemplate messagingTemplate; private final SessionRegistry sessionRegistry; @@ -24,14 +26,17 @@ public class GameController { * Creates a new {@code GameController}. * * @param lobbyService the service handling lobby operations + * @param gameService the service handling game state retrieval * @param messagingTemplate the messaging template used for broadcasting updates * @param sessionRegistry the registry managing WebSocket sessions */ public GameController( LobbyService lobbyService, + GameService gameService, SimpMessagingTemplate messagingTemplate, SessionRegistry sessionRegistry) { this.lobbyService = lobbyService; + this.gameService = gameService; this.messagingTemplate = messagingTemplate; this.sessionRegistry = sessionRegistry; } @@ -59,8 +64,11 @@ public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) } boolean joined = lobbyService.joinLobby(message.getName(), message.getCode()); + boolean reconnect = + lobbyService.getPlayers(message.getCode()).stream() + .anyMatch(player -> player.username().equals(message.getName())); - if (!joined) { + if (!joined && !reconnect) { messagingTemplate.convertAndSend("/topic/errors/" + sessionId, "Join failed"); return; } @@ -68,6 +76,7 @@ public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) sessionRegistry.register(sessionId, message.getName(), message.getCode()); sendPlayerUpdate(message.getCode()); + sendGameStateUpdate(message.getCode()); } /** @@ -80,4 +89,13 @@ private void sendPlayerUpdate(String code) { messagingTemplate.convertAndSend("/topic/lobby/" + code, players); } + + /** + * Sends the current game state to all clients subscribed to the lobby game topic. + * + * @param code the lobby code identifying the game + */ + private void sendGameStateUpdate(String code) { + messagingTemplate.convertAndSend("/topic/game/" + code, gameService.createGameStateDto(code)); + } } From 5d7d21dae862ab56abeaa1ba73d4fc961bc10485 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 17 May 2026 15:03:21 +0200 Subject: [PATCH 128/207] test: update websocket disconnect listener tests for reconnect behavior --- .../backend/websocket/GameControllerTest.java | 39 ++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java index a9ffcca8..1d52bc07 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java @@ -10,6 +10,7 @@ import static org.mockito.Mockito.when; import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.playingfield.GameService; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -24,6 +25,7 @@ class GameControllerTest { private LobbyService lobbyService; + private GameService gameService; private SessionRegistry sessionRegistry; private GameController controller; private SimpMessagingTemplate messagingTemplate; @@ -31,10 +33,11 @@ class GameControllerTest { @BeforeEach void setup() { lobbyService = mock(LobbyService.class); + gameService = mock(GameService.class); messagingTemplate = mock(SimpMessagingTemplate.class); sessionRegistry = new SessionRegistry(); - controller = new GameController(lobbyService, messagingTemplate, sessionRegistry); + controller = new GameController(lobbyService, gameService, messagingTemplate, sessionRegistry); } @Test @@ -54,6 +57,8 @@ void shouldRegisterJoinAndRegisterSession() { when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(true); when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); + when(gameService.createGameStateDto("ABCDE")) + .thenReturn(mock(com.codenames.codenames.backend.game.dto.GameStateDto.class)); controller.join(msg, accessor); @@ -63,6 +68,7 @@ void shouldRegisterJoinAndRegisterSession() { assertEquals("ABCDE", sessionRegistry.getLobby("123")); verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); + verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } @Test @@ -79,6 +85,7 @@ void shouldSendErrorMessageWhenJoinFails() { accessor.setSessionAttributes(attrs); when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(false); + when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of()); controller.join(msg, accessor); @@ -117,6 +124,8 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(true); when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); + when(gameService.createGameStateDto("ABCDE")) + .thenReturn(mock(com.codenames.codenames.backend.game.dto.GameStateDto.class)); controller.join(msg, accessor); @@ -125,5 +134,33 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { verify(lobbyService).joinLobby("Max", "ABCDE"); verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); + verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); + } + + @Test + void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { + + JoinMessage msg = new JoinMessage(); + msg.setName("Max"); + msg.setCode("ABCDE"); + + SimpMessageHeaderAccessor accessor = SimpMessageHeaderAccessor.create(); + + java.util.Map attrs = new java.util.HashMap<>(); + attrs.put("sessionId", "reconnect-1"); + accessor.setSessionAttributes(attrs); + + when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(false); + when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); + when(gameService.createGameStateDto("ABCDE")) + .thenReturn(mock(com.codenames.codenames.backend.game.dto.GameStateDto.class)); + + controller.join(msg, accessor); + + assertEquals("Max", sessionRegistry.getUser("reconnect-1")); + assertEquals("ABCDE", sessionRegistry.getLobby("reconnect-1")); + + verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); + verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } } From 95eed7dec824f24f29e41dd959d1c93381bec19d Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 17 May 2026 15:05:06 +0200 Subject: [PATCH 129/207] test: update websocket disconnect listener tests for reconnect behavior --- .../websocket/WebSocketEventListenerTest.java | 59 +++++++++++-------- 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java index fcb45290..cd94ba01 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java @@ -1,59 +1,47 @@ package com.codenames.codenames.backend.websocket; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.lobby.services.LobbyService; +import java.lang.reflect.Field; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.web.socket.messaging.SessionDisconnectEvent; /** Unit tests for {@link WebSocketEventListener}. */ class WebSocketEventListenerTest { private SessionRegistry registry; - private LobbyService lobbyService; - private SimpMessagingTemplate messagingTemplate; private WebSocketEventListener listener; @BeforeEach void setup() { registry = new SessionRegistry(); - lobbyService = mock(LobbyService.class); - messagingTemplate = mock(SimpMessagingTemplate.class); - - listener = new WebSocketEventListener(registry, lobbyService, messagingTemplate); + listener = new WebSocketEventListener(registry); } @Test - void shouldHandleDisconnectAndRemovePlayer() { + void shouldHandleDisconnectAndRemoveSessionMapping() { registry.register("123", "Max", "ABCDE"); - SessionDisconnectEvent event = mock(SessionDisconnectEvent.class); + SessionDisconnectEvent event = org.mockito.Mockito.mock(SessionDisconnectEvent.class); when(event.getSessionId()).thenReturn("123"); - when(lobbyService.getPlayers("ABCDE")).thenReturn(java.util.List.of()); - listener.handleDisconnect(event); - verify(lobbyService).leaveLobby("Max", "ABCDE"); assertNull(registry.getUser("123")); + assertNull(registry.getLobby("123")); } @Test void shouldIgnoreDisconnectWhenUsernameIsNull() { - SessionDisconnectEvent event = mock(SessionDisconnectEvent.class); + SessionDisconnectEvent event = org.mockito.Mockito.mock(SessionDisconnectEvent.class); when(event.getSessionId()).thenReturn("123"); listener.handleDisconnect(event); - - verifyNoInteractions(lobbyService); - verifyNoInteractions(messagingTemplate); } @Test @@ -62,23 +50,42 @@ void shouldIgnoreDisconnectWhenLobbyIsMissing() { registry.register("123", "Max", "ABCDE"); registry.remove("123"); - SessionDisconnectEvent event = mock(SessionDisconnectEvent.class); + SessionDisconnectEvent event = org.mockito.Mockito.mock(SessionDisconnectEvent.class); when(event.getSessionId()).thenReturn("123"); listener.handleDisconnect(event); - - verifyNoInteractions(lobbyService); - verifyNoInteractions(messagingTemplate); } @Test void shouldIgnoreUnknownSession() { - SessionDisconnectEvent event = mock(SessionDisconnectEvent.class); + SessionDisconnectEvent event = org.mockito.Mockito.mock(SessionDisconnectEvent.class); when(event.getSessionId()).thenReturn("unknown"); listener.handleDisconnect(event); + } + + @Test + void shouldIgnoreDisconnectWhenLobbyIsNullButUserExists() throws Exception { + + registry.register("123", "Max", "ABCDE"); + removeLobbyMappingOnly("123"); + + SessionDisconnectEvent event = org.mockito.Mockito.mock(SessionDisconnectEvent.class); + when(event.getSessionId()).thenReturn("123"); + + listener.handleDisconnect(event); + + assertEquals("Max", registry.getUser("123")); + assertNull(registry.getLobby("123")); + } + + @SuppressWarnings("unchecked") + private void removeLobbyMappingOnly(String sessionId) throws Exception { + Field lobbyField = SessionRegistry.class.getDeclaredField("sessionToLobby"); + lobbyField.setAccessible(true); - verifyNoInteractions(lobbyService); + Map sessionToLobby = (Map) lobbyField.get(registry); + sessionToLobby.remove(sessionId); } } From a31e654c094cf4cb89e36eee2e12ec05d710661a Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 17 May 2026 15:05:59 +0200 Subject: [PATCH 130/207] docs: modify javadocs --- .../backend/websocket/WebSocketEventListener.java | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java b/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java index 35ad6f4e..3e9be843 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java @@ -7,8 +7,7 @@ /** * Listener for WebSocket lifecycle events. * - *

Handles client disconnections by removing playerList from lobbies, cleaning up session mappings, - * and notifying remaining clients. + *

Handles client disconnections by cleaning up session mappings. */ @Component public class WebSocketEventListener { @@ -18,8 +17,6 @@ public class WebSocketEventListener { * Creates a new {@code WebSocketEventListener}. * * @param sessionRegistry the registry managing WebSocket sessions - * @param lobbyService the service handling lobby operations - * @param messagingTemplate the messaging template used for broadcasting updates */ public WebSocketEventListener(SessionRegistry sessionRegistry) { this.sessionRegistry = sessionRegistry; @@ -28,8 +25,8 @@ public WebSocketEventListener(SessionRegistry sessionRegistry) { /** * Handles a WebSocket disconnect event. * - *

Removes the disconnected player from the lobby, cleans up session data, and broadcasts the - * updated player list. + *

Removes transient WebSocket session mappings while preserving lobby membership for + * reconnect. * * @param event the disconnect event containing session information */ From 1ffc13747d839465c1ceea739d9f9eabaccd26e0 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Sun, 17 May 2026 15:10:22 +0200 Subject: [PATCH 131/207] style: remove constant parameter warning in websocket listener test --- .../websocket/WebSocketEventListenerTest.java | 31 ++++++++++--------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java index cd94ba01..30c61d96 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java @@ -12,6 +12,7 @@ /** Unit tests for {@link WebSocketEventListener}. */ class WebSocketEventListenerTest { + private static final String TEST_SESSION_ID = "123"; private SessionRegistry registry; private WebSocketEventListener listener; @@ -24,22 +25,22 @@ void setup() { @Test void shouldHandleDisconnectAndRemoveSessionMapping() { - registry.register("123", "Max", "ABCDE"); + registry.register(TEST_SESSION_ID, "Max", "ABCDE"); SessionDisconnectEvent event = org.mockito.Mockito.mock(SessionDisconnectEvent.class); - when(event.getSessionId()).thenReturn("123"); + when(event.getSessionId()).thenReturn(TEST_SESSION_ID); listener.handleDisconnect(event); - assertNull(registry.getUser("123")); - assertNull(registry.getLobby("123")); + assertNull(registry.getUser(TEST_SESSION_ID)); + assertNull(registry.getLobby(TEST_SESSION_ID)); } @Test void shouldIgnoreDisconnectWhenUsernameIsNull() { SessionDisconnectEvent event = org.mockito.Mockito.mock(SessionDisconnectEvent.class); - when(event.getSessionId()).thenReturn("123"); + when(event.getSessionId()).thenReturn(TEST_SESSION_ID); listener.handleDisconnect(event); } @@ -47,11 +48,11 @@ void shouldIgnoreDisconnectWhenUsernameIsNull() { @Test void shouldIgnoreDisconnectWhenLobbyIsMissing() { - registry.register("123", "Max", "ABCDE"); - registry.remove("123"); + registry.register(TEST_SESSION_ID, "Max", "ABCDE"); + registry.remove(TEST_SESSION_ID); SessionDisconnectEvent event = org.mockito.Mockito.mock(SessionDisconnectEvent.class); - when(event.getSessionId()).thenReturn("123"); + when(event.getSessionId()).thenReturn(TEST_SESSION_ID); listener.handleDisconnect(event); } @@ -68,24 +69,24 @@ void shouldIgnoreUnknownSession() { @Test void shouldIgnoreDisconnectWhenLobbyIsNullButUserExists() throws Exception { - registry.register("123", "Max", "ABCDE"); - removeLobbyMappingOnly("123"); + registry.register(TEST_SESSION_ID, "Max", "ABCDE"); + removeLobbyMappingForTestSession(); SessionDisconnectEvent event = org.mockito.Mockito.mock(SessionDisconnectEvent.class); - when(event.getSessionId()).thenReturn("123"); + when(event.getSessionId()).thenReturn(TEST_SESSION_ID); listener.handleDisconnect(event); - assertEquals("Max", registry.getUser("123")); - assertNull(registry.getLobby("123")); + assertEquals("Max", registry.getUser(TEST_SESSION_ID)); + assertNull(registry.getLobby(TEST_SESSION_ID)); } @SuppressWarnings("unchecked") - private void removeLobbyMappingOnly(String sessionId) throws Exception { + private void removeLobbyMappingForTestSession() throws Exception { Field lobbyField = SessionRegistry.class.getDeclaredField("sessionToLobby"); lobbyField.setAccessible(true); Map sessionToLobby = (Map) lobbyField.get(registry); - sessionToLobby.remove(sessionId); + sessionToLobby.remove(TEST_SESSION_ID); } } From d250720f9da6fa9a426f0fa2c25bf5063f95e80c Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Sun, 17 May 2026 18:33:05 +0200 Subject: [PATCH 132/207] websocket now returns a game state data transfer object, added some helper methods -changed card color from black and white to neutral and assassin to fit the frontend - changed the type of color from string to color in the card dto - simplified the mapping method for card dtos --- .../game/controller/GameSocketController.java | 8 ++-- .../lobby/controller/LobbyController.java | 29 +++++++++------ .../backend/lobby/dto/LobbyResponse.java | 2 +- .../backend/lobby/services/LobbyService.java | 6 ++- .../backend/playingfield/CardGenerator.java | 4 +- .../backend/playingfield/GameManager.java | 2 +- .../backend/playingfield/GameService.java | 37 ++++++++++++++++++- .../serialization/CardDataTransferObject.java | 4 +- .../DataTransferObjectService.java | 26 +++---------- .../codenames/backend/utility/Color.java | 4 +- .../backend/playingfield/BoardTest.java | 8 ++-- .../playingfield/CardGeneratorTest.java | 4 +- .../backend/playingfield/CardTest.java | 2 +- .../backend/playingfield/GameManagerTest.java | 8 ++-- .../backend/playingfield/GameServiceTest.java | 5 ++- .../DataTransferObjectServiceTest.java | 7 ++-- .../serialization/SerializationJsonTest.java | 2 +- 17 files changed, 96 insertions(+), 62 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index eeac7aad..b9ec4969 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -47,7 +47,7 @@ public void startGame(StartGameMessage message) { messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), - gameService.createGameStateDto(message.getLobbyCode())); + gameService.getCurrentGameState(message.getLobbyCode())); } /** @@ -64,7 +64,7 @@ public void revealCard(RevealCardMessage message) { messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), - gameService.createGameStateDto(message.getLobbyCode())); + gameService.getCurrentGameState(message.getLobbyCode())); } /** @@ -84,7 +84,7 @@ public void submitClue(ClueMessage message) { messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), - gameService.createGameStateDto(message.getLobbyCode())); + gameService.getCurrentGameState(message.getLobbyCode())); } /** @@ -99,6 +99,6 @@ public void passTurn(PassTurnMessage message) { messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), - gameService.createGameStateDto(message.getLobbyCode())); + gameService.getCurrentGameState(message.getLobbyCode())); } } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 0bbc9d17..25d6eb00 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -49,11 +49,11 @@ public ResponseEntity createLobby(@RequestParam String username) String lobbyCode = service.createLobby(username); if (lobbyCode == null || lobbyCode.isBlank()) { return ResponseEntity.internalServerError() - .body(new LobbyResponse("Error while creating lobby.", "", null)); + .body(new LobbyResponse("Error while creating lobby.", "", null, false)); } else { List players = service.getPlayersDto(lobbyCode); return ResponseEntity.ok( - new LobbyResponse("Successfully created Lobby.", lobbyCode, players) + new LobbyResponse("Successfully created Lobby.", lobbyCode, players, false) ); } } @@ -74,12 +74,13 @@ public ResponseEntity joinLobby( new LobbyResponse( "Joined Lobby successfully.", lobbyCode, - service.getPlayersDto(lobbyCode) + service.getPlayersDto(lobbyCode), + false ) ); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); } } @@ -100,14 +101,15 @@ public ResponseEntity leaveLobby( new LobbyResponse( "Left lobby successfully.", lobbyCode, - service.getPlayersDto(lobbyCode) + service.getPlayersDto(lobbyCode), + false ) ); service.checkLobbyStillHasPlayers(lobbyCode); return response; } else { return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); } } @@ -124,8 +126,9 @@ public ResponseEntity getLobbyInfo( @PathVariable String lobbyCode ) { List players = service.getPlayersDto(lobbyCode); + boolean isStarted = service.getIsStarted(lobbyCode); return ResponseEntity.ok( - new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players) + new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players, isStarted) ); } @@ -151,7 +154,8 @@ public ResponseEntity selectPosition( new LobbyResponse( "Position selected successfully.", lobbyCode, - service.getPlayersDto(lobbyCode) + service.getPlayersDto(lobbyCode), + false ) ); } else { @@ -159,20 +163,21 @@ public ResponseEntity selectPosition( new LobbyResponse( "Could not assign selected team/role.", lobbyCode, - service.getPlayersDto(lobbyCode) + service.getPlayersDto(lobbyCode), + false ) ); } } @GetMapping("/{lobbyCode}/start-game") - public ResponseEntity startGame( + public ResponseEntity startGame( @PathVariable String lobbyCode, @RequestParam String username ) { boolean isStarted = service.startGame(lobbyCode, username); if(isStarted) return ResponseEntity.ok( - new GameStartResponse( + new LobbyResponse( "Game is starting now.", lobbyCode, service.getPlayersDto(lobbyCode), @@ -180,7 +185,7 @@ public ResponseEntity startGame( ) ); return ResponseEntity.badRequest().body( - new GameStartResponse("Could not start the game.", + new LobbyResponse("Could not start the game.", username, service.getPlayersDto(lobbyCode), false diff --git a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java b/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java index 02269fe5..858c1547 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java @@ -7,5 +7,5 @@ * *

Contains a message describing the outcome and the associated lobby code. */ -public record LobbyResponse(String message, String lobbyCode, List playerList) { +public record LobbyResponse(String message, String lobbyCode, List playerList, boolean isStarted) { } diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index f676840b..783cc43d 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -260,7 +260,7 @@ public boolean startGame(String lobbyCode, String username) { Lobby lobby = lobbyList.get(lobbyCode); addGameManagerForLobby(lobby, lobbyCode); - log.info("{}: Game start requested", lobbyCode); + log.info("{}: Game start requested, returning: {}", lobbyCode, isStarted); return isStarted; } @@ -273,4 +273,8 @@ private String getHost(String lobbyCode) { } return ""; } + + public boolean getIsStarted(String lobbyCode) { + return gameService.isGameStarted(lobbyCode); + } } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/CardGenerator.java b/src/main/java/com/codenames/codenames/backend/playingfield/CardGenerator.java index f9a88451..af2d1d2b 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/CardGenerator.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/CardGenerator.java @@ -72,8 +72,8 @@ public List generateCards(int totalWords, int red, int blue, int white, in List colorList = new ArrayList<>(); addColorToList(colorList, red, Color.RED); addColorToList(colorList, blue, Color.BLUE); - addColorToList(colorList, white, Color.WHITE); - addColorToList(colorList, black, Color.BLACK); + addColorToList(colorList, white, Color.NEUTRAL); + addColorToList(colorList, black, Color.ASSASSIN); List cardList = new ArrayList<>(); List wordList = pickWords(totalWords); diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index a673b491..30f47e25 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -96,7 +96,7 @@ private void updateScore(Color cardColor) { case BLUE: currentBlueFound++; break; - case BLACK: + case ASSASSIN: if (this.currentTurn == Team.RED) { this.winner = Team.BLUE; } else { diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index ae180344..4d8f1ea2 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -2,7 +2,13 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.game.dto.GameStateDto; +import com.codenames.codenames.backend.serialization.CardDataTransferObject; +import com.codenames.codenames.backend.serialization.DataTransferObjectService; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.utility.Team; + +import java.util.ArrayList; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Service; @@ -17,14 +23,16 @@ public class GameService { private final Map games = new ConcurrentHashMap<>(); private final GameManagerFactory gameManagerFactory; + private final DataTransferObjectService dtoService; /** * Constructor for a GameService object. * * @param gameManagerFactory the factory responsible for generating GameManagers */ - public GameService(GameManagerFactory gameManagerFactory) { + public GameService(GameManagerFactory gameManagerFactory, DataTransferObjectService dtoService) { this.gameManagerFactory = gameManagerFactory; + this.dtoService = dtoService; } /** @@ -124,4 +132,31 @@ public GameStateDto createGameStateDto(String lobbyCode) { gm.getCurrentTurn(), gm.getCurrentPhase()); } + + /** + * Maps current game State into a @link GameStateTransferObject. + * + * @param lobbyCode + * @return + */ + public GameStateDataTransferObject getCurrentGameState(String lobbyCode) { + GameManager gm = getGame(lobbyCode); + return dtoService.createGameStateDataTransferObject(gm, gm.getCurrentTurn()); + } + + /** + * This method uses the private method getGame to check if a game is already started + * via the existence of a game manager. + * + * @param lobbyCode the lobbyCode of the lobby + * @return if the game manager for the lobby already exists aka the game is started + */ + public boolean isGameStarted(String lobbyCode) { + try { + getGame(lobbyCode); + return true; + } catch (Exception e) { + return false; + } + } } diff --git a/src/main/java/com/codenames/codenames/backend/serialization/CardDataTransferObject.java b/src/main/java/com/codenames/codenames/backend/serialization/CardDataTransferObject.java index 8174a249..e367571e 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/CardDataTransferObject.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/CardDataTransferObject.java @@ -1,5 +1,7 @@ package com.codenames.codenames.backend.serialization; +import com.codenames.codenames.backend.utility.Color; + /** * Represents the state of a single card, for JSON serialization. * @@ -7,4 +9,4 @@ * @param color the color of the card (could also be "hidden") * @param isGuessed the guess state of the card */ -public record CardDataTransferObject(String word, String color, boolean isGuessed) {} +public record CardDataTransferObject(String word, Color color, boolean isGuessed) {} diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java index df3e594a..6cee24e7 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java @@ -2,7 +2,7 @@ import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.playingfield.GameManager; -import com.codenames.codenames.backend.utility.Role; +import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Team; import java.util.ArrayList; import java.util.List; @@ -16,17 +16,10 @@ public class DataTransferObjectService { * Helper method to create a card DTO with the correct visibility based on role and guess state. * * @param card card object from the board - * @param role role of the player, which determines the visibility of the card's color * @return the card DTO for the game state DTO */ - private CardDataTransferObject createCardDataTransferObject(Card card, Role role) { - String displayColor; - - if (role == Role.SPYMASTER || card.isGuessed()) { - displayColor = card.getColor().toString(); - } else { - displayColor = "HIDDEN"; - } + private CardDataTransferObject createCardDataTransferObject(Card card) { + Color displayColor = card.getColor(); return new CardDataTransferObject(card.getWord(), displayColor, card.isGuessed()); } @@ -34,26 +27,19 @@ private CardDataTransferObject createCardDataTransferObject(Card card, Role role * Creates the game state DTO that needs to be serialized into JSON. * * @param gameManager the game manager that holds the state of the game - * @param role the role of the player who requires the DTO * @param currentTurn the current turn * @return a DTO of the current game state */ public GameStateDataTransferObject createGameStateDataTransferObject( - GameManager gameManager, Role role, Team currentTurn) { + GameManager gameManager, Team currentTurn) { List cardList = gameManager.getCardList(); List cardDataTransferObject = new ArrayList<>(); for (Card card : cardList) { - cardDataTransferObject.add(createCardDataTransferObject(card, role)); - } - Team winner; - if (gameManager.getWinner() == null) { - winner = null; - } else { - winner = gameManager.getWinner(); + cardDataTransferObject.add(createCardDataTransferObject(card)); } return new GameStateDataTransferObject( - winner, + gameManager.getWinner(), currentTurn, gameManager.getCurrentRedFound(), gameManager.getCurrentBlueFound(), diff --git a/src/main/java/com/codenames/codenames/backend/utility/Color.java b/src/main/java/com/codenames/codenames/backend/utility/Color.java index ab654f4b..37c4b6f6 100644 --- a/src/main/java/com/codenames/codenames/backend/utility/Color.java +++ b/src/main/java/com/codenames/codenames/backend/utility/Color.java @@ -6,6 +6,6 @@ public enum Color { RED, BLUE, - WHITE, - BLACK + NEUTRAL, + ASSASSIN } diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java index 5e38e918..bfde018c 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java @@ -28,8 +28,8 @@ void setUp() { Card card1 = new Card("Test1", Color.RED); Card card2 = new Card("Test2", Color.BLUE); - Card card3 = new Card("Test3", Color.WHITE); - Card card4 = new Card("Test4", Color.BLACK); + Card card3 = new Card("Test3", Color.NEUTRAL); + Card card4 = new Card("Test4", Color.ASSASSIN); dummyCardList = Arrays.asList(card1, card2, card3, card4); @@ -56,8 +56,8 @@ void testGetCardList() { void testCheckColor() { assertEquals(Color.RED, board.checkColor(0)); assertEquals(Color.BLUE, board.checkColor(1)); - assertEquals(Color.WHITE, board.checkColor(2)); - assertEquals(Color.BLACK, board.checkColor(3)); + assertEquals(Color.NEUTRAL, board.checkColor(2)); + assertEquals(Color.ASSASSIN, board.checkColor(3)); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/CardGeneratorTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/CardGeneratorTest.java index 58694d94..eed02baa 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/CardGeneratorTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/CardGeneratorTest.java @@ -30,8 +30,8 @@ void testGenerateCardsCorrectDistribution() { assertEquals(total, cards.size()); assertEquals(red, cards.stream().filter(card -> card.getColor() == Color.RED).count()); assertEquals(blue, cards.stream().filter(card -> card.getColor() == Color.BLUE).count()); - assertEquals(white, cards.stream().filter(card -> card.getColor() == Color.WHITE).count()); - assertEquals(black, cards.stream().filter(card -> card.getColor() == Color.BLACK).count()); + assertEquals(white, cards.stream().filter(card -> card.getColor() == Color.NEUTRAL).count()); + assertEquals(black, cards.stream().filter(card -> card.getColor() == Color.ASSASSIN).count()); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/CardTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/CardTest.java index 0ab1da8a..8c9a0b0d 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/CardTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/CardTest.java @@ -21,7 +21,7 @@ void testConstructorHappyPath() { @Test void testConstructorNullWord() { - assertThrows(IllegalArgumentException.class, () -> new Card(null, Color.BLACK)); + assertThrows(IllegalArgumentException.class, () -> new Card(null, Color.ASSASSIN)); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 8bbc2125..a5f5149e 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -164,7 +164,7 @@ void testGetWinner_blueStartsBlueWins() { @Test void testGetWinner_redFoundBlackCardFound() { - mockCardGeneration(List.of(new Card("Test", Color.BLACK))); + mockCardGeneration(List.of(new Card("Test", Color.ASSASSIN))); gameManager = new GameManager(blueTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, blueTeam); helperMethodAdvanceTurns(2); // red spymaster @@ -177,7 +177,7 @@ void testGetWinner_redFoundBlackCardFound() { @Test void testGetWinner_blueFoundBlackCardFound() { - mockCardGeneration(List.of(new Card("Test", Color.BLACK))); + mockCardGeneration(List.of(new Card("Test", Color.ASSASSIN))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodAdvanceTurns(2); // blue spymaster helperMethodSubmitClue(gameManager, 1, blueTeam); @@ -188,7 +188,7 @@ void testGetWinner_blueFoundBlackCardFound() { @Test void testFlipWhiteCard() { - mockCardGeneration(List.of(new Card("Test", Color.WHITE))); + mockCardGeneration(List.of(new Card("Test", Color.NEUTRAL))); gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, redTeam); helperMethodAdvanceTurns(1); @@ -206,7 +206,7 @@ void testFlipCard_cardAlreadyFlipped() { @Test void testFlipCard_winnerAlreadyDetermined() { - mockCardGeneration(List.of(new Card("Test", Color.BLACK))); + mockCardGeneration(List.of(new Card("Test", Color.ASSASSIN))); gameManager = new GameManager(blueTeam, mockCardGenerator, mockClueValidationService); helperMethodSubmitClue(gameManager, 1, blueTeam); helperMethodAdvanceTurns(1); // red operative diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index cfa8725b..5c76e850 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.playingfield; +import com.codenames.codenames.backend.serialization.DataTransferObjectService; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -20,6 +21,7 @@ class GameServiceTest { private GameService gameService; private GameManager mockGameManager; private GameManagerFactory mockGameManagerFactory; + private DataTransferObjectService mockDtoService; private final String lobbyCode = "ABCDE"; private final Team redTeam = Team.RED; @@ -28,8 +30,9 @@ class GameServiceTest { void setup() { mockGameManagerFactory = mock(GameManagerFactory.class); mockGameManager = mock(GameManager.class); + mockDtoService = mock(DataTransferObjectService.class); - gameService = new GameService(mockGameManagerFactory); + gameService = new GameService(mockGameManagerFactory, mockDtoService); when(mockGameManagerFactory.create(redTeam)).thenReturn(mockGameManager); gameService.createGameManager(lobbyCode, redTeam); diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 57e5003a..45c67f41 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -8,7 +8,6 @@ import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.utility.Color; -import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -36,13 +35,13 @@ void setUp() { when(mockGameManager.getCurrentBlueFound()).thenReturn(0); gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.OPERATIVE, redTeam); + service.createGameStateDataTransferObject(mockGameManager, redTeam); } @Test void testSpymasterVisibility() { gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.SPYMASTER, redTeam); + service.createGameStateDataTransferObject(mockGameManager, redTeam); assertEquals("RED", gameStateDto.cardList().get(0).color()); } @@ -65,7 +64,7 @@ void testGetWinner_exists() { void testGetWinner_null() { when(mockGameManager.getWinner()).thenReturn(null); gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, Role.OPERATIVE, redTeam); + service.createGameStateDataTransferObject(mockGameManager, redTeam); assertNull(gameStateDto.winner()); } } diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index 286ea635..670b5a6a 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -28,7 +28,7 @@ void setUp() { card = new Card("TEST", Color.RED); serializer = new SerializationJson(mapper); - dummyList = List.of(new CardDataTransferObject("TEST", "HIDDEN", false)); + dummyList = List.of(new CardDataTransferObject("TEST", null, false)); dummyGameState = new GameStateDataTransferObject(Team.RED, Team.RED, 0, 0, "Test", 1, dummyList); } From bc9d867359f8115158834f19cabc5925ff0164dc Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Sun, 17 May 2026 19:10:32 +0200 Subject: [PATCH 133/207] test: added & fixed tests --- .../lobby/controller/LobbyController.java | 6 +- .../backend/lobby/dto/GameStartResponse.java | 15 ----- .../backend/lobby/services/LobbyService.java | 4 +- .../controller/GameSocketControllerTest.java | 9 +-- .../lobby/controller/LobbyControllerTest.java | 46 ++++++++++++++++ .../lobby/services/LobbyServiceTest.java | 55 ++++++++++++++++++- .../DataTransferObjectServiceTest.java | 14 +---- .../serialization/SerializationJsonTest.java | 2 +- 8 files changed, 114 insertions(+), 37 deletions(-) delete mode 100644 src/main/java/com/codenames/codenames/backend/lobby/dto/GameStartResponse.java diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 25d6eb00..9e072172 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -1,6 +1,5 @@ package com.codenames.codenames.backend.lobby.controller; -import com.codenames.codenames.backend.lobby.dto.GameStartResponse; import com.codenames.codenames.backend.lobby.dto.LobbyResponse; import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyService; @@ -185,8 +184,9 @@ public ResponseEntity startGame( ) ); return ResponseEntity.badRequest().body( - new LobbyResponse("Could not start the game.", - username, + new LobbyResponse( + "Could not start the game.", + lobbyCode, service.getPlayersDto(lobbyCode), false ) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/dto/GameStartResponse.java b/src/main/java/com/codenames/codenames/backend/lobby/dto/GameStartResponse.java deleted file mode 100644 index 8c827889..00000000 --- a/src/main/java/com/codenames/codenames/backend/lobby/dto/GameStartResponse.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.codenames.codenames.backend.lobby.dto; - -import java.util.List; - -/** - * This response is transferred, when the host of a lobby requests a game start. - * It is used as a broadcast message type which all players receive simultaneously. - * - * @param message the message that is displayed - * @param lobbyCode the lobbyCode of the starting lobby - * @param playerList list of all players including their roles - * @param isGameStarted if the game was started successfully - */ -public record GameStartResponse(String message, String lobbyCode, List playerList, boolean isGameStarted) { -} diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 783cc43d..f5f8b7a2 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -264,8 +264,10 @@ public boolean startGame(String lobbyCode, String username) { return isStarted; } - private String getHost(String lobbyCode) { + public String getHost(String lobbyCode) { + if(lobbyCode == null || lobbyCode.isBlank()) return ""; List players = getPlayers(lobbyCode); + if(players.isEmpty()) return ""; for(Player p : players) { if(p.isHost()) { return p.username(); diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 79bb6b22..3bc20212 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.game.controller; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mock; @@ -43,7 +44,7 @@ void startGameShouldBroadcastState() { message.setLobbyCode("ABCDE"); - when(gameService.createGameStateDto("ABCDE")).thenReturn(mock(GameStateDto.class)); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(mock(GameStateDataTransferObject.class)); controller.startGame(message); @@ -59,7 +60,7 @@ void revealCardShouldBroadcastState() { message.setPosition(0); message.setCurrentTurn(Team.RED); - when(gameService.createGameStateDto("ABCDE")).thenReturn(mock(GameStateDto.class)); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(mock(GameStateDataTransferObject.class)); controller.revealCard(message); @@ -78,7 +79,7 @@ void submitClueShouldBroadcastState() { message.setGuessAmount(2); message.setCurrentTurn(Team.RED); - when(gameService.createGameStateDto("ABCDE")).thenReturn(mock(GameStateDto.class)); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(mock(GameStateDataTransferObject.class)); controller.submitClue(message); @@ -95,7 +96,7 @@ void passTurnShouldBroadcastUpdatedState() { message.setLobbyCode("ABCDE"); message.setCurrentTurn(Team.RED); - when(gameService.createGameStateDto("ABCDE")).thenReturn(mock(GameStateDto.class)); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(mock(GameStateDataTransferObject.class)); controller.passTurn(message); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java index 874b3036..f45a62e9 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java @@ -170,4 +170,50 @@ void getLobbyInfoShouldReturn200_whenLobbyExists() throws Exception { .andExpect(jsonPath("$.playerList[1].username") .value("Bob")); } + + @Test + void testStartGameReturns200_WhenConditionIsMet() throws Exception { + List players = List.of( + new PlayerDto("Alice", null, null, true), + new PlayerDto("Bob", null, null, false) + ); + + when(service.getPlayersDto("ABCDE")).thenReturn(players); + when(service.startGame("ABCDE", "Alice")).thenReturn(true); + + mockMvc.perform(get("/lobby/ABCDE/start-game") + .param("username", "Alice")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message") + .value("Game is starting now.")) + .andExpect(jsonPath("$.lobbyCode") + .value("ABCDE")) + .andExpect(jsonPath("$.playerList[0].username") + .value("Alice")) + .andExpect(jsonPath("$.isStarted") + .value("true")); + } + + @Test + void testStartGameReturns400_WhenServiceReturnsFalse() throws Exception{ + List players = List.of( + new PlayerDto("Alice", null, null, true), + new PlayerDto("Bob", null, null, false) + ); + + when(service.getPlayersDto("ABCDE")).thenReturn(players); + when(service.startGame("ABCDE", "Alice")).thenReturn(false); + + mockMvc.perform(get("/lobby/ABCDE/start-game") + .param("username", "Alice")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message") + .value("Could not start the game.")) + .andExpect(jsonPath("$.lobbyCode") + .value("ABCDE")) + .andExpect(jsonPath("$.playerList[0].username") + .value("Alice")) + .andExpect(jsonPath("$.isStarted") + .value("false")); + } } \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index 8fcfdab5..3ecb5f2a 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -311,7 +311,60 @@ void getPlayersDtoShouldReturnEmptyList_whenLobbyDoesNotExist() { @Test void testAddGameManagerForLobby() { - lobbyService.createLobby("Host"); + lobbyService.createLobby("User"); + lobbyService.startGame("ABCDE", "User"); verify(gameService, times(1)).createGameManager(eq("ABCDE"), any(Team.class)); } + + @Test + void testGetIsStarted() { + when(gameService.isGameStarted("ABCDE")).thenReturn(true); + + boolean result = lobbyService.getIsStarted("ABCDE"); + assertTrue(result); + } + + @Test + void testGetIsStarted_GameServiceReturnsFalse() { + when(gameService.isGameStarted("ABCDE")).thenReturn(false); + + boolean result = lobbyService.getIsStarted("ABCDE"); + assertFalse(result); + } + + @Test + void testGetHost_Works() { + lobbyService.createLobby("Alice"); + lobbyService.joinLobby("Bob", "ABCDE"); + lobbyService.joinLobby("Ceasar", "ABCDE"); + + String expected = "Alice"; + String result = lobbyService.getHost("ABCDE"); + + assertEquals(expected, result); + } + + @Test + void testGetHost_NullCode() { + String result = lobbyService.getHost(null); + String expected = ""; + + assertEquals(expected, result); + } + + @Test + void testGetHost_EmptyCode() { + String result = lobbyService.getHost(""); + String expected = ""; + + assertEquals(expected, result); + } + + @Test + void testGetHost_EmptyPlayers() { + String result = lobbyService.getHost("ABCDE"); + String expected = ""; + + assertEquals(expected, result); + } } diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 45c67f41..8bdad210 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -39,20 +39,10 @@ void setUp() { } @Test - void testSpymasterVisibility() { + void testCorrectColors() { gameStateDto = service.createGameStateDataTransferObject(mockGameManager, redTeam); - assertEquals("RED", gameStateDto.cardList().get(0).color()); - } - - @Test - void testOperatorVisibility_hidden() { - assertEquals("HIDDEN", gameStateDto.cardList().get(0).color()); - } - - @Test - void testOperatorVisibility_isGuessed() { - assertEquals("RED", gameStateDto.cardList().get(1).color()); + assertEquals(Color.RED, gameStateDto.cardList().get(0).color()); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index 670b5a6a..e51eea32 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -38,7 +38,7 @@ void testSerialize_pass() { String expectedResult = "{\"winner\":\"RED\",\"currentTurn\":\"RED\",\"currentRedFound\":0,\"currentBlueFound\":0" + ",\"currentClue\":\"Test\",\"remainingGuesses\":1,\"cardList\":[{\"word\":\"TEST\"," - + "\"color\":\"HIDDEN\",\"isGuessed\":false}]}"; + + "\"color\":null,\"isGuessed\":false}]}"; String result = serializer.serialize(dummyGameState); assertEquals(expectedResult, result); } From cc5590c7937f419f778703dc6672a17dc691136d Mon Sep 17 00:00:00 2001 From: Anna Pschernig Date: Sun, 17 May 2026 19:26:27 +0200 Subject: [PATCH 134/207] refactor: resolved checkstyle and sonar issues --- .../backend/game/dto/GameStateDto.java | 46 +++++-------------- .../lobby/controller/LobbyController.java | 27 +++++++---- .../backend/lobby/dto/LobbyResponse.java | 8 +++- .../backend/lobby/services/LobbyService.java | 39 ++++++++++++++-- .../backend/playingfield/GameService.java | 8 +--- .../backend/websocket/GameController.java | 17 ++++--- .../websocket/WebSocketEventListener.java | 3 +- .../controller/GameSocketControllerTest.java | 1 - .../lobby/services/LobbyServiceTest.java | 34 +++++--------- .../backend/playingfield/GameServiceTest.java | 3 +- 10 files changed, 97 insertions(+), 89 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java b/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java index e7a03286..010b8ef3 100644 --- a/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java +++ b/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java @@ -5,39 +5,15 @@ import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.List; -import lombok.Getter; -/** DTO representing the current game state. */ -@Getter -public class GameStateDto { - private final List cards; - private final Clue currentClue; - private final int remainingGuesses; - private final Team winner; - private final Team currentTurn; - private final Role currentPhase; - - /** - * Creates a new game state DTO. - * - * @param cards current board cards - * @param currentClue current clue - * @param remainingGuesses remaining guesses - * @param winner winning team or null - */ - public GameStateDto( - List cards, - Clue currentClue, - int remainingGuesses, - Team winner, - Team currentTurn, - Role currentPhase) { - - this.cards = cards; - this.currentClue = currentClue; - this.remainingGuesses = remainingGuesses; - this.winner = winner; - this.currentTurn = currentTurn; - this.currentPhase = currentPhase; - } -} +/** + * DTO representing the current game state. + */ +public record GameStateDto( + List cards, + Clue currentClue, + int remainingGuesses, + Team winner, + Team currentTurn, + Role currentPhase +) {} diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 9e072172..2f73b52f 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -169,20 +169,31 @@ public ResponseEntity selectPosition( } } + /** + * Endpoint for starting a game, this is the last http-request only the host can make. + * + * @param lobbyCode the unique lobby code + * @param username the name of the requesting user + * @return a response entity of a lobby response, with isStarted @code true or @code false, + * whether the starting was successful or not + */ + @GetMapping("/{lobbyCode}/start-game") public ResponseEntity startGame( @PathVariable String lobbyCode, @RequestParam String username ) { boolean isStarted = service.startGame(lobbyCode, username); - if(isStarted) return ResponseEntity.ok( - new LobbyResponse( - "Game is starting now.", - lobbyCode, - service.getPlayersDto(lobbyCode), - true - ) - ); + if (isStarted) { + return ResponseEntity.ok( + new LobbyResponse( + "Game is starting now.", + lobbyCode, + service.getPlayersDto(lobbyCode), + true + ) + ); + } return ResponseEntity.badRequest().body( new LobbyResponse( "Could not start the game.", diff --git a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java b/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java index 858c1547..1b750c27 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/dto/LobbyResponse.java @@ -7,5 +7,9 @@ * *

Contains a message describing the outcome and the associated lobby code. */ -public record LobbyResponse(String message, String lobbyCode, List playerList, boolean isStarted) { -} +public record LobbyResponse( + String message, + String lobbyCode, + List playerList, + boolean isStarted +) {} diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index f5f8b7a2..42c88f17 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -255,8 +255,18 @@ public Role getPlayerRole(String username, String lobbyCode) { return null; } + /** + * The service method for starting a game. This creates a game manager object for the lobby + * and checks if the requesting user is liable to start the game. + * + * @param lobbyCode the unique lobby code + * @param username the name of the requesting user + * @return if starting was successful + */ + public boolean startGame(String lobbyCode, String username) { - boolean isStarted = !lobbyCode.isBlank() && !username.isBlank() && Objects.equals(getHost(lobbyCode), username); + boolean isStarted = !lobbyCode.isBlank() && !username.isBlank() + && Objects.equals(getHost(lobbyCode), username); Lobby lobby = lobbyList.get(lobbyCode); addGameManagerForLobby(lobby, lobbyCode); @@ -264,18 +274,37 @@ public boolean startGame(String lobbyCode, String username) { return isStarted; } + /** + * This method computes the host of a lobby. + * + * @param lobbyCode the unique lobby code + * @return the username of the host + */ + public String getHost(String lobbyCode) { - if(lobbyCode == null || lobbyCode.isBlank()) return ""; + if (lobbyCode == null || lobbyCode.isBlank()) { + return ""; + } List players = getPlayers(lobbyCode); - if(players.isEmpty()) return ""; - for(Player p : players) { - if(p.isHost()) { + if (players.isEmpty()) { + return ""; + } + for (Player p : players) { + if (p.isHost()) { return p.username(); } } return ""; } + /** + * Checks if the game is started by looking after an existing + * game manager object. + * + * @param lobbyCode the unique lobby code + * @return whether a game manager exists (@code true or @code false) + */ + public boolean getIsStarted(String lobbyCode) { return gameService.isGameStarted(lobbyCode); } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 4d8f1ea2..03d45a6b 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -2,13 +2,9 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.game.dto.GameStateDto; -import com.codenames.codenames.backend.serialization.CardDataTransferObject; import com.codenames.codenames.backend.serialization.DataTransferObjectService; import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.utility.Team; - -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.springframework.stereotype.Service; @@ -136,8 +132,8 @@ public GameStateDto createGameStateDto(String lobbyCode) { /** * Maps current game State into a @link GameStateTransferObject. * - * @param lobbyCode - * @return + * @param lobbyCode the unique lobby code + * @return the mapped game state transfer object */ public GameStateDataTransferObject getCurrentGameState(String lobbyCode) { GameManager gm = getGame(lobbyCode); diff --git a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java index d2b8c535..5e475930 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java @@ -2,6 +2,7 @@ import com.codenames.codenames.backend.lobby.services.LobbyService; import java.util.List; +import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -13,6 +14,7 @@ *

Processes client messages (e.g. join requests), coordinates with {@link LobbyService}, and * broadcasts updates to subscribed clients. */ +@Slf4j @Controller public class GameController { @@ -49,13 +51,16 @@ public GameController( public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) { String sessionId = headerAccessor.getSessionId(); + try { + if (sessionId == null && headerAccessor.getSessionAttributes() != null) { + sessionId = (String) headerAccessor.getSessionAttributes().get("sessionId"); + } - if (sessionId == null && headerAccessor.getSessionAttributes() != null) { - sessionId = (String) headerAccessor.getSessionAttributes().get("sessionId"); - } - - if (sessionId == null) { - return; + if (sessionId == null) { + return; + } + } catch (NullPointerException e) { + log.error(e.getMessage()); } boolean joined = lobbyService.joinLobby(message.getName(), message.getCode()); diff --git a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java b/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java index 496d6135..fd1c1e31 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/WebSocketEventListener.java @@ -10,7 +10,8 @@ /** * Listener for WebSocket lifecycle events. * - *

Handles client disconnections by removing playerList from lobbies, cleaning up session mappings, + *

Handles client disconnections by removing playerList from lobbies, + * cleaning up session mappings, * and notifying remaining clients. */ @Component diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 3bc20212..a5183a5a 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -8,7 +8,6 @@ import static org.mockito.Mockito.when; import com.codenames.codenames.backend.game.dto.ClueMessage; -import com.codenames.codenames.backend.game.dto.GameStateDto; import com.codenames.codenames.backend.game.dto.PassTurnMessage; import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index 3ecb5f2a..ede33cd1 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -5,6 +5,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -285,7 +288,7 @@ void testGetPlayersDto_lobbyNotExists() { } @Test - void getPlayersDtoShouldReturnPlayerDtos_whenLobbyExists() { + void getPlayersDtoShouldReturnPlayerDTOs_whenLobbyExists() { lobbyService.createLobby("Host"); List result = lobbyService.getPlayersDto("ABCDE"); @@ -336,7 +339,7 @@ void testGetIsStarted_GameServiceReturnsFalse() { void testGetHost_Works() { lobbyService.createLobby("Alice"); lobbyService.joinLobby("Bob", "ABCDE"); - lobbyService.joinLobby("Ceasar", "ABCDE"); + lobbyService.joinLobby("Caesar", "ABCDE"); String expected = "Alice"; String result = lobbyService.getHost("ABCDE"); @@ -344,27 +347,12 @@ void testGetHost_Works() { assertEquals(expected, result); } - @Test - void testGetHost_NullCode() { - String result = lobbyService.getHost(null); - String expected = ""; - - assertEquals(expected, result); - } + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"ABCDE"}) + void testGetHost_ReturnsEmptyString(String lobbyCode) { + String result = lobbyService.getHost(lobbyCode); - @Test - void testGetHost_EmptyCode() { - String result = lobbyService.getHost(""); - String expected = ""; - - assertEquals(expected, result); - } - - @Test - void testGetHost_EmptyPlayers() { - String result = lobbyService.getHost("ABCDE"); - String expected = ""; - - assertEquals(expected, result); + assertEquals("", result); } } diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index 5c76e850..8cda01e2 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -21,7 +21,6 @@ class GameServiceTest { private GameService gameService; private GameManager mockGameManager; private GameManagerFactory mockGameManagerFactory; - private DataTransferObjectService mockDtoService; private final String lobbyCode = "ABCDE"; private final Team redTeam = Team.RED; @@ -30,7 +29,7 @@ class GameServiceTest { void setup() { mockGameManagerFactory = mock(GameManagerFactory.class); mockGameManager = mock(GameManager.class); - mockDtoService = mock(DataTransferObjectService.class); + DataTransferObjectService mockDtoService = mock(DataTransferObjectService.class); gameService = new GameService(mockGameManagerFactory, mockDtoService); when(mockGameManagerFactory.create(redTeam)).thenReturn(mockGameManager); From 2bde83a0e3d6642edc13db02f7fa2bb56bbef4ba Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 17:40:26 +0200 Subject: [PATCH 135/207] feat: add system snapshot model for restart recovery --- .../backend/recovery/snapshot/ClueSnapshot.java | 3 +++ .../backend/recovery/snapshot/GameSnapshot.java | 16 ++++++++++++++++ .../backend/recovery/snapshot/LobbySnapshot.java | 6 ++++++ .../recovery/snapshot/SystemSnapshot.java | 9 +++++++++ 4 files changed, 34 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java create mode 100644 src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java create mode 100644 src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java create mode 100644 src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java new file mode 100644 index 00000000..e4b304e2 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java @@ -0,0 +1,3 @@ +package com.codenames.codenames.backend.recovery.snapshot; + +public record ClueSnapshot(String word, int guessAmount) {} diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java new file mode 100644 index 00000000..f9883703 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java @@ -0,0 +1,16 @@ +package com.codenames.codenames.backend.recovery.snapshot; + +import com.codenames.codenames.backend.playingfield.Card; +import com.codenames.codenames.backend.utility.Role; +import com.codenames.codenames.backend.utility.Team; +import java.util.List; + +public record GameSnapshot( + Team currentTurn, + Role currentPhase, + Team winner, + int currentRedFound, + int currentBlueFound, + int remainingGuesses, + ClueSnapshot currentClue, + List cards) {} diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java new file mode 100644 index 00000000..77f8470b --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java @@ -0,0 +1,6 @@ +package com.codenames.codenames.backend.recovery.snapshot; + +import com.codenames.codenames.backend.lobby.dto.PlayerDto; +import java.util.List; + +public record LobbySnapshot(String lobbyCode, List players) {} diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java new file mode 100644 index 00000000..d8c46308 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java @@ -0,0 +1,9 @@ +package com.codenames.codenames.backend.recovery.snapshot; + +import java.util.Map; + +public record SystemSnapshot( + int schemaVersion, Map lobbies, Map games) { + + public static final int CURRENT_SCHEMA_VERSION = 1; +} From 4f6ec34eb63e2f36fca6e048f61270a4962c7f8e Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 18:35:01 +0200 Subject: [PATCH 136/207] feat: add json state store for atomic snapshot persistence --- .../backend/recovery/JsonStateStore.java | 71 +++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java diff --git a/src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java b/src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java new file mode 100644 index 00000000..def3976e --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java @@ -0,0 +1,71 @@ +package com.codenames.codenames.backend.recovery; + +import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.file.AtomicMoveNotSupportedException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.StandardCopyOption; +import java.util.Optional; +import java.util.concurrent.locks.ReentrantLock; +import lombok.Getter; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@Component +@Getter +public class JsonStateStore { + + private final ObjectMapper objectMapper; + private final Path stateFilePath; + private final ReentrantLock ioLock = new ReentrantLock(); + + public JsonStateStore( + ObjectMapper objectMapper, @Value("${app.state-file:data/state.json}") String stateFile) { + this.objectMapper = objectMapper; + this.stateFilePath = Path.of(stateFile); + } + + public void save(SystemSnapshot snapshot) { + ioLock.lock(); + try { + Path parentDirectory = stateFilePath.getParent(); + if (parentDirectory != null) { + Files.createDirectories(parentDirectory); + } + + Path tempFilePath = stateFilePath.resolveSibling(stateFilePath.getFileName() + ".tmp"); + objectMapper.writerWithDefaultPrettyPrinter().writeValue(tempFilePath.toFile(), snapshot); + + moveAtomically(tempFilePath, stateFilePath); + } catch (IOException exception) { + throw new IllegalStateException("Failed to persist state snapshot.", exception); + } finally { + ioLock.unlock(); + } + } + + public Optional load() { + ioLock.lock(); + try { + if (!Files.exists(stateFilePath) || Files.size(stateFilePath) == 0L) { + return Optional.empty(); + } + return Optional.of(objectMapper.readValue(stateFilePath.toFile(), SystemSnapshot.class)); + } catch (IOException exception) { + throw new IllegalStateException("Failed to load persisted state snapshot.", exception); + } finally { + ioLock.unlock(); + } + } + + private void moveAtomically(Path source, Path target) throws IOException { + try { + Files.move( + source, target, StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING); + } catch (AtomicMoveNotSupportedException exception) { + Files.move(source, target, StandardCopyOption.REPLACE_EXISTING); + } + } +} From a888582fcde6f444b4103eef631793749b005b2e Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 18:37:29 +0200 Subject: [PATCH 137/207] docs: add javadocs --- .../backend/recovery/JsonStateStore.java | 28 +++++++++++++++++++ .../recovery/snapshot/ClueSnapshot.java | 6 ++++ .../recovery/snapshot/GameSnapshot.java | 12 ++++++++ .../recovery/snapshot/LobbySnapshot.java | 6 ++++ .../recovery/snapshot/SystemSnapshot.java | 7 +++++ 5 files changed, 59 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java b/src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java index def3976e..1c58c2ca 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/JsonStateStore.java @@ -13,6 +13,11 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; +/** + * JSON-backed storage for persisted system snapshots. + * + *

Writes are synchronized and performed via temporary-file replace to reduce corruption risk. + */ @Component @Getter public class JsonStateStore { @@ -21,12 +26,23 @@ public class JsonStateStore { private final Path stateFilePath; private final ReentrantLock ioLock = new ReentrantLock(); + /** + * Creates a new state store with configured target file path. + * + * @param objectMapper mapper used for JSON serialization + * @param stateFile configured path to the persisted state file + */ public JsonStateStore( ObjectMapper objectMapper, @Value("${app.state-file:data/state.json}") String stateFile) { this.objectMapper = objectMapper; this.stateFilePath = Path.of(stateFile); } + /** + * Persists the full system snapshot atomically. + * + * @param snapshot full snapshot to store + */ public void save(SystemSnapshot snapshot) { ioLock.lock(); try { @@ -46,6 +62,11 @@ public void save(SystemSnapshot snapshot) { } } + /** + * Loads the persisted system snapshot when present. + * + * @return optional snapshot + */ public Optional load() { ioLock.lock(); try { @@ -60,6 +81,13 @@ public Optional load() { } } + /** + * Replaces target file with source file using atomic move where supported. + * + * @param source temporary source file + * @param target final target file + * @throws IOException when move fails + */ private void moveAtomically(Path source, Path target) throws IOException { try { Files.move( diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java index e4b304e2..2e3b2fbf 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java @@ -1,3 +1,9 @@ package com.codenames.codenames.backend.recovery.snapshot; +/** + * Persisted clue payload used for restart recovery snapshots. + * + * @param word clue word + * @param guessAmount clue guess amount + */ public record ClueSnapshot(String word, int guessAmount) {} diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java index f9883703..779088e9 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java @@ -5,6 +5,18 @@ import com.codenames.codenames.backend.utility.Team; import java.util.List; +/** + * Persisted game payload used for restart recovery snapshots. + * + * @param currentTurn current team turn + * @param currentPhase current gameplay phase + * @param winner winner if already decided + * @param currentRedFound discovered red cards + * @param currentBlueFound discovered blue cards + * @param remainingGuesses remaining guesses + * @param currentClue current clue if present + * @param cards full card state list + */ public record GameSnapshot( Team currentTurn, Role currentPhase, diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java index 77f8470b..992b00d9 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java @@ -3,4 +3,10 @@ import com.codenames.codenames.backend.lobby.dto.PlayerDto; import java.util.List; +/** + * Persisted lobby payload used for restart recovery snapshots. + * + * @param lobbyCode lobby identifier + * @param players players including team/role/host metadata + */ public record LobbySnapshot(String lobbyCode, List players) {} diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java index d8c46308..cdee2c24 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java @@ -2,6 +2,13 @@ import java.util.Map; +/** + * Root snapshot aggregate for persisted backend runtime state. + * + * @param schemaVersion persisted schema version + * @param lobbies lobby snapshots keyed by lobby code + * @param games game snapshots keyed by lobby code + */ public record SystemSnapshot( int schemaVersion, Map lobbies, Map games) { From 11c84b1d0f76819ceceb42a9e993d281b91af229 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 18:41:34 +0200 Subject: [PATCH 138/207] test: add unit tests for json state store load and save behavior --- .../backend/recovery/JsonStateStoreTest.java | 121 ++++++++++++++++++ 1 file changed, 121 insertions(+) create mode 100644 src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java diff --git a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java new file mode 100644 index 00000000..086c538d --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java @@ -0,0 +1,121 @@ +package com.codenames.codenames.backend.recovery; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.codenames.codenames.backend.lobby.dto.PlayerDto; +import com.codenames.codenames.backend.recovery.snapshot.ClueSnapshot; +import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; +import com.codenames.codenames.backend.recovery.snapshot.LobbySnapshot; +import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; +import com.codenames.codenames.backend.utility.Role; +import com.codenames.codenames.backend.utility.Team; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class JsonStateStoreTest { + + @TempDir Path tempDir; + + @Test + void loadReturnsEmptyWhenStateFileDoesNotExist() { + Path stateFile = tempDir.resolve("state.json"); + JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); + + Optional loadedSnapshot = stateStore.load(); + + assertTrue(loadedSnapshot.isEmpty()); + } + + @Test + void loadReturnsEmptyWhenStateFileIsEmpty() throws IOException { + Path stateFile = tempDir.resolve("state.json"); + Files.createFile(stateFile); + JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); + + Optional loadedSnapshot = stateStore.load(); + + assertTrue(loadedSnapshot.isEmpty()); + } + + @Test + void saveAndLoadRoundTrip() { + Path stateFile = tempDir.resolve("state.json"); + JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); + + LobbySnapshot lobbySnapshot = + new LobbySnapshot( + "ABCDE", + List.of( + new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), + new PlayerDto("Player", Team.BLUE, Role.OPERATIVE, false))); + + GameSnapshot gameSnapshot = + new GameSnapshot( + Team.RED, Role.OPERATIVE, null, 1, 0, 2, new ClueSnapshot("ANIMAL", 2), List.of()); + + SystemSnapshot expectedSnapshot = + new SystemSnapshot( + SystemSnapshot.CURRENT_SCHEMA_VERSION, + Map.of("ABCDE", lobbySnapshot), + Map.of("ABCDE", gameSnapshot)); + + stateStore.save(expectedSnapshot); + Optional loadedSnapshot = stateStore.load(); + + assertTrue(loadedSnapshot.isPresent()); + assertTrue(Files.exists(stateFile)); + + SystemSnapshot actualSnapshot = loadedSnapshot.get(); + assertEquals(SystemSnapshot.CURRENT_SCHEMA_VERSION, actualSnapshot.schemaVersion()); + assertEquals(1, actualSnapshot.lobbies().size()); + assertEquals(1, actualSnapshot.games().size()); + + LobbySnapshot actualLobbySnapshot = actualSnapshot.lobbies().get("ABCDE"); + assertEquals("ABCDE", actualLobbySnapshot.lobbyCode()); + assertEquals(2, actualLobbySnapshot.players().size()); + assertEquals("Host", actualLobbySnapshot.players().get(0).username()); + assertEquals(Team.RED, actualLobbySnapshot.players().get(0).team()); + assertEquals(Role.SPYMASTER, actualLobbySnapshot.players().get(0).role()); + assertTrue(actualLobbySnapshot.players().get(0).isHost()); + + GameSnapshot actualGameSnapshot = actualSnapshot.games().get("ABCDE"); + assertEquals(Team.RED, actualGameSnapshot.currentTurn()); + assertEquals(Role.OPERATIVE, actualGameSnapshot.currentPhase()); + assertEquals(2, actualGameSnapshot.remainingGuesses()); + assertEquals("ANIMAL", actualGameSnapshot.currentClue().word()); + assertEquals(2, actualGameSnapshot.currentClue().guessAmount()); + assertTrue(actualGameSnapshot.cards().isEmpty()); + } + + @Test + void saveOverwritesPreviousSnapshot() { + Path stateFile = tempDir.resolve("state.json"); + JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); + + SystemSnapshot firstSnapshot = + new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of(), Map.of()); + SystemSnapshot secondSnapshot = + new SystemSnapshot( + SystemSnapshot.CURRENT_SCHEMA_VERSION, + Map.of("ABCDE", new LobbySnapshot("ABCDE", List.of())), + Map.of()); + + stateStore.save(firstSnapshot); + stateStore.save(secondSnapshot); + + Optional loadedSnapshot = stateStore.load(); + + assertTrue(loadedSnapshot.isPresent()); + assertTrue(loadedSnapshot.get().lobbies().containsKey("ABCDE")); + assertFalse(loadedSnapshot.get().lobbies().isEmpty()); + } +} From 55bae8f735c2c1379fbc0e7d31a6b149c0216b2a Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 18:44:14 +0200 Subject: [PATCH 139/207] test: replace redundant lobby emptiness assertion with size check --- .../codenames/backend/recovery/JsonStateStoreTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java index 086c538d..887a38bf 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java @@ -116,6 +116,6 @@ void saveOverwritesPreviousSnapshot() { assertTrue(loadedSnapshot.isPresent()); assertTrue(loadedSnapshot.get().lobbies().containsKey("ABCDE")); - assertFalse(loadedSnapshot.get().lobbies().isEmpty()); + assertEquals(1, loadedSnapshot.get().lobbies().size()); } } From acef4373caeb93dc73ab7cd361324f153fb5bdaf Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Mon, 18 May 2026 19:31:49 +0200 Subject: [PATCH 140/207] change data type in game state dto --- .../serialization/DataTransferObjectService.java | 6 ++---- .../serialization/GameStateDataTransferObject.java | 11 +++-------- .../backend/serialization/SerializationJsonTest.java | 3 ++- 3 files changed, 7 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java index 5926080c..77e55cf0 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.serialization; +import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.utility.Role; @@ -56,10 +57,7 @@ public GameStateDataTransferObject createGameStateDataTransferObject( winner, currentTurn, currentPhase, - gameManager.getCurrentRedFound(), - gameManager.getCurrentBlueFound(), - gameManager.getCurrentClueWord(), - gameManager.getRemainingGuesses(), + new Clue(gameManager.getCurrentClue().word(), gameManager.getCurrentClue().guessAmount()), cardDataTransferObject); } } diff --git a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java index c17708c5..5b00e3d6 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.serialization; +import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.List; @@ -9,18 +10,12 @@ * * @param winner the winner * @param currentTurn the current team who is allowed to make a move - * @param currentRedFound the amount of red cards revealed - * @param currentBlueFound the amount of red cards revealed - * @param currentClue the word of the clue - * @param remainingGuesses the amount of guesses + * @param currentClue the current clue object, consisting of word and amount of guesses * @param cardList the cards on the board */ public record GameStateDataTransferObject( Team winner, Team currentTurn, Role currentPhase, - int currentRedFound, - int currentBlueFound, - String currentClue, - int remainingGuesses, + Clue currentClue, List cardList) {} diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index 8112bb94..0528d8b6 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -6,6 +6,7 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Role; @@ -33,7 +34,7 @@ void setUp() { dummyList = List.of(new CardDataTransferObject("TEST", "HIDDEN", false)); dummyGameState = - new GameStateDataTransferObject(redTeam, redTeam, spymaster, 0, 0, "Test", 1, dummyList); + new GameStateDataTransferObject(redTeam, redTeam, spymaster, new Clue("Test", 1), dummyList); } @Test From 7676076dac968544c1b746c9cd3e20ceb57f301b Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Mon, 18 May 2026 20:36:41 +0200 Subject: [PATCH 141/207] add clue dto and change parameter of game state dto from clue to clue dto --- .../codenames/backend/game/dto/ClueDto.java | 12 ++++++++++++ .../serialization/DataTransferObjectService.java | 14 +++++++++++++- .../serialization/GameStateDataTransferObject.java | 3 ++- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/codenames/codenames/backend/game/dto/ClueDto.java diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/ClueDto.java b/src/main/java/com/codenames/codenames/backend/game/dto/ClueDto.java new file mode 100644 index 00000000..26a54187 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/dto/ClueDto.java @@ -0,0 +1,12 @@ +package com.codenames.codenames.backend.game.dto; + +/** + * Data transfer object for a clue, containing the clue word and the allowed number of guesses. + * This is only used for sending clues, not for receiving them. + * Therefore, the +1 is already included in the guessAmount. + * + * @param word the hint word + * @param guessAmount how many guesses are allowed + */ +public record ClueDto(String word, int guessAmount) { +} diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java index 50ce46ba..b3edb98e 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.serialization; import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.utility.Color; @@ -41,11 +42,22 @@ public GameStateDataTransferObject createGameStateDataTransferObject( for (Card card : cardList) { cardDataTransferObject.add(createCardDataTransferObject(card)); } + if (gameManager.getCurrentClue() == null) { + return new GameStateDataTransferObject( + gameManager.getWinner(), + currentTurn, + currentPhase, + null, + cardDataTransferObject); + } + String word = gameManager.getCurrentClue().word(); + if(word == null) word = ""; + int guessAmount = gameManager.getCurrentClue().guessAmount(); return new GameStateDataTransferObject( gameManager.getWinner(), currentTurn, currentPhase, - new Clue(gameManager.getCurrentClue().word(), gameManager.getCurrentClue().guessAmount()), + new ClueDto(word, guessAmount), cardDataTransferObject); } } diff --git a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java index 5b00e3d6..fe7e3ed1 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.serialization; import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.List; @@ -17,5 +18,5 @@ public record GameStateDataTransferObject( Team winner, Team currentTurn, Role currentPhase, - Clue currentClue, + ClueDto currentClue, List cardList) {} From 63a2dcba015f591c32e640e017acf73b821f572a Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Mon, 18 May 2026 20:57:00 +0200 Subject: [PATCH 142/207] test: fix and add tests --- .../DataTransferObjectServiceTest.java | 19 +++++++++++++++++++ .../serialization/SerializationJsonTest.java | 10 +++++----- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 02d93be0..e9cc3547 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -5,6 +5,8 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.utility.Color; @@ -70,4 +72,21 @@ void testGetWinner_null() { service.createGameStateDataTransferObject(mockGameManager, operative, redTeam, operative); assertNull(gameStateDto.winner()); } + + @Test + void testCreateGameStateDataTransferObject() { + Clue clue = new Clue("word", 1); + when(mockGameManager.getCardList()).thenReturn(List.of()); + when(mockGameManager.getWinner()).thenReturn(null); + when(mockGameManager.getCurrentClue()).thenReturn(clue); + + GameStateDataTransferObject dto = service.createGameStateDataTransferObject(mockGameManager, operative, redTeam, operative); + + assertEquals(clue.word(), dto.currentClue().word()); + assertEquals(clue.guessAmount(), dto.currentClue().guessAmount()); + assertNull(dto.winner()); + assertEquals(0, dto.cardList().size()); + assertEquals(redTeam, dto.currentTurn()); + assertEquals(operative, dto.currentPhase()); + } } diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index 6f40920d..657ae302 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -7,6 +7,7 @@ import static org.mockito.Mockito.when; import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Role; @@ -34,16 +35,15 @@ void setUp() { dummyList = List.of(new CardDataTransferObject("TEST", null, false)); dummyGameState = - new GameStateDataTransferObject(redTeam, redTeam, spymaster, new Clue("Test", 1), dummyList); + new GameStateDataTransferObject(redTeam, redTeam, spymaster, new ClueDto("Test", 1), dummyList); } @Test void testSerialize_pass() { String expectedResult = - "{\"winner\":\"RED\",\"currentTurn\":\"RED\",\"currentPhase\":\"SPYMASTER\"," - + "\"currentRedFound\":0,\"currentBlueFound\":0,\"currentClue\":\"Test\"," - + "\"remainingGuesses\":1,\"cardList\":[{\"word\":\"TEST\",\"color\":null," - + "\"isGuessed\":false}]}"; + """ + {"winner":"RED","currentTurn":"RED","currentPhase":"SPYMASTER","currentClue":{"word":"Test","guessAmount":1},"cardList":[{"word":"TEST","color":null,"isGuessed":false}]}""" + ; String result = serializer.serialize(dummyGameState); assertEquals(expectedResult, result); } From 7c06d0eadb3a382cc6cf6bdb72184e40615ac1d5 Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Mon, 18 May 2026 21:43:22 +0200 Subject: [PATCH 143/207] refactor: remove unused imports, clear warnings --- .../codenames/backend/playingfield/GameService.java | 4 +++- .../backend/serialization/DataTransferObjectService.java | 8 ++++---- .../serialization/DataTransferObjectServiceTest.java | 9 ++++----- .../backend/serialization/SerializationJsonTest.java | 1 - 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index a938179d..d750c385 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -136,7 +136,9 @@ public GameStateDto createGameStateDto(String lobbyCode) { */ public GameStateDataTransferObject getCurrentGameState(String lobbyCode) { GameManager gm = getGame(lobbyCode); - return dtoService.createGameStateDataTransferObject(gm, null, gm.getCurrentTurn(), gm.getCurrentPhase()); + return dtoService.createGameStateDataTransferObject( + gm, gm.getCurrentTurn(), gm.getCurrentPhase() + ); } /** diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java index b3edb98e..f01aaade 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java @@ -1,6 +1,5 @@ package com.codenames.codenames.backend.serialization; -import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.playingfield.GameManager; @@ -30,12 +29,11 @@ private CardDataTransferObject createCardDataTransferObject(Card card) { * Creates the game state DTO that needs to be serialized into JSON. * * @param gameManager the game manager that holds the state of the game - * @param role the role of the player who requires the DTO * @param currentTurn the current turn * @return a DTO of the current game state */ public GameStateDataTransferObject createGameStateDataTransferObject( - GameManager gameManager, Role role, Team currentTurn, Role currentPhase) { + GameManager gameManager, Team currentTurn, Role currentPhase) { List cardList = gameManager.getCardList(); List cardDataTransferObject = new ArrayList<>(); @@ -51,7 +49,9 @@ public GameStateDataTransferObject createGameStateDataTransferObject( cardDataTransferObject); } String word = gameManager.getCurrentClue().word(); - if(word == null) word = ""; + if (word == null) { + word = ""; + } int guessAmount = gameManager.getCurrentClue().guessAmount(); return new GameStateDataTransferObject( gameManager.getWinner(), diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index e9cc3547..5b848712 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -6,7 +6,6 @@ import static org.mockito.Mockito.when; import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.utility.Color; @@ -40,13 +39,13 @@ void setUp() { when(mockGameManager.getCurrentBlueFound()).thenReturn(0); gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, operative, redTeam, spymaster); + service.createGameStateDataTransferObject(mockGameManager, redTeam, spymaster); } @Test void testSpymasterVisibility() { gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, spymaster, redTeam, spymaster); + service.createGameStateDataTransferObject(mockGameManager, redTeam, spymaster); assertEquals(Color.RED, gameStateDto.cardList().get(0).color()); } @@ -69,7 +68,7 @@ void testGetWinner_exists() { void testGetWinner_null() { when(mockGameManager.getWinner()).thenReturn(null); gameStateDto = - service.createGameStateDataTransferObject(mockGameManager, operative, redTeam, operative); + service.createGameStateDataTransferObject(mockGameManager, redTeam, operative); assertNull(gameStateDto.winner()); } @@ -80,7 +79,7 @@ void testCreateGameStateDataTransferObject() { when(mockGameManager.getWinner()).thenReturn(null); when(mockGameManager.getCurrentClue()).thenReturn(clue); - GameStateDataTransferObject dto = service.createGameStateDataTransferObject(mockGameManager, operative, redTeam, operative); + GameStateDataTransferObject dto = service.createGameStateDataTransferObject(mockGameManager, redTeam, operative); assertEquals(clue.word(), dto.currentClue().word()); assertEquals(clue.guessAmount(), dto.currentClue().guessAmount()); diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index 657ae302..5f6837dd 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -6,7 +6,6 @@ import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.playingfield.Card; import com.codenames.codenames.backend.utility.Color; From 93d6cec100f0582fdf0638b9e8ac9ea7488f4841 Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Mon, 18 May 2026 21:52:53 +0200 Subject: [PATCH 144/207] refactor: remove unnecessary empty string check --- .../backend/serialization/DataTransferObjectService.java | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java index f01aaade..a282acf5 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java @@ -49,9 +49,6 @@ public GameStateDataTransferObject createGameStateDataTransferObject( cardDataTransferObject); } String word = gameManager.getCurrentClue().word(); - if (word == null) { - word = ""; - } int guessAmount = gameManager.getCurrentClue().guessAmount(); return new GameStateDataTransferObject( gameManager.getWinner(), From 6470db28e6ad3d687496d0dd49fa5b8d2929bbb5 Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Mon, 18 May 2026 21:56:15 +0200 Subject: [PATCH 145/207] refactor: remove unused import --- .../backend/serialization/GameStateDataTransferObject.java | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java index fe7e3ed1..37f18532 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java @@ -1,6 +1,5 @@ package com.codenames.codenames.backend.serialization; -import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; From aa5e9903b90cb30822dc81c28752bc9918205174 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 22:08:26 +0200 Subject: [PATCH 146/207] test: add json state store exception path coverage --- .../backend/recovery/JsonStateStoreTest.java | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java index 887a38bf..0ef83630 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java @@ -1,7 +1,7 @@ package com.codenames.codenames.backend.recovery; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import com.codenames.codenames.backend.lobby.dto.PlayerDto; @@ -118,4 +118,25 @@ void saveOverwritesPreviousSnapshot() { assertTrue(loadedSnapshot.get().lobbies().containsKey("ABCDE")); assertEquals(1, loadedSnapshot.get().lobbies().size()); } + + @Test + void saveThrowsIllegalStateWhenStateParentCannotBeCreated() throws IOException { + Path fileAsParent = tempDir.resolve("not-a-directory"); + Files.writeString(fileAsParent, "occupied"); + Path stateFile = fileAsParent.resolve("state.json"); + JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); + SystemSnapshot snapshot = + new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of(), Map.of()); + + assertThrows(IllegalStateException.class, () -> stateStore.save(snapshot)); + } + + @Test + void loadThrowsIllegalStateWhenJsonIsInvalid() throws IOException { + Path stateFile = tempDir.resolve("state.json"); + Files.writeString(stateFile, "{invalid-json"); + JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); + + assertThrows(IllegalStateException.class, stateStore::load); + } } From ef7145afef9d5f98623c8adab67290fe77f08e84 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 22:10:19 +0200 Subject: [PATCH 147/207] refactor: extract snapshot load helper in json state store test --- .../backend/recovery/JsonStateStoreTest.java | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java index 0ef83630..3156c384 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java @@ -30,7 +30,7 @@ void loadReturnsEmptyWhenStateFileDoesNotExist() { Path stateFile = tempDir.resolve("state.json"); JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); - Optional loadedSnapshot = stateStore.load(); + Optional loadedSnapshot = loadSnapshot(stateStore); assertTrue(loadedSnapshot.isEmpty()); } @@ -41,7 +41,7 @@ void loadReturnsEmptyWhenStateFileIsEmpty() throws IOException { Files.createFile(stateFile); JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); - Optional loadedSnapshot = stateStore.load(); + Optional loadedSnapshot = loadSnapshot(stateStore); assertTrue(loadedSnapshot.isEmpty()); } @@ -69,7 +69,7 @@ void saveAndLoadRoundTrip() { Map.of("ABCDE", gameSnapshot)); stateStore.save(expectedSnapshot); - Optional loadedSnapshot = stateStore.load(); + Optional loadedSnapshot = loadSnapshot(stateStore); assertTrue(loadedSnapshot.isPresent()); assertTrue(Files.exists(stateFile)); @@ -112,7 +112,7 @@ void saveOverwritesPreviousSnapshot() { stateStore.save(firstSnapshot); stateStore.save(secondSnapshot); - Optional loadedSnapshot = stateStore.load(); + Optional loadedSnapshot = loadSnapshot(stateStore); assertTrue(loadedSnapshot.isPresent()); assertTrue(loadedSnapshot.get().lobbies().containsKey("ABCDE")); @@ -139,4 +139,8 @@ void loadThrowsIllegalStateWhenJsonIsInvalid() throws IOException { assertThrows(IllegalStateException.class, stateStore::load); } + + private Optional loadSnapshot(JsonStateStore stateStore) { + return stateStore.load(); + } } From cb9d408312243ba1821163387a7159a2cd27c6d3 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 22:21:19 +0200 Subject: [PATCH 148/207] test: improve JsonStateStore coverage for getters and parentless save path --- .../backend/recovery/JsonStateStoreTest.java | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java index 3156c384..4c3081a3 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.recovery; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -140,6 +141,36 @@ void loadThrowsIllegalStateWhenJsonIsInvalid() throws IOException { assertThrows(IllegalStateException.class, stateStore::load); } + @Test + void gettersExposeConfiguredDependencies() { + ObjectMapper mapper = new ObjectMapper(); + Path stateFile = tempDir.resolve("state.json"); + JsonStateStore stateStore = new JsonStateStore(mapper, stateFile.toString()); + + assertSame(mapper, stateStore.getObjectMapper()); + assertEquals(stateFile, stateStore.getStateFilePath()); + assertSame(stateStore.getIoLock(), stateStore.getIoLock()); + } + + @Test + void saveWorksWhenStateFileHasNoParentDirectory() throws IOException { + String fileName = "json-state-store-" + System.nanoTime() + ".json"; + Path stateFile = Path.of(fileName); + JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), fileName); + SystemSnapshot snapshot = + new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of(), Map.of()); + + try { + stateStore.save(snapshot); + + assertTrue(Files.exists(stateFile)); + assertTrue(stateStore.load().isPresent()); + } finally { + Files.deleteIfExists(stateFile); + Files.deleteIfExists(Path.of(fileName + ".tmp")); + } + } + private Optional loadSnapshot(JsonStateStore stateStore) { return stateStore.load(); } From cc55c714fc30ba2fe440926ea618dcce2b474585 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 22:23:32 +0200 Subject: [PATCH 149/207] test: replace self-assertion on ioLock getter with non-null assertion --- .../codenames/backend/recovery/JsonStateStoreTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java index 4c3081a3..8f1afa69 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.recovery; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -149,7 +150,7 @@ void gettersExposeConfiguredDependencies() { assertSame(mapper, stateStore.getObjectMapper()); assertEquals(stateFile, stateStore.getStateFilePath()); - assertSame(stateStore.getIoLock(), stateStore.getIoLock()); + assertNotNull(stateStore.getIoLock()); } @Test From 36fa601473c4f57000b76a70d5ae61b82d93ad04 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 23:04:43 +0200 Subject: [PATCH 150/207] feat: add startup state recovery service --- .../recovery/SystemStateRecoveryService.java | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java diff --git a/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java b/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java new file mode 100644 index 00000000..e7a3104f --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java @@ -0,0 +1,111 @@ +package com.codenames.codenames.backend.recovery; + +import com.codenames.codenames.backend.lobby.Lobby; +import com.codenames.codenames.backend.lobby.dto.PlayerDto; +import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.playingfield.GameManager; +import com.codenames.codenames.backend.playingfield.GameManagerFactory; +import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; +import com.codenames.codenames.backend.recovery.snapshot.LobbySnapshot; +import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +@Slf4j +@Service +public class SystemStateRecoveryService { + + private final JsonStateStore stateStore; + private final LobbyService lobbyService; + private final GameService gameService; + private final GameManagerFactory gameManagerFactory; + + public SystemStateRecoveryService( + JsonStateStore stateStore, + LobbyService lobbyService, + GameService gameService, + GameManagerFactory gameManagerFactory) { + this.stateStore = stateStore; + this.lobbyService = lobbyService; + this.gameService = gameService; + this.gameManagerFactory = gameManagerFactory; + } + + @jakarta.annotation.PostConstruct + public void recoverOnStartup() { + stateStore + .load() + .ifPresent( + snapshot -> { + if (snapshot.schemaVersion() != SystemSnapshot.CURRENT_SCHEMA_VERSION) { + log.warn( + "Skipping snapshot restore due to schema mismatch. Found {}, expected {}.", + snapshot.schemaVersion(), + SystemSnapshot.CURRENT_SCHEMA_VERSION); + return; + } + restoreLobbies(snapshot.lobbies()); + restoreGames(snapshot.games()); + }); + } + + private void restoreLobbies(Map lobbySnapshots) { + if (lobbySnapshots == null || lobbySnapshots.isEmpty()) { + return; + } + for (Map.Entry entry : lobbySnapshots.entrySet()) { + Lobby restoredLobby = buildLobby(entry.getKey(), entry.getValue()); + if (restoredLobby != null) { + lobbyService.restoreLobby(entry.getKey(), restoredLobby); + } + } + } + + private void restoreGames(Map gameSnapshots) { + if (gameSnapshots == null || gameSnapshots.isEmpty()) { + return; + } + for (Map.Entry entry : gameSnapshots.entrySet()) { + GameManager restoredGame = gameManagerFactory.createFromSnapshot(entry.getValue()); + gameService.restoreGameManager(entry.getKey(), restoredGame); + } + } + + private Lobby buildLobby(String lobbyCode, LobbySnapshot snapshot) { + if (snapshot == null || snapshot.players() == null || snapshot.players().isEmpty()) { + log.warn("Skipping restore for lobby {} due to missing player data.", lobbyCode); + return null; + } + + List players = + snapshot.players().stream() + .filter(player -> player.username() != null && !player.username().isBlank()) + .sorted(Comparator.comparing(PlayerDto::isHost).reversed()) + .toList(); + + if (players.isEmpty()) { + return null; + } + + PlayerDto host = players.get(0); + Lobby lobby = new Lobby(lobbyCode, host.username()); + + for (PlayerDto player : players) { + if (!player.username().equals(host.username())) { + lobby.addPlayer(player.username(), player.isHost()); + } + if (player.team() != null) { + lobby.setPlayerTeam(player.username(), player.team()); + } + if (player.role() != null) { + lobby.setPlayerRole(player.username(), player.role()); + } + } + + return lobby; + } +} From 7dc1752d130fc5d1bb45b67c160742c57a6346dc Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 23:07:54 +0200 Subject: [PATCH 151/207] feat: add lobby and game restore hooks --- .../codenames/backend/lobby/services/LobbyService.java | 4 ++++ .../codenames/codenames/backend/playingfield/GameService.java | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 42c88f17..efe032cc 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -90,6 +90,10 @@ public boolean joinLobby(String username, String lobbyCode) { return false; } + public void restoreLobby(String lobbyCode, Lobby lobby) { + lobbyList.put(lobbyCode, lobby); + } + /** * Removes a player from a lobby. * diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index d750c385..c5af45fa 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -51,6 +51,10 @@ public void removeGame(String lobbyCode) { games.remove(lobbyCode); } + public void restoreGameManager(String lobbyCode, GameManager gameManager) { + games.put(lobbyCode, gameManager); + } + /** * Helper method to retrieve a GM object from the hash map. * From 87708a4811abfc040cd4cd28e97ef594c1c7c4fc Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 23:15:52 +0200 Subject: [PATCH 152/207] feat: add GameManager and Board constructors for snapshot restore --- .../codenames/backend/playingfield/Board.java | 5 +++ .../backend/playingfield/GameManager.java | 35 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/Board.java b/src/main/java/com/codenames/codenames/backend/playingfield/Board.java index ad38df1c..6580bdc2 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/Board.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/Board.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.playingfield; import com.codenames.codenames.backend.utility.Color; +import java.util.ArrayList; import java.util.List; import lombok.Getter; @@ -23,6 +24,10 @@ public Board( this.cardList = cardGenerator.generateCards(totalWords, red, blue, white, black); } + public Board(List cards) { + this.cardList = new ArrayList<>(cards); + } + /** * Returns the card object at the position passed. * diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index 6a35b5fa..26ad5a86 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -64,6 +64,41 @@ public GameManager( new Board(cardGenerator, TOTAL_CARDS, redCards, blueCards, WHITE_CARDS, BLACK_CARDS); } + public GameManager( + List cards, + Team currentTurn, + Role currentPhase, + Team winner, + int currentRedFound, + int currentBlueFound, + int remainingGuesses, + Clue currentClue, + ClueValidationService clueValidationService) { + if (cards == null || cards.isEmpty()) { + throw new IllegalArgumentException("cards cannot be null or empty"); + } + if (currentTurn == null || currentPhase == null) { + throw new IllegalArgumentException("current turn and phase cannot be null"); + } + + this.currentTurn = currentTurn; + this.currentPhase = currentPhase; + this.winner = winner; + this.currentRedFound = currentRedFound; + this.currentBlueFound = currentBlueFound; + this.remainingGuesses = remainingGuesses; + this.currentClue = currentClue; + this.clueValidationService = clueValidationService; + + this.redCards = countCardsByColor(cards, Color.RED); + this.blueCards = countCardsByColor(cards, Color.BLUE); + this.board = new Board(cards); + } + + private int countCardsByColor(List cards, Color color) { + return (int) cards.stream().filter(card -> card.getColor() == color).count(); + } + /** * Returns the current list of cards in a board. * From 7a170d916ce5dc1c4d6ae096be8fd8f4de3ec2b7 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 23:16:49 +0200 Subject: [PATCH 153/207] refactor: use CardDataTransferObject in game snapshots for recovery --- .../playingfield/GameManagerFactory.java | 31 +++++++++++++++++++ .../recovery/snapshot/GameSnapshot.java | 4 +-- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java index 37e10322..d1608e26 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java @@ -1,7 +1,11 @@ package com.codenames.codenames.backend.playingfield; +import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; +import com.codenames.codenames.backend.serialization.CardDataTransferObject; import com.codenames.codenames.backend.utility.Team; +import java.util.List; import org.springframework.stereotype.Component; /** Generates GameManager instances to be used by GameService. */ @@ -31,4 +35,31 @@ public GameManagerFactory( public GameManager create(Team startingTeam) { return new GameManager(startingTeam, cardGenerator, clueValidationService); } + + public GameManager createFromSnapshot(GameSnapshot snapshot) { + Clue clue = + snapshot.currentClue() == null + ? null + : new Clue(snapshot.currentClue().word(), snapshot.currentClue().guessAmount()); + List cards = snapshot.cards().stream().map(this::toCard).toList(); + + return new GameManager( + cards, + snapshot.currentTurn(), + snapshot.currentPhase(), + snapshot.winner(), + snapshot.currentRedFound(), + snapshot.currentBlueFound(), + snapshot.remainingGuesses(), + clue, + clueValidationService); + } + + private Card toCard(CardDataTransferObject cardDto) { + Card card = new Card(cardDto.word(), cardDto.color()); + if (cardDto.isGuessed()) { + card.setIsGuessedTrue(); + } + return card; + } } diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java index 779088e9..871263f7 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java @@ -1,6 +1,6 @@ package com.codenames.codenames.backend.recovery.snapshot; -import com.codenames.codenames.backend.playingfield.Card; +import com.codenames.codenames.backend.serialization.CardDataTransferObject; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.List; @@ -25,4 +25,4 @@ public record GameSnapshot( int currentBlueFound, int remainingGuesses, ClueSnapshot currentClue, - List cards) {} + List cards) {} From 078d507ed982103e7c1d75e62592d6a04c437466 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Mon, 18 May 2026 23:25:06 +0200 Subject: [PATCH 154/207] test: add startup recovery tests for SystemStateRecoveryService --- .../SystemStateRecoveryServiceTest.java | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java diff --git a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java new file mode 100644 index 00000000..9fe7f1c5 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java @@ -0,0 +1,125 @@ +package com.codenames.codenames.backend.recovery; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.codenames.codenames.backend.chat.ChatService; +import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.lobby.Lobby; +import com.codenames.codenames.backend.lobby.dto.PlayerDto; +import com.codenames.codenames.backend.lobby.services.LobbyCodeGenerator; +import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.playingfield.CardGenerator; +import com.codenames.codenames.backend.playingfield.GameManager; +import com.codenames.codenames.backend.playingfield.GameManagerFactory; +import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.recovery.snapshot.ClueSnapshot; +import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; +import com.codenames.codenames.backend.recovery.snapshot.LobbySnapshot; +import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; +import com.codenames.codenames.backend.serialization.CardDataTransferObject; +import com.codenames.codenames.backend.serialization.DataTransferObjectService; +import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.utility.Role; +import com.codenames.codenames.backend.utility.Team; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.nio.file.Path; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +class SystemStateRecoveryServiceTest { + + @TempDir Path tempDir; + + @Test + void recoverOnStartupDoesNothingWhenNoSnapshotExists() { + TestContext context = createContext(tempDir.resolve("state.json")); + + context.recoveryService().recoverOnStartup(); + + assertTrue(context.lobbyService().getLobbyList().isEmpty()); + assertFalse(context.gameService().isGameStarted("ABCDE")); + } + + @Test + void recoverOnStartupSkipsSnapshotWhenSchemaVersionDiffers() { + TestContext context = createContext(tempDir.resolve("state.json")); + SystemSnapshot snapshot = + new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION + 1, Map.of(), Map.of()); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + assertTrue(context.lobbyService().getLobbyList().isEmpty()); + assertFalse(context.gameService().isGameStarted("ABCDE")); + } + + @Test + void recoverOnStartupRestoresLobbiesAndGamesFromSnapshot() { + TestContext context = createContext(tempDir.resolve("state.json")); + LobbySnapshot lobbySnapshot = + new LobbySnapshot( + "ABCDE", + List.of( + new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), + new PlayerDto("Player", Team.BLUE, Role.OPERATIVE, false))); + GameSnapshot gameSnapshot = + new GameSnapshot( + Team.RED, + Role.OPERATIVE, + null, + 1, + 0, + 2, + new ClueSnapshot("ANIMAL", 2), + List.of( + new CardDataTransferObject("Dog", Color.RED, true), + new CardDataTransferObject("Cat", Color.BLUE, false))); + SystemSnapshot snapshot = + new SystemSnapshot( + SystemSnapshot.CURRENT_SCHEMA_VERSION, + Map.of("ABCDE", lobbySnapshot), + Map.of("ABCDE", gameSnapshot)); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + Lobby restoredLobby = context.lobbyService().getLobbyList().get("ABCDE"); + assertEquals(2, restoredLobby.getPlayerList().size()); + assertEquals(Team.RED, restoredLobby.getPlayerTeam("Host")); + assertEquals(Role.SPYMASTER, restoredLobby.getPlayerRole("Host")); + assertEquals(Team.BLUE, restoredLobby.getPlayerTeam("Player")); + assertEquals(Role.OPERATIVE, restoredLobby.getPlayerRole("Player")); + + assertTrue(context.gameService().isGameStarted("ABCDE")); + GameManager restoredGame = context.gameService().getGameState("ABCDE"); + assertEquals(Team.RED, restoredGame.getCurrentTurn()); + assertEquals(Role.OPERATIVE, restoredGame.getCurrentPhase()); + assertEquals(2, restoredGame.getRemainingGuesses()); + assertEquals("ANIMAL", restoredGame.getCurrentClue().word()); + assertTrue(restoredGame.getCardList().get(0).isGuessed()); + assertFalse(restoredGame.getCardList().get(1).isGuessed()); + } + + private TestContext createContext(Path stateFile) { + JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); + GameManagerFactory gameManagerFactory = + new GameManagerFactory(new CardGenerator("CodenamesWordlist.txt"), new ClueValidationService()); + GameService gameService = new GameService(gameManagerFactory, new DataTransferObjectService()); + LobbyService lobbyService = + new LobbyService(new LobbyCodeGenerator(), new ChatService(null), gameService); + SystemStateRecoveryService recoveryService = + new SystemStateRecoveryService(stateStore, lobbyService, gameService, gameManagerFactory); + + return new TestContext(stateStore, lobbyService, gameService, recoveryService); + } + + private record TestContext( + JsonStateStore stateStore, + LobbyService lobbyService, + GameService gameService, + SystemStateRecoveryService recoveryService) {} +} From 9b99653d86cdde4316f2a92a591104afab483909 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 00:16:32 +0200 Subject: [PATCH 155/207] test: maximize SystemStateRecoveryService branch coverage --- .../SystemStateRecoveryServiceTest.java | 155 +++++++++++++++++- 1 file changed, 154 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java index 9fe7f1c5..87f59ee7 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java @@ -2,6 +2,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; import com.codenames.codenames.backend.chat.ChatService; @@ -25,6 +26,7 @@ import com.codenames.codenames.backend.utility.Team; import com.fasterxml.jackson.databind.ObjectMapper; import java.nio.file.Path; +import java.util.HashMap; import java.util.List; import java.util.Map; import org.junit.jupiter.api.Test; @@ -104,10 +106,161 @@ void recoverOnStartupRestoresLobbiesAndGamesFromSnapshot() { assertFalse(restoredGame.getCardList().get(1).isGuessed()); } + @Test + void recoverOnStartupWithCurrentSchemaAndEmptyMapsDoesNothing() { + TestContext context = createContext(tempDir.resolve("state-empty.json")); + SystemSnapshot snapshot = + new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of(), Map.of()); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + assertTrue(context.lobbyService().getLobbyList().isEmpty()); + assertFalse(context.gameService().isGameStarted("ABCDE")); + } + + @Test + void recoverOnStartupHandlesNullLobbyAndGameMaps() { + TestContext context = createContext(tempDir.resolve("state-null-maps.json")); + SystemSnapshot snapshot = + new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, null, null); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + assertTrue(context.lobbyService().getLobbyList().isEmpty()); + assertFalse(context.gameService().isGameStarted("ABCDE")); + } + + @Test + void recoverOnStartupSkipsLobbyWhenSnapshotEntryIsNull() { + TestContext context = createContext(tempDir.resolve("state-null-lobby-entry.json")); + Map lobbies = new HashMap<>(); + lobbies.put("ABCDE", null); + SystemSnapshot snapshot = + new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, lobbies, Map.of()); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + assertTrue(context.lobbyService().getLobbyList().isEmpty()); + } + + @Test + void recoverOnStartupSkipsLobbyWhenPlayersListIsNull() { + TestContext context = createContext(tempDir.resolve("state-null-players.json")); + LobbySnapshot lobbySnapshot = new LobbySnapshot("ABCDE", null); + SystemSnapshot snapshot = + new SystemSnapshot( + SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + assertTrue(context.lobbyService().getLobbyList().isEmpty()); + } + + @Test + void recoverOnStartupSkipsLobbyWhenPlayersListIsEmpty() { + TestContext context = createContext(tempDir.resolve("state-empty-players.json")); + LobbySnapshot lobbySnapshot = new LobbySnapshot("ABCDE", List.of()); + SystemSnapshot snapshot = + new SystemSnapshot( + SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + assertTrue(context.lobbyService().getLobbyList().isEmpty()); + } + + @Test + void recoverOnStartupSkipsLobbyWhenAllUsernamesAreInvalid() { + TestContext context = createContext(tempDir.resolve("state-invalid-usernames.json")); + LobbySnapshot lobbySnapshot = + new LobbySnapshot( + "ABCDE", + List.of( + new PlayerDto(" ", Team.RED, Role.SPYMASTER, true), + new PlayerDto(null, Team.BLUE, Role.OPERATIVE, false))); + SystemSnapshot snapshot = + new SystemSnapshot( + SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + assertTrue(context.lobbyService().getLobbyList().isEmpty()); + } + + @Test + void recoverOnStartupRestoresLobbyWhenSomeTeamOrRoleValuesAreMissing() { + TestContext context = createContext(tempDir.resolve("state-missing-team-role.json")); + LobbySnapshot lobbySnapshot = + new LobbySnapshot( + "ABCDE", + List.of( + new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), + new PlayerDto("Player", null, null, false))); + SystemSnapshot snapshot = + new SystemSnapshot( + SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + Lobby restoredLobby = context.lobbyService().getLobbyList().get("ABCDE"); + assertEquals(Team.RED, restoredLobby.getPlayerTeam("Host")); + assertEquals(Role.SPYMASTER, restoredLobby.getPlayerRole("Host")); + assertNull(restoredLobby.getPlayerTeam("Player")); + assertNull(restoredLobby.getPlayerRole("Player")); + } + + @Test + void recoverOnStartupRestoresOnlyGamesWhenLobbiesMapIsNull() { + TestContext context = createContext(tempDir.resolve("state-lobbies-null-games-present.json")); + GameSnapshot gameSnapshot = + new GameSnapshot( + Team.BLUE, + Role.SPYMASTER, + null, + 0, + 0, + 0, + null, + List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); + SystemSnapshot snapshot = + new SystemSnapshot( + SystemSnapshot.CURRENT_SCHEMA_VERSION, null, Map.of("ABCDE", gameSnapshot)); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + assertTrue(context.lobbyService().getLobbyList().isEmpty()); + assertTrue(context.gameService().isGameStarted("ABCDE")); + } + + @Test + void recoverOnStartupRestoresOnlyLobbiesWhenGamesMapIsNull() { + TestContext context = createContext(tempDir.resolve("state-games-null-lobbies-present.json")); + LobbySnapshot lobbySnapshot = + new LobbySnapshot("ABCDE", List.of(new PlayerDto("Host", Team.RED, Role.SPYMASTER, true))); + SystemSnapshot snapshot = + new SystemSnapshot( + SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), null); + context.stateStore().save(snapshot); + + context.recoveryService().recoverOnStartup(); + + assertTrue(context.lobbyService().getLobbyList().containsKey("ABCDE")); + assertFalse(context.gameService().isGameStarted("ABCDE")); + } + private TestContext createContext(Path stateFile) { JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); GameManagerFactory gameManagerFactory = - new GameManagerFactory(new CardGenerator("CodenamesWordlist.txt"), new ClueValidationService()); + new GameManagerFactory( + new CardGenerator("CodenamesWordlist.txt"), new ClueValidationService()); GameService gameService = new GameService(gameManagerFactory, new DataTransferObjectService()); LobbyService lobbyService = new LobbyService(new LobbyCodeGenerator(), new ChatService(null), gameService); From 9d654bd205862a89a117b61ff639afc0bd54bd3c Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 00:59:00 +0200 Subject: [PATCH 156/207] test: add tests for recovery --- .../backend/lobby/services/LobbyService.java | 17 ++-- .../backend/playingfield/GameService.java | 7 +- .../lobby/services/LobbyServiceTest.java | 12 +++ .../backend/playingfield/BoardTest.java | 10 ++ .../playingfield/GameManagerFactoryTest.java | 51 ++++++++++ .../backend/playingfield/GameManagerTest.java | 97 +++++++++++++++++++ .../backend/playingfield/GameServiceTest.java | 53 +++++++++- 7 files changed, 229 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index efe032cc..9106cb06 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -25,8 +25,7 @@ @Service public class LobbyService { - @Getter - private final Map lobbyList = new ConcurrentHashMap<>(); + @Getter private final Map lobbyList = new ConcurrentHashMap<>(); private final LobbyCodeGenerator generator; private final GameService gameService; private final ChatService chatService; @@ -260,17 +259,16 @@ public Role getPlayerRole(String username, String lobbyCode) { } /** - * The service method for starting a game. This creates a game manager object for the lobby - * and checks if the requesting user is liable to start the game. + * The service method for starting a game. This creates a game manager object for the lobby and + * checks if the requesting user is liable to start the game. * * @param lobbyCode the unique lobby code * @param username the name of the requesting user * @return if starting was successful */ - public boolean startGame(String lobbyCode, String username) { - boolean isStarted = !lobbyCode.isBlank() && !username.isBlank() - && Objects.equals(getHost(lobbyCode), username); + boolean isStarted = + !lobbyCode.isBlank() && !username.isBlank() && Objects.equals(getHost(lobbyCode), username); Lobby lobby = lobbyList.get(lobbyCode); addGameManagerForLobby(lobby, lobbyCode); @@ -284,7 +282,6 @@ public boolean startGame(String lobbyCode, String username) { * @param lobbyCode the unique lobby code * @return the username of the host */ - public String getHost(String lobbyCode) { if (lobbyCode == null || lobbyCode.isBlank()) { return ""; @@ -302,13 +299,11 @@ public String getHost(String lobbyCode) { } /** - * Checks if the game is started by looking after an existing - * game manager object. + * Checks if the game is started by looking after an existing game manager object. * * @param lobbyCode the unique lobby code * @return whether a game manager exists (@code true or @code false) */ - public boolean getIsStarted(String lobbyCode) { return gameService.isGameStarted(lobbyCode); } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index c5af45fa..59f8d48c 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -141,13 +141,12 @@ public GameStateDto createGameStateDto(String lobbyCode) { public GameStateDataTransferObject getCurrentGameState(String lobbyCode) { GameManager gm = getGame(lobbyCode); return dtoService.createGameStateDataTransferObject( - gm, gm.getCurrentTurn(), gm.getCurrentPhase() - ); + gm, gm.getCurrentTurn(), gm.getCurrentPhase()); } /** - * This method uses the private method getGame to check if a game is already started - * via the existence of a game manager. + * This method uses the private method getGame to check if a game is already started via the + * existence of a game manager. * * @param lobbyCode the lobbyCode of the lobby * @return if the game manager for the lobby already exists aka the game is started diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index ede33cd1..46ad282b 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -16,6 +16,7 @@ import static org.mockito.Mockito.when; import com.codenames.codenames.backend.chat.ChatService; +import com.codenames.codenames.backend.lobby.Lobby; import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.playingfield.GameService; import com.codenames.codenames.backend.utility.Role; @@ -260,6 +261,7 @@ void testLobbyIsRemovedWhenItIsEmpty() { lobbyService.leaveLobby("Host", "ABCDE"); lobbyService.checkLobbyStillHasPlayers("ABCDE"); assertFalse(lobbyService.getLobbyList().containsKey("ABCDE")); + verify(gameService, times(1)).removeGame("ABCDE"); } @Test @@ -355,4 +357,14 @@ void testGetHost_ReturnsEmptyString(String lobbyCode) { assertEquals("", result); } + + @Test + void testRestoreLobbyAddsLobbyToLobbyList() { + Lobby restoredLobby = new Lobby("ABCDE", "Host"); + + lobbyService.restoreLobby("ABCDE", restoredLobby); + + assertTrue(lobbyService.getLobbyList().containsKey("ABCDE")); + assertEquals(restoredLobby, lobbyService.getLobbyList().get("ABCDE")); + } } diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java index bfde018c..1f6308c1 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/BoardTest.java @@ -11,6 +11,7 @@ import static org.mockito.Mockito.when; import com.codenames.codenames.backend.utility.Color; +import java.util.ArrayList; import java.util.Arrays; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -80,4 +81,13 @@ void testSetIsGuessed() { board.setGuessed(0); assertTrue(board.getIsGuessed(0)); } + + @Test + void testRecoveryConstructorCopiesInputList() { + List input = new ArrayList<>(dummyCardList); + Board recoveredBoard = new Board(input); + input.add(new Card("Extra", Color.RED)); + + assertEquals(4, recoveredBoard.getCardList().size()); + } } diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java index 48574950..983433ae 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java @@ -1,10 +1,19 @@ package com.codenames.codenames.backend.playingfield; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.recovery.snapshot.ClueSnapshot; +import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; +import com.codenames.codenames.backend.serialization.CardDataTransferObject; +import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -26,4 +35,46 @@ void testCreate() { assertNotNull(gameManager); } + + @Test + void testCreateFromSnapshotWithClue() { + GameSnapshot snapshot = + new GameSnapshot( + Team.RED, + Role.OPERATIVE, + null, + 1, + 0, + 2, + new ClueSnapshot("ANIMAL", 2), + List.of( + new CardDataTransferObject("Dog", Color.RED, true), + new CardDataTransferObject("Cat", Color.BLUE, false))); + + GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); + + assertNotNull(recovered); + assertTrue(recovered.getCardList().get(0).isGuessed()); + assertEquals(2, recovered.getRemainingGuesses()); + assertEquals("ANIMAL", recovered.getCurrentClueWord()); + } + + @Test + void testCreateFromSnapshotWithoutClue() { + GameSnapshot snapshot = + new GameSnapshot( + Team.BLUE, + Role.SPYMASTER, + null, + 0, + 0, + 0, + null, + List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); + + GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); + + assertNotNull(recovered); + assertNull(recovered.getCurrentClue()); + } } diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 5397a1d7..dcfa984e 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -3,6 +3,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; @@ -328,4 +329,100 @@ void testCheckCorrectTurn_throwsWhenWrongTeam() { void testPassTurn_throwsWhenSpymaster() { assertThrows(IllegalStateException.class, () -> gameManager.passTurn(redTeam)); } + + @Test + void recoveryConstructorThrowsWhenCardsAreNull() { + assertThrows( + IllegalArgumentException.class, + () -> + new GameManager( + null, + Team.RED, + Role.SPYMASTER, + null, + 0, + 0, + 0, + null, + mockClueValidationService)); + } + + @Test + void recoveryConstructorThrowsWhenCardsAreEmpty() { + assertThrows( + IllegalArgumentException.class, + () -> + new GameManager( + List.of(), + Team.RED, + Role.SPYMASTER, + null, + 0, + 0, + 0, + null, + mockClueValidationService)); + } + + @Test + void recoveryConstructorThrowsWhenCurrentTurnIsNull() { + assertThrows( + IllegalArgumentException.class, + () -> + new GameManager( + List.of(new Card("Dog", Color.RED)), + null, + Role.SPYMASTER, + null, + 0, + 0, + 0, + null, + mockClueValidationService)); + } + + @Test + void recoveryConstructorThrowsWhenCurrentPhaseIsNull() { + assertThrows( + IllegalArgumentException.class, + () -> + new GameManager( + List.of(new Card("Dog", Color.RED)), + Team.RED, + null, + null, + 0, + 0, + 0, + null, + mockClueValidationService)); + } + + @Test + void recoveryConstructorRestoresPersistedState() { + Card guessedRed = new Card("Dog", Color.RED); + guessedRed.setIsGuessedTrue(); + List cards = new ArrayList<>(); + cards.add(guessedRed); + cards.add(new Card("Cat", Color.BLUE)); + + GameManager restored = + new GameManager( + cards, + Team.BLUE, + Role.OPERATIVE, + Team.RED, + 1, + 0, + 2, + new Clue("ANIMAL", 2), + mockClueValidationService); + + assertEquals(Team.BLUE, restored.getCurrentTurn()); + assertEquals(Role.OPERATIVE, restored.getCurrentPhase()); + assertEquals(2, restored.getRemainingGuesses()); + assertEquals("ANIMAL", restored.getCurrentClueWord()); + assertEquals(Team.RED, restored.getWinner()); + assertTrue(restored.getCardList().get(0).isGuessed()); + } } diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index 9bcbaa80..5c7df39c 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -1,16 +1,23 @@ package com.codenames.codenames.backend.playingfield; -import com.codenames.codenames.backend.serialization.DataTransferObjectService; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.game.dto.GameStateDto; +import com.codenames.codenames.backend.serialization.CardDataTransferObject; +import com.codenames.codenames.backend.serialization.DataTransferObjectService; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; +import com.codenames.codenames.backend.utility.Color; +import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -21,6 +28,7 @@ class GameServiceTest { private GameService gameService; private GameManager mockGameManager; private GameManagerFactory mockGameManagerFactory; + private DataTransferObjectService mockDtoService; private final String lobbyCode = "ABCDE"; private final Team redTeam = Team.RED; @@ -29,7 +37,7 @@ class GameServiceTest { void setup() { mockGameManagerFactory = mock(GameManagerFactory.class); mockGameManager = mock(GameManager.class); - DataTransferObjectService mockDtoService = mock(DataTransferObjectService.class); + mockDtoService = mock(DataTransferObjectService.class); gameService = new GameService(mockGameManagerFactory, mockDtoService); when(mockGameManagerFactory.create(redTeam)).thenReturn(mockGameManager); @@ -58,7 +66,7 @@ void testRemoveGame() { @Test void testSubmitClue() { - Clue mockClue = mock(Clue.class); + Clue mockClue = new Clue("ANIMAL", 2); gameService.submitClue(lobbyCode, mockClue, redTeam); verify(mockGameManager, times(1)).submitClue(mockClue, redTeam); @@ -96,4 +104,43 @@ void testCreateGameStateDto() { assertNotNull(dto); } + + @Test + void testRestoreGameManager() { + String restoredLobbyCode = "VWXYZ"; + GameManager restoredGameManager = mock(GameManager.class); + + gameService.restoreGameManager(restoredLobbyCode, restoredGameManager); + + assertEquals(restoredGameManager, gameService.getGameState(restoredLobbyCode)); + } + + @Test + void testGetCurrentGameState() { + GameStateDataTransferObject expected = + new GameStateDataTransferObject( + null, + redTeam, + Role.SPYMASTER, + new ClueDto("ANIMAL", 2), + List.of(new CardDataTransferObject("Dog", Color.RED, false))); + when(mockGameManager.getCurrentTurn()).thenReturn(redTeam); + when(mockGameManager.getCurrentPhase()).thenReturn(Role.SPYMASTER); + when(mockDtoService.createGameStateDataTransferObject(mockGameManager, redTeam, Role.SPYMASTER)) + .thenReturn(expected); + + GameStateDataTransferObject result = gameService.getCurrentGameState(lobbyCode); + + assertEquals(expected, result); + } + + @Test + void testIsGameStartedWhenGameExists() { + assertTrue(gameService.isGameStarted(lobbyCode)); + } + + @Test + void testIsGameStartedWhenGameDoesNotExist() { + assertFalse(gameService.isGameStarted("UNKNOWN")); + } } From 7393f5e8a85e56818fefacc560f93eee7ace0001 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 01:01:25 +0200 Subject: [PATCH 157/207] style: reorder imports in LobbyServiceTest --- .../codenames/backend/lobby/services/LobbyServiceTest.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index 46ad282b..7b8f7875 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -5,9 +5,6 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.NullAndEmptySource; -import org.junit.jupiter.params.provider.ValueSource; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -25,6 +22,9 @@ import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; /** * Tests for {@link LobbyService}. From 885cf5e07035892ab4137293f3fc5123601ae16d Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 01:02:00 +0200 Subject: [PATCH 158/207] test: configure Mockito mock maker for local test stability --- .../resources/mockito-extensions/org.mockito.plugins.MockMaker | 1 + 1 file changed, 1 insertion(+) create mode 100644 src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker diff --git a/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker new file mode 100644 index 00000000..fdbd0b15 --- /dev/null +++ b/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker @@ -0,0 +1 @@ +mock-maker-subclass From a0a41de3f75fea47dd9703b0edd20517a891590b Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 01:04:58 +0200 Subject: [PATCH 159/207] docs: add javadocs --- .../backend/lobby/services/LobbyService.java | 6 +++++ .../codenames/backend/playingfield/Board.java | 5 ++++ .../backend/playingfield/GameManager.java | 20 ++++++++++++++ .../playingfield/GameManagerFactory.java | 12 +++++++++ .../backend/playingfield/GameService.java | 6 +++++ .../recovery/SystemStateRecoveryService.java | 27 +++++++++++++++++++ 6 files changed, 76 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 9106cb06..865bbee2 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -89,6 +89,12 @@ public boolean joinLobby(String username, String lobbyCode) { return false; } + /** + * Registers a recovered lobby into in-memory lobby storage. + * + * @param lobbyCode lobby identifier + * @param lobby recovered lobby instance + */ public void restoreLobby(String lobbyCode, Lobby lobby) { lobbyList.put(lobbyCode, lobby); } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/Board.java b/src/main/java/com/codenames/codenames/backend/playingfield/Board.java index 6580bdc2..4e9cefc6 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/Board.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/Board.java @@ -24,6 +24,11 @@ public Board( this.cardList = cardGenerator.generateCards(totalWords, red, blue, white, black); } + /** + * Constructs a board from an already existing card list (recovery path). + * + * @param cards recovered cards + */ public Board(List cards) { this.cardList = new ArrayList<>(cards); } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index 26ad5a86..678f7634 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -64,6 +64,19 @@ public GameManager( new Board(cardGenerator, TOTAL_CARDS, redCards, blueCards, WHITE_CARDS, BLACK_CARDS); } + /** + * Constructor used by recovery logic to rebuild an already running game state. + * + * @param cards recovered card list + * @param currentTurn recovered active team + * @param currentPhase recovered active phase + * @param winner recovered winner, if present + * @param currentRedFound recovered red score + * @param currentBlueFound recovered blue score + * @param remainingGuesses recovered remaining guesses + * @param currentClue recovered current clue, if present + * @param clueValidationService clue validation service + */ public GameManager( List cards, Team currentTurn, @@ -95,6 +108,13 @@ public GameManager( this.board = new Board(cards); } + /** + * Counts cards of a specific color within the current board snapshot. + * + * @param cards recovered cards + * @param color color to count + * @return number of cards with the requested color + */ private int countCardsByColor(List cards, Color color) { return (int) cards.stream().filter(card -> card.getColor() == color).count(); } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java index d1608e26..28e48009 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java @@ -36,6 +36,12 @@ public GameManager create(Team startingTeam) { return new GameManager(startingTeam, cardGenerator, clueValidationService); } + /** + * Recreates a {@link GameManager} from a persisted snapshot. + * + * @param snapshot persisted game state snapshot + * @return restored game manager + */ public GameManager createFromSnapshot(GameSnapshot snapshot) { Clue clue = snapshot.currentClue() == null @@ -55,6 +61,12 @@ public GameManager createFromSnapshot(GameSnapshot snapshot) { clueValidationService); } + /** + * Maps persisted card DTO representation to runtime {@link Card}. + * + * @param cardDto persisted card payload + * @return runtime card instance + */ private Card toCard(CardDataTransferObject cardDto) { Card card = new Card(cardDto.word(), cardDto.color()); if (cardDto.isGuessed()) { diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 59f8d48c..896f1cfd 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -51,6 +51,12 @@ public void removeGame(String lobbyCode) { games.remove(lobbyCode); } + /** + * Registers a recovered {@link GameManager} for a lobby after backend restart. + * + * @param lobbyCode lobby identifier + * @param gameManager recovered game manager + */ public void restoreGameManager(String lobbyCode, GameManager gameManager) { games.put(lobbyCode, gameManager); } diff --git a/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java b/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java index e7a3104f..7de5c165 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java @@ -15,6 +15,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; +/** Restores persisted lobbies and games into in-memory runtime services on backend startup. */ @Slf4j @Service public class SystemStateRecoveryService { @@ -24,6 +25,14 @@ public class SystemStateRecoveryService { private final GameService gameService; private final GameManagerFactory gameManagerFactory; + /** + * Creates a recovery service. + * + * @param stateStore JSON state store used for loading snapshots + * @param lobbyService lobby runtime service + * @param gameService game runtime service + * @param gameManagerFactory factory used to rebuild game managers from snapshots + */ public SystemStateRecoveryService( JsonStateStore stateStore, LobbyService lobbyService, @@ -35,6 +44,7 @@ public SystemStateRecoveryService( this.gameManagerFactory = gameManagerFactory; } + /** Loads and restores persisted state at startup when a compatible snapshot exists. */ @jakarta.annotation.PostConstruct public void recoverOnStartup() { stateStore @@ -53,6 +63,11 @@ public void recoverOnStartup() { }); } + /** + * Restores all lobby snapshots into {@link LobbyService}. + * + * @param lobbySnapshots persisted lobby snapshots keyed by lobby code + */ private void restoreLobbies(Map lobbySnapshots) { if (lobbySnapshots == null || lobbySnapshots.isEmpty()) { return; @@ -65,6 +80,11 @@ private void restoreLobbies(Map lobbySnapshots) { } } + /** + * Restores all game snapshots into {@link GameService}. + * + * @param gameSnapshots persisted game snapshots keyed by lobby code + */ private void restoreGames(Map gameSnapshots) { if (gameSnapshots == null || gameSnapshots.isEmpty()) { return; @@ -75,6 +95,13 @@ private void restoreGames(Map gameSnapshots) { } } + /** + * Builds a runtime lobby from a persisted lobby snapshot. + * + * @param lobbyCode target lobby code + * @param snapshot persisted lobby snapshot + * @return rebuilt lobby, or {@code null} when snapshot player data is invalid + */ private Lobby buildLobby(String lobbyCode, LobbySnapshot snapshot) { if (snapshot == null || snapshot.players() == null || snapshot.players().isEmpty()) { log.warn("Skipping restore for lobby {} due to missing player data.", lobbyCode); From cad75785dba90d50bfed82fb478c7a5177357b2b Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 12:12:01 +0200 Subject: [PATCH 160/207] test: replace final DTO mocks with concrete instances in controller tests --- .../controller/GameSocketControllerTest.java | 16 ++++++++++------ .../backend/websocket/GameControllerTest.java | 14 ++++++++------ 2 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index a5183a5a..eeb8f3cc 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -1,9 +1,7 @@ package com.codenames.codenames.backend.game.controller; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -12,6 +10,8 @@ import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; +import java.util.List; import com.codenames.codenames.backend.utility.Team; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,7 +43,7 @@ void startGameShouldBroadcastState() { message.setLobbyCode("ABCDE"); - when(gameService.getCurrentGameState("ABCDE")).thenReturn(mock(GameStateDataTransferObject.class)); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStateDataTransferObject()); controller.startGame(message); @@ -59,7 +59,7 @@ void revealCardShouldBroadcastState() { message.setPosition(0); message.setCurrentTurn(Team.RED); - when(gameService.getCurrentGameState("ABCDE")).thenReturn(mock(GameStateDataTransferObject.class)); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStateDataTransferObject()); controller.revealCard(message); @@ -78,7 +78,7 @@ void submitClueShouldBroadcastState() { message.setGuessAmount(2); message.setCurrentTurn(Team.RED); - when(gameService.getCurrentGameState("ABCDE")).thenReturn(mock(GameStateDataTransferObject.class)); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStateDataTransferObject()); controller.submitClue(message); @@ -95,7 +95,7 @@ void passTurnShouldBroadcastUpdatedState() { message.setLobbyCode("ABCDE"); message.setCurrentTurn(Team.RED); - when(gameService.getCurrentGameState("ABCDE")).thenReturn(mock(GameStateDataTransferObject.class)); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStateDataTransferObject()); controller.passTurn(message); @@ -103,4 +103,8 @@ void passTurnShouldBroadcastUpdatedState() { verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } + + private GameStateDataTransferObject createGameStateDataTransferObject() { + return new GameStateDataTransferObject(null, Team.RED, null, null, List.of()); + } } diff --git a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java index 1d52bc07..66ab22ce 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java @@ -9,6 +9,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; +import com.codenames.codenames.backend.game.dto.GameStateDto; import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.GameService; import java.util.List; @@ -57,8 +58,7 @@ void shouldRegisterJoinAndRegisterSession() { when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(true); when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); - when(gameService.createGameStateDto("ABCDE")) - .thenReturn(mock(com.codenames.codenames.backend.game.dto.GameStateDto.class)); + when(gameService.createGameStateDto("ABCDE")).thenReturn(createGameStateDto()); controller.join(msg, accessor); @@ -124,8 +124,7 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(true); when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); - when(gameService.createGameStateDto("ABCDE")) - .thenReturn(mock(com.codenames.codenames.backend.game.dto.GameStateDto.class)); + when(gameService.createGameStateDto("ABCDE")).thenReturn(createGameStateDto()); controller.join(msg, accessor); @@ -152,8 +151,7 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(false); when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); - when(gameService.createGameStateDto("ABCDE")) - .thenReturn(mock(com.codenames.codenames.backend.game.dto.GameStateDto.class)); + when(gameService.createGameStateDto("ABCDE")).thenReturn(createGameStateDto()); controller.join(msg, accessor); @@ -163,4 +161,8 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } + + private GameStateDto createGameStateDto() { + return new GameStateDto(List.of(), null, 0, null, null, null); + } } From 2dc0d4098564ad45878b01c13c30ca31f1f6dcc5 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 12:12:44 +0200 Subject: [PATCH 161/207] style: fix import order --- .../backend/game/controller/GameSocketControllerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index eeb8f3cc..1bc78ef5 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -11,8 +11,8 @@ import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.playingfield.GameService; import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import java.util.List; import com.codenames.codenames.backend.utility.Team; +import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; From 080726d8fd77998cd239e58b096b6bfec2b7d569 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 17:14:42 +0200 Subject: [PATCH 162/207] refactor: delete unused clue snapshot record --- .../backend/recovery/snapshot/ClueSnapshot.java | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java deleted file mode 100644 index 2e3b2fbf..00000000 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/ClueSnapshot.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.codenames.codenames.backend.recovery.snapshot; - -/** - * Persisted clue payload used for restart recovery snapshots. - * - * @param word clue word - * @param guessAmount clue guess amount - */ -public record ClueSnapshot(String word, int guessAmount) {} From a04a04a7050a669db44588bc40a2c9bb96221455 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 17:15:16 +0200 Subject: [PATCH 163/207] refactor: delete unused lobby snapshot record --- .../backend/recovery/snapshot/LobbySnapshot.java | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java deleted file mode 100644 index 992b00d9..00000000 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/LobbySnapshot.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.codenames.codenames.backend.recovery.snapshot; - -import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import java.util.List; - -/** - * Persisted lobby payload used for restart recovery snapshots. - * - * @param lobbyCode lobby identifier - * @param players players including team/role/host metadata - */ -public record LobbySnapshot(String lobbyCode, List players) {} From bd5cf0cb950a484f38ed00cb792ffa3af6c69bb8 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 17:15:59 +0200 Subject: [PATCH 164/207] refactor: use ClueDto in game snapshot --- .../codenames/backend/recovery/snapshot/GameSnapshot.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java index 871263f7..19f8b568 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.recovery.snapshot; +import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.serialization.CardDataTransferObject; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; @@ -14,7 +15,7 @@ * @param currentRedFound discovered red cards * @param currentBlueFound discovered blue cards * @param remainingGuesses remaining guesses - * @param currentClue current clue if present + * @param currentClue current clue DTO if present * @param cards full card state list */ public record GameSnapshot( @@ -24,5 +25,5 @@ public record GameSnapshot( int currentRedFound, int currentBlueFound, int remainingGuesses, - ClueSnapshot currentClue, + ClueDto currentClue, List cards) {} From b9d9f25f4fb8a20a47f84f9abdef5b00f9f54e75 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 17:16:47 +0200 Subject: [PATCH 165/207] refactor: use player dto list in system snapshot --- .../codenames/backend/recovery/snapshot/SystemSnapshot.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java index cdee2c24..0300db5e 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java @@ -1,16 +1,18 @@ package com.codenames.codenames.backend.recovery.snapshot; +import com.codenames.codenames.backend.lobby.dto.PlayerDto; +import java.util.List; import java.util.Map; /** * Root snapshot aggregate for persisted backend runtime state. * * @param schemaVersion persisted schema version - * @param lobbies lobby snapshots keyed by lobby code + * @param lobbies lobby player lists keyed by lobby code * @param games game snapshots keyed by lobby code */ public record SystemSnapshot( - int schemaVersion, Map lobbies, Map games) { + int schemaVersion, Map> lobbies, Map games) { public static final int CURRENT_SCHEMA_VERSION = 1; } From be8573105b685d32796fb38d64c1297eb13d0047 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 17:26:17 +0200 Subject: [PATCH 166/207] refactor: adapt game manager factory to dto based clue recovery --- .../codenames/backend/playingfield/GameManagerFactory.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java index 28e48009..ea91d156 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java @@ -2,6 +2,7 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; import com.codenames.codenames.backend.serialization.CardDataTransferObject; import com.codenames.codenames.backend.utility.Team; @@ -43,10 +44,9 @@ public GameManager create(Team startingTeam) { * @return restored game manager */ public GameManager createFromSnapshot(GameSnapshot snapshot) { + ClueDto clueDto = snapshot.currentClue(); Clue clue = - snapshot.currentClue() == null - ? null - : new Clue(snapshot.currentClue().word(), snapshot.currentClue().guessAmount()); + clueDto == null ? null : new Clue(clueDto.word(), clueDto.guessAmount()); List cards = snapshot.cards().stream().map(this::toCard).toList(); return new GameManager( From 746ceec64fcd2ad1b46add8d7e764441bf2934f1 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 17:32:20 +0200 Subject: [PATCH 167/207] refactor: adapt lobby recovery to dto based snapshot structure --- .../recovery/SystemStateRecoveryService.java | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java b/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java index 7de5c165..17d94eca 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java @@ -7,7 +7,6 @@ import com.codenames.codenames.backend.playingfield.GameManagerFactory; import com.codenames.codenames.backend.playingfield.GameService; import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; -import com.codenames.codenames.backend.recovery.snapshot.LobbySnapshot; import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; import java.util.Comparator; import java.util.List; @@ -66,13 +65,13 @@ public void recoverOnStartup() { /** * Restores all lobby snapshots into {@link LobbyService}. * - * @param lobbySnapshots persisted lobby snapshots keyed by lobby code + * @param lobbySnapshots persisted lobby player lists keyed by lobby code */ - private void restoreLobbies(Map lobbySnapshots) { + private void restoreLobbies(Map> lobbySnapshots) { if (lobbySnapshots == null || lobbySnapshots.isEmpty()) { return; } - for (Map.Entry entry : lobbySnapshots.entrySet()) { + for (Map.Entry> entry : lobbySnapshots.entrySet()) { Lobby restoredLobby = buildLobby(entry.getKey(), entry.getValue()); if (restoredLobby != null) { lobbyService.restoreLobby(entry.getKey(), restoredLobby); @@ -99,29 +98,29 @@ private void restoreGames(Map gameSnapshots) { * Builds a runtime lobby from a persisted lobby snapshot. * * @param lobbyCode target lobby code - * @param snapshot persisted lobby snapshot - * @return rebuilt lobby, or {@code null} when snapshot player data is invalid + * @param players persisted lobby players + * @return rebuilt lobby, or {@code null} when player data is invalid */ - private Lobby buildLobby(String lobbyCode, LobbySnapshot snapshot) { - if (snapshot == null || snapshot.players() == null || snapshot.players().isEmpty()) { + private Lobby buildLobby(String lobbyCode, List players) { + if (players == null || players.isEmpty()) { log.warn("Skipping restore for lobby {} due to missing player data.", lobbyCode); return null; } - List players = - snapshot.players().stream() + List validPlayers = + players.stream() .filter(player -> player.username() != null && !player.username().isBlank()) .sorted(Comparator.comparing(PlayerDto::isHost).reversed()) .toList(); - if (players.isEmpty()) { + if (validPlayers.isEmpty()) { return null; } - PlayerDto host = players.get(0); + PlayerDto host = validPlayers.get(0); Lobby lobby = new Lobby(lobbyCode, host.username()); - for (PlayerDto player : players) { + for (PlayerDto player : validPlayers) { if (!player.username().equals(host.username())) { lobby.addPlayer(player.username(), player.isHost()); } From 04e823b9dcfed33591059e9252258a29c2b9e671 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 17:45:55 +0200 Subject: [PATCH 168/207] test: update game manager factory recovery tests --- .../backend/playingfield/GameManagerFactoryTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java index 983433ae..ecfe86af 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java @@ -7,7 +7,7 @@ import static org.mockito.Mockito.mock; import com.codenames.codenames.backend.clue.ClueValidationService; -import com.codenames.codenames.backend.recovery.snapshot.ClueSnapshot; +import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; import com.codenames.codenames.backend.serialization.CardDataTransferObject; import com.codenames.codenames.backend.utility.Color; @@ -46,7 +46,7 @@ void testCreateFromSnapshotWithClue() { 1, 0, 2, - new ClueSnapshot("ANIMAL", 2), + new ClueDto("ANIMAL", 2), List.of( new CardDataTransferObject("Dog", Color.RED, true), new CardDataTransferObject("Cat", Color.BLUE, false))); From 5d05310cb691a93fd4730c03412320e7415dbb2d Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 17:48:39 +0200 Subject: [PATCH 169/207] test: update json state store tests for dto snapshots --- .../backend/recovery/JsonStateStoreTest.java | 32 ++++++++----------- 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java index 8f1afa69..b83dcea4 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java @@ -6,10 +6,9 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.recovery.snapshot.ClueSnapshot; import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; -import com.codenames.codenames.backend.recovery.snapshot.LobbySnapshot; import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; @@ -53,21 +52,19 @@ void saveAndLoadRoundTrip() { Path stateFile = tempDir.resolve("state.json"); JsonStateStore stateStore = new JsonStateStore(new ObjectMapper(), stateFile.toString()); - LobbySnapshot lobbySnapshot = - new LobbySnapshot( - "ABCDE", - List.of( - new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), - new PlayerDto("Player", Team.BLUE, Role.OPERATIVE, false))); + List lobbyPlayers = + List.of( + new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), + new PlayerDto("Player", Team.BLUE, Role.OPERATIVE, false)); GameSnapshot gameSnapshot = new GameSnapshot( - Team.RED, Role.OPERATIVE, null, 1, 0, 2, new ClueSnapshot("ANIMAL", 2), List.of()); + Team.RED, Role.OPERATIVE, null, 1, 0, 2, new ClueDto("ANIMAL", 2), List.of()); SystemSnapshot expectedSnapshot = new SystemSnapshot( SystemSnapshot.CURRENT_SCHEMA_VERSION, - Map.of("ABCDE", lobbySnapshot), + Map.of("ABCDE", lobbyPlayers), Map.of("ABCDE", gameSnapshot)); stateStore.save(expectedSnapshot); @@ -81,13 +78,12 @@ void saveAndLoadRoundTrip() { assertEquals(1, actualSnapshot.lobbies().size()); assertEquals(1, actualSnapshot.games().size()); - LobbySnapshot actualLobbySnapshot = actualSnapshot.lobbies().get("ABCDE"); - assertEquals("ABCDE", actualLobbySnapshot.lobbyCode()); - assertEquals(2, actualLobbySnapshot.players().size()); - assertEquals("Host", actualLobbySnapshot.players().get(0).username()); - assertEquals(Team.RED, actualLobbySnapshot.players().get(0).team()); - assertEquals(Role.SPYMASTER, actualLobbySnapshot.players().get(0).role()); - assertTrue(actualLobbySnapshot.players().get(0).isHost()); + List actualPlayers = actualSnapshot.lobbies().get("ABCDE"); + assertEquals(2, actualPlayers.size()); + assertEquals("Host", actualPlayers.get(0).username()); + assertEquals(Team.RED, actualPlayers.get(0).team()); + assertEquals(Role.SPYMASTER, actualPlayers.get(0).role()); + assertTrue(actualPlayers.get(0).isHost()); GameSnapshot actualGameSnapshot = actualSnapshot.games().get("ABCDE"); assertEquals(Team.RED, actualGameSnapshot.currentTurn()); @@ -108,7 +104,7 @@ void saveOverwritesPreviousSnapshot() { SystemSnapshot secondSnapshot = new SystemSnapshot( SystemSnapshot.CURRENT_SCHEMA_VERSION, - Map.of("ABCDE", new LobbySnapshot("ABCDE", List.of())), + Map.of("ABCDE", List.of()), Map.of()); stateStore.save(firstSnapshot); From 510688ec2b01923659fa18b121c891fe7fd3ee5b Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 17:48:50 +0200 Subject: [PATCH 170/207] test: update system state recovery tests for dto snapshots --- .../SystemStateRecoveryServiceTest.java | 53 ++++++++----------- 1 file changed, 22 insertions(+), 31 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java index 87f59ee7..32c9602b 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java @@ -7,6 +7,7 @@ import com.codenames.codenames.backend.chat.ChatService; import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.lobby.Lobby; import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyCodeGenerator; @@ -15,9 +16,7 @@ import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.playingfield.GameManagerFactory; import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.snapshot.ClueSnapshot; import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; -import com.codenames.codenames.backend.recovery.snapshot.LobbySnapshot; import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; import com.codenames.codenames.backend.serialization.CardDataTransferObject; import com.codenames.codenames.backend.serialization.DataTransferObjectService; @@ -62,12 +61,10 @@ void recoverOnStartupSkipsSnapshotWhenSchemaVersionDiffers() { @Test void recoverOnStartupRestoresLobbiesAndGamesFromSnapshot() { TestContext context = createContext(tempDir.resolve("state.json")); - LobbySnapshot lobbySnapshot = - new LobbySnapshot( - "ABCDE", - List.of( - new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), - new PlayerDto("Player", Team.BLUE, Role.OPERATIVE, false))); + List lobbyPlayers = + List.of( + new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), + new PlayerDto("Player", Team.BLUE, Role.OPERATIVE, false)); GameSnapshot gameSnapshot = new GameSnapshot( Team.RED, @@ -76,14 +73,14 @@ void recoverOnStartupRestoresLobbiesAndGamesFromSnapshot() { 1, 0, 2, - new ClueSnapshot("ANIMAL", 2), + new ClueDto("ANIMAL", 2), List.of( new CardDataTransferObject("Dog", Color.RED, true), new CardDataTransferObject("Cat", Color.BLUE, false))); SystemSnapshot snapshot = new SystemSnapshot( SystemSnapshot.CURRENT_SCHEMA_VERSION, - Map.of("ABCDE", lobbySnapshot), + Map.of("ABCDE", lobbyPlayers), Map.of("ABCDE", gameSnapshot)); context.stateStore().save(snapshot); @@ -122,8 +119,7 @@ void recoverOnStartupWithCurrentSchemaAndEmptyMapsDoesNothing() { @Test void recoverOnStartupHandlesNullLobbyAndGameMaps() { TestContext context = createContext(tempDir.resolve("state-null-maps.json")); - SystemSnapshot snapshot = - new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, null, null); + SystemSnapshot snapshot = new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, null, null); context.stateStore().save(snapshot); context.recoveryService().recoverOnStartup(); @@ -135,7 +131,7 @@ void recoverOnStartupHandlesNullLobbyAndGameMaps() { @Test void recoverOnStartupSkipsLobbyWhenSnapshotEntryIsNull() { TestContext context = createContext(tempDir.resolve("state-null-lobby-entry.json")); - Map lobbies = new HashMap<>(); + Map> lobbies = new HashMap<>(); lobbies.put("ABCDE", null); SystemSnapshot snapshot = new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, lobbies, Map.of()); @@ -149,10 +145,10 @@ void recoverOnStartupSkipsLobbyWhenSnapshotEntryIsNull() { @Test void recoverOnStartupSkipsLobbyWhenPlayersListIsNull() { TestContext context = createContext(tempDir.resolve("state-null-players.json")); - LobbySnapshot lobbySnapshot = new LobbySnapshot("ABCDE", null); + Map> lobbies = new HashMap<>(); + lobbies.put("ABCDE", null); SystemSnapshot snapshot = - new SystemSnapshot( - SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); + new SystemSnapshot(SystemSnapshot.CURRENT_SCHEMA_VERSION, lobbies, Map.of()); context.stateStore().save(snapshot); context.recoveryService().recoverOnStartup(); @@ -163,7 +159,7 @@ void recoverOnStartupSkipsLobbyWhenPlayersListIsNull() { @Test void recoverOnStartupSkipsLobbyWhenPlayersListIsEmpty() { TestContext context = createContext(tempDir.resolve("state-empty-players.json")); - LobbySnapshot lobbySnapshot = new LobbySnapshot("ABCDE", List.of()); + List lobbySnapshot = List.of(); SystemSnapshot snapshot = new SystemSnapshot( SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); @@ -177,12 +173,10 @@ void recoverOnStartupSkipsLobbyWhenPlayersListIsEmpty() { @Test void recoverOnStartupSkipsLobbyWhenAllUsernamesAreInvalid() { TestContext context = createContext(tempDir.resolve("state-invalid-usernames.json")); - LobbySnapshot lobbySnapshot = - new LobbySnapshot( - "ABCDE", - List.of( - new PlayerDto(" ", Team.RED, Role.SPYMASTER, true), - new PlayerDto(null, Team.BLUE, Role.OPERATIVE, false))); + List lobbySnapshot = + List.of( + new PlayerDto(" ", Team.RED, Role.SPYMASTER, true), + new PlayerDto(null, Team.BLUE, Role.OPERATIVE, false)); SystemSnapshot snapshot = new SystemSnapshot( SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); @@ -196,12 +190,10 @@ void recoverOnStartupSkipsLobbyWhenAllUsernamesAreInvalid() { @Test void recoverOnStartupRestoresLobbyWhenSomeTeamOrRoleValuesAreMissing() { TestContext context = createContext(tempDir.resolve("state-missing-team-role.json")); - LobbySnapshot lobbySnapshot = - new LobbySnapshot( - "ABCDE", - List.of( - new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), - new PlayerDto("Player", null, null, false))); + List lobbySnapshot = + List.of( + new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), + new PlayerDto("Player", null, null, false)); SystemSnapshot snapshot = new SystemSnapshot( SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), Map.of()); @@ -243,8 +235,7 @@ void recoverOnStartupRestoresOnlyGamesWhenLobbiesMapIsNull() { @Test void recoverOnStartupRestoresOnlyLobbiesWhenGamesMapIsNull() { TestContext context = createContext(tempDir.resolve("state-games-null-lobbies-present.json")); - LobbySnapshot lobbySnapshot = - new LobbySnapshot("ABCDE", List.of(new PlayerDto("Host", Team.RED, Role.SPYMASTER, true))); + List lobbySnapshot = List.of(new PlayerDto("Host", Team.RED, Role.SPYMASTER, true)); SystemSnapshot snapshot = new SystemSnapshot( SystemSnapshot.CURRENT_SCHEMA_VERSION, Map.of("ABCDE", lobbySnapshot), null); From 7b8d7628f8620432b3c7269aed014e1dff6b2275 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:08:55 +0200 Subject: [PATCH 171/207] test: simplify assertThrows lambdas in recovery constructor tests --- .../backend/playingfield/GameManagerTest.java | 47 ++++--------------- 1 file changed, 10 insertions(+), 37 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index dcfa984e..2ecd375f 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -336,66 +336,39 @@ void recoveryConstructorThrowsWhenCardsAreNull() { IllegalArgumentException.class, () -> new GameManager( - null, - Team.RED, - Role.SPYMASTER, - null, - 0, - 0, - 0, - null, - mockClueValidationService)); + null, Team.RED, Role.SPYMASTER, null, 0, 0, 0, null, mockClueValidationService)); } @Test void recoveryConstructorThrowsWhenCardsAreEmpty() { + List cards = List.of(); + assertThrows( IllegalArgumentException.class, () -> new GameManager( - List.of(), - Team.RED, - Role.SPYMASTER, - null, - 0, - 0, - 0, - null, - mockClueValidationService)); + cards, Team.RED, Role.SPYMASTER, null, 0, 0, 0, null, mockClueValidationService)); } @Test void recoveryConstructorThrowsWhenCurrentTurnIsNull() { + List cards = List.of(new Card("Dog", Color.RED)); + assertThrows( IllegalArgumentException.class, () -> new GameManager( - List.of(new Card("Dog", Color.RED)), - null, - Role.SPYMASTER, - null, - 0, - 0, - 0, - null, - mockClueValidationService)); + cards, null, Role.SPYMASTER, null, 0, 0, 0, null, mockClueValidationService)); } @Test void recoveryConstructorThrowsWhenCurrentPhaseIsNull() { + List cards = List.of(new Card("Dog", Color.RED)); + assertThrows( IllegalArgumentException.class, () -> - new GameManager( - List.of(new Card("Dog", Color.RED)), - Team.RED, - null, - null, - 0, - 0, - 0, - null, - mockClueValidationService)); + new GameManager(cards, Team.RED, null, null, 0, 0, 0, null, mockClueValidationService)); } @Test From 9278789454032eb03761cf6f5c772f87a90844e5 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:27:26 +0200 Subject: [PATCH 172/207] refactor: add remaining guesses to game state transfer object --- .../backend/serialization/GameStateDataTransferObject.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java index 37f18532..54eb2c7c 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java @@ -10,7 +10,9 @@ * * @param winner the winner * @param currentTurn the current team who is allowed to make a move + * @param currentPhase the current phase (spymaster or operative) * @param currentClue the current clue object, consisting of word and amount of guesses + * @param remainingGuesses amount of guesses left for the current turn * @param cardList the cards on the board */ public record GameStateDataTransferObject( @@ -18,4 +20,5 @@ public record GameStateDataTransferObject( Team currentTurn, Role currentPhase, ClueDto currentClue, + int remainingGuesses, List cardList) {} From f7f4d0c90e45a59a1aaf80f9edcf771782dc7a45 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:28:29 +0200 Subject: [PATCH 173/207] refactor: map remaining guesses in dto service --- .../backend/serialization/DataTransferObjectService.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java index a282acf5..d1dfb13c 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java @@ -33,7 +33,7 @@ private CardDataTransferObject createCardDataTransferObject(Card card) { * @return a DTO of the current game state */ public GameStateDataTransferObject createGameStateDataTransferObject( - GameManager gameManager, Team currentTurn, Role currentPhase) { + GameManager gameManager, Team currentTurn, Role currentPhase) { List cardList = gameManager.getCardList(); List cardDataTransferObject = new ArrayList<>(); @@ -46,6 +46,7 @@ public GameStateDataTransferObject createGameStateDataTransferObject( currentTurn, currentPhase, null, + gameManager.getRemainingGuesses(), cardDataTransferObject); } String word = gameManager.getCurrentClue().word(); @@ -55,6 +56,7 @@ public GameStateDataTransferObject createGameStateDataTransferObject( currentTurn, currentPhase, new ClueDto(word, guessAmount), + gameManager.getRemainingGuesses(), cardDataTransferObject); } } From 1240f532f88eee93c3aa0c300355f862da7973c3 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:29:55 +0200 Subject: [PATCH 174/207] refactor: remove legacy game state dto creation from game service --- .../backend/playingfield/GameService.java | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 896f1cfd..4488f70d 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -1,7 +1,6 @@ package com.codenames.codenames.backend.playingfield; import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.game.dto.GameStateDto; import com.codenames.codenames.backend.serialization.DataTransferObjectService; import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.utility.Team; @@ -119,25 +118,6 @@ public void passTurn(String lobbyCode, Team callingTeam) { gm.passTurn(callingTeam); } - /** - * Creates a DTO representing the current game state. - * - * @param lobbyCode lobby identifier - * @return DTO containing board and turn information - */ - public GameStateDto createGameStateDto(String lobbyCode) { - - GameManager gm = getGame(lobbyCode); - - return new GameStateDto( - gm.getCardList(), - gm.getCurrentClue(), - gm.getRemainingGuesses(), - gm.getWinner(), - gm.getCurrentTurn(), - gm.getCurrentPhase()); - } - /** * Maps the current game state into a @link GameStateTransferObject. * From 23e1358c17e41547f433bdb03e0b0e092b095efe Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:31:54 +0200 Subject: [PATCH 175/207] refactor: use unified game state payload in game controller --- .../codenames/codenames/backend/websocket/GameController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java index b386568f..db30cfc2 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java @@ -101,6 +101,6 @@ private void sendPlayerUpdate(String code) { * @param code the lobby code identifying the game */ private void sendGameStateUpdate(String code) { - messagingTemplate.convertAndSend("/topic/game/" + code, gameService.createGameStateDto(code)); + messagingTemplate.convertAndSend("/topic/game/" + code, gameService.getCurrentGameState(code)); } } From 79b01a8169fd2d1f7d3b61307e696ee8b61eab82 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:32:15 +0200 Subject: [PATCH 176/207] refactor: remove unused game state dto record --- .../backend/game/dto/GameStateDto.java | 19 ------------------- 1 file changed, 19 deletions(-) delete mode 100644 src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java diff --git a/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java b/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java deleted file mode 100644 index 010b8ef3..00000000 --- a/src/main/java/com/codenames/codenames/backend/game/dto/GameStateDto.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.codenames.codenames.backend.game.dto; - -import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.playingfield.Card; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import java.util.List; - -/** - * DTO representing the current game state. - */ -public record GameStateDto( - List cards, - Clue currentClue, - int remainingGuesses, - Team winner, - Team currentTurn, - Role currentPhase -) {} From 6abec7644f0a3c6ecc3bf036d58ac9b8cc282a83 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:35:19 +0200 Subject: [PATCH 177/207] test: update game service tests for unified payload --- .../backend/playingfield/GameServiceTest.java | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index 5c7df39c..6389d6b9 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -2,7 +2,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; @@ -12,7 +11,6 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.game.dto.GameStateDto; import com.codenames.codenames.backend.serialization.CardDataTransferObject; import com.codenames.codenames.backend.serialization.DataTransferObjectService; import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; @@ -95,16 +93,6 @@ void testGetGameState() { assertEquals(mockGameManager, result); } - @Test - void testCreateGameStateDto() { - - when(mockGameManager.getCardList()).thenReturn(List.of()); - - GameStateDto dto = gameService.createGameStateDto(lobbyCode); - - assertNotNull(dto); - } - @Test void testRestoreGameManager() { String restoredLobbyCode = "VWXYZ"; @@ -123,9 +111,11 @@ void testGetCurrentGameState() { redTeam, Role.SPYMASTER, new ClueDto("ANIMAL", 2), + 2, List.of(new CardDataTransferObject("Dog", Color.RED, false))); when(mockGameManager.getCurrentTurn()).thenReturn(redTeam); when(mockGameManager.getCurrentPhase()).thenReturn(Role.SPYMASTER); + when(mockGameManager.getRemainingGuesses()).thenReturn(2); when(mockDtoService.createGameStateDataTransferObject(mockGameManager, redTeam, Role.SPYMASTER)) .thenReturn(expected); From 82481fce7c30f381b5751900a9c9fe0253b78ebd Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:36:39 +0200 Subject: [PATCH 178/207] test: update game controller tests for unified payload --- .../backend/websocket/GameControllerTest.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java index 66ab22ce..a9637955 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java @@ -9,7 +9,7 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.game.dto.GameStateDto; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.GameService; import java.util.List; @@ -58,7 +58,7 @@ void shouldRegisterJoinAndRegisterSession() { when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(true); when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); - when(gameService.createGameStateDto("ABCDE")).thenReturn(createGameStateDto()); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStatePayload()); controller.join(msg, accessor); @@ -124,7 +124,7 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(true); when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); - when(gameService.createGameStateDto("ABCDE")).thenReturn(createGameStateDto()); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStatePayload()); controller.join(msg, accessor); @@ -151,7 +151,7 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { when(lobbyService.joinLobby("Max", "ABCDE")).thenReturn(false); when(lobbyService.getPlayers("ABCDE")).thenReturn(List.of(new Player("Max", true))); - when(gameService.createGameStateDto("ABCDE")).thenReturn(createGameStateDto()); + when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStatePayload()); controller.join(msg, accessor); @@ -162,7 +162,7 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } - private GameStateDto createGameStateDto() { - return new GameStateDto(List.of(), null, 0, null, null, null); + private GameStateDataTransferObject createGameStatePayload() { + return new GameStateDataTransferObject(null, null, null, null, 0, List.of()); } } From 414b1893db175b7005770b38832747b0a8e11ec3 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:38:50 +0200 Subject: [PATCH 179/207] test: update game socket controller test payload constructor --- .../backend/game/controller/GameSocketControllerTest.java | 2 +- .../codenames/backend/websocket/GameControllerTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 1bc78ef5..a5166d2e 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -105,6 +105,6 @@ void passTurnShouldBroadcastUpdatedState() { } private GameStateDataTransferObject createGameStateDataTransferObject() { - return new GameStateDataTransferObject(null, Team.RED, null, null, List.of()); + return new GameStateDataTransferObject(null, Team.RED, null, null, 0, List.of()); } } diff --git a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java index a9637955..192a9d79 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java @@ -9,9 +9,9 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import java.util.List; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; From 27e903c5eef04212cc576080c55fa9ca8fd90b67 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:39:38 +0200 Subject: [PATCH 180/207] test: update serialization dto service tests for remaining guesses --- .../serialization/DataTransferObjectServiceTest.java | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 5b848712..0097b0c0 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -37,6 +37,7 @@ void setUp() { when(mockGameManager.getWinner()).thenReturn(Team.RED); when(mockGameManager.getCurrentRedFound()).thenReturn(0); when(mockGameManager.getCurrentBlueFound()).thenReturn(0); + when(mockGameManager.getRemainingGuesses()).thenReturn(2); gameStateDto = service.createGameStateDataTransferObject(mockGameManager, redTeam, spymaster); @@ -78,8 +79,10 @@ void testCreateGameStateDataTransferObject() { when(mockGameManager.getCardList()).thenReturn(List.of()); when(mockGameManager.getWinner()).thenReturn(null); when(mockGameManager.getCurrentClue()).thenReturn(clue); + when(mockGameManager.getRemainingGuesses()).thenReturn(3); - GameStateDataTransferObject dto = service.createGameStateDataTransferObject(mockGameManager, redTeam, operative); + GameStateDataTransferObject dto = + service.createGameStateDataTransferObject(mockGameManager, redTeam, operative); assertEquals(clue.word(), dto.currentClue().word()); assertEquals(clue.guessAmount(), dto.currentClue().guessAmount()); @@ -87,5 +90,6 @@ void testCreateGameStateDataTransferObject() { assertEquals(0, dto.cardList().size()); assertEquals(redTeam, dto.currentTurn()); assertEquals(operative, dto.currentPhase()); + assertEquals(3, dto.remainingGuesses()); } } From ccfb59c472d059418d4fdf82218f2f5d2ff060ed Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Tue, 19 May 2026 19:40:27 +0200 Subject: [PATCH 181/207] test: update serialization dto service tests for remaining guesses --- .../backend/serialization/SerializationJsonTest.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index 5f6837dd..a82de2b7 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -34,14 +34,15 @@ void setUp() { dummyList = List.of(new CardDataTransferObject("TEST", null, false)); dummyGameState = - new GameStateDataTransferObject(redTeam, redTeam, spymaster, new ClueDto("Test", 1), dummyList); + new GameStateDataTransferObject( + redTeam, redTeam, spymaster, new ClueDto("Test", 1), 1, dummyList); } @Test void testSerialize_pass() { String expectedResult = """ - {"winner":"RED","currentTurn":"RED","currentPhase":"SPYMASTER","currentClue":{"word":"Test","guessAmount":1},"cardList":[{"word":"TEST","color":null,"isGuessed":false}]}""" + {"winner":"RED","currentTurn":"RED","currentPhase":"SPYMASTER","currentClue":{"word":"Test","guessAmount":1},"remainingGuesses":1,"cardList":[{"word":"TEST","color":null,"isGuessed":false}]}""" ; String result = serializer.serialize(dummyGameState); assertEquals(expectedResult, result); From c9af76c25ce15dac804da18d6bd6f187645a3111 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 00:06:10 +0200 Subject: [PATCH 182/207] refactor: unify game recovery state dto --- .../backend/playingfield/GameManager.java | 44 ++++++------------- .../playingfield/GameManagerFactory.java | 38 +++++++++------- .../playingfield/GameRecoveryState.java | 28 ++++++++++++ .../recovery/SystemStateRecoveryService.java | 8 ++-- .../recovery/snapshot/GameSnapshot.java | 29 ------------ .../recovery/snapshot/SystemSnapshot.java | 9 ++-- 6 files changed, 75 insertions(+), 81 deletions(-) create mode 100644 src/main/java/com/codenames/codenames/backend/playingfield/GameRecoveryState.java delete mode 100644 src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index 678f7634..f4908d34 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -67,45 +67,29 @@ public GameManager( /** * Constructor used by recovery logic to rebuild an already running game state. * - * @param cards recovered card list - * @param currentTurn recovered active team - * @param currentPhase recovered active phase - * @param winner recovered winner, if present - * @param currentRedFound recovered red score - * @param currentBlueFound recovered blue score - * @param remainingGuesses recovered remaining guesses - * @param currentClue recovered current clue, if present + * @param state bundled recovery state * @param clueValidationService clue validation service */ - public GameManager( - List cards, - Team currentTurn, - Role currentPhase, - Team winner, - int currentRedFound, - int currentBlueFound, - int remainingGuesses, - Clue currentClue, - ClueValidationService clueValidationService) { - if (cards == null || cards.isEmpty()) { + public GameManager(GameRecoveryState state, ClueValidationService clueValidationService) { + if (state.cards() == null || state.cards().isEmpty()) { throw new IllegalArgumentException("cards cannot be null or empty"); } - if (currentTurn == null || currentPhase == null) { + if (state.currentTurn() == null || state.currentPhase() == null) { throw new IllegalArgumentException("current turn and phase cannot be null"); } - this.currentTurn = currentTurn; - this.currentPhase = currentPhase; - this.winner = winner; - this.currentRedFound = currentRedFound; - this.currentBlueFound = currentBlueFound; - this.remainingGuesses = remainingGuesses; - this.currentClue = currentClue; + this.currentTurn = state.currentTurn(); + this.currentPhase = state.currentPhase(); + this.winner = state.winner(); + this.currentRedFound = state.currentRedFound(); + this.currentBlueFound = state.currentBlueFound(); + this.remainingGuesses = state.remainingGuesses(); + this.currentClue = state.currentClue(); this.clueValidationService = clueValidationService; - this.redCards = countCardsByColor(cards, Color.RED); - this.blueCards = countCardsByColor(cards, Color.BLUE); - this.board = new Board(cards); + this.redCards = countCardsByColor(state.cards(), Color.RED); + this.blueCards = countCardsByColor(state.cards(), Color.BLUE); + this.board = new Board(state.cards()); } /** diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java index ea91d156..72178362 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java @@ -3,8 +3,9 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; import com.codenames.codenames.backend.serialization.CardDataTransferObject; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; +import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Team; import java.util.List; import org.springframework.stereotype.Component; @@ -43,22 +44,24 @@ public GameManager create(Team startingTeam) { * @param snapshot persisted game state snapshot * @return restored game manager */ - public GameManager createFromSnapshot(GameSnapshot snapshot) { + public GameManager createFromSnapshot(GameStateDataTransferObject snapshot) { ClueDto clueDto = snapshot.currentClue(); - Clue clue = - clueDto == null ? null : new Clue(clueDto.word(), clueDto.guessAmount()); - List cards = snapshot.cards().stream().map(this::toCard).toList(); + Clue clue = clueDto == null ? null : new Clue(clueDto.word(), clueDto.guessAmount()); + List cards = snapshot.cardList().stream().map(this::toCard).toList(); + int recoveredRedFound = countGuessedByColor(cards, Color.RED); + int recoveredBlueFound = countGuessedByColor(cards, Color.BLUE); + GameRecoveryState recoveryState = + new GameRecoveryState( + cards, + snapshot.currentTurn(), + snapshot.currentPhase(), + snapshot.winner(), + recoveredRedFound, + recoveredBlueFound, + snapshot.remainingGuesses(), + clue); - return new GameManager( - cards, - snapshot.currentTurn(), - snapshot.currentPhase(), - snapshot.winner(), - snapshot.currentRedFound(), - snapshot.currentBlueFound(), - snapshot.remainingGuesses(), - clue, - clueValidationService); + return new GameManager(recoveryState, clueValidationService); } /** @@ -74,4 +77,9 @@ private Card toCard(CardDataTransferObject cardDto) { } return card; } + + private int countGuessedByColor(List cards, Color color) { + return (int) + cards.stream().filter(card -> card.isGuessed() && card.getColor() == color).count(); + } } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameRecoveryState.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameRecoveryState.java new file mode 100644 index 00000000..3f7693c1 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameRecoveryState.java @@ -0,0 +1,28 @@ +package com.codenames.codenames.backend.playingfield; + +import com.codenames.codenames.backend.clue.Clue; +import com.codenames.codenames.backend.utility.Role; +import com.codenames.codenames.backend.utility.Team; +import java.util.List; + +/** + * Compact recovery payload used to rebuild a {@link GameManager} after restart. + * + * @param cards recovered board cards + * @param currentTurn recovered active team + * @param currentPhase recovered active role phase + * @param winner recovered winner if game already ended + * @param currentRedFound recovered count of discovered red cards + * @param currentBlueFound recovered count of discovered blue cards + * @param remainingGuesses recovered remaining guesses + * @param currentClue recovered clue if present + */ +public record GameRecoveryState( + List cards, + Team currentTurn, + Role currentPhase, + Team winner, + int currentRedFound, + int currentBlueFound, + int remainingGuesses, + Clue currentClue) {} diff --git a/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java b/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java index 17d94eca..1425196d 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryService.java @@ -6,8 +6,8 @@ import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.playingfield.GameManagerFactory; import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import java.util.Comparator; import java.util.List; import java.util.Map; @@ -82,13 +82,13 @@ private void restoreLobbies(Map> lobbySnapshots) { /** * Restores all game snapshots into {@link GameService}. * - * @param gameSnapshots persisted game snapshots keyed by lobby code + * @param gameSnapshots persisted game states keyed by lobby code */ - private void restoreGames(Map gameSnapshots) { + private void restoreGames(Map gameSnapshots) { if (gameSnapshots == null || gameSnapshots.isEmpty()) { return; } - for (Map.Entry entry : gameSnapshots.entrySet()) { + for (Map.Entry entry : gameSnapshots.entrySet()) { GameManager restoredGame = gameManagerFactory.createFromSnapshot(entry.getValue()); gameService.restoreGameManager(entry.getKey(), restoredGame); } diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java deleted file mode 100644 index 19f8b568..00000000 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/GameSnapshot.java +++ /dev/null @@ -1,29 +0,0 @@ -package com.codenames.codenames.backend.recovery.snapshot; - -import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.serialization.CardDataTransferObject; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import java.util.List; - -/** - * Persisted game payload used for restart recovery snapshots. - * - * @param currentTurn current team turn - * @param currentPhase current gameplay phase - * @param winner winner if already decided - * @param currentRedFound discovered red cards - * @param currentBlueFound discovered blue cards - * @param remainingGuesses remaining guesses - * @param currentClue current clue DTO if present - * @param cards full card state list - */ -public record GameSnapshot( - Team currentTurn, - Role currentPhase, - Team winner, - int currentRedFound, - int currentBlueFound, - int remainingGuesses, - ClueDto currentClue, - List cards) {} diff --git a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java index 0300db5e..3ae69bb4 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/snapshot/SystemSnapshot.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.recovery.snapshot; import com.codenames.codenames.backend.lobby.dto.PlayerDto; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import java.util.List; import java.util.Map; @@ -9,10 +10,12 @@ * * @param schemaVersion persisted schema version * @param lobbies lobby player lists keyed by lobby code - * @param games game snapshots keyed by lobby code + * @param games game states keyed by lobby code */ public record SystemSnapshot( - int schemaVersion, Map> lobbies, Map games) { + int schemaVersion, + Map> lobbies, + Map games) { - public static final int CURRENT_SCHEMA_VERSION = 1; + public static final int CURRENT_SCHEMA_VERSION = 2; } From 6c242f7604e865bc976469be6fa1dac199859624 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 00:06:54 +0200 Subject: [PATCH 183/207] test: update recovery tests for unified game state dto --- .../playingfield/GameManagerFactoryTest.java | 24 ++++------- .../backend/playingfield/GameManagerTest.java | 43 ++++++++----------- .../backend/recovery/JsonStateStoreTest.java | 12 +++--- .../SystemStateRecoveryServiceTest.java | 20 ++++----- .../serialization/SerializationJsonTest.java | 5 +-- 5 files changed, 41 insertions(+), 63 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java index ecfe86af..5a4143ff 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java @@ -8,8 +8,8 @@ import com.codenames.codenames.backend.clue.ClueValidationService; import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; import com.codenames.codenames.backend.serialization.CardDataTransferObject; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; @@ -38,15 +38,13 @@ void testCreate() { @Test void testCreateFromSnapshotWithClue() { - GameSnapshot snapshot = - new GameSnapshot( + GameStateDataTransferObject snapshot = + new GameStateDataTransferObject( + null, Team.RED, Role.OPERATIVE, - null, - 1, - 0, - 2, new ClueDto("ANIMAL", 2), + 2, List.of( new CardDataTransferObject("Dog", Color.RED, true), new CardDataTransferObject("Cat", Color.BLUE, false))); @@ -61,15 +59,9 @@ void testCreateFromSnapshotWithClue() { @Test void testCreateFromSnapshotWithoutClue() { - GameSnapshot snapshot = - new GameSnapshot( - Team.BLUE, - Role.SPYMASTER, - null, - 0, - 0, - 0, - null, + GameStateDataTransferObject snapshot = + new GameStateDataTransferObject( + null, Team.BLUE, Role.SPYMASTER, null, 0, List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 2ecd375f..644f4dbe 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -332,43 +332,41 @@ void testPassTurn_throwsWhenSpymaster() { @Test void recoveryConstructorThrowsWhenCardsAreNull() { + GameRecoveryState state = + new GameRecoveryState(null, Team.RED, Role.SPYMASTER, null, 0, 0, 0, null); + assertThrows( - IllegalArgumentException.class, - () -> - new GameManager( - null, Team.RED, Role.SPYMASTER, null, 0, 0, 0, null, mockClueValidationService)); + IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); } @Test void recoveryConstructorThrowsWhenCardsAreEmpty() { List cards = List.of(); + GameRecoveryState state = + new GameRecoveryState(cards, Team.RED, Role.SPYMASTER, null, 0, 0, 0, null); assertThrows( - IllegalArgumentException.class, - () -> - new GameManager( - cards, Team.RED, Role.SPYMASTER, null, 0, 0, 0, null, mockClueValidationService)); + IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); } @Test void recoveryConstructorThrowsWhenCurrentTurnIsNull() { List cards = List.of(new Card("Dog", Color.RED)); + GameRecoveryState state = + new GameRecoveryState(cards, null, Role.SPYMASTER, null, 0, 0, 0, null); assertThrows( - IllegalArgumentException.class, - () -> - new GameManager( - cards, null, Role.SPYMASTER, null, 0, 0, 0, null, mockClueValidationService)); + IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); } @Test void recoveryConstructorThrowsWhenCurrentPhaseIsNull() { List cards = List.of(new Card("Dog", Color.RED)); + GameRecoveryState state = + new GameRecoveryState(cards, Team.RED, null, null, 0, 0, 0, null); assertThrows( - IllegalArgumentException.class, - () -> - new GameManager(cards, Team.RED, null, null, 0, 0, 0, null, mockClueValidationService)); + IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); } @Test @@ -379,17 +377,10 @@ void recoveryConstructorRestoresPersistedState() { cards.add(guessedRed); cards.add(new Card("Cat", Color.BLUE)); - GameManager restored = - new GameManager( - cards, - Team.BLUE, - Role.OPERATIVE, - Team.RED, - 1, - 0, - 2, - new Clue("ANIMAL", 2), - mockClueValidationService); + GameRecoveryState state = + new GameRecoveryState( + cards, Team.BLUE, Role.OPERATIVE, Team.RED, 1, 0, 2, new Clue("ANIMAL", 2)); + GameManager restored = new GameManager(state, mockClueValidationService); assertEquals(Team.BLUE, restored.getCurrentTurn()); assertEquals(Role.OPERATIVE, restored.getCurrentPhase()); diff --git a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java index b83dcea4..86abe926 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java @@ -8,8 +8,8 @@ import com.codenames.codenames.backend.game.dto.ClueDto; import com.codenames.codenames.backend.lobby.dto.PlayerDto; -import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.fasterxml.jackson.databind.ObjectMapper; @@ -57,9 +57,9 @@ void saveAndLoadRoundTrip() { new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), new PlayerDto("Player", Team.BLUE, Role.OPERATIVE, false)); - GameSnapshot gameSnapshot = - new GameSnapshot( - Team.RED, Role.OPERATIVE, null, 1, 0, 2, new ClueDto("ANIMAL", 2), List.of()); + GameStateDataTransferObject gameSnapshot = + new GameStateDataTransferObject( + null, Team.RED, Role.OPERATIVE, new ClueDto("ANIMAL", 2), 2, List.of()); SystemSnapshot expectedSnapshot = new SystemSnapshot( @@ -85,13 +85,13 @@ void saveAndLoadRoundTrip() { assertEquals(Role.SPYMASTER, actualPlayers.get(0).role()); assertTrue(actualPlayers.get(0).isHost()); - GameSnapshot actualGameSnapshot = actualSnapshot.games().get("ABCDE"); + GameStateDataTransferObject actualGameSnapshot = actualSnapshot.games().get("ABCDE"); assertEquals(Team.RED, actualGameSnapshot.currentTurn()); assertEquals(Role.OPERATIVE, actualGameSnapshot.currentPhase()); assertEquals(2, actualGameSnapshot.remainingGuesses()); assertEquals("ANIMAL", actualGameSnapshot.currentClue().word()); assertEquals(2, actualGameSnapshot.currentClue().guessAmount()); - assertTrue(actualGameSnapshot.cards().isEmpty()); + assertTrue(actualGameSnapshot.cardList().isEmpty()); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java index 32c9602b..30a757ba 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java @@ -16,10 +16,10 @@ import com.codenames.codenames.backend.playingfield.GameManager; import com.codenames.codenames.backend.playingfield.GameManagerFactory; import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.snapshot.GameSnapshot; import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; import com.codenames.codenames.backend.serialization.CardDataTransferObject; import com.codenames.codenames.backend.serialization.DataTransferObjectService; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; @@ -65,15 +65,13 @@ void recoverOnStartupRestoresLobbiesAndGamesFromSnapshot() { List.of( new PlayerDto("Host", Team.RED, Role.SPYMASTER, true), new PlayerDto("Player", Team.BLUE, Role.OPERATIVE, false)); - GameSnapshot gameSnapshot = - new GameSnapshot( + GameStateDataTransferObject gameSnapshot = + new GameStateDataTransferObject( + null, Team.RED, Role.OPERATIVE, - null, - 1, - 0, - 2, new ClueDto("ANIMAL", 2), + 2, List.of( new CardDataTransferObject("Dog", Color.RED, true), new CardDataTransferObject("Cat", Color.BLUE, false))); @@ -211,15 +209,13 @@ void recoverOnStartupRestoresLobbyWhenSomeTeamOrRoleValuesAreMissing() { @Test void recoverOnStartupRestoresOnlyGamesWhenLobbiesMapIsNull() { TestContext context = createContext(tempDir.resolve("state-lobbies-null-games-present.json")); - GameSnapshot gameSnapshot = - new GameSnapshot( + GameStateDataTransferObject gameSnapshot = + new GameStateDataTransferObject( + null, Team.BLUE, Role.SPYMASTER, null, 0, - 0, - 0, - null, List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); SystemSnapshot snapshot = new SystemSnapshot( diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index a82de2b7..403ee7f2 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -41,9 +41,8 @@ void setUp() { @Test void testSerialize_pass() { String expectedResult = - """ - {"winner":"RED","currentTurn":"RED","currentPhase":"SPYMASTER","currentClue":{"word":"Test","guessAmount":1},"remainingGuesses":1,"cardList":[{"word":"TEST","color":null,"isGuessed":false}]}""" - ; + """ + {"winner":"RED","currentTurn":"RED","currentPhase":"SPYMASTER","currentClue":{"word":"Test","guessAmount":1},"remainingGuesses":1,"cardList":[{"word":"TEST","color":null,"isGuessed":false}]}"""; String result = serializer.serialize(dummyGameState); assertEquals(expectedResult, result); } From 4b8387d4b9b53cfafe2a2022656a50eee3728f55 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 00:34:50 +0200 Subject: [PATCH 184/207] refactor: remove redundant game recovery state --- .../backend/playingfield/GameManager.java | 38 +++++++++++++---- .../playingfield/GameManagerFactory.java | 42 +------------------ 2 files changed, 31 insertions(+), 49 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index f4908d34..ee000c4b 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -2,6 +2,8 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.serialization.CardDataTransferObject; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; @@ -70,8 +72,9 @@ public GameManager( * @param state bundled recovery state * @param clueValidationService clue validation service */ - public GameManager(GameRecoveryState state, ClueValidationService clueValidationService) { - if (state.cards() == null || state.cards().isEmpty()) { + public GameManager( + GameStateDataTransferObject state, ClueValidationService clueValidationService) { + if (state.cardList() == null || state.cardList().isEmpty()) { throw new IllegalArgumentException("cards cannot be null or empty"); } if (state.currentTurn() == null || state.currentPhase() == null) { @@ -81,15 +84,34 @@ public GameManager(GameRecoveryState state, ClueValidationService clueValidation this.currentTurn = state.currentTurn(); this.currentPhase = state.currentPhase(); this.winner = state.winner(); - this.currentRedFound = state.currentRedFound(); - this.currentBlueFound = state.currentBlueFound(); this.remainingGuesses = state.remainingGuesses(); - this.currentClue = state.currentClue(); + this.currentClue = + state.currentClue() == null + ? null + : new Clue(state.currentClue().word(), state.currentClue().guessAmount()); this.clueValidationService = clueValidationService; - this.redCards = countCardsByColor(state.cards(), Color.RED); - this.blueCards = countCardsByColor(state.cards(), Color.BLUE); - this.board = new Board(state.cards()); + List cards = state.cardList().stream().map(this::toCard).toList(); + + this.currentRedFound = countGuessedByColor(cards, Color.RED); + this.currentBlueFound = countGuessedByColor(cards, Color.BLUE); + + this.redCards = countCardsByColor(cards, Color.RED); + this.blueCards = countCardsByColor(cards, Color.BLUE); + this.board = new Board(cards); + } + + private Card toCard(CardDataTransferObject cardDto) { + Card card = new Card(cardDto.word(), cardDto.color()); + if (cardDto.isGuessed()) { + card.setIsGuessedTrue(); + } + return card; + } + + private int countGuessedByColor(List cards, Color color) { + return (int) + cards.stream().filter(card -> card.isGuessed() && card.getColor() == color).count(); } /** diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java index 72178362..63b468ab 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManagerFactory.java @@ -1,13 +1,8 @@ package com.codenames.codenames.backend.playingfield; -import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.clue.ClueValidationService; -import com.codenames.codenames.backend.game.dto.ClueDto; -import com.codenames.codenames.backend.serialization.CardDataTransferObject; import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; -import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Team; -import java.util.List; import org.springframework.stereotype.Component; /** Generates GameManager instances to be used by GameService. */ @@ -45,41 +40,6 @@ public GameManager create(Team startingTeam) { * @return restored game manager */ public GameManager createFromSnapshot(GameStateDataTransferObject snapshot) { - ClueDto clueDto = snapshot.currentClue(); - Clue clue = clueDto == null ? null : new Clue(clueDto.word(), clueDto.guessAmount()); - List cards = snapshot.cardList().stream().map(this::toCard).toList(); - int recoveredRedFound = countGuessedByColor(cards, Color.RED); - int recoveredBlueFound = countGuessedByColor(cards, Color.BLUE); - GameRecoveryState recoveryState = - new GameRecoveryState( - cards, - snapshot.currentTurn(), - snapshot.currentPhase(), - snapshot.winner(), - recoveredRedFound, - recoveredBlueFound, - snapshot.remainingGuesses(), - clue); - - return new GameManager(recoveryState, clueValidationService); - } - - /** - * Maps persisted card DTO representation to runtime {@link Card}. - * - * @param cardDto persisted card payload - * @return runtime card instance - */ - private Card toCard(CardDataTransferObject cardDto) { - Card card = new Card(cardDto.word(), cardDto.color()); - if (cardDto.isGuessed()) { - card.setIsGuessedTrue(); - } - return card; - } - - private int countGuessedByColor(List cards, Color color) { - return (int) - cards.stream().filter(card -> card.isGuessed() && card.getColor() == color).count(); + return new GameManager(snapshot, clueValidationService); } } From 5c74a0c8e3be77dbacd97a69588413978e9d10a6 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 00:36:45 +0200 Subject: [PATCH 185/207] refactor: remove GameRecoveryState and use game state dto directly --- .../playingfield/GameRecoveryState.java | 28 ------------------- 1 file changed, 28 deletions(-) delete mode 100644 src/main/java/com/codenames/codenames/backend/playingfield/GameRecoveryState.java diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameRecoveryState.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameRecoveryState.java deleted file mode 100644 index 3f7693c1..00000000 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameRecoveryState.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.codenames.codenames.backend.playingfield; - -import com.codenames.codenames.backend.clue.Clue; -import com.codenames.codenames.backend.utility.Role; -import com.codenames.codenames.backend.utility.Team; -import java.util.List; - -/** - * Compact recovery payload used to rebuild a {@link GameManager} after restart. - * - * @param cards recovered board cards - * @param currentTurn recovered active team - * @param currentPhase recovered active role phase - * @param winner recovered winner if game already ended - * @param currentRedFound recovered count of discovered red cards - * @param currentBlueFound recovered count of discovered blue cards - * @param remainingGuesses recovered remaining guesses - * @param currentClue recovered clue if present - */ -public record GameRecoveryState( - List cards, - Team currentTurn, - Role currentPhase, - Team winner, - int currentRedFound, - int currentBlueFound, - int remainingGuesses, - Clue currentClue) {} From b516914809a5750876534e4a0bfbf6941cbf3828 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 00:37:05 +0200 Subject: [PATCH 186/207] test: update recovery tests for direct game state dto --- .../playingfield/GameManagerFactoryTest.java | 53 +++++++++++++++++-- .../backend/playingfield/GameManagerTest.java | 48 ++++++++++------- 2 files changed, 77 insertions(+), 24 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java index 5a4143ff..0fdce572 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java @@ -40,8 +40,8 @@ void testCreate() { void testCreateFromSnapshotWithClue() { GameStateDataTransferObject snapshot = new GameStateDataTransferObject( - null, Team.RED, + Team.BLUE, Role.OPERATIVE, new ClueDto("ANIMAL", 2), 2, @@ -52,21 +52,66 @@ void testCreateFromSnapshotWithClue() { GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); assertNotNull(recovered); - assertTrue(recovered.getCardList().get(0).isGuessed()); - assertEquals(2, recovered.getRemainingGuesses()); + assertEquals(Team.BLUE, recovered.getCurrentTurn()); + assertEquals(Role.OPERATIVE, recovered.getCurrentPhase()); assertEquals("ANIMAL", recovered.getCurrentClueWord()); + assertEquals(2, recovered.getRemainingGuesses()); + assertEquals(1, recovered.getCurrentRedFound()); + assertEquals(0, recovered.getCurrentBlueFound()); + assertEquals(Team.RED, recovered.getWinner()); + assertTrue(recovered.getCardList().get(0).isGuessed()); } @Test void testCreateFromSnapshotWithoutClue() { GameStateDataTransferObject snapshot = new GameStateDataTransferObject( - null, Team.BLUE, Role.SPYMASTER, null, 0, + null, + Team.BLUE, + Role.SPYMASTER, + null, + 0, List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); assertNotNull(recovered); + assertEquals(Team.BLUE, recovered.getCurrentTurn()); + assertEquals(Role.SPYMASTER, recovered.getCurrentPhase()); assertNull(recovered.getCurrentClue()); } + + @Test + void testCreateFromSnapshotMapsCardsAndCountsGuessedCardsByColor() { + GameStateDataTransferObject snapshot = + new GameStateDataTransferObject( + null, + Team.RED, + Role.OPERATIVE, + null, + 1, + List.of( + new CardDataTransferObject("Dog", Color.RED, true), + new CardDataTransferObject("Cat", Color.RED, false), + new CardDataTransferObject("Tree", Color.BLUE, true), + new CardDataTransferObject("Sun", Color.NEUTRAL, true))); + + GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); + + assertEquals(4, recovered.getCardList().size()); + + assertEquals("Dog", recovered.getCardList().get(0).getWord()); + assertEquals(Color.RED, recovered.getCardList().get(0).getColor()); + assertTrue(recovered.getCardList().get(0).isGuessed()); + + assertEquals("Cat", recovered.getCardList().get(1).getWord()); + assertEquals(Color.RED, recovered.getCardList().get(1).getColor()); + + assertEquals("Tree", recovered.getCardList().get(2).getWord()); + assertEquals(Color.BLUE, recovered.getCardList().get(2).getColor()); + assertTrue(recovered.getCardList().get(2).isGuessed()); + + assertEquals(1, recovered.getCurrentRedFound()); + assertEquals(1, recovered.getCurrentBlueFound()); + } } diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 644f4dbe..40e934c7 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -14,6 +14,9 @@ import com.codenames.codenames.backend.clue.Clue; import com.codenames.codenames.backend.clue.ClueValidationService; +import com.codenames.codenames.backend.game.dto.ClueDto; +import com.codenames.codenames.backend.serialization.CardDataTransferObject; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.utility.Color; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; @@ -332,8 +335,8 @@ void testPassTurn_throwsWhenSpymaster() { @Test void recoveryConstructorThrowsWhenCardsAreNull() { - GameRecoveryState state = - new GameRecoveryState(null, Team.RED, Role.SPYMASTER, null, 0, 0, 0, null); + GameStateDataTransferObject state = + new GameStateDataTransferObject(null, Team.RED, Role.SPYMASTER, null, 0, null); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -341,9 +344,8 @@ void recoveryConstructorThrowsWhenCardsAreNull() { @Test void recoveryConstructorThrowsWhenCardsAreEmpty() { - List cards = List.of(); - GameRecoveryState state = - new GameRecoveryState(cards, Team.RED, Role.SPYMASTER, null, 0, 0, 0, null); + GameStateDataTransferObject state = + new GameStateDataTransferObject(null, Team.RED, Role.SPYMASTER, null, 0, List.of()); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -351,9 +353,11 @@ void recoveryConstructorThrowsWhenCardsAreEmpty() { @Test void recoveryConstructorThrowsWhenCurrentTurnIsNull() { - List cards = List.of(new Card("Dog", Color.RED)); - GameRecoveryState state = - new GameRecoveryState(cards, null, Role.SPYMASTER, null, 0, 0, 0, null); + List cards = + List.of(new CardDataTransferObject("Dog", Color.RED, false)); + + GameStateDataTransferObject state = + new GameStateDataTransferObject(null, null, Role.SPYMASTER, null, 0, cards); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -361,9 +365,11 @@ void recoveryConstructorThrowsWhenCurrentTurnIsNull() { @Test void recoveryConstructorThrowsWhenCurrentPhaseIsNull() { - List cards = List.of(new Card("Dog", Color.RED)); - GameRecoveryState state = - new GameRecoveryState(cards, Team.RED, null, null, 0, 0, 0, null); + List cards = + List.of(new CardDataTransferObject("Dog", Color.RED, false)); + + GameStateDataTransferObject state = + new GameStateDataTransferObject(null, Team.RED, null, null, 0, cards); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -371,15 +377,15 @@ void recoveryConstructorThrowsWhenCurrentPhaseIsNull() { @Test void recoveryConstructorRestoresPersistedState() { - Card guessedRed = new Card("Dog", Color.RED); - guessedRed.setIsGuessedTrue(); - List cards = new ArrayList<>(); - cards.add(guessedRed); - cards.add(new Card("Cat", Color.BLUE)); - - GameRecoveryState state = - new GameRecoveryState( - cards, Team.BLUE, Role.OPERATIVE, Team.RED, 1, 0, 2, new Clue("ANIMAL", 2)); + List cards = + List.of( + new CardDataTransferObject("Dog", Color.RED, true), + new CardDataTransferObject("Cat", Color.BLUE, false)); + + GameStateDataTransferObject state = + new GameStateDataTransferObject( + Team.RED, Team.BLUE, Role.OPERATIVE, new ClueDto("ANIMAL", 2), 2, cards); + GameManager restored = new GameManager(state, mockClueValidationService); assertEquals(Team.BLUE, restored.getCurrentTurn()); @@ -387,6 +393,8 @@ void recoveryConstructorRestoresPersistedState() { assertEquals(2, restored.getRemainingGuesses()); assertEquals("ANIMAL", restored.getCurrentClueWord()); assertEquals(Team.RED, restored.getWinner()); + assertEquals(1, restored.getCurrentRedFound()); + assertEquals(0, restored.getCurrentBlueFound()); assertTrue(restored.getCardList().get(0).isGuessed()); } } From ada50ff263bd000407a82d51586b4563bcad352d Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 01:35:46 +0200 Subject: [PATCH 187/207] feat: add snapshot retrieval for lobby and game state --- .../backend/lobby/services/LobbyService.java | 10 ++++++++++ .../codenames/backend/playingfield/GameService.java | 11 +++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index 865bbee2..a078e8cc 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -313,4 +313,14 @@ public String getHost(String lobbyCode) { public boolean getIsStarted(String lobbyCode) { return gameService.isGameStarted(lobbyCode); } + + /** + * Returns all lobbies as serializable snapshots. + * + * @return map of lobby codes to player dto lists + */ + public Map> getLobbySnapshots() { + return lobbyList.keySet().stream() + .collect(java.util.stream.Collectors.toMap(lobbyCode -> lobbyCode, this::getPlayersDto)); + } } diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java index 4488f70d..a3ec0b52 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameService.java @@ -145,4 +145,15 @@ public boolean isGameStarted(String lobbyCode) { return false; } } + + /** + * Returns all active games as serializable snapshots. + * + * @return map of lobby codes to game state dto snapshots + */ + public Map getGameSnapshots() { + return games.keySet().stream() + .collect( + java.util.stream.Collectors.toMap(lobbyCode -> lobbyCode, this::getCurrentGameState)); + } } From 7301272e4f3d5572a5af15f89800a9495072b0a8 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 01:36:42 +0200 Subject: [PATCH 188/207] feat: add system state persistence service --- .../SystemStatePersistenceService.java | 33 +++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java diff --git a/src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java b/src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java new file mode 100644 index 00000000..8a630672 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java @@ -0,0 +1,33 @@ +package com.codenames.codenames.backend.recovery; + +import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; +import org.springframework.stereotype.Service; + +@Service +public class SystemStatePersistenceService { + + private final JsonStateStore stateStore; + private final LobbyService lobbyService; + private final GameService gameService; + + public SystemStatePersistenceService( + JsonStateStore stateStore, LobbyService lobbyService, GameService gameService) { + + this.stateStore = stateStore; + this.lobbyService = lobbyService; + this.gameService = gameService; + } + + public void persistCurrentState() { + + SystemSnapshot snapshot = + new SystemSnapshot( + SystemSnapshot.CURRENT_SCHEMA_VERSION, + lobbyService.getLobbySnapshots(), + gameService.getGameSnapshots()); + + stateStore.save(snapshot); + } +} From 7c9bbc15641c271067cc87e518b3ac17e3bf51ee Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 01:37:08 +0200 Subject: [PATCH 189/207] test: add persistence service and snapshot tests --- .../lobby/services/LobbyServiceTest.java | 23 +++++++++ .../backend/playingfield/GameServiceTest.java | 23 +++++++++ .../SystemStatePersistenceServiceTest.java | 49 +++++++++++++++++++ 3 files changed, 95 insertions(+) create mode 100644 src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index 7b8f7875..60dba667 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -20,6 +20,7 @@ import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.Player; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -367,4 +368,26 @@ void testRestoreLobbyAddsLobbyToLobbyList() { assertTrue(lobbyService.getLobbyList().containsKey("ABCDE")); assertEquals(restoredLobby, lobbyService.getLobbyList().get("ABCDE")); } + + @Test + void getLobbySnapshotsShouldReturnAllLobbyPlayerDtos() { + lobbyService.createLobby("Host"); + lobbyService.joinLobby("Player", "ABCDE"); + lobbyService.selectPosition("Host", "ABCDE", Team.RED, Role.SPYMASTER); + + Map> snapshots = lobbyService.getLobbySnapshots(); + + assertTrue(snapshots.containsKey("ABCDE")); + assertEquals(2, snapshots.get("ABCDE").size()); + + PlayerDto host = + snapshots.get("ABCDE").stream() + .filter(player -> player.username().equals("Host")) + .findFirst() + .orElseThrow(); + + assertEquals(Team.RED, host.team()); + assertEquals(Role.SPYMASTER, host.role()); + assertTrue(host.isHost()); + } } diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index 6389d6b9..d1e17882 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -18,6 +18,7 @@ import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import java.util.List; +import java.util.Map; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -133,4 +134,26 @@ void testIsGameStartedWhenGameExists() { void testIsGameStartedWhenGameDoesNotExist() { assertFalse(gameService.isGameStarted("UNKNOWN")); } + + @Test + void getGameSnapshotsShouldReturnAllCurrentGameStates() { + GameStateDataTransferObject expected = + new GameStateDataTransferObject( + null, + redTeam, + Role.SPYMASTER, + new ClueDto("ANIMAL", 2), + 2, + List.of(new CardDataTransferObject("Dog", Color.RED, false))); + + when(mockGameManager.getCurrentTurn()).thenReturn(redTeam); + when(mockGameManager.getCurrentPhase()).thenReturn(Role.SPYMASTER); + when(mockDtoService.createGameStateDataTransferObject(mockGameManager, redTeam, Role.SPYMASTER)) + .thenReturn(expected); + + Map snapshots = gameService.getGameSnapshots(); + + assertEquals(1, snapshots.size()); + assertEquals(expected, snapshots.get(lobbyCode)); + } } diff --git a/src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java b/src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java new file mode 100644 index 00000000..e2d2c0f9 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java @@ -0,0 +1,49 @@ +package com.codenames.codenames.backend.recovery; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.codenames.codenames.backend.lobby.dto.PlayerDto; +import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; +import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Test; +import org.mockito.ArgumentCaptor; + +class SystemStatePersistenceServiceTest { + + @Test + void persistCurrentStateSavesCurrentSystemSnapshot() { + JsonStateStore stateStore = mock(JsonStateStore.class); + LobbyService lobbyService = mock(LobbyService.class); + GameService gameService = mock(GameService.class); + + Map> lobbySnapshots = Map.of("ABCDE", List.of()); + + Map gameSnapshots = Map.of(); + + when(lobbyService.getLobbySnapshots()).thenReturn(lobbySnapshots); + when(gameService.getGameSnapshots()).thenReturn(gameSnapshots); + + SystemStatePersistenceService persistenceService = + new SystemStatePersistenceService(stateStore, lobbyService, gameService); + + persistenceService.persistCurrentState(); + + ArgumentCaptor snapshotCaptor = ArgumentCaptor.forClass(SystemSnapshot.class); + + verify(stateStore, times(1)).save(snapshotCaptor.capture()); + + SystemSnapshot snapshot = snapshotCaptor.getValue(); + + org.junit.jupiter.api.Assertions.assertEquals( + SystemSnapshot.CURRENT_SCHEMA_VERSION, snapshot.schemaVersion()); + org.junit.jupiter.api.Assertions.assertEquals(lobbySnapshots, snapshot.lobbies()); + org.junit.jupiter.api.Assertions.assertEquals(gameSnapshots, snapshot.games()); + } +} From 2848ef16db1b3dc8d46b6fbcff48529527e1c1d8 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 01:38:22 +0200 Subject: [PATCH 190/207] docs: add javadocs for system state persistence service --- .../SystemStatePersistenceService.java | 21 ++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java b/src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java index 8a630672..17bf2406 100644 --- a/src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java +++ b/src/main/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceService.java @@ -5,6 +5,12 @@ import com.codenames.codenames.backend.recovery.snapshot.SystemSnapshot; import org.springframework.stereotype.Service; +/** + * Persists the current runtime state of the backend. + * + *

Collects lobby and game snapshots and stores them through the configured {@link + * JsonStateStore} to enable recovery after application restart. + */ @Service public class SystemStatePersistenceService { @@ -12,14 +18,27 @@ public class SystemStatePersistenceService { private final LobbyService lobbyService; private final GameService gameService; + /** + * Creates a persistence service responsible for storing backend state snapshots. + * + * @param stateStore JSON storage implementation + * @param lobbyService service providing lobby snapshots + * @param gameService service providing game snapshots + */ public SystemStatePersistenceService( - JsonStateStore stateStore, LobbyService lobbyService, GameService gameService) { + JsonStateStore stateStore, LobbyService lobbyService, GameService gameService) { this.stateStore = stateStore; this.lobbyService = lobbyService; this.gameService = gameService; } + /** + * Persists the current backend state. + * + *

Creates a {@link SystemSnapshot} containing all active lobbies and games and stores it using + * the configured {@link JsonStateStore}. + */ public void persistCurrentState() { SystemSnapshot snapshot = From 8d4bf230ec2f01142858432a468f99bf016b76b6 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 01:44:29 +0200 Subject: [PATCH 191/207] refactor: persist system state after lobby and gameplay actions --- .../game/controller/GameSocketController.java | 13 ++++++++++++- .../backend/lobby/controller/LobbyController.java | 13 +++++++++++-- .../codenames/backend/websocket/GameController.java | 8 +++++++- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index b9ec4969..c688b7bb 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -6,6 +6,7 @@ import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; import org.springframework.stereotype.Controller; @@ -22,6 +23,7 @@ public class GameSocketController { private final GameService gameService; private final SimpMessagingTemplate messagingTemplate; + private final SystemStatePersistenceService persistenceService; private static final String GAME_TOPIC_PREFIX = "/topic/game/"; @@ -30,11 +32,16 @@ public class GameSocketController { * * @param gameService service responsible for gameplay logic * @param messagingTemplate template used for broadcasting websocket messages + * @param persistenceService service used to persist current backend state */ - public GameSocketController(GameService gameService, SimpMessagingTemplate messagingTemplate) { + public GameSocketController( + GameService gameService, + SimpMessagingTemplate messagingTemplate, + SystemStatePersistenceService persistenceService) { this.gameService = gameService; this.messagingTemplate = messagingTemplate; + this.persistenceService = persistenceService; } /** @@ -44,6 +51,7 @@ public GameSocketController(GameService gameService, SimpMessagingTemplate messa */ @MessageMapping("/start-game") public void startGame(StartGameMessage message) { + persistenceService.persistCurrentState(); messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), @@ -61,6 +69,7 @@ public void startGame(StartGameMessage message) { public void revealCard(RevealCardMessage message) { gameService.flipCard(message.getLobbyCode(), message.getPosition(), message.getCurrentTurn()); + persistenceService.persistCurrentState(); messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), @@ -81,6 +90,7 @@ public void submitClue(ClueMessage message) { message.getLobbyCode(), new Clue(message.getWord(), message.getGuessAmount()), message.getCurrentTurn()); + persistenceService.persistCurrentState(); messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), @@ -96,6 +106,7 @@ public void submitClue(ClueMessage message) { public void passTurn(PassTurnMessage message) { gameService.passTurn(message.getLobbyCode(), message.getCurrentTurn()); + persistenceService.persistCurrentState(); messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 2f73b52f..2efadfac 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -3,6 +3,7 @@ import com.codenames.codenames.backend.lobby.dto.LobbyResponse; import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; import java.util.List; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; @@ -25,15 +26,18 @@ public class LobbyController { private final LobbyService service; + private final SystemStatePersistenceService persistenceService; private static final String LOBBY_NOT_FOUND = "Could not find lobby."; /** * Creates a new {@code LobbyController}. * * @param service the lobby service used to handle business logic + * @param persistenceService service used to persist current backend state */ - public LobbyController(LobbyService service) { + public LobbyController(LobbyService service, SystemStatePersistenceService persistenceService) { this.service = service; + this.persistenceService = persistenceService; } /** @@ -50,6 +54,7 @@ public ResponseEntity createLobby(@RequestParam String username) return ResponseEntity.internalServerError() .body(new LobbyResponse("Error while creating lobby.", "", null, false)); } else { + persistenceService.persistCurrentState(); List players = service.getPlayersDto(lobbyCode); return ResponseEntity.ok( new LobbyResponse("Successfully created Lobby.", lobbyCode, players, false) @@ -69,6 +74,7 @@ public ResponseEntity joinLobby( @RequestParam String username, @PathVariable String lobbyCode) { boolean joined = service.joinLobby(username, lobbyCode); if (joined) { + persistenceService.persistCurrentState(); return ResponseEntity.ok( new LobbyResponse( "Joined Lobby successfully.", @@ -96,6 +102,8 @@ public ResponseEntity leaveLobby( @RequestParam String username) { boolean left = service.leaveLobby(username, lobbyCode); if (left) { + service.checkLobbyStillHasPlayers(lobbyCode); + persistenceService.persistCurrentState(); ResponseEntity response = ResponseEntity.ok( new LobbyResponse( "Left lobby successfully.", @@ -104,7 +112,6 @@ public ResponseEntity leaveLobby( false ) ); - service.checkLobbyStillHasPlayers(lobbyCode); return response; } else { return ResponseEntity.badRequest() @@ -149,6 +156,7 @@ public ResponseEntity selectPosition( ); if (updated) { + persistenceService.persistCurrentState(); return ResponseEntity.ok( new LobbyResponse( "Position selected successfully.", @@ -185,6 +193,7 @@ public ResponseEntity startGame( boolean isStarted = service.startGame(lobbyCode, username); if (isStarted) { + persistenceService.persistCurrentState(); return ResponseEntity.ok( new LobbyResponse( "Game is starting now.", diff --git a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java index db30cfc2..bfb791e2 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java @@ -2,6 +2,7 @@ import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; import java.util.List; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -23,6 +24,7 @@ public class GameController { private final GameService gameService; private final SimpMessagingTemplate messagingTemplate; private final SessionRegistry sessionRegistry; + private final SystemStatePersistenceService persistenceService; /** * Creates a new {@code GameController}. @@ -31,16 +33,19 @@ public class GameController { * @param gameService the service handling game state retrieval * @param messagingTemplate the messaging template used for broadcasting updates * @param sessionRegistry the registry managing WebSocket sessions + * @param persistenceService service used to persist current backend state */ public GameController( LobbyService lobbyService, GameService gameService, SimpMessagingTemplate messagingTemplate, - SessionRegistry sessionRegistry) { + SessionRegistry sessionRegistry, + SystemStatePersistenceService persistenceService) { this.lobbyService = lobbyService; this.gameService = gameService; this.messagingTemplate = messagingTemplate; this.sessionRegistry = sessionRegistry; + this.persistenceService = persistenceService; } /** @@ -79,6 +84,7 @@ public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) } sessionRegistry.register(sessionId, message.getName(), message.getCode()); + persistenceService.persistCurrentState(); sendPlayerUpdate(message.getCode()); sendGameStateUpdate(message.getCode()); From 5c23caf8a797194593ae6dc2442c7059bb6f7ef7 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 01:45:18 +0200 Subject: [PATCH 192/207] test: update controller tests for state persistence calls --- .../game/controller/GameSocketControllerTest.java | 11 +++++++---- .../lobby/controller/LobbyControllerTest.java | 6 +++++- .../backend/websocket/GameControllerTest.java | 13 +++++++++++-- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index a5166d2e..0963a116 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -10,6 +10,7 @@ import com.codenames.codenames.backend.game.dto.RevealCardMessage; import com.codenames.codenames.backend.game.dto.StartGameMessage; import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import com.codenames.codenames.backend.utility.Team; import java.util.List; @@ -27,13 +28,14 @@ class GameSocketControllerTest { @Mock private GameService gameService; @Mock private SimpMessagingTemplate messagingTemplate; + @Mock private SystemStatePersistenceService persistenceService; private GameSocketController controller; @BeforeEach void setUp() { - controller = new GameSocketController(gameService, messagingTemplate); + controller = new GameSocketController(gameService, messagingTemplate, persistenceService); } @Test @@ -47,6 +49,7 @@ void startGameShouldBroadcastState() { controller.startGame(message); + verify(persistenceService).persistCurrentState(); verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } @@ -64,7 +67,7 @@ void revealCardShouldBroadcastState() { controller.revealCard(message); verify(gameService).flipCard("ABCDE", 0, Team.RED); - + verify(persistenceService).persistCurrentState(); verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } @@ -83,7 +86,7 @@ void submitClueShouldBroadcastState() { controller.submitClue(message); verify(gameService).submitClue(anyString(), any(), any()); - + verify(persistenceService).persistCurrentState(); verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } @@ -100,7 +103,7 @@ void passTurnShouldBroadcastUpdatedState() { controller.passTurn(message); verify(gameService).passTurn("ABCDE", Team.RED); - + verify(persistenceService).persistCurrentState(); verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } diff --git a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java index f45a62e9..38a970ae 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java @@ -2,6 +2,7 @@ import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import org.junit.jupiter.api.Test; @@ -28,6 +29,9 @@ class LobbyControllerTest { @MockBean private LobbyService service; + @MockBean + private SystemStatePersistenceService persistenceService; + @Test void createLobbyShouldReturn200() throws Exception { when(service.createLobby("TestUser")).thenReturn("ABCDE"); @@ -216,4 +220,4 @@ void testStartGameReturns400_WhenServiceReturnsFalse() throws Exception{ .andExpect(jsonPath("$.isStarted") .value("false")); } -} \ No newline at end of file +} diff --git a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java index 192a9d79..3dbf0c2d 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java @@ -11,6 +11,7 @@ import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.GameService; +import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -30,15 +31,19 @@ class GameControllerTest { private SessionRegistry sessionRegistry; private GameController controller; private SimpMessagingTemplate messagingTemplate; + private SystemStatePersistenceService persistenceService; @BeforeEach void setup() { lobbyService = mock(LobbyService.class); gameService = mock(GameService.class); messagingTemplate = mock(SimpMessagingTemplate.class); + persistenceService = mock(SystemStatePersistenceService.class); sessionRegistry = new SessionRegistry(); - controller = new GameController(lobbyService, gameService, messagingTemplate, sessionRegistry); + controller = + new GameController( + lobbyService, gameService, messagingTemplate, sessionRegistry, persistenceService); } @Test @@ -63,6 +68,7 @@ void shouldRegisterJoinAndRegisterSession() { controller.join(msg, accessor); verify(lobbyService).joinLobby("Max", "ABCDE"); + verify(persistenceService).persistCurrentState(); assertEquals("Max", sessionRegistry.getUser("123")); assertEquals("ABCDE", sessionRegistry.getLobby("123")); @@ -90,7 +96,7 @@ void shouldSendErrorMessageWhenJoinFails() { controller.join(msg, accessor); verify(messagingTemplate).convertAndSend("/topic/errors/123", "Join failed"); - + verifyNoInteractions(persistenceService); verifyNoMoreInteractions(messagingTemplate); } @@ -107,6 +113,7 @@ void shouldDoNothingWhenSessionIdIsNull() { verifyNoInteractions(lobbyService); verifyNoInteractions(messagingTemplate); + verifyNoInteractions(persistenceService); } @Test @@ -132,6 +139,7 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { assertEquals("ABCDE", sessionRegistry.getLobby("123")); verify(lobbyService).joinLobby("Max", "ABCDE"); + verify(persistenceService).persistCurrentState(); verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } @@ -158,6 +166,7 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { assertEquals("Max", sessionRegistry.getUser("reconnect-1")); assertEquals("ABCDE", sessionRegistry.getLobby("reconnect-1")); + verify(persistenceService).persistCurrentState(); verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } From d689b01457f70a950d0abbc29075b788e024af3f Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 01:46:45 +0200 Subject: [PATCH 193/207] style: fix imports --- .../lobby/controller/LobbyControllerTest.java | 199 ++++++++---------- 1 file changed, 91 insertions(+), 108 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java index 38a970ae..79e057b0 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java @@ -1,10 +1,17 @@ package com.codenames.codenames.backend.lobby.controller; +import static org.mockito.Mockito.when; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + import com.codenames.codenames.backend.lobby.dto.PlayerDto; import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; +import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; @@ -12,116 +19,107 @@ import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; -import java.util.List; - -import static org.mockito.Mockito.when; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; -import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; -import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; - @WebMvcTest(LobbyController.class) class LobbyControllerTest { - @Autowired - private MockMvc mockMvc; + @Autowired private MockMvc mockMvc; - @MockBean - private LobbyService service; + @MockBean private LobbyService service; - @MockBean - private SystemStatePersistenceService persistenceService; + @MockBean private SystemStatePersistenceService persistenceService; @Test void createLobbyShouldReturn200() throws Exception { when(service.createLobby("TestUser")).thenReturn("ABCDE"); - mockMvc.perform(get("/lobby/create") - .param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + mockMvc + .perform(get("/lobby/create").param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); } @Test void createLobbyBlankLobbyCode() throws Exception { when(service.createLobby("TestUser")).thenReturn(""); - mockMvc.perform(get("/lobby/create") - .param("username", "TestUser")) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.message").value("Error while creating lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("")); + mockMvc + .perform(get("/lobby/create").param("username", "TestUser")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Error while creating lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("")); } @Test void createLobbyNullLobbyCode() throws Exception { when(service.createLobby("TestUser")).thenReturn(null); - mockMvc.perform(get("/lobby/create") - .param("username", "TestUser")) - .andExpect(status().isInternalServerError()) - .andExpect(jsonPath("$.message").value("Error while creating lobby.")) - .andExpect(jsonPath("$.lobbyCode").value("")); + mockMvc + .perform(get("/lobby/create").param("username", "TestUser")) + .andExpect(status().isInternalServerError()) + .andExpect(jsonPath("$.message").value("Error while creating lobby.")) + .andExpect(jsonPath("$.lobbyCode").value("")); } @Test void joinLobbyShouldReturn200_whenSuccess() throws Exception { when(service.joinLobby("TestUser", "ABCDE")).thenReturn(true); - mockMvc.perform(get("/lobby/ABCDE/join") - .param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); + mockMvc + .perform(get("/lobby/ABCDE/join").param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); } @Test void getLobbyInfoShouldReturn200() throws Exception { - when(service.getPlayersDto("ABCDE")).thenReturn(List.of(new PlayerDto("test", null, null, true))); + when(service.getPlayersDto("ABCDE")) + .thenReturn(List.of(new PlayerDto("test", null, null, true))); String url = "/lobby/ABCDE"; - mockMvc.perform(get(url)) - .andExpect(status().isOk()); + mockMvc.perform(get(url)).andExpect(status().isOk()); } @Test void joinLobbyShouldReturn400_whenNotFound() throws Exception { when(service.joinLobby("TestUser", "XXXXX")).thenReturn(false); - mockMvc.perform(get("/lobby/XXXXX/join") - .param("username", "TestUser")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not find lobby.")); + mockMvc + .perform(get("/lobby/XXXXX/join").param("username", "TestUser")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not find lobby.")); } @Test void leaveLobbyShouldReturn200_whenSuccess() throws Exception { when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(true); - mockMvc.perform(get("/lobby/ABCDE/leave") - .param("username", "TestUser")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Left lobby successfully.")); + mockMvc + .perform(get("/lobby/ABCDE/leave").param("username", "TestUser")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Left lobby successfully.")); } @Test void leaveLobbyNoSuccess() throws Exception { when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(false); - mockMvc.perform(get("/lobby/ABCDE/leave") - .param("username", "TestUser") - .param("lobbyCode", "ABCDE")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not find lobby.")); + mockMvc + .perform( + get("/lobby/ABCDE/leave").param("username", "TestUser").param("lobbyCode", "ABCDE")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not find lobby.")); } @Test void selectPositionShouldReturn200whenSuccess() throws Exception { when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(true); - mockMvc.perform(post("/lobby/ABCDE/select-position") - .contentType(MediaType.APPLICATION_JSON) - .content( - """ + mockMvc + .perform( + post("/lobby/ABCDE/select-position") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ { "username": "TestUser", "team": "RED", @@ -129,19 +127,21 @@ void selectPositionShouldReturn200whenSuccess() throws Exception { "isHost": "true" } """)) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message").value("Position selected successfully.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Position selected successfully.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); } @Test void selectPositionShouldReturn400whenAssignmentFails() throws Exception { when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(false); - mockMvc.perform(post("/lobby/ABCDE/select-position") - .contentType(MediaType.APPLICATION_JSON) - .content( - """ + mockMvc + .perform( + post("/lobby/ABCDE/select-position") + .contentType(MediaType.APPLICATION_JSON) + .content( + """ { "username": "TestUser", "team": "RED", @@ -149,75 +149,58 @@ void selectPositionShouldReturn400whenAssignmentFails() throws Exception { "isHost": "true" } """)) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) - .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not assign selected team/role.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); } @Test void getLobbyInfoShouldReturn200_whenLobbyExists() throws Exception { - List players = List.of( - new PlayerDto("Alice", null, null, true), - new PlayerDto("Bob", null, null, false) - ); + List players = + List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); when(service.getPlayersDto("ABCDE")).thenReturn(players); - mockMvc.perform(get("/lobby/ABCDE")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message") - .value("Lobby info retrieved successfully.")) - .andExpect(jsonPath("$.lobbyCode") - .value("ABCDE")) - .andExpect(jsonPath("$.playerList[0].username") - .value("Alice")) - .andExpect(jsonPath("$.playerList[1].username") - .value("Bob")); + mockMvc + .perform(get("/lobby/ABCDE")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Lobby info retrieved successfully.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")) + .andExpect(jsonPath("$.playerList[0].username").value("Alice")) + .andExpect(jsonPath("$.playerList[1].username").value("Bob")); } @Test void testStartGameReturns200_WhenConditionIsMet() throws Exception { - List players = List.of( - new PlayerDto("Alice", null, null, true), - new PlayerDto("Bob", null, null, false) - ); + List players = + List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); when(service.getPlayersDto("ABCDE")).thenReturn(players); when(service.startGame("ABCDE", "Alice")).thenReturn(true); - mockMvc.perform(get("/lobby/ABCDE/start-game") - .param("username", "Alice")) - .andExpect(status().isOk()) - .andExpect(jsonPath("$.message") - .value("Game is starting now.")) - .andExpect(jsonPath("$.lobbyCode") - .value("ABCDE")) - .andExpect(jsonPath("$.playerList[0].username") - .value("Alice")) - .andExpect(jsonPath("$.isStarted") - .value("true")); + mockMvc + .perform(get("/lobby/ABCDE/start-game").param("username", "Alice")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.message").value("Game is starting now.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")) + .andExpect(jsonPath("$.playerList[0].username").value("Alice")) + .andExpect(jsonPath("$.isStarted").value("true")); } @Test - void testStartGameReturns400_WhenServiceReturnsFalse() throws Exception{ - List players = List.of( - new PlayerDto("Alice", null, null, true), - new PlayerDto("Bob", null, null, false) - ); + void testStartGameReturns400_WhenServiceReturnsFalse() throws Exception { + List players = + List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); when(service.getPlayersDto("ABCDE")).thenReturn(players); when(service.startGame("ABCDE", "Alice")).thenReturn(false); - mockMvc.perform(get("/lobby/ABCDE/start-game") - .param("username", "Alice")) - .andExpect(status().isBadRequest()) - .andExpect(jsonPath("$.message") - .value("Could not start the game.")) - .andExpect(jsonPath("$.lobbyCode") - .value("ABCDE")) - .andExpect(jsonPath("$.playerList[0].username") - .value("Alice")) - .andExpect(jsonPath("$.isStarted") - .value("false")); + mockMvc + .perform(get("/lobby/ABCDE/start-game").param("username", "Alice")) + .andExpect(status().isBadRequest()) + .andExpect(jsonPath("$.message").value("Could not start the game.")) + .andExpect(jsonPath("$.lobbyCode").value("ABCDE")) + .andExpect(jsonPath("$.playerList[0].username").value("Alice")) + .andExpect(jsonPath("$.isStarted").value("false")); } } From a31b94e019f4a2fb12e0667015a4e35472e9ceab Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 01:51:09 +0200 Subject: [PATCH 194/207] refactor: remove redundant response variable and verify persistence service usage in lobby controller test --- .../codenames/backend/lobby/controller/LobbyController.java | 3 +-- .../backend/lobby/controller/LobbyControllerTest.java | 3 +++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index 2efadfac..fd108aaf 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -104,7 +104,7 @@ public ResponseEntity leaveLobby( if (left) { service.checkLobbyStillHasPlayers(lobbyCode); persistenceService.persistCurrentState(); - ResponseEntity response = ResponseEntity.ok( + return ResponseEntity.ok( new LobbyResponse( "Left lobby successfully.", lobbyCode, @@ -112,7 +112,6 @@ public ResponseEntity leaveLobby( false ) ); - return response; } else { return ResponseEntity.badRequest() .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java index 79e057b0..e32e2a91 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.lobby.controller; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -37,6 +38,8 @@ void createLobbyShouldReturn200() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + + verify(persistenceService).persistCurrentState(); } @Test From e2b0e4628c71432502bddfd519f0a4aeb6e59ba4 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 10:59:07 +0200 Subject: [PATCH 195/207] style: fix checkstyle issues --- .../backend/lobby/services/LobbyService.java | 3 ++- .../lobby/controller/LobbyControllerTest.java | 14 +++++++------- .../backend/lobby/services/LobbyServiceTest.java | 8 ++++---- 3 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java index a078e8cc..91b542db 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/services/LobbyService.java @@ -25,7 +25,8 @@ @Service public class LobbyService { - @Getter private final Map lobbyList = new ConcurrentHashMap<>(); + @Getter + private final Map lobbyList = new ConcurrentHashMap<>(); private final LobbyCodeGenerator generator; private final GameService gameService; private final ChatService chatService; diff --git a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java index e32e2a91..47d2c1c3 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java @@ -65,7 +65,7 @@ void createLobbyNullLobbyCode() throws Exception { } @Test - void joinLobbyShouldReturn200_whenSuccess() throws Exception { + void joinLobbyShouldReturn200WhenSuccess() throws Exception { when(service.joinLobby("TestUser", "ABCDE")).thenReturn(true); mockMvc @@ -83,7 +83,7 @@ void getLobbyInfoShouldReturn200() throws Exception { } @Test - void joinLobbyShouldReturn400_whenNotFound() throws Exception { + void joinLobbyShouldReturn400WhenNotFound() throws Exception { when(service.joinLobby("TestUser", "XXXXX")).thenReturn(false); mockMvc @@ -93,7 +93,7 @@ void joinLobbyShouldReturn400_whenNotFound() throws Exception { } @Test - void leaveLobbyShouldReturn200_whenSuccess() throws Exception { + void leaveLobbyShouldReturn200WhenSuccess() throws Exception { when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(true); mockMvc @@ -136,7 +136,7 @@ void selectPositionShouldReturn200whenSuccess() throws Exception { } @Test - void selectPositionShouldReturn400whenAssignmentFails() throws Exception { + void selectPositionShouldReturn400WhenAssignmentFails() throws Exception { when(service.selectPosition("TestUser", "ABCDE", Team.RED, Role.SPYMASTER)).thenReturn(false); mockMvc @@ -158,7 +158,7 @@ void selectPositionShouldReturn400whenAssignmentFails() throws Exception { } @Test - void getLobbyInfoShouldReturn200_whenLobbyExists() throws Exception { + void getLobbyInfoShouldReturn200WhenLobbyExists() throws Exception { List players = List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); @@ -174,7 +174,7 @@ void getLobbyInfoShouldReturn200_whenLobbyExists() throws Exception { } @Test - void testStartGameReturns200_WhenConditionIsMet() throws Exception { + void testStartGameReturns200WhenConditionIsMet() throws Exception { List players = List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); @@ -191,7 +191,7 @@ void testStartGameReturns200_WhenConditionIsMet() throws Exception { } @Test - void testStartGameReturns400_WhenServiceReturnsFalse() throws Exception { + void testStartGameReturns400WhenServiceReturnsFalse() throws Exception { List players = List.of(new PlayerDto("Alice", null, null, true), new PlayerDto("Bob", null, null, false)); diff --git a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java index 60dba667..3363a43e 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/services/LobbyServiceTest.java @@ -291,7 +291,7 @@ void testGetPlayersDto_lobbyNotExists() { } @Test - void getPlayersDtoShouldReturnPlayerDTOs_whenLobbyExists() { + void getPlayersDtoShouldReturnPlayerDtosWhenLobbyExists() { lobbyService.createLobby("Host"); List result = lobbyService.getPlayersDto("ABCDE"); @@ -331,7 +331,7 @@ void testGetIsStarted() { } @Test - void testGetIsStarted_GameServiceReturnsFalse() { + void testGetIsStartedGameServiceReturnsFalse() { when(gameService.isGameStarted("ABCDE")).thenReturn(false); boolean result = lobbyService.getIsStarted("ABCDE"); @@ -339,7 +339,7 @@ void testGetIsStarted_GameServiceReturnsFalse() { } @Test - void testGetHost_Works() { + void testGetHostWorks() { lobbyService.createLobby("Alice"); lobbyService.joinLobby("Bob", "ABCDE"); lobbyService.joinLobby("Caesar", "ABCDE"); @@ -353,7 +353,7 @@ void testGetHost_Works() { @ParameterizedTest @NullAndEmptySource @ValueSource(strings = {"ABCDE"}) - void testGetHost_ReturnsEmptyString(String lobbyCode) { + void testGetHostReturnsEmptyString(String lobbyCode) { String result = lobbyService.getHost(lobbyCode); assertEquals("", result); From 6d87ef980000c17c464a495cd21dda46aafbdeff Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 20 May 2026 16:46:44 +0200 Subject: [PATCH 196/207] removed verification with session registry from chat + adapted tests --- .../backend/chat/ChatController.java | 63 ++++------------- .../backend/chat/ChatControllerTest.java | 69 +++++++------------ 2 files changed, 40 insertions(+), 92 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/chat/ChatController.java b/src/main/java/com/codenames/codenames/backend/chat/ChatController.java index 23a7c814..95df2f74 100644 --- a/src/main/java/com/codenames/codenames/backend/chat/ChatController.java +++ b/src/main/java/com/codenames/codenames/backend/chat/ChatController.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.chat; import com.codenames.codenames.backend.lobby.services.LobbyService; +import com.codenames.codenames.backend.utility.ChatMessageType; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; import com.codenames.codenames.backend.websocket.SessionRegistry; @@ -9,6 +10,7 @@ import org.springframework.messaging.handler.annotation.Payload; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.RequestParam; /** * Controller for broadcasting client messages to the desired destination with STOMP. @@ -35,35 +37,6 @@ public ChatController( this.sessionRegistry = sessionRegistry; } - /** - * Verifies the username of the sender and checks if they are sending the message to their own - * lobby before allowing them to send a message. - * - * @param headerAccessor the header accessor containing the session information of the sender - * @param targetLobbyId the actual lobby ID associated with the sender's session, used to verify - * that they are sending the message to their own lobby - * @return the username if sender is verified, otherwise throws an exception - * @throws IllegalStateException if the username is null or if the sender is trying to send a - * message to a lobby they are not in - */ - private String getVerifiedUsername( - SimpMessageHeaderAccessor headerAccessor, String targetLobbyId) { - String sessionId = headerAccessor.getSessionId(); - String realUsername = sessionRegistry.getUser(sessionId); - String realLobbyId = sessionRegistry.getLobby(sessionId); - - if (realUsername == null) { - throw new IllegalStateException("Null username."); - } - if (targetLobbyId == null) { - throw new IllegalStateException("Null lobby ID."); - } - if (!targetLobbyId.equals(realLobbyId)) { - throw new IllegalStateException("Wrong lobby, user not in lobby " + realLobbyId); - } - return realUsername; - } - /** * Sends a message to the entire lobby. * @@ -72,18 +45,15 @@ private String getVerifiedUsername( */ @MessageMapping("/chat/{lobbyId}") public void sendLobbyMessage( - @DestinationVariable String lobbyId, - @Payload ChatDto chatDto, - SimpMessageHeaderAccessor headerAccessor) { - String realUsername = getVerifiedUsername(headerAccessor, lobbyId); + @DestinationVariable String lobbyId, + @Payload ChatDto chatDto) { - // Create a new ChatDto with the verified username to prevent passing false username - ChatDto verifiedChatDto = new ChatDto(realUsername, chatDto.content(), chatDto.type()); + ChatDto verifiedChatDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), ChatMessageType.CHAT); chatService.processMessage(lobbyId, "LOBBY", "", verifiedChatDto); } /** - * Verifies the sender's name, lobbyID, and team before sending a message to the entire team. + * Verifies the sender's team and delegates message processing to @link ChatService. * * @param lobbyId the ID of the lobby the client is in * @param team the team the client is in (RED, BLUE) @@ -93,16 +63,14 @@ public void sendLobbyMessage( public void sendTeamMessage( @DestinationVariable String lobbyId, @DestinationVariable Team team, - @Payload ChatDto chatDto, - SimpMessageHeaderAccessor headerAccessor) { - String realUsername = getVerifiedUsername(headerAccessor, lobbyId); + @Payload ChatDto chatDto) { - Team playerTeam = lobbyService.getPlayerTeam(realUsername, lobbyId); + Team playerTeam = lobbyService.getPlayerTeam(chatDto.senderUsername(), lobbyId); if (playerTeam != team) { throw new IllegalStateException("You are not on team " + team.name()); } - ChatDto verifiedDto = new ChatDto(realUsername, chatDto.content(), chatDto.type()); + ChatDto verifiedDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), ChatMessageType.CHAT); String roomKey = "TEAM_" + team.name(); String topicSuffix = "/" + team.name(); @@ -110,8 +78,7 @@ public void sendTeamMessage( } /** - * Verifies the sender's name, lobbyID, team and role sending a message to the operatives on the - * respective team. + * Verifies the sender's team and role and delegates message processing to @link ChatService. * * @param lobbyId the ID of the lobby the client is in * @param team the team the client is in (RED, BLUE) @@ -121,19 +88,17 @@ public void sendTeamMessage( public void sendTeamOperativeMessage( @DestinationVariable String lobbyId, @DestinationVariable Team team, - @Payload ChatDto chatDto, - SimpMessageHeaderAccessor headerAccessor) { - String realUsername = getVerifiedUsername(headerAccessor, lobbyId); + @Payload ChatDto chatDto) { - Team playerTeam = lobbyService.getPlayerTeam(realUsername, lobbyId); - Role playerRole = lobbyService.getPlayerRole(realUsername, lobbyId); + Team playerTeam = lobbyService.getPlayerTeam(chatDto.senderUsername(), lobbyId); + Role playerRole = lobbyService.getPlayerRole(chatDto.senderUsername(), lobbyId); if (playerTeam != team || playerRole != Role.OPERATIVE) { throw new IllegalStateException( "You are either not an operative or are sending to the wrong team."); } - ChatDto verifiedDto = new ChatDto(realUsername, chatDto.content(), chatDto.type()); + ChatDto verifiedDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), ChatMessageType.CHAT); String roomKey = "OPERATIVE_" + team.name(); String topicSuffix = "/" + team.name() + "/operative"; diff --git a/src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java b/src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java index 31bfd57b..95481c23 100644 --- a/src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java @@ -51,46 +51,19 @@ void setUp() { @Test void testSendLobbyMessage_valid() { - chatController.sendLobbyMessage(lobbyId, chatDto, headerAccessor); + chatController.sendLobbyMessage(lobbyId, chatDto); - ChatDto verifiedChatDto = new ChatDto(realUsername, chatDto.content(), chatDto.type()); + ChatDto verifiedChatDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), chatDto.type()); verify(chatService, times(1)).processMessage(lobbyId, "LOBBY", "", verifiedChatDto); } - @Test - void testSendLobbyMessage_nullUsername() { - when(sessionRegistry.getUser(sessionId)).thenReturn(null); - - assertThrows( - IllegalStateException.class, - () -> chatController.sendLobbyMessage(lobbyId, chatDto, headerAccessor)); - } - - @Test - void testSendLobbyMessage_wrongLobby() { - when(sessionRegistry.getLobby(sessionId)).thenReturn("differentLobby"); - - assertThrows( - IllegalStateException.class, - () -> chatController.sendLobbyMessage(lobbyId, chatDto, headerAccessor)); - } - - @Test - void testSendLobbyMessage_nullLobby() { - when(sessionRegistry.getLobby(sessionId)).thenReturn("differentLobby"); - - assertThrows( - IllegalStateException.class, - () -> chatController.sendLobbyMessage(null, chatDto, headerAccessor)); - } - @Test void testSendTeamMessage_valid() { - when(lobbyService.getPlayerTeam(realUsername, lobbyId)).thenReturn(redTeam); + when(lobbyService.getPlayerTeam(chatDto.senderUsername(), lobbyId)).thenReturn(redTeam); - chatController.sendTeamMessage(lobbyId, redTeam, chatDto, headerAccessor); + chatController.sendTeamMessage(lobbyId, redTeam, chatDto); - ChatDto verifiedChatDto = new ChatDto(realUsername, chatDto.content(), chatDto.type()); + ChatDto verifiedChatDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), chatDto.type()); verify(chatService, times(1)).processMessage(lobbyId, "TEAM_RED", "/RED", verifiedChatDto); } @@ -100,38 +73,48 @@ void testSendTeamMessage_sendToDifferentTeam_throwException() { assertThrows( IllegalStateException.class, - () -> chatController.sendTeamMessage(lobbyId, redTeam, chatDto, headerAccessor)); + () -> chatController.sendTeamMessage(lobbyId, redTeam, chatDto)); } @Test void testSendTeamOperativeMessage_valid() { - when(lobbyService.getPlayerTeam(realUsername, lobbyId)).thenReturn(blueTeam); - when(lobbyService.getPlayerRole(realUsername, lobbyId)).thenReturn(Role.OPERATIVE); + when(lobbyService.getPlayerTeam(chatDto.senderUsername(), lobbyId)).thenReturn(blueTeam); + when(lobbyService.getPlayerRole(chatDto.senderUsername(), lobbyId)).thenReturn(Role.OPERATIVE); - chatController.sendTeamOperativeMessage(lobbyId, blueTeam, chatDto, headerAccessor); + chatController.sendTeamOperativeMessage(lobbyId, blueTeam, chatDto); - ChatDto verifiedChatDto = new ChatDto(realUsername, chatDto.content(), chatDto.type()); + ChatDto verifiedChatDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), chatDto.type()); verify(chatService, times(1)) .processMessage(lobbyId, "OPERATIVE_BLUE", "/BLUE/operative", verifiedChatDto); } @Test void testSendTeamOperativeMessage_wrongRoleCorrectTeam() { - when(lobbyService.getPlayerTeam(realUsername, lobbyId)).thenReturn(blueTeam); - when(lobbyService.getPlayerRole(realUsername, lobbyId)).thenReturn(Role.SPYMASTER); + when(lobbyService.getPlayerTeam(chatDto.senderUsername(), lobbyId)).thenReturn(blueTeam); + when(lobbyService.getPlayerRole(chatDto.senderUsername(), lobbyId)).thenReturn(Role.SPYMASTER); assertThrows( IllegalStateException.class, - () -> chatController.sendTeamOperativeMessage(lobbyId, blueTeam, chatDto, headerAccessor)); + () -> chatController.sendTeamOperativeMessage(lobbyId, blueTeam, chatDto)); } @Test void testSendTeamOperativeMessage_wrongTeamCorrectRole() { - when(lobbyService.getPlayerTeam(realUsername, lobbyId)).thenReturn(redTeam); - when(lobbyService.getPlayerRole(realUsername, lobbyId)).thenReturn(Role.OPERATIVE); + when(lobbyService.getPlayerTeam(chatDto.senderUsername(), lobbyId)).thenReturn(redTeam); + when(lobbyService.getPlayerRole(chatDto.senderUsername(), lobbyId)).thenReturn(Role.OPERATIVE); + + assertThrows( + IllegalStateException.class, + () -> chatController.sendTeamOperativeMessage(lobbyId, blueTeam, chatDto)); + } + + @Test + void testSendTeamOperativeMessage_wrongRoleWrongTeam() { + when(lobbyService.getPlayerTeam(chatDto.senderUsername(), lobbyId)).thenReturn(redTeam); + when(lobbyService.getPlayerRole(chatDto.senderUsername(), lobbyId)).thenReturn(Role.SPYMASTER); assertThrows( IllegalStateException.class, - () -> chatController.sendTeamOperativeMessage(lobbyId, blueTeam, chatDto, headerAccessor)); + () -> chatController.sendTeamOperativeMessage(lobbyId, blueTeam, chatDto)); } } From f709d98384883c487376be4667ad6b95091925cd Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 20 May 2026 17:19:49 +0200 Subject: [PATCH 197/207] refactor: removed unused import & cleared checkstyle warnings --- .../backend/chat/ChatController.java | 27 ++++++++++++------- .../backend/chat/ChatControllerTest.java | 2 +- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/chat/ChatController.java b/src/main/java/com/codenames/codenames/backend/chat/ChatController.java index 95df2f74..c1827312 100644 --- a/src/main/java/com/codenames/codenames/backend/chat/ChatController.java +++ b/src/main/java/com/codenames/codenames/backend/chat/ChatController.java @@ -4,26 +4,22 @@ import com.codenames.codenames.backend.utility.ChatMessageType; import com.codenames.codenames.backend.utility.Role; import com.codenames.codenames.backend.utility.Team; -import com.codenames.codenames.backend.websocket.SessionRegistry; import org.springframework.messaging.handler.annotation.DestinationVariable; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.handler.annotation.Payload; -import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.stereotype.Controller; -import org.springframework.web.bind.annotation.RequestParam; - /** * Controller for broadcasting client messages to the desired destination with STOMP. * *

The destination is based on the lobbyID or team parameters passed when the method is invoked. * The parameters are appended to the destination and broadcasted to all subscribers. */ + @Controller public class ChatController { private final ChatService chatService; private final LobbyService lobbyService; - private final SessionRegistry sessionRegistry; /** * Constructor for the ChatController. @@ -31,10 +27,9 @@ public class ChatController { * @param chatService the service used to validate and persist chat history */ public ChatController( - ChatService chatService, LobbyService lobbyService, SessionRegistry sessionRegistry) { + ChatService chatService, LobbyService lobbyService) { this.chatService = chatService; this.lobbyService = lobbyService; - this.sessionRegistry = sessionRegistry; } /** @@ -48,7 +43,11 @@ public void sendLobbyMessage( @DestinationVariable String lobbyId, @Payload ChatDto chatDto) { - ChatDto verifiedChatDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), ChatMessageType.CHAT); + ChatDto verifiedChatDto = new ChatDto( + chatDto.senderUsername(), + chatDto.content(), + ChatMessageType.CHAT + ); chatService.processMessage(lobbyId, "LOBBY", "", verifiedChatDto); } @@ -70,7 +69,11 @@ public void sendTeamMessage( throw new IllegalStateException("You are not on team " + team.name()); } - ChatDto verifiedDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), ChatMessageType.CHAT); + ChatDto verifiedDto = new ChatDto( + chatDto.senderUsername(), + chatDto.content(), + ChatMessageType.CHAT + ); String roomKey = "TEAM_" + team.name(); String topicSuffix = "/" + team.name(); @@ -98,7 +101,11 @@ public void sendTeamOperativeMessage( "You are either not an operative or are sending to the wrong team."); } - ChatDto verifiedDto = new ChatDto(chatDto.senderUsername(), chatDto.content(), ChatMessageType.CHAT); + ChatDto verifiedDto = new ChatDto( + chatDto.senderUsername(), + chatDto.content(), + ChatMessageType.CHAT + ); String roomKey = "OPERATIVE_" + team.name(); String topicSuffix = "/" + team.name() + "/operative"; diff --git a/src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java b/src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java index 95481c23..6a18a085 100644 --- a/src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/chat/ChatControllerTest.java @@ -37,7 +37,7 @@ void setUp() { lobbyService = mock(LobbyService.class); headerAccessor = mock(SimpMessageHeaderAccessor.class); - chatController = new ChatController(chatService, lobbyService, sessionRegistry); + chatController = new ChatController(chatService, lobbyService); chatDto = new ChatDto("TestName", "TestMessage", ChatMessageType.CHAT); From e4ea92e66244ad9b02f575d8c3b3539550cdd1a1 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 17:45:44 +0200 Subject: [PATCH 198/207] chore: persist backend state with docker volume --- docker-compose.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docker-compose.yml b/docker-compose.yml index 62960875..44d70a2e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,4 +3,6 @@ services: image: ghcr.io/ss26-se2-codenames/backend:latest restart: unless-stopped ports: - - "53213:8080" \ No newline at end of file + - "53213:8080" + volumes: + - ./data:/app/data \ No newline at end of file From 24fabaa50123104c5190aff7d6d50edac9b81d25 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 17:51:18 +0200 Subject: [PATCH 199/207] refactor: reduce pre-game lobby persistence --- .../lobby/controller/LobbyController.java | 125 +++++++----------- 1 file changed, 47 insertions(+), 78 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java index fd108aaf..4415ca4e 100644 --- a/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java +++ b/src/main/java/com/codenames/codenames/backend/lobby/controller/LobbyController.java @@ -20,7 +20,6 @@ *

Provides endpoints for creating, joining, and leaving lobbies. Delegates business logic to * {@link LobbyService}. */ - @RestController @RequestMapping("/lobby") public class LobbyController { @@ -46,75 +45,68 @@ public LobbyController(LobbyService service, SystemStatePersistenceService persi * @param username the username of the requesting user * @return a response containing the result and the generated lobby code */ - @GetMapping("/create") public ResponseEntity createLobby(@RequestParam String username) { String lobbyCode = service.createLobby(username); if (lobbyCode == null || lobbyCode.isBlank()) { return ResponseEntity.internalServerError() - .body(new LobbyResponse("Error while creating lobby.", "", null, false)); + .body(new LobbyResponse("Error while creating lobby.", "", null, false)); } else { - persistenceService.persistCurrentState(); + List players = service.getPlayersDto(lobbyCode); return ResponseEntity.ok( - new LobbyResponse("Successfully created Lobby.", lobbyCode, players, false) - ); + new LobbyResponse("Successfully created Lobby.", lobbyCode, players, false)); } } /** * Handles a request to join an existing lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby * @return a response indicating whether the join was successful */ @GetMapping("/{lobbyCode}/join") public ResponseEntity joinLobby( - @RequestParam String username, @PathVariable String lobbyCode) { + @RequestParam String username, @PathVariable String lobbyCode) { boolean joined = service.joinLobby(username, lobbyCode); if (joined) { - persistenceService.persistCurrentState(); + return ResponseEntity.ok( - new LobbyResponse( - "Joined Lobby successfully.", - lobbyCode, - service.getPlayersDto(lobbyCode), - false - ) - ); + new LobbyResponse( + "Joined Lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false)); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); } } /** * Handles a request to leave a lobby. * - * @param username the username of the player + * @param username the username of the player * @param lobbyCode the lobby code identifying the lobby * @return a response indicating whether the operation was successful */ @GetMapping("/{lobbyCode}/leave") public ResponseEntity leaveLobby( - @PathVariable String lobbyCode, - @RequestParam String username) { + @PathVariable String lobbyCode, @RequestParam String username) { + boolean wasStarted = service.getIsStarted(lobbyCode); boolean left = service.leaveLobby(username, lobbyCode); + if (left) { service.checkLobbyStillHasPlayers(lobbyCode); - persistenceService.persistCurrentState(); + + if (wasStarted) { + persistenceService.persistCurrentState(); + } + return ResponseEntity.ok( - new LobbyResponse( - "Left lobby successfully.", - lobbyCode, - service.getPlayersDto(lobbyCode), - false - ) - ); + new LobbyResponse( + "Left lobby successfully.", lobbyCode, service.getPlayersDto(lobbyCode), false)); } else { return ResponseEntity.badRequest() - .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); + .body(new LobbyResponse(LOBBY_NOT_FOUND, lobbyCode, null, false)); } } @@ -122,19 +114,15 @@ public ResponseEntity leaveLobby( * An endpoint for retrieving all lobby-specific info used during polling in lobby-state. * * @param lobbyCode unique lobby code - * @return a response entity with the http code 200 for ok and - * 400 for bad request, if an error occurred + * @return a response entity with the http code 200 for ok and 400 for bad request, if an error + * occurred */ - @GetMapping("/{lobbyCode}") - public ResponseEntity getLobbyInfo( - @PathVariable String lobbyCode - ) { + public ResponseEntity getLobbyInfo(@PathVariable String lobbyCode) { List players = service.getPlayersDto(lobbyCode); boolean isStarted = service.getIsStarted(lobbyCode); return ResponseEntity.ok( - new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players, isStarted) - ); + new LobbyResponse("Lobby info retrieved successfully.", lobbyCode, players, isStarted)); } /** @@ -145,34 +133,26 @@ public ResponseEntity getLobbyInfo( */ @PostMapping("/{lobbyCode}/select-position") public ResponseEntity selectPosition( - @PathVariable String lobbyCode, @RequestBody PlayerDto request - ) { - boolean updated = service.selectPosition( - request.username(), - lobbyCode, - request.team(), - request.role() - ); + @PathVariable String lobbyCode, @RequestBody PlayerDto request) { + boolean updated = + service.selectPosition(request.username(), lobbyCode, request.team(), request.role()); if (updated) { - persistenceService.persistCurrentState(); + return ResponseEntity.ok( - new LobbyResponse( - "Position selected successfully.", - lobbyCode, - service.getPlayersDto(lobbyCode), - false - ) - ); + new LobbyResponse( + "Position selected successfully.", + lobbyCode, + service.getPlayersDto(lobbyCode), + false)); } else { - return ResponseEntity.badRequest().body( + return ResponseEntity.badRequest() + .body( new LobbyResponse( - "Could not assign selected team/role.", - lobbyCode, - service.getPlayersDto(lobbyCode), - false - ) - ); + "Could not assign selected team/role.", + lobbyCode, + service.getPlayersDto(lobbyCode), + false)); } } @@ -182,33 +162,22 @@ public ResponseEntity selectPosition( * @param lobbyCode the unique lobby code * @param username the name of the requesting user * @return a response entity of a lobby response, with isStarted @code true or @code false, - * whether the starting was successful or not + * whether the starting was successful or not */ - @GetMapping("/{lobbyCode}/start-game") public ResponseEntity startGame( - @PathVariable String lobbyCode, @RequestParam String username - ) { + @PathVariable String lobbyCode, @RequestParam String username) { boolean isStarted = service.startGame(lobbyCode, username); if (isStarted) { persistenceService.persistCurrentState(); return ResponseEntity.ok( - new LobbyResponse( - "Game is starting now.", - lobbyCode, - service.getPlayersDto(lobbyCode), - true - ) - ); + new LobbyResponse( + "Game is starting now.", lobbyCode, service.getPlayersDto(lobbyCode), true)); } - return ResponseEntity.badRequest().body( + return ResponseEntity.badRequest() + .body( new LobbyResponse( - "Could not start the game.", - lobbyCode, - service.getPlayersDto(lobbyCode), - false - ) - ); + "Could not start the game.", lobbyCode, service.getPlayersDto(lobbyCode), false)); } } From e05976ed76d187bd352e3c1127b4c916a3aeca98 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 18:15:11 +0200 Subject: [PATCH 200/207] test: align lobby and game controller persistence expectations --- .../game/controller/GameSocketController.java | 1 - .../backend/websocket/GameController.java | 2 +- .../controller/GameSocketControllerTest.java | 47 +++++++++---------- .../lobby/controller/LobbyControllerTest.java | 12 ++++- .../backend/websocket/GameControllerTest.java | 6 +-- 5 files changed, 38 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java index c688b7bb..59d41e9b 100644 --- a/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/controller/GameSocketController.java @@ -51,7 +51,6 @@ public GameSocketController( */ @MessageMapping("/start-game") public void startGame(StartGameMessage message) { - persistenceService.persistCurrentState(); messagingTemplate.convertAndSend( GAME_TOPIC_PREFIX + message.getLobbyCode(), diff --git a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java index bfb791e2..a208cc99 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java @@ -84,7 +84,7 @@ public void join(JoinMessage message, SimpMessageHeaderAccessor headerAccessor) } sessionRegistry.register(sessionId, message.getName(), message.getCode()); - persistenceService.persistCurrentState(); + sendPlayerUpdate(message.getCode()); sendGameStateUpdate(message.getCode()); diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 0963a116..1c56dd67 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -2,6 +2,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -25,63 +26,62 @@ @ExtendWith(MockitoExtension.class) class GameSocketControllerTest { + private static final String LOBBY_CODE = "ABCDE"; + @Mock private GameService gameService; @Mock private SimpMessagingTemplate messagingTemplate; + @Mock private SystemStatePersistenceService persistenceService; private GameSocketController controller; @BeforeEach void setUp() { - controller = new GameSocketController(gameService, messagingTemplate, persistenceService); } @Test - void startGameShouldBroadcastState() { - + void startGameShouldBroadcastStateWithoutPersisting() { StartGameMessage message = new StartGameMessage(); + message.setLobbyCode(LOBBY_CODE); - message.setLobbyCode("ABCDE"); - - when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStateDataTransferObject()); + when(gameService.getCurrentGameState(LOBBY_CODE)) + .thenReturn(createGameStateDataTransferObject()); controller.startGame(message); - verify(persistenceService).persistCurrentState(); + verify(persistenceService, never()).persistCurrentState(); verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } @Test - void revealCardShouldBroadcastState() { - + void revealCardShouldPersistAndBroadcastState() { RevealCardMessage message = new RevealCardMessage(); - - message.setLobbyCode("ABCDE"); + message.setLobbyCode(LOBBY_CODE); message.setPosition(0); message.setCurrentTurn(Team.RED); - when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStateDataTransferObject()); + when(gameService.getCurrentGameState(LOBBY_CODE)) + .thenReturn(createGameStateDataTransferObject()); controller.revealCard(message); - verify(gameService).flipCard("ABCDE", 0, Team.RED); + verify(gameService).flipCard(LOBBY_CODE, 0, Team.RED); verify(persistenceService).persistCurrentState(); verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } @Test - void submitClueShouldBroadcastState() { - + void submitClueShouldPersistAndBroadcastState() { ClueMessage message = new ClueMessage(); - - message.setLobbyCode("ABCDE"); + message.setLobbyCode(LOBBY_CODE); message.setWord("animal"); message.setGuessAmount(2); message.setCurrentTurn(Team.RED); - when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStateDataTransferObject()); + when(gameService.getCurrentGameState(LOBBY_CODE)) + .thenReturn(createGameStateDataTransferObject()); controller.submitClue(message); @@ -91,18 +91,17 @@ void submitClueShouldBroadcastState() { } @Test - void passTurnShouldBroadcastUpdatedState() { - + void passTurnShouldPersistAndBroadcastUpdatedState() { PassTurnMessage message = new PassTurnMessage(); - - message.setLobbyCode("ABCDE"); + message.setLobbyCode(LOBBY_CODE); message.setCurrentTurn(Team.RED); - when(gameService.getCurrentGameState("ABCDE")).thenReturn(createGameStateDataTransferObject()); + when(gameService.getCurrentGameState(LOBBY_CODE)) + .thenReturn(createGameStateDataTransferObject()); controller.passTurn(message); - verify(gameService).passTurn("ABCDE", Team.RED); + verify(gameService).passTurn(LOBBY_CODE, Team.RED); verify(persistenceService).persistCurrentState(); verify(messagingTemplate).convertAndSend(anyString(), any(Object.class)); } diff --git a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java index 47d2c1c3..3ae261c6 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/controller/LobbyControllerTest.java @@ -1,6 +1,7 @@ package com.codenames.codenames.backend.lobby.controller; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; @@ -39,7 +40,7 @@ void createLobbyShouldReturn200() throws Exception { .andExpect(jsonPath("$.message").value("Successfully created Lobby.")) .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); - verify(persistenceService).persistCurrentState(); + verifyNoInteractions(persistenceService); } @Test @@ -72,6 +73,8 @@ void joinLobbyShouldReturn200WhenSuccess() throws Exception { .perform(get("/lobby/ABCDE/join").param("username", "TestUser")) .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("Joined Lobby successfully.")); + + verifyNoInteractions(persistenceService); } @Test @@ -94,12 +97,15 @@ void joinLobbyShouldReturn400WhenNotFound() throws Exception { @Test void leaveLobbyShouldReturn200WhenSuccess() throws Exception { + when(service.getIsStarted("ABCDE")).thenReturn(false); when(service.leaveLobby("TestUser", "ABCDE")).thenReturn(true); mockMvc .perform(get("/lobby/ABCDE/leave").param("username", "TestUser")) .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("Left lobby successfully.")); + + verifyNoInteractions(persistenceService); } @Test @@ -133,6 +139,8 @@ void selectPositionShouldReturn200whenSuccess() throws Exception { .andExpect(status().isOk()) .andExpect(jsonPath("$.message").value("Position selected successfully.")) .andExpect(jsonPath("$.lobbyCode").value("ABCDE")); + + verifyNoInteractions(persistenceService); } @Test @@ -188,6 +196,8 @@ void testStartGameReturns200WhenConditionIsMet() throws Exception { .andExpect(jsonPath("$.lobbyCode").value("ABCDE")) .andExpect(jsonPath("$.playerList[0].username").value("Alice")) .andExpect(jsonPath("$.isStarted").value("true")); + + verify(persistenceService).persistCurrentState(); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java index 3dbf0c2d..a8aad262 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java @@ -68,7 +68,7 @@ void shouldRegisterJoinAndRegisterSession() { controller.join(msg, accessor); verify(lobbyService).joinLobby("Max", "ABCDE"); - verify(persistenceService).persistCurrentState(); + verifyNoInteractions(persistenceService); assertEquals("Max", sessionRegistry.getUser("123")); assertEquals("ABCDE", sessionRegistry.getLobby("123")); @@ -139,7 +139,7 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { assertEquals("ABCDE", sessionRegistry.getLobby("123")); verify(lobbyService).joinLobby("Max", "ABCDE"); - verify(persistenceService).persistCurrentState(); + verifyNoInteractions(persistenceService); verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } @@ -166,7 +166,7 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { assertEquals("Max", sessionRegistry.getUser("reconnect-1")); assertEquals("ABCDE", sessionRegistry.getLobby("reconnect-1")); - verify(persistenceService).persistCurrentState(); + verifyNoInteractions(persistenceService); verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } From e2898e43f587183a4f61d0e3c08c54008dae3dba Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 18:18:55 +0200 Subject: [PATCH 201/207] refactor: remove unused persistence dependency from game controller --- .../codenames/backend/websocket/GameController.java | 7 +------ .../backend/websocket/GameControllerTest.java | 12 +----------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java index a208cc99..3cec2aee 100644 --- a/src/main/java/com/codenames/codenames/backend/websocket/GameController.java +++ b/src/main/java/com/codenames/codenames/backend/websocket/GameController.java @@ -2,7 +2,6 @@ import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; import java.util.List; import lombok.extern.slf4j.Slf4j; import org.springframework.messaging.handler.annotation.MessageMapping; @@ -24,7 +23,6 @@ public class GameController { private final GameService gameService; private final SimpMessagingTemplate messagingTemplate; private final SessionRegistry sessionRegistry; - private final SystemStatePersistenceService persistenceService; /** * Creates a new {@code GameController}. @@ -33,19 +31,16 @@ public class GameController { * @param gameService the service handling game state retrieval * @param messagingTemplate the messaging template used for broadcasting updates * @param sessionRegistry the registry managing WebSocket sessions - * @param persistenceService service used to persist current backend state */ public GameController( LobbyService lobbyService, GameService gameService, SimpMessagingTemplate messagingTemplate, - SessionRegistry sessionRegistry, - SystemStatePersistenceService persistenceService) { + SessionRegistry sessionRegistry) { this.lobbyService = lobbyService; this.gameService = gameService; this.messagingTemplate = messagingTemplate; this.sessionRegistry = sessionRegistry; - this.persistenceService = persistenceService; } /** diff --git a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java index a8aad262..e630b4f1 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java @@ -11,7 +11,6 @@ import com.codenames.codenames.backend.lobby.services.LobbyService; import com.codenames.codenames.backend.playingfield.GameService; -import com.codenames.codenames.backend.recovery.SystemStatePersistenceService; import com.codenames.codenames.backend.serialization.GameStateDataTransferObject; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -31,19 +30,15 @@ class GameControllerTest { private SessionRegistry sessionRegistry; private GameController controller; private SimpMessagingTemplate messagingTemplate; - private SystemStatePersistenceService persistenceService; @BeforeEach void setup() { lobbyService = mock(LobbyService.class); gameService = mock(GameService.class); messagingTemplate = mock(SimpMessagingTemplate.class); - persistenceService = mock(SystemStatePersistenceService.class); sessionRegistry = new SessionRegistry(); - controller = - new GameController( - lobbyService, gameService, messagingTemplate, sessionRegistry, persistenceService); + controller = new GameController(lobbyService, gameService, messagingTemplate, sessionRegistry); } @Test @@ -68,7 +63,6 @@ void shouldRegisterJoinAndRegisterSession() { controller.join(msg, accessor); verify(lobbyService).joinLobby("Max", "ABCDE"); - verifyNoInteractions(persistenceService); assertEquals("Max", sessionRegistry.getUser("123")); assertEquals("ABCDE", sessionRegistry.getLobby("123")); @@ -96,7 +90,6 @@ void shouldSendErrorMessageWhenJoinFails() { controller.join(msg, accessor); verify(messagingTemplate).convertAndSend("/topic/errors/123", "Join failed"); - verifyNoInteractions(persistenceService); verifyNoMoreInteractions(messagingTemplate); } @@ -113,7 +106,6 @@ void shouldDoNothingWhenSessionIdIsNull() { verifyNoInteractions(lobbyService); verifyNoInteractions(messagingTemplate); - verifyNoInteractions(persistenceService); } @Test @@ -139,7 +131,6 @@ void shouldUseSessionAttributesFallbackWhenSessionIdIsNull() { assertEquals("ABCDE", sessionRegistry.getLobby("123")); verify(lobbyService).joinLobby("Max", "ABCDE"); - verifyNoInteractions(persistenceService); verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } @@ -166,7 +157,6 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { assertEquals("Max", sessionRegistry.getUser("reconnect-1")); assertEquals("ABCDE", sessionRegistry.getLobby("reconnect-1")); - verifyNoInteractions(persistenceService); verify(messagingTemplate).convertAndSend(eq("/topic/lobby/ABCDE"), any(Object.class)); verify(messagingTemplate).convertAndSend(eq("/topic/game/ABCDE"), any(Object.class)); } From 34d8e9b7db9d3658500ed918dcf7c1dd71f063f1 Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 20 May 2026 19:21:05 +0200 Subject: [PATCH 202/207] doc: changed format of link comments --- .../com/codenames/codenames/backend/chat/ChatController.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/codenames/codenames/backend/chat/ChatController.java b/src/main/java/com/codenames/codenames/backend/chat/ChatController.java index c1827312..91c22ddc 100644 --- a/src/main/java/com/codenames/codenames/backend/chat/ChatController.java +++ b/src/main/java/com/codenames/codenames/backend/chat/ChatController.java @@ -52,7 +52,7 @@ public void sendLobbyMessage( } /** - * Verifies the sender's team and delegates message processing to @link ChatService. + * Verifies the sender's team and delegates message processing to {@link ChatService}. * * @param lobbyId the ID of the lobby the client is in * @param team the team the client is in (RED, BLUE) @@ -81,7 +81,7 @@ public void sendTeamMessage( } /** - * Verifies the sender's team and role and delegates message processing to @link ChatService. + * Verifies the sender's team and role and delegates message processing to {@link ChatService}. * * @param lobbyId the ID of the lobby the client is in * @param team the team the client is in (RED, BLUE) From 38c490dfd72ca907e1736238451a2cf591990779 Mon Sep 17 00:00:00 2001 From: tasaje1 Date: Wed, 20 May 2026 20:41:58 +0200 Subject: [PATCH 203/207] test: fix imports --- .../recovery/SystemStatePersistenceServiceTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java b/src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java index e2d2c0f9..c0d956dc 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/SystemStatePersistenceServiceTest.java @@ -1,5 +1,6 @@ package com.codenames.codenames.backend.recovery; +import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -41,9 +42,8 @@ void persistCurrentStateSavesCurrentSystemSnapshot() { SystemSnapshot snapshot = snapshotCaptor.getValue(); - org.junit.jupiter.api.Assertions.assertEquals( - SystemSnapshot.CURRENT_SCHEMA_VERSION, snapshot.schemaVersion()); - org.junit.jupiter.api.Assertions.assertEquals(lobbySnapshots, snapshot.lobbies()); - org.junit.jupiter.api.Assertions.assertEquals(gameSnapshots, snapshot.games()); + assertEquals(SystemSnapshot.CURRENT_SCHEMA_VERSION, snapshot.schemaVersion()); + assertEquals(lobbySnapshots, snapshot.lobbies()); + assertEquals(gameSnapshots, snapshot.games()); } } From 81a27d3b1b79b8e41efc1747a64b521fc3781cd4 Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 20 May 2026 22:08:01 +0200 Subject: [PATCH 204/207] refactor: remove wrongly added parameter from game state dto --- data/state.json | 600 ++++++++++++++++++ .../backend/playingfield/GameManager.java | 1 - .../DataTransferObjectService.java | 6 +- .../GameStateDataTransferObject.java | 10 +- .../controller/GameSocketControllerTest.java | 2 +- .../playingfield/GameManagerFactoryTest.java | 7 +- .../backend/playingfield/GameManagerTest.java | 10 +- .../backend/playingfield/GameServiceTest.java | 6 +- .../backend/recovery/JsonStateStoreTest.java | 3 +- .../SystemStateRecoveryServiceTest.java | 6 +- .../DataTransferObjectServiceTest.java | 1 - .../serialization/SerializationJsonTest.java | 2 +- .../backend/websocket/GameControllerTest.java | 2 +- 13 files changed, 621 insertions(+), 35 deletions(-) create mode 100644 data/state.json diff --git a/data/state.json b/data/state.json new file mode 100644 index 00000000..2bac2bd0 --- /dev/null +++ b/data/state.json @@ -0,0 +1,600 @@ +{ + "schemaVersion" : 2, + "lobbies" : { + "PRTKG" : [ { + "username" : "c", + "team" : "BLUE", + "role" : "OPERATIVE", + "isHost" : true + }, { + "username" : "a", + "team" : "BLUE", + "role" : "SPYMASTER", + "isHost" : false + } ], + "95WL7" : [ { + "username" : "anna2", + "team" : "BLUE", + "role" : "OPERATIVE", + "isHost" : true + }, { + "username" : "anna", + "team" : "BLUE", + "role" : "OPERATIVE", + "isHost" : false + } ], + "D44MV" : [ { + "username" : "anna", + "team" : "BLUE", + "role" : "OPERATIVE", + "isHost" : true + }, { + "username" : "anna2", + "team" : "BLUE", + "role" : "OPERATIVE", + "isHost" : false + } ], + "Y8XE7" : [ { + "username" : "hi", + "team" : "BLUE", + "role" : "SPYMASTER", + "isHost" : true + }, { + "username" : "ho", + "team" : "BLUE", + "role" : "OPERATIVE", + "isHost" : false + } ], + "34LLZ" : [ { + "username" : "ana", + "team" : "BLUE", + "role" : "SPYMASTER", + "isHost" : true + }, { + "username" : "ass", + "team" : "BLUE", + "role" : "OPERATIVE", + "isHost" : false + } ] + }, + "games" : { + "PRTKG" : { + "winner" : null, + "currentTurn" : "BLUE", + "currentPhase" : "SPYMASTER", + "currentClue" : null, + "cardList" : [ { + "word" : "BOND", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "AZTEC", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "MICROSCOPE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "TOWER", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "TAP", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "WAVE", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "CRICKET", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "CZECH", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "SPY", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "SOCK", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "SPOT", + "color" : "ASSASSIN", + "isGuessed" : false + }, { + "word" : "BRIDGE", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "FORK", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "LASER", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "BOW", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "HEART", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "SOLDIER", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "DRESS", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "PIRATE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "PIN", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "LEMON", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "BAND", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "DEGREE", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "GLOVE", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "ROW", + "color" : "BLUE", + "isGuessed" : false + } ] + }, + "95WL7" : { + "winner" : null, + "currentTurn" : "RED", + "currentPhase" : "SPYMASTER", + "currentClue" : null, + "cardList" : [ { + "word" : "PAPER", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "CHICK", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "DRAFT", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "UNDERTAKER", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "EUROPE", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "DWARF", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "HAND", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "SUIT", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "HORN", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "BOND", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "SHIP", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "ANTARCTICA", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "BERRY", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "POLE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "DISEASE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "ATLANTIS", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "DROP", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "WITCH", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "SPIKE", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "BEACH", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "TABLET", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "MARBLE", + "color" : "ASSASSIN", + "isGuessed" : false + }, { + "word" : "TOWER", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "STRIKE", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "LEMON", + "color" : "NEUTRAL", + "isGuessed" : false + } ] + }, + "D44MV" : { + "winner" : null, + "currentTurn" : "BLUE", + "currentPhase" : "SPYMASTER", + "currentClue" : null, + "cardList" : [ { + "word" : "SPRING", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "TRACK", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "TELESCOPE", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "GLASS", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "ALPS", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "BED", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "FALL", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "MUG", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "BATTERY", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "VACUUM", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "HORN", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "BOX", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "BRIDGE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "JUPITER", + "color" : "ASSASSIN", + "isGuessed" : false + }, { + "word" : "BOW", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "FIGURE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "TRIANGLE", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "SHAKESPEARE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "BANK", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "MAMMOTH", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "CROSS", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "BILL", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "STICK", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "ANGEL", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "BEAR", + "color" : "RED", + "isGuessed" : false + } ] + }, + "Y8XE7" : { + "winner" : null, + "currentTurn" : "RED", + "currentPhase" : "SPYMASTER", + "currentClue" : null, + "cardList" : [ { + "word" : "STRAW", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "SLUG", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "ATLANTIS", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "SHIP", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "WALL", + "color" : "ASSASSIN", + "isGuessed" : false + }, { + "word" : "APPLE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "KING", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "SPOT", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "BRUSH", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "TRIANGLE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "DRILL", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "TORCH", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "BATTERY", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "UNDERTAKER", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "DEATH", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "STRIKE", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "MAPLE", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "KNIFE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "HOSPITAL", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "RABBIT", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "TAIL", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "COVER", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "DINOSAUR", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "THIEF", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "SPIDER", + "color" : "BLUE", + "isGuessed" : false + } ] + }, + "34LLZ" : { + "winner" : null, + "currentTurn" : "RED", + "currentPhase" : "OPERATIVE", + "currentClue" : { + "word" : "HI", + "guessAmount" : 2 + }, + "cardList" : [ { + "word" : "GROUND", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "SPELL", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "PLATE", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "TAIL", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "EYE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "FAIR", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "LUCK", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "TURKEY", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "MATCH", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "SCUBA DIVER", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "BALL", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "CHANGE", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "UNDERTAKER", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "BEAT", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "HAM", + "color" : "NEUTRAL", + "isGuessed" : false + }, { + "word" : "EAGLE", + "color" : "ASSASSIN", + "isGuessed" : false + }, { + "word" : "CENTAUR", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "SHAKESPEARE", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "PLAY", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "ENGINE", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "GIANT", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "SEAL", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "CAPITAL", + "color" : "BLUE", + "isGuessed" : false + }, { + "word" : "REVOLUTION", + "color" : "RED", + "isGuessed" : false + }, { + "word" : "APPLE", + "color" : "RED", + "isGuessed" : false + } ] + } + } +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java index ee000c4b..81aca161 100644 --- a/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/playingfield/GameManager.java @@ -84,7 +84,6 @@ public GameManager( this.currentTurn = state.currentTurn(); this.currentPhase = state.currentPhase(); this.winner = state.winner(); - this.remainingGuesses = state.remainingGuesses(); this.currentClue = state.currentClue() == null ? null diff --git a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java index d1dfb13c..f54d3442 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/DataTransferObjectService.java @@ -46,8 +46,7 @@ public GameStateDataTransferObject createGameStateDataTransferObject( currentTurn, currentPhase, null, - gameManager.getRemainingGuesses(), - cardDataTransferObject); + cardDataTransferObject); } String word = gameManager.getCurrentClue().word(); int guessAmount = gameManager.getCurrentClue().guessAmount(); @@ -56,7 +55,6 @@ public GameStateDataTransferObject createGameStateDataTransferObject( currentTurn, currentPhase, new ClueDto(word, guessAmount), - gameManager.getRemainingGuesses(), - cardDataTransferObject); + cardDataTransferObject); } } diff --git a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java index 54eb2c7c..95f9c982 100644 --- a/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java +++ b/src/main/java/com/codenames/codenames/backend/serialization/GameStateDataTransferObject.java @@ -8,17 +8,15 @@ /** * Represents the current state of the game to be serialized into JSON. * - * @param winner the winner - * @param currentTurn the current team who is allowed to make a move + * @param winner the winner + * @param currentTurn the current team who is allowed to make a move * @param currentPhase the current phase (spymaster or operative) - * @param currentClue the current clue object, consisting of word and amount of guesses - * @param remainingGuesses amount of guesses left for the current turn - * @param cardList the cards on the board + * @param currentClue the current clue object, consisting of word and amount of guesses + * @param cardList the cards on the board */ public record GameStateDataTransferObject( Team winner, Team currentTurn, Role currentPhase, ClueDto currentClue, - int remainingGuesses, List cardList) {} diff --git a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java index 1c56dd67..44022629 100644 --- a/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/controller/GameSocketControllerTest.java @@ -107,6 +107,6 @@ void passTurnShouldPersistAndBroadcastUpdatedState() { } private GameStateDataTransferObject createGameStateDataTransferObject() { - return new GameStateDataTransferObject(null, Team.RED, null, null, 0, List.of()); + return new GameStateDataTransferObject(null, Team.RED, null, null, List.of()); } } diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java index 0fdce572..70bf4956 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java @@ -44,8 +44,7 @@ void testCreateFromSnapshotWithClue() { Team.BLUE, Role.OPERATIVE, new ClueDto("ANIMAL", 2), - 2, - List.of( + List.of( new CardDataTransferObject("Dog", Color.RED, true), new CardDataTransferObject("Cat", Color.BLUE, false))); @@ -70,8 +69,7 @@ void testCreateFromSnapshotWithoutClue() { Team.BLUE, Role.SPYMASTER, null, - 0, - List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); + List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); @@ -89,7 +87,6 @@ void testCreateFromSnapshotMapsCardsAndCountsGuessedCardsByColor() { Team.RED, Role.OPERATIVE, null, - 1, List.of( new CardDataTransferObject("Dog", Color.RED, true), new CardDataTransferObject("Cat", Color.RED, false), diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index 40e934c7..f4803cea 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -336,7 +336,7 @@ void testPassTurn_throwsWhenSpymaster() { @Test void recoveryConstructorThrowsWhenCardsAreNull() { GameStateDataTransferObject state = - new GameStateDataTransferObject(null, Team.RED, Role.SPYMASTER, null, 0, null); + new GameStateDataTransferObject(null, Team.RED, Role.SPYMASTER, null, null); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -345,7 +345,7 @@ void recoveryConstructorThrowsWhenCardsAreNull() { @Test void recoveryConstructorThrowsWhenCardsAreEmpty() { GameStateDataTransferObject state = - new GameStateDataTransferObject(null, Team.RED, Role.SPYMASTER, null, 0, List.of()); + new GameStateDataTransferObject(null, Team.RED, Role.SPYMASTER, null, List.of()); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -357,7 +357,7 @@ void recoveryConstructorThrowsWhenCurrentTurnIsNull() { List.of(new CardDataTransferObject("Dog", Color.RED, false)); GameStateDataTransferObject state = - new GameStateDataTransferObject(null, null, Role.SPYMASTER, null, 0, cards); + new GameStateDataTransferObject(null, null, Role.SPYMASTER, null, cards); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -369,7 +369,7 @@ void recoveryConstructorThrowsWhenCurrentPhaseIsNull() { List.of(new CardDataTransferObject("Dog", Color.RED, false)); GameStateDataTransferObject state = - new GameStateDataTransferObject(null, Team.RED, null, null, 0, cards); + new GameStateDataTransferObject(null, Team.RED, null, null, cards); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -384,7 +384,7 @@ void recoveryConstructorRestoresPersistedState() { GameStateDataTransferObject state = new GameStateDataTransferObject( - Team.RED, Team.BLUE, Role.OPERATIVE, new ClueDto("ANIMAL", 2), 2, cards); + Team.RED, Team.BLUE, Role.OPERATIVE, new ClueDto("ANIMAL", 2), cards); GameManager restored = new GameManager(state, mockClueValidationService); diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java index d1e17882..c8b01b29 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameServiceTest.java @@ -112,8 +112,7 @@ void testGetCurrentGameState() { redTeam, Role.SPYMASTER, new ClueDto("ANIMAL", 2), - 2, - List.of(new CardDataTransferObject("Dog", Color.RED, false))); + List.of(new CardDataTransferObject("Dog", Color.RED, false))); when(mockGameManager.getCurrentTurn()).thenReturn(redTeam); when(mockGameManager.getCurrentPhase()).thenReturn(Role.SPYMASTER); when(mockGameManager.getRemainingGuesses()).thenReturn(2); @@ -143,8 +142,7 @@ void getGameSnapshotsShouldReturnAllCurrentGameStates() { redTeam, Role.SPYMASTER, new ClueDto("ANIMAL", 2), - 2, - List.of(new CardDataTransferObject("Dog", Color.RED, false))); + List.of(new CardDataTransferObject("Dog", Color.RED, false))); when(mockGameManager.getCurrentTurn()).thenReturn(redTeam); when(mockGameManager.getCurrentPhase()).thenReturn(Role.SPYMASTER); diff --git a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java index 86abe926..b03acf68 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/JsonStateStoreTest.java @@ -59,7 +59,7 @@ void saveAndLoadRoundTrip() { GameStateDataTransferObject gameSnapshot = new GameStateDataTransferObject( - null, Team.RED, Role.OPERATIVE, new ClueDto("ANIMAL", 2), 2, List.of()); + null, Team.RED, Role.OPERATIVE, new ClueDto("ANIMAL", 2), List.of()); SystemSnapshot expectedSnapshot = new SystemSnapshot( @@ -88,7 +88,6 @@ void saveAndLoadRoundTrip() { GameStateDataTransferObject actualGameSnapshot = actualSnapshot.games().get("ABCDE"); assertEquals(Team.RED, actualGameSnapshot.currentTurn()); assertEquals(Role.OPERATIVE, actualGameSnapshot.currentPhase()); - assertEquals(2, actualGameSnapshot.remainingGuesses()); assertEquals("ANIMAL", actualGameSnapshot.currentClue().word()); assertEquals(2, actualGameSnapshot.currentClue().guessAmount()); assertTrue(actualGameSnapshot.cardList().isEmpty()); diff --git a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java index 30a757ba..890b95e2 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java @@ -71,8 +71,7 @@ void recoverOnStartupRestoresLobbiesAndGamesFromSnapshot() { Team.RED, Role.OPERATIVE, new ClueDto("ANIMAL", 2), - 2, - List.of( + List.of( new CardDataTransferObject("Dog", Color.RED, true), new CardDataTransferObject("Cat", Color.BLUE, false))); SystemSnapshot snapshot = @@ -215,8 +214,7 @@ void recoverOnStartupRestoresOnlyGamesWhenLobbiesMapIsNull() { Team.BLUE, Role.SPYMASTER, null, - 0, - List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); + List.of(new CardDataTransferObject("Tree", Color.BLUE, false))); SystemSnapshot snapshot = new SystemSnapshot( SystemSnapshot.CURRENT_SCHEMA_VERSION, null, Map.of("ABCDE", gameSnapshot)); diff --git a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java index 0097b0c0..12050802 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/DataTransferObjectServiceTest.java @@ -90,6 +90,5 @@ void testCreateGameStateDataTransferObject() { assertEquals(0, dto.cardList().size()); assertEquals(redTeam, dto.currentTurn()); assertEquals(operative, dto.currentPhase()); - assertEquals(3, dto.remainingGuesses()); } } diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index 403ee7f2..f140d101 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -35,7 +35,7 @@ void setUp() { dummyList = List.of(new CardDataTransferObject("TEST", null, false)); dummyGameState = new GameStateDataTransferObject( - redTeam, redTeam, spymaster, new ClueDto("Test", 1), 1, dummyList); + redTeam, redTeam, spymaster, new ClueDto("Test", 1), dummyList); } @Test diff --git a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java index e630b4f1..815bde5d 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/GameControllerTest.java @@ -162,6 +162,6 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { } private GameStateDataTransferObject createGameStatePayload() { - return new GameStateDataTransferObject(null, null, null, null, 0, List.of()); + return new GameStateDataTransferObject(null, null, null, null, List.of()); } } From c40c04e05cf23c5fd59a39acc88107cf2982fa16 Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 20 May 2026 22:13:51 +0200 Subject: [PATCH 205/207] test: remove assertions from tests that were testing the removed parameter --- .../codenames/backend/playingfield/GameManagerFactoryTest.java | 1 - .../codenames/backend/playingfield/GameManagerTest.java | 1 - .../backend/recovery/SystemStateRecoveryServiceTest.java | 1 - .../codenames/backend/serialization/SerializationJsonTest.java | 2 +- 4 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java index 70bf4956..25e72ad7 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerFactoryTest.java @@ -54,7 +54,6 @@ void testCreateFromSnapshotWithClue() { assertEquals(Team.BLUE, recovered.getCurrentTurn()); assertEquals(Role.OPERATIVE, recovered.getCurrentPhase()); assertEquals("ANIMAL", recovered.getCurrentClueWord()); - assertEquals(2, recovered.getRemainingGuesses()); assertEquals(1, recovered.getCurrentRedFound()); assertEquals(0, recovered.getCurrentBlueFound()); assertEquals(Team.RED, recovered.getWinner()); diff --git a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java index f4803cea..615386a3 100644 --- a/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/playingfield/GameManagerTest.java @@ -390,7 +390,6 @@ void recoveryConstructorRestoresPersistedState() { assertEquals(Team.BLUE, restored.getCurrentTurn()); assertEquals(Role.OPERATIVE, restored.getCurrentPhase()); - assertEquals(2, restored.getRemainingGuesses()); assertEquals("ANIMAL", restored.getCurrentClueWord()); assertEquals(Team.RED, restored.getWinner()); assertEquals(1, restored.getCurrentRedFound()); diff --git a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java index 890b95e2..a0d7528d 100644 --- a/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/recovery/SystemStateRecoveryServiceTest.java @@ -94,7 +94,6 @@ void recoverOnStartupRestoresLobbiesAndGamesFromSnapshot() { GameManager restoredGame = context.gameService().getGameState("ABCDE"); assertEquals(Team.RED, restoredGame.getCurrentTurn()); assertEquals(Role.OPERATIVE, restoredGame.getCurrentPhase()); - assertEquals(2, restoredGame.getRemainingGuesses()); assertEquals("ANIMAL", restoredGame.getCurrentClue().word()); assertTrue(restoredGame.getCardList().get(0).isGuessed()); assertFalse(restoredGame.getCardList().get(1).isGuessed()); diff --git a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java index f140d101..e2b109c1 100644 --- a/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java +++ b/src/test/java/com/codenames/codenames/backend/serialization/SerializationJsonTest.java @@ -42,7 +42,7 @@ void setUp() { void testSerialize_pass() { String expectedResult = """ - {"winner":"RED","currentTurn":"RED","currentPhase":"SPYMASTER","currentClue":{"word":"Test","guessAmount":1},"remainingGuesses":1,"cardList":[{"word":"TEST","color":null,"isGuessed":false}]}"""; + {"winner":"RED","currentTurn":"RED","currentPhase":"SPYMASTER","currentClue":{"word":"Test","guessAmount":1},"cardList":[{"word":"TEST","color":null,"isGuessed":false}]}"""; String result = serializer.serialize(dummyGameState); assertEquals(expectedResult, result); } From b20de7ed52b7db080d00fc01fe7b77092c1f07c6 Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 20 May 2026 22:17:08 +0200 Subject: [PATCH 206/207] changes to gitignore: excludes data/state.json --- .gitignore | 3 + data/state.json | 600 ------------------------------------------------ 2 files changed, 3 insertions(+), 600 deletions(-) delete mode 100644 data/state.json diff --git a/.gitignore b/.gitignore index a71cd2b8..f8a64ed3 100644 --- a/.gitignore +++ b/.gitignore @@ -30,3 +30,6 @@ build/ ### VS Code ### .vscode/ + +### State Persistence ### +data/ diff --git a/data/state.json b/data/state.json deleted file mode 100644 index 2bac2bd0..00000000 --- a/data/state.json +++ /dev/null @@ -1,600 +0,0 @@ -{ - "schemaVersion" : 2, - "lobbies" : { - "PRTKG" : [ { - "username" : "c", - "team" : "BLUE", - "role" : "OPERATIVE", - "isHost" : true - }, { - "username" : "a", - "team" : "BLUE", - "role" : "SPYMASTER", - "isHost" : false - } ], - "95WL7" : [ { - "username" : "anna2", - "team" : "BLUE", - "role" : "OPERATIVE", - "isHost" : true - }, { - "username" : "anna", - "team" : "BLUE", - "role" : "OPERATIVE", - "isHost" : false - } ], - "D44MV" : [ { - "username" : "anna", - "team" : "BLUE", - "role" : "OPERATIVE", - "isHost" : true - }, { - "username" : "anna2", - "team" : "BLUE", - "role" : "OPERATIVE", - "isHost" : false - } ], - "Y8XE7" : [ { - "username" : "hi", - "team" : "BLUE", - "role" : "SPYMASTER", - "isHost" : true - }, { - "username" : "ho", - "team" : "BLUE", - "role" : "OPERATIVE", - "isHost" : false - } ], - "34LLZ" : [ { - "username" : "ana", - "team" : "BLUE", - "role" : "SPYMASTER", - "isHost" : true - }, { - "username" : "ass", - "team" : "BLUE", - "role" : "OPERATIVE", - "isHost" : false - } ] - }, - "games" : { - "PRTKG" : { - "winner" : null, - "currentTurn" : "BLUE", - "currentPhase" : "SPYMASTER", - "currentClue" : null, - "cardList" : [ { - "word" : "BOND", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "AZTEC", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "MICROSCOPE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "TOWER", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "TAP", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "WAVE", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "CRICKET", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "CZECH", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "SPY", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "SOCK", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "SPOT", - "color" : "ASSASSIN", - "isGuessed" : false - }, { - "word" : "BRIDGE", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "FORK", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "LASER", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "BOW", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "HEART", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "SOLDIER", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "DRESS", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "PIRATE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "PIN", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "LEMON", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "BAND", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "DEGREE", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "GLOVE", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "ROW", - "color" : "BLUE", - "isGuessed" : false - } ] - }, - "95WL7" : { - "winner" : null, - "currentTurn" : "RED", - "currentPhase" : "SPYMASTER", - "currentClue" : null, - "cardList" : [ { - "word" : "PAPER", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "CHICK", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "DRAFT", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "UNDERTAKER", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "EUROPE", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "DWARF", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "HAND", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "SUIT", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "HORN", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "BOND", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "SHIP", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "ANTARCTICA", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "BERRY", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "POLE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "DISEASE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "ATLANTIS", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "DROP", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "WITCH", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "SPIKE", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "BEACH", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "TABLET", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "MARBLE", - "color" : "ASSASSIN", - "isGuessed" : false - }, { - "word" : "TOWER", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "STRIKE", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "LEMON", - "color" : "NEUTRAL", - "isGuessed" : false - } ] - }, - "D44MV" : { - "winner" : null, - "currentTurn" : "BLUE", - "currentPhase" : "SPYMASTER", - "currentClue" : null, - "cardList" : [ { - "word" : "SPRING", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "TRACK", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "TELESCOPE", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "GLASS", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "ALPS", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "BED", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "FALL", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "MUG", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "BATTERY", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "VACUUM", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "HORN", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "BOX", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "BRIDGE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "JUPITER", - "color" : "ASSASSIN", - "isGuessed" : false - }, { - "word" : "BOW", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "FIGURE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "TRIANGLE", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "SHAKESPEARE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "BANK", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "MAMMOTH", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "CROSS", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "BILL", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "STICK", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "ANGEL", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "BEAR", - "color" : "RED", - "isGuessed" : false - } ] - }, - "Y8XE7" : { - "winner" : null, - "currentTurn" : "RED", - "currentPhase" : "SPYMASTER", - "currentClue" : null, - "cardList" : [ { - "word" : "STRAW", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "SLUG", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "ATLANTIS", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "SHIP", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "WALL", - "color" : "ASSASSIN", - "isGuessed" : false - }, { - "word" : "APPLE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "KING", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "SPOT", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "BRUSH", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "TRIANGLE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "DRILL", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "TORCH", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "BATTERY", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "UNDERTAKER", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "DEATH", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "STRIKE", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "MAPLE", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "KNIFE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "HOSPITAL", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "RABBIT", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "TAIL", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "COVER", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "DINOSAUR", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "THIEF", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "SPIDER", - "color" : "BLUE", - "isGuessed" : false - } ] - }, - "34LLZ" : { - "winner" : null, - "currentTurn" : "RED", - "currentPhase" : "OPERATIVE", - "currentClue" : { - "word" : "HI", - "guessAmount" : 2 - }, - "cardList" : [ { - "word" : "GROUND", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "SPELL", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "PLATE", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "TAIL", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "EYE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "FAIR", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "LUCK", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "TURKEY", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "MATCH", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "SCUBA DIVER", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "BALL", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "CHANGE", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "UNDERTAKER", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "BEAT", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "HAM", - "color" : "NEUTRAL", - "isGuessed" : false - }, { - "word" : "EAGLE", - "color" : "ASSASSIN", - "isGuessed" : false - }, { - "word" : "CENTAUR", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "SHAKESPEARE", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "PLAY", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "ENGINE", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "GIANT", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "SEAL", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "CAPITAL", - "color" : "BLUE", - "isGuessed" : false - }, { - "word" : "REVOLUTION", - "color" : "RED", - "isGuessed" : false - }, { - "word" : "APPLE", - "color" : "RED", - "isGuessed" : false - } ] - } - } -} \ No newline at end of file From 5a6d05ccc7cf34d3cae71cf33aabff2c1cdf5f1d Mon Sep 17 00:00:00 2001 From: the-only-queen-anna Date: Wed, 20 May 2026 22:24:26 +0200 Subject: [PATCH 207/207] fix: add assertions to test w/o assertions --- .../backend/websocket/WebSocketEventListenerTest.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java b/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java index 30c61d96..88577291 100644 --- a/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java +++ b/src/test/java/com/codenames/codenames/backend/websocket/WebSocketEventListenerTest.java @@ -43,6 +43,8 @@ void shouldIgnoreDisconnectWhenUsernameIsNull() { when(event.getSessionId()).thenReturn(TEST_SESSION_ID); listener.handleDisconnect(event); + + assertNull(registry.getUser(TEST_SESSION_ID)); } @Test @@ -64,6 +66,8 @@ void shouldIgnoreUnknownSession() { when(event.getSessionId()).thenReturn("unknown"); listener.handleDisconnect(event); + + assertNull(registry.getUser(TEST_SESSION_ID)); } @Test @@ -88,5 +92,7 @@ private void removeLobbyMappingForTestSession() throws Exception { Map sessionToLobby = (Map) lobbyField.get(registry); sessionToLobby.remove(TEST_SESSION_ID); + + assertNull(registry.getLobby(TEST_SESSION_ID)); } }