mirror of
https://github.com/DeNNiiInc/Connect-5.git
synced 2026-04-17 18:26:01 +00:00
738 lines
27 KiB
JavaScript
738 lines
27 KiB
JavaScript
const Filter = require('bad-words');
|
|
const filter = new Filter();
|
|
|
|
class GameManager {
|
|
constructor(io, db) {
|
|
this.io = io;
|
|
this.db = db;
|
|
this.players = new Map(); // socketId -> playerData
|
|
this.challenges = new Map(); // challengeId -> challengeData
|
|
this.activeGames = new Map(); // gameId -> gameData
|
|
this.playerSockets = new Map(); // playerId -> socketId
|
|
this.activeGames = new Map(); // gameId -> gameData
|
|
this.playerSockets = new Map(); // playerId -> socketId
|
|
this.rematches = new Map(); // rematchId -> rematchData
|
|
this.disconnectTimeouts = new Map(); // playerId -> timeoutId
|
|
}
|
|
|
|
// Validate and register player
|
|
async registerPlayer(socket, username) {
|
|
try {
|
|
// Validate username
|
|
if (!username || typeof username !== 'string') {
|
|
return { success: false, error: 'Invalid username' };
|
|
}
|
|
|
|
// Clean and validate length
|
|
username = username.trim();
|
|
if (username.length < 3 || username.length > 20) {
|
|
return { success: false, error: 'Username must be 3-20 characters' };
|
|
}
|
|
|
|
// Check for profanity
|
|
if (filter.isProfane(username)) {
|
|
return { success: false, error: 'Please use a family-friendly username' };
|
|
}
|
|
|
|
// Check alphanumeric plus basic chars
|
|
if (!/^[a-zA-Z0-9_-]+$/.test(username)) {
|
|
return { success: false, error: 'Username can only contain letters, numbers, underscores, and hyphens' };
|
|
}
|
|
|
|
// Create or get player from database
|
|
const playerId = await this.db.createPlayer(username);
|
|
const player = await this.db.getPlayerById(playerId);
|
|
|
|
// Add session to database
|
|
await this.db.addSession(socket.id, playerId, username);
|
|
|
|
// Store in memory
|
|
this.players.set(socket.id, {
|
|
id: playerId,
|
|
username: username,
|
|
socketId: socket.id,
|
|
currentGameId: null
|
|
});
|
|
|
|
this.playerSockets.set(playerId, socket.id);
|
|
|
|
// Broadcast updated player list
|
|
await this.broadcastActivePlayers();
|
|
|
|
const response = {
|
|
success: true,
|
|
player: {
|
|
id: playerId,
|
|
username: username,
|
|
stats: {
|
|
wins: player.total_wins,
|
|
losses: player.total_losses,
|
|
draws: player.total_draws
|
|
}
|
|
},
|
|
activeGame: null // Will be populated if they are reconnecting
|
|
};
|
|
|
|
// Check if player is in an active game (reconnection)
|
|
for (const [gameId, game] of this.activeGames.entries()) {
|
|
if (game.state === 'active' && (game.player1Id === playerId || game.player2Id === playerId)) {
|
|
// Restore game state for player
|
|
const playerEntry = this.players.get(socket.id);
|
|
if (playerEntry) {
|
|
playerEntry.currentGameId = gameId;
|
|
}
|
|
|
|
// Clear any pending disconnect timeout
|
|
if (this.disconnectTimeouts.has(playerId)) {
|
|
clearTimeout(this.disconnectTimeouts.get(playerId));
|
|
this.disconnectTimeouts.delete(playerId);
|
|
}
|
|
|
|
// Notify opponent of reconnection
|
|
const opponentId = game.player1Id === playerId ? game.player2Id : game.player1Id;
|
|
const opponentSocket = this.playerSockets.get(opponentId);
|
|
if (opponentSocket) {
|
|
this.io.to(opponentSocket).emit('opponent_reconnected', {
|
|
message: `${username} reconnected!`
|
|
});
|
|
}
|
|
|
|
// Add game data to response so client can restore board
|
|
response.activeGame = {
|
|
gameId: gameId,
|
|
opponent: game.player1Id === playerId ? game.player2Username : game.player1Username,
|
|
opponentId: game.player1Id === playerId ? game.player2Id : game.player1Id,
|
|
yourSymbol: game.player1Id === playerId ? game.player1Symbol : game.player2Symbol,
|
|
boardSize: game.boardSize,
|
|
yourTurn: game.currentTurn === playerId,
|
|
board: game.board,
|
|
currentTurnSymbol: game.currentTurn === game.player1Id ? game.player1Symbol : game.player2Symbol
|
|
};
|
|
break;
|
|
}
|
|
}
|
|
|
|
return response;
|
|
} catch (error) {
|
|
console.error('Error registering player:', error);
|
|
return { success: false, error: 'Registration failed' };
|
|
}
|
|
}
|
|
|
|
// Handle player disconnect
|
|
async handleDisconnect(socket) {
|
|
const player = this.players.get(socket.id);
|
|
if (!player) return;
|
|
|
|
// Check if player is in an active game
|
|
if (player.currentGameId) {
|
|
const game = this.activeGames.get(player.currentGameId);
|
|
if (game && game.state === 'active') {
|
|
// Notify opponent
|
|
const opponentId = game.player1Id === player.id ? game.player2Id : game.player1Id;
|
|
const opponentSocketId = this.playerSockets.get(opponentId);
|
|
|
|
if (opponentSocketId) {
|
|
this.io.to(opponentSocketId).emit('opponent_disconnected', {
|
|
message: `${player.username} disconnected`,
|
|
waitTime: 30
|
|
});
|
|
}
|
|
|
|
// Set timeout for game abandonment
|
|
const timeoutId = setTimeout(async () => {
|
|
// Check if player has reconnected (is in playerSockets)
|
|
// We check this.playerSockets because if they reconnected, registerPlayer
|
|
// would have put them back in there.
|
|
const hasReconnected = this.playerSockets.has(player.id);
|
|
|
|
if (!hasReconnected && game.state === 'active') {
|
|
// Award win to opponent
|
|
await this.db.abandonGame(player.currentGameId, opponentId);
|
|
|
|
if (opponentSocketId) {
|
|
const opponentPlayer = await this.db.getPlayerById(opponentId);
|
|
this.io.to(opponentSocketId).emit('game_ended', {
|
|
reason: 'opponent_abandoned',
|
|
winner: opponentId,
|
|
stats: {
|
|
wins: opponentPlayer.total_wins,
|
|
losses: opponentPlayer.total_losses,
|
|
draws: opponentPlayer.total_draws
|
|
}
|
|
});
|
|
}
|
|
|
|
this.activeGames.delete(player.currentGameId);
|
|
}
|
|
|
|
this.disconnectTimeouts.delete(player.id);
|
|
}, 30000); // 30 second grace period
|
|
|
|
this.disconnectTimeouts.set(player.id, timeoutId);
|
|
}
|
|
}
|
|
|
|
// Remove from active lists
|
|
await this.db.removeSession(socket.id);
|
|
this.players.delete(socket.id);
|
|
this.playerSockets.delete(player.id);
|
|
|
|
// Broadcast updated player list
|
|
await this.broadcastActivePlayers();
|
|
}
|
|
|
|
// Broadcast active players to all connected clients
|
|
async broadcastActivePlayers() {
|
|
try {
|
|
const activePlayers = await this.db.getActivePlayers();
|
|
this.io.emit('active_players_update', activePlayers);
|
|
} catch (error) {
|
|
console.error('Error broadcasting active players:', error);
|
|
}
|
|
}
|
|
|
|
// Send challenge
|
|
async sendChallenge(socket, targetUsername, boardSize) {
|
|
const challenger = this.players.get(socket.id);
|
|
if (!challenger) {
|
|
return { success: false, error: 'Not registered' };
|
|
}
|
|
|
|
// Find target player
|
|
const target = Array.from(this.players.values()).find(p => p.username === targetUsername);
|
|
if (!target) {
|
|
return { success: false, error: 'Player not found' };
|
|
}
|
|
|
|
if (target.currentGameId) {
|
|
return { success: false, error: 'Player is already in a game' };
|
|
}
|
|
|
|
// Create challenge
|
|
const challengeId = `${challenger.id}-${target.id}-${Date.now()}`;
|
|
this.challenges.set(challengeId, {
|
|
id: challengeId,
|
|
challengerId: challenger.id,
|
|
challengerUsername: challenger.username,
|
|
targetId: target.id,
|
|
targetUsername: target.username,
|
|
boardSize: boardSize,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
// Send challenge to target
|
|
this.io.to(target.socketId).emit('challenge_received', {
|
|
challengeId: challengeId,
|
|
from: challenger.username,
|
|
boardSize: boardSize
|
|
});
|
|
|
|
return { success: true, message: 'Challenge sent' };
|
|
}
|
|
|
|
// Accept challenge
|
|
async acceptChallenge(socket, challengeId) {
|
|
const player = this.players.get(socket.id);
|
|
const challenge = this.challenges.get(challengeId);
|
|
|
|
if (!player || !challenge) {
|
|
return { success: false, error: 'Invalid challenge' };
|
|
}
|
|
|
|
if (challenge.targetId !== player.id) {
|
|
return { success: false, error: 'Not your challenge' };
|
|
}
|
|
|
|
// Remove challenge from pending
|
|
this.challenges.delete(challengeId);
|
|
|
|
// Create game in database
|
|
const gameId = await this.db.createGame(
|
|
challenge.challengerId,
|
|
challenge.targetId,
|
|
challenge.challengerUsername,
|
|
challenge.targetUsername,
|
|
challenge.boardSize
|
|
);
|
|
|
|
// Randomly assign X and O
|
|
const player1IsX = Math.random() < 0.5;
|
|
|
|
// Create game in memory
|
|
const gameData = {
|
|
id: gameId,
|
|
player1Id: challenge.challengerId,
|
|
player2Id: challenge.targetId,
|
|
player1Username: challenge.challengerUsername,
|
|
player2Username: challenge.targetUsername,
|
|
player1Symbol: player1IsX ? 'X' : 'O',
|
|
player2Symbol: player1IsX ? 'O' : 'X',
|
|
boardSize: challenge.boardSize,
|
|
currentTurn: challenge.challengerId, // Challenger goes first
|
|
state: 'active',
|
|
board: Array(challenge.boardSize).fill(null).map(() => Array(challenge.boardSize).fill(null)),
|
|
moveCount: 0
|
|
};
|
|
|
|
this.activeGames.set(gameId, gameData);
|
|
|
|
// Update players' current game
|
|
const challenger = this.players.get(this.playerSockets.get(challenge.challengerId));
|
|
const accepter = this.players.get(socket.id);
|
|
if (challenger) challenger.currentGameId = gameId;
|
|
if (accepter) accepter.currentGameId = gameId;
|
|
|
|
// Notify both players
|
|
const challengerSocket = this.playerSockets.get(challenge.challengerId);
|
|
if (challengerSocket) {
|
|
this.io.to(challengerSocket).emit('game_started', {
|
|
gameId: gameId,
|
|
opponent: challenge.targetUsername,
|
|
opponentId: challenge.targetId,
|
|
yourSymbol: gameData.player1Symbol,
|
|
boardSize: challenge.boardSize,
|
|
yourTurn: true
|
|
});
|
|
}
|
|
|
|
this.io.to(socket.id).emit('game_started', {
|
|
gameId: gameId,
|
|
opponent: challenge.challengerUsername,
|
|
opponentId: challenge.challengerId,
|
|
yourSymbol: gameData.player2Symbol,
|
|
boardSize: challenge.boardSize,
|
|
yourTurn: false
|
|
});
|
|
|
|
return { success: true };
|
|
}
|
|
|
|
// Decline challenge
|
|
declineChallenge(socket, challengeId) {
|
|
const challenge = this.challenges.get(challengeId);
|
|
if (!challenge) return;
|
|
|
|
this.challenges.delete(challengeId);
|
|
|
|
// Notify challenger
|
|
const challengerSocket = this.playerSockets.get(challenge.challengerId);
|
|
if (challengerSocket) {
|
|
this.io.to(challengerSocket).emit('challenge_declined', {
|
|
by: challenge.targetUsername
|
|
});
|
|
}
|
|
}
|
|
|
|
// Handle game move
|
|
async handleMove(socket, moveData) {
|
|
const player = this.players.get(socket.id);
|
|
if (!player) return { success: false, error: 'Not registered' };
|
|
|
|
const game = this.activeGames.get(moveData.gameId);
|
|
if (!game) return { success: false, error: 'Game not found' };
|
|
|
|
if (game.state !== 'active') return { success: false, error: 'Game not active' };
|
|
|
|
// Validate it's player's turn
|
|
if (game.currentTurn !== player.id) {
|
|
return { success: false, error: 'Not your turn' };
|
|
}
|
|
|
|
// Validate move
|
|
const { row, col } = moveData;
|
|
if (row < 0 || row >= game.boardSize || col < 0 || col >= game.boardSize) {
|
|
return { success: false, error: 'Invalid position' };
|
|
}
|
|
|
|
if (game.board[row][col] !== null) {
|
|
return { success: false, error: 'Cell occupied' };
|
|
}
|
|
|
|
// Determine player's symbol
|
|
const playerSymbol = game.player1Id === player.id ? game.player1Symbol : game.player2Symbol;
|
|
|
|
// Make move
|
|
game.board[row][col] = playerSymbol;
|
|
game.moveCount++;
|
|
|
|
// Record move in database
|
|
await this.db.recordMove(game.id, player.id, row, col, game.moveCount);
|
|
|
|
// Broadcast move to opponent
|
|
const opponentId = game.player1Id === player.id ? game.player2Id : game.player1Id;
|
|
const opponentSocket = this.playerSockets.get(opponentId);
|
|
|
|
if (opponentSocket) {
|
|
this.io.to(opponentSocket).emit('opponent_move', {
|
|
row: row,
|
|
col: col,
|
|
symbol: playerSymbol
|
|
});
|
|
}
|
|
|
|
// Check for win
|
|
const winner = this.checkWin(game, row, col, playerSymbol);
|
|
if (winner) {
|
|
game.state = 'completed';
|
|
await this.db.completeGame(game.id, player.id);
|
|
|
|
const winnerPlayer = await this.db.getPlayerById(player.id);
|
|
const loserPlayer = await this.db.getPlayerById(opponentId);
|
|
|
|
// Notify both players
|
|
this.io.to(socket.id).emit('game_ended', {
|
|
reason: 'win',
|
|
winner: player.id,
|
|
stats: {
|
|
wins: winnerPlayer.total_wins,
|
|
losses: winnerPlayer.total_losses,
|
|
draws: winnerPlayer.total_draws
|
|
}
|
|
});
|
|
|
|
if (opponentSocket) {
|
|
this.io.to(opponentSocket).emit('game_ended', {
|
|
reason: 'loss',
|
|
winner: player.id,
|
|
stats: {
|
|
wins: loserPlayer.total_wins,
|
|
losses: loserPlayer.total_losses,
|
|
draws: loserPlayer.total_draws
|
|
}
|
|
});
|
|
}
|
|
|
|
// Clean up
|
|
this.activeGames.delete(game.id);
|
|
if (this.players.has(socket.id)) this.players.get(socket.id).currentGameId = null;
|
|
if (opponentSocket && this.players.has(opponentSocket)) {
|
|
this.players.get(opponentSocket).currentGameId = null;
|
|
}
|
|
|
|
return { success: true, gameOver: true, winner: true };
|
|
}
|
|
|
|
// Check for draw
|
|
if (game.moveCount === game.boardSize * game.boardSize) {
|
|
game.state = 'completed';
|
|
await this.db.completeGame(game.id, null);
|
|
|
|
const player1Data = await this.db.getPlayerById(game.player1Id);
|
|
const player2Data = await this.db.getPlayerById(game.player2Id);
|
|
|
|
this.io.to(socket.id).emit('game_ended', {
|
|
reason: 'draw',
|
|
stats: {
|
|
wins: player.id === game.player1Id ? player1Data.total_wins : player2Data.total_wins,
|
|
losses: player.id === game.player1Id ? player1Data.total_losses : player2Data.total_losses,
|
|
draws: player.id === game.player1Id ? player1Data.total_draws : player2Data.total_draws
|
|
}
|
|
});
|
|
|
|
if (opponentSocket) {
|
|
this.io.to(opponentSocket).emit('game_ended', {
|
|
reason: 'draw',
|
|
stats: {
|
|
wins: opponentId === game.player1Id ? player1Data.total_wins : player2Data.total_wins,
|
|
losses: opponentId === game.player1Id ? player1Data.total_losses : player2Data.total_losses,
|
|
draws: opponentId === game.player1Id ? player1Data.total_draws : player2Data.total_draws
|
|
}
|
|
});
|
|
}
|
|
|
|
this.activeGames.delete(game.id);
|
|
if (this.players.has(socket.id)) this.players.get(socket.id).currentGameId = null;
|
|
if (opponentSocket && this.players.has(opponentSocket)) {
|
|
this.players.get(opponentSocket).currentGameId = null;
|
|
}
|
|
|
|
return { success: true, gameOver: true, draw: true };
|
|
}
|
|
|
|
// Switch turn
|
|
game.currentTurn = opponentId;
|
|
|
|
return { success: true, gameOver: false };
|
|
}
|
|
|
|
// Check win condition (same as frontend logic)
|
|
checkWin(game, row, col, symbol) {
|
|
const directions = [
|
|
[0, 1], // Horizontal
|
|
[1, 0], // Vertical
|
|
[1, 1], // Diagonal \
|
|
[1, -1] // Diagonal /
|
|
];
|
|
|
|
for (const [dx, dy] of directions) {
|
|
let count = 1;
|
|
count += this.countDirection(game, row, col, dx, dy, symbol);
|
|
count += this.countDirection(game, row, col, -dx, -dy, symbol);
|
|
|
|
if (count >= 5) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
countDirection(game, row, col, dx, dy, symbol) {
|
|
let count = 0;
|
|
let r = row + dx;
|
|
let c = col + dy;
|
|
|
|
while (
|
|
r >= 0 && r < game.boardSize &&
|
|
c >= 0 && c < game.boardSize &&
|
|
game.board[r][c] === symbol
|
|
) {
|
|
count++;
|
|
r += dx;
|
|
c += dy;
|
|
}
|
|
|
|
return count;
|
|
}
|
|
|
|
// Heartbeat to keep session alive
|
|
async heartbeat(socket) {
|
|
const player = this.players.get(socket.id);
|
|
if (player) {
|
|
await this.db.updateHeartbeat(socket.id);
|
|
}
|
|
}
|
|
|
|
// Handle player surrender
|
|
async handleSurrender(socket, data) {
|
|
const player = this.players.get(socket.id);
|
|
if (!player) {
|
|
socket.emit('error', { message: 'Player not found' });
|
|
return;
|
|
}
|
|
|
|
const game = this.activeGames.get(data.gameId);
|
|
if (!game) {
|
|
socket.emit('error', { message: 'Game not found' });
|
|
return;
|
|
}
|
|
|
|
if (game.state !== 'active') {
|
|
socket.emit('error', { message: 'Game is already ending' });
|
|
return;
|
|
}
|
|
|
|
// Set state immediately to prevent double-surrender race conditions
|
|
game.state = 'completed';
|
|
|
|
// Determine winner (the opponent)
|
|
const winnerId = game.player1Id === player.id ? game.player2Id : game.player1Id;
|
|
|
|
// Update database - mark as completed with winner
|
|
try {
|
|
await this.db.abandonGame(data.gameId, winnerId);
|
|
|
|
// Get updated stats
|
|
const loserStats = await this.db.getPlayerById(player.id);
|
|
const winnerStats = await this.db.getPlayerById(winnerId);
|
|
|
|
// Find opponent socket
|
|
const opponentSocket = this.playerSockets.get(winnerId);
|
|
|
|
// Notify both players
|
|
socket.emit('game_ended', {
|
|
reason: 'surrender',
|
|
message: 'You surrendered',
|
|
stats: {
|
|
wins: loserStats.total_wins,
|
|
losses: loserStats.total_losses,
|
|
draws: loserStats.total_draws
|
|
}
|
|
});
|
|
|
|
if (opponentSocket) {
|
|
this.io.to(opponentSocket).emit('game_ended', {
|
|
reason: 'win',
|
|
message: `${player.username} surrendered`,
|
|
stats: {
|
|
wins: winnerStats.total_wins,
|
|
losses: winnerStats.total_losses,
|
|
draws: winnerStats.total_draws
|
|
}
|
|
});
|
|
}
|
|
|
|
// Clean up
|
|
this.activeGames.delete(data.gameId);
|
|
if (this.players.has(socket.id)) this.players.get(socket.id).currentGameId = null;
|
|
if (opponentSocket && this.players.has(opponentSocket)) {
|
|
this.players.get(opponentSocket).currentGameId = null;
|
|
}
|
|
|
|
} catch (error) {
|
|
console.error('Error handling surrender:', error);
|
|
socket.emit('error', { message: 'Failed to process surrender' });
|
|
}
|
|
}
|
|
|
|
// Send rematch request
|
|
sendRematch(socket, data) {
|
|
const player = this.players.get(socket.id);
|
|
if (!player) return;
|
|
|
|
// Find opponent's socket
|
|
const opponentSocket = this.playerSockets.get(data.opponentId);
|
|
|
|
if (!opponentSocket) {
|
|
socket.emit('error', { message: 'Opponent not online' });
|
|
return;
|
|
}
|
|
|
|
const rematchId = `rematch_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
|
|
if (!this.rematches) {
|
|
this.rematches = new Map();
|
|
}
|
|
|
|
this.rematches.set(rematchId, {
|
|
rematchId,
|
|
challenger: player.id,
|
|
challengerUsername: player.username,
|
|
challenged: data.opponentId,
|
|
boardSize: data.boardSize || 15,
|
|
timestamp: Date.now()
|
|
});
|
|
|
|
this.io.to(opponentSocket).emit('rematch_request', {
|
|
rematchId,
|
|
from: player.username,
|
|
boardSize: data.boardSize || 15
|
|
});
|
|
}
|
|
|
|
// Accept rematch
|
|
async acceptRematch(socket, data) {
|
|
const player = this.players.get(socket.id);
|
|
if (!player) return;
|
|
|
|
const rematch = this.rematches.get(data.rematchId);
|
|
if (!rematch) {
|
|
socket.emit('error', { message: 'Rematch request expired' });
|
|
return;
|
|
}
|
|
|
|
// Verify players are not already in a game
|
|
if (player.currentGameId) {
|
|
socket.emit('error', { message: 'You are already in a game' });
|
|
return;
|
|
}
|
|
|
|
// Verify this player is the challenged one
|
|
if (rematch.challenged !== player.id) {
|
|
socket.emit('error', { message: 'Invalid rematch accept' });
|
|
return;
|
|
}
|
|
|
|
// Find challenger's socket
|
|
const challengerSocket = this.playerSockets.get(rematch.challenger);
|
|
|
|
if (!challengerSocket) {
|
|
socket.emit('error', { message: 'Challenger no longer online' });
|
|
this.rematches.delete(data.rematchId);
|
|
return;
|
|
}
|
|
|
|
const challenger = this.players.get(challengerSocket);
|
|
if (challenger && challenger.currentGameId) {
|
|
socket.emit('error', { message: 'Challenger is already in a game' });
|
|
this.rematches.delete(data.rematchId);
|
|
return;
|
|
}
|
|
|
|
// Create new game (similar to acceptChallenge logic)
|
|
try {
|
|
const gameId = await this.db.createGame(
|
|
rematch.challenger,
|
|
player.id,
|
|
rematch.challengerUsername,
|
|
player.username,
|
|
rematch.boardSize
|
|
);
|
|
|
|
// Randomly assign symbols
|
|
const player1IsX = Math.random() < 0.5;
|
|
|
|
const gameState = {
|
|
id: gameId,
|
|
player1Id: rematch.challenger,
|
|
player2Id: player.id,
|
|
player1Username: rematch.challengerUsername,
|
|
player2Username: player.username,
|
|
player1Symbol: player1IsX ? 'X' : 'O',
|
|
player2Symbol: player1IsX ? 'O' : 'X',
|
|
currentTurn: rematch.challenger,
|
|
boardSize: rematch.boardSize,
|
|
state: 'active',
|
|
board: Array(rematch.boardSize).fill(null).map(() => Array(rematch.boardSize).fill(null)),
|
|
moveCount: 0
|
|
};
|
|
|
|
this.activeGames.set(gameId, gameState);
|
|
|
|
// Update players' current game
|
|
const challenger = this.players.get(challengerSocket);
|
|
const accepter = this.players.get(socket.id);
|
|
if (challenger) challenger.currentGameId = gameId;
|
|
if (accepter) accepter.currentGameId = gameId;
|
|
|
|
// Notify both players
|
|
this.io.to(challengerSocket).emit('rematch_accepted', {
|
|
gameId: gameId,
|
|
opponent: player.username,
|
|
opponentId: player.id,
|
|
yourSymbol: gameState.player1Symbol,
|
|
yourTurn: true,
|
|
boardSize: rematch.boardSize
|
|
});
|
|
|
|
this.io.to(socket.id).emit('game_started', {
|
|
gameId: gameId,
|
|
opponent: rematch.challengerUsername,
|
|
opponentId: rematch.challenger,
|
|
yourSymbol: gameState.player2Symbol,
|
|
yourTurn: false,
|
|
boardSize: rematch.boardSize
|
|
});
|
|
|
|
this.rematches.delete(data.rematchId);
|
|
|
|
} catch (error) {
|
|
console.error('Error accepting rematch:', error);
|
|
socket.emit('error', { message: 'Failed to start rematch' });
|
|
}
|
|
}
|
|
|
|
// Decline rematch
|
|
declineRematch(socket, data) {
|
|
const player = this.players.get(socket.id);
|
|
if (!player) return;
|
|
|
|
const rematch = this.rematches.get(data.rematchId);
|
|
if (!rematch) return;
|
|
|
|
// Find challenger's socket
|
|
const challengerSocket = this.playerSockets.get(rematch.challenger);
|
|
|
|
if (challengerSocket) {
|
|
this.io.to(challengerSocket).emit('rematch_declined', {
|
|
by: player.username
|
|
});
|
|
}
|
|
|
|
this.rematches.delete(data.rematchId);
|
|
}
|
|
}
|
|
|
|
module.exports = GameManager;
|