Browse Source

refactor

drawing-pad
Stephanie Gredell 1 month ago
parent
commit
fe65bec9db
  1. 52
      backend/src/games/base/game-handler.interface.ts
  2. 28
      backend/src/games/game-manager.ts
  3. 417
      backend/src/games/tic-tac-toe/tic-tac-toe.handler.ts
  4. 7
      backend/src/games/types.ts
  5. 252
      backend/src/services/websocket.service.ts
  6. 2
      frontend/src/pages/TicTacToeApp.tsx

52
backend/src/games/base/game-handler.interface.ts

@ -0,0 +1,52 @@
import type { WebSocket as WS } from 'ws';
import type { GameType } from '../types.js';
export interface Player {
id: string;
ws: WS;
symbol: string | null;
[key: string]: any; // Allow game-specific player properties
}
export interface GameState {
board?: any;
currentPlayer?: string;
winner?: string | null;
isDraw?: boolean;
players: Array<{ id: string; symbol: string | null; [key: string]: any }>;
queue: string[];
[key: string]: any; // Allow game-specific state properties
}
export interface AddPlayerResult {
success: boolean;
error?: string;
gameState?: GameState;
[key: string]: any; // Allow game-specific return properties
}
export interface GameHandler {
// Handle incoming messages
handleMessage(
roomId: string,
playerId: string,
message: any,
ws: WS
): void;
// Add player to game
addPlayer(
roomId: string,
playerId: string,
ws: WS
): AddPlayerResult;
// Remove player
removePlayer(roomId: string, playerId: string): void;
// Get current game state for a player
getGameState(roomId: string, playerId: string): GameState | null;
// Get game type
getGameType(): string;
}

28
backend/src/games/game-manager.ts

@ -0,0 +1,28 @@
import type { GameHandler } from './base/game-handler.interface.js';
class GameManager {
private handlers = new Map<string, GameHandler>();
register(gameType: string, handler: GameHandler): void {
this.handlers.set(gameType, handler);
console.log(`✅ Registered game handler: ${gameType}`);
}
getHandler(gameType: string): GameHandler {
const handler = this.handlers.get(gameType);
if (!handler) {
throw new Error(`Unknown game type: ${gameType}`);
}
return handler;
}
hasHandler(gameType: string): boolean {
return this.handlers.has(gameType);
}
getAllGameTypes(): string[] {
return Array.from(this.handlers.keys());
}
}
export const gameManager = new GameManager();

417
backend/src/games/tic-tac-toe/tic-tac-toe.handler.ts

