diff --git a/server/routes/games.ts b/server/routes/games.ts index fbde815..32f8576 100755 --- a/server/routes/games.ts +++ b/server/routes/games.ts @@ -16,7 +16,7 @@ import { } from "./games/constants"; import { getValidRoads, getValidCorners, isRuleEnabled } from "../util/validLocations"; -import { Player, Game, Session, CornerPlacement, RoadPlacement, Offer } from "./games/types"; +import { Player, Game, Session, CornerPlacement, RoadPlacement, Offer, Turn } from "./games/types"; import { newPlayer } from "./games/playerFactory"; import { normalizeIncoming, shuffleArray } from "./games/utils"; // import type { GameState } from './games/state'; // unused import removed during typing pass @@ -49,7 +49,7 @@ initGameDB() // shuffleArray imported from './games/utils.ts' -const games: Record = {}; +const games: Record = {}; const audio: Record = {}; const processTies = (players: Player[]): boolean => { @@ -57,12 +57,12 @@ const processTies = (players: Player[]): boolean => { * order, and their current roll. If a resulting * roll array has more than one element, then there * is a tie that must be resolved */ - let slots: any[] = []; + let slots: Player[][] = []; players.forEach((player: Player) => { if (!slots[player.order]) { slots[player.order] = []; } - slots[player.order].push(player); + slots[player.order]!.push(player); }); let ties = false, @@ -385,7 +385,7 @@ const distributeResources = (game: Game, roll: number): void => { } }; -const pickRobber = (game: any): void => { +const pickRobber = (game: Game): void => { const selection = Math.floor(Math.random() * 3); switch (selection) { case 0: @@ -756,8 +756,13 @@ const loadGame = async (id: string) => { return game; }; -const adminCommands = (game: any, action: string, value: string, query: any): any => { - let color: string | undefined, parts: RegExpMatchArray | null, session: any, corners: any, corner: any, error: any; +const adminCommands = (game: Game, action: string, value: string, query: any): any => { + let color: string | undefined, + parts: RegExpMatchArray | null, + session: Session | any, + corners: any, + corner: any, + error: any; void color; switch (action) { @@ -809,9 +814,11 @@ const adminCommands = (game: any, action: string, value: string, query: any): an const type = parts[1], card = parts[3] || 1; - for (let id in game.sessions) { - if (game.sessions[id].name === game.turn.name) { - session = game.sessions[id]; + if (game.sessions) { + for (let id in game.sessions) { + if (game.sessions[id] && game.sessions[id].name === game.turn.name) { + session = game.sessions[id]; + } } } @@ -902,8 +909,10 @@ const adminCommands = (game: any, action: string, value: string, query: any): an } let tmp = game.developmentCards.splice(index, 1)[0]; - tmp.turn = game.turns ? game.turns - 1 : 0; - session.player.development.push(tmp); + if (tmp) { + (tmp as any)["turn"] = game.turns ? game.turns - 1 : 0; + session.player.development.push(tmp); + } addChatMessage(game, null, `Admin gave a ${card}-${type} to ${game.turn.name}.`); break; @@ -972,16 +981,16 @@ const adminCommands = (game: any, action: string, value: string, query: any): an case "pass": let name = game.turn.name; - const next = getNextPlayerSession(game, name); + const next = getNextPlayerSession(game, name || ""); if (!next) { addChatMessage(game, null, `Admin attempted to skip turn but no next player was found.`); break; } game.turn = { - name: next.player, + name: next.name, color: next.color, - }; - game.turns++; + } as unknown as Turn; + game.turns = (game.turns || 0) + 1; startTurnTimer(game, next); addChatMessage(game, null, `The admin skipped ${name}'s turn.`); addChatMessage(game, null, `It is ${next.name}'s turn.`); @@ -1055,7 +1064,7 @@ const adminCommands = (game: any, action: string, value: string, query: any): an } }; -const setPlayerName = (game: any, session: any, name: string): string | undefined => { +const setPlayerName = (game: Game, session: Session, name: string): string | undefined => { if (session.name === name) { return; /* no-op */ } @@ -1118,7 +1127,7 @@ const setPlayerName = (game: any, session: any, name: string): string | undefine session.name = name; session.live = true; if (session.player) { - session.color = session.player.color; + session.color = session.player.color || ""; session.player.name = session.name; session.player.status = `Active`; session.player.lastActive = Date.now(); @@ -1394,7 +1403,7 @@ const processRoad = (game: Game, color: string, roadIndex: number, placedRoad: R return roadLength; }; -const buildRoadGraph = (game: Game, color: string, roadIndex: number, placedRoad: RoadPlacement, set: any) => { +const buildRoadGraph = (game: Game, color: string, roadIndex: number, placedRoad: RoadPlacement, set: number[]) => { /* If this road isn't assigned to the walking color, skip it */ if (placedRoad.color !== color) { return; @@ -1407,7 +1416,7 @@ const buildRoadGraph = (game: Game, color: string, roadIndex: number, placedRoad placedRoad.walking = true; set.push(roadIndex); /* Calculate the longest road branching from both corners */ - layout.roads?.[roadIndex]?.corners.forEach((cornerIndex: any) => { + layout.roads?.[roadIndex]?.corners.forEach((cornerIndex: number) => { const placedCorner = game.placements?.corners?.[cornerIndex]; if (!placedCorner) return; buildCornerGraph(game, color, cornerIndex, placedCorner, set); @@ -1454,11 +1463,11 @@ const calculateRoadLengths = (game: Game, session: Session): void => { * needed to catch loops where starting from an outside end * point may result in not counting the length of the loop */ - let graphs: any[] = []; + let graphs: { color: string; set: number[]; longestRoad?: number; longestStartSegment?: number }[] = []; layout.roads.forEach((_: any, roadIndex: number) => { const placedRoad = game.placements?.roads?.[roadIndex]; if (placedRoad && placedRoad.color && typeof placedRoad.color === "string") { - let set: any[] = []; + let set: number[] = []; buildRoadGraph(game, placedRoad.color, roadIndex, placedRoad, set); if (set.length) { graphs.push({ color: placedRoad.color, set }); @@ -1744,26 +1753,26 @@ const canMeetOffer = (player: Player, offer: Offer): boolean => { return true; }; -const gameSignature = (game: any): string => { +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.borderOrder || []).map((border: any) => `00${(Number(border) ^ salt).toString(16)}`.slice(-2)).join("") + "-" + - game.pipOrder + (game.pipOrder || []) .map((pip: any, index: number) => `00${(Number(pip) ^ salt ^ (salt * index)).toString(16)}`.slice(-2)) .join("") + "-" + - game.tileOrder + (game.tileOrder || []) .map((tile: any, index: number) => `00${(Number(tile) ^ salt ^ (salt * index)).toString(16)}`.slice(-2)) .join(""); return signature; }; -const setGameFromSignature = (game: any, border: string, pip: string, tile: string): boolean => { +const setGameFromSignature = (game: Game, border: string, pip: string, tile: string): boolean => { const salt = 251; const borders = [], pips = [], @@ -2074,19 +2083,19 @@ const trade = (game: Game, session: Session, action: string, offer?: Offer): str return undefined; }; -const clearTimeNotice = (game: any, session: any): string | undefined => { - if (!session.player.turnNotice) { +const clearTimeNotice = (game: Game, session: Session): string | undefined => { + if (!session.player || !session.player.turnNotice) { /* benign state; don't alert the user */ //return `You have not been idle.`; } - session.player.turnNotice = ""; + if (session.player) session.player.turnNotice = ""; sendUpdateToPlayer(game, session, { private: session.player, }); return undefined; }; -const startTurnTimer = (game: any, session: any) => { +const startTurnTimer = (game: Game, session: Session) => { const timeout = 90; if (!session.ws) { console.log(`${session.id}: Aborting turn timer as ${session.name} is disconnected.`); @@ -2102,7 +2111,9 @@ const startTurnTimer = (game: any, session: any) => { } game.turnTimer = setTimeout(() => { console.log(`${session.id}: Turn timer expired for ${session.name}`); - session.player.turnNotice = "It is still your turn."; + if (session.player) { + session.player.turnNotice = "It is still your turn."; + } sendUpdateToPlayer(game, session, { private: session.player, }); @@ -2110,14 +2121,18 @@ const startTurnTimer = (game: any, session: any) => { }, timeout * 1000); }; -const resetTurnTimer = (game: any, session: any): void => { +const resetTurnTimer = (game: Game, session: Session): void => { startTurnTimer(game, session); }; -const stopTurnTimer = (game: any): void => { +const stopTurnTimer = (game: Game): void => { if (game.turnTimer) { console.log(`${info}: Stopping turn timer.`); - clearTimeout(game.turnTimer); + try { + clearTimeout(game.turnTimer); + } catch (e) { + /* ignore if not a real timeout */ + } game.turnTimer = 0; } return undefined; @@ -2172,7 +2187,7 @@ const pass = (game: any, session: any): string | undefined => { color: next.color, }; if (next.player) { - (next.player as any)["turnStart"] = Date.now(); + next.player.turnStart = Date.now(); } startTurnTimer(game, next); game.turns++; @@ -2196,11 +2211,9 @@ const pass = (game: any, session: any): string | undefined => { return undefined; }; -const placeRobber = (game: any, session: any, robber: any): string | undefined => { +const placeRobber = (game: Game, session: Session, robber: number | string): string | undefined => { const name = session.name; - if (typeof robber === "string") { - robber = parseInt(robber); - } + let robberIdx = typeof robber === "string" ? parseInt(robber) : robber; if (game.state !== "normal" && game.turn.roll !== 7) { return `You cannot place robber unless 7 was rolled!`; @@ -2209,26 +2222,26 @@ const placeRobber = (game: any, session: any, robber: any): string | undefined = return `You cannot place the robber when it isn't your turn.`; } - for (let color in game.players) { - if (game.players[color].status === "Not active") { - continue; - } - if (game.players[color].mustDiscard > 0) { + for (const color in game.players) { + const p = game.players[color]; + if (!p) continue; + if (p.status === "Not active") continue; + if ((p.mustDiscard || 0) > 0) { return `You cannot place the robber until everyone has discarded!`; } } - if (game.robber === robber) { + if (game.robber === robberIdx) { return `You must move the robber to a new location!`; } - game.robber = robber; - game.turn.placedRobber = true; + game.robber = robberIdx as number; + (game.turn as any).placedRobber = true; pickRobber(game); addActivity(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`); - let targets: Array<{ color: string; name: string }> = []; - layout.tiles?.[robber]?.corners?.forEach((cornerIndex: number) => { + const targets: Array<{ color: string; name: string }> = []; + layout.tiles?.[robberIdx as number]?.corners?.forEach((cornerIndex: number) => { const active = game.placements?.corners?.[cornerIndex]; if ( active && @@ -2244,7 +2257,8 @@ const placeRobber = (game: any, session: any, robber: any): string | undefined = }); if (targets.length) { - (game.turn.actions = ["steal-resource"]), (game.turn.limits = { players: targets }); + game.turn.actions = ["steal-resource"]; + game.turn.limits = { players: targets } as any; } else { game.turn.actions = []; game.turn.robberInAction = false; @@ -2272,28 +2286,24 @@ const placeRobber = (game: any, session: any, robber: any): string | undefined = return undefined; }; -const stealResource = (game: any, session: any, color: any): string | undefined => { - if (game.turn.actions.indexOf("steal-resource") === -1) { +const stealResource = (game: Game, session: Session, color: string): string | undefined => { + if (!game.turn.actions || game.turn.actions.indexOf("steal-resource") === -1) { return `You can only steal a resource when it is valid to do so!`; } - if (game.turn.limits.players.findIndex((item: any) => item.color === color) === -1) { + const playersLimit = (game.turn.limits as any)?.players || []; + if (playersLimit.findIndex((item: any) => item.color === color) === -1) { return `You can only steal a resource from a player on this terrain!`; } - let victim: any | undefined; - for (let key in game.sessions) { - if (game.sessions[key].color === color) { - victim = game.sessions[key]; - break; - } + + const victimSession = sessionFromColor(game, color); + if (!victimSession || !victimSession.player) { + return `You sent a weird color for the target to steal from.`; } - if (!victim || !victim.player) { - return `You sent a wierd color for the target to steal from.`; - } - const victimPlayer: Record = victim.player; - const sessionPlayer: Record = session.player; + const victimPlayer = victimSession.player as Player; + const sessionPlayer = session.player as Player; const cards: string[] = []; ["wheat", "brick", "sheep", "stone", "wood"].forEach((field: string) => { - for (let i = 0; i < (victimPlayer[field] || 0); i++) { + for (let i = 0; i < ((victimPlayer as any)[field] || 0); i++) { cards.push(field); } }); @@ -2301,30 +2311,29 @@ const stealResource = (game: any, session: any, color: any): string | undefined debugChat(game, "Before steal"); if (cards.length === 0) { - addChatMessage(game, session, `${victim.name} ` + `did not have any cards for ${session.name} to steal.`); + addChatMessage(game, session, `${victimSession.name} did not have any cards for ${session.name} to steal.`); game.turn.actions = []; - game.turn.limits = {}; + game.turn.limits = {} as any; } else { - let index = Math.floor(Math.random() * cards.length), - type = cards[index]; + const idx = Math.floor(Math.random() * cards.length); + const type = cards[idx]; if (!type) { - // Defensive: no card type found game.turn.actions = []; - game.turn.limits = {}; - return; + game.turn.limits = {} as any; + return undefined; } const t = String(type); - victimPlayer[t] = (victimPlayer[t] || 0) - 1; - victimPlayer["resources"] = (victimPlayer["resources"] || 0) - 1; - sessionPlayer[t] = (sessionPlayer[t] || 0) + 1; - sessionPlayer["resources"] = (sessionPlayer["resources"] || 0) + 1; + (victimPlayer as any)[t] = ((victimPlayer as any)[t] || 0) - 1; + (victimPlayer as any)["resources"] = ((victimPlayer as any)["resources"] || 0) - 1; + (sessionPlayer as any)[t] = ((sessionPlayer as any)[t] || 0) + 1; + (sessionPlayer as any)["resources"] = ((sessionPlayer as any)["resources"] || 0) + 1; game.turn.actions = []; - game.turn.limits = {}; - trackTheft(game, victim.color || "", session.color, type, 1); + game.turn.limits = {} as any; + trackTheft(game, (victimSession as any).color || "", session.color, type, 1); - addChatMessage(game, session, `${session.name} randomly stole 1 ${type} from ` + `${victim.name}.`); - sendUpdateToPlayer(game, victim, { - private: victim.player, + addChatMessage(game, session, `${session.name} randomly stole 1 ${type} from ${victimSession.name}.`); + sendUpdateToPlayer(game, victimSession, { + private: victimSession.player, }); } debugChat(game, "After steal"); @@ -2343,8 +2352,8 @@ const stealResource = (game: any, session: any, color: any): string | undefined return undefined; }; -const buyDevelopment = (game: any, session: any): string | undefined => { - const player = session.player; +const buyDevelopment = (game: Game, session: Session): string | undefined => { + const player = session.player as Player; if (game.state !== "normal") { return `You cannot purchase a development card unless the game is active (${game.state}).`; @@ -2362,42 +2371,46 @@ const buyDevelopment = (game: any, session: any): string | undefined => { return `Robber is in action. You can not purchase until all Robber tasks are resolved.`; } - if (game.developmentCards.length < 1) { + if (!game.developmentCards || game.developmentCards.length < 1) { return `There are no more development cards!`; } - if (player.stone < 1 || player.wheat < 1 || player.sheep < 1) { + if ((player.stone || 0) < 1 || (player.wheat || 0) < 1 || (player.sheep || 0) < 1) { return `You have insufficient resources to purchase a development card.`; } - if (game.turn.developmentPurchased) { + if ((game.turn as any).developmentPurchased) { return `You have already purchased a development card this turn.`; } debugChat(game, "Before development purchase"); addActivity(game, session, `${session.name} purchased a development card.`); addChatMessage(game, session, `${session.name} spent 1 stone, 1 wheat, 1 sheep to purchase a development card.`); - player.stone--; - player.wheat--; - player.sheep--; + player.stone = (player.stone || 0) - 1; + player.wheat = (player.wheat || 0) - 1; + player.sheep = (player.sheep || 0) - 1; player.resources = 0; - player.developmentCards++; + player.developmentCards = (player.developmentCards || 0) + 1; ["wheat", "brick", "sheep", "stone", "wood"].forEach((resource) => { - player.resources += player[resource]; + player.resources = (player.resources || 0) + ((player as any)[resource] || 0); }); debugChat(game, "After development purchase"); - const card = game.developmentCards.pop(); - card.turn = game.turns ? game.turns - 1 : 0; - player.development.push(card); + const card = (game.developmentCards || []).pop(); + if (card) { + (card as any).turn = game.turns ? game.turns - 1 : 0; + if (!player.development) player.development = [] as any; + (player.development as any).push(card as any); + } if (isRuleEnabled(game, "most-developed")) { if ( - player.development.length >= 5 && - (!game.mostDeveloped || player.developmentCards > game.players[game.mostDeveloped].developmentCards) + (player.development?.length || 0) >= 5 && + (!(game as any)["mostDeveloped"] || + (player.developmentCards || 0) > (game.players[(game as any)["mostDeveloped"] as string]?.developmentCards || 0)) ) { - if (game.mostDeveloped !== session.color) { - game.mostDeveloped = session.color; - game.mostPortCount = player.developmentCards; + if ((game as any)["mostDeveloped"] !== session.color) { + (game as any)["mostDeveloped"] = session.color; + (game as any)["mostPortCount"] = player.developmentCards; addChatMessage( game, session, @@ -2414,15 +2427,15 @@ const buyDevelopment = (game: any, session: any): string | undefined => { sendUpdateToPlayers(game, { chat: game.chat, activities: game.activities, - mostDeveloped: game.mostDeveloped, + mostDeveloped: (game as any)["mostDeveloped"], players: getFilteredPlayers(game), }); return undefined; }; -const playCard = (game: any, session: any, card: any): string | undefined => { - const name = session.name, - player = session.player; +const playCard = (game: Game, session: Session, card: any): string | undefined => { + const name = session.name; + const player = session.player as Player; if (game.state !== "normal") { return `You cannot purchase a development card unless the game is active (${game.state}).`; @@ -2438,19 +2451,21 @@ const playCard = (game: any, session: any, card: any): string | undefined => { return `Robber is in action. You can not play a card until all Robber tasks are resolved.`; } - card = player.development.find((item: any) => item.type == card.type && item.card == card.card && !item.card.played); + card = (player.development || []).find( + (item: any) => item.type == card.type && item.card == card.card && !(item.card as any).played + ); if (!card) { return `The card you want to play was not found in your hand!`; } - if (player.playedCard === game.turns && card.type !== "vp") { + if ((player as any)["playedCard"] === game.turns && card.type !== "vp") { return `You can only play one development card per turn!`; } /* Check if this is a victory point */ if (card.type === "vp") { - let points = player.points; - player.development.forEach((item: any) => { + let points = player.points || 0; + (player.development || []).forEach((item: any) => { if (item.type === "vp") { points++; } @@ -2464,13 +2479,13 @@ const playCard = (game: any, session: any, card: any): string | undefined => { if (card.type === "progress") { switch (card.card) { case "road-1": - case "road-2": - const allowed = Math.min(player.roads, 2); + case "road-2": { + const allowed = Math.min(player.roads || 0, 2); if (!allowed) { addChatMessage(game, session, `${session.name} played a Road Building card, but has no roads to build.`); break; } - let roads = getValidRoads(game, session.color); + const roads = getValidRoads(game, session.color as string); if (roads.length === 0) { addChatMessage( game, @@ -2479,9 +2494,9 @@ const playCard = (game: any, session: any, card: any): string | undefined => { ); break; } - game.turn.active = "road-building"; - game.turn.free = true; - game.turn.freeRoads = allowed; + game.turn.active = "road-building" as any; + (game.turn as any).free = true; + (game.turn as any).freeRoads = allowed; addChatMessage( game, session, @@ -2489,9 +2504,10 @@ const playCard = (game: any, session: any, card: any): string | undefined => { ); setForRoadPlacement(game, roads); break; + } case "monopoly": game.turn.actions = ["select-resources"]; - game.turn.active = "monopoly"; + game.turn.active = "monopoly" as any; addActivity( game, session, @@ -2500,7 +2516,7 @@ const playCard = (game: any, session: any, card: any): string | undefined => { break; case "year-of-plenty": game.turn.actions = ["select-resources"]; - game.turn.active = "year-of-plenty"; + game.turn.active = "year-of-plenty" as any; addActivity(game, session, `${session.name} played the Year of Plenty card.`); break; default: @@ -2508,23 +2524,27 @@ const playCard = (game: any, session: any, card: any): string | undefined => { break; } } - card.played = true; - player.playedCard = game.turns; + (card as any).played = true; + (player as any)["playedCard"] = game.turns; if (card.type === "army") { - player.army++; + (player as any)["army"] = ((player as any)["army"] || 0) + 1; addChatMessage(game, session, `${session.name} played a Knight and must move the robber!`); - if (player.army > 2 && (!game.largestArmy || game.players[game.largestArmy].army < player.army)) { - if (game.largestArmy !== session.color) { - game.largestArmy = session.color; - game.largestArmySize = player.army; - addChatMessage(game, session, `${session.name} now has the largest army (${player.army})!`); + if ( + (player as any)["army"] > 2 && + (!(game as any)["largestArmy"] || + ((game.players as any)[(game as any)["largestArmy"]]?.army || 0) < (player as any)["army"]) + ) { + if ((game as any)["largestArmy"] !== session.color) { + (game as any)["largestArmy"] = session.color; + (game as any)["largestArmySize"] = (player as any)["army"]; + addChatMessage(game, session, `${session.name} now has the largest army (${(player as any)["army"]})!`); } } game.turn.robberInAction = true; - delete game.turn.placedRobber; + delete (game.turn as any).placedRobber; addChatMessage( game, null, @@ -2532,12 +2552,10 @@ const playCard = (game: any, session: any, card: any): string | undefined => { `but a new robber has returned and ${session.name} must now place them.` ); game.turn.actions = ["place-robber", "playing-knight"]; - game.turn.limits = { pips: [] }; + game.turn.limits = { pips: [] } as any; for (let i = 0; i < 19; i++) { - if (i === game.robber) { - continue; - } - game.turn.limits.pips.push(i); + if (i === game.robber) continue; + (game.turn.limits as any).pips.push(i); } } @@ -2547,8 +2565,8 @@ const playCard = (game: any, session: any, card: any): string | undefined => { sendUpdateToPlayers(game, { chat: game.chat, activities: game.activities, - largestArmy: game.largestArmy, - largestArmySize: game.largestArmySize, + largestArmy: (game as any)["largestArmy"], + largestArmySize: (game as any)["largestArmySize"], turn: game.turn, players: getFilteredPlayers(game), }); diff --git a/server/routes/games/types.ts b/server/routes/games/types.ts index 09ccd1d..bfffb3f 100644 --- a/server/routes/games/types.ts +++ b/server/routes/games/types.ts @@ -27,6 +27,9 @@ export interface Player { status?: string; developmentCards?: number; development?: DevelopmentCard[]; + turnNotice?: string; + turnStart?: number; + totalTime?: number; [key: string]: any; // allow incremental fields until fully typed } @@ -82,6 +85,7 @@ export interface Session { live?: boolean; lastActive?: number; keepAlive?: any; + connected?: boolean; _initialSnapshotSent?: boolean; _getBatch?: { fields: Set; timer?: any }; _pendingMessage?: any; @@ -107,6 +111,8 @@ export interface Game { players: Record; sessions: Record; unselected?: any[]; + turnTimer?: any; + debug?: boolean; active?: number; rules?: any; step?: number;