From da90e012fc29960239e0fc2803e17d27e96b256d Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Sat, 11 Oct 2025 14:15:18 -0700 Subject: [PATCH] Lots of typescript fixes --- server/routes/games.ts | 1668 +++++++++++++++++++--------------- server/routes/games/types.ts | 33 +- 2 files changed, 981 insertions(+), 720 deletions(-) diff --git a/server/routes/games.ts b/server/routes/games.ts index ac83df8..6449b9b 100755 --- a/server/routes/games.ts +++ b/server/routes/games.ts @@ -1,10 +1,10 @@ -import express from "express"; -import crypto from "crypto"; -import { layout, staticData } from "../util/layout"; -import basePath from "../basepath"; -import { types, debug, all, info, INCOMING_GET_BATCH_MS } from "./games/constants"; +import express from 'express'; +import crypto from 'crypto'; +import { layout, staticData } from '../util/layout'; +import basePath from '../basepath'; +import { types, debug, all, info, INCOMING_GET_BATCH_MS } from './games/constants'; -import { getValidRoads, getValidCorners, isRuleEnabled } from "../util/validLocations"; +import { getValidRoads, getValidCorners, isRuleEnabled } from '../util/validLocations'; import { Player, Game, @@ -18,7 +18,7 @@ import { PLAYER_COLORS, RESOURCE_TYPES, ResourceType, -} from "./games/types"; +} from './games/types'; import { audio, join as webrtcJoin, @@ -26,7 +26,7 @@ import { handleRelayICECandidate, handleRelaySessionDescription, broadcastPeerStateUpdate, -} from "./webrtc-signaling"; +} from './webrtc-signaling'; const router = express.Router(); @@ -48,13 +48,14 @@ import { queueSend, shuffle, resetTurnTimer, -} from "./games/helpers"; -import { gameDB, games } from "./games/store"; -import { transientState } from "./games/sessionState"; -import { createGame, resetGame, setBeginnerGame } from "./games/gameFactory"; -import { getVictoryPointRule, setRules, supportedRules } from "./games/rules"; -import { pickRobber } from "./games/robber"; -import { IncomingMessage } from "./games/types"; +} from './games/helpers'; +import { gameDB, games } from './games/store'; +import { transientState } from './games/sessionState'; +import { createGame, resetGame, setBeginnerGame } from './games/gameFactory'; +import { getVictoryPointRule, setRules, supportedRules } from './games/rules'; +import { pickRobber } from './games/robber'; +import { IncomingMessage } from './games/types'; +import { newTurn } from './games/turnFactory'; const processTies = (players: Player[]): boolean => { /* Sort the players into buckets based on their @@ -210,19 +211,22 @@ const processGameOrder = (game: Game, player: Player, dice: number): string | un addChatMessage( game, null, - `Player order set to ` + players.map((player) => `${player.position}: ${player.name}`).join(", ") + `.` + `Player order set to ` + + players.map(player => `${player.position}: ${player.name}`).join(', ') + + `.` ); - game.playerOrder = players.map((player) => player.color); - game.state = "initial-placement"; - game.direction = "forward"; - const first = players[0]; - game.turn = { - name: first?.name as string, - color: first?.color as PlayerColor, - }; + game.playerOrder = players.map(player => player.color); + game.state = 'initial-placement'; + game.direction = 'forward'; + const first = players[0]!; + game.turn = newTurn(first); setForSettlementPlacement(game, getValidCorners(game)); - addActivity(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`); + addActivity( + game, + null, + `${game.robberName} Robber Robinson entered the scene as the nefarious robber!` + ); addChatMessage(game, null, `Initial settlement placement has started!`); addChatMessage(game, null, `It is ${game.turn.name}'s turn to place a settlement.`); @@ -239,12 +243,16 @@ const processGameOrder = (game: Game, player: Player, dice: number): string | un }; const processVolcano = (game: Game, session: Session, dice: number[]) => { - const name = session.name ? session.name : "Unnamed"; + const name = session.name ? session.name : 'Unnamed'; void session.player; const volcano = layout.tiles.findIndex((_tile, index) => { const tileIndex = game.tileOrder ? game.tileOrder[index] : undefined; - return typeof tileIndex === "number" && !!staticData.tiles && staticData.tiles[tileIndex]?.type === "desert"; + return ( + typeof tileIndex === 'number' && + !!staticData.tiles && + staticData.tiles[tileIndex]?.type === 'desert' + ); }); /* Find the volcano tile */ @@ -252,7 +260,7 @@ const processVolcano = (game: Game, session: Session, dice: number[]) => { addChatMessage(game, session, `${name} rolled ${dice[0]} for the Volcano!`); game.dice = dice; - game.state = "normal"; + game.state = 'normal'; if (volcano !== -1 && layout.tiles?.[volcano] && dice && dice[0] !== undefined) { const corners = layout.tiles[volcano].corners; @@ -260,27 +268,31 @@ const processVolcano = (game: Game, session: Session, dice: number[]) => { game.turn.volcano = corners[dice[0] % 6]; } } - const volcanoIdx = typeof game.turn.volcano === "number" ? game.turn.volcano : undefined; + const volcanoIdx = typeof game.turn.volcano === 'number' ? game.turn.volcano : undefined; const corner = volcanoIdx !== undefined ? game.placements.corners[volcanoIdx] : undefined; - if (corner && corner.color && corner.color !== "unassigned") { + if (corner && corner.color && corner.color !== 'unassigned') { const player = game.players[corner.color]; if (player) { - if (corner.type === "city") { + if (corner.type === 'city') { if (player.settlements && player.settlements > 0) { addChatMessage(game, null, `${player.name}'s city was wiped back to just a settlement!`); player.cities = (player.cities || 0) + 1; player.settlements = (player.settlements || 0) - 1; - corner.type = "settlement"; + corner.type = 'settlement'; } else { - addChatMessage(game, null, `${player.name}'s city was wiped out, and they have no settlements to replace it!`); - corner.type = "none"; - corner.color = "unassigned"; + addChatMessage( + game, + null, + `${player.name}'s city was wiped out, and they have no settlements to replace it!` + ); + corner.type = 'none'; + corner.color = 'unassigned'; player.cities = (player.cities || 0) + 1; } } else { addChatMessage(game, null, `${player.name}'s settlement was wiped out!`); - corner.type = "none"; - corner.color = "unassigned"; + corner.type = 'none'; + corner.color = 'unassigned'; player.settlements = (player.settlements || 0) + 1; } } @@ -300,27 +312,27 @@ const processVolcano = (game: Game, session: Session, dice: number[]) => { const roll = (game: Game, session: Session, dice?: number[] | undefined): string | undefined => { const player = session.player as Player, - name = session.name ? session.name : "Unnamed"; + name = session.name ? session.name : 'Unnamed'; if (!dice) { dice = [Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6)]; } switch (game.state) { - case "lobby": + case 'lobby': /* currently not available as roll is only after color is * set for players */ addChatMessage(game, session, `${name} rolled ${dice[0]}.`); sendUpdateToPlayers(game, { chat: game.chat }); return undefined; - case "game-order": + case 'game-order': game.startTime = Date.now(); addChatMessage(game, session, `${name} rolled ${dice[0]}.`); - if (typeof dice[0] !== "number") { + if (typeof dice[0] !== 'number') { return `Invalid roll value.`; } return processGameOrder(game, player, dice[0]); - case "normal": + case 'normal': if (game.turn.color !== session.color) { return `It is not your turn.`; } @@ -330,7 +342,7 @@ const roll = (game: Game, session: Session, dice?: number[] | undefined): string processRoll(game, session, dice); return; - case "volcano": + case 'volcano': if (game.turn.color !== session.color) { return `It is not your turn.`; } @@ -338,7 +350,7 @@ const roll = (game: Game, session: Session, dice?: number[] | undefined): string return `You can not roll for the Volcano until all players have mined their resources.`; } /* Only use the first die for the Volcano roll */ - if (typeof dice[0] !== "number") { + if (typeof dice[0] !== 'number') { return `Invalid roll value.`; } processVolcano(game, session, [dice[0]]); @@ -369,7 +381,7 @@ interface ResourceCount { bank: number; } -type Received = Record; +type Received = Record; const distributeResources = (game: Game, roll: number): void => { console.log(`Roll: ${roll}`); @@ -381,7 +393,7 @@ const distributeResources = (game: Game, roll: number): void => { /* TODO: Fix so it isn't hard coded to "wheat" and instead is the correct resource given * the resource distribution in shuffeled */ matchedTiles.push({ - type: "wheat", + type: 'wheat', robber: game.robber === pos, index: pos, corners: [], @@ -395,16 +407,16 @@ const distributeResources = (game: Game, roll: number): void => { const receives: Received = {} as Received; /* Initialize all fields of 'receives' to zero so we can safely increment any * matched tile */ - PLAYER_COLORS.forEach((color) => { + PLAYER_COLORS.forEach(color => { receives[color] = {} as ResourceCount; - RESOURCE_TYPES.forEach((type) => { + RESOURCE_TYPES.forEach(type => { receives[color]![type] = 0; }); }); /* Ensure robber entry exists */ if (!receives.robber) { receives.robber = {} as ResourceCount; - RESOURCE_TYPES.forEach((type) => { + RESOURCE_TYPES.forEach(type => { receives.robber[type] = 0; }); } @@ -432,15 +444,16 @@ const distributeResources = (game: Game, roll: number): void => { return; } - const count = active.type === "settlement" ? 1 : 2; + const count = active.type === 'settlement' ? 1 : 2; if (!tile.robber) { if (resource && resource.type) { // ensure receives entry for this color exists if (!receives[active.color]) { receives[active.color] = {} as ResourceCount; - RESOURCE_TYPES.forEach((t) => (receives[active.color]![t] = 0)); + RESOURCE_TYPES.forEach(t => (receives[active.color]![t] = 0)); } - receives[active.color]![resource.type] = (receives[active.color]![resource.type] || 0) + count; + receives[active.color]![resource.type] = + (receives[active.color]![resource.type] || 0) + count; } } else { const victim = game.players[active.color]; @@ -453,14 +466,15 @@ const distributeResources = (game: Game, roll: number): void => { if (resource && resource.type) { if (!receives[active.color]) { receives[active.color] = {} as ResourceCount; - RESOURCE_TYPES.forEach((t) => (receives[active.color]![t] = 0)); + RESOURCE_TYPES.forEach(t => (receives[active.color]![t] = 0)); } - receives[active.color]![resource.type] = (receives[active.color]![resource.type] || 0) + count; + receives[active.color]![resource.type] = + (receives[active.color]![resource.type] || 0) + count; } } else { // If resource.type is falsy, skip. if (resource && resource.type) { - trackTheft(game, active.color, "robber", resource.type, count); + trackTheft(game, active.color, 'robber', resource.type, count); receives.robber[resource.type] = (receives.robber[resource.type] || 0) + count; } } @@ -469,31 +483,31 @@ const distributeResources = (game: Game, roll: number): void => { }); const robberList: string[] = []; - PLAYER_COLORS.forEach((color) => { + PLAYER_COLORS.forEach(color => { const entry = receives[color]; if (!(entry.wood || entry.brick || entry.sheep || entry.wheat || entry.stone)) { return; } const messageParts: string[] = []; let s: Session | undefined; - RESOURCE_TYPES.forEach((type) => { + RESOURCE_TYPES.forEach(type => { if (entry[type] === 0) { return; } - if (color !== "robber") { + if (color !== 'robber') { s = sessionFromColor(game, color); if (s && s.player) { switch (type) { - case "wood": - case "brick": - case "sheep": - case "wheat": - case "stone": + case 'wood': + case 'brick': + case 'sheep': + case 'wheat': + case 'stone': s.player[type] += entry[type]; s.player.resources += entry[type]; break; default: - console.error("Invalid resource type to distribute:", type); + console.error('Invalid resource type to distribute:', type); return; } messageParts.push(`${entry[type]} ${type}`); @@ -504,12 +518,16 @@ const distributeResources = (game: Game, roll: number): void => { }); if (s) { - addChatMessage(game, s, `${s.name} receives ${messageParts.join(", ")} for pip ${roll}.`); + addChatMessage(game, s, `${s.name} receives ${messageParts.join(', ')} for pip ${roll}.`); } }); if (robberList.length) { - addChatMessage(game, null, `That pesky ${game.robberName} Robber Roberson stole ${robberList.join(", ")}!`); + addChatMessage( + game, + null, + `That pesky ${game.robberName} Robber Roberson stole ${robberList.join(', ')}!` + ); } }; @@ -527,11 +545,11 @@ const processRoll = (game: Game, session: Session, dice: number[]): any => { game.turn.roll = sum; if (game.turn.roll !== 7) { - let synonym = isRuleEnabled(game, "twelve-and-two-are-synonyms") && (sum === 2 || sum === 12); + let synonym = isRuleEnabled(game, 'twelve-and-two-are-synonyms') && (sum === 2 || sum === 12); distributeResources(game, game.turn.roll); - if (isRuleEnabled(game, "twelve-and-two-are-synonyms")) { + if (isRuleEnabled(game, 'twelve-and-two-are-synonyms')) { if (sum === 12) { addChatMessage( game, @@ -552,7 +570,7 @@ const processRoll = (game: Game, session: Session, dice: number[]): any => { } } - if (isRuleEnabled(game, "roll-double-roll-again")) { + if (isRuleEnabled(game, 'roll-double-roll-again')) { if (dice[0] === dice[1]) { addChatMessage( game, @@ -564,10 +582,10 @@ const processRoll = (game: Game, session: Session, dice: number[]): any => { } } - if (isRuleEnabled(game, "volcano")) { + if (isRuleEnabled(game, 'volcano')) { if ( - sum === parseInt(game.rules["volcano"].number) || - (synonym && (game.rules["volcano"].number === 2 || game.rules["volcano"].number === 12)) + sum === parseInt(game.rules['volcano'].number) || + (synonym && (game.rules['volcano'].number === 2 || game.rules['volcano'].number === 12)) ) { addChatMessage( game, @@ -576,21 +594,25 @@ const processRoll = (game: Game, session: Session, dice: number[]): any => { Volcano is erupting!` ); - game.state = "volcano"; + game.state = 'volcano'; let count = 0; - if (game.rules["volcano"].gold) { + if (game.rules['volcano'].gold) { game.turn.select = {}; const volcanoIdx = layout.tiles.findIndex((_tile, index) => { const tileIndex = game.tileOrder ? game.tileOrder[index] : undefined; - return typeof tileIndex === "number" && !!staticData.tiles && staticData.tiles[tileIndex]?.type === "desert"; + return ( + typeof tileIndex === 'number' && + !!staticData.tiles && + staticData.tiles[tileIndex]?.type === 'desert' + ); }); if (volcanoIdx !== -1 && layout.tiles[volcanoIdx]) { const vCorners = layout.tiles[volcanoIdx].corners || []; vCorners.forEach((index: number) => { const corner = game.placements.corners[index]; - if (corner && corner.color && corner.color !== "unassigned") { + if (corner && corner.color && corner.color !== 'unassigned') { if (!game.turn.select) { game.turn.select = {} as Record; } @@ -601,13 +623,13 @@ const processRoll = (game: Game, session: Session, dice: number[]): any => { game.turn.select[corner.color] = 0; } game.turn.select[corner.color] = - (game.turn.select[corner.color] || 0) + (corner.type === "settlement" ? 1 : 2); - count += corner.type === "settlement" ? 1 : 2; + (game.turn.select[corner.color] || 0) + (corner.type === 'settlement' ? 1 : 2); + count += corner.type === 'settlement' ? 1 : 2; } }); } console.log(`Volcano! - `, { - mode: "gold", + mode: 'gold', selected: game.turn.select, }); if (count) { @@ -631,8 +653,8 @@ const processRoll = (game: Game, session: Session, dice: number[]): any => { null, `House rule 'Volcanoes have minerals' activated. Players must select which resources to receive from the Volcano!` ); - game.turn.actions = ["select-resources"]; - game.turn.active = "volcano"; + game.turn.actions = ['select-resources']; + game.turn.active = 'volcano'; } } else { addChatMessage( @@ -668,14 +690,18 @@ const processRoll = (game: Game, session: Session, dice: number[]): any => { /* ROBBER Robber Robinson! */ game.turn.robberInAction = true; - delete game.turn.placedRobber; + game.turn.placedRobber = false; const mustDiscard = []; for (let id in game.sessions) { const player = game.sessions[id]?.player; if (player) { let discard = - (player.stone || 0) + (player.wheat || 0) + (player.brick || 0) + (player.wood || 0) + (player.sheep || 0); + (player.stone || 0) + + (player.wheat || 0) + + (player.brick || 0) + + (player.wood || 0) + + (player.sheep || 0); if (discard > 7) { discard = Math.floor(discard / 2); player.mustDiscard = discard; @@ -687,9 +713,13 @@ const processRoll = (game: Game, session: Session, dice: number[]): any => { } if (mustDiscard.length === 0) { - addChatMessage(game, null, `ROBBER! ${game.robberName} Robber Roberson has fled, and no one had to discard!`); + addChatMessage( + game, + null, + `ROBBER! ${game.robberName} Robber Roberson has fled, and no one had to discard!` + ); addChatMessage(game, null, `A new robber has arrived and must be placed by ${game.turn.name}.`); - game.turn.actions = ["place-robber"]; + game.turn.actions = ['place-robber']; game.turn.limits = { pips: [] }; for (let i = 0; i < staticData.tiles.length; i++) { if (i === game.robber) { @@ -698,7 +728,7 @@ const processRoll = (game: Game, session: Session, dice: number[]): any => { game.turn.limits.pips.push(i); } } else { - mustDiscard.forEach((player) => { + mustDiscard.forEach(player => { addChatMessage( game, null, @@ -736,8 +766,8 @@ const getSession = (game: Game, id: string): Session => { game.sessions[id] = { id: id, short: `[${id.substring(0, 8)}]`, - name: "", - color: "unassigned", + name: '', + color: 'unassigned', lastActive: Date.now(), live: true, }; @@ -758,7 +788,7 @@ const getSession = (game: Game, id: string): Session => { continue; } // Treat the explicit "unassigned" sentinel as not set for expiring sessions - if ((_session.color && _session.color !== "unassigned") || _session.name || _session.player) { + if ((_session.color && _session.color !== 'unassigned') || _session.name || _session.player) { continue; } if (_id === id) { @@ -779,7 +809,7 @@ const getSession = (game: Game, id: string): Session => { const loadGame = async (id: string): Promise => { if (/^\.|\//.exec(id)) { - throw Error("Invalid game ID"); + throw Error('Invalid game ID'); } if (id in games) { @@ -821,35 +851,40 @@ const loadGame = async (id: string): Promise => { /* Clear out cached names from player colors and rebuild them * from the information in the saved game sessions */ for (let color in game.players) { - game.players[color]!.name = ""; - game.players[color]!.status = "Not active"; + game.players[color]!.name = ''; + game.players[color]!.status = 'Not active'; } /* Reconnect session player colors to the player objects */ game.unselected = []; for (let id in game.sessions) { const session = game.sessions[id]!; - if (session.name && session.color && session.color !== "unassigned" && session.color in game.players) { + if ( + session.name && + session.color && + session.color !== 'unassigned' && + session.color in game.players + ) { session.player = game.players[session.color]!; session.player.name = session.name; - session.player.status = "Active"; + session.player.status = 'Active'; session.player.live = false; } else { - session.color = "unassigned"; + session.color = 'unassigned'; delete session.player; } session.live = false; /* Populate the 'unselected' list from the session table */ - if ((!session.color || session.color === "unassigned") && session.name) { + if ((!session.color || session.color === 'unassigned') && session.name) { game.unselected.push(session); } } /* Reconstruct turn.limits if in initial-placement state and limits are missing */ if ( - game.state === "initial-placement" && + game.state === 'initial-placement' && game.turn && (!game.turn.limits || Object.keys(game.turn.limits).length === 0) ) { @@ -857,23 +892,29 @@ const loadGame = async (id: string): Promise => { const currentColor = game.turn.color; // Check if we need to place a settlement (no action or place-settlement action) - if (!game.turn.actions || game.turn.actions.length === 0 || game.turn.actions.indexOf("place-settlement") !== -1) { + if ( + !game.turn.actions || + game.turn.actions.length === 0 || + game.turn.actions.indexOf('place-settlement') !== -1 + ) { // During initial placement, settlements may be placed on any valid corner // (they are not constrained by existing roads), so do not filter by color. setForSettlementPlacement(game, getValidCorners(game)); console.log( - `${info}: Set turn limits for settlement placement (${game.turn.limits?.corners?.length || 0} valid corners)` + `${info}: Set turn limits for settlement placement (${ + game.turn.limits?.corners?.length || 0 + } valid corners)` ); } // Check if we need to place a road - else if (game.turn.actions.indexOf("place-road") !== -1) { + else if (game.turn.actions.indexOf('place-road') !== -1) { // Find the most recently placed settlement by the current player let mostRecentSettlementIndex = -1; if (game.placements && game.placements.corners) { // Look for settlements of the current player's color for (let i = game.placements.corners.length - 1; i >= 0; i--) { const corner = game.placements.corners[i]; - if (corner && corner.color === currentColor && corner.type === "settlement") { + if (corner && corner.color === currentColor && corner.type === 'settlement') { mostRecentSettlementIndex = i; break; } @@ -890,7 +931,9 @@ const loadGame = async (id: string): Promise => { // Fallback: Allow all valid roads for this player const roads = getValidRoads(game, currentColor); setForRoadPlacement(game, roads); - console.log(`${info}: Set turn limits for road placement (fallback: ${roads.length} valid roads)`); + console.log( + `${info}: Set turn limits for road placement (fallback: ${roads.length} valid roads)` + ); } } } @@ -909,9 +952,9 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a void color; switch (action) { - case "rules": - const rule = value.replace(/=.*$/, ""); - if (rule === "list") { + case 'rules': + const rule = value.replace(/=.*$/, ''); + if (rule === 'list') { const rules: any = {}; for (let key in supportedRules) { if (game.rules[key]) { @@ -922,17 +965,17 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a } return JSON.stringify(rules, null, 2); } - let values = value.replace(/^.*=/, "").split(","); + let values = value.replace(/^.*=/, '').split(','); const rulesObj: Record = {}; rulesObj[rule] = {}; - values.forEach((keypair) => { - let [key, val] = keypair.split(":"); + values.forEach(keypair => { + let [key, val] = keypair.split(':'); let parsed: any = val; - if (val === "true") { + if (val === 'true') { parsed = true; - } else if (val === "false") { + } else if (val === 'false') { parsed = false; - } else if (typeof val === "string" && !isNaN(parseInt(val))) { + } else if (typeof val === 'string' && !isNaN(parseInt(val))) { parsed = parseInt(val); } if (rule && key) rulesObj[rule][key] = parsed; @@ -941,15 +984,15 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a setRules(game, undefined, rulesObj); break; - case "debug": - if (parseInt(value) === 0 || value === "false") { + case 'debug': + if (parseInt(value) === 0 || value === 'false') { delete game.debug; } else { game.debug = true; } break; - case "give": + case 'give': parts = value.match(/^([^-]+)(-(.*))?$/); if (!parts) { return `Unable to parse give request.`; @@ -971,7 +1014,7 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a let done = true; switch (type) { - case "road": + case 'road': error = canGiveBuilding(game); if (error) { return error; @@ -980,15 +1023,19 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a if (session.player.roads === 0) { return `Player ${game.turn.name} does not have any more roads to give.`; } - let roads = getValidRoads(game, session.color === "unassigned" ? "" : session.color); + let roads = getValidRoads(game, session.color === 'unassigned' ? '' : session.color); if (roads.length === 0) { return `There are no valid locations for ${game.turn.name} to place a road.`; } game.turn.free = true; setForRoadPlacement(game, roads); - addChatMessage(game, null, `Admin gave a road to ${game.turn.name}.` + `They must now place the road.`); + addChatMessage( + game, + null, + `Admin gave a road to ${game.turn.name}.` + `They must now place the road.` + ); break; - case "city": + case 'city': error = canGiveBuilding(game); if (error) { return error; @@ -997,15 +1044,23 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a if (session.player.cities === 0) { return `Player ${game.turn.name} does not have any more cities to give.`; } - corners = getValidCorners(game, session.color === "unassigned" ? "" : session.color, "settlement"); + corners = getValidCorners( + game, + session.color === 'unassigned' ? '' : session.color, + 'settlement' + ); if (corners.length === 0) { return `There are no valid locations for ${game.turn.name} to place a settlement.`; } game.turn.free = true; setForCityPlacement(game, corners); - addChatMessage(game, null, `Admin gave a city to ${game.turn.name}. ` + `They must now place the city.`); + addChatMessage( + game, + null, + `Admin gave a city to ${game.turn.name}. ` + `They must now place the city.` + ); break; - case "settlement": + case 'settlement': error = canGiveBuilding(game); if (error) { return error; @@ -1014,7 +1069,7 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a if (session.player.settlements === 0) { return `Player ${game.turn.name} does not have any more settlements to give.`; } - corners = getValidCorners(game, session.color === "unassigned" ? "" : session.color); + corners = getValidCorners(game, session.color === 'unassigned' ? '' : session.color); if (corners.length === 0) { return `There are no valid locations for ${game.turn.name} to place a settlement.`; } @@ -1026,11 +1081,11 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a `Admin gave a settlment to ${game.turn.name}. ` + `They must now place the settlement.` ); break; - case "wheat": - case "sheep": - case "wood": - case "stone": - case "brick": + case 'wheat': + case 'sheep': + case 'wood': + case 'stone': + case 'brick': const count = parseInt(String(card)); session.player[type] += count; session.resources += count; @@ -1044,28 +1099,30 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a break; } - const index = game.developmentCards.findIndex((item: any) => item.card.toString() === card && item.type === type); + const index = game.developmentCards.findIndex( + (item: any) => item.card.toString() === card && item.type === type + ); if (index === -1) { console.log({ card, type }, game.developmentCards); return `Unable to find ${type}-${card} in the current deck of development cards.`; } - let tmp = game.developmentCards.splice(index, 1)[0]; - if (tmp) { - (tmp as any)["turn"] = game.turns ? game.turns - 1 : 0; - session.player.development.push(tmp); - } + const tmp = game.developmentCards.splice(index, 1)[0]!; + tmp.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; - case "cards": - let results = game.developmentCards.map((card: any) => `${card.type}-${card.card}`).join(", "); + case 'cards': + let results = game.developmentCards + .map((card: any) => `${card.type}-${card.card}`) + .join(', '); return results; - case "roll": + case 'roll': let diceRaw = (query.dice || Math.ceil(Math.random() * 6)).toString(); - let dice = diceRaw.split(",").map((die: string) => parseInt(die)); + let dice = diceRaw.split(',').map((die: string) => parseInt(die)); console.log({ dice }); if (!value) { @@ -1073,28 +1130,32 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a } switch (value) { - case "orange": - color = "O"; + case 'orange': + color = 'O'; break; - case "red": - color = "R"; + case 'red': + color = 'R'; break; - case "blue": - color = "B"; + case 'blue': + color = 'B'; break; - case "white": - color = "W"; + case 'white': + color = 'W'; break; } - if (corner && corner.color && corner.color !== "unassigned") { + if (corner && corner.color && corner.color !== 'unassigned') { const player = game.players ? game.players[corner.color] : undefined; if (player) { - if (corner.type === "city") { + if (corner.type === 'city') { if (player.settlements) { - addChatMessage(game, null, `${player.name}'s city was wiped back to just a settlement!`); + addChatMessage( + game, + null, + `${player.name}'s city was wiped back to just a settlement!` + ); player.cities = (player.cities || 0) + 1; player.settlements = (player.settlements || 1) - 1; - corner.type = "settlement"; + corner.type = 'settlement'; } else { addChatMessage( game, @@ -1122,9 +1183,9 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a } break; - case "pass": + 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; @@ -1139,30 +1200,34 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a addChatMessage(game, null, `It is ${next.name}'s turn.`); break; - case "kick": + case 'kick': switch (value) { - case "orange": - color = "O"; + case 'orange': + color = 'O'; break; - case "red": - color = "R"; + case 'red': + color = 'R'; break; - case "blue": - color = "B"; + case 'blue': + color = 'B'; break; - case "white": - color = "W"; + case 'white': + color = 'W'; break; } - if (corner && corner.color && corner.color !== "unassigned") { + if (corner && corner.color && corner.color !== 'unassigned') { const player = game.players[corner.color]; if (player) { - if (corner.type === "city") { + if (corner.type === 'city') { if (player.settlements && player.settlements > 0) { - addChatMessage(game, null, `${player.name}'s city was wiped back to just a settlement!`); + addChatMessage( + game, + null, + `${player.name}'s city was wiped back to just a settlement!` + ); player.cities = (player.cities || 0) + 1; player.settlements = (player.settlements || 0) - 1; - corner.type = "settlement"; + corner.type = 'settlement'; } else { addChatMessage( game, @@ -1183,20 +1248,20 @@ const adminCommands = (game: Game, action: string, value: string, query: any): a } break; - case "state": - if (game.state !== "lobby") { + case 'state': + if (game.state !== 'lobby') { return `Game already started.`; } if (!game.active || game.active < 2) { return `Not enough players in game to start.`; } - game.state = "game-order"; + game.state = 'game-order'; /* Delete any non-played colors from the player map; reduces all * code that would otherwise have to filter out players by checking * the 'Not active' state of player.status */ for (let key in game.players) { const p = game.players[key]; - if (!p || p.status !== "Active") { + if (!p || p.status !== 'Active') { delete game.players[key]; } } @@ -1212,7 +1277,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und if (session.name === name) { return; /* no-op */ } - if (session.color !== "unassigned") { + if (session.color !== 'unassigned') { return `You cannot change your name while you have a color selected.`; } @@ -1220,7 +1285,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und return `You can not set your name to nothing!`; } - if (name.toLowerCase() === "the bank") { + if (name.toLowerCase() === 'the bank') { return `You cannot play as the bank!`; } @@ -1252,7 +1317,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und message = `A new player has entered the lobby as ${name}.`; } else { if (rejoin) { - if (session.color !== "unassigned") { + if (session.color !== 'unassigned') { message = `${name} has reconnected to the game.`; } else { message = `${name} has rejoined the lobby.`; @@ -1272,7 +1337,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und 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(); @@ -1287,7 +1352,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und addChatMessage(game, null, message); /* Rebuild the unselected list */ - if (session.color === "unassigned") { + if (session.color === 'unassigned') { console.log(`${info}: Adding ${session.name} to the unselected`); } game.unselected = []; @@ -1318,16 +1383,16 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und const colorToWord = (color: string): string => { switch (color) { - case "O": - return "orange"; - case "W": - return "white"; - case "B": - return "blue"; - case "R": - return "red"; + case 'O': + return 'orange'; + case 'W': + return 'white'; + case 'B': + return 'blue'; + case 'R': + return 'red'; default: - return ""; + return ''; } }; @@ -1354,7 +1419,7 @@ const setPlayerColor = (game: Game, session: Session, color: PlayerColor): strin return `You may only select a player when you have set your name.`; } - if (game.state !== "lobby") { + if (game.state !== 'lobby') { return `You may only select a player when the game is in the lobby.`; } @@ -1369,7 +1434,7 @@ const setPlayerColor = (game: Game, session: Session, color: PlayerColor): strin if (!candidate) { return `An invalid player selection was attempted.`; } - if (candidate.status !== "Not active") { + if (candidate.status !== 'Not active') { return `${candidate.name} already has ${colorToWord(color)}`; } } @@ -1380,16 +1445,17 @@ const setPlayerColor = (game: Game, session: Session, color: PlayerColor): strin /* Deselect currently active player for this session */ clearPlayer(session.player); // remove the player association - delete (session as any).player; + delete session.player; const old_color = session.color; - session.color = "unassigned"; + session.color = 'unassigned'; active--; /* If the player is not selecting a color, then return */ if (!color) { - const msg = String(session.name || "") + " is no longer " + String(colorToWord(String(old_color))); + const msg = + String(session.name || '') + ' is no longer ' + String(colorToWord(String(old_color))); addChatMessage(game, null, msg); - if (!game.unselected) game.unselected = [] as any[]; + if (!game.unselected) game.unselected = []; game.unselected.push(session); game.active = active; if (active === 1) { @@ -1397,7 +1463,7 @@ const setPlayerColor = (game: Game, session: Session, color: PlayerColor): strin } sendUpdateToPlayer(game, session, { name: session.name, - color: "", + color: '', live: session.live, private: session.player, }); @@ -1417,7 +1483,7 @@ const setPlayerColor = (game: Game, session: Session, color: PlayerColor): strin session.live = true; const picked = game.players[color]; if (picked) { - (session as any).player = picked; + session.player = picked; picked.name = session.name; picked.status = `Active`; picked.lastActive = Date.now(); @@ -1440,7 +1506,7 @@ const setPlayerColor = (game: Game, session: Session, color: PlayerColor): strin unselected.push(s); } } - if (!game.unselected) game.unselected = [] as any[]; + if (!game.unselected) game.unselected = []; if (unselected.length !== game.unselected.length) { game.unselected = unselected; update.unselected = getFilteredUnselected(game); @@ -1465,9 +1531,14 @@ const setPlayerColor = (game: Game, session: Session, color: PlayerColor): strin return undefined; }; -const processCorner = (game: Game, color: string, cornerIndex: number, placedCorner: CornerPlacement): number => { +const processCorner = ( + game: Game, + color: string, + cornerIndex: number, + placedCorner: CornerPlacement +): number => { /* If this corner is allocated and isn't assigned to the walking color, skip it */ - if (placedCorner.color && placedCorner.color !== "unassigned" && placedCorner.color !== color) { + if (placedCorner.color && placedCorner.color !== 'unassigned' && placedCorner.color !== color) { return 0; } /* If this corner is already being walked, skip it */ @@ -1506,7 +1577,7 @@ const buildCornerGraph = ( set: any ): void => { /* If this corner is allocated and isn't assigned to the walking color, skip it */ - if (placedCorner.color && placedCorner.color !== "unassigned" && placedCorner.color !== color) { + if (placedCorner.color && placedCorner.color !== 'unassigned' && placedCorner.color !== color) { return; } /* If this corner is already being walked, skip it */ @@ -1523,7 +1594,12 @@ const buildCornerGraph = ( }); }; -const processRoad = (game: Game, color: string, roadIndex: number, placedRoad: RoadPlacement): number => { +const processRoad = ( + game: Game, + color: string, + roadIndex: number, + placedRoad: RoadPlacement +): number => { /* If this road isn't assigned to the walking color, skip it */ if (placedRoad.color !== color) { return 0; @@ -1549,7 +1625,13 @@ const processRoad = (game: Game, color: string, roadIndex: number, placedRoad: R return roadLength; }; -const buildRoadGraph = (game: Game, color: string, roadIndex: number, placedRoad: RoadPlacement, set: number[]) => { +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; @@ -1590,7 +1672,7 @@ const calculateRoadLengths = (game: Game, session: Session): void => { let currentLongest = game.longestRoad, currentLength = - currentLongest && typeof currentLongest === "string" && game.players[currentLongest] + currentLongest && typeof currentLongest === 'string' && game.players[currentLongest] ? game.players[currentLongest].longestRoad || -1 : -1; @@ -1609,10 +1691,20 @@ 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: { color: string; set: number[]; longestRoad?: number; longestStartSegment?: number }[] = []; + 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 && placedRoad.color !== "unassigned" && typeof placedRoad.color === "string") { + if ( + placedRoad && + placedRoad.color && + placedRoad.color !== 'unassigned' && + typeof placedRoad.color === 'string' + ) { let set: number[] = []; buildRoadGraph(game, placedRoad.color, roadIndex, placedRoad, set); if (set.length) { @@ -1621,7 +1713,7 @@ const calculateRoadLengths = (game: Game, session: Session): void => { } }); - if (debug.road) console.log("Graphs A:", graphs); + if (debug.road) console.log('Graphs A:', graphs); clearRoadWalking(game); graphs.forEach((graph: any) => { @@ -1638,16 +1730,16 @@ const calculateRoadLengths = (game: Game, session: Session): void => { }); }); - if (debug.road) console.log("Graphs B:", graphs); + if (debug.road) console.log('Graphs B:', graphs); if (debug.road) console.log( - "Pre update:", - game.placements.roads.filter((road) => road && (road as any).color) + 'Pre update:', + game.placements.roads.filter(road => road && road.color) ); for (let color in game.players) { - if (game.players[color]?.status === "Not active") { + if (game.players[color]?.status === 'Not active') { continue; } if (game.players[color]) { @@ -1661,12 +1753,16 @@ const calculateRoadLengths = (game: Game, session: Session): void => { if (!placedRoad) return; clearRoadWalking(game); const longestRoad = processRoad(game, placedRoad.color as string, roadIndex, placedRoad); - placedRoad["longestRoad"] = longestRoad; - if (placedRoad.color && placedRoad.color !== "unassigned" && typeof placedRoad.color === "string") { + placedRoad['longestRoad'] = longestRoad; + if ( + placedRoad.color && + placedRoad.color !== 'unassigned' && + typeof placedRoad.color === 'string' + ) { const player = game.players[placedRoad.color]; if (player) { - const prevVal = player["longestRoad"] || 0; - player["longestRoad"] = Math.max(prevVal, longestRoad); + const prevVal = player['longestRoad'] || 0; + player['longestRoad'] = Math.max(prevVal, longestRoad); } } }); @@ -1676,7 +1772,7 @@ const calculateRoadLengths = (game: Game, session: Session): void => { if (debug.road) console.log( - "Post update:", + 'Post update:', game.placements.roads.filter((road: any) => road && road.color) ); @@ -1684,8 +1780,8 @@ const calculateRoadLengths = (game: Game, session: Session): void => { if (debug.road) console.log(currentLongest, currentLength); - if (currentLongest && typeof currentLongest === "string" && game.players[currentLongest]) { - const playerLongest = game.players[currentLongest]["longestRoad"] || 0; + if (currentLongest && typeof currentLongest === 'string' && game.players[currentLongest]) { + const playerLongest = game.players[currentLongest]['longestRoad'] || 0; if (playerLongest < currentLength) { const prevSession = sessionFromColor(game, currentLongest as string); if (prevSession) { @@ -1699,7 +1795,7 @@ const calculateRoadLengths = (game: Game, session: Session): void => { let longestPlayers: Player[] = []; for (let key in game.players) { const player = game.players[key]; - if (!player || player.status === "Not active") { + if (!player || player.status === 'Not active') { continue; } const pLen = player.longestRoad || 0; @@ -1721,14 +1817,22 @@ const calculateRoadLengths = (game: Game, session: Session): void => { if (longestPlayers[0] && longestPlayers[0].color) { if (game.longestRoad !== longestPlayers[0].color) { game.longestRoad = longestPlayers[0].color; - addChatMessage(game, session, `${longestPlayers[0].name} now has the longest ` + `road (${longestRoad})!`); + addChatMessage( + game, + session, + `${longestPlayers[0].name} now has the longest ` + `road (${longestRoad})!` + ); } } } else { if (checkForTies) { game.longestRoadLength = longestRoad; - const names = longestPlayers.map((player) => player.name); - addChatMessage(game, session, `${names.join(", ")} are tied for longest ` + `road (${longestRoad})!`); + const names = longestPlayers.map(player => player.name); + addChatMessage( + game, + session, + `${names.join(', ')} are tied for longest ` + `road (${longestRoad})!` + ); } /* Do not reset the longest road! Current Longest is still longest! */ } @@ -1739,12 +1843,12 @@ const calculateRoadLengths = (game: Game, session: Session): void => { }; const isCompatibleOffer = (player: Player, offer: Offer): boolean => { - const isBank = (offer as any)["name"] === "The bank"; + const isBank = offer.name === 'The bank'; - const playerGetsLen = (player as any)["gets"] ? (player as any)["gets"].length : 0; - const playerGivesLen = (player as any)["gives"] ? (player as any)["gives"].length : 0; - const offerGetsLen = (offer as any)["gets"] ? (offer as any)["gets"].length : 0; - const offerGivesLen = (offer as any)["gives"] ? (offer as any)["gives"].length : 0; + const playerGetsLen = player.gets.length; + const playerGivesLen = player.gives.length; + const offerGetsLen = offer.gets.length; + const offerGivesLen = offer.gives.length; let valid = playerGetsLen === offerGivesLen && playerGivesLen === offerGetsLen; @@ -1755,32 +1859,28 @@ const isCompatibleOffer = (player: Player, offer: Offer): boolean => { console.log( { - player: "Submitting player", - gets: (player as any)["gets"], - gives: (player as any)["gives"], + player: 'Submitting player', + gets: player.gets, + gives: player.gives, }, { - name: (offer as any)["name"], - gets: (offer as any)["gets"], - gives: (offer as any)["gives"], + name: offer.name, + gets: offer.gets, + gives: offer.gives, } ); - for (const get of (player as any)["gets"] || []) { - if ( - !(offer as any)["gives"] || - !(offer as any)["gives"].some((item: any) => (item.type === get.type || isBank) && item.count === get.count) - ) { + for (const get of player.gets) { + if (!offer.gives.some(item => (item.type === get.type || isBank) && item.count === get.count)) { valid = false; break; } } if (valid) { - for (const give of (player as any)["gives"] || []) { + for (const give of player.gives) { if ( - !(offer as any)["gets"] || - !(offer as any)["gets"].some((item: any) => (item.type === give.type || isBank) && item.count === give.count) + !offer.gets.some(item => (item.type === give.type || isBank) && item.count === give.count) ) { valid = false; break; @@ -1791,30 +1891,23 @@ const isCompatibleOffer = (player: Player, offer: Offer): boolean => { }; const isSameOffer = (player: Player, offer: Offer): boolean => { - const isBank = (offer as any)["name"] === "The bank"; + const isBank = offer.name === 'The bank'; if (isBank) { return false; } - if (!(player as any)["gets"] || !(player as any)["gives"] || !(offer as any)["gets"] || !(offer as any)["gives"]) { + if (player.gets.length !== offer.gets.length || player.gives.length !== offer.gives.length) { return false; } - if ( - (player as any)["gets"].length !== (offer as any)["gets"].length || - (player as any)["gives"].length !== (offer as any)["gives"].length - ) { - return false; - } - - for (const get of (player as any)["gets"]) { - if (!(offer as any)["gets"].find((item: any) => item.type === get.type && item.count === get.count)) { + for (const get of player.gets) { + if (!offer.gets.find(item => item.type === get.type && item.count === get.count)) { return false; } } - for (const give of (player as any)["gives"]) { - if (!(offer as any)["gives"].find((item: any) => item.type === give.type && item.count === give.count)) { + for (const give of player.gives) { + if (!offer.gives.find(item => item.type === give.type && item.count === give.count)) { return false; } } @@ -1825,7 +1918,7 @@ const isSameOffer = (player: Player, offer: Offer): boolean => { /* Verifies player can meet the offer */ const checkPlayerOffer = (_game: Game, player: Player, offer: Offer): string | undefined => { let error: string | undefined = undefined; - const name = player.name || "Unknown"; + const name = player.name || 'Unknown'; console.log({ checkPlayerOffer: { @@ -1842,10 +1935,9 @@ const checkPlayerOffer = (_game: Game, player: Player, offer: Offer): string | u }, }); - for (const give of (offer as any)["gives"] || []) { + for (const give of offer.gives) { if (error) break; - - if (!(give.type in (player as any))) { + if (!RESOURCE_TYPES.includes(give.type)) { error = `${give.type} is not a valid resource!`; break; } @@ -1855,25 +1947,25 @@ const checkPlayerOffer = (_game: Game, player: Player, offer: Offer): string | u break; } - if ((player as any)[give.type] < give.count) { + if (player[give.type] < give.count) { error = `${name} does do not have ${give.count} ${give.type}!`; break; } - if (((offer as any)["gets"] || []).find((get: any) => give.type === get.type)) { + if (offer.gets.find(get => give.type === get.type)) { error = `${name} can not give and get the same resource type!`; break; } } if (!error) { - for (const get of (offer as any)["gets"] || []) { + for (const get of offer.gets) { if (error) break; if (get.count <= 0) { error = `${get.count} must be more than 0!`; break; } - if (((offer as any)["gives"] || []).find((give: any) => get.type === give.type)) { + if (offer.gives.find(give => get.type === give.type)) { error = `${name} can not give and get the same resource type!`; break; } @@ -1884,15 +1976,16 @@ const checkPlayerOffer = (_game: Game, player: Player, offer: Offer): string | u }; const canMeetOffer = (player: Player, offer: Offer): boolean => { - for (const get of (offer as any)["gets"] || []) { - if (get.type === "bank") { - const giveType = - (player as any)["gives"] && (player as any)["gives"][0] ? (player as any)["gives"][0].type : undefined; - if (!giveType) return false; - if ((player as any)[giveType] < get.count || get.count <= 0) { + for (const get of offer.gets) { + if (get.type === 'bank') { + if (!player.gives[0]) { return false; } - } else if ((player as any)[get.type] < get.count || get.count <= 0) { + const giveType = player.gives[0].type; + if (player[giveType] < get.count || get.count <= 0) { + return false; + } + } else if (player[get.type] < get.count || get.count <= 0) { return false; } } @@ -1939,15 +2032,15 @@ const setGameFromSignature = (game: Game, border: string, pip: string, tile: str const offerToString = (offer: Offer): string => { return ( - (offer.gives || []).map((item) => `${item.count} ${item.type}`).join(", ") + - " in exchange for " + - (offer.gets || []).map((item) => `${item.count} ${item.type}`).join(", ") + (offer.gives || []).map(item => `${item.count} ${item.type}`).join(', ') + + ' in exchange for ' + + (offer.gets || []).map(item => `${item.count} ${item.type}`).join(', ') ); }; -router.put("/:id/:action/:value?", async (req, res) => { +router.put('/:id/:action/:value?', async (req, res) => { const { action, id } = req.params, - value = req.params.value ? req.params.value : ""; + value = req.params.value ? req.params.value : ''; console.log(`PUT games/${id}/${action}/${value}`); const game = await loadGame(id); @@ -1956,10 +2049,10 @@ router.put("/:id/:action/:value?", async (req, res) => { return res.status(404).send(error); } - let error = "Invalid request"; + let error = 'Invalid request'; - if ("private-token" in req.headers) { - if (req.headers["private-token"] !== req.app.get("admin")) { + if ('private-token' in req.headers) { + if (req.headers['private-token'] !== req.app.get('admin')) { error = `Invalid admin credentials.`; } else { error = adminCommands(game, action, value, req.query); @@ -1980,17 +2073,17 @@ const startTrade = (game: Game, session: Session): string | undefined => { return `You cannot start trading negotiations when it is not your turn.`; } /* Clear any free gives if the player begins trading */ - if (game.turn.free) { - delete game.turn.free; - } - game.turn.actions = ["trade"]; + game.turn.free = false; + game.turn.actions = ['trade']; game.turn.limits = {}; for (let key in game.players) { const p = game.players[key]; if (!p) continue; - (p as any)["gives"] = []; - (p as any)["gets"] = []; - delete (p as any)["offerRejected"]; + p.gives = []; + p.gets = []; + PLAYER_COLORS.forEach(color => { + p.offerRejected[color] = false; + }); } addActivity(game, session, `${session.name} requested to begin trading negotiations.`); return undefined; @@ -2018,9 +2111,8 @@ const processOffer = (game: Game, session: Session, offer: Offer): string | unde return `You already have a pending offer submitted for ${offerToString(offer)}.`; } - (player as any)["gives"] = (offer as any)["gives"]; - (player as any)["gets"] = (offer as any)["gets"]; - (player as any)["offerRejected"] = {}; + player.gives = offer.gives; + player.gets = offer.gets; if (game.turn.color === session.color) { game.turn.offer = offer; @@ -2029,14 +2121,11 @@ const processOffer = (game: Game, session: Session, offer: Offer): string | unde /* If this offer matches what another player wants, clear rejection on that other player's offer */ for (const color in game.players) { if (color === session.color) continue; - const other = game.players[color]; - if (!other) continue; - if ((other as any)["status"] !== "Active") continue; + const other = game.players[color]!; + if (other.status !== 'Active') continue; /* Comparison reverses give/get order */ - if (isSameOffer(other, { gives: (offer as any)["gets"], gets: (offer as any)["gives"] } as Offer)) { - if ((other as any)["offerRejected"]) { - delete (other as any)["offerRejected"][session.color as string]; - } + if (isSameOffer(other, { name: other.name, color: other.color, gives: offer.gets, gets: offer.gives })) { + other.offerRejected[session.color] = false; } } @@ -2046,17 +2135,10 @@ const processOffer = (game: Game, session: Session, offer: Offer): string | unde const rejectOffer = (game: Game, session: Session, offer: Offer): void => { /* If the active player rejected an offer, they rejected another player */ - const other = game.players[(offer as any)["color"] as string]; - if (!other) return; - if (!(other as any)["offerRejected"]) { - (other as any)["offerRejected"] = {}; - } - (other as any)["offerRejected"][session.color as string] = true; - if (!session.player) session.player = {} as Player; - if (!(session.player as any)["offerRejected"]) { - (session.player as any)["offerRejected"] = {}; - } - (session.player as any)["offerRejected"][(offer as any)["color"] as string] = true; + const other = game.players[offer.color]!; + + other.offerRejected[session.color] = true; + session.player!.offerRejected[offer.color] = true; addActivity(game, session, `${session.name} rejected ${other.name}'s offer.`); }; @@ -2068,7 +2150,7 @@ const acceptOffer = (game: Game, session: Session, offer: Offer): string | undef return `Only the active player can accept an offer.`; } - let target: any = undefined; + let target: Player | undefined = undefined; console.log({ description: offerToString(offer) }); @@ -2079,136 +2161,147 @@ const acceptOffer = (game: Game, session: Session, offer: Offer): string | undef if ( !isCompatibleOffer(player, { - name: (offer as any)["name"], - gives: (offer as any)["gets"], - gets: (offer as any)["gives"], - } as Offer) + name: offer.name, + color: offer.color, + gives: offer.gets, + gets: offer.gives, + }) ) { - return `Unfortunately, trades were re-negotiated in transit and 1 ` + `the deal is invalid!`; + return `Unfortunately, trades were re-negotiated in transit and the deal is invalid!`; } /* Verify that the offer sent by the active player matches what * the latest offer was that was received by the requesting player */ - if (!(offer as any)["name"] || (offer as any)["name"] !== "The bank") { - target = game.players[(offer as any)["color"] as string]; + if (offer.name !== 'The bank') { + target = game.players[offer.color]; if (!target) return `Invalid trade target.`; - if ((target as any)["offerRejected"] && (offer as any)["color"] in (target as any)["offerRejected"]) { + if ( + target.offerRejected[offer.color] + ) { return `${target.name} rejected this offer.`; } - if (!isCompatibleOffer(target as Player, offer)) { + if (!isCompatibleOffer(target, offer)) { return `Unfortunately, trades were re-negotiated in transit and ` + `the deal is invalid!`; } warning = checkPlayerOffer( game, - target as Player, + target, { + name: target.name, + color: target.color, gives: offer.gets, gets: offer.gives, - } as Offer + } ); if (warning) { return warning; } - if (!isSameOffer(target as Player, { gives: (offer as any)["gets"], gets: (offer as any)["gives"] } as Offer)) { + if ( + !isSameOffer( + target, + { name: target.name, color: target.color, gives: offer.gets, gets: offer.gives } + ) + ) { console.log({ target, offer }); return `These terms were not agreed to by ${target.name}!`; } - if (!canMeetOffer(target as Player, player as any)) { + if (!canMeetOffer(target, player)) { return `${target.name} cannot meet the terms.`; } - } else { - target = offer; } - debugChat(game, "Before trade"); + debugChat(game, 'Before trade'); /* Transfer goods */ - for (const item of (offer as any)["gets"] || []) { - if ((target as any)["name"] !== "The bank") { - (target as any)[item.type] -= item.count; - (target as any).resources -= item.count; + for (const item of offer.gets) { + if (target) { + target[item.type] -= item.count; + target.resources -= item.count; } - (player as any)[item.type] += item.count; - (player as any).resources += item.count; + player[item.type] += item.count; + player.resources += item.count; } - for (const item of (offer as any)["gives"] || []) { - if ((target as any)["name"] !== "The bank") { - (target as any)[item.type] += item.count; - (target as any).resources += item.count; + for (const item of offer.gives) { + if (target) { + target[item.type] += item.count; + target.resources += item.count; } - (player as any)[item.type] -= item.count; - (player as any).resources -= item.count; + player[item.type] -= item.count; + player.resources -= item.count; } - const from = (offer as any)["name"] === "The bank" ? "the bank" : (offer as any)["name"]; - addChatMessage(game, session, `${session.name} traded ` + ` ${offerToString(offer)} ` + `from ${from}.`); + const from = offer.name === 'The bank' ? 'the bank' : offer.name; + addChatMessage( + game, + session, + `${session.name} traded ` + ` ${offerToString(offer)} ` + `from ${from}.` + ); addActivity(game, session, `${session.name} accepted a trade from ${from}.`); - delete (game.turn as any)["offer"]; if (target) { - delete (target as any).gives; - delete (target as any).gets; + target.gives = []; + target.gets = []; } if (session.player) { - delete (session.player as any)["gives"]; - delete (session.player as any)["gets"]; + session.player.gives = []; + session.player.gets = []; } - delete (game.turn as any)["offer"]; + game.turn.offer = null; - debugChat(game, "After trade"); + debugChat(game, 'After trade'); - /* Debug!!! */ - for (const key in game.players) { - const p = game.players[key]; - if (!p) continue; - if ((p as any)["state"] !== "Active") { - continue; - } - types.forEach((type) => { - if ((p as any)[type] < 0) { - throw new Error(`Player resources are below zero! BUG BUG BUG!`); - } - }); - } game.turn.actions = []; return undefined; }; const trade = (game: Game, session: Session, action: string, offer?: Offer): string | undefined => { - if (game.state !== "normal") { + if (game.state !== 'normal') { return `Game not in correct state to begin trading.`; } - if (!game.turn.actions || game.turn.actions.indexOf("trade") === -1) { + if (!session.player) { + return `You must select a color before trading.`; + } + + if (!game.turn.actions || game.turn.actions.indexOf('trade') === -1) { return startTrade(game, session); } /* Only the active player can cancel trading */ - if (action === "cancel") { + if (action === 'cancel') { return cancelTrade(game, session); } /* Any player can make an offer */ - if (action === "offer") { - return processOffer(game, session, offer as Offer); + if (action === 'offer') { + if (!offer) { + return `No offer was specified.`; + } + return processOffer(game, session, offer); } /* Any player can reject an offer */ - if (action === "reject") { - rejectOffer(game, session, offer as Offer); + if (action === 'reject') { + if (!offer) { + return `No offer was specified.`; + } + rejectOffer(game, session, offer); return undefined; } /* Only the active player can accept an offer */ - if (action === "accept") { - if (offer && (offer as any)["name"] === "The bank") { - if (!session.player) session.player = {} as Player; - (session.player as any)["gets"] = (offer as any)["gets"]; - (session.player as any)["gives"] = (offer as any)["gives"]; + if (action === 'accept') { + if (!offer) { + return `No offer was specified.`; } - return acceptOffer(game, session, offer as Offer); + + if (offer.name === 'The bank') { + session.player.gets = offer.gets; + session.player.gives = offer.gives; + } + return acceptOffer(game, session, offer); } return undefined; }; @@ -2218,42 +2311,41 @@ const clearTimeNotice = (game: Game, session: Session): string | undefined => { /* benign state; don't alert the user */ //return `You have not been idle.`; } - if (session.player) session.player.turnNotice = ""; + if (session.player) session.player.turnNotice = ''; sendUpdateToPlayer(game, session, { private: session.player, }); return undefined; }; -const pass = (game: any, session: any): string | undefined => { +const pass = (game: Game, session: Session): string | undefined => { const name = session.name; if (game.turn.name !== name) { return `You cannot pass when it isn't your turn.`; } + if (!session.player) { + return `You must select a color before passing.`; + } + /* If the current turn is a robber placement, and everyone has * discarded, set the limits for where the robber can be placed */ if (game.turn && game.turn.robberInAction) { return `Robber is in action. Turn can not stop until all Robber tasks are resolved.`; } - if (game.state === "volcano") { + if (game.state === 'volcano') { return `You cannot not stop turn until you have finished the Volcano tasks.`; } const next = getNextPlayerSession(game, session.name); - if (!next) { + if (!next || !next.player) { return `Unable to find the next player to pass to.`; } session.player.totalTime += Date.now() - session.player.turnStart; - session.player.turnNotice = ""; - game.turn = { - name: next.name, - color: next.color, - }; - if (next.player) { - next.player.turnStart = Date.now(); - } + session.player.turnNotice = ''; + game.turn = newTurn(next.player); + next.player.turnStart = Date.now(); startTurnTimer(game, next); game.turns++; addActivity(game, session, `${name} passed their turn.`); @@ -2278,9 +2370,9 @@ const pass = (game: any, session: any): string | undefined => { const placeRobber = (game: Game, session: Session, robber: number | string): string | undefined => { const name = session.name; - let robberIdx = typeof robber === "string" ? parseInt(robber) : robber; + let robberIdx = typeof robber === 'string' ? parseInt(robber) : robber; - if (game.state !== "normal" && game.turn.roll !== 7) { + if (game.state !== 'normal' && game.turn.roll !== 7) { return `You cannot place robber unless 7 was rolled!`; } if (game.turn.name !== name) { @@ -2290,7 +2382,7 @@ const placeRobber = (game: Game, session: Session, robber: number | string): str for (const color in game.players) { const p = game.players[color]; if (!p) continue; - if (p.status === "Not active") continue; + if (p.status === 'Not active') continue; if ((p.mustDiscard || 0) > 0) { return `You cannot place the robber until everyone has discarded!`; } @@ -2300,10 +2392,14 @@ const placeRobber = (game: Game, session: Session, robber: number | string): str return `You must move the robber to a new location!`; } game.robber = robberIdx as number; - (game.turn as any).placedRobber = true; + game.turn.placedRobber = true; pickRobber(game); - addActivity(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`); + addActivity( + game, + null, + `${game.robberName} Robber Robinson entered the scene as the nefarious robber!` + ); const targets: Array<{ color: string; name: string }> = []; layout.tiles?.[robberIdx as number]?.corners?.forEach((cornerIndex: number) => { @@ -2312,17 +2408,17 @@ const placeRobber = (game: Game, session: Session, robber: number | string): str active && active.color && active.color !== game.turn.color && - targets.findIndex((item) => item.color === active.color) === -1 + targets.findIndex(item => item.color === active.color) === -1 ) { targets.push({ color: active.color, - name: game.players?.[active.color]?.name || "", + name: game.players?.[active.color]?.name || '', }); } }); if (targets.length) { - game.turn.actions = ["steal-resource"]; + game.turn.actions = ['steal-resource']; game.turn.limits = { players: targets } as any; } else { game.turn.actions = []; @@ -2354,7 +2450,7 @@ const placeRobber = (game: Game, session: Session, robber: number | string): str }; const stealResource = (game: Game, session: Session, color: string): string | undefined => { - if (!game.turn.actions || game.turn.actions.indexOf("steal-resource") === -1) { + 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!`; } const playersLimit = (game.turn.limits as any)?.players || []; @@ -2369,16 +2465,20 @@ const stealResource = (game: Game, session: Session, color: string): string | un const victimPlayer = victimSession.player as Player; const sessionPlayer = session.player as Player; const cards: string[] = []; - ["wheat", "brick", "sheep", "stone", "wood"].forEach((field: string) => { + ['wheat', 'brick', 'sheep', 'stone', 'wood'].forEach((field: string) => { for (let i = 0; i < ((victimPlayer as any)[field] || 0); i++) { cards.push(field); } }); - debugChat(game, "Before steal"); + debugChat(game, 'Before steal'); if (cards.length === 0) { - addChatMessage(game, session, `${victimSession.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 = {} as any; } else { @@ -2395,14 +2495,18 @@ const stealResource = (game: Game, session: Session, color: string): string | un adjustResources(sessionPlayer as Player, { [t]: 1 }); game.turn.actions = []; game.turn.limits = {} as any; - trackTheft(game, (victimSession as any).color || "", session.color, type, 1); + trackTheft(game, (victimSession as any).color || '', session.color, type, 1); - addChatMessage(game, session, `${session.name} randomly stole 1 ${type} from ${victimSession.name}.`); + addChatMessage( + game, + session, + `${session.name} randomly stole 1 ${type} from ${victimSession.name}.` + ); sendUpdateToPlayer(game, victimSession, { private: victimSession.player, }); } - debugChat(game, "After steal"); + debugChat(game, 'After steal'); game.turn.robberInAction = false; @@ -2421,7 +2525,7 @@ const stealResource = (game: Game, session: Session, color: string): string | un const buyDevelopment = (game: Game, session: Session): string | undefined => { const player = session.player as Player; - if (game.state !== "normal") { + if (game.state !== 'normal') { return `You cannot purchase a development card unless the game is active (${game.state}).`; } @@ -2445,22 +2549,26 @@ const buyDevelopment = (game: Game, session: Session): string | undefined => { return `You have insufficient resources to purchase a development card.`; } - if ((game.turn as any).developmentPurchased) { + if (game.turn.developmentPurchased) { return `You have already purchased a development card this turn.`; } - debugChat(game, "Before development purchase"); + 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.`); + addChatMessage( + game, + session, + `${session.name} spent 1 stone, 1 wheat, 1 sheep to purchase a development card.` + ); 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 || 0) + 1; - ["wheat", "brick", "sheep", "stone", "wood"].forEach((resource) => { + ['wheat', 'brick', 'sheep', 'stone', 'wood'].forEach(resource => { player.resources = (player.resources || 0) + ((player as any)[resource] || 0); }); - debugChat(game, "After development purchase"); + debugChat(game, 'After development purchase'); const card = (game.developmentCards || []).pop(); if (card) { (card as any).turn = game.turns ? game.turns - 1 : 0; @@ -2468,15 +2576,16 @@ const buyDevelopment = (game: Game, session: Session): string | undefined => { (player.development as any).push(card as any); } - if (isRuleEnabled(game, "most-developed")) { + if (isRuleEnabled(game, 'most-developed')) { if ( (player.development?.length || 0) >= 5 && - (!(game as any)["mostDeveloped"] || - (player.developmentCards || 0) > (game.players[(game as any)["mostDeveloped"] as string]?.developmentCards || 0)) + (!(game as any)['mostDeveloped'] || + (player.developmentCards || 0) > + (game.players[(game as any)['mostDeveloped'] as string]?.developmentCards || 0)) ) { - if ((game as any)["mostDeveloped"] !== session.color) { - (game as any)["mostDeveloped"] = session.color; - (game as any)["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, @@ -2493,7 +2602,7 @@ const buyDevelopment = (game: Game, session: Session): string | undefined => { sendUpdateToPlayers(game, { chat: game.chat, activities: game.activities, - mostDeveloped: (game as any)["mostDeveloped"], + mostDeveloped: (game as any)['mostDeveloped'], players: getFilteredPlayers(game), }); return undefined; @@ -2503,7 +2612,7 @@ const playCard = (game: Game, session: Session, card: any): string | undefined = const name = session.name; const player = session.player as Player; - if (game.state !== "normal") { + if (game.state !== 'normal') { return `You cannot purchase a development card unless the game is active (${game.state}).`; } if (session.color !== game.turn.color) { @@ -2524,31 +2633,37 @@ const playCard = (game: Game, session: Session, card: any): string | undefined = return `The card you want to play was not found in your hand!`; } - if ((player as any)["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") { + if (card.type === 'vp') { let points = player.points || 0; (player.development || []).forEach((item: any) => { - if (item.type === "vp") { + if (item.type === 'vp') { points++; } }); if (points < getVictoryPointRule(game)) { - return `You can not play victory point cards until you can reach ${getVictoryPointRule(game)}!`; + return `You can not play victory point cards until you can reach ${getVictoryPointRule( + game + )}!`; } addChatMessage(game, session, `${name} played a Victory Point card.`); } - if (card.type === "progress") { + if (card.type === 'progress') { switch (card.card) { - case "road-1": - case "road-2": { + case 'road-1': + 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.`); + addChatMessage( + game, + session, + `${session.name} played a Road Building card, but has no roads to build.` + ); break; } const roads = getValidRoads(game, session.color as string); @@ -2560,9 +2675,9 @@ const playCard = (game: Game, session: Session, card: any): string | undefined = ); break; } - game.turn.active = "road-building" as any; - (game.turn as any).free = true; - (game.turn as any).freeRoads = allowed; + game.turn.active = 'road-building' as any; + game.turn.free = true; + game.turn.freeRoads = allowed; addChatMessage( game, session, @@ -2571,18 +2686,18 @@ const playCard = (game: Game, session: Session, card: any): string | undefined = setForRoadPlacement(game, roads); break; } - case "monopoly": - game.turn.actions = ["select-resources"]; - game.turn.active = "monopoly" as any; + case 'monopoly': + game.turn.actions = ['select-resources']; + game.turn.active = 'monopoly' as any; addActivity( game, session, `${session.name} played the Monopoly card, and is selecting their resource type to claim.` ); break; - case "year-of-plenty": - game.turn.actions = ["select-resources"]; - game.turn.active = "year-of-plenty" as any; + case 'year-of-plenty': + game.turn.actions = ['select-resources']; + game.turn.active = 'year-of-plenty' as any; addActivity(game, session, `${session.name} played the Year of Plenty card.`); break; default: @@ -2591,33 +2706,37 @@ const playCard = (game: Game, session: Session, card: any): string | undefined = } } (card as any).played = true; - (player as any)["playedCard"] = game.turns; + (player as any)['playedCard'] = game.turns; - if (card.type === "army") { - (player as any)["army"] = ((player as any)["army"] || 0) + 1; + if (card.type === '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 as any)["army"] > 2 && - (!(game as any)["largestArmy"] || - ((game.players as any)[(game as any)["largestArmy"]]?.army || 0) < (player as any)["army"]) + (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"]})!`); + 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 as any).placedRobber; + game.turn.placedRobber = false; addChatMessage( game, null, `The robber ${game.robberName} has fled before the power of the Knight, ` + `but a new robber has returned and ${session.name} must now place them.` ); - game.turn.actions = ["place-robber", "playing-knight"]; + game.turn.actions = ['place-robber', 'playing-knight']; game.turn.limits = { pips: [] } as any; for (let i = 0; i < staticData.tiles.length; i++) { if (i === game.robber) continue; @@ -2631,20 +2750,24 @@ const playCard = (game: Game, session: Session, card: any): string | undefined = sendUpdateToPlayers(game, { chat: game.chat, activities: game.activities, - largestArmy: (game as any)["largestArmy"], - largestArmySize: (game as any)["largestArmySize"], + largestArmy: (game as any)['largestArmy'], + largestArmySize: (game as any)['largestArmySize'], turn: game.turn, players: getFilteredPlayers(game), }); return undefined; }; -const placeSettlement = (game: Game, session: Session, index: number | string): string | undefined => { +const placeSettlement = ( + game: Game, + session: Session, + index: number | string +): string | undefined => { if (!session.player) return `You are not playing a player.`; const player: Player = session.player; - if (typeof index === "string") index = parseInt(index); + if (typeof index === 'string') index = parseInt(index); - if (game.state !== "initial-placement" && game.state !== "normal") { + if (game.state !== 'initial-placement' && game.state !== 'normal') { return `You cannot place a settlement unless the game is active (${game.state}).`; } @@ -2653,18 +2776,27 @@ const placeSettlement = (game: Game, session: Session, index: number | string): } /* index out of range... */ - if (!game.placements || game.placements.corners === undefined || game.placements.corners[index] === undefined) { + if ( + !game.placements || + game.placements.corners === undefined || + game.placements.corners[index] === undefined + ) { return `You have requested to place a settlement illegally!`; } /* If this is not a valid road in the turn limits, discard it */ - if (!game.turn || !game.turn.limits || !game.turn.limits.corners || game.turn.limits.corners.indexOf(index) === -1) { + if ( + !game.turn || + !game.turn.limits || + !game.turn.limits.corners || + game.turn.limits.corners.indexOf(index) === -1 + ) { return `You tried to cheat! You should not try to break the rules.`; } const corner = game.placements.corners[index]!; - if (corner.color && corner.color !== "unassigned") { + if (corner.color && corner.color !== 'unassigned') { const owner = game.players && game.players[corner.color]; - const ownerName = owner ? owner.name : "unknown"; + const ownerName = owner ? owner.name : 'unknown'; return `This location already has a settlement belonging to ${ownerName}!`; } @@ -2672,9 +2804,14 @@ const placeSettlement = (game: Game, session: Session, index: number | string): player.banks = []; } - if (game.state === "normal") { + if (game.state === 'normal') { if (!game.turn.free) { - if ((player.brick || 0) < 1 || (player.wood || 0) < 1 || (player.wheat || 0) < 1 || (player.sheep || 0) < 1) { + if ( + (player.brick || 0) < 1 || + (player.wood || 0) < 1 || + (player.wheat || 0) < 1 || + (player.sheep || 0) < 1 + ) { return `You have insufficient resources to build a settlement.`; } } @@ -2686,17 +2823,21 @@ const placeSettlement = (game: Game, session: Session, index: number | string): player.settlements = (player.settlements || 0) - 1; if (!game.turn.free) { - addChatMessage(game, session, `${session.name} spent 1 brick, 1 wood, 1 sheep, 1 wheat to purchase a settlement.`); + addChatMessage( + game, + session, + `${session.name} spent 1 brick, 1 wood, 1 sheep, 1 wheat to purchase a settlement.` + ); player.brick = (player.brick || 0) - 1; player.wood = (player.wood || 0) - 1; player.wheat = (player.wheat || 0) - 1; player.sheep = (player.sheep || 0) - 1; player.resources -= 4; } - delete game.turn.free; + game.turn.free = false; corner.color = session.color; - corner.type = "settlement"; + corner.type = 'settlement'; let bankType = undefined; const banks = layout.corners?.[index]?.banks; @@ -2709,20 +2850,24 @@ const placeSettlement = (game: Game, session: Session, index: number | string): console.log(`${session.short}: Bank ${bank}`); return; } - bankType = type === "bank" ? "3 of anything for 1 resource" : `2 ${type} for 1 resource`; + bankType = type === 'bank' ? '3 of anything for 1 resource' : `2 ${type} for 1 resource`; if (player.banks.indexOf(type) === -1) { player.banks.push(type); } player.ports++; - if (isRuleEnabled(game, "port-of-call")) { + if (isRuleEnabled(game, 'port-of-call')) { console.log(`Checking port-of-call`, player.ports, game.mostPorts); if (player.ports >= 3 && (!game.mostPorts || player.ports > game.mostPortCount)) { if (game.mostPorts !== session.color) { game.mostPorts = session.color; game.mostPortCount = player.ports; - addChatMessage(game, session, `${session.name} now has the most ports (${player.ports})!`); + addChatMessage( + game, + session, + `${session.name} now has the most ports (${player.ports})!` + ); } } } @@ -2731,17 +2876,21 @@ const placeSettlement = (game: Game, session: Session, index: number | string): game.turn.actions = []; game.turn.limits = {}; if (bankType) { - addActivity(game, session, `${session.name} placed a settlement by a maritime bank that trades ${bankType}.`); + addActivity( + game, + session, + `${session.name} placed a settlement by a maritime bank that trades ${bankType}.` + ); } else { addActivity(game, session, `${session.name} placed a settlement.`); } calculateRoadLengths(game, session); - } else if (game.state === "initial-placement") { - if (game.direction && game.direction === "backward") { + } else if (game.state === 'initial-placement') { + if (game.direction && game.direction === 'backward') { (session as any).initialSettlement = index; } - corner.color = session.color || ""; - corner.type = "settlement"; + corner.color = session.color || ''; + corner.type = 'settlement'; let bankType = undefined; const banks2 = layout.corners?.[index]?.banks; if (banks2 && banks2.length) { @@ -2752,7 +2901,7 @@ const placeSettlement = (game: Game, session: Session, index: number | string): if (!type) { return; } - bankType = type === "bank" ? "3 of anything for 1 resource" : `2 ${type} for 1 resource`; + bankType = type === 'bank' ? '3 of anything for 1 resource' : `2 ${type} for 1 resource`; if (player.banks.indexOf(type) === -1) { player.banks.push(type); } @@ -2768,7 +2917,11 @@ const placeSettlement = (game: Game, session: Session, index: number | string): `Next, they need to place a road.` ); } else { - addActivity(game, session, `${session.name} placed a settlement. ` + `Next, they need to place a road.`); + addActivity( + game, + session, + `${session.name} placed a settlement. ` + `Next, they need to place a road.` + ); } setForRoadPlacement(game, layout.corners?.[index]?.roads || []); } @@ -2807,7 +2960,11 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi return `You have requested to place a road illegally!`; } - if (!game.turn.limits || !game.turn.limits.roads || game.turn.limits.roads.indexOf(index) === -1) { + if ( + !game.turn.limits || + !game.turn.limits.roads || + game.turn.limits.roads.indexOf(index) === -1 + ) { return `You tried to cheat! You should not try to break the rules.`; } @@ -2816,7 +2973,7 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi return `This location already has a road belonging to ${game.players[road.color]?.name}!`; } - if (game.state === "normal") { + if (game.state === 'normal') { if (!game.turn.free) { if (player.brick < 1 || player.wood < 1) { return `You have insufficient resources to build a road.`; @@ -2833,49 +2990,53 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi player.wood--; player.resources -= 2; } - delete game.turn.free; + game.turn.free = false; } road.color = session.color; - road.type = "road"; + road.type = 'road'; player.roads--; /* Handle normal play road placement including Road Building free roads. * If the turn is a road-building action, decrement freeRoads and * only clear actions when no free roads remain. Otherwise, during * initial placement we advance the initial-placement sequence. */ - if (game.state === "normal") { + if (game.state === 'normal') { addActivity(game, session, `${session.name} placed a road.`); calculateRoadLengths(game, session); let resetLimits = true; - if (game.turn && (game.turn as any).active === "road-building") { - if ((game.turn as any).freeRoads !== undefined) { - (game.turn as any).freeRoads = (game.turn as any).freeRoads - 1; + if (game.turn && game.turn.active === 'road-building') { + if (game.turn.freeRoads !== undefined) { + game.turn.freeRoads -= 1; } - if ((game.turn as any).freeRoads === 0) { - delete (game.turn as any).free; - delete (game.turn as any).active; - delete (game.turn as any).freeRoads; + if (game.turn.freeRoads === 0) { + game.turn.free = false; + game.turn.active = null; + game.turn.freeRoads = 0; } const roads = getValidRoads(game, session.color as string); if (!roads || roads.length === 0) { - delete (game.turn as any).active; - delete (game.turn as any).freeRoads; - addActivity(game, session, `${session.name} has another road to play, but there are no more valid locations.`); - } else if ((game.turn as any).freeRoads > 0) { - (game.turn as any).free = true; + game.turn.active = null; + game.turn.freeRoads = 0; + addActivity( + game, + session, + `${session.name} has another road to play, but there are no more valid locations.` + ); + } else if (game.turn.freeRoads > 0) { + game.turn.free = true; setForRoadPlacement(game, roads); resetLimits = false; } } if (resetLimits) { - delete (game.turn as any).free; + game.turn.free = false; game.turn.actions = []; game.turn.limits = {}; } - } else if (game.state === "initial-placement") { + } else if (game.state === 'initial-placement') { const order: PlayerColor[] = game.playerOrder; const idx = order.indexOf(session.color); // defensive: if player not found, just clear actions and continue @@ -2883,21 +3044,21 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi game.turn.actions = []; game.turn.limits = {}; } else { - const direction = game.direction || "forward"; - if (direction === "forward") { + const direction = game.direction || 'forward'; + if (direction === 'forward') { if (idx === order.length - 1) { // Last player in forward pass: switch to backward and allow that // same last player to place a settlement to begin reverse pass. - game.direction = "backward"; + game.direction = 'backward'; const nextColor = order[order.length - 1]; if (nextColor && game.players && game.players[nextColor]) { const limits = getValidCorners(game); console.log( `${info}: initial-placement - ${ session.name - } placed road; direction=forward; next=${nextColor}; nextName=${game.players[nextColor].name}; corners=${ - limits ? limits.length : 0 - }` + } placed road; direction=forward; next=${nextColor}; nextName=${ + game.players[nextColor].name + }; corners=${limits ? limits.length : 0}` ); game.turn = { name: game.players[nextColor].name, @@ -2928,7 +3089,7 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi // backward if (idx === 0) { // Finished reverse initial placement; move to normal play and first player's turn - game.state = "normal"; + game.state = 'normal'; const firstColor = order[0]; if (firstColor && game.players && game.players[firstColor]) { game.turn = { @@ -2948,7 +3109,10 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi const p = s && s.player; const receives: Record = {} as Record; if (!p) continue; - if ((s as any).initialSettlement !== undefined && (s as any).initialSettlement !== null) { + if ( + (s as any).initialSettlement !== undefined && + (s as any).initialSettlement !== null + ) { layout.tiles.forEach((tile, tindex) => { if (tile.corners.indexOf((s as any).initialSettlement) !== -1) { const tileIdx = game.tileOrder ? game.tileOrder[tindex] : undefined; @@ -2969,11 +3133,11 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi // update player resources // guard against unknown resource keys switch (type) { - case "wood": - case "brick": - case "sheep": - case "wheat": - case "stone": + case 'wood': + case 'brick': + case 'sheep': + case 'wheat': + case 'stone': (p as any)[type] = ((p as any)[type] || 0) + cnt; p.resources = (p.resources || 0) + cnt; break; @@ -2990,7 +3154,7 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi addChatMessage( game, s, - `${s.name} receives ${messageParts.join(", ")} for initial settlement placement.` + `${s.name} receives ${messageParts.join(', ')} for initial settlement placement.` ); } } @@ -3004,9 +3168,9 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi console.log( `${info}: initial-placement - ${ session.name - } placed road; direction=backward; next=${nextColor}; nextName=${game.players[nextColor].name}; corners=${ - limits ? limits.length : 0 - }` + } placed road; direction=backward; next=${nextColor}; nextName=${ + game.players[nextColor].name + }; corners=${limits ? limits.length : 0}` ); game.turn = { name: game.players[nextColor].name, @@ -3036,7 +3200,7 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi activities: game.activities, players: getFilteredPlayers(game), state: game.state, - direction: (game as any)["direction"], + direction: (game as any)['direction'], longestRoad: game.longestRoad, longestRoadLength: game.longestRoadLength, }); @@ -3051,7 +3215,7 @@ const discard = (game: any, session: any, discards: Record): string let sum = 0; for (let type in discards) { const val = discards[type]; - const parsed = typeof val === "string" ? parseInt(val) : Number(val); + const parsed = typeof val === 'string' ? parseInt(val) : Number(val); if (player[type] < parsed) { return `You have requested to discard more ${type} than you have.`; } @@ -3089,8 +3253,12 @@ const discard = (game: any, session: any, discards: Record): string } if (move) { - addChatMessage(game, null, `Drat! A new robber has arrived and must be placed by ${game.turn.name}!`); - game.turn.actions = ["place-robber"]; + addChatMessage( + game, + null, + `Drat! A new robber has arrived and must be placed by ${game.turn.name}!` + ); + game.turn.actions = ['place-robber']; game.turn.limits = { pips: [] }; for (let i = 0; i < staticData.tiles.length; i++) { if (i === game.robber) { @@ -3113,7 +3281,7 @@ const discard = (game: any, session: any, discards: Record): string const buyRoad = (game: any, session: any): string | undefined => { const player = session.player; - if (game.state !== "normal") { + if (game.state !== 'normal') { return `You cannot purchase a development card unless the game is active (${game.state}).`; } if (session.color !== game.turn.color) { @@ -3150,20 +3318,28 @@ const buyRoad = (game: any, session: any): string | undefined => { const selectResources = (game: any, session: any, cards: string[]): string | undefined => { const player = session.player; void player; - if (!game || !game.turn || !game.turn.actions || game.turn.actions.indexOf("select-resources") === -1) { + if ( + !game || + !game.turn || + !game.turn.actions || + game.turn.actions.indexOf('select-resources') === -1 + ) { return `Please, let's not cheat. Ok?`; } - if (session.color !== game.turn.color && (!game.turn.select || !(session.color in game.turn.select))) { + if ( + session.color !== game.turn.color && + (!game.turn.select || !(session.color in game.turn.select)) + ) { console.log(session.color, game.turn.color, game.turn.select); return `It is not your turn! It is ${game.turn.name}'s turn.`; } let count = 2; - if (game.turn && game.turn.active === "monopoly") { + if (game.turn && game.turn.active === 'monopoly') { count = 1; } - if (game.state === "volcano") { + if (game.state === 'volcano') { console.log({ cards, turn: game.turn }); if (!game.turn.select) { count = 0; @@ -3189,11 +3365,11 @@ const selectResources = (game: any, session: any, cards: string[]): string | und const isValidCard = (type: string): boolean => { switch (type.trim()) { - case "wheat": - case "brick": - case "sheep": - case "stone": - case "wood": + case 'wheat': + case 'brick': + case 'sheep': + case 'stone': + case 'wood': return true; default: return false; @@ -3213,13 +3389,13 @@ const selectResources = (game: any, session: any, cards: string[]): string | und } switch (game.turn.active) { - case "monopoly": + case 'monopoly': const gave: string[] = [], type = String(cards[0]); let total = 0; for (let color in game.players) { const player = game.players[color]; - if (player.status === "Not active") { + if (player.status === 'Not active') { continue; } if (color === session.color) { @@ -3246,39 +3422,46 @@ const selectResources = (game: any, session: any, cards: string[]): string | und addChatMessage( game, session, - `${session.name} played Monopoly and selected ${display.join(", ")}. ` + - `Players ${gave.join(", ")}. In total, they received ${total} ${type}.` + `${session.name} played Monopoly and selected ${display.join(', ')}. ` + + `Players ${gave.join(', ')}. In total, they received ${total} ${type}.` ); } else { addActivity( game, session, - `${session.name} has chosen ${display.join(", ")}! Unfortunately, no players had that resource. Wa-waaaa.` + `${session.name} has chosen ${display.join( + ', ' + )}! Unfortunately, no players had that resource. Wa-waaaa.` ); } delete game.turn.active; game.turn.actions = []; break; - case "year-of-plenty": - cards.forEach((type) => { + case 'year-of-plenty': + cards.forEach(type => { session.player[type]++; session.player.resources++; }); addChatMessage( game, session, - `${session.name} player Year of Plenty.` + `They chose to receive ${display.join(", ")} from the bank.` + `${session.name} player Year of Plenty.` + + `They chose to receive ${display.join(', ')} from the bank.` ); delete game.turn.active; game.turn.actions = []; break; - case "volcano": - cards.forEach((type) => { + case 'volcano': + cards.forEach(type => { session.player[type]++; session.player.resources++; }); - addChatMessage(game, session, `${session.name} player mined ${display.join(", ")} from the Volcano!`); + addChatMessage( + game, + session, + `${session.name} player mined ${display.join(', ')} from the Volcano!` + ); if (!game.turn.select) { delete game.turn.active; game.turn.actions = []; @@ -3300,7 +3483,7 @@ const selectResources = (game: any, session: any, cards: string[]): string | und const buySettlement = (game: any, session: any): string | undefined => { const player = session.player; - if (game.state !== "normal") { + if (game.state !== 'normal') { return `You cannot purchase a development card unless the game is active (${game.state}).`; } if (session.color !== game.turn.color) { @@ -3336,7 +3519,7 @@ const buySettlement = (game: any, session: any): string | undefined => { const buyCity = (game: any, session: any): string | undefined => { const player = session.player; - if (game.state !== "normal") { + if (game.state !== 'normal') { return `You cannot purchase a development card unless the game is active (${game.state}).`; } if (session.color !== game.turn.color) { @@ -3356,7 +3539,7 @@ const buyCity = (game: any, session: any): string | undefined => { if (player.city < 1) { return `You have already built all of your cities.`; } - const corners = getValidCorners(game, session.color, "settlement"); + const corners = getValidCorners(game, session.color, 'settlement'); if (corners.length === 0) { return `There are no valid locations for you to place a city.`; } @@ -3372,8 +3555,8 @@ const buyCity = (game: any, session: any): string | undefined => { const placeCity = (game: any, session: any, index: any): string | undefined => { const player = session.player; - if (typeof index === "string") index = parseInt(index); - if (game.state !== "normal") { + if (typeof index === 'string') index = parseInt(index); + if (game.state !== 'normal') { return `You cannot purchase a development card unless the game is active (${game.state}).`; } if (session.color !== game.turn.color) { @@ -3384,24 +3567,31 @@ const placeCity = (game: any, session: any, index: any): string | undefined => { return `You have requested to place a city illegally!`; } /* If this is not a placement the turn limits, discard it */ - if (!game.turn || !game.turn.limits || !game.turn.limits.corners || game.turn.limits.corners.indexOf(index) === -1) { + if ( + !game.turn || + !game.turn.limits || + !game.turn.limits.corners || + game.turn.limits.corners.indexOf(index) === -1 + ) { return `You tried to cheat! You should not try to break the rules.`; } const corner = game.placements.corners[index]; - if (corner.color !== "unassigned" && corner.color !== session.color) { - return `This location already has a settlement belonging to ${game.players[corner.color].name}!`; + if (corner.color !== 'unassigned' && corner.color !== session.color) { + return `This location already has a settlement belonging to ${ + game.players[corner.color].name + }!`; } - if (corner.type !== "settlement") { + if (corner.type !== 'settlement') { return `This location already has a city!`; } if (game.turn.free) { delete game.turn.free; } - debugChat(game, "Before city placement"); + debugChat(game, 'Before city placement'); corner.color = session.color; - corner.type = "city"; + corner.type = 'city'; player.cities--; player.settlements++; if (!game.turn.free) { @@ -3409,13 +3599,13 @@ const placeCity = (game: any, session: any, index: any): string | undefined => { player.wheat -= 2; player.stone -= 3; player.resources = 0; - ["wheat", "brick", "sheep", "stone", "wood"].forEach((resource) => { + ['wheat', 'brick', 'sheep', 'stone', 'wood'].forEach(resource => { player.resources += player[resource]; }); } delete game.turn.free; - debugChat(game, "After city placement"); + debugChat(game, 'After city placement'); game.turn.actions = []; game.turn.limits = {}; addActivity(game, session, `${session.name} upgraded a settlement to a city!`); @@ -3444,7 +3634,7 @@ const ping = (session: Session) => { // console.log(`${session.short}: Sending ping to ${session.name}`); try { - session.ws.send(JSON.stringify({ type: "ping", ping: session.ping })); + session.ws.send(JSON.stringify({ type: 'ping', ping: session.ping })); } catch (e) { console.error(`${session.id}: Failed to send ping:`, e); // If send fails, the socket is likely dead - clean up @@ -3462,7 +3652,9 @@ const ping = (session: Session) => { // Set timeout to disconnect if no pong received within 20 seconds session.keepAlive = setTimeout(() => { - console.warn(`${session.id}: No pong received from ${session.name} within 20s, closing connection`); + console.warn( + `${session.id}: No pong received from ${session.name} within 20s, closing connection` + ); if (session.ws) { try { session.ws.close(); @@ -3480,7 +3672,9 @@ const schedulePing = (session: Session) => { // Diagnostic logging to help detect multiple intervals being created try { console.log( - `${session.short}: schedulePing called for ${getName(session)} - existing pingInterval? ${!!session.pingInterval}` + `${session.short}: schedulePing called for ${getName( + session + )} - existing pingInterval? ${!!session.pingInterval}` ); } catch (e) { /* ignore logging errors */ @@ -3513,7 +3707,7 @@ const setGame = (game: any, session: any, state: any): string | undefined => { return `Invalid state.`; } - if (session.color === "unassigned") { + if (session.color === 'unassigned') { return `You must have an active player to start the game.`; } @@ -3522,8 +3716,8 @@ const setGame = (game: any, session: any, state: any): string | undefined => { } switch (state) { - case "game-order": - if (game.state !== "lobby") { + case 'game-order': + if (game.state !== 'lobby') { return `You can only start the game from the lobby.`; } const active = getActiveCount(game); @@ -3534,7 +3728,7 @@ const setGame = (game: any, session: any, state: any): string | undefined => { * code that would otherwise have to filter out players by checking * the 'Not active' state of player.status */ for (let key in game.players) { - if (game.players[key].status !== "Active") { + if (game.players[key].status !== 'Active') { delete game.players[key]; } } @@ -3571,14 +3765,17 @@ const departLobby = (game: any, session: any, _color?: string): void => { } if (session.name) { - if (session.color !== "unassigned") { + if (session.color !== 'unassigned') { addChatMessage(game, null, `${session.name} has disconnected ` + `from the game.`); } else { addChatMessage(game, null, `${session.name} has left the lobby.`); } update.chat = game.chat; } else { - console.log(`${session.short}: departLobby - ${getName(session)} is ` + `being removed from ${game.id}'s sessions.`); + console.log( + `${session.short}: departLobby - ${getName(session)} is ` + + `being removed from ${game.id}'s sessions.` + ); for (let id in game.sessions) { if (game.sessions[id] === session) { delete game.sessions[id]; @@ -3602,14 +3799,16 @@ const sendGameToPlayer = (game: any, session: any): void => { /* Only send empty name data to unnamed players */ if (!session.name) { - console.log(`${session.short}: -> sendGamePlayer:${getName(session)} - only sending empty name`); - update = { name: "" }; + console.log( + `${session.short}: -> sendGamePlayer:${getName(session)} - only sending empty name` + ); + update = { name: '' }; } else { update = getFilteredGameForPlayer(game, session); } const message = JSON.stringify({ - type: "game-update", + type: 'game-update', update: update, }); queueSend(session, message); @@ -3637,17 +3836,21 @@ const parseChatCommands = (game: any, message: string): void => { return; } const parts = partsRaw as RegExpMatchArray; - const key = parts[1] || ""; + const key = parts[1] || ''; switch (key.toLowerCase()) { - case "game": + case 'game': if (parts[2] && parts[2].trim().match(/^beginner('?s)?( +layout)?/i)) { setBeginnerGame(game); addChatMessage(game, null, `Game board set to the Beginner's Layout.`); break; } - const signature = parts[2] ? parts[2].match(/^([0-9a-f]{12})-([0-9a-f]{38})-([0-9a-f]{38})/i) : null; + const signature = parts[2] + ? parts[2].match(/^([0-9a-f]{12})-([0-9a-f]{38})-([0-9a-f]{38})/i) + : null; if (signature) { - if (setGameFromSignature(game, signature[1] || "", signature[2] || "", signature[3] || "")) { + if ( + setGameFromSignature(game, signature[1] || '', signature[2] || '', signature[3] || '') + ) { game.signature = parts[2]; addChatMessage(game, null, `Game board set to ${parts[2]}.`); } else { @@ -3661,7 +3864,7 @@ const parseChatCommands = (game: any, message: string): void => { const sendError = (session: Session, error: string): void => { console.error(`${session.short}: Error: ${error}`); try { - session.ws.send(JSON.stringify({ type: "error", data: error })); + session.ws.send(JSON.stringify({ type: 'error', data: error })); } catch (e) { /* ignore */ } @@ -3670,7 +3873,7 @@ const sendError = (session: Session, error: string): void => { const sendWarning = (session: Session, warning: string): void => { console.warn(`${session.short}: Warning: ${warning}`); try { - session?.ws?.send(JSON.stringify({ type: "warning", warning })); + session?.ws?.send(JSON.stringify({ type: 'warning', warning })); } catch (e) { /* ignore */ } @@ -3725,7 +3928,8 @@ const clearGame = (game: any, _session: any): string | undefined => { addChatMessage( game, null, - `The game has been reset. You can play again with this board, or ` + `click 'New Table' to mix things up a bit.` + `The game has been reset. You can play again with this board, or ` + + `click 'New Table' to mix things up a bit.` ); sendGameToPlayers(game); return undefined; @@ -3752,7 +3956,10 @@ const gotoLobby = (game: any, session: any): string | undefined => { game.waiting.push(session.name); addChatMessage(game, null, `${session.name} has gone to the lobby.`); } else if (waitingFor.length !== 0) { - return `You are already waiting in the lobby. ` + `${waitingFor.join(",")} still needs to go to the lobby.`; + return ( + `You are already waiting in the lobby. ` + + `${waitingFor.join(',')} still needs to go to the lobby.` + ); } if (waitingFor.length === 0) { @@ -3761,13 +3968,14 @@ const gotoLobby = (game: any, session: any): string | undefined => { addChatMessage( game, null, - `The game has been reset. You can play again with this board, or ` + `click 'New Table' to mix things up a bit.` + `The game has been reset. You can play again with this board, or ` + + `click 'New Table' to mix things up a bit.` ); sendGameToPlayers(game); return; } - addChatMessage(game, null, `Waiting for ${waitingFor.join(",")} to go to lobby.`); + addChatMessage(game, null, `Waiting for ${waitingFor.join(',')} to go to lobby.`); sendUpdateToPlayers(game, { chat: game.chat, }); @@ -3777,7 +3985,7 @@ const gotoLobby = (game: any, session: any): string | undefined => { const normalizeIncoming = (msg: unknown): IncomingMessage | null => { let parsed: IncomingMessage | null = null; try { - if (typeof msg === "string") { + if (typeof msg === 'string') { parsed = JSON.parse(msg); } else { parsed = msg as IncomingMessage; @@ -3791,9 +3999,9 @@ const normalizeIncoming = (msg: unknown): IncomingMessage | null => { return { type, data }; }; -router.ws("/ws/:id", async (ws, req) => { - console.log("New WebSocket connection"); - if (!req.cookies || !(req.cookies as any)["player"]) { +router.ws('/ws/:id', async (ws, req) => { + console.log('New WebSocket connection'); + if (!req.cookies || !(req.cookies as any)['player']) { // If the client hasn't established a session cookie, they cannot // participate in a websocket-backed game session. Log the request // headers to aid debugging (e.g. missing Cookie header due to @@ -3802,23 +4010,28 @@ router.ws("/ws/:id", async (ws, req) => { try { const remote = req.ip || - (req.headers && (req.headers["x-forwarded-for"] || (req.connection && req.connection.remoteAddress))) || - "unknown"; + (req.headers && + (req.headers['x-forwarded-for'] || (req.connection && req.connection.remoteAddress))) || + 'unknown'; console.warn( - `[ws] Rejecting connection from ${remote} - missing session cookie. headers=${JSON.stringify(req.headers || {})}` + `[ws] Rejecting connection from ${remote} - missing session cookie. headers=${JSON.stringify( + req.headers || {} + )}` ); } catch (e) { - console.warn("[ws] Rejecting connection - missing session cookie (unable to serialize headers)"); + console.warn( + '[ws] Rejecting connection - missing session cookie (unable to serialize headers)' + ); } try { // Inform the client why we are closing, then close the socket. - ws.send(JSON.stringify({ type: "error", error: `Unable to find session cookie` })); + ws.send(JSON.stringify({ type: 'error', error: `Unable to find session cookie` })); } catch (e) { /* ignore send errors */ } try { // 1008 = Policy Violation - appropriate for missing auth cookie - ws.close && ws.close(1008, "Missing session cookie"); + ws.close && ws.close(1008, 'Missing session cookie'); } catch (e) { /* ignore close errors */ } @@ -3829,24 +4042,27 @@ router.ws("/ws/:id", async (ws, req) => { const gameId = id; if (!gameId) { - console.log("Missing game id"); + console.log('Missing game id'); try { - ws.send(JSON.stringify({ type: "error", error: "Missing game id" })); + ws.send(JSON.stringify({ type: 'error', error: 'Missing game id' })); } catch (e) {} try { - console.log("Missing game id"); - ws.close && ws.close(1008, "Missing game id"); + console.log('Missing game id'); + ws.close && ws.close(1008, 'Missing game id'); } catch (e) {} return; } - const playerCookie = req.cookies && (req.cookies as any)["player"] ? String((req.cookies as any)["player"]) : ""; - const short = playerCookie ? `[${playerCookie.substring(0, 8)}]` : "[unknown]"; + const playerCookie = + req.cookies && (req.cookies as any)['player'] ? String((req.cookies as any)['player']) : ''; + const short = playerCookie ? `[${playerCookie.substring(0, 8)}]` : '[unknown]'; (ws as any).id = short; console.log(`${short}: Game ${gameId} - New connection from client.`); try { - console.log(`${short}: WS handshake headers: origin=${req.headers.origin} cookie=${req.headers.cookie}`); + console.log( + `${short}: WS handshake headers: origin=${req.headers.origin} cookie=${req.headers.cookie}` + ); } catch (e) { /* ignore logging errors */ } @@ -3859,7 +4075,7 @@ router.ws("/ws/:id", async (ws, req) => { /* Setup WebSocket event handlers prior to performing any async calls or * we may miss the first messages from clients */ - ws.on("error", async (event) => { + ws.on('error', async event => { console.error(`WebSocket error: `, event && event.message ? event.message : event); const game = await loadGame(gameId); if (!game) { @@ -3867,7 +4083,7 @@ router.ws("/ws/:id", async (ws, req) => { } const _session = getSession( game, - req.cookies && (req.cookies as any)["player"] ? String((req.cookies as any)["player"]) : "" + req.cookies && (req.cookies as any)['player'] ? String((req.cookies as any)['player']) : '' ); if (!_session) return; const session = _session; @@ -3904,9 +4120,11 @@ router.ws("/ws/:id", async (ws, req) => { departLobby(game, session); }); - ws.on("close", async (event) => { + ws.on('close', async event => { console.log( - `${short} - closed connection (event: ${event && typeof event === "object" ? JSON.stringify(event) : event})` + `${short} - closed connection (event: ${ + event && typeof event === 'object' ? JSON.stringify(event) : event + })` ); const game = await loadGame(gameId); @@ -3915,7 +4133,7 @@ router.ws("/ws/:id", async (ws, req) => { } const _session = getSession( game, - req.cookies && (req.cookies as any)["player"] ? String((req.cookies as any)["player"]) : "" + req.cookies && (req.cookies as any)['player'] ? String((req.cookies as any)['player']) : '' ); if (!_session) return; const session = _session; @@ -3929,7 +4147,9 @@ router.ws("/ws/:id", async (ws, req) => { try { console.log(`${short}: ws.on('close') - session.ws === ws? ${session.ws === ws}`); console.log( - `${short}: ws.on('close') - session.id=${session && session.id}, lastActive=${session && session.lastActive}` + `${short}: ws.on('close') - session.id=${session && session.id}, lastActive=${ + session && session.lastActive + }` ); if (session.ws && session.ws === ws) { // Clear ping interval @@ -3968,7 +4188,7 @@ router.ws("/ws/:id", async (ws, req) => { /* Check for a game in the Winner state with no more connections * and remove it */ - if (game.state === "winner") { + if (game.state === 'winner') { let dead = true; for (let id in game.sessions) { if (game.sessions[id]!.live && game.sessions[id]!.name) { @@ -3977,14 +4197,20 @@ router.ws("/ws/:id", async (ws, req) => { } if (dead) { console.log(`${session.short}: No more players in ${game.id}. ` + `Removing.`); - addChatMessage(game, null, `No more active players in game. ` + `It is being removed from the server.`); + addChatMessage( + game, + null, + `No more active players in game. ` + `It is being removed from the server.` + ); sendUpdateToPlayers(game, { chat: game.chat, }); for (let id in game.sessions) { if (game.sessions[id]!.ws) { try { - console.log(`${short}: Removing game - closing session ${id} socket (game removal cleanup)`); + console.log( + `${short}: Removing game - closing session ${id} socket (game removal cleanup)` + ); console.log(`${short}: Closing socket stack:`, new Error().stack); game.sessions[id]!.ws.close(); } catch (e) { @@ -4010,7 +4236,7 @@ router.ws("/ws/:id", async (ws, req) => { } }); - ws.on("message", async (message) => { + ws.on('message', async message => { // Normalize the incoming message to { type, data } so handlers can // reliably access the payload without repeated defensive checks. const incoming = normalizeIncoming(message); @@ -4020,7 +4246,7 @@ router.ws("/ws/:id", async (ws, req) => { try { console.error(`${all}: parse/normalize error`, message); } catch (e) { - console.error("parse/normalize error"); + console.error('parse/normalize error'); } return; } @@ -4028,7 +4254,7 @@ router.ws("/ws/:id", async (ws, req) => { const game = await loadGame(gameId); const _session = getSession( game, - req.cookies && (req.cookies as any)["player"] ? String((req.cookies as any)["player"]) : "" + req.cookies && (req.cookies as any)['player'] ? String((req.cookies as any)['player']) : '' ); if (!_session) return; const session = _session; @@ -4043,12 +4269,16 @@ router.ws("/ws/:id", async (ws, req) => { if (session.pingInterval) { clearInterval(session.pingInterval); session.pingInterval = undefined; - console.log(`${short}: Cleared old pingInterval during reconnection for ${getName(session)}`); + console.log( + `${short}: Cleared old pingInterval during reconnection for ${getName(session)}` + ); } if (session.keepAlive) { clearTimeout(session.keepAlive); session.keepAlive = undefined; - console.log(`${short}: Cleared old keepAlive during reconnection for ${getName(session)}`); + console.log( + `${short}: Cleared old keepAlive during reconnection for ${getName(session)}` + ); } } catch (e) { console.warn(`${short}: Error clearing old timers during reconnection:`, e); @@ -4058,7 +4288,9 @@ router.ws("/ws/:id", async (ws, req) => { if (gameId in audio) { try { webrtcPart(audio[gameId]!, session); - console.log(`${short}: Cleaned up peer ${session.name} from audio registry during reconnection`); + console.log( + `${short}: Cleaned up peer ${session.name} from audio registry during reconnection` + ); } catch (e) { console.warn(`${short}: Error cleaning up peer during reconnection:`, e); } @@ -4089,18 +4321,18 @@ router.ws("/ws/:id", async (ws, req) => { // client sends messages during the attach/reconnect sequence. switch (incoming.type) { - case "join": + case 'join': // Accept either legacy `config`, newer `data`, or flat payloads where // the client sent fields at the top level (normalizeIncoming will // populate `data` with the parsed object in that case). webrtcJoin(audio[gameId]!, session); break; - case "part": + case 'part': webrtcPart(audio[gameId]!, session); break; - case "relayICECandidate": + case 'relayICECandidate': { // Delegate to the webrtc signaling helper (it performs its own checks) // Accept either config/data or a flat payload (data). @@ -4109,7 +4341,7 @@ router.ws("/ws/:id", async (ws, req) => { } break; - case "relaySessionDescription": + case 'relaySessionDescription': { // Accept either config/data or a flat payload (data). const cfg = data.config || data.data || data || {}; @@ -4117,7 +4349,7 @@ router.ws("/ws/:id", async (ws, req) => { } break; - case "pong": + case 'pong': // Clear the keepAlive timeout since we got a response if (session.keepAlive) { clearTimeout(session.keepAlive); @@ -4136,12 +4368,12 @@ router.ws("/ws/:id", async (ws, req) => { // No need to resetDisconnectCheck since it's non-functional break; - case "game-update": + case 'game-update': console.log(`${short}: <- game-update ${getName(session)} - full game update.`); sendGameToPlayer(game, session); break; - case "peer_state_update": + case 'peer_state_update': { // Accept either config/data or a flat payload (data). const cfg = data.config || data.data || data || {}; @@ -4149,7 +4381,7 @@ router.ws("/ws/:id", async (ws, req) => { } break; - case "player-name": + case 'player-name': // Support both legacy { type: 'player-name', name: 'Foo' } // and normalized { type: 'player-name', data: { name: 'Foo' } } const _pname = (data && data.name) || (data && data.data && data.data.name); @@ -4162,10 +4394,10 @@ router.ws("/ws/:id", async (ws, req) => { } break; - case "set": + case 'set': console.log(`${short}: <- set:${getName(session)} ${data.field} = ${data.value}`); switch (data.field) { - case "state": + case 'state': warning = setGame(game, session, data.value); if (warning) { sendWarning(session, warning); @@ -4174,7 +4406,7 @@ router.ws("/ws/:id", async (ws, req) => { } break; - case "color": + case 'color': warning = setPlayerColor(game, session, data.value); if (warning) { sendWarning(session, warning); @@ -4188,7 +4420,7 @@ router.ws("/ws/:id", async (ws, req) => { } break; - case "get": + case 'get': // Batch 'get' requests per-session for a short window so multiple // near-simultaneous requests are merged into one response. This // reduces CPU and network churn during client startup. @@ -4199,7 +4431,9 @@ router.ws("/ws/:id", async (ws, req) => { : []; console.log( - `${short}: <- get:${getName(session)} ${requestedFields.length ? requestedFields.join(",") : ""}` + `${short}: <- get:${getName(session)} ${ + requestedFields.length ? requestedFields.join(',') : '' + }` ); // Ensure a batch structure exists on the session @@ -4207,7 +4441,9 @@ router.ws("/ws/:id", async (ws, req) => { session._getBatch = { fields: new Set(), timer: undefined }; } // Merge requested fields into the batch set - requestedFields.forEach((f: string) => session._getBatch && session._getBatch.fields.add(f)); + requestedFields.forEach( + (f: string) => session._getBatch && session._getBatch.fields.add(f) + ); // If a timer is already scheduled, we will respond when it fires. if (session._getBatch.timer) { @@ -4222,68 +4458,72 @@ router.ws("/ws/:id", async (ws, req) => { const batchedUpdate: any = {}; fieldsArray.forEach((field: string) => { switch (field) { - case "player": + case 'player': sendWarning(session, `'player' is not a valid item. use 'private' instead`); batchedUpdate.player = undefined; break; - case "id": - case "chat": - case "startTime": - case "state": - case "turn": - case "turns": - case "winner": - case "placements": - case "longestRoadLength": - case "robber": - case "robberName": - case "tileOrder": - case "active": - case "largestArmy": - case "mostDeveloped": - case "mostPorts": - case "longestRoad": - case "pipOrder": - case "signature": - case "borderOrder": - case "dice": - case "activities": + case 'id': + case 'chat': + case 'startTime': + case 'state': + case 'turn': + case 'turns': + case 'winner': + case 'placements': + case 'longestRoadLength': + case 'robber': + case 'robberName': + case 'tileOrder': + case 'active': + case 'largestArmy': + case 'mostDeveloped': + case 'mostPorts': + case 'longestRoad': + case 'pipOrder': + case 'signature': + case 'borderOrder': + case 'dice': + case 'activities': batchedUpdate[field] = game[field]; break; - case "tiles": + case 'tiles': batchedUpdate.tiles = staticData.tiles; break; - case "pips": + case 'pips': // pips are static data (number/roll mapping). Return from staticData batchedUpdate.pips = staticData.pips; break; - case "borders": + case 'borders': // borders are static data describing ports/banks batchedUpdate.borders = staticData.borders; break; - case "rules": + case 'rules': batchedUpdate[field] = game.rules ? game.rules : {}; break; - case "name": + case 'name': batchedUpdate.name = session.name; break; - case "unselected": + case 'unselected': batchedUpdate.unselected = getFilteredUnselected(game); break; - case "private": + case 'private': batchedUpdate.private = session.player; break; - case "players": + case 'players': batchedUpdate.players = getFilteredPlayers(game); break; - case "participants": + case 'participants': batchedUpdate.participants = getParticipants(game); break; - case "color": - console.log(`${_session.short}: -> Returning color as ${session.color} for ${getName(session)}`); + case 'color': + console.log( + `${_session.short}: -> Returning color as ${session.color} for ${getName( + session + )}` + ); batchedUpdate.color = session.color; break; - case "timestamp": + case 'timestamp': batchedUpdate.timestamp = Date.now(); break; default: @@ -4291,10 +4531,14 @@ router.ws("/ws/:id", async (ws, req) => { return key in (obj as unknown as Record); } if (hasKey(game, field)) { - console.warn(`${short}: WARNING: Requested GET not-privatized/sanitized field: ${field}`); + console.warn( + `${short}: WARNING: Requested GET not-privatized/sanitized field: ${field}` + ); batchedUpdate[field] = game[field]; } else if (hasKey(session, field)) { - console.warn(`${short}: WARNING: Requested GET not-sanitized session field: ${field}`); + console.warn( + `${short}: WARNING: Requested GET not-sanitized session field: ${field}` + ); batchedUpdate[field as keyof typeof game] = session[field]; } else { console.warn(`${short}: WARNING: Requested GET unsupported field: ${field}`); @@ -4315,9 +4559,9 @@ router.ws("/ws/:id", async (ws, req) => { }, INCOMING_GET_BATCH_MS); break; - case "chat": + case 'chat': /* If the chat message is empty, do not add it to the chat */ - if (data.message.trim() == "") { + if (data.message.trim() == '') { break; } console.log(`${short}:${id} - ${data.type} - "${data.message}"`); @@ -4327,10 +4571,10 @@ router.ws("/ws/:id", async (ws, req) => { gameDB.saveGame(game); break; - case "media-status": + case 'media-status': console.log(`${short}: <- media-status - `, data.audio, data.video); - session["video"] = data.video; - session["audio"] = data.audio; + session['video'] = data.video; + session['audio'] = data.audio; break; default: @@ -4346,7 +4590,7 @@ router.ws("/ws/:id", async (ws, req) => { /* The rest of the actions and commands require an active game * participant */ - if (session.player?.color === "unassigned") { + if (session.player?.color === 'unassigned') { error = `Player must have an active color.`; sendError(session, error); return; @@ -4355,115 +4599,117 @@ router.ws("/ws/:id", async (ws, req) => { processed = true; switch (incoming.type) { - case "roll": + case 'roll': console.log(`${short}: <- roll:${getName(session)}`); warning = roll(game, session); if (warning) { sendWarning(session, warning); } break; - case "shuffle": + case 'shuffle': console.log(`${short}: <- shuffle:${getName(session)}`); warning = shuffle(game, session); if (warning) { sendWarning(session, warning); } break; - case "place-settlement": + case 'place-settlement': console.log(`${short}: <- place-settlement:${getName(session)} ${data.index}`); warning = placeSettlement(game, session, data.index); if (warning) { sendWarning(session, warning); } break; - case "place-city": + case 'place-city': console.log(`${short}: <- place-city:${getName(session)} ${data.index}`); warning = placeCity(game, session, data.index); if (warning) { sendWarning(session, warning); } break; - case "place-road": + case 'place-road': console.log(`${short}: <- place-road:${getName(session)} ${data.index}`); warning = placeRoad(game, session, parseInt(data.index)); if (warning) { sendWarning(session, warning); } break; - case "place-robber": + case 'place-robber': console.log(`${short}: <- place-robber:${getName(session)} ${data.index}`); warning = placeRobber(game, session, data.index); if (warning) { sendWarning(session, warning); } break; - case "steal-resource": + case 'steal-resource': console.log(`${short}: <- steal-resource:${getName(session)} ${data.color}`); warning = stealResource(game, session, data.color); if (warning) { sendWarning(session, warning); } break; - case "discard": + case 'discard': console.log(`${short}: <- discard:${getName(session)}`); warning = discard(game, session, data.discards); if (warning) { sendWarning(session, warning); } break; - case "pass": + case 'pass': console.log(`${short}: <- pass:${getName(session)}`); warning = pass(game, session); if (warning) { sendWarning(session, warning); } break; - case "select-resources": + case 'select-resources': console.log(`${short}: <- select-resources:${getName(session)} - `, data.cards); warning = selectResources(game, session, data.cards); if (warning) { sendWarning(session, warning); } break; - case "buy-city": + case 'buy-city': console.log(`${short}: <- buy-city:${getName(session)}`); warning = buyCity(game, session); if (warning) { sendWarning(session, warning); } break; - case "buy-road": + case 'buy-road': console.log(`${short}: <- buy-road:${getName(session)}`); warning = buyRoad(game, session); if (warning) { sendWarning(session, warning); } break; - case "buy-settlement": + case 'buy-settlement': console.log(`${short}: <- buy-settlement:${getName(session)}`); warning = buySettlement(game, session); if (warning) { sendWarning(session, warning); } break; - case "buy-development": + case 'buy-development': console.log(`${short}: <- buy-development:${getName(session)}`); warning = buyDevelopment(game, session); if (warning) { sendWarning(session, warning); } break; - case "play-card": + case 'play-card': console.log(`${short}: <- play-card:${getName(session)}`); warning = playCard(game, session, data.card); if (warning) { sendWarning(session, warning); } break; - case "trade": + case 'trade': console.log( - `${short}: <- trade:${getName(session)} - ` + (data.action ? data.action : "start") + ` -`, - data.offer ? data.offer : "no trade yet" + `${short}: <- trade:${getName(session)} - ` + + (data.action ? data.action : 'start') + + ` -`, + data.offer ? data.offer : 'no trade yet' ); warning = trade(game, session, data.action, data.offer); if (warning) { @@ -4485,28 +4731,28 @@ router.ws("/ws/:id", async (ws, req) => { }); } break; - case "turn-notice": + case 'turn-notice': console.log(`${short}: <- turn-notice:${getName(session)}`); warning = clearTimeNotice(game, session); if (warning) { sendWarning(session, warning); } break; - case "clear-game": + case 'clear-game': console.log(`${short}: <- clear-game:${getName(session)}`); warning = clearGame(game, session); if (warning) { sendWarning(session, warning); } break; - case "goto-lobby": + case 'goto-lobby': console.log(`${short}: <- goto-lobby:${getName(session)}`); warning = gotoLobby(game, session); if (warning) { sendWarning(session, warning); } break; - case "rules": + case 'rules': console.log(`${short} - <- rules:${getName(session)} - `, data.rules); warning = setRules(game, session, data.rules); if (warning) { @@ -4525,7 +4771,7 @@ router.ws("/ws/:id", async (ws, req) => { } /* If the current player took an action, reset the session timer */ - if (processed && session.color === game.turn.color && game.state !== "winner") { + if (processed && session.color === game.turn.color && game.state !== 'winner') { resetTurnTimer(game, session); } }); @@ -4541,7 +4787,7 @@ router.ws("/ws/:id", async (ws, req) => { const _session2 = getSession( game, - req.cookies && (req.cookies as any)["player"] ? String((req.cookies as any)["player"]) : "" + req.cookies && (req.cookies as any)['player'] ? String((req.cookies as any)['player']) : '' ); if (!_session2) return; const session = _session2; @@ -4565,12 +4811,12 @@ router.ws("/ws/:id", async (ws, req) => { } /* If the current turn player just rejoined, set their turn timer */ - if (game.turn && game.turn.color === session.color && game.state !== "winner") { + if (game.turn && game.turn.color === session.color && game.state !== 'winner') { resetTurnTimer(game, session); } if (session.name) { - if (session.color !== "unassigned") { + if (session.color !== 'unassigned') { addChatMessage(game, null, `${session.name} has reconnected to the game.`); } else { addChatMessage(game, null, `${session.name} has rejoined the lobby.`); @@ -4593,22 +4839,22 @@ const debugChat = (game: any, preamble: any) => { for (let key in game.players) { const player = game.players[key]; - if (player.status === "Not active") { + if (player.status === 'Not active') { continue; } - if (playerInventory !== "") { - playerInventory += " player"; + if (playerInventory !== '') { + playerInventory += ' player'; } else { - playerInventory += " Player"; + playerInventory += ' Player'; } playerInventory += ` ${player.name} has `; - const has = ["wheat", "brick", "sheep", "stone", "wood"] - .map((resource) => { + const has = ['wheat', 'brick', 'sheep', 'stone', 'wood'] + .map(resource => { const count = player[resource] ? player[resource] : 0; return `${count} ${resource}`; }) - .filter((item) => item !== "") - .join(", "); + .filter(item => item !== '') + .join(', '); if (has) { playerInventory += `${has}, `; } else { @@ -4616,9 +4862,9 @@ const debugChat = (game: any, preamble: any) => { } } if (game.debug) { - addChatMessage(game, null, playerInventory.replace(/, $/, "").trim()); + addChatMessage(game, null, playerInventory.replace(/, $/, '').trim()); } else { - console.log(playerInventory.replace(/, $/, "").trim()); + console.log(playerInventory.replace(/, $/, '').trim()); } }; @@ -4635,26 +4881,26 @@ const getFilteredGameForPlayer = (game: any, session: any) => { const reduced = Object.assign({}, original); // Remove obvious non-serializable fields - if ("player" in reduced) delete reduced.player; - if ("ws" in reduced) delete reduced.ws; - if ("keepAlive" in reduced) delete reduced.keepAlive; + if ('player' in reduced) delete reduced.player; + if ('ws' in reduced) delete reduced.ws; + if ('keepAlive' in reduced) delete reduced.keepAlive; // Remove internal helper fields (e.g. _pendingTimeout) and functions - Object.keys(reduced).forEach((k) => { + Object.keys(reduced).forEach(k => { try { - if (k.startsWith("_")) { + if (k.startsWith('_')) { delete reduced[k]; - } else if (typeof reduced[k] === "function") { + } else if (typeof reduced[k] === 'function') { delete reduced[k]; } else { // Remove values that are likely to be non-serializable objects // such as Timers that may appear on some runtime fields. const v = reduced[k]; - if (typeof v === "object" && v !== null) { + if (typeof v === 'object' && v !== null) { // A quick heuristic: if the object has constructor name 'Timeout' or // properties typical of timer internals, drop it to avoid circular refs. - const ctor = v.constructor && v.constructor.name ? v.constructor.name : ""; - if (ctor === "Timeout" || ctor === "TimersList") { + const ctor = v.constructor && v.constructor.name ? v.constructor.name : ''; + if (ctor === 'Timeout' || ctor === 'TimersList') { delete reduced[k]; } } @@ -4686,7 +4932,7 @@ const getFilteredGameForPlayer = (game: any, session: any) => { return Object.assign(reducedGame, { live: true, - status: session.error ? session.error : "success", + status: session.error ? session.error : 'success', name: session.name, color: session.color, order: session.color in game.players ? game.players[session.color].order : 0, @@ -4723,13 +4969,13 @@ const sendInitialGameSnapshot = (game: Game, session: Session) => { try { const snapshot = getFilteredGameForPlayer(game, session); - const message = JSON.stringify({ type: "initial-game", snapshot }); + const message = JSON.stringify({ type: 'initial-game', snapshot }); // Small debug log to help test harnesses detect that the server sent // the consolidated snapshot. Keep output small to avoid noisy logs. try { const topKeys = Object.keys(snapshot || {}) .slice(0, 10) - .join(","); + .join(','); console.log(`${session.short}: sending initial-game snapshot keys: ${topKeys}`); } catch (e) { /* ignore logging errors */ @@ -4784,7 +5030,7 @@ const trackTheft = (game: any, from: any, to: any, type: any, count: any) => { const stats = game.stolen; /* Initialize the stole / stolen structures */ - [to, from].forEach((player) => { + [to, from].forEach(player => { if (!(player in stats)) { stats[player] = { stole: { @@ -4809,7 +5055,7 @@ const trackTheft = (game: any, from: any, to: any, type: any, count: any) => { /* Update counts */ stats[from].stolen.total += count; - if (to === "robber") { + if (to === 'robber') { stats[from].stolen.robber += count; } else { stats[from].stolen.player += count; @@ -4821,28 +5067,30 @@ const trackTheft = (game: any, from: any, to: any, type: any, count: any) => { /* Simple NO-OP to set session cookie so player-id can use it as the * index */ -router.get("/", (req, res /*, next*/) => { +router.get('/', (req, res /*, next*/) => { let playerId; if (!req.cookies.player) { - playerId = crypto.randomBytes(16).toString("hex"); + playerId = crypto.randomBytes(16).toString('hex'); // Determine whether this request is secure so we can set cookie flags // appropriately. In production behind TLS we want SameSite=None and // Secure so the cookie is sent on cross-site websocket connects. const secure = req.secure || - (req.headers && req.headers["x-forwarded-proto"] === "https") || - process.env["NODE_ENV"] === "production"; + (req.headers && req.headers['x-forwarded-proto'] === 'https') || + process.env['NODE_ENV'] === 'production'; const cookieOpts: any = { httpOnly: false, - sameSite: secure ? "none" : "lax", + sameSite: secure ? 'none' : 'lax', secure: !!secure, }; // Ensure cookie is scoped to the application basePath so it will be // included on requests under the same prefix (and on the websocket // handshake which uses the same path prefix). - cookieOpts.path = basePath || "/"; - res.cookie("player", playerId, cookieOpts as any); - console.log(`[${playerId.substring(0, 8)}]: Set player cookie (opts=${JSON.stringify(cookieOpts)})`); + cookieOpts.path = basePath || '/'; + res.cookie('player', playerId, cookieOpts as any); + console.log( + `[${playerId.substring(0, 8)}]: Set player cookie (opts=${JSON.stringify(cookieOpts)})` + ); } else { playerId = req.cookies.player; } @@ -4850,7 +5098,7 @@ router.get("/", (req, res /*, next*/) => { console.log(`[${playerId.substring(0, 8)}]: Browser hand-shake achieved.`); // Mark this response as coming from the backend API to aid debugging - res.setHeader("X-Backend", "games"); + res.setHeader('X-Backend', 'games'); return res.status(200).send({ id: playerId, player: playerId, @@ -4860,24 +5108,26 @@ router.get("/", (req, res /*, next*/) => { }); }); -router.post("/:id?", async (req, res /*, next*/) => { +router.post('/:id?', async (req, res /*, next*/) => { const { id } = req.params; let playerId; if (!req.cookies.player) { - playerId = crypto.randomBytes(16).toString("hex"); + playerId = crypto.randomBytes(16).toString('hex'); const secure = req.secure || - (req.headers && req.headers["x-forwarded-proto"] === "https") || - process.env["NODE_ENV"] === "production"; + (req.headers && req.headers['x-forwarded-proto'] === 'https') || + process.env['NODE_ENV'] === 'production'; const cookieOpts: any = { httpOnly: false, - sameSite: secure ? "none" : "lax", + sameSite: secure ? 'none' : 'lax', secure: !!secure, }; - cookieOpts.path = basePath || "/"; - res.cookie("player", playerId, cookieOpts as any); - console.log(`[${playerId.substring(0, 8)}]: Set player cookie (opts=${JSON.stringify(cookieOpts)})`); + cookieOpts.path = basePath || '/'; + res.cookie('player', playerId, cookieOpts as any); + console.log( + `[${playerId.substring(0, 8)}]: Set player cookie (opts=${JSON.stringify(cookieOpts)})` + ); } else { playerId = req.cookies.player; } @@ -4887,7 +5137,7 @@ router.post("/:id?", async (req, res /*, next*/) => { } else { console.log(`[${playerId.substring(0, 8)}]: Creating new game.`); } - const game = await loadGame(id || ""); /* will create game if it doesn't exist */ + const game = await loadGame(id || ''); /* will create game if it doesn't exist */ console.log(`[${playerId.substring(0, 8)}]: ${game.id} loaded.`); return res.status(200).send({ id: game.id }); diff --git a/server/routes/games/types.ts b/server/routes/games/types.ts index cc9fbe0..be6e189 100644 --- a/server/routes/games/types.ts +++ b/server/routes/games/types.ts @@ -20,11 +20,15 @@ export interface Player { cities: number; longestRoad: number; mustDiscard?: number; + /* Resources */ sheep: number; wheat: number; stone: number; brick: number; wood: number; + desert: number; /* Not used -- for Typescript compliance */ + bank: number; /* Not used -- for Typescript compliance */ + /* End Resources */ army: number; points: number; ports: number; @@ -38,6 +42,11 @@ export interface Player { turnStart: number; totalTime: number; banks: ResourceType[]; + /* Offer */ + gives: OfferItem[]; + gets: OfferItem[]; + offerRejected: Record; + /* End Offer */ } export type CornerType = "settlement" | "city" | "none"; @@ -72,20 +81,21 @@ export interface Turn { limits?: any; roll?: number; volcano?: number | null | undefined; - free?: boolean; - freeRoads?: number; + free: boolean; + freeRoads: number; select?: Record; - active?: string; - robberInAction?: boolean; - placedRobber?: number; - offer?: Offer; + active: "volcano" | "robber" | "road-building" | "offer" | null; + robberInAction: boolean; + placedRobber: boolean; + developmentPurchased: boolean; + offer: Offer | null; [key: string]: any; } export interface DevelopmentCard { card?: number | string; type?: string; - [key: string]: any; + turn?: number; } // Import from schema for DRY compliance @@ -105,8 +115,8 @@ export interface PersistentSessionData { resources?: number; } -export type PlayerColor = "R" | "B" | "O" | "W" | "robber" | "unassigned"; -export const PLAYER_COLORS: PlayerColor[] = ["R", "B", "O", "W", "robber", "unassigned"]; +export type PlayerColor = "R" | "B" | "O" | "W" | "robber" | "bank" | "unassigned"; +export const PLAYER_COLORS: PlayerColor[] = ["R", "B", "O", "W", "robber", "bank", "unassigned"]; /** * Runtime Session type = Persistent + Transient @@ -115,14 +125,15 @@ export const PLAYER_COLORS: PlayerColor[] = ["R", "B", "O", "W", "robber", "unas export type Session = PersistentSessionData & TransientSessionState; export interface OfferItem { - type: string; // 'bank' or resource key or other + type: ResourceType; // 'bank' or resource key or other count: number; } export interface Offer { gets: OfferItem[]; gives: OfferItem[]; - [key: string]: any; + name: string; + color: PlayerColor; // Omit for bank trades } export type ResourceType = "wood" | "brick" | "sheep" | "wheat" | "stone" | "desert" | "bank";