Skip to content
Merged
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
* Enum representing the type of chat message.
*/
public enum ChatMessageType {
CHAT
}
CHAT,
SYSTEM
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,13 @@ private GameStateEntity mapGameState(
gameStateEntity.setClueGuessAmount(0);
gameStateEntity.setRemainingGuesses(0);
}

gameStateEntity.setRedTeamCheatUsed(
gameManager.isRedTeamCheatUsed());

gameStateEntity.setBlueTeamCheatUsed(
gameManager.isBlueTeamCheatUsed());

return gameStateEntity;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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());
Expand All @@ -56,8 +55,15 @@ public GameStateDto mapToGameStateDto(LobbyEntity lobbyEntity) {
}
int remainingGuesses = gameStateEntity.getRemainingGuesses();
List<CardDto> 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<CardDto> mapToCardDto(LobbyEntity lobbyEntity) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -21,6 +26,7 @@
public class GameSocketController {

private final GameService gameService;
private final CheatService cheatService;

private final SimpMessagingTemplate messagingTemplate;
private final PersistenceService persistenceService;
Expand All @@ -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) {
Comment thread
Zheaver marked this conversation as resolved.

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());
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Integer> positions;
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,7 @@ public record GameStateDto(
Role currentPhase,
ClueDto currentClue,
int remainingGuesses,
List<CardDto> cardList) {}
List<CardDto> cardList,
boolean redTeamCheatUsed,
boolean blueTeamCheatUsed) {
}
Original file line number Diff line number Diff line change
@@ -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<Integer> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -162,4 +164,20 @@ public Map<String, GameStateDto> 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<Integer> positions,
Team team) {

return getGame(lobbyCode).useCheat(positions, team);
}
}
Original file line number Diff line number Diff line change
@@ -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) {}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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<Integer> 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<Card> selectedCards =
positions.stream()
.filter(this::isValidPosition)
.filter(position -> !board.getIsGuessed(position))
.map(position -> board.getCardList().get(position))
.toList();

if (selectedCards.isEmpty()) {
return null;
}

List<Card> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,23 +48,30 @@ 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(),
currentTurn,
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());
}
}
Loading
Loading