diff --git a/src/main/java/com/codenames/codenames/backend/chat/api/dto/ChatMessageType.java b/src/main/java/com/codenames/codenames/backend/chat/api/dto/ChatMessageType.java index c6f78b84..07dcf5ef 100644 --- a/src/main/java/com/codenames/codenames/backend/chat/api/dto/ChatMessageType.java +++ b/src/main/java/com/codenames/codenames/backend/chat/api/dto/ChatMessageType.java @@ -4,5 +4,6 @@ * Enum representing the type of chat message. */ public enum ChatMessageType { - CHAT -} + CHAT, + SYSTEM +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/database/entity/GameStateEntity.java b/src/main/java/com/codenames/codenames/backend/database/entity/GameStateEntity.java index ee0832bb..2fd26858 100644 --- a/src/main/java/com/codenames/codenames/backend/database/entity/GameStateEntity.java +++ b/src/main/java/com/codenames/codenames/backend/database/entity/GameStateEntity.java @@ -37,4 +37,8 @@ public class GameStateEntity { private int clueGuessAmount; @Column(name = "remaining_guesses") private int remainingGuesses; + @Column(name = "red_team_cheat_used", nullable = false) + private boolean redTeamCheatUsed; + @Column(name = "blue_team_cheat_used", nullable = false) + private boolean blueTeamCheatUsed; } diff --git a/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceMapper.java b/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceMapper.java index 7e0531d9..7eb4e6d7 100644 --- a/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceMapper.java +++ b/src/main/java/com/codenames/codenames/backend/database/persistence/PersistenceMapper.java @@ -78,6 +78,13 @@ private GameStateEntity mapGameState( gameStateEntity.setClueGuessAmount(0); gameStateEntity.setRemainingGuesses(0); } + + gameStateEntity.setRedTeamCheatUsed( + gameManager.isRedTeamCheatUsed()); + + gameStateEntity.setBlueTeamCheatUsed( + gameManager.isBlueTeamCheatUsed()); + return gameStateEntity; } diff --git a/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java index 0771e25d..3bf4c41c 100644 --- a/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java +++ b/src/main/java/com/codenames/codenames/backend/database/restoration/RestorationMapper.java @@ -46,7 +46,6 @@ public GameStateDto mapToGameStateDto(LobbyEntity lobbyEntity) { throw new IllegalStateException("Game state entity is null"); } - Team winner = null; // valueOf takes string and returns enum of that string Team currentTeam = Team.valueOf(gameStateEntity.getCurrentTurn()); Role currentPhase = Role.valueOf(gameStateEntity.getCurrentPhase()); @@ -56,8 +55,15 @@ public GameStateDto mapToGameStateDto(LobbyEntity lobbyEntity) { } int remainingGuesses = gameStateEntity.getRemainingGuesses(); List cardList = mapToCardDto(lobbyEntity); - return new GameStateDto(winner, currentTeam, currentPhase, clueDto, - remainingGuesses, cardList); + return new GameStateDto( + null, + currentTeam, + currentPhase, + clueDto, + remainingGuesses, + cardList, + gameStateEntity.isRedTeamCheatUsed(), + gameStateEntity.isBlueTeamCheatUsed()); } private List mapToCardDto(LobbyEntity lobbyEntity) { diff --git a/src/main/java/com/codenames/codenames/backend/game/api/GameSocketController.java b/src/main/java/com/codenames/codenames/backend/game/api/GameSocketController.java index 1c3aa35b..9bb465d5 100644 --- a/src/main/java/com/codenames/codenames/backend/game/api/GameSocketController.java +++ b/src/main/java/com/codenames/codenames/backend/game/api/GameSocketController.java @@ -1,11 +1,16 @@ package com.codenames.codenames.backend.game.api; +import com.codenames.codenames.backend.chat.api.dto.ChatDto; +import com.codenames.codenames.backend.chat.api.dto.ChatMessageType; import com.codenames.codenames.backend.database.persistence.PersistenceService; +import com.codenames.codenames.backend.game.api.dto.CheatCardMessage; import com.codenames.codenames.backend.game.api.dto.ClueMessage; import com.codenames.codenames.backend.game.api.dto.PassTurnMessage; import com.codenames.codenames.backend.game.api.dto.RevealCardMessage; import com.codenames.codenames.backend.game.api.dto.StartGameMessage; +import com.codenames.codenames.backend.game.application.CheatService; import com.codenames.codenames.backend.game.application.GameService; +import com.codenames.codenames.backend.game.domain.CheatResult; import com.codenames.codenames.backend.game.domain.Clue; import org.springframework.messaging.handler.annotation.MessageMapping; import org.springframework.messaging.simp.SimpMessagingTemplate; @@ -21,6 +26,7 @@ public class GameSocketController { private final GameService gameService; + private final CheatService cheatService; private final SimpMessagingTemplate messagingTemplate; private final PersistenceService persistenceService; @@ -33,15 +39,53 @@ 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 + * @param cheatService service responsible for cheat handling */ public GameSocketController( GameService gameService, SimpMessagingTemplate messagingTemplate, - PersistenceService persistenceService) { + PersistenceService persistenceService, + CheatService cheatService) { this.gameService = gameService; this.messagingTemplate = messagingTemplate; this.persistenceService = persistenceService; + this.cheatService = cheatService; + } + + /** + * Handles a cheat request and sends the result only to the requesting player. + * + * @param message the cheat request containing lobby, username and selected cards + */ + @MessageMapping("/cheat") + public void useCheat(CheatCardMessage message) { + + CheatResult result = + cheatService.useCheat( + message.getLobbyCode(), + message.getUsername(), + message.getPositions()); + + if (result == null) { + return; + } + + ChatDto systemMessage = + new ChatDto( + "System", + result.message(), + ChatMessageType.SYSTEM); + + messagingTemplate.convertAndSend( + "/topic/chat/" + + message.getLobbyCode() + + "/" + + result.team() + + "/operative", + systemMessage); + + persistenceService.saveSnapShot(message.getLobbyCode()); } /** diff --git a/src/main/java/com/codenames/codenames/backend/game/api/dto/CheatCardMessage.java b/src/main/java/com/codenames/codenames/backend/game/api/dto/CheatCardMessage.java new file mode 100644 index 00000000..dba6690f --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/api/dto/CheatCardMessage.java @@ -0,0 +1,16 @@ +package com.codenames.codenames.backend.game.api.dto; + +import java.util.List; +import lombok.Getter; +import lombok.Setter; + +/** + * WebSocket message for requesting a cheat check on selected cards. + */ +@Getter +@Setter +public class CheatCardMessage { + private String lobbyCode; + private String username; + private List positions; +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/game/api/dto/GameStateDto.java b/src/main/java/com/codenames/codenames/backend/game/api/dto/GameStateDto.java index 1e83f8be..9b0e390f 100644 --- a/src/main/java/com/codenames/codenames/backend/game/api/dto/GameStateDto.java +++ b/src/main/java/com/codenames/codenames/backend/game/api/dto/GameStateDto.java @@ -19,4 +19,7 @@ public record GameStateDto( Role currentPhase, ClueDto currentClue, int remainingGuesses, - List cardList) {} + List cardList, + boolean redTeamCheatUsed, + boolean blueTeamCheatUsed) { +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/game/application/CheatService.java b/src/main/java/com/codenames/codenames/backend/game/application/CheatService.java new file mode 100644 index 00000000..8b534fdf --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/application/CheatService.java @@ -0,0 +1,46 @@ +package com.codenames.codenames.backend.game.application; + +import com.codenames.codenames.backend.game.domain.CheatResult; +import com.codenames.codenames.backend.lobby.application.LobbyService; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; +import java.util.List; +import org.springframework.stereotype.Service; + +/** Service responsible for validating and executing cheat requests. */ +@Service +public class CheatService { + + private final GameService gameService; + private final LobbyService lobbyService; + + /** + * Creates a new cheat service. + * + * @param gameService service responsible for game logic + * @param lobbyService service responsible for lobby/player data + */ + public CheatService(GameService gameService, LobbyService lobbyService) { + this.gameService = gameService; + this.lobbyService = lobbyService; + } + + /** + * Performs a cheat request for a player. + * + * @param lobbyCode the lobby code + * @param username the requesting username + * @param positions selected card positions + * @return the cheat result or null if the request is invalid + */ + public CheatResult useCheat(String lobbyCode, String username, List positions) { + Team team = lobbyService.getPlayerTeam(username, lobbyCode); + Role role = lobbyService.getPlayerRole(username, lobbyCode); + + if (team == null || role != Role.OPERATIVE) { + return null; + } + + return gameService.useCheat(lobbyCode, positions, team); + } +} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/game/application/GameService.java b/src/main/java/com/codenames/codenames/backend/game/application/GameService.java index cdb57463..fc7df620 100644 --- a/src/main/java/com/codenames/codenames/backend/game/application/GameService.java +++ b/src/main/java/com/codenames/codenames/backend/game/application/GameService.java @@ -1,11 +1,13 @@ package com.codenames.codenames.backend.game.application; import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.domain.CheatResult; import com.codenames.codenames.backend.game.domain.Clue; import com.codenames.codenames.backend.game.domain.GameManager; import com.codenames.codenames.backend.game.domain.GameManagerFactory; import com.codenames.codenames.backend.game.mapping.DataTransferObjectService; import com.codenames.codenames.backend.lobby.domain.Team; +import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import lombok.extern.slf4j.Slf4j; @@ -162,4 +164,20 @@ public Map getGameSnapshots() { .collect( java.util.stream.Collectors.toMap(lobbyCode -> lobbyCode, this::getCurrentGameState)); } + + /** + * Performs a cheat request for the given team. + * + * @param lobbyCode the lobby code of the game + * @param positions the selected card positions + * @param team the team requesting the cheat + * @return the cheat result or null if the request is invalid + */ + public CheatResult useCheat( + String lobbyCode, + List positions, + Team team) { + + return getGame(lobbyCode).useCheat(positions, team); + } } diff --git a/src/main/java/com/codenames/codenames/backend/game/domain/CheatResult.java b/src/main/java/com/codenames/codenames/backend/game/domain/CheatResult.java new file mode 100644 index 00000000..94fdfc03 --- /dev/null +++ b/src/main/java/com/codenames/codenames/backend/game/domain/CheatResult.java @@ -0,0 +1,12 @@ +package com.codenames.codenames.backend.game.domain; + +import com.codenames.codenames.backend.lobby.domain.Team; +/** + * Result of a cheat attempt. + * + * @param message private message for the player + * @param team the team that used the cheat + */ +public record CheatResult( + String message, + Team team) {} \ No newline at end of file diff --git a/src/main/java/com/codenames/codenames/backend/game/domain/GameManager.java b/src/main/java/com/codenames/codenames/backend/game/domain/GameManager.java index 2ed4e1de..72d31828 100644 --- a/src/main/java/com/codenames/codenames/backend/game/domain/GameManager.java +++ b/src/main/java/com/codenames/codenames/backend/game/domain/GameManager.java @@ -35,6 +35,9 @@ public class GameManager { @Getter private Team currentTurn; @Getter private Role currentPhase; + @Getter private boolean redTeamCheatUsed = false; + @Getter private boolean blueTeamCheatUsed = false; + /** * Constructor for a new GameManager and initializes the playing board. * @@ -83,6 +86,8 @@ public GameManager( this.currentTurn = state.currentTurn(); this.currentPhase = state.currentPhase(); this.winner = state.winner(); + this.redTeamCheatUsed = state.redTeamCheatUsed(); + this.blueTeamCheatUsed = state.blueTeamCheatUsed(); this.currentClue = state.currentClue() == null ? null @@ -304,4 +309,78 @@ private void checkCorrectTurn(Team team, Role role) { throw new IllegalStateException("Not your turn/ role"); } } + + /** + * Checks whether a card position is within the valid board range. + * + * @param position the position to validate + * @return true if the position is valid + */ + private boolean isValidPosition(Integer position) { + return position != null + && position >= 0 + && position < board.getCardList().size(); + } + + /** + * Performs the cheat action for the current team. + * The team may use the cheat only once per game. + * If at least one selected card belongs to the team, + * one correct card is returned. + * + * @param positions selected card positions + * @param team the team requesting the cheat + * @return the cheat result or null if the request is invalid + */ + public CheatResult useCheat(List positions, Team team) { + + if (positions == null || positions.isEmpty()) { + return null; + } + + if (team != currentTurn || currentPhase != Role.OPERATIVE) { + return null; + } + + if (team == Team.RED && redTeamCheatUsed) { + return null; + } + + if (team == Team.BLUE && blueTeamCheatUsed) { + return null; + } + + List selectedCards = + positions.stream() + .filter(this::isValidPosition) + .filter(position -> !board.getIsGuessed(position)) + .map(position -> board.getCardList().get(position)) + .toList(); + + if (selectedCards.isEmpty()) { + return null; + } + + List correctCards = + selectedCards.stream() + .filter( + card -> + (team == Team.RED && card.getColor() == Color.RED) + || (team == Team.BLUE && card.getColor() == Color.BLUE)) + .toList(); + + if (team == Team.RED) { + redTeamCheatUsed = true; + } else { + blueTeamCheatUsed = true; + } + + if (correctCards.isEmpty()) { + return new CheatResult("Keine der ausgewählten Karten ist richtig.", team); + } + + Card correctCard = correctCards.get(0); + + return new CheatResult("Die Karte \"" + correctCard.getWord() + "\" ist richtig.", team); + } } diff --git a/src/main/java/com/codenames/codenames/backend/game/mapping/DataTransferObjectService.java b/src/main/java/com/codenames/codenames/backend/game/mapping/DataTransferObjectService.java index a92aefd2..5687b1c2 100644 --- a/src/main/java/com/codenames/codenames/backend/game/mapping/DataTransferObjectService.java +++ b/src/main/java/com/codenames/codenames/backend/game/mapping/DataTransferObjectService.java @@ -48,6 +48,7 @@ public GameStateDto createGameStateDto( log.info("A Game is over. Winner is {}.", gameManager.getWinner()); } int remainingGuesses = gameManager.getRemainingGuesses(); + if (gameManager.getCurrentClue() == null) { return new GameStateDto( gameManager.getWinner(), @@ -55,16 +56,22 @@ public GameStateDto createGameStateDto( currentPhase, null, remainingGuesses, - cardDataTransferObject); + cardDataTransferObject, + gameManager.isRedTeamCheatUsed(), + gameManager.isBlueTeamCheatUsed()); } + String word = gameManager.getCurrentClue().word(); int guessAmount = gameManager.getCurrentClue().guessAmount(); + return new GameStateDto( gameManager.getWinner(), currentTurn, currentPhase, new ClueDto(word, guessAmount), remainingGuesses, - cardDataTransferObject); + cardDataTransferObject, + gameManager.isRedTeamCheatUsed(), + gameManager.isBlueTeamCheatUsed()); } } diff --git a/src/main/resources/db/migration/V2__add_cheat_flags_to_game_state.sql b/src/main/resources/db/migration/V2__add_cheat_flags_to_game_state.sql new file mode 100644 index 00000000..664a2dc9 --- /dev/null +++ b/src/main/resources/db/migration/V2__add_cheat_flags_to_game_state.sql @@ -0,0 +1,5 @@ +ALTER TABLE game_state + ADD COLUMN red_team_cheat_used BOOLEAN DEFAULT FALSE NOT NULL; + +ALTER TABLE game_state + ADD COLUMN blue_team_cheat_used BOOLEAN DEFAULT FALSE NOT NULL; \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationServiceTest.java b/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationServiceTest.java index 97dc1aba..aa347a47 100644 --- a/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/database/restoration/RestorationServiceTest.java @@ -57,7 +57,15 @@ void setUp() { cardDtoList = List.of(new CardDto("TEST", null, false)); gameStateDto = - new GameStateDto(redTeam, redTeam, spymaster, new ClueDto("Test", 1), 2, cardDtoList); + new GameStateDto( + redTeam, + redTeam, + spymaster, + new ClueDto("Test", 1), + 2, + cardDtoList, + false, + false); when(transactionManager.getTransaction(any())).thenReturn(transactionStatus); diff --git a/src/test/java/com/codenames/codenames/backend/game/api/GameSocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/game/api/GameSocketControllerTest.java index 575eae91..f74d08bf 100644 --- a/src/test/java/com/codenames/codenames/backend/game/api/GameSocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/api/GameSocketControllerTest.java @@ -6,13 +6,18 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import com.codenames.codenames.backend.chat.api.dto.ChatDto; +import com.codenames.codenames.backend.chat.api.dto.ChatMessageType; import com.codenames.codenames.backend.database.persistence.PersistenceService; +import com.codenames.codenames.backend.game.api.dto.CheatCardMessage; import com.codenames.codenames.backend.game.api.dto.ClueMessage; import com.codenames.codenames.backend.game.api.dto.GameStateDto; import com.codenames.codenames.backend.game.api.dto.PassTurnMessage; import com.codenames.codenames.backend.game.api.dto.RevealCardMessage; import com.codenames.codenames.backend.game.api.dto.StartGameMessage; +import com.codenames.codenames.backend.game.application.CheatService; import com.codenames.codenames.backend.game.application.GameService; +import com.codenames.codenames.backend.game.domain.CheatResult; import com.codenames.codenames.backend.lobby.domain.Team; import java.util.List; import org.junit.jupiter.api.BeforeEach; @@ -34,11 +39,18 @@ class GameSocketControllerTest { @Mock private PersistenceService persistenceService; + @Mock private CheatService cheatService; + private GameSocketController controller; @BeforeEach void setUp() { - controller = new GameSocketController(gameService, messagingTemplate, persistenceService); + controller = + new GameSocketController( + gameService, + messagingTemplate, + persistenceService, + cheatService); } @Test @@ -107,6 +119,46 @@ void passTurnShouldPersistAndBroadcastUpdatedState() { } private GameStateDto createGameStateDto() { - return new GameStateDto(null, Team.RED, null, null, 0, List.of()); + return new GameStateDto(null, Team.RED, null, null, 0, List.of(), false, false); + } + + @Test + void useCheatShouldSendPrivateSystemMessageAndPersistSnapshot() { + CheatCardMessage message = new CheatCardMessage(); + message.setLobbyCode(LOBBY_CODE); + message.setUsername("Max"); + message.setPositions(List.of(0, 1)); + + CheatResult result = new CheatResult("Die Karte \"Dog\" ist richtig.", Team.RED); + + when(cheatService.useCheat(LOBBY_CODE, "Max", List.of(0, 1))).thenReturn(result); + + controller.useCheat(message); + + verify(cheatService).useCheat(LOBBY_CODE, "Max", List.of(0, 1)); + + verify(messagingTemplate) + .convertAndSend( + "/topic/chat/" + LOBBY_CODE + "/RED/operative", + new ChatDto("System", result.message(), ChatMessageType.SYSTEM)); + + verify(persistenceService).saveSnapShot(LOBBY_CODE); + } + + @Test + void useCheatShouldDoNothingWhenResultIsNull() { + CheatCardMessage message = new CheatCardMessage(); + message.setLobbyCode(LOBBY_CODE); + message.setUsername("Max"); + message.setPositions(List.of(0, 1)); + + when(cheatService.useCheat(LOBBY_CODE, "Max", List.of(0, 1))).thenReturn(null); + + controller.useCheat(message); + + verify(cheatService).useCheat(LOBBY_CODE, "Max", List.of(0, 1)); + verify(messagingTemplate, never()) + .convertAndSendToUser(anyString(), anyString(), any(Object.class)); + verify(persistenceService, never()).saveSnapShot(LOBBY_CODE); } } diff --git a/src/test/java/com/codenames/codenames/backend/game/application/CheatServiceTest.java b/src/test/java/com/codenames/codenames/backend/game/application/CheatServiceTest.java new file mode 100644 index 00000000..dfb3d865 --- /dev/null +++ b/src/test/java/com/codenames/codenames/backend/game/application/CheatServiceTest.java @@ -0,0 +1,75 @@ +package com.codenames.codenames.backend.game.application; + +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.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.codenames.codenames.backend.game.domain.CheatResult; +import com.codenames.codenames.backend.lobby.application.LobbyService; +import com.codenames.codenames.backend.lobby.domain.Role; +import com.codenames.codenames.backend.lobby.domain.Team; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +/** Tests the functionality of CheatService. */ +class CheatServiceTest { + + private static final String LOBBY_CODE = "ABCDE"; + private static final String USERNAME = "Max"; + + private GameService gameService; + private LobbyService lobbyService; + private CheatService cheatService; + + @BeforeEach + void setUp() { + gameService = mock(GameService.class); + lobbyService = mock(LobbyService.class); + cheatService = new CheatService(gameService, lobbyService); + } + + @Test + void useCheatShouldReturnResultForOperative() { + List positions = List.of(0, 1); + CheatResult expected = new CheatResult("Die Karte \"Dog\" ist richtig.", Team.RED); + + when(lobbyService.getPlayerTeam(USERNAME, LOBBY_CODE)).thenReturn(Team.RED); + when(lobbyService.getPlayerRole(USERNAME, LOBBY_CODE)).thenReturn(Role.OPERATIVE); + when(gameService.useCheat(LOBBY_CODE, positions, Team.RED)).thenReturn(expected); + + CheatResult result = cheatService.useCheat(LOBBY_CODE, USERNAME, positions); + + assertEquals(expected, result); + verify(gameService).useCheat(LOBBY_CODE, positions, Team.RED); + } + + @Test + void useCheatShouldReturnNullWhenTeamIsMissing() { + List positions = List.of(0, 1); + + when(lobbyService.getPlayerTeam(USERNAME, LOBBY_CODE)).thenReturn(null); + when(lobbyService.getPlayerRole(USERNAME, LOBBY_CODE)).thenReturn(Role.OPERATIVE); + + CheatResult result = cheatService.useCheat(LOBBY_CODE, USERNAME, positions); + + assertNull(result); + verify(gameService, never()).useCheat(LOBBY_CODE, positions, Team.RED); + } + + @Test + void useCheatShouldReturnNullWhenPlayerIsNotOperative() { + List positions = List.of(0, 1); + + when(lobbyService.getPlayerTeam(USERNAME, LOBBY_CODE)).thenReturn(Team.RED); + when(lobbyService.getPlayerRole(USERNAME, LOBBY_CODE)).thenReturn(Role.SPYMASTER); + + CheatResult result = cheatService.useCheat(LOBBY_CODE, USERNAME, positions); + + assertNull(result); + verify(gameService, never()).useCheat(LOBBY_CODE, positions, Team.RED); + } +} \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/game/application/GameServiceTest.java b/src/test/java/com/codenames/codenames/backend/game/application/GameServiceTest.java index 6089b375..1cfc2611 100644 --- a/src/test/java/com/codenames/codenames/backend/game/application/GameServiceTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/application/GameServiceTest.java @@ -12,6 +12,7 @@ import com.codenames.codenames.backend.game.api.dto.CardDto; import com.codenames.codenames.backend.game.api.dto.ClueDto; import com.codenames.codenames.backend.game.api.dto.GameStateDto; +import com.codenames.codenames.backend.game.domain.CheatResult; import com.codenames.codenames.backend.game.domain.Clue; import com.codenames.codenames.backend.game.domain.Color; import com.codenames.codenames.backend.game.domain.GameManager; @@ -115,7 +116,7 @@ void testGetCurrentGameState() { Role.SPYMASTER, new ClueDto("ANIMAL", 2), 3, - List.of(new CardDto("Dog", Color.RED, false))); + List.of(new CardDto("Dog", Color.RED, false)), false, false); when(mockGameManager.getCurrentTurn()).thenReturn(redTeam); when(mockGameManager.getCurrentPhase()).thenReturn(Role.SPYMASTER); when(mockGameManager.getRemainingGuesses()).thenReturn(2); @@ -146,7 +147,7 @@ void getGameSnapshotsShouldReturnAllCurrentGameStates() { Role.SPYMASTER, new ClueDto("ANIMAL", 2), 3, - List.of(new CardDto("Dog", Color.RED, false))); + List.of(new CardDto("Dog", Color.RED, false)), false, false); when(mockGameManager.getCurrentTurn()).thenReturn(redTeam); when(mockGameManager.getCurrentPhase()).thenReturn(Role.SPYMASTER); @@ -158,4 +159,18 @@ void getGameSnapshotsShouldReturnAllCurrentGameStates() { assertEquals(1, snapshots.size()); assertEquals(expected, snapshots.get(lobbyCode)); } -} + + @Test + void testUseCheat() { + List positions = List.of(0, 1); + CheatResult expected = new CheatResult("Die Karte \"Dog\" ist richtig.", Team.RED); + + when(mockGameManager.useCheat(positions, redTeam)).thenReturn(expected); + + CheatResult result = + gameService.useCheat(lobbyCode, positions, redTeam); + + assertEquals(expected, result); + verify(mockGameManager, times(1)).useCheat(positions, redTeam); + } +} \ No newline at end of file diff --git a/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java b/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java index 7c0f3ac3..27a26e89 100644 --- a/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerFactoryTest.java @@ -45,7 +45,7 @@ void testCreateFromSnapshotWithClue() { Role.OPERATIVE, new ClueDto("ANIMAL", 2), 3, - List.of(new CardDto("Dog", Color.RED, true), new CardDto("Cat", Color.BLUE, false))); + List.of(new CardDto("Dog", Color.RED, true), new CardDto("Cat", Color.BLUE, false)), false, false); GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); @@ -63,7 +63,7 @@ void testCreateFromSnapshotWithClue() { void testCreateFromSnapshotWithoutClue() { GameStateDto snapshot = new GameStateDto( - null, Team.BLUE, Role.SPYMASTER, null, 0, List.of(new CardDto("Tree", Color.BLUE, false))); + null, Team.BLUE, Role.SPYMASTER, null,0, List.of(new CardDto("Tree", Color.BLUE, false)), false, false); GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); @@ -86,7 +86,7 @@ void testCreateFromSnapshotMapsCardsAndCountsGuessedCardsByColor() { new CardDto("Dog", Color.RED, true), new CardDto("Cat", Color.RED, false), new CardDto("Tree", Color.BLUE, true), - new CardDto("Sun", Color.NEUTRAL, true))); + new CardDto("Sun", Color.NEUTRAL, true)), false, false); GameManager recovered = gameManagerFactory.createFromSnapshot(snapshot); diff --git a/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerTest.java b/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerTest.java index be5c4ddf..d3face6b 100644 --- a/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerTest.java +++ b/src/test/java/com/codenames/codenames/backend/game/domain/GameManagerTest.java @@ -335,7 +335,7 @@ void testPassTurn_throwsWhenSpymaster() { @Test void recoveryConstructorThrowsWhenCardsAreNull() { GameStateDto state = - new GameStateDto(null, Team.RED, Role.SPYMASTER, null, 0, null); + new GameStateDto(null, Team.RED, Role.SPYMASTER, null, 0, null, false, false); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -344,7 +344,7 @@ void recoveryConstructorThrowsWhenCardsAreNull() { @Test void recoveryConstructorThrowsWhenCardsAreEmpty() { GameStateDto state = - new GameStateDto(null, Team.RED, Role.SPYMASTER, null, 0, List.of()); + new GameStateDto(null, Team.RED, Role.SPYMASTER, null, 0, List.of(), false, false); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -356,7 +356,7 @@ void recoveryConstructorThrowsWhenCurrentTurnIsNull() { List.of(new CardDto("Dog", Color.RED, false)); GameStateDto state = - new GameStateDto(null, null, Role.SPYMASTER, null, 0, cards); + new GameStateDto(null, null, Role.SPYMASTER, null, 0, cards, false, false); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -368,7 +368,7 @@ void recoveryConstructorThrowsWhenCurrentPhaseIsNull() { List.of(new CardDto("Dog", Color.RED, false)); GameStateDto state = - new GameStateDto(null, Team.RED, null, null, 0, cards); + new GameStateDto(null, Team.RED, null, null, 0, cards, false, false); assertThrows( IllegalArgumentException.class, () -> new GameManager(state, mockClueValidationService)); @@ -383,7 +383,7 @@ void recoveryConstructorRestoresPersistedState() { GameStateDto state = new GameStateDto( - Team.RED, Team.BLUE, Role.OPERATIVE, new ClueDto("ANIMAL", 2), 3, cards); + Team.RED, Team.BLUE, Role.OPERATIVE, new ClueDto("ANIMAL", 2), 3, cards, false, false); GameManager restored = new GameManager(state, mockClueValidationService); @@ -395,4 +395,73 @@ void recoveryConstructorRestoresPersistedState() { assertEquals(0, restored.getCurrentBlueFound()); assertTrue(restored.getCardList().get(0).isGuessed()); } + + @Test + void useCheatReturnsNullWhenNoCardsSelected() { + helperMethodAdvanceTurns(gameManager, 1); + + CheatResult result = gameManager.useCheat(List.of(), redTeam); + + assertNull(result); + } + + @Test + void useCheatReturnsNullWhenNotOperativePhase() { + CheatResult result = gameManager.useCheat(List.of(0), redTeam); + + assertNull(result); + } + + @Test + void useCheatReturnsCorrectCardNameWhenSelectedCardsContainTeamCard() { + mockCardGeneration( + List.of( + new Card("Dog", Color.BLUE), + new Card("Cat", Color.RED), + new Card("Tree", Color.NEUTRAL))); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); + helperMethodAdvanceTurns(gameManager, 1); + + CheatResult result = gameManager.useCheat(List.of(0, 1, 2), redTeam); + + assertEquals("Die Karte \"Cat\" ist richtig.", result.message()); + assertTrue(gameManager.isRedTeamCheatUsed()); + } + + @Test + void useCheatReturnsNoCorrectCardMessageWhenNoSelectedCardIsCorrect() { + mockCardGeneration( + List.of( + new Card("Dog", Color.BLUE), + new Card("Tree", Color.NEUTRAL))); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); + helperMethodAdvanceTurns(gameManager, 1); + + CheatResult result = gameManager.useCheat(List.of(0, 1), redTeam); + + assertEquals("Keine der ausgewählten Karten ist richtig.", result.message()); + assertTrue(gameManager.isRedTeamCheatUsed()); + } + + @Test + void useCheatCanOnlyBeUsedOncePerTeam() { + mockCardGeneration(List.of(new Card("Dog", Color.RED))); + gameManager = new GameManager(redTeam, mockCardGenerator, mockClueValidationService); + helperMethodAdvanceTurns(gameManager, 1); + + CheatResult firstResult = gameManager.useCheat(List.of(0), redTeam); + CheatResult secondResult = gameManager.useCheat(List.of(0), redTeam); + + assertEquals("Die Karte \"Dog\" ist richtig.", firstResult.message()); + assertNull(secondResult); + } + + @Test + void useCheatReturnsNullWhenOnlyInvalidPositionsAreSelected() { + helperMethodAdvanceTurns(gameManager, 1); + + CheatResult result = gameManager.useCheat(List.of(-1, 99), redTeam); + + assertNull(result); + } } diff --git a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java index 058e7435..a657b8f6 100644 --- a/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java +++ b/src/test/java/com/codenames/codenames/backend/lobby/api/LobbySocketControllerTest.java @@ -166,6 +166,6 @@ void shouldTreatExistingPlayerAsReconnectWhenJoinReturnsFalse() { } private GameStateDto createGameStatePayload() { - return new GameStateDto(null, null, null, null, 0, List.of()); + return new GameStateDto(null, null, null, null, 0, List.of(), false, false); } }