diff --git a/client/src/RoomView.tsx b/client/src/RoomView.tsx index acb7fbc..555c20f 100644 --- a/client/src/RoomView.tsx +++ b/client/src/RoomView.tsx @@ -394,14 +394,7 @@ const RoomView = (props: RoomProps) => { )} {name && } - {/* Trade is an untyped JS component; assert its type to avoid `any` */} - {(() => { - const TradeComponent = Trade as unknown as React.ComponentType<{ - tradeActive: boolean; - setTradeActive: (v: boolean) => void; - }>; - return ; - })()} + {tradeActive && } {name !== "" && } {/* name !== "" && */} {loaded && ( diff --git a/client/src/Trade.css b/client/src/Trade.css index 20fd671..850bea2 100644 --- a/client/src/Trade.css +++ b/client/src/Trade.css @@ -7,7 +7,7 @@ } .Trade > * { - max-height: calc(100vh - 2rem); + max-height: calc(100dvh - 2rem); overflow: auto; width: 32em; display: inline-flex; @@ -100,9 +100,6 @@ line-height: 1.5em; } -.Trade .Resource.None { - /* filter: brightness(70%); */ -} .Trade .PlayerColor { align-self: center; diff --git a/client/src/Trade.tsx b/client/src/Trade.tsx index 0213f87..4793070 100644 --- a/client/src/Trade.tsx +++ b/client/src/Trade.tsx @@ -601,26 +601,24 @@ const Trade: React.FC = () => { }); return ( -
- -
{tradeElements}
- {priv.resources === 0 && ( -
- You have no resources to participate in this trade. + +
{tradeElements}
+ {priv.resources === 0 && ( +
+ You have no resources to participate in this trade. +
+ )} + {priv.resources !== 0 && ( +
+
+
Get
+
Give
+
Have
- )} - {priv.resources !== 0 && ( -
-
-
Get
-
Give
-
Have
-
- {transfers} -
- )} - -
+ {transfers} +
+ )} +
); }; diff --git a/server/routes/games.ts b/server/routes/games.ts index e80cb32..fbde815 100755 --- a/server/routes/games.ts +++ b/server/routes/games.ts @@ -16,7 +16,7 @@ import { } from "./games/constants"; import { getValidRoads, getValidCorners, isRuleEnabled } from "../util/validLocations"; -import { Player, Game, Session, CornerPlacement, RoadPlacement } from "./games/types"; +import { Player, Game, Session, CornerPlacement, RoadPlacement, Offer } from "./games/types"; import { newPlayer } from "./games/playerFactory"; import { normalizeIncoming, shuffleArray } from "./games/utils"; // import type { GameState } from './games/state'; // unused import removed during typing pass @@ -106,22 +106,27 @@ const processTies = (players: Player[]): boolean => { return ties; }; -const processGameOrder = (game: any, player: any, dice: number): any => { +const processGameOrder = (game: Game, player: Player, dice: number): any => { if (player.orderRoll) { return `You have already rolled for game order and are not in a tie.`; } player.orderRoll = dice; - player.order = player.order * 6 + dice; + player.order = (player.order || 0) * 6 + dice; - const players = []; + const players: Player[] = []; let doneRolling = true; - for (let key in game.players) { - if (!game.players[key].orderRoll) { + for (const key in game.players) { + const p = game.players[key]; + if (!p) { + doneRolling = false; + continue; + } + if (!p.orderRoll) { doneRolling = false; } - players.push(game.players[key]); + players.push(p); } /* If 'doneRolling' is FALSE then there are still players to roll */ @@ -155,12 +160,13 @@ const processGameOrder = (game: any, player: any, dice: number): any => { `Player order set to ` + players.map((player) => `${player.position}: ${player.name}`).join(", ") + `.` ); - game.playerOrder = players.map((player) => player.color); + game.playerOrder = players.map((player) => player.color as string); game.state = "initial-placement"; - game.direction = "forward"; + (game as any)["direction"] = "forward"; + const first = players[0]; game.turn = { - name: players[0].name, - color: players[0].color, + name: first?.name as string, + color: first?.color as string, }; setForSettlementPlacement(game, getValidCorners(game, "")); addActivity(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`); @@ -170,7 +176,7 @@ const processGameOrder = (game: any, player: any, dice: number): any => { sendUpdateToPlayers(game, { players: getFilteredPlayers(game), state: game.state, - direction: game.direction, + direction: (game as any)["direction"], turn: game.turn, chat: game.chat, activities: game.activities, @@ -235,8 +241,8 @@ const processVolcano = (game: Game, session: Session, dice: number[]): any => { }); }; -const roll = (game: any, session: any, dice?: number[] | undefined): any => { - const player = session.player, +const roll = (game: Game, session: Session, dice?: number[] | undefined): any => { + const player = session.player as Player, name = session.name ? session.name : "Unnamed"; if (!dice) { @@ -250,7 +256,7 @@ const roll = (game: any, session: any, dice?: number[] | undefined): any => { return undefined; case "game-order": - game.startTime = Date.now(); + (game as any)["startTime"] = Date.now(); addChatMessage(game, session, `${name} rolled ${dice[0]}.`); if (typeof dice[0] !== "number") { return `Invalid roll value.`; @@ -286,31 +292,29 @@ const roll = (game: any, session: any, dice?: number[] | undefined): any => { } }; -const sessionFromColor = (game: any, color: string): any | undefined => { - for (let key in game.sessions) { - if (game.sessions[key].color === color) { - return game.sessions[key]; +const sessionFromColor = (game: Game, color: string): Session | undefined => { + for (const key in game.sessions) { + const s = game.sessions[key]; + if (s && s.color === color) { + return s; } } return undefined; }; -const distributeResources = (game: any, roll: number): void => { +const distributeResources = (game: Game, roll: number): void => { console.log(`Roll: ${roll}`); /* Find which tiles have this roll */ - let tiles = []; - for (let i = 0; i < game.pipOrder.length; i++) { - let index = game.pipOrder[i]; - if (staticData.pips?.[index] && staticData.pips[index].roll === roll) { - if (game.robber === i) { - tiles.push({ robber: true, index: i }); - } else { - tiles.push({ robber: false, index: i }); - } + const matchedTiles: { robber: boolean; index: number }[] = []; + const pipOrder = game.pipOrder || []; + for (let i = 0; i < pipOrder.length; i++) { + const index = pipOrder[i]; + if (typeof index === "number" && staticData.pips?.[index] && staticData.pips[index].roll === roll) { + matchedTiles.push({ robber: game.robber === i, index: i }); } } - const receives: Record = { + const receives: Record> = { O: { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 }, R: { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 }, W: { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 }, @@ -319,65 +323,65 @@ const distributeResources = (game: any, roll: number): void => { }; /* Find which corners are on each tile */ - tiles.forEach((tile) => { - let shuffle = game.tileOrder[tile.index]; - const resource = game.tiles[shuffle]; + matchedTiles.forEach((tile) => { + const tileOrder = game.tileOrder || []; + const gameTiles = game.tiles || []; + const shuffle = tileOrder[tile.index]; + const resource = typeof shuffle === "number" ? gameTiles[shuffle] : undefined; const tileLayout = layout.tiles?.[tile.index]; tileLayout?.corners.forEach((cornerIndex: number) => { const active = game.placements.corners?.[cornerIndex]; - if (active && active.color) { + if (active && active.color && resource) { const count = active.type === "settlement" ? 1 : 2; if (!tile.robber) { - receives[active.color][resource.type] += count; + if (!receives[active.color]) receives[active.color] = { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 }; + if (resource && resource.type) (receives as any)[active.color][resource.type] += count; } else { - if (isRuleEnabled(game, `robin-hood-robber`) && game.players[active.color].points <= 2) { + const victim = game.players[active.color]; + if (isRuleEnabled(game, `robin-hood-robber`) && victim && (victim.points || 0) <= 2) { addChatMessage( game, null, - `Robber does not steal ${count} - ${resource.type} from ${game.players[active.color].name} ` + `due to Robin Hood Robber house rule.` + `Robber does not steal ${count} ${resource.type} from ${victim?.name} due to Robin Hood Robber house rule.` ); - console.log(`robin-hood-robber`, game.players[active.color], active.color); - receives[active.color][resource.type] += count; + if (resource && resource.type) (receives as any)[active.color][resource.type] += count; } else { trackTheft(game, active.color, "robber", resource.type, count); - receives["robber"][resource.type] += count; + if (resource && resource.type) (receives as any)["robber"][resource.type] += count; } } } }); }); - const robber = []; - for (let color in receives) { + const robberList: string[] = []; + for (const color in receives) { const entry = receives[color]; - if (!entry.wood && !entry.brick && !entry.sheep && !entry.wheat && !entry.stone) { + if (!entry || !(entry["wood"] || entry["brick"] || entry["sheep"] || entry["wheat"] || entry["stone"])) { continue; } - let messageParts: string[] = [], - session; - for (let type in entry) { - if (entry[type] === 0) { - continue; - } - + const messageParts: string[] = []; + let s: Session | undefined; + for (const type in entry) { + if (entry[type] === 0) continue; if (color !== "robber") { - session = sessionFromColor(game, color); - session.player[type] += entry[type]; - session.player.resources += entry[type]; + s = sessionFromColor(game, color); + if (!s || !s.player) continue; + (s.player as any)[type] = ((s.player as any)[type] || 0) + entry[type]; + (s.player as any).resources = ((s.player as any).resources || 0) + entry[type]; messageParts.push(`${entry[type]} ${type}`); } else { - robber.push(`${entry[type]} ${type}`); + robberList.push(`${entry[type]} ${type}`); } } - if (session) { - addChatMessage(game, session, `${session.name} receives ${messageParts.join(", ")} for pip ${roll}.`); + if (s) { + addChatMessage(game, s, `${s.name} receives ${messageParts.join(", ")} for pip ${roll}.`); } } - if (robber.length) { - addChatMessage(game, null, `That pesky ${game.robberName} Robber Roberson stole ${robber.join(", ")}!`); + if (robberList.length) { + addChatMessage(game, null, `That pesky ${game.robberName} Robber Roberson stole ${robberList.join(", ")}!`); } }; @@ -1231,7 +1235,7 @@ const setPlayerColor = (game: Game, session: Session, color: string): string | u if (!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 = [] as any[]; game.unselected.push(session); game.active = active; if (active === 1) { @@ -1579,9 +1583,15 @@ const calculateRoadLengths = (game: Game, session: Session): void => { } }; -const isCompatibleOffer = (player: any, offer: any): boolean => { - const isBank = offer.name === "The bank"; - let valid = player.gets.length === offer.gives.length && player.gives.length === offer.gets.length; +const isCompatibleOffer = (player: Player, offer: Offer): boolean => { + const isBank = (offer as any)["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; + + let valid = playerGetsLen === offerGivesLen && playerGivesLen === offerGetsLen; if (!valid) { console.log(`Gives and gets lengths do not match!`); @@ -1591,76 +1601,81 @@ const isCompatibleOffer = (player: any, offer: any): boolean => { console.log( { player: "Submitting player", - gets: player.gets, - gives: player.gives, + gets: (player as any)["gets"], + gives: (player as any)["gives"], }, { - name: offer.name, - gets: offer.gets, - gives: offer.gives, + name: (offer as any)["name"], + gets: (offer as any)["gets"], + gives: (offer as any)["gives"], } ); - player.gets.forEach((get: any) => { - if (!valid) { - return; + 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) + ) { + valid = false; + break; } - valid = - offer.gives.find((item: any) => (item.type === get.type || isBank) && item.count === get.count) !== undefined; - }); + } - if (valid) - player.gives.forEach((give: any) => { - if (!valid) { - return; + if (valid) { + for (const give of (player as any)["gives"] || []) { + if ( + !(offer as any)["gets"] || + !(offer as any)["gets"].some((item: any) => (item.type === give.type || isBank) && item.count === give.count) + ) { + valid = false; + break; } - valid = - offer.gets.find((item: any) => (item.type === give.type || isBank) && item.count === give.count) !== undefined; - }); + } + } return valid; }; -const isSameOffer = (player: any, offer: any): boolean => { - const isBank = offer.name === "The bank"; +const isSameOffer = (player: Player, offer: Offer): boolean => { + const isBank = (offer as any)["name"] === "The bank"; if (isBank) { return false; } - let same = - player.gets && - player.gives && - player.gets.length === offer.gets.length && - player.gives.length === offer.gives.length; - if (!same) { + if (!(player as any)["gets"] || !(player as any)["gives"] || !(offer as any)["gets"] || !(offer as any)["gives"]) { return false; } - player.gets.forEach((get: any) => { - if (!same) { - return; - } - same = offer.gets.find((item: any) => item.type === get.type && item.count === get.count) !== undefined; - }); + if ( + (player as any)["gets"].length !== (offer as any)["gets"].length || + (player as any)["gives"].length !== (offer as any)["gives"].length + ) { + return false; + } - if (same) - player.gives.forEach((give: any) => { - if (!same) { - return; - } - same = offer.gives.find((item: any) => item.type === give.type && item.count === give.count) !== undefined; - }); - return same; + for (const get of (player as any)["gets"]) { + if (!(offer as any)["gets"].find((item: any) => 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)) { + return false; + } + } + + return true; }; /* Verifies player can meet the offer */ -const checkPlayerOffer = (_game: any, player: any, offer: any): string | undefined => { +const checkPlayerOffer = (_game: Game, player: Player, offer: Offer): string | undefined => { let error: string | undefined = undefined; - const name = player.name; + const name = player.name || "Unknown"; console.log({ checkPlayerOffer: { - name: name, - player: player, + name, + player, gets: offer.gets, gives: offer.gives, sheep: player.sheep, @@ -1672,57 +1687,57 @@ const checkPlayerOffer = (_game: any, player: any, offer: any): string | undefin }, }); - offer.gives.forEach((give: any) => { - if (error) { - return; - } + for (const give of (offer as any)["gives"] || []) { + if (error) break; - if (!(give.type in player)) { + if (!(give.type in (player as any))) { error = `${give.type} is not a valid resource!`; - return; + break; } if (give.count <= 0) { error = `${give.count} must be more than 0!`; - return; + break; } - if (player[give.type] < give.count) { + if ((player as any)[give.type] < give.count) { error = `${name} does do not have ${give.count} ${give.type}!`; - return; + break; } - if (offer.gets.find((get: any) => give.type === get.type)) { + if (((offer as any)["gets"] || []).find((get: any) => give.type === get.type)) { error = `${name} can not give and get the same resource type!`; - return; + break; } - }); + } - if (!error) - offer.gets.forEach((get: any) => { - if (error) { - return; - } + if (!error) { + for (const get of (offer as any)["gets"] || []) { + if (error) break; if (get.count <= 0) { error = `${get.count} must be more than 0!`; - return; + break; } - if (offer.gives.find((give: any) => get.type === give.type)) { + if (((offer as any)["gives"] || []).find((give: any) => get.type === give.type)) { error = `${name} can not give and get the same resource type!`; + break; } - }); + } + } return error; }; -const canMeetOffer = (player: any, offer: any): boolean => { - for (let i = 0; i < offer.gets.length; i++) { - const get = offer.gets[i]; +const canMeetOffer = (player: Player, offer: Offer): boolean => { + for (const get of (offer as any)["gets"] || []) { if (get.type === "bank") { - if (player[player.gives[0].type] < get.count || get.count <= 0) { + 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) { return false; } - } else if (player[get.type] < get.count || get.count <= 0) { + } else if ((player as any)[get.type] < get.count || get.count <= 0) { return false; } } @@ -1783,11 +1798,11 @@ const setGameFromSignature = (game: any, border: string, pip: string, tile: stri return true; }; -const offerToString = (offer: any): string => { +const offerToString = (offer: Offer): string => { return ( - (offer.gives || []).map((item: any) => `${item.count} ${item.type}`).join(", ") + + (offer.gives || []).map((item) => `${item.count} ${item.type}`).join(", ") + " in exchange for " + - (offer.gets || []).map((item: any) => `${item.count} ${item.type}`).join(", ") + (offer.gets || []).map((item) => `${item.count} ${item.type}`).join(", ") ); }; @@ -1820,7 +1835,7 @@ router.put("/:id/:action/:value?", async (req, res) => { return res.status(400).send(error); }); -const startTrade = (game: any, session: any): string | undefined => { +const startTrade = (game: Game, session: Session): string | undefined => { /* Only the active player can begin trading */ if (game.turn.name !== session.name) { return `You cannot start trading negotiations when it is not your turn.`; @@ -1832,15 +1847,17 @@ const startTrade = (game: any, session: any): string | undefined => { game.turn.actions = ["trade"]; game.turn.limits = {}; for (let key in game.players) { - game.players[key].gives = []; - game.players[key].gets = []; - delete game.players[key].offerRejected; + const p = game.players[key]; + if (!p) continue; + (p as any)["gives"] = []; + (p as any)["gets"] = []; + delete (p as any)["offerRejected"]; } addActivity(game, session, `${session.name} requested to begin trading negotiations.`); return undefined; }; -const cancelTrade = (game: any, session: any): string | undefined => { +const cancelTrade = (game: Game, session: Session): string | undefined => { /* TODO: Perhaps 'cancel' is how a player can remove an offer... */ if (game.turn.name !== session.name) { return `Only the active player can cancel trading negotiations.`; @@ -1851,39 +1868,35 @@ const cancelTrade = (game: any, session: any): string | undefined => { return undefined; }; -const processOffer = (game: any, session: any, offer: any): string | undefined => { - let warning = checkPlayerOffer(game, session.player, offer); +const processOffer = (game: Game, session: Session, offer: Offer): string | undefined => { + const player = session.player as Player; + let warning = checkPlayerOffer(game, player, offer); if (warning) { return warning; } - - if (isSameOffer(session.player, offer)) { - console.log(session.player); + if (isSameOffer(player, offer)) { + console.log(player); return `You already have a pending offer submitted for ${offerToString(offer)}.`; } - session.player.gives = offer.gives; - session.player.gets = offer.gets; - session.player.offerRejected = {}; + (player as any)["gives"] = (offer as any)["gives"]; + (player as any)["gets"] = (offer as any)["gets"]; + (player as any)["offerRejected"] = {}; - if (game.turn.color === session.color) { - game.turn.offer = offer; + if ((game.turn as any)["color"] === session.color) { + (game.turn as any)["offer"] = offer; } - /* If this offer matches what another player wants, clear rejection - * on of that other player's offer */ - for (let color in game.players) { - if (color === session.color) { - continue; - } + /* 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.status !== "Active") { - continue; - } + if (!other) continue; + if ((other as any)["status"] !== "Active") continue; /* Comparison reverses give/get order */ - if (isSameOffer(other, { gives: offer.gets, gets: offer.gives })) { - if (other.offerRejected) { - delete other.offerRejected[session.color]; + 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]; } } } @@ -1892,72 +1905,79 @@ const processOffer = (game: any, session: any, offer: any): string | undefined = return undefined; }; -const rejectOffer = (game: any, session: any, offer: any): void => { +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.color]; - if (!other.offerRejected) { - other.offerRejected = {}; + const other = game.players[(offer as any)["color"] as string]; + if (!other) return; + if (!(other as any)["offerRejected"]) { + (other as any)["offerRejected"] = {}; } - other.offerRejected[session.color] = true; - if (!session.player.offerRejected) { - session.player.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.offerRejected[offer.color] = true; + (session.player as any)["offerRejected"][(offer as any)["color"] as string] = true; addActivity(game, session, `${session.name} rejected ${other.name}'s offer.`); }; -const acceptOffer = (game: any, session: any, offer: any): string | undefined => { +const acceptOffer = (game: Game, session: Session, offer: Offer): string | undefined => { const name = session.name, - player = session.player; + player = session.player as Player; if (game.turn.name !== name) { return `Only the active player can accept an offer.`; } - let target; + let target: any = undefined; console.log({ description: offerToString(offer) }); - let warning = checkPlayerOffer(game, session.player, offer); + let warning = checkPlayerOffer(game, player, offer); if (warning) { return warning; } if ( - !isCompatibleOffer(session.player, { - name: offer.name, - gives: offer.gets, - gets: offer.gives, - }) + !isCompatibleOffer(player, { + name: (offer as any)["name"], + gives: (offer as any)["gets"], + gets: (offer as any)["gives"], + } as Offer) ) { return `Unfortunately, trades were re-negotiated in transit and 1 ` + `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.name || offer.name !== "The bank") { - target = game.players[offer.color]; - if (target.offerRejected && offer.color in target.offerRejected) { + if (!(offer as any)["name"] || (offer as any)["name"] !== "The bank") { + target = game.players[(offer as any)["color"] as string]; + if (!target) return `Invalid trade target.`; + if ((target as any)["offerRejected"] && (offer as any)["color"] in (target as any)["offerRejected"]) { return `${target.name} rejected this offer.`; } - if (!isCompatibleOffer(target, offer)) { + if (!isCompatibleOffer(target as Player, offer)) { return `Unfortunately, trades were re-negotiated in transit and ` + `the deal is invalid!`; } - warning = checkPlayerOffer(game, target, { - gives: offer.gets, - gets: offer.gives, - }); + warning = checkPlayerOffer( + game, + target as Player, + { + gives: offer.gets, + gets: offer.gives, + } as Offer + ); if (warning) { return warning; } - if (!isSameOffer(target, { gives: offer.gets, gets: offer.gives })) { + if (!isSameOffer(target as Player, { gives: (offer as any)["gets"], gets: (offer as any)["gives"] } as Offer)) { console.log({ target, offer }); return `These terms were not agreed to by ${target.name}!`; } - if (!canMeetOffer(target, player)) { + if (!canMeetOffer(target as Player, player as any)) { return `${target.name} cannot meet the terms.`; } } else { @@ -1967,44 +1987,48 @@ const acceptOffer = (game: any, session: any, offer: any): string | undefined => debugChat(game, "Before trade"); /* Transfer goods */ - offer.gets.forEach((item: any) => { - if (target.name !== "The bank") { - target[item.type] -= item.count; - target.resources -= item.count; + 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; } - player[item.type] += item.count; - player.resources += item.count; - }); - offer.gives.forEach((item: any) => { - if (target.name !== "The bank") { - target[item.type] += item.count; - target.resources += item.count; + (player as any)[item.type] += item.count; + (player as any).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; } - player[item.type] -= item.count; - player.resources -= item.count; - }); + (player as any)[item.type] -= item.count; + (player as any).resources -= item.count; + } - const from = offer.name === "The bank" ? "the bank" : offer.name; + const from = (offer as any)["name"] === "The bank" ? "the bank" : (offer as any)["name"]; addChatMessage(game, session, `${session.name} traded ` + ` ${offerToString(offer)} ` + `from ${from}.`); addActivity(game, session, `${session.name} accepted a trade from ${from}.`); - delete game.turn.offer; + delete (game.turn as any)["offer"]; if (target) { - delete target.gives; - delete target.gets; + delete (target as any).gives; + delete (target as any).gets; } - delete session.player.gives; - delete session.player.gets; - delete game.turn.offer; + if (session.player) { + delete (session.player as any)["gives"]; + delete (session.player as any)["gets"]; + } + delete (game.turn as any)["offer"]; debugChat(game, "After trade"); /* Debug!!! */ - for (let key in game.players) { - if (game.players[key].state !== "Active") { + 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 (game.players[key][type] < 0) { + if ((p as any)[type] < 0) { throw new Error(`Player resources are below zero! BUG BUG BUG!`); } }); @@ -2013,7 +2037,7 @@ const acceptOffer = (game: any, session: any, offer: any): string | undefined => return undefined; }; -const trade = (game: any, session: any, action: string, offer: any) => { +const trade = (game: Game, session: Session, action: string, offer?: Offer): string | undefined => { if (game.state !== "normal") { return `Game not in correct state to begin trading.`; } @@ -2029,22 +2053,25 @@ const trade = (game: any, session: any, action: string, offer: any) => { /* Any player can make an offer */ if (action === "offer") { - return processOffer(game, session, offer); + return processOffer(game, session, offer as Offer); } /* Any player can reject an offer */ if (action === "reject") { - return rejectOffer(game, session, offer); + rejectOffer(game, session, offer as Offer); + return undefined; } /* Only the active player can accept an offer */ if (action === "accept") { - if (offer.name === "The bank") { - session.player.gets = offer.gets; - session.player.gives = offer.gives; + 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"]; } - return acceptOffer(game, session, offer); + return acceptOffer(game, session, offer as Offer); } + return undefined; }; const clearTimeNotice = (game: any, session: any): string | undefined => { @@ -2531,6 +2558,7 @@ const playCard = (game: any, session: any, card: any): 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: any = session.player; + const anyGame: any = game as any; if (typeof index === "string") index = parseInt(index); if (game.state !== "initial-placement" && game.state !== "normal") { @@ -2542,7 +2570,11 @@ const placeSettlement = (game: Game, session: Session, index: number | string): } /* index out of range... */ - if (game.placements.corners[index] === undefined) { + if ( + !anyGame.placements || + anyGame.placements.corners === undefined || + anyGame.placements.corners[index] === undefined + ) { return `You have requested to place a settlement illegally!`; } @@ -2550,9 +2582,11 @@ const placeSettlement = (game: Game, session: Session, index: number | string): 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]; + const corner = anyGame.placements.corners[index]; if (corner.color) { - return `This location already has a settlement belonging to ${game.players[corner.color].name}!`; + const owner = game.players && game.players[corner.color]; + const ownerName = owner ? owner.name : "unknown"; + return `This location already has a settlement belonging to ${ownerName}!`; } if (!player.banks) { @@ -2592,8 +2626,8 @@ const placeSettlement = (game: Game, session: Session, index: number | string): const banks = layout.corners?.[index]?.banks; if (banks && banks.length) { banks.forEach((bank: any) => { - const border = game.borderOrder[Math.floor(bank / 3)], - type = game.borders?.[border]?.[bank % 3]; + const border = anyGame.borderOrder[Math.floor(bank / 3)], + type = anyGame.borders?.[border]?.[bank % 3]; console.log(`${session.id}: Bank ${bank} = ${type}`); if (!type) { console.log(`${session.id}: Bank ${bank}`); @@ -2607,11 +2641,11 @@ const placeSettlement = (game: Game, session: Session, index: number | string): player.ports++; 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; + console.log(`Checking port-of-call`, player.ports, anyGame.mostPorts); + if (player.ports >= 3 && (!anyGame.mostPorts || player.ports > anyGame.mostPortCount)) { + if (anyGame.mostPorts !== session.color) { + anyGame.mostPorts = session.color; + anyGame.mostPortCount = player.ports; addChatMessage(game, session, `${session.name} now has the most ports (${player.ports})!`); } } @@ -2627,17 +2661,17 @@ const placeSettlement = (game: Game, session: Session, index: number | string): } calculateRoadLengths(game, session); } else if (game.state === "initial-placement") { - if (game.direction && game.direction === "backward") { - session.initialSettlement = index; + if (anyGame.direction && anyGame.direction === "backward") { + (session as any).initialSettlement = index; } - corner.color = session.color; + corner.color = session.color || ""; corner.type = "settlement"; let bankType = undefined; const banks2 = layout.corners?.[index]?.banks; if (banks2 && banks2.length) { banks2.forEach((bank: any) => { - const border = game.borderOrder[Math.floor(bank / 3)], - type = game.borders?.[border]?.[bank % 3]; + const border = anyGame.borderOrder[Math.floor(bank / 3)], + type = anyGame.borders?.[border]?.[bank % 3]; console.log(`${session.id}: Bank ${bank} = ${type}`); if (!type) { return; @@ -2649,7 +2683,7 @@ const placeSettlement = (game: Game, session: Session, index: number | string): player.ports++; }); } - player.settlements = (player.settlements || 0) - 1; + player.settlements = (player.settlements || 0) - 1; if (bankType) { addActivity( game, @@ -3273,53 +3307,37 @@ const placeCity = (game: any, session: any, index: any): string | undefined => { return undefined; }; -const ping = (session: any) => { +const ping = (session: Session) => { if (!session.ws) { console.log(`[no socket] Not sending ping to ${session.name} -- connection does not exist.`); return; } - session.ping = Date.now(); + (session as any)["ping"] = Date.now(); // console.log(`Sending ping to ${session.name}`); - session.ws.send(JSON.stringify({ type: "ping", ping: session.ping })); + try { + session.ws.send(JSON.stringify({ type: "ping", ping: (session as any)["ping"] })); + } catch (e) { + // ignore send errors + } + if (session.keepAlive) { clearTimeout(session.keepAlive); } session.keepAlive = setTimeout(() => { - ping(session); - }, 2500); -}; - -const wsInactive = (game: any, req: any) => { - void game; // referenced for API completeness - const playerCookie = req.cookies && (req.cookies as any)["player"] ? String((req.cookies as any)["player"]) : ""; - const session = getSession(game, playerCookie || ""); - - if (session && session.ws) { - console.log(`Closing WebSocket to ${session.name} due to inactivity.`); + // mark the session as inactive if the keepAlive fires try { - // Defensive: close only if a socket exists; swallow any errors from closing if (session.ws) { - try { - session.ws.close(); - } catch (e) { - /* ignore close errors */ - } + session.ws.close?.(); } } catch (e) { /* ignore */ } session.ws = undefined; - } - - /* Prevent future pings */ - if (req.keepAlive) { - clearTimeout(req.keepAlive); - } + }, 20000); }; -// keep a void reference so linters/typecheckers don't complain about unused declarations -void wsInactive; +// wsInactive not present in this refactor; no-op placeholder removed const setGameState = (game: any, session: any, state: any): string | undefined => { if (!state) {