/// /** * Multiplayer Free-for-All Scrabble - Lobby Pattern * * ARCHITECTURE: * - ALL shared state stored as JSON STRINGS to bypass Cell array proxy issues * - bagJson, boardJson, playersJson, gameEventsJson, allRacksJson, allPlacedJson * - Parse functions handle BOTH string and object input (runtime may auto-deserialize) * * See: scrabble-game.tsx for the game room pattern */ import { Cell, computed, Default, handler, NAME, navigateTo, pattern, UI, } from "commontools"; import ScrabbleGame, { createTileBag, drawTilesFromBag, getRandomColor, parseAllPlacedJson, parseAllRacksJson, parseGameEventsJson, parsePlayersJson, Player, } from "./scrabble-game.tsx"; // ============================================================================= // LOBBY PATTERN // ============================================================================= interface LobbyInput { gameName: Default; boardJson: Cell>; // JSON string of PlacedTile[] bagJson: Cell>; bagIndex: Cell>; playersJson: Cell>; // JSON string of Player[] gameEventsJson: Cell>; // JSON string of GameEvent[] allRacksJson: Cell>; // JSON string of AllRacks allPlacedJson: Cell>; // JSON string of AllPlaced } interface LobbyOutput { gameName: string; boardJson: string; bagJson: string; bagIndex: number; playersJson: string; gameEventsJson: string; allRacksJson: string; allPlacedJson: string; } let createGameAndNavigate: ( gameName: string, boardJson: Cell, bagJson: Cell, bagIndex: Cell, playersJson: Cell, gameEventsJson: Cell, allRacksJson: Cell, allPlacedJson: Cell, myName: string, ) => unknown = null as any; // Handler for joining as a specific player slot (0 or 1) const joinAsPlayer = handler< unknown, { gameName: string; nameInput: Cell; playerSlot: number; boardJson: Cell; bagJson: Cell; bagIndex: Cell; playersJson: Cell; gameEventsJson: Cell; allRacksJson: Cell; allPlacedJson: Cell; } >(( _event, { gameName, nameInput, playerSlot, boardJson, bagJson, bagIndex, playersJson, gameEventsJson, allRacksJson, allPlacedJson, }, ) => { 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); // Lazy initialize bag if empty let currentBagJson = bagJson.get(); if (!currentBagJson || currentBagJson === "") { console.log("[joinAsPlayer] Bag is empty, initializing fresh tile bag..."); const freshBag = createTileBag(); currentBagJson = JSON.stringify(freshBag); bagJson.set(currentBagJson); console.log( "[joinAsPlayer] Fresh bag created with", freshBag.length, "tiles", ); } // Initialize board if empty let currentBoardJson = boardJson.get(); if (!currentBoardJson || currentBoardJson === "") { boardJson.set("[]"); currentBoardJson = "[]"; } // Draw 7 tiles for this player console.log("[joinAsPlayer] Drawing tiles from bag..."); const currentIndex = bagIndex.get(); const drawnTiles = drawTilesFromBag(currentBagJson, currentIndex, 7); console.log( "[joinAsPlayer] Drew tiles:", drawnTiles.map((t) => t.char).join(","), ); const newIndex = currentIndex + drawnTiles.length; bagIndex.set(newIndex); console.log("[joinAsPlayer] Bag index updated to:", newIndex); // Store rack for this player const currentAllRacks = parseAllRacksJson(allRacksJson.get()); currentAllRacks[name] = drawnTiles; allRacksJson.set(JSON.stringify(currentAllRacks)); console.log("[joinAsPlayer] Rack stored for:", name); // Initialize empty placed tiles for this player const currentAllPlaced = parseAllPlacedJson(allPlacedJson.get()); currentAllPlaced[name] = []; allPlacedJson.set(JSON.stringify(currentAllPlaced)); // Create/update player at the specified slot const existingPlayers = parsePlayersJson(playersJson.get()); const newPlayer: Player = { name, color: getRandomColor(playerSlot), score: 0, joinedAt: Date.now(), }; // Ensure array is big enough and set at exact slot while (existingPlayers.length <= playerSlot) { existingPlayers.push(null as any); } existingPlayers[playerSlot] = newPlayer; // Filter out any null entries if slot 1 was set before slot 0 const cleanedPlayers = existingPlayers.filter((p) => p !== null); playersJson.set(JSON.stringify(cleanedPlayers)); console.log("[joinAsPlayer] Player added at slot:", playerSlot); // Add join event const existingEvents = parseGameEventsJson(gameEventsJson.get()); existingEvents.push({ id: `event-${Date.now()}-${Math.random().toString(36).slice(2)}`, type: "join", player: name, details: `${name} joined as Player ${playerSlot + 1}`, timestamp: Date.now(), }); gameEventsJson.set(JSON.stringify(existingEvents)); nameInput.set(""); // Navigate to game room console.log("[joinAsPlayer] Navigating to game room..."); if (createGameAndNavigate) { return createGameAndNavigate( gameName, boardJson, bagJson, bagIndex, playersJson, gameEventsJson, allRacksJson, allPlacedJson, name, ); } }); // Handler to reset the lobby (clear all game state) const resetLobby = handler< unknown, { boardJson: Cell; bagJson: Cell; bagIndex: Cell; playersJson: Cell; gameEventsJson: Cell; allRacksJson: Cell; allPlacedJson: Cell; } >(( _event, { boardJson, bagJson, bagIndex, playersJson, gameEventsJson, allRacksJson, allPlacedJson, }, ) => { console.log("[resetLobby] Resetting all game state..."); // Clear all state boardJson.set("[]"); bagIndex.set(0); playersJson.set("[]"); gameEventsJson.set("[]"); allRacksJson.set("{}"); allPlacedJson.set("{}"); // Initialize fresh bag const freshBag = createTileBag(); bagJson.set(JSON.stringify(freshBag)); console.log("[resetLobby] Game state reset complete"); }); const ScrabbleLobby = pattern( ( { gameName, boardJson, bagJson, bagIndex, playersJson, gameEventsJson, allRacksJson, allPlacedJson, }, ) => { // Separate name inputs for each player slot const player1NameInput = Cell.of(""); const player2NameInput = Cell.of(""); // Derive player data reactively from playersJson const player1 = computed(() => { const players = parsePlayersJson(playersJson.get()); return players[0] || null; }); const player2 = computed(() => { const players = parsePlayersJson(playersJson.get()); return players[1] || null; }); const player1Name = computed(() => player1?.name || null); const player2Name = computed(() => player2?.name || null); return { [NAME]: computed(() => `${gameName} - Lobby`), [UI]: (

SCRABBLE

Free-for-all multiplayer — no turns!

{/* Two Player Join Sections */}
{/* Player 1 Section */}
Player 1
{player1Name ? (
{player1Name}
Now playing
) : ( <> )}
{/* Player 2 Section */}
Player 2
{player2Name ? (
{player2Name}
Now playing
) : ( <> )}
{/* Reset Button */}
), gameName, boardJson, bagJson, bagIndex, playersJson, gameEventsJson, allRacksJson, allPlacedJson, }; }, ); createGameAndNavigate = ( gameName: string, boardJson: Cell, bagJson: Cell, bagIndex: Cell, playersJson: Cell, gameEventsJson: Cell, allRacksJson: Cell, allPlacedJson: Cell, myName: string, ) => { console.log("[createGameAndNavigate] Starting..."); console.log("[createGameAndNavigate] myName:", myName); const racks = parseAllRacksJson(allRacksJson.get()); console.log("[createGameAndNavigate] allRacksJson keys:", Object.keys(racks)); console.log("[createGameAndNavigate] Creating ScrabbleGame instance..."); const gameInstance = ScrabbleGame({ gameName, boardJson, bagJson, bagIndex, playersJson, gameEventsJson, allRacksJson, allPlacedJson, myName, }); console.log( "[createGameAndNavigate] ScrabbleGame instance created:", gameInstance, ); console.log("[createGameAndNavigate] Calling navigateTo..."); const navResult = navigateTo(gameInstance); console.log("[createGameAndNavigate] navigateTo returned:", navResult); return navResult; }; export default ScrabbleLobby;