// src/game-client/WebSocketClient.ts class WebSocketClient { ws = null; url; messageQueue = []; isConnected = false; reconnectCount = 0; options; manualClose = false; onMessageHandler = () => {}; onOpenHandler = () => {}; onCloseHandler = () => {}; onErrorHandler = () => {}; constructor(url, options) { this.url = url; this.options = { reconnectAttempts: 5, reconnectInterval: 3000, ...options, }; } connect() { this.manualClose = false; this.ws = new WebSocket(this.url); this.ws.onopen = this.handleOpen.bind(this); this.ws.onmessage = this.handleMessage.bind(this); this.ws.onclose = this.handleClose.bind(this); this.ws.onerror = this.handleError.bind(this); } send(message) { if (this.isConnected && this.ws) { this.ws.send(message); } else { this.messageQueue.push(message); } } close() { this.manualClose = true; if (this.ws) { this.ws.close(); } } onMessage(handler) { this.onMessageHandler = handler; } onOpen(handler) { this.onOpenHandler = handler; } onClose(handler) { this.onCloseHandler = handler; } onError(handler) { this.onErrorHandler = handler; } handleOpen() { this.isConnected = true; this.reconnectCount = 0; this.onOpenHandler(); this.flushMessageQueue(); } handleMessage(event) { this.onMessageHandler(event.data); } handleClose(event) { this.isConnected = false; this.onCloseHandler(event.code, event.reason); if ( !this.manualClose && this.reconnectCount < this.options.reconnectAttempts ) { this.reconnectCount++; setTimeout(() => this.connect(), this.options.reconnectInterval); } } handleError(event) { this.onErrorHandler(event); } flushMessageQueue() { while (this.messageQueue.length > 0 && this.isConnected && this.ws) { const message = this.messageQueue.shift(); if (message) { this.ws.send(message); } } } } // src/game-client/GameStateManager.ts class GameStateManager { gameState; stateHistory; constructor() { this.gameState = this.getDefaultGameState(); this.stateHistory = []; } getDefaultGameState() { const emptyBoard = Array(15) .fill(null) .map(() => Array(15).fill(null)); return { id: '', board: emptyBoard, currentPlayer: 'black', status: 'waiting', winner: null, players: {}, }; } getGameState() { return this.gameState; } updateGameState(newState) { this.stateHistory.push(JSON.parse(JSON.stringify(this.gameState))); this.gameState = newState; } rollbackGameState() { if (this.stateHistory.length > 0) { this.gameState = this.stateHistory.pop(); } else { console.warn('No previous state to rollback to.'); } } } // src/game-client/GameBoardUI.ts class GameBoardUI { boardElement; cells = []; onCellClickCallback = null; isInteractionEnabled = true; constructor(boardElement) { this.boardElement = boardElement; this.initializeBoard(); } initializeBoard() { this.boardElement.innerHTML = ''; this.boardElement.style.display = 'grid'; this.boardElement.style.gridTemplateColumns = 'repeat(15, 1fr)'; this.boardElement.style.width = '450px'; this.boardElement.style.height = '450px'; this.boardElement.style.border = '1px solid black'; for (let row = 0; row < 15; row++) { this.cells[row] = []; for (let col = 0; col < 15; col++) { const cell = document.createElement('div'); cell.classList.add('board-cell'); cell.style.width = '30px'; cell.style.height = '30px'; cell.style.border = '1px solid #ccc'; cell.style.boxSizing = 'border-box'; cell.style.display = 'flex'; cell.style.justifyContent = 'center'; cell.style.alignItems = 'center'; cell.dataset.row = row.toString(); cell.dataset.col = col.toString(); cell.addEventListener('click', () => this.handleCellClick(row, col)); this.boardElement.appendChild(cell); this.cells[row][col] = cell; } } } updateBoard(gameState) { const board = gameState.board; const lastMove = { row: -1, col: -1 }; for (let row = 0; row < 15; row++) { for (let col = 0; col < 15; col++) { const cell = this.cells[row][col]; cell.innerHTML = ''; const stone = board[row][col]; if (stone) { const stoneElement = document.createElement('div'); stoneElement.style.width = '24px'; stoneElement.style.height = '24px'; stoneElement.style.borderRadius = '50%'; stoneElement.style.backgroundColor = stone === 'black' ? 'black' : 'white'; stoneElement.style.border = '1px solid #333'; cell.appendChild(stoneElement); } cell.classList.remove('last-move'); } } this.isInteractionEnabled = gameState.status === 'playing' && gameState.currentPlayer === 'black'; this.boardElement.style.pointerEvents = this.isInteractionEnabled ? 'auto' : 'none'; this.boardElement.style.opacity = this.isInteractionEnabled ? '1' : '0.7'; console.log( `Current Player: ${gameState.currentPlayer}, Status: ${gameState.status}`, ); } setOnCellClick(callback) { this.onCellClickCallback = callback; } handleCellClick(row, col) { if (this.isInteractionEnabled && this.onCellClickCallback) { this.onCellClickCallback(row, col); } } } // src/client-entry.ts console.log('Gomoku client entry point loaded.'); var WS_URL = 'ws://localhost:3000/ws'; var gameStateManager = new GameStateManager(); var wsClient = new WebSocketClient(WS_URL); var gameBoardElement = document.getElementById('game-board'); console.log('gameBoardElement: ', gameBoardElement); var messagesElement = document.getElementById('messages'); var playerInfoElement = document.getElementById('player-info'); if (!gameBoardElement || !messagesElement || !playerInfoElement) { console.error( 'Missing essential DOM elements (game-board, messages, or player-info)', ); throw new Error( 'Missing essential DOM elements (game-board, messages, or player-info)', ); } var gameBoardUI = new GameBoardUI(gameBoardElement); console.log('GameBoardUI initialized.', gameBoardUI); wsClient.onMessage((message) => { try { const msg = JSON.parse(message); console.log('Parsed message:', msg); switch (msg.type) { case 'game_state': gameStateManager.updateGameState(msg.state); gameBoardUI.updateBoard(gameStateManager.getGameState()); console.log('Game state updated: ', gameStateManager.getGameState()); break; case 'move_result': if (msg.success) { console.log('Move successful!'); } else { console.error(`Move failed: ${msg.error}`); gameStateManager.rollbackGameState(); gameBoardUI.updateBoard(gameStateManager.getGameState()); } break; case 'player_joined': console.log(`${msg.playerId} joined the game.`); break; case 'player_disconnected': console.log(`${msg.playerId} disconnected.`); break; case 'ping': break; default: console.log(`Unknown message type: ${msg.type}`); } } catch (e) { console.error( 'Error parsing WebSocket message:', e, 'Message was:', message, ); } }); gameBoardUI.setOnCellClick((row, col) => { const moveMessage = { type: 'make_move', row, col, }; console.log('Sending move:', moveMessage); wsClient.send(JSON.stringify(moveMessage)); const currentGameState = gameStateManager.getGameState(); const nextPlayer = currentGameState.currentPlayer === 'black' ? 'white' : 'black'; const newBoard = currentGameState.board.map((rowArr) => [...rowArr]); newBoard[row][col] = currentGameState.currentPlayer; const optimisticState = { ...currentGameState, board: newBoard, currentPlayer: nextPlayer, }; gameStateManager.updateGameState(optimisticState); gameBoardUI.updateBoard(gameStateManager.getGameState()); }); wsClient.onOpen(() => { console.log('Connected to game server.'); const playerId = `player-${Math.random().toString(36).substring(2, 9)}`; const joinMessage = { type: 'join_game', gameId: 'some-game-id', playerId, }; wsClient.send(JSON.stringify(joinMessage)); if (playerInfoElement) { playerInfoElement.textContent = `You are: ${playerId} (Waiting for game state...)`; } }); wsClient.onClose(() => { console.log('Disconnected from game server. Attempting to reconnect...'); }); wsClient.onError((error) => { console.error( `WebSocket error: ${error instanceof ErrorEvent ? error.message : String(error)}`, ); }); wsClient.connect(); gameBoardUI.updateBoard(gameStateManager.getGameState()); if (playerInfoElement) { playerInfoElement.textContent = `You are: (Connecting...)`; }