1
0

Refactoring continues

This commit is contained in:
James Ketr 2025-10-07 17:17:59 -07:00
parent 4d061a8054
commit 5312b0dc7f
4 changed files with 301 additions and 295 deletions

View File

@ -394,14 +394,7 @@ const RoomView = (props: RoomProps) => {
</Paper> </Paper>
)} )}
{name && <PlayerList />} {name && <PlayerList />}
{/* Trade is an untyped JS component; assert its type to avoid `any` */} {tradeActive && <Trade />}
{(() => {
const TradeComponent = Trade as unknown as React.ComponentType<{
tradeActive: boolean;
setTradeActive: (v: boolean) => void;
}>;
return <TradeComponent tradeActive={tradeActive} setTradeActive={setTradeActive} />;
})()}
{name !== "" && <Chat />} {name !== "" && <Chat />}
{/* name !== "" && <VideoFeeds/> */} {/* name !== "" && <VideoFeeds/> */}
{loaded && ( {loaded && (

View File

@ -7,7 +7,7 @@
} }
.Trade > * { .Trade > * {
max-height: calc(100vh - 2rem); max-height: calc(100dvh - 2rem);
overflow: auto; overflow: auto;
width: 32em; width: 32em;
display: inline-flex; display: inline-flex;
@ -100,9 +100,6 @@
line-height: 1.5em; line-height: 1.5em;
} }
.Trade .Resource.None {
/* filter: brightness(70%); */
}
.Trade .PlayerColor { .Trade .PlayerColor {
align-self: center; align-self: center;

View File

@ -601,26 +601,24 @@ const Trade: React.FC = () => {
}); });
return ( return (
<div className="Trade"> <Paper className="Trade">
<Paper> <div className="PlayerList">{tradeElements}</div>
<div className="PlayerList">{tradeElements}</div> {priv.resources === 0 && (
{priv.resources === 0 && ( <div>
<div> <b>You have no resources to participate in this trade.</b>
<b>You have no resources to participate in this trade.</b> </div>
)}
{priv.resources !== 0 && (
<div className="Transfers">
<div className="GiveGet">
<div>Get</div>
<div>Give</div>
<div>Have</div>
</div> </div>
)} {transfers}
{priv.resources !== 0 && ( </div>
<div className="Transfers"> )}
<div className="GiveGet"> </Paper>
<div>Get</div>
<div>Give</div>
<div>Have</div>
</div>
{transfers}
</div>
)}
</Paper>
</div>
); );
}; };

View File

