Skip to content

Commit

Permalink
Implement player disconnection handling; update WebSocket service and…
Browse files Browse the repository at this point in the history
… GameService to manage room state
  • Loading branch information
athrvk committed Dec 12, 2024
1 parent 7c0228a commit 583bf17
Show file tree
Hide file tree
Showing 5 changed files with 213 additions and 23 deletions.
44 changes: 44 additions & 0 deletions backend/src/main/java/com/game/config/WebSocketEventListener.java
Original file line number Diff line number Diff line change
@@ -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));
}
}
}
}
134 changes: 112 additions & 22 deletions backend/src/main/java/com/game/model/GameState.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,89 @@
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<String> squares;
private List<Integer> history;
private final List<String> squares;
private final List<Integer> history;
private boolean xIsNext;
private int players;
private Map<String, String> playerSymbols; // Maps username to symbol
private final Map<String, String> playerSymbols; // Maps username to symbol
private final ReentrantLock lock;

public GameState() {
this.squares = new ArrayList<>(Collections.nCopies(9, null));
this.history = new ArrayList<>();
this.xIsNext = true; // X always starts
this.players = 0;
this.playerSymbols = new HashMap<>();
this.lock = new ReentrantLock();
}

public List<String> getSquares() {
return squares;
lock.lock();
try {
return new ArrayList<>(squares);
} finally {
lock.unlock();
}
}

public void setSquares(List<String> squares) {
this.squares = squares;
lock.lock();
try {
this.squares.clear();
this.squares.addAll(squares);
} finally {
lock.unlock();
}
}

public List<Integer> getHistory() {
return history;
lock.lock();
try {
return new ArrayList<>(history);
} finally {
lock.unlock();
}
}

public void setHistory(List<Integer> 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();
}
}

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

/**
Expand All @@ -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<String, String> 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();
}
}
}
43 changes: 42 additions & 1 deletion backend/src/main/java/com/game/service/GameService.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ public class GameService {

// Stores roomId to game state mapping
private final Map<String, GameState> rooms = new ConcurrentHashMap<>();
// Stores username to roomId mapping
private final Map<String, String> playerRoomMap = new ConcurrentHashMap<>();

/**
* Creates a new game room with a unique ID.
Expand All @@ -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
Expand All @@ -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<String, String> playerSymbols = room.getPlayerSymbols();
logger.info("Players in room {}: {}", entry.getKey(), playerSymbols);
return new JoinRoomResponse(entry.getKey(), symbol);
Expand All @@ -69,13 +71,15 @@ 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<String, String> playerSymbols = newRoom.getPlayerSymbols();
logger.info("Players in room {}: {}", newRoomId, playerSymbols);
return new JoinRoomResponse(newRoomId, symbol);
}
GameState desiredRoom = rooms.get(desiredRoomId);
if (desiredRoom != null && desiredRoom.getPlayers() < 2) {
String symbol = desiredRoom.assignSymbol(username);
playerRoomMap.put(username, desiredRoomId);
Map<String, String> playerSymbols = desiredRoom.getPlayerSymbols();
logger.info("Players in room {}: {}", desiredRoomId, playerSymbols);
return new JoinRoomResponse(desiredRoomId, symbol);
Expand All @@ -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<String, String> playerSymbols = newRoom.getPlayerSymbols();
logger.info("Players in room {}: {}", newRoomId, playerSymbols);
return new JoinRoomResponse(newRoomId, symbol);
Expand All @@ -108,11 +113,47 @@ public void updateGameState(String roomId, Map<String, Object> 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.
*
Expand Down
11 changes: 11 additions & 0 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -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('');
Expand Down Expand Up @@ -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) => {
Expand Down
4 changes: 4 additions & 0 deletions frontend/src/utils/websocket.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -78,6 +81,7 @@ class WebSocketService {
console.log(`[/topic/room/${roomId}] - Received message:`, data);
callback(data);
});
this.roomId = roomId;
}

sendGameState(roomId, gameState) {
Expand Down

0 comments on commit 583bf17

Please sign in to comment.