/// /** * Battleship Multiplayer - Lobby Pattern * * ARCHITECTURE: * - Two-player lobby where each player joins from their own browser * - Shared state stored as properly typed Cells (no JSON serialization) * - Each player navigates to their own game room instance with myName parameter * * See: room.tsx for the game room pattern */ import { computed, handler, NAME, navigateTo, pattern, Stream, UI, Writable, } from "commontools"; import BattleshipRoom from "./room.tsx"; import { createInitialShots, type GameState, generateRandomShips, getRandomColor, INITIAL_GAME_STATE, type LobbyState, type PlayerData, type ShotsState, } from "./schemas.tsx"; // ============================================================================= // LOBBY PATTERN // ============================================================================= interface LobbyOutput { gameName: string; player1: PlayerData | null; player2: PlayerData | null; shots: ShotsState; gameState: GameState; // Streams for testing and programmatic control joinPlayer1: Stream<{ name: string }>; joinPlayer2: Stream<{ name: string }>; reset: Stream; } // Module-level function for navigation (pattern from Scrabble) let createGameAndNavigate: ( gameName: string, player1: Writable, player2: Writable, shots: Writable, gameState: Writable, myName: string, myPlayerNumber: 1 | 2, ) => unknown = null as any; // Handler for joining as a specific player slot const joinAsPlayer = handler< unknown, { gameName: string; nameInput: Writable; playerSlot: 1 | 2; player1: Writable; player2: Writable; shots: Writable; gameState: Writable; } >( ( _event, { gameName, nameInput, playerSlot, player1, player2, shots, gameState, }, ) => { console.log(`[joinAsPlayer] Handler started for slot ${playerSlot}`); const name = nameInput.get().trim(); if (!name) { console.log("[joinAsPlayer] No name entered, returning"); return; } console.log("[joinAsPlayer] Name:", name, "Slot:", playerSlot); // Generate random ships for this player const ships = generateRandomShips(); console.log("[joinAsPlayer] Generated ships:", ships.length); // Create player data const playerData: PlayerData = { name, ships, color: getRandomColor(playerSlot - 1), joinedAt: Date.now(), }; // Store player data directly (no JSON serialization) if (playerSlot === 1) { player1.set(playerData); } else { player2.set(playerData); } // Check if both players have joined const p1 = player1.get(); const p2 = player2.get(); if (p1 && p2) { // Both players joined - initialize game state gameState.set({ phase: "playing", currentTurn: 1, winner: null, lastMessage: `${p1.name}'s turn - fire at the enemy fleet!`, }); // Initialize shots grids shots.set(createInitialShots()); } nameInput.set(""); // Navigate to game room console.log("[joinAsPlayer] Navigating to game room..."); if (createGameAndNavigate) { return createGameAndNavigate( gameName, player1, player2, shots, gameState, name, playerSlot, ); } }, ); // Handler to rejoin an existing game const rejoinGame = handler< unknown, { gameName: string; playerSlot: 1 | 2; player1: Writable; player2: Writable; shots: Writable; gameState: Writable; } >( ( _event, { gameName, playerSlot, player1, player2, shots, gameState, }, ) => { const playerData = playerSlot === 1 ? player1.get() : player2.get(); if (!playerData) return; console.log("[rejoinGame] Rejoining as:", playerData.name); if (createGameAndNavigate) { return createGameAndNavigate( gameName, player1, player2, shots, gameState, playerData.name, playerSlot, ); } }, ); // Handler to reset the lobby const resetLobby = handler< unknown, { player1: Writable; player2: Writable; shots: Writable; gameState: Writable; } >((_event, { player1, player2, shots, gameState }) => { console.log("[resetLobby] Resetting all game state..."); player1.set(null); player2.set(null); shots.set(createInitialShots()); gameState.set(INITIAL_GAME_STATE); console.log("[resetLobby] Game state reset complete"); }); // Programmatic handler for joining as Player 1 (for testing) const joinPlayer1Handler = handler< { name: string }, { player1: Writable; player2: Writable; shots: Writable; gameState: Writable; } >(({ name }, { player1, player2, shots, gameState }) => { if (!name || !name.trim()) return; const playerData: PlayerData = { name: name.trim(), ships: generateRandomShips(), color: getRandomColor(0), joinedAt: Date.now(), }; player1.set(playerData); // Check if both players have joined const p1 = player1.get(); const p2 = player2.get(); if (p1 && p2) { gameState.set({ phase: "playing", currentTurn: 1, winner: null, lastMessage: `${p1.name}'s turn - fire at the enemy fleet!`, }); shots.set(createInitialShots()); } }); // Programmatic handler for joining as Player 2 (for testing) const joinPlayer2Handler = handler< { name: string }, { player1: Writable; player2: Writable; shots: Writable; gameState: Writable; } >(({ name }, { player1, player2, shots, gameState }) => { if (!name || !name.trim()) return; const playerData: PlayerData = { name: name.trim(), ships: generateRandomShips(), color: getRandomColor(1), joinedAt: Date.now(), }; player2.set(playerData); // Check if both players have joined const p1 = player1.get(); const p2 = player2.get(); if (p1 && p2) { gameState.set({ phase: "playing", currentTurn: 1, winner: null, lastMessage: `${p1.name}'s turn - fire at the enemy fleet!`, }); shots.set(createInitialShots()); } }); // Programmatic reset handler (for testing) const resetHandler = handler< void, { player1: Writable; player2: Writable; shots: Writable; gameState: Writable; } >((_event, { player1, player2, shots, gameState }) => { player1.set(null); player2.set(null); shots.set(createInitialShots()); gameState.set(INITIAL_GAME_STATE); }); const BattleshipLobby = pattern( ({ gameName, player1, player2, shots, gameState }) => { // Separate name inputs for each player slot const player1NameInput = Writable.of(""); const player2NameInput = Writable.of(""); // Derive player names reactively (direct Cell access, no JSON parsing) const player1Data = computed(() => player1.get()); const player2Data = computed(() => player2.get()); const player1Name = computed(() => player1Data?.name || null); const player2Name = computed(() => player2Data?.name || null); // Game state const gameStateData = computed(() => gameState.get()); const isGameStarted = computed(() => gameStateData.phase === "playing"); // Programmatic handlers for testing const joinPlayer1 = joinPlayer1Handler({ player1, player2, shots, gameState, }); const joinPlayer2 = joinPlayer2Handler({ player1, player2, shots, gameState, }); const reset = resetHandler({ player1, player2, shots, gameState }); return { [NAME]: computed(() => `${gameName} - Lobby`), [UI]: (