@ -16,7 +16,7 @@ import {
} from "./games/constants"; } from "./games/constants";
import { getValidRoads, getValidCorners, isRuleEnabled } from "../util/validLocations"; 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 { newPlayer } from "./games/playerFactory";
import { normalizeIncoming, shuffleArray } from "./games/utils"; import { normalizeIncoming, shuffleArray } from "./games/utils";
// import type { GameState } from './games/state'; // unused import removed during typing pass // import type { GameState } from './games/state'; // unused import removed during typing pass
@ -106,22 +106,27 @@ const processTies = (players: Player[]): boolean => {
return ties; return ties;
}; };
const processGameOrder = (game: any, player: any, dice: number): any => { const processGameOrder = (game: Game, player: Player, dice: number): any => {
if (player.orderRoll) { if (player.orderRoll) {
return `You have already rolled for game order and are not in a tie.`; return `You have already rolled for game order and are not in a tie.`;
} }
player.orderRoll = dice; player.orderRoll = dice;
player.order = player.order * 6 + dice; player.order = (player.order || 0) * 6 + dice;
const players = []; const players: Player[] = [];
let doneRolling = true; let doneRolling = true;
for (let key in game.players) { for (const key in game.players) {
if (!game.players[key].orderRoll) { const p = game.players[key];
if (!p) {
doneRolling = false;
continue;
}
if (!p.orderRoll) {
doneRolling = false; doneRolling = false;
} }
players.push(game.players[key]); players.push(p);
} }
/* If 'doneRolling' is FALSE then there are still players to roll */ /* 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(", ") + `.` `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.state = "initial-placement";
game.direction = "forward"; (game as any)["direction"] = "forward";
const first = players[0];
game.turn = { game.turn = {
name: players[0].name, name: first?.name as string,
color: players[0].color, color: first?.color as string,
}; };
setForSettlementPlacement(game, getValidCorners(game, "")); 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!`);
@ -170,7 +176,7 @@ const processGameOrder = (game: any, player: any, dice: number): any => {
sendUpdateToPlayers(game, { sendUpdateToPlayers(game, {
players: getFilteredPlayers(game), players: getFilteredPlayers(game),
state: game.state, state: game.state,
direction: game.direction, direction: (game as any)["direction"],
turn: game.turn, turn: game.turn,
chat: game.chat, chat: game.chat,
activities: game.activities, 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 roll = (game: Game, session: Session, dice?: number[] | undefined): any => {
const player = session.player, const player = session.player as Player,
name = session.name ? session.name : "Unnamed"; name = session.name ? session.name : "Unnamed";
if (!dice) { if (!dice) {
@ -250,7 +256,7 @@ const roll = (game: any, session: any, dice?: number[] | undefined): any => {
return undefined; return undefined;
case "game-order": case "game-order":
game.startTime = Date.now(); (game as any)["startTime"] = Date.now();
addChatMessage(game, session, `${name} rolled ${dice[0]}.`); addChatMessage(game, session, `${name} rolled ${dice[0]}.`);
if (typeof dice[0] !== "number") { if (typeof dice[0] !== "number") {
return `Invalid roll value.`; 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 => { const sessionFromColor = (game: Game, color: string): Session | undefined => {
for (let key in game.sessions) { for (const key in game.sessions) {
if (game.sessions[key].color === color) { const s = game.sessions[key];
return game.sessions[key]; if (s && s.color === color) {
return s;
} }
} }
return undefined; return undefined;
}; };
const distributeResources = (game: any, roll: number): void => { const distributeResources = (game: Game, roll: number): void => {
console.log(`Roll: ${roll}`); console.log(`Roll: ${roll}`);
/* Find which tiles have this roll */ /* Find which tiles have this roll */
let tiles = []; const matchedTiles: { robber: boolean; index: number }[] = [];
for (let i = 0; i < game.pipOrder.length; i++) { const pipOrder = game.pipOrder || [];
let index = game.pipOrder[i]; for (let i = 0; i < pipOrder.length; i++) {
if (staticData.pips?.[index] && staticData.pips[index].roll === roll) { const index = pipOrder[i];
if (game.robber === i) { if (typeof index === "number" && staticData.pips?.[index] && staticData.pips[index].roll === roll) {
tiles.push({ robber: true, index: i }); matchedTiles.push({ robber: game.robber === i, index: i });
} else {
tiles.push({ robber: false, index: i });
}
} }
} }
const receives: Record<string, any> = { const receives: Record<string, Record<string, number>> = {
O: { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 }, O: { wood: 0, brick: 0, sheep: 0, wheat: 0, stone: 0 },
R: { 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 }, 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 */ /* Find which corners are on each tile */
tiles.forEach((tile) => { matchedTiles.forEach((tile) => {
let shuffle = game.tileOrder[tile.index]; const tileOrder = game.tileOrder || [];
const resource = game.tiles[shuffle]; const gameTiles = game.tiles || [];
const shuffle = tileOrder[tile.index];
const resource = typeof shuffle === "number" ? gameTiles[shuffle] : undefined;
const tileLayout = layout.tiles?.[tile.index]; const tileLayout = layout.tiles?.[tile.index];
tileLayout?.corners.forEach((cornerIndex: number) => { tileLayout?.corners.forEach((cornerIndex: number) => {
const active = game.placements.corners?.[cornerIndex]; const active = game.placements.corners?.[cornerIndex];
if (active && active.color) { if (active && active.color && resource) {
const count = active.type === "settlement" ? 1 : 2; const count = active.type === "settlement" ? 1 : 2;
if (!tile.robber) { 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 { } 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( addChatMessage(
game, game,
null, null,
`Robber does not steal ${count} `Robber does not steal ${count} ${resource.type} from ${victim?.name} due to Robin Hood Robber house rule.`
${resource.type} from ${game.players[active.color].name} ` + `due to Robin Hood Robber house rule.`
); );
console.log(`robin-hood-robber`, game.players[active.color], active.color); if (resource && resource.type) (receives as any)[active.color][resource.type] += count;
receives[active.color][resource.type] += count;
} else { } else {
trackTheft(game, active.color, "robber", resource.type, count); 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 = []; const robberList: string[] = [];
for (let color in receives) { for (const color in receives) {
const entry = receives[color]; 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; continue;
} }
let messageParts: string[] = [], const messageParts: string[] = [];
session; let s: Session | undefined;
for (let type in entry) { for (const type in entry) {
if (entry[type] === 0) { if (entry[type] === 0) continue;
continue;
}
if (color !== "robber") { if (color !== "robber") {
session = sessionFromColor(game, color); s = sessionFromColor(game, color);
session.player[type] += entry[type]; if (!s || !s.player) continue;
session.player.resources += entry[type]; (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}`); messageParts.push(`${entry[type]} ${type}`);
} else { } else {
robber.push(`${entry[type]} ${type}`); robberList.push(`${entry[type]} ${type}`);
} }
} }
if (session) { if (s) {
addChatMessage(game, session, `${session.name} receives ${messageParts.join(", ")} for pip ${roll}.`); addChatMessage(game, s, `${s.name} receives ${messageParts.join(", ")} for pip ${roll}.`);
} }
} }
if (robber.length) { if (robberList.length) {
addChatMessage(game, null, `That pesky ${game.robberName} Robber Roberson stole ${robber.join(", ")}!`); 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) { 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); addChatMessage(game, null, msg);
if (!game.unselected) game.unselected = [] as any[]; if (!game.unselected) game.unselected = [] as any[];
game.unselected.push(session); game.unselected.push(session);
game.active = active; game.active = active;
if (active === 1) { if (active === 1) {
@ -1579,9 +1583,15 @@ const calculateRoadLengths = (game: Game, session: Session): void => {
} }
}; };
const isCompatibleOffer = (player: any, offer: any): boolean => { const isCompatibleOffer = (player: Player, offer: Offer): boolean => {
const isBank = offer.name === "The bank"; const isBank = (offer as any)["name"] === "The bank";
let valid = player.gets.length === offer.gives.length && player.gives.length === offer.gets.length;
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) { if (!valid) {
console.log(`Gives and gets lengths do not match!`); console.log(`Gives and gets lengths do not match!`);
@ -1591,76 +1601,81 @@ const isCompatibleOffer = (player: any, offer: any): boolean => {
console.log( console.log(
{ {
player: "Submitting player", player: "Submitting player",
gets: player.gets, gets: (player as any)["gets"],
gives: player.gives, gives: (player as any)["gives"],
}, },
{ {
name: offer.name, name: (offer as any)["name"],
gets: offer.gets, gets: (offer as any)["gets"],
gives: offer.gives, gives: (offer as any)["gives"],
} }
); );
player.gets.forEach((get: any) => { for (const get of (player as any)["gets"] || []) {
if (!valid) { if (
return; !(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) if (valid) {
player.gives.forEach((give: any) => { for (const give of (player as any)["gives"] || []) {
if (!valid) { if (
return; !(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; return valid;
}; };
const isSameOffer = (player: any, offer: any): boolean => { const isSameOffer = (player: Player, offer: Offer): boolean => {
const isBank = offer.name === "The bank"; const isBank = (offer as any)["name"] === "The bank";
if (isBank) { if (isBank) {
return false; 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; return false;
} }
player.gets.forEach((get: any) => { if (
if (!same) { (player as any)["gets"].length !== (offer as any)["gets"].length ||
return; (player as any)["gives"].length !== (offer as any)["gives"].length
} ) {
same = offer.gets.find((item: any) => item.type === get.type && item.count === get.count) !== undefined; return false;
}); }
if (same) for (const get of (player as any)["gets"]) {
player.gives.forEach((give: any) => { if (!(offer as any)["gets"].find((item: any) => item.type === get.type && item.count === get.count)) {
if (!same) { return false;
return; }
} }
same = offer.gives.find((item: any) => item.type === give.type && item.count === give.count) !== undefined;
}); for (const give of (player as any)["gives"]) {
return same; 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 */ /* 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; let error: string | undefined = undefined;
const name = player.name; const name = player.name || "Unknown";
console.log({ console.log({
checkPlayerOffer: { checkPlayerOffer: {
name: name, name,
player: player, player,
gets: offer.gets, gets: offer.gets,
gives: offer.gives, gives: offer.gives,
sheep: player.sheep, sheep: player.sheep,
@ -1672,57 +1687,57 @@ const checkPlayerOffer = (_game: any, player: any, offer: any): string | undefin
}, },
}); });
offer.gives.forEach((give: any) => { for (const give of (offer as any)["gives"] || []) {
if (error) { if (error) break;
return;
}
if (!(give.type in player)) { if (!(give.type in (player as any))) {
error = `${give.type} is not a valid resource!`; error = `${give.type} is not a valid resource!`;
return; break;
} }
if (give.count <= 0) { if (give.count <= 0) {
error = `${give.count} must be more than 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}!`; 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!`; error = `${name} can not give and get the same resource type!`;
return; break;
} }
}); }
if (!error) if (!error) {
offer.gets.forEach((get: any) => { for (const get of (offer as any)["gets"] || []) {
if (error) { if (error) break;
return;
}
if (get.count <= 0) { if (get.count <= 0) {
error = `${get.count} must be more than 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!`; error = `${name} can not give and get the same resource type!`;
break;
} }
}); }
}
return error; return error;
}; };
const canMeetOffer = (player: any, offer: any): boolean => { const canMeetOffer = (player: Player, offer: Offer): boolean => {
for (let i = 0; i < offer.gets.length; i++) { for (const get of (offer as any)["gets"] || []) {
const get = offer.gets[i];
if (get.type === "bank") { 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; 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; return false;
} }
} }
@ -1783,11 +1798,11 @@ const setGameFromSignature = (game: any, border: string, pip: string, tile: stri
return true; return true;
}; };
const offerToString = (offer: any): string => { const offerToString = (offer: Offer): string => {
return ( return (
(offer.gives || []).map((item: any) => `${item.count} ${item.type}`).join(", ") + (offer.gives || []).map((item) => `${item.count} ${item.type}`).join(", ") +
" in exchange for " + " 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); 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 */ /* Only the active player can begin trading */
if (game.turn.name !== session.name) { if (game.turn.name !== session.name) {
return `You cannot start trading negotiations when it is not your turn.`; 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.actions = ["trade"];
game.turn.limits = {}; game.turn.limits = {};
for (let key in game.players) { for (let key in game.players) {
game.players[key].gives = []; const p = game.players[key];
game.players[key].gets = []; if (!p) continue;
delete game.players[key].offerRejected; (p as any)["gives"] = [];
(p as any)["gets"] = [];
delete (p as any)["offerRejected"];
} }
addActivity(game, session, `${session.name} requested to begin trading negotiations.`); addActivity(game, session, `${session.name} requested to begin trading negotiations.`);
return undefined; 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... */ /* TODO: Perhaps 'cancel' is how a player can remove an offer... */
if (game.turn.name !== session.name) { if (game.turn.name !== session.name) {
return `Only the active player can cancel trading negotiations.`; return `Only the active player can cancel trading negotiations.`;
@ -1851,39 +1868,35 @@ const cancelTrade = (game: any, session: any): string | undefined => {
return undefined; return undefined;
}; };
const processOffer = (game: any, session: any, offer: any): string | undefined => { const processOffer = (game: Game, session: Session, offer: Offer): string | undefined => {
let warning = checkPlayerOffer(game, session.player, offer); const player = session.player as Player;
let warning = checkPlayerOffer(game, player, offer);
if (warning) { if (warning) {
return warning; return warning;
} }
if (isSameOffer(player, offer)) {
if (isSameOffer(session.player, offer)) { console.log(player);
console.log(session.player);
return `You already have a pending offer submitted for ${offerToString(offer)}.`; return `You already have a pending offer submitted for ${offerToString(offer)}.`;
} }
session.player.gives = offer.gives; (player as any)["gives"] = (offer as any)["gives"];
session.player.gets = offer.gets; (player as any)["gets"] = (offer as any)["gets"];
session.player.offerRejected = {}; (player as any)["offerRejected"] = {};
if (game.turn.color === session.color) { if ((game.turn as any)["color"] === session.color) {
game.turn.offer = offer; (game.turn as any)["offer"] = offer;
} }
/* If this offer matches what another player wants, clear rejection /* If this offer matches what another player wants, clear rejection on that other player's offer */
* on of that other player's offer */ for (const color in game.players) {
for (let color in game.players) { if (color === session.color) continue;
if (color === session.color) {
continue;
}
const other = game.players[color]; const other = game.players[color];
if (other.status !== "Active") { if (!other) continue;
continue; if ((other as any)["status"] !== "Active") continue;
}
/* Comparison reverses give/get order */ /* Comparison reverses give/get order */
if (isSameOffer(other, { gives: offer.gets, gets: offer.gives })) { if (isSameOffer(other, { gives: (offer as any)["gets"], gets: (offer as any)["gives"] } as Offer)) {
if (other.offerRejected) { if ((other as any)["offerRejected"]) {
delete other.offerRejected[session.color]; 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; 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 */ /* If the active player rejected an offer, they rejected another player */
const other = game.players[offer.color]; const other = game.players[(offer as any)["color"] as string];
if (!other.offerRejected) { if (!other) return;
other.offerRejected = {}; if (!(other as any)["offerRejected"]) {
(other as any)["offerRejected"] = {};
} }
other.offerRejected[session.color] = true; (other as any)["offerRejected"][session.color as string] = true;
if (!session.player.offerRejected) { if (!session.player) session.player = {} as Player;
session.player.offerRejected = {}; 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.`); 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, const name = session.name,
player = session.player; player = session.player as Player;
if (game.turn.name !== name) { if (game.turn.name !== name) {
return `Only the active player can accept an offer.`; return `Only the active player can accept an offer.`;
} }
let target; let target: any = undefined;
console.log({ description: offerToString(offer) }); console.log({ description: offerToString(offer) });
let warning = checkPlayerOffer(game, session.player, offer); let warning = checkPlayerOffer(game, player, offer);
if (warning) { if (warning) {
return warning; return warning;
} }
if ( if (
!isCompatibleOffer(session.player, { !isCompatibleOffer(player, {
name: offer.name, name: (offer as any)["name"],
gives: offer.gets, gives: (offer as any)["gets"],
gets: offer.gives, gets: (offer as any)["gives"],
}) } as Offer)
) { ) {
return `Unfortunately, trades were re-negotiated in transit and 1 ` + `the deal is invalid!`; 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 /* Verify that the offer sent by the active player matches what
* the latest offer was that was received by the requesting player */ * the latest offer was that was received by the requesting player */
if (!offer.name || offer.name !== "The bank") { if (!(offer as any)["name"] || (offer as any)["name"] !== "The bank") {
target = game.players[offer.color]; target = game.players[(offer as any)["color"] as string];
if (target.offerRejected && offer.color in target.offerRejected) { 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.`; 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!`; return `Unfortunately, trades were re-negotiated in transit and ` + `the deal is invalid!`;
} }
warning = checkPlayerOffer(game, target, { warning = checkPlayerOffer(
gives: offer.gets, game,
gets: offer.gives, target as Player,
}); {
gives: offer.gets,
gets: offer.gives,
} as Offer
);
if (warning) { if (warning) {
return 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 }); console.log({ target, offer });
return `These terms were not agreed to by ${target.name}!`; 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.`; return `${target.name} cannot meet the terms.`;
} }
} else { } else {
@ -1967,44 +1987,48 @@ const acceptOffer = (game: any, session: any, offer: any): string | undefined =>
debugChat(game, "Before trade"); debugChat(game, "Before trade");
/* Transfer goods */ /* Transfer goods */
offer.gets.forEach((item: any) => { for (const item of (offer as any)["gets"] || []) {
if (target.name !== "The bank") { if ((target as any)["name"] !== "The bank") {
target[item.type] -= item.count; (target as any)[item.type] -= item.count;
target.resources -= item.count; (target as any).resources -= item.count;
} }
player[item.type] += item.count; (player as any)[item.type] += item.count;
player.resources += item.count; (player as any).resources += item.count;
}); }
offer.gives.forEach((item: any) => { for (const item of (offer as any)["gives"] || []) {
if (target.name !== "The bank") { if ((target as any)["name"] !== "The bank") {
target[item.type] += item.count; (target as any)[item.type] += item.count;
target.resources += item.count; (target as any).resources += item.count;
} }
player[item.type] -= item.count; (player as any)[item.type] -= item.count;
player.resources -= 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}.`); addChatMessage(game, session, `${session.name} traded ` + ` ${offerToString(offer)} ` + `from ${from}.`);
addActivity(game, session, `${session.name} accepted a trade from ${from}.`); addActivity(game, session, `${session.name} accepted a trade from ${from}.`);
delete game.turn.offer; delete (game.turn as any)["offer"];
if (target) { if (target) {
delete target.gives; delete (target as any).gives;
delete target.gets; delete (target as any).gets;
} }
delete session.player.gives; if (session.player) {
delete session.player.gets; delete (session.player as any)["gives"];
delete game.turn.offer; delete (session.player as any)["gets"];
}
delete (game.turn as any)["offer"];
debugChat(game, "After trade"); debugChat(game, "After trade");
/* Debug!!! */ /* Debug!!! */
for (let key in game.players) { for (const key in game.players) {
if (game.players[key].state !== "Active") { const p = game.players[key];
if (!p) continue;
if ((p as any)["state"] !== "Active") {
continue; continue;
} }
types.forEach((type) => { 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!`); 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; 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") { if (game.state !== "normal") {
return `Game not in correct state to begin trading.`; 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 */ /* Any player can make an offer */
if (action === "offer") { if (action === "offer") {
return processOffer(game, session, offer); return processOffer(game, session, offer as Offer);
} }
/* Any player can reject an offer */ /* Any player can reject an offer */
if (action === "reject") { if (action === "reject") {
return rejectOffer(game, session, offer); rejectOffer(game, session, offer as Offer);
return undefined;
} }
/* Only the active player can accept an offer */ /* Only the active player can accept an offer */
if (action === "accept") { if (action === "accept") {
if (offer.name === "The bank") { if (offer && (offer as any)["name"] === "The bank") {
session.player.gets = offer.gets; if (!session.player) session.player = {} as Player;
session.player.gives = offer.gives; (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 => { 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 => { const placeSettlement = (game: Game, session: Session, index: number | string): string | undefined => {
if (!session.player) return `You are not playing a player.`; if (!session.player) return `You are not playing a player.`;
const player: any = session.player; const player: any = session.player;
const anyGame: any = game as any;
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") {
@ -2542,7 +2570,11 @@ const placeSettlement = (game: Game, session: Session, index: number | string):
} }
/* index out of range... */ /* 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!`; 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) { 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.`; 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) { 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) { if (!player.banks) {
@ -2592,8 +2626,8 @@ const placeSettlement = (game: Game, session: Session, index: number | string):
const banks = layout.corners?.[index]?.banks; const banks = layout.corners?.[index]?.banks;
if (banks && banks.length) { if (banks && banks.length) {
banks.forEach((bank: any) => { banks.forEach((bank: any) => {
const border = game.borderOrder[Math.floor(bank / 3)], const border = anyGame.borderOrder[Math.floor(bank / 3)],
type = game.borders?.[border]?.[bank % 3]; type = anyGame.borders?.[border]?.[bank % 3];
console.log(`${session.id}: Bank ${bank} = ${type}`); console.log(`${session.id}: Bank ${bank} = ${type}`);
if (!type) { if (!type) {
console.log(`${session.id}: Bank ${bank}`); console.log(`${session.id}: Bank ${bank}`);
@ -2607,11 +2641,11 @@ const placeSettlement = (game: Game, session: Session, index: number | string):
player.ports++; player.ports++;
if (isRuleEnabled(game, "port-of-call")) { if (isRuleEnabled(game, "port-of-call")) {
console.log(`Checking port-of-call`, player.ports, game.mostPorts); console.log(`Checking port-of-call`, player.ports, anyGame.mostPorts);
if (player.ports >= 3 && (!game.mostPorts || player.ports > game.mostPortCount)) { if (player.ports >= 3 && (!anyGame.mostPorts || player.ports > anyGame.mostPortCount)) {
if (game.mostPorts !== session.color) { if (anyGame.mostPorts !== session.color) {
game.mostPorts = session.color; anyGame.mostPorts = session.color;
game.mostPortCount = player.ports; anyGame.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})!`);
} }
} }
@ -2627,17 +2661,17 @@ const placeSettlement = (game: Game, session: Session, index: number | string):
} }
calculateRoadLengths(game, session); calculateRoadLengths(game, session);
} else if (game.state === "initial-placement") { } else if (game.state === "initial-placement") {
if (game.direction && game.direction === "backward") { if (anyGame.direction && anyGame.direction === "backward") {
session.initialSettlement = index; (session as any).initialSettlement = index;
} }
corner.color = session.color; corner.color = session.color || "";
corner.type = "settlement"; corner.type = "settlement";
let bankType = undefined; let bankType = undefined;
const banks2 = layout.corners?.[index]?.banks; const banks2 = layout.corners?.[index]?.banks;
if (banks2 && banks2.length) { if (banks2 && banks2.length) {
banks2.forEach((bank: any) => { banks2.forEach((bank: any) => {
const border = game.borderOrder[Math.floor(bank / 3)], const border = anyGame.borderOrder[Math.floor(bank / 3)],
type = game.borders?.[border]?.[bank % 3]; type = anyGame.borders?.[border]?.[bank % 3];
console.log(`${session.id}: Bank ${bank} = ${type}`); console.log(`${session.id}: Bank ${bank} = ${type}`);
if (!type) { if (!type) {
return; return;
@ -2649,7 +2683,7 @@ const placeSettlement = (game: Game, session: Session, index: number | string):
player.ports++; player.ports++;
}); });
} }
player.settlements = (player.settlements || 0) - 1; player.settlements = (player.settlements || 0) - 1;
if (bankType) { if (bankType) {
addActivity( addActivity(
game, game,
@ -3273,53 +3307,37 @@ const placeCity = (game: any, session: any, index: any): string | undefined => {
return undefined; return undefined;
}; };
const ping = (session: any) => { const ping = (session: Session) => {
if (!session.ws) { if (!session.ws) {
console.log(`[no socket] Not sending ping to ${session.name} -- connection does not exist.`); console.log(`[no socket] Not sending ping to ${session.name} -- connection does not exist.`);
return; return;
} }
session.ping = Date.now(); (session as any)["ping"] = Date.now();
// console.log(`Sending ping to ${session.name}`); // 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) { if (session.keepAlive) {
clearTimeout(session.keepAlive); clearTimeout(session.keepAlive);
} }
session.keepAlive = setTimeout(() => { session.keepAlive = setTimeout(() => {
ping(session); // mark the session as inactive if the keepAlive fires
}, 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.`);
try { try {
// Defensive: close only if a socket exists; swallow any errors from closing
if (session.ws) { if (session.ws) {
try { session.ws.close?.();
session.ws.close();
} catch (e) {
/* ignore close errors */
}
} }
} catch (e) { } catch (e) {
/* ignore */ /* ignore */
} }
session.ws = undefined; session.ws = undefined;
} }, 20000);
/* Prevent future pings */
if (req.keepAlive) {
clearTimeout(req.keepAlive);
}
}; };
// keep a void reference so linters/typecheckers don't complain about unused declarations // wsInactive not present in this refactor; no-op placeholder removed
void wsInactive;
const setGameState = (game: any, session: any, state: any): string | undefined => { const setGameState = (game: any, session: any, state: any): string | undefined => {
if (!state) { if (!state) {