Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
966f826
Merge remote-tracking branch 'origin/development' into feature/124-ch…
tasaje1 Jun 17, 2026
94b4031
feat: add expose cheat methods
tasaje1 Jun 17, 2026
bdf7d60
feat: add expose cheat penalty handling
tasaje1 Jun 17, 2026
9abf9d6
feat: expose cheat penalty handling
tasaje1 Jun 17, 2026
250ca6c
feat: add expose cheat flow to cheat service
tasaje1 Jun 17, 2026
111472f
feat: add expose cheat websocket endpoint
tasaje1 Jun 17, 2026
3d30a4e
feat: skip opponent turn on correct cheat exposure
tasaje1 Jun 17, 2026
4c16d5f
test: add expose cheat backend coverage
tasaje1 Jun 17, 2026
de2c8a5
feat: return expose cheat result
tasaje1 Jun 17, 2026
e574f1a
test: cover expose cheat result messages
tasaje1 Jun 17, 2026
4ddac37
feat: add expose cheat result dto
tasaje1 Jun 18, 2026
5a26fda
Merge branch 'development' into feature/124-cheat-detection
tasaje1 Jun 18, 2026
d004cee
fix: resolve player UUID before validating cheat requests
tasaje1 Jun 21, 2026
20bc748
refactor: extract shared operative team resolution
tasaje1 Jun 21, 2026
83af081
test: cover player resolution in cheat service
tasaje1 Jun 21, 2026
0887947
test: cover player resolution in cheat service
tasaje1 Jun 21, 2026
ad2d7f1
style: fix code style warnings
tasaje1 Jun 21, 2026
4a78247
Merge pull request #184 from SS26-SE2-Codenames/feature/124-cheat-det…
tasaje1 Jun 24, 2026
0538daf
Merge remote-tracking branch 'origin/development' into chore/merge-ma…
tasaje1 Jun 24, 2026
7ce312a
Merge branch 'main' into chore/merge-main-into-dev1
tasaje1 Jun 24, 2026
7587412
Merge pull request #186 from SS26-SE2-Codenames/chore/merge-main-into…
tasaje1 Jun 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
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 com.codenames.codenames.backend.game.domain.ExposeCheatResult;
import org.springframework.messaging.handler.annotation.MessageMapping;
import org.springframework.messaging.simp.SimpMessagingTemplate;
import org.springframework.stereotype.Controller;
Expand Down Expand Up @@ -88,6 +89,46 @@ public void useCheat(CheatCardMessage message) {
persistenceService.saveSnapShot(message.getLobbyCode());
}

/**
* Handles an expose-cheat request and broadcasts the updated game state.
*
* @param message the expose-cheat request containing lobby and username
*/
@MessageMapping("/expose-cheat")
public void exposeCheat(CheatCardMessage message) {

ExposeCheatResult result =
cheatService.exposeCheat(
message.getLobbyCode(),
message.getUsername());

if (result == null) {
return;
}

ChatDto systemMessage =
new ChatDto(
"System",
result.correct()
? "EXPOSE_CORRECT"
: "EXPOSE_WRONG",
ChatMessageType.SYSTEM);

messagingTemplate.convertAndSend(
"/topic/chat/"
+ message.getLobbyCode()
+ "/"
+ result.team()
+ "/operative",
systemMessage);

persistenceService.saveSnapShot(message.getLobbyCode());

messagingTemplate.convertAndSend(
GAME_TOPIC_PREFIX + message.getLobbyCode(),
gameService.getCurrentGameState(message.getLobbyCode()));
}

/**
* Sends the current game state to subscribed players.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.codenames.codenames.backend.game.application;

import com.codenames.codenames.backend.game.domain.CheatResult;
import com.codenames.codenames.backend.game.domain.ExposeCheatResult;
import com.codenames.codenames.backend.lobby.application.LobbyService;
import com.codenames.codenames.backend.lobby.domain.Player;
import com.codenames.codenames.backend.lobby.domain.Role;
import com.codenames.codenames.backend.lobby.domain.Team;
import java.util.List;
Expand Down Expand Up @@ -34,13 +36,40 @@ public CheatService(GameService gameService, LobbyService lobbyService) {
* @return the cheat result or null if the request is invalid
*/
public CheatResult useCheat(String lobbyCode, String username, List<Integer> positions) {
Team team = lobbyService.getPlayerTeam(username, lobbyCode);
Role role = lobbyService.getPlayerRole(username, lobbyCode);

if (team == null || role != Role.OPERATIVE) {
Team team = getOperativeTeam(lobbyCode, username);
if (team == null) {
return null;
}

return gameService.useCheat(lobbyCode, positions, team);
}
}

