This commit is contained in:
sepia 2025-07-15 12:13:17 -05:00
commit c15c9c16c8
15 changed files with 1601 additions and 0 deletions

View file

@ -0,0 +1,138 @@
import { GameInstance } from './GameInstance';
import { expect, test, describe } from 'bun:test';
describe('GameInstance', () => {
test('should initialize with correct default state', () => {
const game = new GameInstance();
expect(game.id).toBeDefined();
expect(game.board.length).toBe(15);
expect(game.board[0].length).toBe(15);
expect(game.currentPlayer).toBeNull();
expect(game.status).toBe('waiting');
expect(game.winner).toBeNull();
expect(game.players).toEqual({});
});
test('should add players correctly', () => {
const game = new GameInstance();
const player1 = 'player1-uuid';
const player2 = 'player2-uuid';
// First player joins as black
const joined1 = game.addPlayer(player1);
expect(joined1).toBe(true);
// Second player joins as white
const joined2 = game.addPlayer(player2);
expect(joined2).toBe(true);
// Game should now be in playing state
expect(game.status).toBe('playing');
expect(game.currentPlayer).toBe('black');
expect(game.players.black).toBe(player1);
expect(game.players.white).toBe(player2);
});
test('should prevent more than two players from joining', () => {
const game = new GameInstance();
const player1 = 'player1-uuid';
const player2 = 'player2-uuid';
const player3 = 'player3-uuid';
game.addPlayer(player1);
game.addPlayer(player2);
// Third player tries to join (should fail)
const joined = game.addPlayer(player3);
expect(joined).toBe(false);
// Players object should remain unchanged, only two players should be present
expect(game.players.black).toBe(player1);
expect(game.players.white).toBe(player2);
expect(Object.values(game.players).length).toBe(2);
});
test('should validate moves correctly', () => {
const game = new GameInstance();
const player1 = 'player1-uuid';
const player2 = 'player2-uuid';
game.addPlayer(player1);
game.addPlayer(player2);
// Player black makes first move
const move1 = game.makeMove(player1, 7, 7);
expect(move1.success).toBe(true);
// Same player tries to move again (should fail)
const move2 = game.makeMove(player1, 7, 8);
expect(move2.success).toBe(false);
// White player makes a move
const move3 = game.makeMove(player2, 7, 8);
expect(move3.success).toBe(true);
// Try to place on occupied cell (should fail)
const move4 = game.makeMove(player2, 7, 7);
expect(move4.success).toBe(false);
// Try to place out of bounds (should fail)
const move5 = game.makeMove(player2, 15, 15);
expect(move5.success).toBe(false);
});
test('should detect win conditions', () => {
const game = new GameInstance();
const player1 = 'player1-uuid';
const player2 = 'player2-uuid';
game.addPlayer(player1);
game.addPlayer(player2);
// Create a horizontal win for black
for (let col = 0; col < 5; col++) {
game.makeMove(player1, 7, col);
// Switch to other player for next move
if (col < 4) game.makeMove(player2, 8, col);
}
expect(game.winner).toBe('black');
expect(game.status).toBe('finished');
});
test('should detect draw condition', () => {
const game = new GameInstance();
const player1 = 'player1-uuid';
const player2 = 'player2-uuid';
game.addPlayer(player1);
game.addPlayer(player2);
// Create a pattern that doesn't result in a win but fills the board
// We'll use a simple alternating pattern
for (let row = 0; row < 15; row++) {
for (let col = 0; col < 15; col++) {
const currentPlayer = game.currentPlayer!;
const playerId = game.players[currentPlayer]!;
// Make move
const result = game.makeMove(playerId, row, col);
// If we can't make a move, it means someone won already
if (!result.success) {
expect(game.winner).not.toBeNull();
return;
}
}
}
expect(game.winner).toBe('draw');
expect(game.status).toBe('finished');
});
});

176
src/game/GameInstance.ts Normal file
View file

