Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
63 commits
Select commit Hold shift + click to select a range
f9802da
Merge remote-tracking branch 'origin/development' into feature/game-s…
tasaje1 May 18, 2026
2bde83a
feat: add system snapshot model for restart recovery
tasaje1 May 18, 2026
4f6ec34
feat: add json state store for atomic snapshot persistence
tasaje1 May 18, 2026
a888582
docs: add javadocs
tasaje1 May 18, 2026
11c84b1
test: add unit tests for json state store load and save behavior
tasaje1 May 18, 2026
55bae8f
test: replace redundant lobby emptiness assertion with size check
tasaje1 May 18, 2026
aa5e990
test: add json state store exception path coverage
tasaje1 May 18, 2026
ef7145a
refactor: extract snapshot load helper in json state store test
tasaje1 May 18, 2026
cb9d408
test: improve JsonStateStore coverage for getters and parentless save…
tasaje1 May 18, 2026
cc55c71
test: replace self-assertion on ioLock getter with non-null assertion
tasaje1 May 18, 2026
bfd2daf
Merge remote-tracking branch 'origin/development' into feature/game-s…
tasaje1 May 18, 2026
36fa601
feat: add startup state recovery service
tasaje1 May 18, 2026
7dc1752
feat: add lobby and game restore hooks
tasaje1 May 18, 2026
87708a4
feat: add GameManager and Board constructors for snapshot restore
tasaje1 May 18, 2026
7a170d9
refactor: use CardDataTransferObject in game snapshots for recovery
tasaje1 May 18, 2026
078d507
test: add startup recovery tests for SystemStateRecoveryService
tasaje1 May 18, 2026
9b99653
test: maximize SystemStateRecoveryService branch coverage
tasaje1 May 18, 2026
9d654bd
test: add tests for recovery
tasaje1 May 18, 2026
7393f5e
style: reorder imports in LobbyServiceTest
tasaje1 May 18, 2026
885cf5e
test: configure Mockito mock maker for local test stability
tasaje1 May 18, 2026
a0a41de
docs: add javadocs
tasaje1 May 18, 2026
cad7578
test: replace final DTO mocks with concrete instances in controller t…
tasaje1 May 19, 2026
2dc0d40
style: fix import order
tasaje1 May 19, 2026
080726d
refactor: delete unused clue snapshot record
tasaje1 May 19, 2026
a04a04a
refactor: delete unused lobby snapshot record
tasaje1 May 19, 2026
bd5cf0c
refactor: use ClueDto in game snapshot
tasaje1 May 19, 2026
b9d9f25
refactor: use player dto list in system snapshot
tasaje1 May 19, 2026
be85731
refactor: adapt game manager factory to dto based clue recovery
tasaje1 May 19, 2026
746ceec
refactor: adapt lobby recovery to dto based snapshot structure
tasaje1 May 19, 2026
04e823b
test: update game manager factory recovery tests
tasaje1 May 19, 2026
5d05310
test: update json state store tests for dto snapshots
tasaje1 May 19, 2026
510688e
test: update system state recovery tests for dto snapshots
tasaje1 May 19, 2026
7b8d762
test: simplify assertThrows lambdas in recovery constructor tests
tasaje1 May 19, 2026
9278789
refactor: add remaining guesses to game state transfer object
tasaje1 May 19, 2026
f7f4d0c
refactor: map remaining guesses in dto service
tasaje1 May 19, 2026
1240f53
refactor: remove legacy game state dto creation from game service
tasaje1 May 19, 2026
23e1358
refactor: use unified game state payload in game controller
tasaje1 May 19, 2026
79b01a8
refactor: remove unused game state dto record
tasaje1 May 19, 2026
6abec76
test: update game service tests for unified payload
tasaje1 May 19, 2026
82481fc
test: update game controller tests for unified payload
tasaje1 May 19, 2026
414b189
test: update game socket controller test payload constructor
tasaje1 May 19, 2026
27e903c
test: update serialization dto service tests for remaining guesses
tasaje1 May 19, 2026
ccfb59c
test: update serialization dto service tests for remaining guesses
tasaje1 May 19, 2026
c9af76c
refactor: unify game recovery state dto
tasaje1 May 19, 2026
6c242f7
test: update recovery tests for unified game state dto
tasaje1 May 19, 2026
4b8387d
refactor: remove redundant game recovery state
tasaje1 May 19, 2026
5c74a0c
refactor: remove GameRecoveryState and use game state dto directly
tasaje1 May 19, 2026
b516914
test: update recovery tests for direct game state dto
tasaje1 May 19, 2026
ada50ff
feat: add snapshot retrieval for lobby and game state
tasaje1 May 19, 2026
7301272
feat: add system state persistence service
tasaje1 May 19, 2026
7c9bbc1
test: add persistence service and snapshot tests
tasaje1 May 19, 2026
2848ef1
docs: add javadocs for system state persistence service
tasaje1 May 19, 2026
8d4bf23
refactor: persist system state after lobby and gameplay actions
tasaje1 May 19, 2026
5c23caf
test: update controller tests for state persistence calls
tasaje1 May 19, 2026
d689b01
style: fix imports
tasaje1 May 19, 2026
a31b94e
refactor: remove redundant response variable and verify persistence s…
tasaje1 May 19, 2026
e2b0e46
style: fix checkstyle issues
tasaje1 May 20, 2026
e4ea92e
chore: persist backend state with docker volume
tasaje1 May 20, 2026
24fabaa
refactor: reduce pre-game lobby persistence
tasaje1 May 20, 2026
e05976e
test: align lobby and game controller persistence expectations
tasaje1 May 20, 2026
e2898e4
refactor: remove unused persistence dependency from game controller
tasaje1 May 20, 2026
87318a6
Merge branch 'development' into feature/game-state-recovery-restart
tasaje1 May 20, 2026
38c490d
test: fix imports
tasaje1 May 20, 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
4 changes: 3 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,6 @@ services:
image: ghcr.io/ss26-se2-codenames/backend:latest
restart: unless-stopped
ports:
- "53213:8080"
- "53213:8080"
volumes:
- ./data:/app/data
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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/";