/**
* Performs an expose-cheat attempt for a player and applies the matching penalty.
*
* @param lobbyCode the lobby code
* @param username the requesting username
* @return the expose-cheat result or null if the request is invalid
*/
public ExposeCheatResult exposeCheat(String lobbyCode, String username) {
Team team = getOperativeTeam(lobbyCode, username);
if (team == null) {
return null;
}

boolean correct = gameService.exposeCheatAndApplyPenalty(lobbyCode, team);
return new ExposeCheatResult(correct, team);
}

private Team getOperativeTeam(String lobbyCode, String username) {
Player player = lobbyService.getPlayer(lobbyCode, username);
if (player == null) {
return null;
}

Team team = lobbyService.getPlayerTeam(player.uuid(), lobbyCode);
Role role = lobbyService.getPlayerRole(player.uuid(), lobbyCode);

return role == Role.OPERATIVE ? team : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -180,4 +180,15 @@ public CheatResult useCheat(

return getGame(lobbyCode).useCheat(positions, team);
}

/**
* Performs an expose-cheat attempt and applies the matching penalty.
*
* @param lobbyCode the lobby code of the game
* @param team the team trying to expose the opponent's cheat
* @return true if the opposing team has used their cheat, false otherwise
*/
public boolean exposeCheatAndApplyPenalty(String lobbyCode, Team team) {
return getGame(lobbyCode).exposeCheatAndApplyPenalty(team);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.codenames.codenames.backend.game.domain;

import com.codenames.codenames.backend.lobby.domain.Team;

/**
* Result of an expose-cheat attempt.
*
* @param correct whether the opposing team had used their cheat
* @param team the team that tried to expose the cheat
*/
public record ExposeCheatResult(
boolean correct,
Team team) {
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,8 +74,7 @@ public GameManager(
* @param state bundled recovery state
* @param clueValidationService clue validation service
*/
public GameManager(
GameStateDto state, ClueValidationService clueValidationService) {
public GameManager(GameStateDto state, ClueValidationService clueValidationService) {
if (state.cardList() == null || state.cardList().isEmpty()) {
throw new IllegalArgumentException("cards cannot be null or empty");
}
Expand Down Expand Up @@ -298,6 +297,53 @@ public void passTurn(Team callingTeam) {
advanceTurn();
}

/**
* Checks whether the opposing team has already used their cheat.
*
* @param callingTeam the team trying to expose the opponent's cheat
* @return true if the opposing team has used their cheat, false otherwise
*/
public boolean exposeCheat(Team callingTeam) {
if (callingTeam == Team.RED) {
return blueTeamCheatUsed;
}

return redTeamCheatUsed;
}

/**
* Applies the penalty for an expose-cheat attempt.
*
* <p>If the exposure is correct, the opposing team's next turn is skipped. If it is wrong, the
* calling team passes their turn.
*
* @param callingTeam the team trying to expose the opponent's cheat
* @return true if the opposing team has used their cheat, false otherwise
*/
public boolean exposeCheatAndApplyPenalty(Team callingTeam) {
boolean correct = exposeCheat(callingTeam);

if (correct) {
skipOpponentTurn(callingTeam);
} else {
passTurn(callingTeam);
}

return correct;
}

/**
* Skips the opposing team's full next turn after a correct expose-cheat attempt.
*
* @param callingTeam the team that correctly exposed the opponent's cheat
*/
private void skipOpponentTurn(Team callingTeam) {
checkCorrectTurn(callingTeam, Role.OPERATIVE);
advanceTurn();
advanceTurn();
advanceTurn();
}

/**
* Helper method to check if the current team calling a method is allowed to do so.
*
Expand All @@ -317,16 +363,12 @@ private void checkCorrectTurn(Team team, Role role) {
* @return true if the position is valid
*/
private boolean isValidPosition(Integer position) {
return position != null
&& position >= 0
&& position < board.getCardList().size();
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.
* 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
Expand Down Expand Up @@ -376,11 +418,12 @@ public CheatResult useCheat(List<Integer> positions, Team team) {
}

if (correctCards.isEmpty()) {
return new CheatResult("Keine der ausgewählten Karten ist richtig.", team);
return new CheatResult("None of the selected cards belongs to your team.", team);
}

Card correctCard = correctCards.get(0);

return new CheatResult("Die Karte \"" + correctCard.getWord() + "\" ist richtig.", team);
return new CheatResult(
"The card \"" + correctCard.getWord() + "\" belongs to your team.", team);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
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.ExposeCheatResult;
import com.codenames.codenames.backend.lobby.domain.Team;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
Expand Down Expand Up @@ -129,7 +130,7 @@ void useCheatShouldSendPrivateSystemMessageAndPersistSnapshot() {
message.setUsername("Max");
message.setPositions(List.of(0, 1));

CheatResult result = new CheatResult("Die Karte \"Dog\" ist richtig.", Team.RED);
CheatResult result = new CheatResult("The card \"Dog\" belongs to your team.", Team.RED);

when(cheatService.useCheat(LOBBY_CODE, "Max", List.of(0, 1))).thenReturn(result);

Expand Down Expand Up @@ -161,4 +162,64 @@ void useCheatShouldDoNothingWhenResultIsNull() {
.convertAndSendToUser(anyString(), anyString(), any(Object.class));
verify(persistenceService, never()).saveSnapShot(LOBBY_CODE);
}

@Test
void exposeCheatShouldPersistAndBroadcastUpdatedState() {
CheatCardMessage message = new CheatCardMessage();
message.setLobbyCode(LOBBY_CODE);
message.setUsername("Max");
message.setPositions(List.of());
GameStateDto gameState = createGameStateDto();

when(cheatService.exposeCheat(LOBBY_CODE, "Max"))
.thenReturn(new ExposeCheatResult(true, Team.RED));
when(gameService.getCurrentGameState(LOBBY_CODE)).thenReturn(gameState);

controller.exposeCheat(message);

verify(cheatService).exposeCheat(LOBBY_CODE, "Max");
verify(messagingTemplate)
.convertAndSend(
"/topic/chat/" + LOBBY_CODE + "/RED/operative",
new ChatDto("System", "EXPOSE_CORRECT", ChatMessageType.SYSTEM));
verify(persistenceService).saveSnapShot(LOBBY_CODE);
verify(messagingTemplate).convertAndSend("/topic/game/" + LOBBY_CODE, gameState);
}

@Test
void exposeCheatShouldSendWrongSystemMessage() {
CheatCardMessage message = new CheatCardMessage();
message.setLobbyCode(LOBBY_CODE);
message.setUsername("Max");
message.setPositions(List.of());
GameStateDto gameState = createGameStateDto();

when(cheatService.exposeCheat(LOBBY_CODE, "Max"))
.thenReturn(new ExposeCheatResult(false, Team.BLUE));
when(gameService.getCurrentGameState(LOBBY_CODE)).thenReturn(gameState);

controller.exposeCheat(message);

verify(messagingTemplate)
.convertAndSend(
"/topic/chat/" + LOBBY_CODE + "/BLUE/operative",
new ChatDto("System", "EXPOSE_WRONG", ChatMessageType.SYSTEM));
verify(messagingTemplate).convertAndSend("/topic/game/" + LOBBY_CODE, gameState);
}

@Test
void exposeCheatShouldDoNothingWhenResultIsNull() {
CheatCardMessage message = new CheatCardMessage();
message.setLobbyCode(LOBBY_CODE);
message.setUsername("Max");
message.setPositions(List.of());

when(cheatService.exposeCheat(LOBBY_CODE, "Max")).thenReturn(null);

controller.exposeCheat(message);

verify(cheatService).exposeCheat(LOBBY_CODE, "Max");
verify(persistenceService, never()).saveSnapShot(LOBBY_CODE);
verify(messagingTemplate, never()).convertAndSend(anyString(), any(Object.class));
}
}
Loading
Loading