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>
)}
{name && <PlayerList />}
{/* 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 <TradeComponent tradeActive={tradeActive} setTradeActive={setTradeActive} />;
})()}
{tradeActive && <Trade />}
{name !== "" && <Chat />}
{/* name !== "" && <VideoFeeds/> */}
{loaded && (

View File

@ -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;

View File

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

View File

@ -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<string, any> = {
const receives: Record<string, Record<string, number>> = {
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) {