@ -0,0 +1,176 @@
import { v4 as uuidv4 } from 'uuid';
type PlayerColor = 'black' | 'white';
type GameStatus = 'waiting' | 'playing' | 'finished';
type BoardCell = null | 'black' | 'white';
export class GameInstance {
public readonly id: string;
public readonly board: BoardCell[][];
public currentPlayer: PlayerColor | null;
public status: GameStatus;
public winner: null | PlayerColor | 'draw';
public players: { black?: string; white?: string };
private readonly boardSize = 15;
private moveCount = 0;
constructor() {
this.id = uuidv4();
this.board = Array.from({ length: this.boardSize }, () =>
Array(this.boardSize).fill(null),
);
this.currentPlayer = null;
this.status = 'waiting';
this.winner = null;
this.players = {};
}
public getPlayerCount(): number {
return Object.values(this.players).filter(Boolean).length;
}
public addPlayer(playerId: string): boolean {
// If game is full, prevent new players from joining.
if (this.getPlayerCount() >= 2) {
return false;
}
// If player is already in the game, return true.
if (Object.values(this.players).includes(playerId)) {
return true;
}
// Assign black if available, otherwise white
if (!this.players.black) {
this.players.black = playerId;
} else if (!this.players.white) {
this.players.white = playerId;
} else {
return false; // Should not happen if getPlayerCount() check is correct
}
// If both players have joined, start the game.
if (this.players.black && this.players.white) {
this.currentPlayer = 'black';
this.status = 'playing';
}
return true;
}
public makeMove(
playerId: string,
row: number,
col: number,
): { success: boolean; error?: string } {
// Find player's color
let playerColor: PlayerColor | null = null;
for (const [color, id] of Object.entries(this.players)) {
if (id === playerId) {
playerColor = color as PlayerColor;
break;
}
}
if (!playerColor) {
return { success: false, error: 'Player not in this game' };
}
// Validate it's the player's turn
if (this.currentPlayer !== playerColor) {
return { success: false, error: 'Not your turn' };
}
// Validate move is within bounds
if (row < 0 || row >= this.boardSize || col < 0 || col >= this.boardSize) {
return { success: false, error: 'Move out of bounds' };
}
// Validate cell is empty
if (this.board[row][col] !== null) {
return { success: false, error: 'Cell already occupied' };
}
// Make the move
this.board[row][col] = playerColor;
this.moveCount++;
// Check for win condition
if (this.checkWin(row, col, playerColor)) {
this.winner = playerColor;
this.status = 'finished';
this.currentPlayer = null;
return { success: true };
}
// Check for draw condition
if (this.moveCount === this.boardSize * this.boardSize) {
this.winner = 'draw';
this.status = 'finished';
this.currentPlayer = null;
return { success: true };
}
// Switch turns
this.currentPlayer = playerColor === 'black' ? 'white' : 'black';
return { success: true };
}
private checkWin(row: number, col: number, color: PlayerColor): boolean {
const directions = [
[1, 0], // vertical
[0, 1], // horizontal
[1, 1], // diagonal down-right
[1, -1], // diagonal down-left
];
for (const [dx, dy] of directions) {
let count = 1;
// Check in positive direction
for (let i = 1; i < 5; i++) {
const newRow = row + dx * i;
const newCol = col + dy * i;
if (
newRow < 0 ||
newRow >= this.boardSize ||
newCol < 0 ||
newCol >= this.boardSize
) {
break;
}
if (this.board[newRow][newCol] === color) {
count++;
} else {
break;
}
}
// Check in negative direction
for (let i = 1; i < 5; i++) {
const newRow = row - dx * i;
const newCol = col - dy * i;
if (
newRow < 0 ||
newRow >= this.boardSize ||
newCol < 0 ||
newCol >= this.boardSize
) {
break;
}
if (this.board[newRow][newCol] === color) {
count++;
} else {
break;
}
}
if (count >= 5) {
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,48 @@
import { describe, it, expect, beforeEach } from 'bun:test';
import { GameManager } from './GameManager';
import { GameInstance } from './GameInstance';
describe('GameManager', () => {
let gameManager: GameManager;
beforeEach(() => {
gameManager = new GameManager();
});
it('should create a new game', () => {
const game = gameManager.createGame();
expect(game).toBeInstanceOf(GameInstance);
expect(gameManager.getGame(game.id)).toBe(game);
});
it('should allow players to join games', () => {
const game = gameManager.createGame();
const playerId = 'player1';
const result = gameManager.joinGame(game.id, playerId);
expect(result).toBe(true);
// Add more assertions based on GameInstance implementation
});
it('should not allow joining non-existent games', () => {
const result = gameManager.joinGame('non-existent-id', 'player1');
expect(result).toBe(false);
});
it('should retrieve existing games', () => {
const game = gameManager.createGame();
const retrievedGame = gameManager.getGame(game.id);
expect(retrievedGame).toBe(game);
});
it('should return null for non-existent games', () => {
const game = gameManager.getGame('non-existent-id');
expect(game).toBeNull();
});
it('should remove games', () => {
const game = gameManager.createGame();
gameManager.removeGame(game.id);
const retrievedGame = gameManager.getGame(game.id);
expect(retrievedGame).toBeNull();
});
});

31
src/game/GameManager.ts Normal file
View file

@ -0,0 +1,31 @@
import { GameInstance } from './GameInstance';
export class GameManager {
private games: Map<string, GameInstance>;
constructor() {
this.games = new Map();
}
createGame(): GameInstance {
const game = new GameInstance();
this.games.set(game.id, game);
return game;
}
getGame(gameId: string): GameInstance | null {
return this.games.get(gameId) || null;
}
public joinGame(gameId: string, playerId: string): boolean {
const game = this.games.get(gameId);
if (!game) {
return false;
}
return game.addPlayer(playerId);
}
removeGame(gameId: string): void {
this.games.delete(gameId);
}
}

View file

@ -0,0 +1,381 @@
import { describe, it, expect, beforeEach, mock } from 'bun:test';
import { WebSocketHandler } from './WebSocketHandler';
import { GameManager } from './GameManager';
import { GameInstance } from './GameInstance';
describe('WebSocketHandler', () => {
let gameManager: GameManager;
let webSocketHandler: WebSocketHandler;
let mockWs: any;
let mockWsData: { request: {}; gameId?: string; playerId?: string };
beforeEach(() => {
gameManager = new GameManager();
mockWsData = { request: {} };
mockWs = {
send: mock(() => {}),
on: mock((event: string, callback: Function) => {
if (event === 'message') mockWs._messageCallback = callback;
if (event === 'close') mockWs._closeCallback = callback;
if (event === 'error') mockWs._errorCallback = callback;
}),
_messageCallback: null,
_closeCallback: null,
_errorCallback: null,
data: mockWsData,
};
webSocketHandler = new WebSocketHandler(gameManager);
});
const triggerMessage = (message: string) => {
if (mockWs._messageCallback) {
mockWs._messageCallback(message);
}
};
const triggerClose = () => {
if (mockWs._closeCallback) {
mockWs._closeCallback();
}
};
it('should handle a new connection', () => {
webSocketHandler.handleConnection(mockWs, mockWs.data.request);
expect(mockWs.on).toHaveBeenCalledWith('message', expect.any(Function));
expect(mockWs.on).toHaveBeenCalledWith('close', expect.any(Function));
expect(mockWs.on).toHaveBeenCalledWith('error', expect.any(Function));
});
it('should handle a join_game message for a new game', () => {
webSocketHandler.handleConnection(mockWs, mockWs.data.request);
const joinGameMessage = JSON.stringify({
type: 'join_game',
playerId: 'player1',
});
triggerMessage(joinGameMessage);
expect(mockWs.send).toHaveBeenCalledWith(
expect.stringContaining('game_state'),
);
expect(mockWsData.gameId).toBeDefined();
expect(mockWsData.playerId).toBe('player1');
});
it('should handle a join_game message for an existing game', () => {
const game = gameManager.createGame();
gameManager.joinGame(game.id, 'player1');
webSocketHandler.handleConnection(mockWs, mockWs.data.request);
const joinGameMessage = JSON.stringify({
type: 'join_game',
gameId: game.id,
playerId: 'player2',
});
triggerMessage(joinGameMessage);
expect(mockWs.send).toHaveBeenCalledWith(
expect.stringContaining('game_state'),
);
expect(mockWsData.gameId).toBe(game.id);
expect(mockWsData.playerId).toBe('player2');
});
it('should handle a make_move message', () => {
const game = gameManager.createGame();
gameManager.joinGame(game.id, 'player1');
gameManager.joinGame(game.id, 'player2');
game.status = 'playing';
mockWsData.gameId = game.id;
mockWsData.playerId = 'player1';
webSocketHandler.handleConnection(mockWs, mockWs.data.request);
const makeMoveMessage = JSON.stringify({
type: 'make_move',
row: 7,
col: 7,
});
triggerMessage(makeMoveMessage);
expect(mockWs.send).toHaveBeenCalledWith(
JSON.stringify({ type: 'move_result', success: true }),
);
expect(game.board[7][7]).toBe('black');
});
it('should send an error for an invalid move', () => {
const game = gameManager.createGame();
gameManager.joinGame(game.id, 'player1');
gameManager.joinGame(game.id, 'player2');
game.status = 'playing';
mockWsData.gameId = game.id;
mockWsData.playerId = 'player1';
webSocketHandler.handleConnection(mockWs, mockWs.data.request);
const makeMoveMessage1 = JSON.stringify({
type: 'make_move',
row: 7,
col: 7,
});
triggerMessage(makeMoveMessage1);
expect(mockWs.send).toHaveBeenCalledWith(
JSON.stringify({ type: 'move_result', success: true }),
);
game.currentPlayer = 'black';
const makeMoveMessage2 = JSON.stringify({
type: 'make_move',
row: 7,
col: 7,
});
triggerMessage(makeMoveMessage2);
expect(mockWs.send).toHaveBeenCalledWith(
JSON.stringify({ type: 'error', error: 'Cell already occupied' }),
);
});
it('should handle ping/pong messages', () => {
webSocketHandler.handleConnection(mockWs, mockWs.data.request);
const pingMessage = JSON.stringify({ type: 'ping' });
triggerMessage(pingMessage);
expect(mockWs.send).toHaveBeenCalledWith(JSON.stringify({ type: 'pong' }));
});
it('should handle player disconnection', () => {
webSocketHandler.handleConnection(mockWs, mockWs.data.request);
mockWsData.gameId = 'test-game-id';
mockWsData.playerId = 'test-player-id';
triggerClose();
});
it('should send error for unknown message type', () => {
webSocketHandler.handleConnection(mockWs, mockWs.data.request);
const unknownMessage = JSON.stringify({ type: 'unknown_type' });
triggerMessage(unknownMessage);
expect(mockWs.send).toHaveBeenCalledWith(
JSON.stringify({ type: 'error', error: 'Unknown message type' }),
);
});
it('should send error for invalid JSON message', () => {
webSocketHandler.handleConnection(mockWs, mockWs.data.request);
const invalidJsonMessage = 'not a json';
triggerMessage(invalidJsonMessage);
expect(mockWs.send).toHaveBeenCalledWith(
JSON.stringify({ type: 'error', error: 'Invalid message format' }),
);
});
});
it('should notify other players and remove a disconnected player', () => {
const gameManager = new GameManager();
const webSocketHandler = new WebSocketHandler(gameManager);
// Player 1
let mockWsData1: { request: {}; gameId?: string; playerId?: string } = { request: {} };
const mockWs1: any = {
send: mock(() => {}),
on: mock((event: string, callback: Function) => {
if (event === 'message') mockWs1._messageCallback = callback;
if (event === 'close') mockWs1._closeCallback = callback;
}),
_messageCallback: null,
_closeCallback: null,
data: mockWsData1,
};
// Player 2
let mockWsData2: { request: {}; gameId?: string; playerId?: string } = { request: {} };
const mockWs2: any = {
send: mock(() => {}),
on: mock((event: string, callback: Function) => {
if (event === 'message') mockWs2._messageCallback = callback;
if (event === 'close') mockWs2._closeCallback = callback;
}),
_messageCallback: null,
_closeCallback: null,
data: mockWsData2,
};
const triggerMessageForWs = (ws: any, message: string) => {
if (ws._messageCallback) {
ws._messageCallback(message);
}
};
const triggerCloseForWs = (ws: any) => {
if (ws._closeCallback) {
ws._closeCallback();
}
};
// Player 1 joins, creates game
webSocketHandler.handleConnection(mockWs1, mockWs1.data.request);
triggerMessageForWs(mockWs1, JSON.stringify({ type: 'join_game', playerId: 'player1' }));
mockWs1.data.gameId = mockWsData1.gameId;
mockWs1.data.playerId = 'player1';
// Player 2 joins same game
webSocketHandler.handleConnection(mockWs2, mockWs2.data.request);
triggerMessageForWs(mockWs2, JSON.stringify({ type: 'join_game', gameId: mockWsData1.gameId, playerId: 'player2' }));
mockWs2.data.gameId = mockWsData1.gameId;
mockWs2.data.playerId = 'player2';
// Player 2 disconnects
mockWs1.send.mockClear(); // Clear P1's send history before P2 disconnects
triggerCloseForWs(mockWs2);
// Expect Player 1 to receive player_disconnected message
expect(mockWs1.send).toHaveBeenCalledTimes(1);
const receivedMessage = JSON.parse(mockWs1.send.mock.calls[0][0]);
expect(receivedMessage.type).toBe('player_disconnected');
expect(receivedMessage.playerId).toBe('player2');
expect(receivedMessage.gameId).toBe(mockWsData1.gameId);
// Verify connections map is updated (Player 2 removed)
// @ts-ignore
expect(webSocketHandler.connections.get(mockWsData1.gameId)).toContain(mockWs1);
// @ts-ignore
expect(webSocketHandler.connections.get(mockWsData1.gameId)).not.toContain(mockWs2);
});
it('should broadcast game state to other players when a new player joins', () => {
const gameManager = new GameManager();
const webSocketHandler = new WebSocketHandler(gameManager);
let mockWsData1: { request: {}; gameId?: string; playerId?: string } = { request: {} };
const mockWs1: any = {
send: mock(() => {}),
on: mock((event: string, callback: Function) => {
if (event === 'message') mockWs1._messageCallback = callback;
}),
_messageCallback: null,
data: mockWsData1,
};
let mockWsData2: { request: {}; gameId?: string; playerId?: string } = { request: {} };
const mockWs2: any = {
send: mock(() => {}),
on: mock((event: string, callback: Function) => {
if (event === 'message') mockWs2._messageCallback = callback;
}),
_messageCallback: null,
data: mockWsData2,
};
const triggerMessageForWs = (ws: any, message: string) => {
if (ws._messageCallback) {
ws._messageCallback(message);
}
};
// Player 1 joins and creates a new game
webSocketHandler.handleConnection(mockWs1, mockWs1.data.request);
const joinGameMessage1 = JSON.stringify({
type: 'join_game',
playerId: 'player1',
});
triggerMessageForWs(mockWs1, joinGameMessage1);
const player1GameId = mockWsData1.gameId;
// Player 2 joins the same game
webSocketHandler.handleConnection(mockWs2, mockWs2.data.request);
const joinGameMessage2 = JSON.stringify({
type: 'join_game',
gameId: player1GameId,
playerId: 'player2',
});
triggerMessageForWs(mockWs2, joinGameMessage2);
// Check that Player 1 received the game_state update after Player 2 joined
// Player 1 should have received two messages: initial join and then game_state after P2 joins
expect(mockWs1.send).toHaveBeenCalledTimes(2);
const secondCallArgs = mockWs1.send.mock.calls[1][0];
const receivedMessage = JSON.parse(secondCallArgs);
expect(receivedMessage.type).toBe('game_state');
expect(receivedMessage.state.players.black).toBe('player1');
expect(receivedMessage.state.players.white).toBe('player2');
});
it('should broadcast game state after a successful move', () => {
const gameManager = new GameManager();
const webSocketHandler = new WebSocketHandler(gameManager);
let mockWsData1: { request: {}; gameId?: string; playerId?: string } = { request: {} };
const mockWs1: any = {
send: mock(() => {}),
on: mock((event: string, callback: Function) => {
if (event === 'message') mockWs1._messageCallback = callback;
}),
_messageCallback: null,
data: mockWsData1,
};
let mockWsData2: { request: {}; gameId?: string; playerId?: string } = { request: {} };
const mockWs2: any = {
send: mock(() => {}),
on: mock((event: string, callback: Function) => {
if (event === 'message') mockWs2._messageCallback = callback;
}),
_messageCallback: null,
data: mockWsData2,
};
const triggerMessageForWs = (ws: any, message: string) => {
if (ws._messageCallback) {
ws._messageCallback(message);
}
};
// Player 1 joins and creates a new game
webSocketHandler.handleConnection(mockWs1, mockWs1.data.request);
const joinGameMessage1 = JSON.stringify({
type: 'join_game',
playerId: 'player1',
});
triggerMessageForWs(mockWs1, joinGameMessage1);
const player1GameId = mockWsData1.gameId;
mockWs1.data.gameId = player1GameId; // Manually set gameId for mockWs1
mockWs1.data.playerId = 'player1'; // Manually set playerId for mockWs1
// Player 2 joins the same game
webSocketHandler.handleConnection(mockWs2, mockWs2.data.request);
const joinGameMessage2 = JSON.stringify({
type: 'join_game',
gameId: player1GameId,
playerId: 'player2',
});
triggerMessageForWs(mockWs2, joinGameMessage2);
mockWs2.data.gameId = player1GameId; // Manually set gameId for mockWs2
mockWs2.data.playerId = 'player2'; // Manually set playerId for mockWs2
// Clear previous calls for clean assertion
mockWs1.send.mockClear();
mockWs2.send.mockClear();
// Player 1 makes a move
const makeMoveMessage = JSON.stringify({
type: 'make_move',
row: 7,
col: 7,
});
triggerMessageForWs(mockWs1, makeMoveMessage);
// Expect Player 2 to receive the game state update
expect(mockWs2.send).toHaveBeenCalledTimes(1);
const receivedMessage = JSON.parse(mockWs2.send.mock.calls[0][0]);
expect(receivedMessage.type).toBe('game_state');
expect(receivedMessage.state.board[7][7]).toBe('black');
});

View file

@ -0,0 +1,232 @@
import { GameManager } from './GameManager';
import { GameInstance } from './GameInstance';
interface WebSocketMessage {
type: string;
gameId?: string;
playerId?: string;
row?: number;
col?: number;
state?: any; // GameState
success?: boolean;
error?: string;
}
export class WebSocketHandler {
private gameManager: GameManager;
private connections: Map<string, Array<any>>; // Map of gameId to an array of connected websockets
constructor(gameManager: GameManager) {
this.gameManager = gameManager;
this.connections = new Map();
}
public handleConnection(ws: any, req: any): void {
console.log('WebSocket connected');
ws.on('message', (message: string) => {
this.handleMessage(ws, message);
});
ws.on('close', () => {
console.log('WebSocket disconnected');
this.handleDisconnect(ws);
});
ws.on('error', (error: Error) => {
console.error('WebSocket error:', error);
});
}
private handleMessage(ws: any, message: string): void {
try {
const parsedMessage: WebSocketMessage = JSON.parse(message);
console.log('Received message:', parsedMessage);
switch (parsedMessage.type) {
case 'join_game':
this.handleJoinGame(ws, parsedMessage);
break;
case 'make_move':
this.handleMakeMove(ws, parsedMessage);
break;
case 'ping':
ws.send(JSON.stringify({ type: 'pong' }));
break;
default:
ws.send(
JSON.stringify({ type: 'error', error: 'Unknown message type' }),
);
}
} catch (error) {
console.error('Failed to parse message:', message, error);
ws.send(
JSON.stringify({ type: 'error', error: 'Invalid message format' }),
);
}
}
private handleJoinGame(ws: any, message: WebSocketMessage): void {
const { gameId, playerId } = message;
if (!playerId) {
ws.send(JSON.stringify({ type: 'error', error: 'playerId is required' }));
return;
}
let game: GameInstance | null = null;
let isNewGame = false;
if (gameId) {
game = this.gameManager.getGame(gameId);
if (!game) {
ws.send(JSON.stringify({ type: 'error', error: 'Game not found' }));
return;
}
} else {
// Create a new game if no gameId is provided
game = this.gameManager.createGame();
isNewGame = true;
}
if (game && this.gameManager.joinGame(game.id, playerId)) {
ws.data.gameId = game.id; // Store gameId on the WebSocket object
ws.data.playerId = playerId; // Store playerId on the WebSocket object
if (!this.connections.has(game.id)) {
this.connections.set(game.id, []);
}
this.connections.get(game.id)?.push(ws);
const gameStateMessage = JSON.stringify({
type: 'game_state',
state: {
id: game.id,
board: game.board,
currentPlayer: game.currentPlayer,
status: game.status,
winner: game.winner,
players: game.players,
},
});
ws.send(gameStateMessage);
// Notify other players if any
this.connections.get(game.id)?.forEach((playerWs: any) => {
if (playerWs !== ws) { // Don't send back to the player who just joined
playerWs.send(gameStateMessage);
}
});
console.log(`${playerId} joined game ${game.id}`);
} else {
ws.send(JSON.stringify({ type: 'error', error: 'Failed to join game' }));
}
}
private handleMakeMove(ws: any, message: WebSocketMessage): void {
const { row, col } = message;
const gameId = ws.data.gameId;
const playerId = ws.data.playerId;
if (!gameId || !playerId) {
ws.send(JSON.stringify({ type: 'error', error: 'Not in a game' }));
return;
}
const game = this.gameManager.getGame(gameId);
if (!game) {
ws.send(JSON.stringify({ type: 'error', error: 'Game not found' }));
return;
}
if (row === undefined || col === undefined) {
ws.send(
JSON.stringify({ type: 'error', error: 'Invalid move coordinates' }),
);
return;
}
const playerColor =
game.players.black === playerId
? 'black'
: game.players.white === playerId
? 'white'
: null;
if (!playerColor) {
ws.send(
JSON.stringify({
type: 'error',
error: 'You are not a player in this game',
}),
);
return;
}
if (game.currentPlayer !== playerColor) {
ws.send(JSON.stringify({ type: 'error', error: 'Not your turn' }));
return;
}
try {
const result = game.makeMove(playerId, row, col);
ws.send(JSON.stringify({ type: 'move_result', success: result.success }));
if (result.success) {
// Broadcast updated game state to all players in the game
this.broadcastGameState(gameId, game);
console.log(
`Move made in game ${gameId} by ${playerId}: (${row}, ${col})`,
);
} else {
ws.send(
JSON.stringify({
type: 'error',
error: result.error || 'Invalid move',
}),
);
}
} catch (e: any) {
ws.send(JSON.stringify({ type: 'error', error: e.message }));
}
}
private handleDisconnect(ws: any): void {
const gameId = ws.data.gameId;
const playerId = ws.data.playerId;
if (gameId && playerId) {
// Remove disconnected player's websocket from connections
const connectionsInGame = this.connections.get(gameId);
if (connectionsInGame) {
this.connections.set(gameId, connectionsInGame.filter((conn: any) => conn !== ws));
if (this.connections.get(gameId)?.length === 0) {
this.connections.delete(gameId); // Clean up if no players left
}
}
// Notify other players
if (this.connections.has(gameId)) {
const disconnectMessage = JSON.stringify({
type: 'player_disconnected',
playerId: playerId,
gameId: gameId,
});
this.connections.get(gameId)?.forEach((playerWs: any) => {
playerWs.send(disconnectMessage);
});
}
console.log(`${playerId} disconnected from game ${gameId}`);
}
}
// Method to send updated game state to all participants in a game
// This would typically be called by GameManager when game state changes
public broadcastGameState(gameId: string, state: any): void {
const message = JSON.stringify({ type: 'game_state', state });
const connectionsInGame = this.connections.get(gameId);
if (connectionsInGame) {
connectionsInGame.forEach((ws: any) => {
ws.send(message);
});
}
console.log(`Broadcasting game state for ${gameId}:`, state);
}
}