Get the client to a point where it at least renders the board

This commit is contained in:
sepia 2025-07-15 18:07:48 -05:00
parent 3be0c40b64
commit e8e982c3d6
15 changed files with 1364 additions and 366 deletions

View file

@ -179,203 +179,229 @@ describe('WebSocketHandler', () => {
);
});
});
it('should notify other players and remove a disconnected player', () => {
const gameManager = new GameManager();
const webSocketHandler = new WebSocketHandler(gameManager);
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 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,
};
// 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 triggerMessageForWs = (ws: any, message: string) => {
if (ws._messageCallback) {
ws._messageCallback(message);
}
};
const triggerCloseForWs = (ws: any) => {
if (ws._closeCallback) {
ws._closeCallback();
}
};
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 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({
// Player 2 joins same game
webSocketHandler.handleConnection(mockWs2, mockWs2.data.request);
triggerMessageForWs(
mockWs2,
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,
gameId: mockWsData1.gameId,
playerId: 'player2',
});
triggerMessageForWs(mockWs2, joinGameMessage2);
}),
);
mockWs2.data.gameId = mockWsData1.gameId;
mockWs2.data.playerId = 'player2';
// 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);
// Player 2 disconnects
mockWs1.send.mockClear(); // Clear P1's send history before P2 disconnects
triggerCloseForWs(mockWs2);
expect(receivedMessage.type).toBe('game_state');
expect(receivedMessage.state.players.black).toBe('player1');
expect(receivedMessage.state.players.white).toBe('player2');
// 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;
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');
// 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

@ -23,22 +23,19 @@ export class WebSocketHandler {
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 {
public handleError(ws: any, error: Error): void {
console.error('WebSocket error:', error);
// Optionally send an error message to the client
if (ws) {
ws.send(
JSON.stringify({ type: 'error', error: 'Server-side WebSocket error' }),
);
}
}
public handleMessage(ws: any, message: string): void {
try {
const parsedMessage: WebSocketMessage = JSON.parse(message);
console.log('Received message:', parsedMessage);
@ -111,7 +108,8 @@ export class WebSocketHandler {
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
if (playerWs !== ws) {
// Don't send back to the player who just joined
playerWs.send(gameStateMessage);
}
});
@ -187,7 +185,7 @@ export class WebSocketHandler {
}
}
private handleDisconnect(ws: any): void {
public handleDisconnect(ws: any): void {
const gameId = ws.data.gameId;
const playerId = ws.data.playerId;
@ -195,7 +193,10 @@ export class WebSocketHandler {
// 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));
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
}