diff --git a/experimental/2nub/__tests__/server/routes.test.ts b/experimental/2nub/__tests__/server/routes.test.ts index ea60d41..e6c10f4 100644 --- a/experimental/2nub/__tests__/server/routes.test.ts +++ b/experimental/2nub/__tests__/server/routes.test.ts @@ -102,7 +102,7 @@ describe('Game API Routes', () => { expect(response.status).toBe(400); expect(response.body.success).toBe(false); - expect(response.body.error).toBe('Player name is required when joining as new player'); + expect(response.body.error).toBe('Player name is required'); }); it('should reject joining non-existent game', async () => { @@ -115,7 +115,10 @@ describe('Game API Routes', () => { expect(response.body.error).toBe('Game not found or full'); }); - it('should allow joining as disconnected player via existingPlayerId', async () => { + }); + + describe('POST /api/games/:gameId/rejoin', () => { + it('should allow rejoining as disconnected player', async () => { const game = gameStateManager.createGame('Test Game'); const player = gameStateManager.addPlayer(game.id, 'Alice')!; @@ -123,8 +126,8 @@ describe('Game API Routes', () => { gameStateManager.updatePlayerConnection(game.id, player.id, false); const response = await request(app) - .post(`/api/games/${game.id}/join`) - .send({ existingPlayerId: player.id }); + .post(`/api/games/${game.id}/rejoin`) + .send({ playerId: player.id }); expect(response.status).toBe(200); expect(response.body.success).toBe(true); @@ -132,30 +135,52 @@ describe('Game API Routes', () => { expect(response.body.data.player.connected).toBe(true); }); - it('should reject joining as already connected player', async () => { + it('should reject rejoining as already connected player', async () => { const game = gameStateManager.createGame('Test Game'); const player = gameStateManager.addPlayer(game.id, 'Alice')!; // Player is already connected by default const response = await request(app) - .post(`/api/games/${game.id}/join`) - .send({ existingPlayerId: player.id }); + .post(`/api/games/${game.id}/rejoin`) + .send({ playerId: player.id }); expect(response.status).toBe(404); expect(response.body.success).toBe(false); - expect(response.body.error).toBe('Game or player not found, or player is already connected'); + expect(response.body.error).toBe('Game or player not found'); }); - it('should reject joining with invalid existingPlayerId', async () => { + it('should reject rejoining with invalid playerId', async () => { const game = gameStateManager.createGame('Test Game'); const response = await request(app) - .post(`/api/games/${game.id}/join`) - .send({ existingPlayerId: 'INVALID_ID' }); + .post(`/api/games/${game.id}/rejoin`) + .send({ playerId: 'INVALID_ID' }); + + expect(response.status).toBe(404); + expect(response.body.success).toBe(false); + expect(response.body.error).toBe('Game or player not found'); + }); + + it('should reject empty playerId', async () => { + const game = gameStateManager.createGame('Test Game'); + + const response = await request(app) + .post(`/api/games/${game.id}/rejoin`) + .send({ playerId: '' }); + + expect(response.status).toBe(400); + expect(response.body.success).toBe(false); + expect(response.body.error).toBe('Player ID is required'); + }); + + it('should reject rejoining non-existent game', async () => { + const response = await request(app) + .post('/api/games/INVALID/rejoin') + .send({ playerId: 'PLAYER_ID' }); expect(response.status).toBe(404); expect(response.body.success).toBe(false); - expect(response.body.error).toBe('Game or player not found, or player is already connected'); + expect(response.body.error).toBe('Game or player not found'); }); }); diff --git a/experimental/2nub/src/client/App.tsx b/experimental/2nub/src/client/App.tsx index 01b849d..352229c 100644 --- a/experimental/2nub/src/client/App.tsx +++ b/experimental/2nub/src/client/App.tsx @@ -140,22 +140,12 @@ const App: React.FC = () => { return false; }; - const joinGame = async (gameId: string, playerName?: string, existingPlayerId?: string) => { + const joinGame = async (gameId: string, playerName: string) => { try { - const requestBody: any = {}; - if (existingPlayerId) { - requestBody.existingPlayerId = existingPlayerId; - } else if (playerName) { - requestBody.playerName = playerName; - } else { - console.error('Either playerName or existingPlayerId must be provided'); - return; - } - const response = await fetch(buildApiUrl(`api/games/${gameId}/join`), { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(requestBody) + body: JSON.stringify({ playerName }) }); const result = await response.json(); if (result.success) { @@ -283,7 +273,7 @@ const App: React.FC = () => { {view === 'list' ? (
- +
) : ( void; + onJoinGame: (gameId: string, playerName: string) => void; + onRejoinGame: (gameId: string, playerId: string) => Promise; onRefresh: () => void; } -export const GameList: React.FC = ({ games, onJoinGame, onRefresh }) => { +export const GameList: React.FC = ({ games, onJoinGame, onRejoinGame, onRefresh }) => { const [playerName, setPlayerName] = useState(''); const [joiningGameId, setJoiningGameId] = useState(null); const [selectedExistingPlayer, setSelectedExistingPlayer] = useState(null); @@ -18,16 +19,21 @@ export const GameList: React.FC = ({ games, onJoinGame, onRefresh setPlayerName(''); }; - const handleJoinSubmit = (e: React.FormEvent, gameId: string) => { + const handleJoinSubmit = async (e: React.FormEvent, gameId: string) => { e.preventDefault(); if (selectedExistingPlayer) { - onJoinGame(gameId, undefined, selectedExistingPlayer); + const success = await onRejoinGame(gameId, selectedExistingPlayer); + if (success) { + setPlayerName(''); + setJoiningGameId(null); + setSelectedExistingPlayer(null); + } } else if (playerName.trim()) { onJoinGame(gameId, playerName.trim()); + setPlayerName(''); + setJoiningGameId(null); + setSelectedExistingPlayer(null); } - setPlayerName(''); - setJoiningGameId(null); - setSelectedExistingPlayer(null); }; const handleExistingPlayerSelect = (playerId: string) => { diff --git a/experimental/2nub/src/server/routes.ts b/experimental/2nub/src/server/routes.ts index 488dc21..966cf53 100644 --- a/experimental/2nub/src/server/routes.ts +++ b/experimental/2nub/src/server/routes.ts @@ -1,7 +1,7 @@ import { Router, Response } from 'express'; import { gameStateManager } from './gameState'; import { broadcastToGame, broadcastToAll } from './websocket'; -import { CreateGameRequest, JoinGameRequest, ApiResponse } from '../types'; +import { CreateGameRequest, JoinGameRequest, RejoinGameRequest, ApiResponse } from '../types'; // TODO: consider not returning `game` state in the HTTP response, since websockets should handle real time server-to-client data updates @@ -61,44 +61,12 @@ export function setupRoutes(): Router { return respondSuccessWithData(res, 201, game); }); - // TODO: consider removing rejoining logic from here since /rejoin exists router.post('/games/:gameId/join', (req, res) => { const { gameId } = req.params; - const { playerName, existingPlayerId }: { playerName?: string; existingPlayerId?: string } = req.body; - - // Joining as existing disconnected player - if (existingPlayerId) { - if (!existingPlayerId.trim()) { - return respondFailure(res, 400, 'Player ID is required'); - } - - const result = gameStateManager.rejoinPlayer(gameId, existingPlayerId.trim()); - - if (!result) { - return respondFailure(res, 404, 'Game or player not found, or player is already connected'); - } - - // Set player as connected - gameStateManager.updatePlayerConnection(gameId, result.player.id, true); - - // Get updated game state after connection update - const updatedGame = gameStateManager.getGame(gameId); - - if (!updatedGame) { - return respondGameNotFound(res, null); - } - - broadcastToGame(gameId, 'playerJoined', { player: result.player, game: updatedGame }); - - return respondSuccessWithData(res, 200, { - player: result.player, - game: updatedGame - }); - } + const { playerName }: JoinGameRequest = req.body; - // Joining as new player if (!playerName || playerName.trim() === '') { - return respondFailure(res, 400, 'Player name is required when joining as new player'); + return respondFailure(res, 400, 'Player name is required'); } const player = gameStateManager.addPlayer(gameId, playerName.trim()); @@ -124,7 +92,7 @@ export function setupRoutes(): Router { router.post('/games/:gameId/rejoin', (req, res) => { console.debug('Rejoin request received:', req.params, req.body); const { gameId } = req.params; - const { playerId }: { playerId: string } = req.body; + const { playerId }: RejoinGameRequest = req.body; if (!playerId || playerId.trim() === '') { return respondFailure(res, 400, 'Player ID is required'); diff --git a/experimental/2nub/src/types.ts b/experimental/2nub/src/types.ts index eaa1767..d07fbe9 100644 --- a/experimental/2nub/src/types.ts +++ b/experimental/2nub/src/types.ts @@ -77,8 +77,12 @@ export interface CreateGameRequest { export interface JoinGameRequest { gameId: GameState["id"]; - playerName?: Player["name"]; - existingPlayerId?: Player["id"]; + playerName: Player["name"]; +} + +export interface RejoinGameRequest { + gameId: GameState["id"]; + playerId: Player["id"]; } export interface ServerToClientEventShapes {