From 583bf1736b4d0d25c3e4dc33d79a6f37abfe97b3 Mon Sep 17 00:00:00 2001 From: athrvk Date: Fri, 13 Dec 2024 02:58:06 +0530 Subject: [PATCH] Implement player disconnection handling; update WebSocket service and GameService to manage room state --- .../game/config/WebSocketEventListener.java | 44 ++++++ .../main/java/com/game/model/GameState.java | 134 +++++++++++++++--- .../java/com/game/service/GameService.java | 43 +++++- frontend/src/App.js | 11 ++ frontend/src/utils/websocket.js | 4 + 5 files changed, 213 insertions(+), 23 deletions(-) create mode 100644 backend/src/main/java/com/game/config/WebSocketEventListener.java diff --git a/backend/src/main/java/com/game/config/WebSocketEventListener.java b/backend/src/main/java/com/game/config/WebSocketEventListener.java new file mode 100644 index 0000000..31e9156 --- /dev/null +++ b/backend/src/main/java/com/game/config/WebSocketEventListener.java @@ -0,0 +1,44 @@ +package com.game.config; + +import com.game.service.GameService; + +import java.security.Principal; +import java.util.Map; +import java.util.Optional; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.event.EventListener; +import org.springframework.messaging.simp.SimpMessagingTemplate; +import org.springframework.messaging.simp.stomp.StompHeaderAccessor; +import org.springframework.stereotype.Component; +import org.springframework.web.socket.messaging.SessionDisconnectEvent; + +@Component +public class WebSocketEventListener { + + private static final Logger logger = LoggerFactory.getLogger(WebSocketEventListener.class); + + @Autowired + private GameService gameService; + + @Autowired + private SimpMessagingTemplate messagingTemplate; + + @EventListener + public void handleWebSocketDisconnect(SessionDisconnectEvent event) { + StompHeaderAccessor headerAccessor = StompHeaderAccessor.wrap(event.getMessage()); + String username = Optional.ofNullable(headerAccessor.getUser()) + .map(Principal::getName) + .orElse(null); + String roomId = gameService.getRoomOfPlayer(username); + + if (roomId != null && username != null) { + if (gameService.removePlayerFromRoom(roomId, username)) { + logger.info("Player {} disconnected from room {}", username, roomId); + messagingTemplate.convertAndSend("/topic/room/" + roomId, + Map.of("type", "player_disconnected", "username", username, "roomId", roomId)); + } + } + } +} diff --git a/backend/src/main/java/com/game/model/GameState.java b/backend/src/main/java/com/game/model/GameState.java index 39b1f96..6ef9f52 100644 --- a/backend/src/main/java/com/game/model/GameState.java +++ b/backend/src/main/java/com/game/model/GameState.java @@ -5,13 +5,16 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import java.util.concurrent.locks.ReentrantLock; public class GameState { - private List squares; - private List history; + private final List squares; + private final List history; private boolean xIsNext; private int players; - private Map playerSymbols; // Maps username to symbol + private final Map playerSymbols; // Maps username to symbol + private final ReentrantLock lock; public GameState() { this.squares = new ArrayList<>(Collections.nCopies(9, null)); @@ -19,34 +22,72 @@ public GameState() { this.xIsNext = true; // X always starts this.players = 0; this.playerSymbols = new HashMap<>(); + this.lock = new ReentrantLock(); } public List getSquares() { - return squares; + lock.lock(); + try { + return new ArrayList<>(squares); + } finally { + lock.unlock(); + } } public void setSquares(List squares) { - this.squares = squares; + lock.lock(); + try { + this.squares.clear(); + this.squares.addAll(squares); + } finally { + lock.unlock(); + } } public List getHistory() { - return history; + lock.lock(); + try { + return new ArrayList<>(history); + } finally { + lock.unlock(); + } } public void setHistory(List history) { - this.history = history; + lock.lock(); + try { + this.history.clear(); + this.history.addAll(history); + } finally { + lock.unlock(); + } } public boolean isXIsNext() { - return xIsNext; + lock.lock(); + try { + return xIsNext; + } finally { + lock.unlock(); + } } public void setXIsNext(boolean xIsNext) { - this.xIsNext = xIsNext; + lock.lock(); + try { + this.xIsNext = xIsNext; + } finally { + lock.unlock(); + } } public int getPlayers() { - return players; + lock.lock(); + try { + return players; + } finally { + lock.unlock(); + } } /** @@ -56,17 +97,29 @@ public int getPlayers() { * @return the assigned symbol ('X' or 'O') */ public String assignSymbol(String username) { - String symbol; - if (players == 0) { - symbol = "X"; - } else if (players == 1) { - symbol = "O"; - } else { - symbol = "X"; // Fallback, should not occur + lock.lock(); + try { + // Check if the player already has a symbol assigned + if (playerSymbols.containsKey(username)) { + return playerSymbols.get(username); + } + + // Assign a symbol based on the current number of players and existing assignments + String symbol; + if (!playerSymbols.containsValue("X")) { + symbol = "X"; + } else if (!playerSymbols.containsValue("O")) { + symbol = "O"; + } else { + symbol = "X"; // Fallback, should not occur + } + + playerSymbols.put(username, symbol); + players++; + return symbol; + } finally { + lock.unlock(); } - playerSymbols.put(username, symbol); - players++; - return symbol; } /** @@ -76,10 +129,47 @@ public String assignSymbol(String username) { * @return the assigned symbol ('X' or 'O'), or null if not found */ public String getPlayerSymbol(String username) { - return playerSymbols.get(username); + lock.lock(); + try { + return playerSymbols.get(username); + } finally { + lock.unlock(); + } } public Map getPlayerSymbols() { - return playerSymbols; + lock.lock(); + try { + return new HashMap<>(playerSymbols); + } finally { + lock.unlock(); + } + } + + public boolean removePlayer(String username) { + lock.lock(); + try { + if (Objects.nonNull(playerSymbols.remove(username))) { + // Reset the game state when a player is removed + boolean resetResult = reset(); + players--; + return resetResult; + } + return false; + } finally { + lock.unlock(); + } + } + + public boolean reset() { + lock.lock(); + try { + Collections.fill(squares, null); + history.clear(); + xIsNext = true; + return true; + } finally { + lock.unlock(); + } } } diff --git a/backend/src/main/java/com/game/service/GameService.java b/backend/src/main/java/com/game/service/GameService.java index dda7391..94f957d 100644 --- a/backend/src/main/java/com/game/service/GameService.java +++ b/backend/src/main/java/com/game/service/GameService.java @@ -16,6 +16,8 @@ public class GameService { // Stores roomId to game state mapping private final Map rooms = new ConcurrentHashMap<>(); + // Stores username to roomId mapping + private final Map playerRoomMap = new ConcurrentHashMap<>(); /** * Creates a new game room with a unique ID. @@ -41,7 +43,6 @@ public String createRoom(String roomId) { return roomId; } - /** * Allows a user to join a game room. If the desired room ID is not provided or is empty, * the method will attempt to find a room with only one player and join it. If no such room @@ -60,6 +61,7 @@ public JoinRoomResponse joinRoom(String desiredRoomId, String username) { GameState room = entry.getValue(); if (room.getPlayers() == 1) { String symbol = room.assignSymbol(username); + playerRoomMap.put(username, entry.getKey()); Map playerSymbols = room.getPlayerSymbols(); logger.info("Players in room {}: {}", entry.getKey(), playerSymbols); return new JoinRoomResponse(entry.getKey(), symbol); @@ -69,6 +71,7 @@ public JoinRoomResponse joinRoom(String desiredRoomId, String username) { String newRoomId = createRoom(); GameState newRoom = rooms.get(newRoomId); String symbol = newRoom.assignSymbol(username); + playerRoomMap.put(username, newRoomId); Map playerSymbols = newRoom.getPlayerSymbols(); logger.info("Players in room {}: {}", newRoomId, playerSymbols); return new JoinRoomResponse(newRoomId, symbol); @@ -76,6 +79,7 @@ public JoinRoomResponse joinRoom(String desiredRoomId, String username) { GameState desiredRoom = rooms.get(desiredRoomId); if (desiredRoom != null && desiredRoom.getPlayers() < 2) { String symbol = desiredRoom.assignSymbol(username); + playerRoomMap.put(username, desiredRoomId); Map playerSymbols = desiredRoom.getPlayerSymbols(); logger.info("Players in room {}: {}", desiredRoomId, playerSymbols); return new JoinRoomResponse(desiredRoomId, symbol); @@ -84,6 +88,7 @@ public JoinRoomResponse joinRoom(String desiredRoomId, String username) { String newRoomId = createRoom(); GameState newRoom = rooms.get(newRoomId); String symbol = newRoom.assignSymbol(username); + playerRoomMap.put(username, newRoomId); Map playerSymbols = newRoom.getPlayerSymbols(); logger.info("Players in room {}: {}", newRoomId, playerSymbols); return new JoinRoomResponse(newRoomId, symbol); @@ -108,11 +113,47 @@ public void updateGameState(String roomId, Map gameState) { } } + /** + * Determines if a room is full (i.e. has 2 players). + * + * @param roomId the ID of the room + * @return true if the room is full, false otherwise + */ public boolean isRoomFull(String roomId) { GameState state = rooms.get(roomId); return state != null && state.getPlayers() == 2; } + /** + * Retrieves the room ID of a player. + * + * @param username the username of the player + * @return the room ID, or null if not found + */ + public String getRoomOfPlayer(String username) { + if (username == null) { + return null; + } + return playerRoomMap.get(username); + } + + /** + * Removes the disconnected player from the room. + * + * @param roomId the ID of the room to remove + */ + public boolean removePlayerFromRoom(String roomId, String username) { + GameState gameState = rooms.get(roomId); + if (gameState != null) { + if (gameState.removePlayer(username)) { + playerRoomMap.remove(username); + logger.info("Removed player {} from room {}", username, roomId); + return true; + } + } + return false; + } + /** * Retrieves the list of rooms with 0 or just 1 player. * diff --git a/frontend/src/App.js b/frontend/src/App.js index 9cbc1e0..4096ba1 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -20,6 +20,7 @@ function App() { const [history, setHistory] = useState([]); const [xIsNext, setXIsNext] = useState(true); const [roomId, setRoomId] = useState(''); + // eslint-disable-next-line no-unused-vars const [availableRooms, setAvailableRooms] = useState([]); const [activePlayers, setActivePlayers] = useState(0); const [inputRoomId, setInputRoomId] = useState(''); @@ -96,6 +97,16 @@ function App() { if (data.type === 'player_joined' && data.roomId === roomId) { setIsRoomFull(data.isRoomFull); } + if (data.type === 'player_disconnected' && data.roomId === roomId) { + setIsRoomFull(false); + setSquares(initialSquares); + setHistory([]); + setXIsNext(true); + setGameWinner(null); + if (data.username !== username) { + setMessage('other player disconnected'); + } + } }; const handleCreateRoom = (e) => { diff --git a/frontend/src/utils/websocket.js b/frontend/src/utils/websocket.js index 027e415..6e116cc 100644 --- a/frontend/src/utils/websocket.js +++ b/frontend/src/utils/websocket.js @@ -17,6 +17,9 @@ class WebSocketService { username: this.username, // Add headers if needed }, + disconnectHeaders: { + username: this.username, + }, logRawCommunication: true, // Enable raw communication logging debug: (str) => { console.log("[STOMP DEBUG] - " + str); @@ -78,6 +81,7 @@ class WebSocketService { console.log(`[/topic/room/${roomId}] - Received message:`, data); callback(data); }); + this.roomId = roomId; } sendGameState(roomId, gameState) {