Expand All @@ -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;
}

/**
Expand All @@ -61,6 +68,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(),
Expand All @@ -81,6 +89,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(),
Expand All @@ -96,6 +105,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(),
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -19,21 +20,23 @@
* <p>Provides endpoints for creating, joining, and leaving lobbies. Delegates business logic to
* {@link LobbyService}.
*/

@RestController
@RequestMapping("/lobby")
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;
}

/**
Expand All @@ -42,93 +45,84 @@ public LobbyController(LobbyService service) {
* @param username the username of the requesting user
* @return a response containing the result and the generated lobby code
*/

@GetMapping("/create")
public ResponseEntity<LobbyResponse> 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 {

List<PlayerDto> 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<LobbyResponse> joinLobby(
@RequestParam String username, @PathVariable String lobbyCode) {
@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),
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<LobbyResponse> 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) {
ResponseEntity<LobbyResponse> response = ResponseEntity.ok(
new LobbyResponse(
"Left lobby successfully.",
lobbyCode,
service.getPlayersDto(lobbyCode),
false
)
);
service.checkLobbyStillHasPlayers(lobbyCode);
return response;

if (wasStarted) {
persistenceService.persistCurrentState();
}

return ResponseEntity.ok(
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));
}
}

/**
* 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<LobbyResponse> getLobbyInfo(
@PathVariable String lobbyCode
) {
public ResponseEntity<LobbyResponse> getLobbyInfo(@PathVariable String lobbyCode) {
List<PlayerDto> 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));
}

/**
Expand All @@ -139,33 +133,26 @@ public ResponseEntity<LobbyResponse> getLobbyInfo(
*/
@PostMapping("/{lobbyCode}/select-position")
public ResponseEntity<LobbyResponse> 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) {

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

Expand All @@ -175,32 +162,22 @@ public ResponseEntity<LobbyResponse> 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<LobbyResponse> 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));
}
}
Loading
Loading