@ -0,0 +1,417 @@
import type { WebSocket as WS } from 'ws';
import type { GameHandler, Player, GameState, AddPlayerResult } from '../base/game-handler.interface.js';
type TicTacToePlayer = Player & {
symbol: 'X' | 'O' | null;
};
type TicTacToeGameState = GameState & {
board: (string | null)[];
currentPlayer: 'X' | 'O';
winner: string | null;
isDraw: boolean;
};
const games = new Map<string, TicTacToeGameState>();
function createGame(roomId: string): TicTacToeGameState {
const game: TicTacToeGameState = {
board: Array(9).fill(null),
currentPlayer: 'X',
winner: null,
isDraw: false,
players: [],
queue: [],
};
games.set(roomId, game);
return game;
}
function getGame(roomId: string): TicTacToeGameState | undefined {
return games.get(roomId);
}
function checkWinner(board: (string | null)[]): string | null {
const lines = [
[0, 1, 2], [3, 4, 5], [6, 7, 8], // rows
[0, 3, 6], [1, 4, 7], [2, 5, 8], // columns
[0, 4, 8], [2, 4, 6], // diagonals
];
for (const [a, b, c] of lines) {
if (board[a] && board[a] === board[b] && board[a] === board[c]) {
return board[a];
}
}
return null;
}
function checkDraw(board: (string | null)[]): boolean {
return board.every(cell => cell !== null) && !checkWinner(board);
}
function broadcastState(roomId: string, excludePlayerId?: string): void {
const game = games.get(roomId);
if (!game) return;
game.players.forEach(player => {
if (player.id !== excludePlayerId && player.ws.readyState === 1) {
player.ws.send(JSON.stringify({
type: 'gameState',
game: {
board: game.board,
currentPlayer: game.currentPlayer,
winner: game.winner,
isDraw: game.isDraw,
yourSymbol: player.symbol,
players: game.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: game.queue,
},
}));
}
});
}
export class TicTacToeHandler implements GameHandler {
getGameType(): string {
return 'tic-tac-toe';
}
addPlayer(roomId: string, playerId: string, ws: WS): AddPlayerResult {
let game = getGame(roomId);
if (!game) {
game = createGame(roomId);
}
// Check if player already in game
if (game.players.find(p => p.id === playerId)) {
return { success: false, error: 'Player already in game' };
}
// If 2 players already playing, add to queue
const activePlayers = game.players.filter(p => p.symbol !== null);
if (activePlayers.length >= 2) {
game.queue.push(playerId);
const player: TicTacToePlayer = { id: playerId, ws, symbol: null };
game.players.push(player);
// Broadcast to other players
this.broadcastPlayerJoined(roomId, playerId, null);
return { success: true, gameState: this.getGameState(roomId, playerId)! };
}
// Assign symbol (X or O)
const symbol = activePlayers.length === 0 ? 'X' : 'O';
const player: TicTacToePlayer = { id: playerId, ws, symbol };
game.players.push(player);
// Broadcast to other players
this.broadcastPlayerJoined(roomId, playerId, symbol);
return { success: true, gameState: this.getGameState(roomId, playerId)! };
}
private broadcastPlayerJoined(roomId: string, newPlayerId: string, symbol: 'X' | 'O' | null): void {
const game = getGame(roomId);
if (!game) return;
game.players.forEach(player => {
if (player.id !== newPlayerId && player.ws.readyState === 1) {
const gameState = this.getGameState(roomId, player.id);
if (gameState) {
player.ws.send(JSON.stringify({
type: 'playerJoined',
playerId: newPlayerId,
symbol,
game: gameState,
}));
}
}
});
}
removePlayer(roomId: string, playerId: string): void {
const game = getGame(roomId);
if (!game) return;
const playerIndex = game.players.findIndex(p => p.id === playerId);
if (playerIndex === -1) return;
const player = game.players[playerIndex];
const wasActive = player.symbol !== null;
// Remove from players
game.players.splice(playerIndex, 1);
// Remove from queue if there
const queueIndex = game.queue.indexOf(playerId);
if (queueIndex !== -1) {
game.queue.splice(queueIndex, 1);
}
// If an active player left, promote next in queue
if (wasActive && game.queue.length > 0) {
const nextPlayerId = game.queue.shift()!;
const nextPlayer = game.players.find(p => p.id === nextPlayerId);
if (nextPlayer) {
nextPlayer.symbol = player.symbol as 'X' | 'O' | null;
}
}
// If no players left, delete game
if (game.players.length === 0) {
games.delete(roomId);
} else if (game.players.filter(p => p.symbol !== null).length < 2 && !game.winner && !game.isDraw) {
// Reset game if not enough players
game.board = Array(9).fill(null);
game.currentPlayer = 'X';
game.winner = null;
game.isDraw = false;
}
// Broadcast player left
this.broadcastPlayerLeft(roomId, playerId);
}
private broadcastPlayerLeft(roomId: string, leftPlayerId: string): void {
const game = getGame(roomId);
if (!game) return;
game.players.forEach(player => {
if (player.ws.readyState === 1) {
const gameState = this.getGameState(roomId, player.id);
if (gameState) {
player.ws.send(JSON.stringify({
type: 'playerLeft',
playerId: leftPlayerId,
game: gameState,
}));
}
}
});
}
getGameState(roomId: string, playerId: string): GameState | null {
const game = getGame(roomId);
if (!game) return null;
const player = game.players.find(p => p.id === playerId);
return {
board: game.board,
currentPlayer: game.currentPlayer,
winner: game.winner,
isDraw: game.isDraw,
yourSymbol: player?.symbol || null,
players: game.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: game.queue,
};
}
handleMessage(roomId: string, playerId: string, message: any, ws: WS): void {
switch (message.type) {
case 'move':
this.handleMove(roomId, playerId, message.position);
break;
case 'reset':
this.handleReset(roomId, playerId);
break;
case 'joinQueue':
this.handleJoinQueue(roomId, playerId);
break;
default:
ws.send(JSON.stringify({ type: 'error', message: 'Unknown message type' }));
}
}
private handleMove(roomId: string, playerId: string, position: number): void {
const game = getGame(roomId);
if (!game) return;
const player = game.players.find(p => p.id === playerId);
if (!player || !player.symbol) {
const ws = player?.ws;
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({ type: 'error', message: 'Player not in game' }));
}
return;
}
if (game.winner || game.isDraw) {
const ws = player.ws;
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({ type: 'error', message: 'Game is over' }));
}
return;
}
if (game.currentPlayer !== player.symbol) {
const ws = player.ws;
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({ type: 'error', message: 'Not your turn' }));
}
return;
}
if (game.board[position] !== null) {
const ws = player.ws;
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({ type: 'error', message: 'Position already taken' }));
}
return;
}
game.board[position] = player.symbol;
const winner = checkWinner(game.board);
const isDraw = checkDraw(game.board);
if (winner) {
game.winner = winner;
} else if (isDraw) {
game.isDraw = true;
} else {
game.currentPlayer = game.currentPlayer === 'X' ? 'O' : 'X';
}
// Broadcast current state
this.broadcastGameState(roomId);
// Auto-start next game if there's a winner and queue
if (winner && game.queue.length > 0) {
setTimeout(() => {
const currentGame = getGame(roomId);
if (!currentGame) return;
const loser = currentGame.players.find(p => p.symbol !== winner && p.symbol !== null);
if (loser) {
loser.symbol = null;
currentGame.queue.push(loser.id);
const nextPlayerId = currentGame.queue.shift();
if (nextPlayerId) {
const nextPlayer = currentGame.players.find(p => p.id === nextPlayerId);
if (nextPlayer) {
currentGame.board = Array(9).fill(null);
currentGame.currentPlayer = 'X';
currentGame.winner = null;
currentGame.isDraw = false;
nextPlayer.symbol = winner === 'X' ? 'O' : 'X';
this.broadcastGameState(roomId);
}
}
}
}, 100);
}
// Auto-rematch on draw
else if (isDraw) {
setTimeout(() => {
const currentGame = getGame(roomId);
if (!currentGame) return;
currentGame.board = Array(9).fill(null);
currentGame.currentPlayer = 'X';
currentGame.winner = null;
currentGame.isDraw = false;
this.broadcastGameState(roomId);
}, 1500);
}
}
private handleReset(roomId: string, resettingPlayerId: string): void {
const game = getGame(roomId);
if (!game) return;
const activePlayers = game.players.filter(p => p.symbol !== null);
const previousWinner = game.winner;
const wasDraw = game.isDraw;
const previousLoser = previousWinner ? activePlayers.find(p => p.symbol !== previousWinner && p.symbol !== null) : null;
const hasQueue = game.queue.length > 0;
game.board = Array(9).fill(null);
game.currentPlayer = 'X';
game.winner = null;
game.isDraw = false;
if (previousWinner && hasQueue && previousLoser) {
previousLoser.symbol = null;
game.queue.push(previousLoser.id);
const nextPlayerId = game.queue.shift();
if (nextPlayerId) {
const nextPlayer = game.players.find(p => p.id === nextPlayerId);
if (nextPlayer) {
nextPlayer.symbol = previousWinner === 'X' ? 'O' : 'X';
}
}
} else if (wasDraw && activePlayers.length === 2) {
// Rematch - players keep their symbols
} else if (activePlayers.length > 2) {
activePlayers.forEach(p => {
p.symbol = null;
game.queue.push(p.id);
});
const newPlayer1 = game.queue.shift();
const newPlayer2 = game.queue.shift();
if (newPlayer1) {
const p1 = game.players.find(p => p.id === newPlayer1);
if (p1) p1.symbol = 'X';
}
if (newPlayer2) {
const p2 = game.players.find(p => p.id === newPlayer2);
if (p2) p2.symbol = 'O';
}
}
this.broadcastGameState(roomId);
}
private handleJoinQueue(roomId: string, playerId: string): void {
const game = getGame(roomId);
if (!game) return;
const player = game.players.find(p => p.id === playerId);
if (!player) return;
if (game.queue.includes(playerId)) {
return;
}
if (player.symbol !== null) {
const ws = player.ws;
if (ws && ws.readyState === 1) {
ws.send(JSON.stringify({ type: 'error', message: 'Cannot join queue while playing' }));
}
return;
}
game.queue.push(playerId);
this.broadcastGameState(roomId);
}
private broadcastGameState(roomId: string): void {
const game = getGame(roomId);
if (!game) return;
game.players.forEach(player => {
if (player.ws.readyState === 1) {
player.ws.send(JSON.stringify({
type: 'gameState',
game: {
board: game.board,
currentPlayer: game.currentPlayer,
winner: game.winner,
isDraw: game.isDraw,
yourSymbol: player.symbol,
players: game.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: game.queue,
},
}));
}
});
}
}

