Preference-based refactoring
This commit is contained in:
parent
cd21e4e8bd
commit
1f19022d45
12 changed files with 74 additions and 1062 deletions
|
|
@ -1,48 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { GameInstance } from './GameInstance';
|
||||
|
||||
export class GameManager {
|
||||
private games: Map<string, GameInstance>;
|
||||
|
||||
constructor() {
|
||||
this.games = new Map();
|
||||
}
|
||||
|
||||
// Overload createGame to optionally accept a gameId
|
||||
createGame(gameId?: string): GameInstance {
|
||||
const game = new GameInstance(gameId); // Pass gameId to GameInstance constructor
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,397 +0,0 @@
|
|||
import { describe, it, expect, beforeEach, mock } from 'bun:test';
|
||||
import { WebSocketHandler } from './WebSocketHandler';
|
||||
import { GameManager } from './GameManager';
|
||||
import { GameInstance } from './GameInstance';
|
||||
|
||||
// Mock ElysiaWS type for testing purposes - fully compatible with standard WebSocket
|
||||
type MockElysiaWS = {
|
||||
send: ReturnType<typeof mock>;
|
||||
close: ReturnType<typeof mock>;
|
||||
on: ReturnType<typeof mock>;
|
||||
_messageCallback: ((message: string) => void) | null;
|
||||
_closeCallback: (() => void) | null;
|
||||
_errorCallback: ((error: Error) => void) | null;
|
||||
data: {
|
||||
gameId?: string;
|
||||
playerId?: string;
|
||||
query: Record<string, string>;
|
||||
};
|
||||
// Standard WebSocket properties
|
||||
binaryType: 'blob' | 'arraybuffer';
|
||||
bufferedAmount: number;
|
||||
extensions: string;
|
||||
onclose: ((this: WebSocket, ev: CloseEvent) => any) | null;
|
||||
onerror: ((this: WebSocket, ev: Event) => any) | null;
|
||||
onmessage: ((this: WebSocket, ev: MessageEvent) => any) | null;
|
||||
onopen: ((this: WebSocket, ev: Event) => any) | null;
|
||||
protocol: string;
|
||||
readyState: number;
|
||||
url: string;
|
||||
CLOSED: number;
|
||||
CONNECTING: number;
|
||||
OPEN: number;
|
||||
CLOSING: number;
|
||||
dispatchEvent: ReturnType<typeof mock>;
|
||||
addEventListener: ReturnType<typeof mock>;
|
||||
removeEventListener: ReturnType<typeof mock>;
|
||||
ping: ReturnType<typeof mock>; // Bun.js specific
|
||||
pong: ReturnType<typeof mock>; // Bun.js specific
|
||||
subscribe: ReturnType<typeof mock>; // Bun.js specific
|
||||
unsubscribe: ReturnType<typeof mock>; // Bun.js specific
|
||||
};
|
||||
|
||||
describe('WebSocketHandler', () => {
|
||||
let gameManager: GameManager;
|
||||
let webSocketHandler: WebSocketHandler;
|
||||
let mockWs: MockElysiaWS;
|
||||
|
||||
beforeEach(() => {
|
||||
mockWs = {
|
||||
// Mock standard WebSocket methods
|
||||
send: mock(() => {}),
|
||||
close: mock(() => {}),
|
||||
|
||||
// Mock custom 'on' method for attaching callbacks
|
||||
on: mock((event: string, callback: (...args: any[]) => void) => {
|
||||
if (event === 'message') (mockWs as any)._messageCallback = callback;
|
||||
if (event === 'close') (mockWs as any)._closeCallback = callback;
|
||||
if (event === 'error') (mockWs as any)._errorCallback = callback;
|
||||
}),
|
||||
|
||||
_messageCallback: null,
|
||||
_closeCallback: null,
|
||||
_errorCallback: null,
|
||||
|
||||
data: { query: {} },
|
||||
|
||||
// Initialize all standard WebSocket properties
|
||||
binaryType: 'blob',
|
||||
bufferedAmount: 0,
|
||||
extensions: '',
|
||||
onclose: null,
|
||||
onerror: null,
|
||||
onmessage: null,
|
||||
onopen: null,
|
||||
protocol: '',
|
||||
readyState: 1,
|
||||
url: '',
|
||||
CLOSED: 3,
|
||||
CONNECTING: 0,
|
||||
OPEN: 1,
|
||||
CLOSING: 2,
|
||||
dispatchEvent: mock(() => {}),
|
||||
addEventListener: mock(() => {}),
|
||||
removeEventListener: mock(() => {}),
|
||||
ping: mock(() => {}),
|
||||
pong: mock(() => {}),
|
||||
subscribe: mock(() => {}),
|
||||
unsubscribe: mock(() => {}),
|
||||
};
|
||||
gameManager = new GameManager();
|
||||
webSocketHandler = new WebSocketHandler(gameManager);
|
||||
});
|
||||
|
||||
const triggerMessage = (ws: MockElysiaWS, message: string) => {
|
||||
if (ws._messageCallback) {
|
||||
ws._messageCallback(message);
|
||||
}
|
||||
};
|
||||
|
||||
const triggerClose = (ws: MockElysiaWS) => {
|
||||
if (ws._closeCallback) {
|
||||
ws._closeCallback();
|
||||
}
|
||||
};
|
||||
|
||||
const triggerError = (ws: MockElysiaWS, error: Error) => {
|
||||
if (ws._errorCallback) {
|
||||
ws._errorCallback(error);
|
||||
}
|
||||
};
|
||||
|
||||
const createNewMockWs = (): MockElysiaWS => ({
|
||||
send: mock(() => {}),
|
||||
close: mock(() => {}),
|
||||
on: mock((event: string, callback: (...args: any[]) => void) => {
|
||||
if (event === 'message')
|
||||
(createNewMockWs() as any)._messageCallback = callback;
|
||||
if (event === 'close')
|
||||
(createNewMockWs() as any)._closeCallback = callback;
|
||||
if (event === 'error')
|
||||
(createNewMockWs() as any)._errorCallback = callback;
|
||||
}),
|
||||
_messageCallback: null,
|
||||
_closeCallback: null,
|
||||
_errorCallback: null,
|
||||
data: { query: {} },
|
||||
binaryType: 'blob',
|
||||
bufferedAmount: 0,
|
||||
extensions: '',
|
||||
onclose: null,
|
||||
onerror: null,
|
||||
onmessage: null,
|
||||
onopen: null,
|
||||
protocol: '',
|
||||
readyState: 1,
|
||||
url: '',
|
||||
CLOSED: 3,
|
||||
CONNECTING: 0,
|
||||
OPEN: 1,
|
||||
CLOSING: 2,
|
||||
dispatchEvent: mock(() => {}),
|
||||
addEventListener: mock(() => {}),
|
||||
removeEventListener: mock(() => {}),
|
||||
ping: mock(() => {}),
|
||||
pong: mock(() => {}),
|
||||
subscribe: mock(() => {}),
|
||||
unsubscribe: mock(() => {}),
|
||||
});
|
||||
|
||||
it('should register a new connection', () => {
|
||||
mockWs.data.gameId = 'test-game';
|
||||
mockWs.data.playerId = 'player-alpha';
|
||||
mockWs.data.query.gameId = 'test-game';
|
||||
mockWs.data.query.playerId = 'player-alpha';
|
||||
webSocketHandler.handleConnection(mockWs);
|
||||
expect((webSocketHandler as any).connections.get('test-game')).toContain(
|
||||
mockWs,
|
||||
);
|
||||
});
|
||||
|
||||
it('should process a join_game message for an already connected client', () => {
|
||||
const gameId = gameManager.createGame().id;
|
||||
mockWs.data.query.gameId = gameId;
|
||||
mockWs.data.query.playerId = 'player1';
|
||||
mockWs.data.gameId = gameId;
|
||||
mockWs.data.playerId = 'player1';
|
||||
webSocketHandler.handleConnection(mockWs);
|
||||
const joinGameMessage = JSON.stringify({
|
||||
type: 'join_game',
|
||||
gameId: gameId,
|
||||
playerId: 'player1',
|
||||
});
|
||||
triggerMessage(mockWs, joinGameMessage);
|
||||
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="game-board"'),
|
||||
);
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="player-info"'),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle a make_move message and broadcast HTML updates', () => {
|
||||
const game = gameManager.createGame();
|
||||
game.addPlayer('player1');
|
||||
game.addPlayer('player2');
|
||||
game.currentPlayer = 'black';
|
||||
|
||||
mockWs.data.gameId = game.id;
|
||||
mockWs.data.playerId = 'player1';
|
||||
mockWs.data.query.gameId = game.id;
|
||||
mockWs.data.query.playerId = 'player1';
|
||||
webSocketHandler.handleConnection(mockWs);
|
||||
|
||||
const makeMoveMessage = JSON.stringify({
|
||||
type: 'make_move',
|
||||
gameId: game.id,
|
||||
playerId: 'player1',
|
||||
row: 7,
|
||||
col: 7,
|
||||
});
|
||||
triggerMessage(mockWs, makeMoveMessage);
|
||||
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="game-board"'),
|
||||
);
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="player-info"'),
|
||||
);
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="messages"'),
|
||||
);
|
||||
|
||||
expect(game.board[7][7]).toBe('black');
|
||||
expect(game.currentPlayer).toBe('white');
|
||||
});
|
||||
|
||||
it('should send an error for an invalid move', () => {
|
||||
const game = gameManager.createGame();
|
||||
game.addPlayer('player1');
|
||||
game.addPlayer('player2');
|
||||
game.currentPlayer = 'black';
|
||||
|
||||
mockWs.data.gameId = game.id;
|
||||
mockWs.data.playerId = 'player1';
|
||||
mockWs.data.query.gameId = game.id;
|
||||
mockWs.data.query.playerId = 'player1';
|
||||
webSocketHandler.handleConnection(mockWs);
|
||||
|
||||
const makeMoveMessage1 = JSON.stringify({
|
||||
type: 'make_move',
|
||||
gameId: game.id,
|
||||
playerId: 'player1',
|
||||
row: 7,
|
||||
col: 7,
|
||||
});
|
||||
triggerMessage(mockWs, makeMoveMessage1);
|
||||
mockWs.send.mockClear();
|
||||
|
||||
triggerMessage(mockWs, makeMoveMessage1);
|
||||
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({ type: 'error', error: 'Cell already occupied' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle ping/pong messages', () => {
|
||||
webSocketHandler.handleConnection(mockWs);
|
||||
const pingMessage = JSON.stringify({ type: 'ping' });
|
||||
triggerMessage(mockWs, pingMessage);
|
||||
|
||||
expect(mockWs.send).toHaveBeenCalledWith(JSON.stringify({ type: 'pong' }));
|
||||
});
|
||||
|
||||
it('should handle player disconnection and notify others', () => {
|
||||
const game = gameManager.createGame();
|
||||
const player1Ws = createNewMockWs();
|
||||
const player2Ws = createNewMockWs();
|
||||
|
||||
player1Ws.data.gameId = game.id;
|
||||
player1Ws.data.playerId = 'player1';
|
||||
player1Ws.data.query.gameId = game.id;
|
||||
player1Ws.data.query.playerId = 'player1';
|
||||
|
||||
player2Ws.data.gameId = game.id;
|
||||
player2Ws.data.playerId = 'player2';
|
||||
player2Ws.data.query.gameId = game.id;
|
||||
player2Ws.data.query.playerId = 'player2';
|
||||
|
||||
webSocketHandler.handleConnection(player1Ws);
|
||||
webSocketHandler.handleConnection(player2Ws);
|
||||
|
||||
player1Ws.send.mockClear();
|
||||
player2Ws.send.mockClear();
|
||||
|
||||
webSocketHandler.handleDisconnect(player2Ws);
|
||||
|
||||
expect(player1Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="player-info"'),
|
||||
);
|
||||
expect(player1Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="messages"'),
|
||||
);
|
||||
expect(
|
||||
player1Ws.send.mock.calls
|
||||
.flat()
|
||||
.some((call) => (call as string).includes('player2 disconnected')),
|
||||
).toBeTrue();
|
||||
expect((webSocketHandler as any).connections.get(game.id)).not.toContain(
|
||||
player2Ws,
|
||||
);
|
||||
});
|
||||
|
||||
it('should send error for unknown message type', () => {
|
||||
webSocketHandler.handleConnection(mockWs);
|
||||
const unknownMessage = JSON.stringify({ type: 'unknown_type' });
|
||||
triggerMessage(mockWs, unknownMessage);
|
||||
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({ type: 'error', error: 'Unknown message type' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should send error for invalid JSON message', () => {
|
||||
webSocketHandler.handleConnection(mockWs);
|
||||
const invalidJsonMessage = 'not a json';
|
||||
triggerMessage(mockWs, invalidJsonMessage);
|
||||
|
||||
expect(mockWs.send).toHaveBeenCalledWith(
|
||||
JSON.stringify({ type: 'error', error: 'Invalid message format' }),
|
||||
);
|
||||
});
|
||||
|
||||
it('should broadcast game state to a specific client when targetWs is provided', () => {
|
||||
const game = gameManager.createGame();
|
||||
game.addPlayer('player1');
|
||||
game.addPlayer('player2');
|
||||
game.currentPlayer = 'black';
|
||||
|
||||
const player1Ws = createNewMockWs();
|
||||
player1Ws.data.gameId = game.id;
|
||||
player1Ws.data.playerId = 'player1';
|
||||
player1Ws.data.query.gameId = game.id;
|
||||
player1Ws.data.query.playerId = 'player1';
|
||||
webSocketHandler.handleConnection(player1Ws);
|
||||
|
||||
const player2Ws = createNewMockWs();
|
||||
player2Ws.data.gameId = game.id;
|
||||
player2Ws.data.playerId = 'player2';
|
||||
player2Ws.data.query.gameId = game.id;
|
||||
player2Ws.data.query.playerId = 'player2';
|
||||
webSocketHandler.handleConnection(player2Ws);
|
||||
|
||||
player1Ws.send.mockClear();
|
||||
player2Ws.send.mockClear();
|
||||
|
||||
webSocketHandler.broadcastGameUpdate(game.id, game, null, null, player1Ws);
|
||||
|
||||
expect(player1Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="game-board"'),
|
||||
);
|
||||
expect(player1Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="player-info"'),
|
||||
);
|
||||
expect(player1Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="messages"'),
|
||||
);
|
||||
|
||||
expect(player2Ws.send).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should broadcast game state to all clients if targetWs is not provided', () => {
|
||||
const game = gameManager.createGame();
|
||||
game.addPlayer('player1');
|
||||
game.addPlayer('player2');
|
||||
game.currentPlayer = 'black';
|
||||
|
||||
const player1Ws = createNewMockWs();
|
||||
player1Ws.data.gameId = game.id;
|
||||
player1Ws.data.playerId = 'player1';
|
||||
player1Ws.data.query.gameId = game.id;
|
||||
player1Ws.data.query.playerId = 'player1';
|
||||
webSocketHandler.handleConnection(player1Ws);
|
||||
|
||||
const player2Ws = createNewMockWs();
|
||||
player2Ws.data.gameId = game.id;
|
||||
player2Ws.data.playerId = 'player2';
|
||||
player2Ws.data.query.gameId = game.id;
|
||||
player2Ws.data.query.playerId = 'player2';
|
||||
webSocketHandler.handleConnection(player2Ws);
|
||||
|
||||
player1Ws.send.mockClear();
|
||||
player2Ws.send.mockClear();
|
||||
|
||||
webSocketHandler.broadcastGameUpdate(game.id, game);
|
||||
|
||||
expect(player1Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="game-board"'),
|
||||
);
|
||||
expect(player1Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="player-info"'),
|
||||
);
|
||||
expect(player1Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="messages"'),
|
||||
);
|
||||
|
||||
expect(player2Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="game-board"'),
|
||||
);
|
||||
expect(player2Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="player-info"'),
|
||||
);
|
||||
expect(player2Ws.send).toHaveBeenCalledWith(
|
||||
expect.stringContaining('<div id="messages"'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { ElysiaWS } from 'elysia/dist/ws';
|
||||
import { GameInstance } from './GameInstance';
|
||||
import {
|
||||
renderGameBoardHtml,
|
||||
|
|
@ -11,8 +12,9 @@ interface MakeMoveMessage {
|
|||
col: number;
|
||||
}
|
||||
|
||||
type WS = ElysiaWS<{query: {playerId: string, gameId: string}}>;
|
||||
export class WebSocketHandler {
|
||||
private connections: Map<string, Array<any>>; // Use 'any' for the specific Elysia WS object for now
|
||||
private connections: Map<string, Array<WS>>;
|
||||
private games: Map<string, GameInstance>;
|
||||
|
||||
constructor() {
|
||||
|
|
@ -20,31 +22,28 @@ export class WebSocketHandler {
|
|||
this.games = new Map();
|
||||
}
|
||||
|
||||
public handleConnection(ws: any, gameId: string, playerId: string): void {
|
||||
public handleConnection(ws: WS): void {
|
||||
const {gameId, playerId} = ws.data.query;
|
||||
|
||||
if (!this.connections.has(gameId)) {
|
||||
this.connections.set(gameId, []);
|
||||
}
|
||||
ws.data.playerId = playerId;
|
||||
ws.data.gameId = gameId;
|
||||
this.connections.get(gameId)?.push(ws);
|
||||
|
||||
const game = this.getGame(gameId);
|
||||
if (game) {
|
||||
this.broadcastGameState(game.id);
|
||||
} else {
|
||||
ws.send('Error: game not found');
|
||||
ws.close();
|
||||
}
|
||||
|
||||
console.log(
|
||||
`WebSocket connected, registered for Game ${gameId} as Player ${playerId}`,
|
||||
);
|
||||
}
|
||||
|
||||
public handleError(ws: any, error: Error): void {
|
||||
console.error('WebSocket error:', error);
|
||||
if (ws) {
|
||||
this.sendMessage(
|
||||
ws.data.gameId,
|
||||
'Error: server-side WebSocket error',
|
||||
ws,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public handleMessage(ws: any, message: any): void {
|
||||
public handleMessage(ws: WS, message: any): void {
|
||||
const type: string = message.type;
|
||||
// Someday we might have other message types
|
||||
if (type === 'make_move') {
|
||||
|
|
@ -52,24 +51,22 @@ export class WebSocketHandler {
|
|||
}
|
||||
}
|
||||
|
||||
private handleMakeMove(ws: any, message: MakeMoveMessage): void {
|
||||
private handleMakeMove(ws: WS, message: MakeMoveMessage): void {
|
||||
const { row, col } = message;
|
||||
const gameId = ws.data.gameId;
|
||||
const playerId = ws.data.playerId;
|
||||
const {gameId, playerId} = ws.data.query;
|
||||
console.log(`Handling make_move message in game ${gameId} from player ${playerId}: ${{message}}`);
|
||||
|
||||
if (!gameId || !playerId || row === undefined || col === undefined) {
|
||||
this.sendMessage(
|
||||
gameId,
|
||||
'Error: missing gameId, playerId, row, or col',
|
||||
ws,
|
||||
'Error: missing gameId, playerId, row, or col',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const game = this.games.get(gameId);
|
||||
if (!game) {
|
||||
this.sendMessage(gameId, 'Error: game not found', ws);
|
||||
this.sendMessage(ws, 'Error: game not found');
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -78,15 +75,14 @@ export class WebSocketHandler {
|
|||
)?.[0] as ('black' | 'white') | undefined;
|
||||
if (!playerColor) {
|
||||
this.sendMessage(
|
||||
gameId,
|
||||
'Error: you are not a player in this game',
|
||||
ws,
|
||||
'Error: you are not a player in this game',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (game.currentPlayer !== playerColor) {
|
||||
this.sendMessage(gameId, 'Error: It\'s not your turn', ws);
|
||||
this.sendMessage(ws, "Error: It's not your turn");
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -98,35 +94,34 @@ export class WebSocketHandler {
|
|||
`Move made in game ${game.id} by ${playerId}: (${row}, ${col})`,
|
||||
);
|
||||
} else {
|
||||
this.sendMessage(gameId, result.error || 'Error: invalid move', ws);
|
||||
this.sendMessage(ws, result.error || 'Error: invalid move');
|
||||
}
|
||||
} catch (e: any) {
|
||||
this.sendMessage(gameId, 'Error: ' + e.message, ws);
|
||||
this.sendMessage(ws, 'Error: ' + e.message);
|
||||
}
|
||||
}
|
||||
|
||||
public handleDisconnect(ws: any): void {
|
||||
const gameId = ws.data.gameId;
|
||||
const playerId = ws.data.playerId;
|
||||
public handleDisconnect(ws: WS): void {
|
||||
const {gameId, playerId} = ws.data.query;
|
||||
|
||||
if (gameId && playerId) {
|
||||
const connectionsInGame = this.connections.get(gameId);
|
||||
if (connectionsInGame) {
|
||||
this.connections.set(
|
||||
gameId,
|
||||
connectionsInGame.filter((conn) => conn !== ws),
|
||||
);
|
||||
if (this.connections.get(gameId)?.length === 0) {
|
||||
this.connections.delete(gameId);
|
||||
}
|
||||
}
|
||||
|
||||
if (this.connections.has(gameId)) {
|
||||
// Notify remaining players about disconnect
|
||||
this.sendMessage(gameId, 'message', `${playerId} disconnected.`);
|
||||
}
|
||||
console.log(`${playerId} disconnected from game ${gameId}`);
|
||||
const connectionsInGame = this.connections.get(gameId);
|
||||
if (!connectionsInGame) {
|
||||
console.error(`Disconnecting WebSocket for player ${playerId} from game ${gameId}, but that game has no connections!`);
|
||||
return;
|
||||
}
|
||||
this.connections.set(
|
||||
gameId,
|
||||
connectionsInGame.filter((conn) => conn !== ws),
|
||||
);
|
||||
if (this.connections.get(gameId)?.length === 0) {
|
||||
this.connections.delete(gameId);
|
||||
}
|
||||
|
||||
if (this.connections.has(gameId)) {
|
||||
// Notify remaining players about disconnect
|
||||
this.sendMessageToGame(gameId, `${playerId} disconnected.`);
|
||||
}
|
||||
console.log(`${playerId} disconnected from game ${gameId}`);
|
||||
}
|
||||
|
||||
public broadcastGameState(gameId: string): void {
|
||||
|
|
@ -139,36 +134,33 @@ export class WebSocketHandler {
|
|||
const connectionsToUpdate = this.connections.get(gameId);
|
||||
if (connectionsToUpdate) {
|
||||
connectionsToUpdate.forEach((ws) => {
|
||||
if (!ws.data.playerId) {
|
||||
console.warn('WebSocket without playerId in game for update', gameId);
|
||||
return;
|
||||
}
|
||||
const {gameId, playerId} = ws.data.query;
|
||||
|
||||
const updatedBoardHtml = renderGameBoardHtml(game, ws.data.playerId);
|
||||
const updatedBoardHtml = renderGameBoardHtml(game, playerId);
|
||||
ws.send(updatedBoardHtml);
|
||||
const updatedPlayerInfoHtml = renderPlayerInfoHtml(
|
||||
game.id,
|
||||
ws.data.playerId,
|
||||
playerId,
|
||||
);
|
||||
ws.send(updatedPlayerInfoHtml);
|
||||
|
||||
if (game.status === 'finished') {
|
||||
if (game.winner === 'draw') {
|
||||
this.sendMessage(gameId, 'Game ended in draw.');
|
||||
this.sendMessageToGame(gameId, 'Game ended in draw.');
|
||||
} else if (game.winner) {
|
||||
this.sendMessage(gameId, `${game.winner.toUpperCase()} wins!`);
|
||||
this.sendMessageToGame(gameId, `${game.winner.toUpperCase()} wins!`);
|
||||
}
|
||||
} else if (game.status === 'playing') {
|
||||
const clientPlayerColor = Object.entries(game.players).find(
|
||||
([_, id]) => id === ws.data.playerId,
|
||||
([_, id]) => id === playerId,
|
||||
)?.[0] as ('black' | 'white') | undefined;
|
||||
if (game.currentPlayer && clientPlayerColor === game.currentPlayer) {
|
||||
this.sendMessage(gameId, "It's your turn!", ws);
|
||||
this.sendMessage(ws, "It's your turn!");
|
||||
} else if (game.currentPlayer) {
|
||||
this.sendMessage(gameId, `Waiting for ${game.currentPlayer}'s move.`, ws);
|
||||
this.sendMessage(ws, `Waiting for ${game.currentPlayer}'s move.`);
|
||||
}
|
||||
} else if (game.status === 'waiting') {
|
||||
this.sendMessage(gameId, 'Waiting for another player...', ws);
|
||||
this.sendMessage(ws, 'Waiting for another player...');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
|
|
@ -176,12 +168,11 @@ export class WebSocketHandler {
|
|||
}
|
||||
}
|
||||
|
||||
public sendMessage(
|
||||
public sendMessageToGame(
|
||||
gameId: string,
|
||||
message: string,
|
||||
targetWs?: any,
|
||||
): void {
|
||||
const connections = targetWs ? [targetWs] : this.connections.get(gameId);
|
||||
const connections = this.connections.get(gameId);
|
||||
if (connections) {
|
||||
connections.forEach((ws) => {
|
||||
ws.send('<div id="messages">' + message + '</div>')
|
||||
|
|
@ -189,6 +180,13 @@ export class WebSocketHandler {
|
|||
}
|
||||
}
|
||||
|
||||
public sendMessage(
|
||||
targetWs: WS,
|
||||
message: string,
|
||||
): void {
|
||||
targetWs.send('<div id="messages">' + message + '</div>')
|
||||
}
|
||||
|
||||
public getGame(gameId: string): GameInstance | undefined {
|
||||
return this.games.get(gameId)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue