import randomWords from "random-words"; import { games, gameDB } from "./store"; import { PLAYER_COLORS, type Game } from "./types"; import { addChatMessage, clearPlayer, stopTurnTimer } from "./helpers"; import newPlayer from "./playerFactory"; import { info } from "./constants"; import { layout, staticData } from "../../util/layout"; import { shuffleArray } from "./utils"; import { pickRobber } from "./robber"; import { audio } from "../webrtc-signaling"; export const resetGame = (game: any) => { Object.assign(game, { startTime: Date.now(), state: "lobby", turns: 0, step: 0 /* used for the suffix # in game backups */, turn: {}, sheep: 19, ore: 19, wool: 19, brick: 19, wheat: 19, placements: { corners: [], roads: [], }, developmentCards: [], chat: [], activities: [], pipOrder: game.pipOrder, borderOrder: game.borderOrder, tileOrder: game.tileOrder, signature: game.signature, players: game.players, stolen: { robber: { stole: { total: 0, }, }, total: 0, }, longestRoad: "", longestRoadLength: 0, largestArmy: "", largestArmySize: 0, mostDeveloped: "", mostDevelopmentCards: 0, mostPorts: "", mostPortCount: 0, winner: undefined, active: 0, }); stopTurnTimer(game); /* Populate the game corner and road placement data as cleared */ for (let i = 0; i < layout.corners.length; i++) { game.placements.corners[i] = { color: undefined, type: undefined, }; } for (let i = 0; i < layout.roads.length; i++) { game.placements.roads[i] = { color: undefined, longestRoad: undefined, }; } /* Put the robber back on the Desert */ for (let i = 0; i < game.pipOrder.length; i++) { if (game.pipOrder[i] === 18) { game.robber = i; break; } } /* Populate the game development cards with a fresh deck */ for (let i = 1; i <= 14; i++) { game.developmentCards.push({ type: "army", card: i, }); } ["monopoly", "monopoly", "road-1", "road-2", "year-of-plenty", "year-of-plenty"].forEach((card) => game.developmentCards.push({ type: "progress", card: card, }) ); ["market", "library", "palace", "university"].forEach((card) => game.developmentCards.push({ type: "vp", card: card, }) ); shuffleArray(game.developmentCards); /* Reset all player data, and add in any missing colors */ PLAYER_COLORS.forEach((color) => { if (color in game.players) { clearPlayer(game.players[color]); } else { game.players[color] = newPlayer(color); } }); /* Ensure sessions are connected to player objects */ for (let key in game.sessions) { const session = game.sessions[key]; if (session.color !== "unassigned") { game.active++; session.player = game.players[session.color]; session.player.status = "Active"; session.player.lastActive = Date.now(); session.player.live = session.live; session.player.name = session.name; session.player.color = session.color; } } game.animationSeeds = []; for (let i = 0; i < game.tileOrder.length; i++) { game.animationSeeds.push(Math.random()); } }; export const setBeginnerGame = (game: Game) => { pickRobber(game); shuffleArray(game.developmentCards); game.borderOrder = []; for (let i = 0; i < 6; i++) { game.borderOrder.push(i); } game.tileOrder = [9, 12, 1, 5, 16, 13, 17, 6, 2, 0, 3, 10, 4, 11, 7, 14, 18, 8, 15]; game.robber = 9; game.animationSeeds = []; for (let i = 0; i < game.tileOrder.length; i++) { game.animationSeeds.push(Math.random()); } game.pipOrder = [5, 1, 6, 7, 2, 9, 11, 12, 8, 18, 3, 4, 10, 16, 13, 0, 14, 15, 17]; game.signature = gameSignature(game); }; export const shuffleBoard = (game: any): void => { pickRobber(game); const seq = []; for (let i = 0; i < 6; i++) { seq.push(i); } shuffleArray(seq); game.borderOrder = seq.slice(); for (let i = 6; i < 19; i++) { seq.push(i); } shuffleArray(seq); game.tileOrder = seq.slice(); /* Pip order is from one of the random corners, then rotate around * and skip over the desert (robber) */ /* Board: * 0 1 2 * 3 4 5 6 * 7 8 9 10 11 * 12 13 14 15 * 16 17 18 */ const order = [ [0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 7, 3, 4, 5, 10, 14, 13, 8, 9], [2, 6, 11, 15, 18, 17, 16, 12, 7, 3, 0, 1, 5, 10, 14, 13, 8, 4, 9], [11, 15, 18, 17, 16, 12, 7, 3, 0, 1, 2, 6, 10, 14, 13, 8, 4, 5, 9], [18, 17, 16, 12, 7, 3, 0, 1, 2, 6, 11, 15, 14, 13, 8, 4, 5, 10, 9], [16, 12, 7, 3, 0, 1, 2, 6, 11, 15, 18, 17, 13, 8, 4, 5, 10, 14, 9], [7, 3, 0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 8, 4, 5, 10, 14, 13, 9], ]; const sequence = order[Math.floor(Math.random() * order.length)]; if (!sequence || !Array.isArray(sequence)) { // Defensive: should not happen, but guard for TS strictness return; } game.pipOrder = []; game.animationSeeds = []; for (let i = 0, p = 0; i < sequence.length; i++) { const target: number = sequence[i]!; /* If the target tile is the desert (18), then set the * pip value to the robber (18) otherwise set * the target pip value to the currently incremeneting * pip value. */ const tileIdx = game.tileOrder[target]; const tileType = staticData.tiles[tileIdx]?.type; if (!game.pipOrder) game.pipOrder = []; if (tileType === "desert") { game.robber = target; game.pipOrder[target] = 18; } else { game.pipOrder[target] = p++; } game.animationSeeds.push(Math.random()); } shuffleArray(game.developmentCards); game.signature = gameSignature(game); }; export const createGame = async (id: string | null = null) => { /* Look for a new game with random words that does not already exist */ while (!id) { id = randomWords(4).join("-"); try { /* If a game with this id exists in the DB, look for a new name */ if (!gameDB || !gameDB.getGameById) { throw new Error("Game DB not available for uniqueness check"); } let exists = false; try { const g = await gameDB.getGameById(id); if (g) exists = true; } catch (e) { // if DB check fails treat as non-existent and continue searching } if (exists) { id = ""; } } catch (error) { break; } } console.log(`${info}: creating ${id}`); const game: Game = { id: id, developmentCards: [], playerOrder: [], turns: 0, players: { O: newPlayer("O"), R: newPlayer("R"), B: newPlayer("B"), W: newPlayer("W"), }, sessions: {}, unselected: [], placements: { corners: [], roads: [], }, turn: { name: "", color: "unassigned", actions: [], limits: {}, roll: 0, }, rules: { "victory-points": { points: 10, }, }, pipOrder: [], borderOrder: [], tileOrder: [], step: 0 /* used for the suffix # in game backups */, }; setBeginnerGame(game); resetGame(game); addChatMessage(game, null, `New game created with Beginner's Layout: ${game.id}`); games[game.id] = game; audio[game.id] = {}; return game; }; const gameSignature = (game: Game): string => { if (!game) { return ""; } const salt = 251; const signature = (game.borderOrder || []).map((border: any) => `00${(Number(border) ^ salt).toString(16)}`.slice(-2)).join("") + "-" + (game.pipOrder || []) .map((pip: any, index: number) => `00${(Number(pip) ^ salt ^ (salt * index)).toString(16)}`.slice(-2)) .join("") + "-" + (game.tileOrder || []) .map((tile: any, index: number) => `00${(Number(tile) ^ salt ^ (salt * index)).toString(16)}`.slice(-2)) .join(""); return signature; };