7
backend/src/games/types.ts

@ -0,0 +1,7 @@
export type GameType = 'tic-tac-toe' | 'connect-four' | 'checkers';
export interface GameMessage {
type: 'move' | 'reset' | 'joinQueue' | 'init';
gameType?: GameType;
[key: string]: any;
}

252
backend/src/services/websocket.service.ts

@ -1,21 +1,37 @@
import { WebSocketServer, WebSocket as WS } from 'ws'; import { WebSocketServer, WebSocket as WS } from 'ws';
import { createServer } from 'http'; import { createServer } from 'http';
import { getGame, addPlayer, removePlayer, makeMove, resetGame, joinQueue, broadcastToRoom } from './game.service.js'; import { gameManager } from '../games/game-manager.js';
import { TicTacToeHandler } from '../games/tic-tac-toe/tic-tac-toe.handler.js';
import type { GameType } from '../games/types.js';
let wss: WebSocketServer | null = null; let wss: WebSocketServer | null = null;
// Register game handlers
gameManager.register('tic-tac-toe', new TicTacToeHandler());
export function createWebSocketServer(server: any) { export function createWebSocketServer(server: any) {
wss = new WebSocketServer({ server, path: '/api/ws' }); wss = new WebSocketServer({ server, path: '/api/ws' });
wss.on('connection', (ws: WS, req) => { wss.on('connection', (ws: WS, req) => {
const url = new URL(req.url || '', `http://${req.headers.host}`); const url = new URL(req.url || '', `http://${req.headers.host}`);
const gameType = (url.searchParams.get('gameType') || 'tic-tac-toe') as GameType;
const roomId = url.searchParams.get('room') || 'default'; const roomId = url.searchParams.get('room') || 'default';
const playerId = url.searchParams.get('playerId') || `player-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const playerId = url.searchParams.get('playerId') || `player-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
console.log(`[WebSocket] Player ${playerId} connected to room ${roomId}`); console.log(`[WebSocket] Player ${playerId} connected to room ${roomId} (game: ${gameType})`);
// Get game handler
let gameHandler;
try {
gameHandler = gameManager.getHandler(gameType);
} catch (error) {
ws.send(JSON.stringify({ type: 'error', message: `Unknown game type: ${gameType}` }));
ws.close();
return;
}
// Add player to game // Add player to game
const result = addPlayer(roomId, playerId, ws); const result = gameHandler.addPlayer(roomId, playerId, ws);
if (!result.success) { if (!result.success) {
ws.send(JSON.stringify({ type: 'error', message: result.error })); ws.send(JSON.stringify({ type: 'error', message: result.error }));
@ -24,217 +40,22 @@ export function createWebSocketServer(server: any) {
} }
// Send initial game state // Send initial game state
const initialGame = getGame(roomId); if (result.gameState) {
if (initialGame) {
ws.send(JSON.stringify({ ws.send(JSON.stringify({
type: 'gameState', type: 'gameState',
game: { game: result.gameState,
board: initialGame.board,
currentPlayer: initialGame.currentPlayer,
winner: initialGame.winner,
isDraw: initialGame.isDraw,
yourSymbol: result.symbol,
players: initialGame.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: initialGame.queue,
},
})); }));
} }
// Broadcast player joined to other players // Handler will broadcast player joined internally
const joinGame = getGame(roomId);
if (joinGame) {
joinGame.players.forEach(player => {
if (player.id !== playerId && player.ws.readyState === 1) {
player.ws.send(JSON.stringify({
type: 'playerJoined',
playerId,
symbol: result.symbol,
game: {
board: joinGame.board,
currentPlayer: joinGame.currentPlayer,
winner: joinGame.winner,
isDraw: joinGame.isDraw,
yourSymbol: player.symbol,
players: joinGame.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: joinGame.queue,
},
}));
}
});
}
// Handle messages // Handle messages
ws.on('message', (data: Buffer) => { ws.on('message', (data: Buffer) => {
try { try {
const message = JSON.parse(data.toString()); const message = JSON.parse(data.toString());
switch (message.type) { // Route message to game handler
case 'move': gameHandler.handleMessage(roomId, playerId, message, ws);
if (typeof message.position !== 'number' || message.position < 0 || message.position > 8) {
ws.send(JSON.stringify({ type: 'error', message: 'Invalid position' }));
return;
}
const moveResult = makeMove(roomId, playerId, message.position);
if (!moveResult.success) {
ws.send(JSON.stringify({ type: 'error', message: moveResult.error }));
return;
}
const updatedGame = getGame(roomId);
if (updatedGame) {
// Broadcast current game state (with winner if game ended)
updatedGame.players.forEach(player => {
if (player.ws.readyState === 1) { // WebSocket.OPEN
player.ws.send(JSON.stringify({
type: 'gameState',
game: {
board: updatedGame.board,
currentPlayer: updatedGame.currentPlayer,
winner: updatedGame.winner,
isDraw: updatedGame.isDraw,
yourSymbol: player.symbol,
players: updatedGame.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: updatedGame.queue,
},
}));
}
});
// Auto-rematch on draw - same two players play again
if (moveResult.autoRematch && updatedGame.isDraw) {
// Reset board, keep same players and symbols
updatedGame.board = Array(9).fill(null);
updatedGame.currentPlayer = 'X';
updatedGame.winner = null;
updatedGame.isDraw = false;
// Players keep their symbols, so no change needed
// Broadcast new game state immediately
setTimeout(() => {
const rematchGame = getGame(roomId);
if (rematchGame) {
rematchGame.players.forEach(player => {
if (player.ws.readyState === 1) {
player.ws.send(JSON.stringify({
type: 'gameState',
game: {
board: rematchGame.board,
currentPlayer: rematchGame.currentPlayer,
winner: rematchGame.winner,
isDraw: rematchGame.isDraw,
yourSymbol: player.symbol,
players: rematchGame.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: rematchGame.queue,
},
}));
}
});
}
}, 1500); // Small delay to show draw message before rematch
}
// Auto-start next game if there's a winner and queue
else if (moveResult.autoStartNext && updatedGame.winner && updatedGame.queue.length > 0) {
const winner = updatedGame.winner;
const loser = updatedGame.players.find(p => p.symbol !== winner && p.symbol !== null);
if (loser) {
// Move loser to end of queue
loser.symbol = null;
updatedGame.queue.push(loser.id);
// Promote next player from queue
const nextPlayerId = updatedGame.queue.shift();
if (nextPlayerId) {
const nextPlayer = updatedGame.players.find(p => p.id === nextPlayerId);
if (nextPlayer) {
// Reset board and assign opposite symbol to new player
updatedGame.board = Array(9).fill(null);
updatedGame.currentPlayer = 'X';
updatedGame.winner = null;
updatedGame.isDraw = false;
nextPlayer.symbol = winner === 'X' ? 'O' : 'X';
// Winner keeps their symbol, so no change needed
// Broadcast new game state immediately
updatedGame.players.forEach(player => {
if (player.ws.readyState === 1) {
player.ws.send(JSON.stringify({
type: 'gameState',
game: {
board: updatedGame.board,
currentPlayer: updatedGame.currentPlayer,
winner: updatedGame.winner,
isDraw: updatedGame.isDraw,
yourSymbol: player.symbol,
players: updatedGame.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: updatedGame.queue,
},
}));
}
});
}
}
}
}
}
break;
case 'reset':
resetGame(roomId, playerId);
const resetGameState = getGame(roomId);
if (resetGameState) {
// Broadcast to all players with their individual symbols
resetGameState.players.forEach(player => {
if (player.ws.readyState === 1) { // WebSocket.OPEN
player.ws.send(JSON.stringify({
type: 'gameState',
game: {
board: resetGameState.board,
currentPlayer: resetGameState.currentPlayer,
winner: resetGameState.winner,
isDraw: resetGameState.isDraw,
yourSymbol: player.symbol,
players: resetGameState.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: resetGameState.queue,
},
}));
}
});
}
break;
case 'joinQueue':
const joinResult = joinQueue(roomId, playerId);
if (!joinResult.success) {
ws.send(JSON.stringify({ type: 'error', message: joinResult.error }));
return;
}
const queueGameState = getGame(roomId);
if (queueGameState) {
// Broadcast updated game state to all players
queueGameState.players.forEach(player => {
if (player.ws.readyState === 1) { // WebSocket.OPEN
player.ws.send(JSON.stringify({
type: 'gameState',
game: {
board: queueGameState.board,
currentPlayer: queueGameState.currentPlayer,
winner: queueGameState.winner,
isDraw: queueGameState.isDraw,
yourSymbol: player.symbol,
players: queueGameState.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: queueGameState.queue,
},
}));
}
});
}
break;
default:
ws.send(JSON.stringify({ type: 'error', message: 'Unknown message type' }));
}
} catch (error) { } catch (error) {
console.error('[WebSocket] Error handling message:', error); console.error('[WebSocket] Error handling message:', error);
ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' })); ws.send(JSON.stringify({ type: 'error', message: 'Invalid message format' }));
@ -244,29 +65,8 @@ export function createWebSocketServer(server: any) {
// Handle disconnect // Handle disconnect
ws.on('close', () => { ws.on('close', () => {
console.log(`[WebSocket] Player ${playerId} disconnected from room ${roomId}`); console.log(`[WebSocket] Player ${playerId} disconnected from room ${roomId}`);
removePlayer(roomId, playerId); gameHandler.removePlayer(roomId, playerId);
// Handler will broadcast player left internally
const game = getGame(roomId);
if (game) {
// Broadcast to all remaining players with their individual symbols
game.players.forEach(player => {
if (player.ws.readyState === 1) { // WebSocket.OPEN
player.ws.send(JSON.stringify({
type: 'playerLeft',
playerId,
game: {
board: game.board,
currentPlayer: game.currentPlayer,
winner: game.winner,
isDraw: game.isDraw,
yourSymbol: player.symbol,
players: game.players.map(p => ({ id: p.id, symbol: p.symbol })),
queue: game.queue,
},
}));
}
});
}
}); });
ws.on('error', (error) => { ws.on('error', (error) => {

2
frontend/src/pages/TicTacToeApp.tsx

@ -22,7 +22,7 @@ export function TicTacToeApp() {
const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000/api'; const apiUrl = import.meta.env.VITE_API_URL || 'http://localhost:3000/api';
const wsProtocol = apiUrl.startsWith('https') ? 'wss:' : 'ws:'; const wsProtocol = apiUrl.startsWith('https') ? 'wss:' : 'ws:';
const wsHost = apiUrl.replace(/^https?:\/\//, '').replace('/api', ''); const wsHost = apiUrl.replace(/^https?:\/\//, '').replace('/api', '');
const wsUrl = `${wsProtocol}//${wsHost}/api/ws?room=default&playerId=${playerIdRef.current}`; const wsUrl = `${wsProtocol}//${wsHost}/api/ws?gameType=tic-tac-toe&room=default&playerId=${playerIdRef.current}`;
const ws = new WebSocket(wsUrl); const ws = new WebSocket(wsUrl);
wsRef.current = ws; wsRef.current = ws;

Loading…
Cancel
Save