BATTLESHIP

Two-player naval combat — open on two devices!

{/* Two Player Join Sections */}
{/* Player 1 Section */}
Player 1
{player1Name ? (
{player1Name}
{player1Name}
{isGameStarted ? "In Game" : "Ready"}
{isGameStarted ? ( Rejoin Game ) : <>}
) : ( <> Join as Player 1 )}
{/* Player 2 Section */}
Player 2
{player2Name ? (
{player2Name}
{player2Name}
{isGameStarted ? "In Game" : "Ready"}
{isGameStarted ? ( Rejoin Game ) : <>}
) : ( <> Join as Player 2 )}
{/* Status message */}
{isGameStarted ? "Game in progress! Click 'Rejoin Game' to continue playing." : player1Name && player2Name ? "Both players ready - game starting!" : player1Name || player2Name ? "Waiting for the other player to join..." : "Each player should join from their own device"}
{/* Reset Button */} Reset Game
), gameName, player1: player1Data, player2: player2Data, shots: computed(() => shots.get()), gameState: gameStateData, // Streams for testing and programmatic control joinPlayer1, joinPlayer2, reset, }; }, ); // Navigation function setup createGameAndNavigate = ( gameName: string, player1: Writable, player2: Writable, shots: Writable, gameState: Writable, myName: string, myPlayerNumber: 1 | 2, ) => { console.log("[createGameAndNavigate] Starting..."); console.log( "[createGameAndNavigate] myName:", myName, "myPlayerNumber:", myPlayerNumber, ); // Pass typed Cells to BattleshipRoom const gameInstance = BattleshipRoom({ gameName, player1, player2, shots, gameState, myName, myPlayerNumber, }); console.log("[createGameAndNavigate] Game instance created"); return navigateTo(gameInstance); }; export default BattleshipLobby;