1
0

THings are improving

This commit is contained in:
James Ketr 2025-10-10 19:51:15 -07:00
parent b2e5fe4e03
commit 579632c293
87 changed files with 421 additions and 313 deletions

View File

@ -106,7 +106,7 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
const [signature, setSignature] = useState<string>(""); const [signature, setSignature] = useState<string>("");
const [generated, setGenerated] = useState<string>(""); const [generated, setGenerated] = useState<string>("");
const [robber, setRobber] = useState<number>(-1); const [robber, setRobber] = useState<number>(-1);
const [robberName, setRobberName] = useState<string[]>([]); const [robberName, setRobberName] = useState<string>("");
const [pips, setPips] = useState<any>(undefined); // Keep as any for now, complex structure const [pips, setPips] = useState<any>(undefined); // Keep as any for now, complex structure
const [pipOrder, setPipOrder] = useState<any>(undefined); const [pipOrder, setPipOrder] = useState<any>(undefined);
const [borders, setBorders] = useState<any>(undefined); const [borders, setBorders] = useState<any>(undefined);
@ -147,86 +147,123 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
return; return;
} }
const data = lastJsonMessage; const data = lastJsonMessage;
const handleUpdate = (update: any) => {
console.log(`board - game update`, update);
if ("robber" in update && update.robber !== robber) {
setRobber(update.robber);
}
if ("robberName" in update) {
const newName = Array.isArray(update.robberName) ? String(update.robberName[0] || "") : String(update.robberName || "");
if (newName !== robberName) setRobberName(newName);
}
if ("state" in update && update.state !== state) {
setState(update.state);
}
if ("rules" in update && !equal(update.rules, rules)) {
setRules(update.rules);
}
if ("color" in update && update.color !== color) {
setColor(update.color);
}
if ("longestRoadLength" in update && update.longestRoadLength !== longestRoadLength) {
setLongestRoadLength(update.longestRoadLength);
}
if ("turn" in update) {
if (!equal(update.turn, turn)) {
console.log(`board - turn`, update.turn);
setTurn(update.turn);
}
}
if ("placements" in update && !equal(update.placements, placements)) {
console.log(`board - placements`, update.placements);
setPlacements(update.placements);
}
/* The following are only updated if there is a new game
* signature or changed ordering */
if ("pipOrder" in update && !equal(update.pipOrder, pipOrder)) {
console.log(`board - setting new pipOrder`);
setPipOrder(update.pipOrder);
}
if ("borderOrder" in update && !equal(update.borderOrder, borderOrder)) {
console.log(`board - setting new borderOrder`);
setBorderOrder(update.borderOrder);
}
if ("animationSeeds" in update && !equal(update.animationSeeds, animationSeeds)) {
console.log(`board - setting new animationSeeds`);
setAnimationSeeds(update.animationSeeds);
}
if ("tileOrder" in update && !equal(update.tileOrder, tileOrder)) {
console.log(`board - setting new tileOrder`);
setTileOrder(update.tileOrder);
}
if (update.signature !== undefined && update.signature !== signature) {
console.log(`board - setting new signature`);
setSignature(update.signature);
}
/* Static data from the server (defensive): update when present and different */
if ("pips" in update && !equal(update.pips, pips)) {
console.log(`board - setting new static pips`);
setPips(update.pips);
}
if ("tiles" in update && !equal(update.tiles, tiles)) {
console.log(`board - setting new static tiles`);
setTiles(update.tiles);
}
if ("borders" in update && !equal(update.borders, borders)) {
console.log(`board - setting new static borders`);
setBorders(update.borders);
}
};
switch (data.type) { switch (data.type) {
case "game-update": case "game-update":
console.log(`board - game update`, data.update); handleUpdate(data.update || {});
if ("robber" in data.update && data.update.robber !== robber) { break;
setRobber(data.update.robber);
}
if ("robberName" in data.update && data.update.robberName !== robberName) { case "initial-game":
setRobberName(data.update.robberName); // initial snapshot contains the consolidated game in data.snapshot
} console.log(`board - initial-game snapshot received`);
if (data.snapshot) {
const snap = data.snapshot;
// Normalize snapshot fields to same keys used for incremental updates
const initialUpdate: any = {};
// Pick expected fields from snapshot
[
"robber",
"robberName",
"state",
"rules",
"color",
"longestRoadLength",
"turn",
"placements",
"pipOrder",
"borderOrder",
"animationSeeds",
"tileOrder",
"signature",
].forEach((k) => {
if (k in snap) initialUpdate[k] = snap[k];
});
// static asset metadata
if ("tiles" in snap) initialUpdate.tiles = snap.tiles;
if ("pips" in snap) initialUpdate.pips = snap.pips;
if ("borders" in snap) initialUpdate.borders = snap.borders;
if ("state" in data.update && data.update.state !== state) { handleUpdate(initialUpdate);
setState(data.update.state);
}
if ("rules" in data.update && !equal(data.update.rules, rules)) {
setRules(data.update.rules);
}
if ("color" in data.update && data.update.color !== color) {
setColor(data.update.color);
}
if ("longestRoadLength" in data.update && data.update.longestRoadLength !== longestRoadLength) {
setLongestRoadLength(data.update.longestRoadLength);
}
if ("turn" in data.update) {
if (!equal(data.update.turn, turn)) {
console.log(`board - turn`, data.update.turn);
setTurn(data.update.turn);
}
}
if ("placements" in data.update && !equal(data.update.placements, placements)) {
console.log(`board - placements`, data.update.placements);
setPlacements(data.update.placements);
}
/* The following are only updated if there is a new game
* signature */
if ("pipOrder" in data.update && !equal(data.update.pipOrder, pipOrder)) {
console.log(`board - setting new pipOrder`);
setPipOrder(data.update.pipOrder);
}
if ("borderOrder" in data.update && !equal(data.update.borderOrder, borderOrder)) {
console.log(`board - setting new borderOrder`);
setBorderOrder(data.update.borderOrder);
}
if ("animationSeeds" in data.update && !equal(data.update.animationSeeds, animationSeeds)) {
console.log(`board - setting new animationSeeds`);
setAnimationSeeds(data.update.animationSeeds);
}
if ("tileOrder" in data.update && !equal(data.update.tileOrder, tileOrder)) {
console.log(`board - setting new tileOrder`);
setTileOrder(data.update.tileOrder);
}
if (data.update.signature !== signature) {
console.log(`board - setting new signature`);
setSignature(data.update.signature);
}
/* This is permanent static data from the server -- do not update
* once set */
if ("pips" in data.update && !pips) {
console.log(`board - setting new static pips`);
setPips(data.update.pips);
}
if ("tiles" in data.update && !tiles) {
console.log(`board - setting new static tiles`);
setTiles(data.update.tiles);
}
if ("borders" in data.update && !borders) {
console.log(`board - setting new static borders`);
setBorders(data.update.borders);
} }
break; break;
default: default:
@ -508,10 +545,6 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
console.log(`board - Generate pip, border, and tile elements`); console.log(`board - Generate pip, border, and tile elements`);
const Pip: React.FC<PipProps> = ({ pip, className }) => { const Pip: React.FC<PipProps> = ({ pip, className }) => {
const onPipClicked = (pip) => { const onPipClicked = (pip) => {
if (!ws) {
console.error(`board - sendPlacement - ws is NULL`);
return;
}
sendJsonMessage({ sendJsonMessage({
type: "place-robber", type: "place-robber",
index: pip.index, index: pip.index,
@ -928,7 +961,7 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
const el = document.querySelector(`.Pip[data-index="${robber}"]`); const el = document.querySelector(`.Pip[data-index="${robber}"]`);
if (el) { if (el) {
el.classList.add("Robber"); el.classList.add("Robber");
el.classList.add(robberName); if (robberName) el.classList.add(robberName);
} }
} }
}); });

View File

@ -14,6 +14,7 @@ import { useContext } from "react";
import WebRTCStatus from "./WebRTCStatus"; import WebRTCStatus from "./WebRTCStatus";
import Moveable from "react-moveable"; import Moveable from "react-moveable";
import { flushSync } from "react-dom"; import { flushSync } from "react-dom";
import { SxProps, Theme } from "@mui/material";
const debug = true; const debug = true;
// When true, do not send host candidates to the signaling server. Keeps TURN relays preferred. // When true, do not send host candidates to the signaling server. Keeps TURN relays preferred.
@ -1304,6 +1305,7 @@ interface MediaControlProps {
sendJsonMessage?: (msg: any) => void; sendJsonMessage?: (msg: any) => void;
remoteAudioMuted?: boolean; remoteAudioMuted?: boolean;
remoteVideoOff?: boolean; remoteVideoOff?: boolean;
sx?: SxProps<Theme>;
} }
const MediaControl: React.FC<MediaControlProps> = ({ const MediaControl: React.FC<MediaControlProps> = ({
@ -1313,6 +1315,7 @@ const MediaControl: React.FC<MediaControlProps> = ({
sendJsonMessage, sendJsonMessage,
remoteAudioMuted, remoteAudioMuted,
remoteVideoOff, remoteVideoOff,
sx,
}) => { }) => {
const [muted, setMuted] = useState<boolean>(peer?.muted || false); const [muted, setMuted] = useState<boolean>(peer?.muted || false);
const [videoOn, setVideoOn] = useState<boolean>(peer?.video_on !== false); const [videoOn, setVideoOn] = useState<boolean>(peer?.video_on !== false);
@ -1591,6 +1594,7 @@ const MediaControl: React.FC<MediaControlProps> = ({
alignItems: "center", alignItems: "center",
minWidth: "200px", minWidth: "200px",
minHeight: "100px", minHeight: "100px",
...sx,
}} }}
> >
<div <div

View File

@ -64,6 +64,20 @@ const PlayerList: React.FC = () => {
[session] [session]
); );
useEffect(() => {
if (!players) {
return;
}
players.forEach((player) => {
console.log("rabbit - player:", {
name: player.name,
live: player.live,
in_peers: peers[player.session_id],
local_or_media: player.local || player.has_media !== false,
});
});
}, [players]);
// Use the WebSocket hook for room events with automatic reconnection // Use the WebSocket hook for room events with automatic reconnection
useEffect(() => { useEffect(() => {
if (!lastJsonMessage) { if (!lastJsonMessage) {
@ -179,6 +193,7 @@ const PlayerList: React.FC = () => {
{player.name && player.live && peers[player.session_id] && (player.local || player.has_media !== false) ? ( {player.name && player.live && peers[player.session_id] && (player.local || player.has_media !== false) ? (
<> <>
<MediaControl <MediaControl
sx={{ border: "3px solid blue" }}
className="Medium" className="Medium"
key={player.session_id} key={player.session_id}
peer={peers[player.session_id]} peer={peers[player.session_id]}

View File

@ -48,14 +48,12 @@ const audioEffects: Record<string, AudioEffect | undefined> = {};
const loadAudio = (src: string) => { const loadAudio = (src: string) => {
const audio = document.createElement("audio") as AudioEffect; const audio = document.createElement("audio") as AudioEffect;
audio.src = audioFiles[src]; audio.src = audioFiles[src];
console.log("Loading audio:", audio.src);
audio.setAttribute("preload", "auto"); audio.setAttribute("preload", "auto");
audio.setAttribute("controls", "none"); audio.setAttribute("controls", "none");
audio.style.display = "none"; audio.style.display = "none";
document.body.appendChild(audio); document.body.appendChild(audio);
audio.load(); audio.load();
audio.addEventListener("error", (e) => console.error("Audio load error:", e, audio.src)); audio.addEventListener("error", (e) => console.error("Audio load error:", e, audio.src));
audio.addEventListener("canplay", () => console.log("Audio can play:", audio.src));
return audio; return audio;
}; };

Binary file not shown.

Before

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 339 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 998 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 316 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 952 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 945 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 338 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 921 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 412 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 444 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 442 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 434 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 428 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 436 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 458 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 438 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 484 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 446 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 423 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 468 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 433 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 462 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 466 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 452 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 435 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 920 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 983 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1014 KiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 453 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 MiB

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 MiB

View File

@ -300,7 +300,7 @@ const bestRoadPlacement = (game) => {
return; return;
} }
const placedRoad = game.placements.roads[roadIndex]; const placedRoad = game.placements.roads[roadIndex];
if (placedRoad.color) { if (!placedRoad || placedRoad.color) {
return; return;
} }
attempt = roadIndex; attempt = roadIndex;

View File

@ -16,6 +16,9 @@ const processCorner = (game: any, color: string, cornerIndex: number, placedCorn
let longest = 0; let longest = 0;
layout.corners[cornerIndex].roads.forEach((roadIndex: number) => { layout.corners[cornerIndex].roads.forEach((roadIndex: number) => {
const placedRoad = game.placements.roads[roadIndex]; const placedRoad = game.placements.roads[roadIndex];
if (!placedRoad) {
return;
}
if (placedRoad.walking) { if (placedRoad.walking) {
return; return;
} }
@ -42,11 +45,12 @@ const buildCornerGraph = (game: any, color: string, cornerIndex: number, placedC
if (placedCorner.walking) { if (placedCorner.walking) {
return; return;
} }
placedCorner.walking = true; placedCorner.walking = true;
/* Calculate the longest road branching from both corners */ /* Calculate the longest road branching from both corners */
layout.corners[cornerIndex].roads.forEach((roadIndex: number) => { layout.corners[cornerIndex].roads.forEach((roadIndex: number) => {
const placedRoad = game.placements.roads[roadIndex]; const placedRoad = game.placements.roads[roadIndex];
if (!placedRoad) return;
buildRoadGraph(game, color, roadIndex, placedRoad, set); buildRoadGraph(game, color, roadIndex, placedRoad, set);
}); });
}; };
@ -54,7 +58,7 @@ const buildCornerGraph = (game: any, color: string, cornerIndex: number, placedC
const processRoad = (game: any, color: string, roadIndex: number, placedRoad: any): number => { const processRoad = (game: any, color: string, roadIndex: number, placedRoad: any): number => {
/* If this road isn't assigned to the walking color, skip it */ /* If this road isn't assigned to the walking color, skip it */
if (placedRoad.color !== color) { if (placedRoad.color !== color) {
return 0; return 0;
} }
/* If this road is already being walked, skip it */ /* If this road is already being walked, skip it */
@ -65,8 +69,9 @@ const processRoad = (game: any, color: string, roadIndex: number, placedRoad: an
placedRoad.walking = true; placedRoad.walking = true;
/* Calculate the longest road branching from both corners */ /* Calculate the longest road branching from both corners */
let roadLength = 1; let roadLength = 1;
layout.roads[roadIndex].corners.forEach(cornerIndex => { layout.roads[roadIndex].corners.forEach((cornerIndex) => {
const placedCorner = game.placements.corners[cornerIndex]; const placedCorner = game.placements.corners[cornerIndex];
if (!placedCorner) return;
if (placedCorner.walking) { if (placedCorner.walking) {
return; return;
} }
@ -89,21 +94,26 @@ const buildRoadGraph = (game: any, color: string, roadIndex: number, placedRoad:
placedRoad.walking = true; placedRoad.walking = true;
set.push(roadIndex); set.push(roadIndex);
/* Calculate the longest road branching from both corners */ /* Calculate the longest road branching from both corners */
layout.roads[roadIndex].corners.forEach(cornerIndex => { layout.roads[roadIndex].corners.forEach((cornerIndex) => {
const placedCorner = game.placements.corners[cornerIndex]; const placedCorner = game.placements.corners[cornerIndex];
buildCornerGraph(game, color, cornerIndex, placedCorner, set) if (!placedCorner) return;
buildCornerGraph(game, color, cornerIndex, placedCorner, set);
}); });
}; };
const clearRoadWalking = (game: any) => { const clearRoadWalking = (game: any) => {
/* Clear out walk markers on roads */ /* Clear out walk markers on roads */
layout.roads.forEach((item, itemIndex) => { layout.roads.forEach((item, itemIndex) => {
delete game.placements.roads[itemIndex].walking; if (game.placements && game.placements.roads && game.placements.roads[itemIndex]) {
delete game.placements.roads[itemIndex].walking;
}
}); });
/* Clear out walk markers on corners */ /* Clear out walk markers on corners */
layout.corners.forEach((item, itemIndex) => { layout.corners.forEach((item, itemIndex) => {
delete game.placements.corners[itemIndex].walking; if (game.placements && game.placements.corners && game.placements.corners[itemIndex]) {
delete game.placements.corners[itemIndex].walking;
}
}); });
} }
@ -122,6 +132,7 @@ const calculateRoadLengths = (game: any) => {
let graphs = []; let graphs = [];
layout.roads.forEach((_, roadIndex) => { layout.roads.forEach((_, roadIndex) => {
const placedRoad = game.placements.roads[roadIndex]; const placedRoad = game.placements.roads[roadIndex];
if (!placedRoad) return;
if (placedRoad.color === color) { if (placedRoad.color === color) {
let set = []; let set = [];
buildRoadGraph(game, color, roadIndex, placedRoad, set); buildRoadGraph(game, color, roadIndex, placedRoad, set);
@ -133,13 +144,13 @@ const calculateRoadLengths = (game: any) => {
let final = { let final = {
segments: 0, segments: 0,
index: -1 index: -1,
}; };
clearRoadWalking(game); clearRoadWalking(game);
graphs.forEach(graph => { graphs.forEach((graph) => {
graph.longestRoad = 0; graph.longestRoad = 0;
graph.set.forEach(roadIndex => { graph.set.forEach((roadIndex) => {
const placedRoad = game.placements.roads[roadIndex]; const placedRoad = game.placements.roads[roadIndex];
clearRoadWalking(game); clearRoadWalking(game);
const length = processRoad(game, color, roadIndex, placedRoad); const length = processRoad(game, color, roadIndex, placedRoad);
@ -154,7 +165,9 @@ const calculateRoadLengths = (game: any) => {
}); });
}); });
game.placements.roads.forEach(road => delete road.walking); game.placements.roads.forEach((road: any) => {
if (road) delete road.walking;
});
return final; return final;
}; };

View File

@ -17,8 +17,8 @@ import {
PlayerColor, PlayerColor,
PLAYER_COLORS, PLAYER_COLORS,
RESOURCE_TYPES, RESOURCE_TYPES,
ResourceType,
} from "./games/types"; } from "./games/types";
import { normalizeIncoming } from "./games/utils";
import { import {
audio, audio,
join as webrtcJoin, join as webrtcJoin,
@ -54,6 +54,7 @@ import { transientState } from "./games/sessionState";
import { createGame, resetGame, setBeginnerGame } from "./games/gameFactory"; import { createGame, resetGame, setBeginnerGame } from "./games/gameFactory";
import { getVictoryPointRule, setRules, supportedRules } from "./games/rules"; import { getVictoryPointRule, setRules, supportedRules } from "./games/rules";
import { pickRobber } from "./games/robber"; import { pickRobber } from "./games/robber";
import { IncomingMessage } from "./games/types";
const processTies = (players: Player[]): boolean => { const processTies = (players: Player[]): boolean => {
/* Sort the players into buckets based on their /* Sort the players into buckets based on their
@ -256,6 +257,8 @@ const processVolcano = (game: Game, session: Session, dice: number[]) => {
dice: game.dice, dice: game.dice,
placements: game.placements, placements: game.placements,
players: getFilteredPlayers(game), players: getFilteredPlayers(game),
longestRoad: game.longestRoad,
longestRoadLength: game.longestRoadLength,
}); });
}; };
@ -327,6 +330,7 @@ interface ResourceCount {
wheat: number; wheat: number;
stone: number; stone: number;
desert: number; desert: number;
bank: number;
} }
type Received = Record<PlayerColor | "robber", ResourceCount>; type Received = Record<PlayerColor | "robber", ResourceCount>;
@ -361,6 +365,13 @@ const distributeResources = (game: Game, roll: number): void => {
receives[color]![type] = 0; receives[color]![type] = 0;
}); });
}); });
/* Ensure robber entry exists */
if (!receives.robber) {
receives.robber = {} as ResourceCount;
RESOURCE_TYPES.forEach((type) => {
receives.robber[type] = 0;
});
}
/* Find which corners are on each tile */ /* Find which corners are on each tile */
matchedTiles.forEach((tile: Tile) => { matchedTiles.forEach((tile: Tile) => {
@ -379,18 +390,21 @@ const distributeResources = (game: Game, roll: number): void => {
return; return;
} }
tileLayout.corners.forEach((cornerIndex: number) => { tileLayout.corners.forEach((cornerIndex: number) => {
// corners may refer to undefined slots in placements; guard against that
const active = game.placements.corners[cornerIndex]; const active = game.placements.corners[cornerIndex];
if (!active) { if (!active || !active.color) {
return; return;
} }
const count = active.type === "settlement" ? 1 : 2; const count = active.type === "settlement" ? 1 : 2;
if (!tile.robber) { if (!tile.robber) {
if (resource.type) { if (resource && resource.type) {
try { // ensure receives entry for this color exists
receives[active.color][resource.type]! += count; if (!receives[active.color]) {
} catch (e) { receives[active.color] = {} as ResourceCount;
console.error("Error incrementing resources", { receives, active, resource, count }); RESOURCE_TYPES.forEach((t) => (receives[active.color]![t] = 0));
} }
receives[active.color]![resource.type] = (receives[active.color]![resource.type] || 0) + count;
} }
} else { } else {
const victim = game.players[active.color]; const victim = game.players[active.color];
@ -400,10 +414,19 @@ const distributeResources = (game: Game, roll: number): void => {
null, null,
`Robber does not steal ${count} ${resource.type} from ${victim?.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.`
); );
if (resource && resource.type) receives[active.color]![resource.type]! += count; if (resource && resource.type) {
if (!receives[active.color]) {
receives[active.color] = {} as ResourceCount;
RESOURCE_TYPES.forEach((t) => (receives[active.color]![t] = 0));
}
receives[active.color]![resource.type] = (receives[active.color]![resource.type] || 0) + count;
}
} else { } else {
trackTheft(game, active.color, "robber", resource.type, count); // If resource.type is falsy, skip.
if (resource.type) receives.robber[resource.type] += count; if (resource && resource.type) {
trackTheft(game, active.color, "robber", resource.type, count);
receives.robber[resource.type] = (receives.robber[resource.type] || 0) + count;
}
} }
} }
}); });
@ -424,8 +447,19 @@ const distributeResources = (game: Game, roll: number): void => {
if (color !== "robber") { if (color !== "robber") {
s = sessionFromColor(game, color); s = sessionFromColor(game, color);
if (s && s.player) { if (s && s.player) {
s.player[type] += entry[type]; switch (type) {
s.player.resources += entry[type]; case "wood":
case "brick":
case "sheep":
case "wheat":
case "stone":
s.player[type] += entry[type];
s.player.resources += entry[type];
break;
default:
console.error("Invalid resource type to distribute:", type);
return;
}
messageParts.push(`${entry[type]} ${type}`); messageParts.push(`${entry[type]} ${type}`);
} }
} else { } else {
@ -1188,7 +1222,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und
message = `${name} has rejoined the lobby.`; message = `${name} has rejoined the lobby.`;
} }
session.name = name; session.name = name;
if (session.ws && game.id in audio && session.name in audio[game.id]) { if (session.ws && game.id in audio && session.id in audio[game.id]) {
webrtcPart(audio[game.id], session); webrtcPart(audio[game.id], session);
} }
} else { } else {
@ -1211,10 +1245,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und
} }
if (session.ws && session.hasAudio) { if (session.ws && session.hasAudio) {
webrtcJoin(audio[game.id], session, { webrtcJoin(audio[game.id], session);
hasVideo: session.video ? true : false,
hasAudio: session.audio ? true : false,
});
} }
console.log(`${info}: ${message}`); console.log(`${info}: ${message}`);
addChatMessage(game, null, message); addChatMessage(game, null, message);
@ -1576,7 +1607,7 @@ const calculateRoadLengths = (game: Game, session: Session): void => {
if (debug.road) if (debug.road)
console.log( console.log(
"Pre update:", "Pre update:",
game.placements.roads.filter((road) => road.color) game.placements.roads.filter((road) => road && (road as any).color)
); );
for (let color in game.players) { for (let color in game.players) {
@ -1610,7 +1641,7 @@ const calculateRoadLengths = (game: Game, session: Session): void => {
if (debug.road) if (debug.road)
console.log( console.log(
"Post update:", "Post update:",
game.placements.roads.filter((road: any) => road.color) game.placements.roads.filter((road: any) => road && road.color)
); );
let checkForTies = false; let checkForTies = false;
@ -2277,6 +2308,8 @@ const placeRobber = (game: Game, session: Session, robber: number | string): str
robber: game.robber, robber: game.robber,
robberName: game.robberName, robberName: game.robberName,
activities: game.activities, activities: game.activities,
longestRoad: game.longestRoad,
longestRoadLength: game.longestRoadLength,
}); });
sendUpdateToPlayer(game, session, { sendUpdateToPlayer(game, session, {
private: session.player, private: session.player,
@ -2572,8 +2605,7 @@ const playCard = (game: Game, session: Session, 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: Player = 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") {
@ -2585,11 +2617,7 @@ const placeSettlement = (game: Game, session: Session, index: number | string):
} }
/* index out of range... */ /* index out of range... */
if ( if (!game.placements || game.placements.corners === undefined || game.placements.corners[index] === undefined) {
!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!`;
} }
@ -2597,8 +2625,8 @@ 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 = anyGame.placements.corners[index]; const corner = game.placements.corners[index]!;
if (corner.color) { if (corner.color && corner.color !== "unassigned") {
const owner = game.players && game.players[corner.color]; const owner = game.players && game.players[corner.color];
const ownerName = owner ? owner.name : "unknown"; const ownerName = owner ? owner.name : "unknown";
return `This location already has a settlement belonging to ${ownerName}!`; return `This location already has a settlement belonging to ${ownerName}!`;
@ -2627,10 +2655,7 @@ const placeSettlement = (game: Game, session: Session, index: number | string):
player.wood = (player.wood || 0) - 1; player.wood = (player.wood || 0) - 1;
player.wheat = (player.wheat || 0) - 1; player.wheat = (player.wheat || 0) - 1;
player.sheep = (player.sheep || 0) - 1; player.sheep = (player.sheep || 0) - 1;
player.resources = 0; player.resources -= 4;
["wheat", "brick", "sheep", "stone", "wood"].forEach((resource) => {
player.resources += player[resource] || 0;
});
} }
delete game.turn.free; delete game.turn.free;
@ -2641,8 +2666,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 = anyGame.borderOrder[Math.floor(bank / 3)], const border = game.borderOrder[Math.floor(bank / 3)]!,
type = anyGame.borders?.[border]?.[bank % 3]; type: ResourceType = staticData.borders?.[border]?.[bank % 3]!;
console.log(`${session.short}: Bank ${bank} = ${type}`); console.log(`${session.short}: Bank ${bank} = ${type}`);
if (!type) { if (!type) {
console.log(`${session.short}: Bank ${bank}`); console.log(`${session.short}: Bank ${bank}`);
@ -2656,11 +2681,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, anyGame.mostPorts); console.log(`Checking port-of-call`, player.ports, game.mostPorts);
if (player.ports >= 3 && (!anyGame.mostPorts || player.ports > anyGame.mostPortCount)) { if (player.ports >= 3 && (!game.mostPorts || player.ports > game.mostPortCount)) {
if (anyGame.mostPorts !== session.color) { if (game.mostPorts !== session.color) {
anyGame.mostPorts = session.color; game.mostPorts = session.color;
anyGame.mostPortCount = player.ports; game.mostPortCount = player.ports;
addChatMessage(game, session, `${session.name} now has the most ports (${player.ports})!`); addChatMessage(game, session, `${session.name} now has the most ports (${player.ports})!`);
} }
} }
@ -2676,7 +2701,7 @@ 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 (anyGame.direction && anyGame.direction === "backward") { if (game.direction && game.direction === "backward") {
(session as any).initialSettlement = index; (session as any).initialSettlement = index;
} }
corner.color = session.color || ""; corner.color = session.color || "";
@ -2685,8 +2710,8 @@ const placeSettlement = (game: Game, session: Session, index: number | string):
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 = anyGame.borderOrder[Math.floor(bank / 3)], const border = game.borderOrder[Math.floor(bank / 3)]!,
type = anyGame.borders?.[border]?.[bank % 3]; type = staticData.borders?.[border]?.[bank % 3];
console.log(`${session.short}: Bank ${bank} = ${type}`); console.log(`${session.short}: Bank ${bank} = ${type}`);
if (!type) { if (!type) {
return; return;
@ -2770,10 +2795,7 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi
addChatMessage(game, session, `${session.name} spent 1 brick and 1 wood to build a road.`); addChatMessage(game, session, `${session.name} spent 1 brick and 1 wood to build a road.`);
player.brick--; player.brick--;
player.wood--; player.wood--;
player.resources = 0; player.resources -= 2;
["wheat", "brick", "sheep", "stone", "wood"].forEach((resource) => {
player.resources += player[resource];
});
} }
delete game.turn.free; delete game.turn.free;
} }
@ -2890,6 +2912,8 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi
players: getFilteredPlayers(game), players: getFilteredPlayers(game),
state: game.state, state: game.state,
direction: (game as any)["direction"], direction: (game as any)["direction"],
longestRoad: game.longestRoad,
longestRoadLength: game.longestRoadLength,
}); });
return undefined; return undefined;
}; };
@ -3279,6 +3303,8 @@ const placeCity = (game: any, session: any, index: any): string | undefined => {
chat: game.chat, chat: game.chat,
activities: game.activities, activities: game.activities,
players: getFilteredPlayers(game), players: getFilteredPlayers(game),
longestRoad: game.longestRoad,
longestRoadLength: game.longestRoadLength,
}); });
return undefined; return undefined;
}; };
@ -3623,6 +3649,23 @@ const gotoLobby = (game: any, session: any): string | undefined => {
return undefined; return undefined;
}; };
const normalizeIncoming = (msg: unknown): IncomingMessage | null => {
let parsed: IncomingMessage | null = null;
try {
if (typeof msg === "string") {
parsed = JSON.parse(msg);
} else {
parsed = msg as IncomingMessage;
}
} catch (e) {
return null;
}
if (!parsed) return null;
const type = parsed.type || (parsed as any).action || null;
const data = parsed.data || (Object.keys(parsed).length ? Object.assign({}, parsed) : null);
return { type, data };
};
router.ws("/ws/:id", async (ws, req) => { router.ws("/ws/:id", async (ws, req) => {
console.log("New WebSocket connection"); console.log("New WebSocket connection");
if (!req.cookies || !(req.cookies as any)["player"]) { if (!req.cookies || !(req.cookies as any)["player"]) {
@ -3683,7 +3726,7 @@ router.ws("/ws/:id", async (ws, req) => {
/* ignore logging errors */ /* ignore logging errors */
} }
if (!(gameId in audio)) { if (!(gameId in audio)) {
audio[gameId] = {}; /* List of peer sockets using session.name as index. */ audio[gameId] = {}; /* List of peer sockets using session.id as index. */
console.log(`${short}: Game ${gameId} - New Game Audio`); console.log(`${short}: Game ${gameId} - New Game Audio`);
} else { } else {
console.log(`${short}: Game ${gameId} - Already has Audio`); console.log(`${short}: Game ${gameId} - Already has Audio`);
@ -3846,7 +3889,7 @@ router.ws("/ws/:id", async (ws, req) => {
// Normalize the incoming message to { type, data } so handlers can // Normalize the incoming message to { type, data } so handlers can
// reliably access the payload without repeated defensive checks. // reliably access the payload without repeated defensive checks.
const incoming = normalizeIncoming(message); const incoming = normalizeIncoming(message);
if (!incoming.type) { if (!incoming || !incoming.type) {
// If we couldn't parse or determine the type, log and ignore the // If we couldn't parse or determine the type, log and ignore the
// message to preserve previous behavior. // message to preserve previous behavior.
try { try {
@ -3856,7 +3899,7 @@ router.ws("/ws/:id", async (ws, req) => {
} }
return; return;
} }
const data = (incoming.data as any) || {}; const data = incoming.data;
const game = await loadGame(gameId); const game = await loadGame(gameId);
const _session = getSession( const _session = getSession(
game, game,
@ -3925,7 +3968,7 @@ router.ws("/ws/:id", async (ws, req) => {
// Accept either legacy `config`, newer `data`, or flat payloads where // Accept either legacy `config`, newer `data`, or flat payloads where
// the client sent fields at the top level (normalizeIncoming will // the client sent fields at the top level (normalizeIncoming will
// populate `data` with the parsed object in that case). // populate `data` with the parsed object in that case).
webrtcJoin(audio[gameId], session, data.config || data.data || data || {}); webrtcJoin(audio[gameId], session);
break; break;
case "part": case "part":
@ -3937,7 +3980,7 @@ router.ws("/ws/:id", async (ws, req) => {
// Delegate to the webrtc signaling helper (it performs its own checks) // Delegate to the webrtc signaling helper (it performs its own checks)
// Accept either config/data or a flat payload (data). // Accept either config/data or a flat payload (data).
const cfg = data.config || data.data || data || {}; const cfg = data.config || data.data || data || {};
handleRelayICECandidate(gameId, cfg, session, undefined, debug); handleRelayICECandidate(gameId, cfg, session, debug);
} }
break; break;
@ -3945,7 +3988,7 @@ router.ws("/ws/:id", async (ws, req) => {
{ {
// Accept either config/data or a flat payload (data). // Accept either config/data or a flat payload (data).
const cfg = data.config || data.data || data || {}; const cfg = data.config || data.data || data || {};
handleRelaySessionDescription(gameId, cfg, session, undefined, debug); handleRelaySessionDescription(gameId, cfg, session, debug);
} }
break; break;
@ -3977,7 +4020,7 @@ router.ws("/ws/:id", async (ws, req) => {
{ {
// Accept either config/data or a flat payload (data). // Accept either config/data or a flat payload (data).
const cfg = data.config || data.data || data || {}; const cfg = data.config || data.data || data || {};
broadcastPeerStateUpdate(gameId, cfg, session, undefined); broadcastPeerStateUpdate(gameId, cfg, session);
} }
break; break;
@ -4069,7 +4112,6 @@ router.ws("/ws/:id", async (ws, req) => {
case "longestRoadLength": case "longestRoadLength":
case "robber": case "robber":
case "robberName": case "robberName":
case "pips":
case "tileOrder": case "tileOrder":
case "active": case "active":
case "largestArmy": case "largestArmy":
@ -4086,6 +4128,14 @@ router.ws("/ws/:id", async (ws, req) => {
case "tiles": case "tiles":
batchedUpdate.tiles = staticData.tiles; batchedUpdate.tiles = staticData.tiles;
break; break;
case "pips":
// pips are static data (number/roll mapping). Return from staticData
batchedUpdate.pips = staticData.pips;
break;
case "borders":
// borders are static data describing ports/banks
batchedUpdate.borders = staticData.borders;
break;
case "rules": case "rules":
batchedUpdate[field] = game.rules ? game.rules : {}; batchedUpdate[field] = game.rules ? game.rules : {};
break; break;
@ -4519,6 +4569,18 @@ const getFilteredGameForPlayer = (game: any, session: any) => {
sessions: reducedSessions, sessions: reducedSessions,
layout: layout, layout: layout,
players: getFilteredPlayers(game), players: getFilteredPlayers(game),
// Include static asset metadata so clients can render the board immediately
tiles: staticData.tiles,
pips: staticData.pips,
borders: staticData.borders,
// Include board order/state so clients can render without waiting for extra GETs
pipOrder: game.pipOrder,
tileOrder: game.tileOrder,
borderOrder: game.borderOrder,
signature: game.signature,
animationSeeds: game.animationSeeds,
robber: game.robber,
robberName: game.robberName,
}); });
}; };

View File

@ -250,6 +250,8 @@ export const createGame = async (id: string | null = null) => {
B: newPlayer("B"), B: newPlayer("B"),
W: newPlayer("W"), W: newPlayer("W"),
}, },
mostPorts: null,
mostPortCount: 0,
sessions: {}, sessions: {},
unselected: [], unselected: [],
placements: { placements: {

View File

@ -535,8 +535,16 @@ export const getFilteredPlayers = (game: Game): Record<string, Player> => {
} }
player.resources = 0; player.resources = 0;
RESOURCE_TYPES.forEach((resource) => { RESOURCE_TYPES.forEach((resource) => {
player.resources += player[resource]; switch (resource) {
delete player[resource]; case "wood":
case "brick":
case "sheep":
case "wheat":
case "stone":
player.resources += player[resource];
player[resource] = 0;
break;
}
}); });
player.development = []; player.development = [];
} }

View File

@ -32,6 +32,7 @@ export const newPlayer = (color: PlayerColor): Player => {
live: true, live: true,
turnNotice: "", turnNotice: "",
longestRoad: 0, longestRoad: 0,
banks: [],
}; };
}; };

View File

@ -52,7 +52,7 @@ export const gameDB: GameDB = {
type: db.Sequelize.QueryTypes.SELECT, type: db.Sequelize.QueryTypes.SELECT,
}); });
if (rows && rows.length) { if (rows && rows.length) {
const r = rows[0] as any; const r = rows[0];
// state may be stored as text or JSON // state may be stored as text or JSON
if (typeof r.state === "string") { if (typeof r.state === "string") {
try { try {

View File

@ -25,7 +25,9 @@ export interface Player {
stone: number; stone: number;
brick: number; brick: number;
wood: number; wood: number;
army: number;
points: number; points: number;
ports: number;
resources: number; resources: number;
lastActive: number; lastActive: number;
live: boolean; live: boolean;
@ -35,7 +37,7 @@ export interface Player {
turnNotice: string; turnNotice: string;
turnStart: number; turnStart: number;
totalTime: number; totalTime: number;
[key: string]: any; // allow incremental fields until fully typed banks: ResourceType[];
} }
export type CornerType = "settlement" | "city" | "none"; export type CornerType = "settlement" | "city" | "none";
@ -46,7 +48,6 @@ export interface CornerPlacement {
type: "settlement" | "city" | "none"; type: "settlement" | "city" | "none";
walking?: boolean; walking?: boolean;
longestRoad?: number; longestRoad?: number;
[key: string]: any;
} }
export type RoadType = "road" | "ship"; export type RoadType = "road" | "ship";
@ -60,8 +61,8 @@ export interface RoadPlacement {
} }
export interface Placements { export interface Placements {
corners: CornerPlacement[]; corners: Array<CornerPlacement | undefined>;
roads: RoadPlacement[]; roads: Array<RoadPlacement | undefined>;
} }
export interface Turn { export interface Turn {
@ -124,8 +125,8 @@ export interface Offer {
[key: string]: any; [key: string]: any;
} }
export type ResourceType = "wood" | "brick" | "sheep" | "wheat" | "stone" | "desert"; export type ResourceType = "wood" | "brick" | "sheep" | "wheat" | "stone" | "desert" | "bank";
export const RESOURCE_TYPES: ResourceType[] = ["wood", "brick", "sheep", "wheat", "stone", "desert"]; export const RESOURCE_TYPES: ResourceType[] = ["wood", "brick", "sheep", "wheat", "stone", "desert", "bank"];
export interface Tile { export interface Tile {
robber: boolean; robber: boolean;
@ -166,10 +167,11 @@ export interface Game {
turns: number; turns: number;
longestRoad?: string | false; longestRoad?: string | false;
longestRoadLength?: number; longestRoadLength?: number;
borderOrder?: number[]; borderOrder: number[];
largestArmy?: string | false; largestArmy?: string | false;
largestArmySize?: number; largestArmySize?: number;
mostPorts?: string | false; mostPorts: PlayerColor | null;
mostPortCount: number;
mostDeveloped?: string | false; mostDeveloped?: string | false;
private?: boolean; private?: boolean;
created?: number; created?: number;

View File

@ -1,30 +1,14 @@
export function normalizeIncoming(msg: unknown): { type: string | null, data: unknown } {
if (!msg) return { type: null, data: null };
let parsed: unknown = null;
try {
if (typeof msg === 'string') {
parsed = JSON.parse(msg);
} else {
parsed = msg;
}
} catch (e) {
return { type: null, data: null };
}
if (!parsed) return { type: null, data: null };
const type = (parsed as any).type || (parsed as any).action || null;
const data = (parsed as any).data || (Object.keys(parsed as any).length ? Object.assign({}, parsed as any) : null);
return { type, data };
}
export function shuffleArray<T>(array: T[]): T[] { export function shuffleArray<T>(array: T[]): T[] {
let currentIndex = array.length, temporaryValue: T | undefined, randomIndex: number; let currentIndex = array.length,
temporaryValue: T | undefined,
randomIndex: number;
while (0 !== currentIndex) { while (0 !== currentIndex) {
randomIndex = Math.floor(Math.random() * currentIndex); randomIndex = Math.floor(Math.random() * currentIndex);
currentIndex -= 1; currentIndex -= 1;
// use non-null assertions because we're swapping indices that exist // use non-null assertions because we're swapping indices that exist
temporaryValue = array[currentIndex] as T; temporaryValue = array[currentIndex] as T;
array[currentIndex] = array[randomIndex] as T; array[currentIndex] = array[randomIndex] as T;
array[randomIndex] = temporaryValue as T; array[randomIndex] = temporaryValue as T;
} }
return array; return array;
} }

View File

@ -1,19 +1,26 @@
/* WebRTC signaling helpers extracted from games.ts /* WebRTC signaling helpers extracted from games.ts
* Exports: * Exports:
* - audio: map of gameId -> peers * - audio: map of gameId -> peers
* - join(peers, session, config, safeSend) * - join(peers, session, config)
* - part(peers, session, safeSend) * - part(peers, session)
* - handleRelayICECandidate(gameId, cfg, session, safeSend, debug) * - handleRelayICECandidate(gameId, cfg, session, debug)
* - handleRelaySessionDescription(gameId, cfg, session, safeSend, debug) * - handleRelaySessionDescription(gameId, cfg, session, debug)
* - broadcastPeerStateUpdate(gameId, cfg, session, safeSend) * - broadcastPeerStateUpdate(gameId, cfg, session)
*/ */
import { Session } from "./games/types";
export const audio: Record<string, any> = {}; export const audio: Record<string, any> = {};
// Default send helper used when caller doesn't provide a safeSend implementation. // Default send helper used when caller doesn't provide a safeSend implementation.
const defaultSend = (targetOrSession: any, message: any): boolean => { const defaultSend = (targetOrSession: any, message: any): boolean => {
try { try {
const target = targetOrSession && typeof targetOrSession.send === "function" ? targetOrSession : targetOrSession && targetOrSession.ws ? targetOrSession.ws : null; const target =
targetOrSession && typeof targetOrSession.send === "function"
? targetOrSession
: targetOrSession && targetOrSession.ws
? targetOrSession.ws
: null;
if (!target) return false; if (!target) return false;
target.send(typeof message === "string" ? message : JSON.stringify(message)); target.send(typeof message === "string" ? message : JSON.stringify(message));
return true; return true;
@ -22,13 +29,8 @@ const defaultSend = (targetOrSession: any, message: any): boolean => {
} }
}; };
export const join = ( export const join = (peers: any, session: any): void => {
peers: any, const send = defaultSend;
session: any,
{ hasVideo, hasAudio, has_media }: { hasVideo?: boolean; hasAudio?: boolean; has_media?: boolean },
safeSend?: (targetOrSession: any, message: any) => boolean
): void => {
const send = safeSend ? safeSend : defaultSend;
const ws = session.ws; const ws = session.ws;
if (!session.name) { if (!session.name) {
@ -43,23 +45,18 @@ export const join = (
console.log(`${session.short}: <- join - ${session.name}`); console.log(`${session.short}: <- join - ${session.name}`);
// Determine media capability - prefer has_media if provided // Use session.id as the canonical peer key
const peerHasMedia = has_media !== undefined ? has_media : hasVideo || hasAudio; if (session.id in peers) {
console.log(`${session.short}:${session.id} - Already joined to Audio, updating WebSocket reference.`);
if (session.name in peers) {
console.log(`${session.short}:${session.name} - Already joined to Audio, updating WebSocket reference.`);
try { try {
const prev = peers[session.name] && peers[session.name].ws; const prev = peers[session.id] && peers[session.id].ws;
if (prev && prev._pingInterval) { if (prev && prev._pingInterval) {
clearInterval(prev._pingInterval); clearInterval(prev._pingInterval);
} }
} catch (e) { } catch (e) {
/* ignore */ /* ignore */
} }
peers[session.name].ws = ws; peers[session.id].ws = ws;
peers[session.name].has_media = peerHasMedia;
peers[session.name].hasAudio = hasAudio;
peers[session.name].hasVideo = hasVideo;
send(ws, { send(ws, {
type: "join_status", type: "join_status",
@ -67,34 +64,30 @@ export const join = (
message: "Reconnected", message: "Reconnected",
}); });
for (const peer in peers) { // Tell the reconnecting client about existing peers
if (peer === session.name) continue; for (const peerId in peers) {
if (peerId === session.id) continue;
send(ws, { send(ws, {
type: "addPeer", type: "addPeer",
data: { data: {
peer_id: peer, peer_id: peerId,
peer_name: peer, peer_name: peers[peerId].name || peerId,
has_media: peers[peer].has_media,
should_create_offer: true, should_create_offer: true,
hasAudio: peers[peer].hasAudio,
hasVideo: peers[peer].hasVideo,
}, },
}); });
} }
for (const peer in peers) { // Tell existing peers about the reconnecting client
if (peer === session.name) continue; for (const peerId in peers) {
if (peerId === session.id) continue;
send(peers[peer].ws, { send(peers[peerId].ws, {
type: "addPeer", type: "addPeer",
data: { data: {
peer_id: session.name, peer_id: session.id,
peer_name: session.name, peer_name: session.name,
has_media: peerHasMedia,
should_create_offer: false, should_create_offer: false,
hasAudio,
hasVideo,
}, },
}); });
} }
@ -102,37 +95,32 @@ export const join = (
return; return;
} }
for (let peer in peers) { for (let peerId in peers) {
send(peers[peer].ws, { // notify existing peers about the new client
send(peers[peerId].ws, {
type: "addPeer", type: "addPeer",
data: { data: {
peer_id: session.name, peer_id: session.id,
peer_name: session.name, peer_name: session.name,
has_media: peers[session.name]?.has_media ?? peerHasMedia,
should_create_offer: false, should_create_offer: false,
hasAudio,
hasVideo,
}, },
}); });
// tell the new client about existing peers
send(ws, { send(ws, {
type: "addPeer", type: "addPeer",
data: { data: {
peer_id: peer, peer_id: peerId,
peer_name: peer, peer_name: peers[peerId].name || peerId,
has_media: peers[peer].has_media,
should_create_offer: true, should_create_offer: true,
hasAudio: peers[peer].hasAudio,
hasVideo: peers[peer].hasVideo,
}, },
}); });
} }
peers[session.name] = { // Store peer keyed by session.id and keep the display name
peers[session.id] = {
ws, ws,
hasAudio, name: session.name,
hasVideo,
has_media: peerHasMedia,
}; };
send(ws, { send(ws, {
@ -142,16 +130,16 @@ export const join = (
}); });
}; };
export const part = (peers: any, session: any, safeSend?: (targetOrSession: any, message: any) => boolean): void => { export const part = (peers: any, session: any): void => {
const ws = session.ws; const ws = session.ws;
const send = safeSend ? safeSend : defaultSend; const send = defaultSend;
if (!session.name) { if (!session.name) {
console.error(`${session.id}: <- part - No name set yet. Audio not available.`); console.error(`${session.id}: <- part - No name set yet. Audio not available.`);
return; return;
} }
if (!(session.name in peers)) { if (!(session.id in peers)) {
console.log(`${session.short}: <- ${session.name} - Does not exist in game audio.`); console.log(`${session.short}: <- ${session.name} - Does not exist in game audio.`);
return; return;
} }
@ -159,34 +147,29 @@ export const part = (peers: any, session: any, safeSend?: (targetOrSession: any,
console.log(`${session.short}: <- ${session.name} - Audio part.`); console.log(`${session.short}: <- ${session.name} - Audio part.`);
console.log(`${session.short}: -> removePeer - ${session.name}`); console.log(`${session.short}: -> removePeer - ${session.name}`);
delete peers[session.name]; // Remove this peer
delete peers[session.id];
for (let peer in peers) { for (let peerId in peers) {
send(peers[peer].ws, { send(peers[peerId].ws, {
type: "removePeer", type: "removePeer",
data: { data: {
peer_id: session.name, peer_id: session.id,
peer_name: session.name, peer_name: session.name,
}, },
}); });
send(ws, { send(ws, {
type: "removePeer", type: "removePeer",
data: { data: {
peer_id: peer, peer_id: peerId,
peer_name: peer, peer_name: peers[peerId].name || peerId,
}, },
}); });
} }
}; };
export const handleRelayICECandidate = ( export const handleRelayICECandidate = (gameId: string, cfg: any, session: Session, debug?: any) => {
gameId: string, const send = defaultSend;
cfg: any,
session: any,
safeSend?: (targetOrSession: any, message: any) => boolean,
debug?: any
) => {
const send = safeSend ? safeSend : defaultSend;
const ws = session && session.ws; const ws = session && session.ws;
if (!cfg) { if (!cfg) {
@ -200,12 +183,13 @@ export const handleRelayICECandidate = (
return; return;
} }
const { peer_id, candidate } = cfg; const { peer_id, candidate } = cfg;
if (debug && debug.audio) console.log(`${session.id}:${gameId} <- relayICECandidate ${session.name} to ${peer_id}`, candidate); if (debug && debug.audio)
console.log(`${session.id}:${gameId} <- relayICECandidate ${session.name} to ${peer_id}`, candidate);
const message = JSON.stringify({ const message = JSON.stringify({
type: "iceCandidate", type: "iceCandidate",
data: { data: {
peer_id: session.name, peer_id: session.id,
peer_name: session.name, peer_name: session.name,
candidate, candidate,
}, },
@ -221,14 +205,8 @@ export const handleRelayICECandidate = (
} }
}; };
export const handleRelaySessionDescription = ( export const handleRelaySessionDescription = (gameId: string, cfg: any, session: any, debug?: any) => {
gameId: string, const send = defaultSend;
cfg: any,
session: any,
safeSend?: (targetOrSession: any, message: any) => boolean,
debug?: any
) => {
const send = safeSend ? safeSend : defaultSend;
const ws = session && session.ws; const ws = session && session.ws;
if (!cfg) { if (!cfg) {
@ -253,7 +231,7 @@ export const handleRelaySessionDescription = (
const message = JSON.stringify({ const message = JSON.stringify({
type: "sessionDescription", type: "sessionDescription",
data: { data: {
peer_id: session.name, peer_id: session.id,
peer_name: session.name, peer_name: session.name,
session_description, session_description,
}, },
@ -268,19 +246,22 @@ export const handleRelaySessionDescription = (
} }
}; };
export const broadcastPeerStateUpdate = (gameId: string, cfg: any, session: any, safeSend?: (targetOrSession: any, message: any) => boolean) => { export const broadcastPeerStateUpdate = (gameId: string, cfg: any, session: any) => {
const send = safeSend const send = (targetOrSession: any, message: any) => {
? safeSend try {
: (targetOrSession: any, message: any) => { const target =
try { targetOrSession && typeof targetOrSession.send === "function"
const target = targetOrSession && typeof targetOrSession.send === "function" ? targetOrSession : targetOrSession && targetOrSession.ws ? targetOrSession.ws : null; ? targetOrSession
if (!target) return false; : targetOrSession && targetOrSession.ws
target.send(typeof message === "string" ? message : JSON.stringify(message)); ? targetOrSession.ws
return true; : null;
} catch (e) { if (!target) return false;
return false; target.send(typeof message === "string" ? message : JSON.stringify(message));
} return true;
}; } catch (e) {
return false;
}
};
if (!(gameId in audio)) { if (!(gameId in audio)) {
console.error(`${session.id}:${gameId} <- peer_state_update - Does not have Audio`); console.error(`${session.id}:${gameId} <- peer_state_update - Does not have Audio`);
@ -296,24 +277,24 @@ export const broadcastPeerStateUpdate = (gameId: string, cfg: any, session: any,
const messagePayload = JSON.stringify({ const messagePayload = JSON.stringify({
type: "peer_state_update", type: "peer_state_update",
data: { data: {
peer_id: session.name, peer_id: session.id,
peer_name: session.name, peer_name: session.name,
muted, muted,
video_on, video_on,
}, },
}); });
for (const other in audio[gameId]) { for (const otherId in audio[gameId]) {
if (other === session.name) continue; if (otherId === session.id) continue;
try { try {
const tgt = audio[gameId][other] as any; const tgt = audio[gameId][otherId] as any;
if (!tgt || !tgt.ws) { if (!tgt || !tgt.ws) {
console.warn(`${session.id}:${gameId} peer_state_update - target ${other} has no ws`); console.warn(`${session.id}:${gameId} peer_state_update - target ${otherId} has no ws`);
} else if (!send(tgt.ws, messagePayload)) { } else if (!send(tgt.ws, messagePayload)) {
console.warn(`${session.id}:${gameId} peer_state_update - send failed to ${other}`); console.warn(`${session.id}:${gameId} peer_state_update - send failed to ${otherId}`);
} }
} catch (e) { } catch (e) {
console.warn(`Failed sending peer_state_update to ${other}:`, e); console.warn(`Failed sending peer_state_update to ${otherId}:`, e);
} }
} }
}; };

View File

@ -317,13 +317,13 @@ const staticData = {
{ roll: 7, pips: 0 } /* Robber is at the end or indexing gets off */, { roll: 7, pips: 0 } /* Robber is at the end or indexing gets off */,
], ],
borders: [ borders: [
["bank", undefined, "sheep"], ["bank", "none", "sheep"],
[undefined, "bank", undefined], ["none", "bank", "none"],
["bank", undefined, "brick"], ["bank", "none", "brick"],
[undefined, "wood", undefined], ["none", "wood", "none"],
["bank", undefined, "wheat"], ["bank", "none", "wheat"],
[undefined, "stone", undefined], ["none", "stone", "none"],
], ] as ResourceType[][],
}; };
export { export {

View File

@ -85,9 +85,13 @@ const getValidCorners = (game: Game, color: PlayerColor = "unassigned", type?: C
* Volcano is enabled, verify the tile is not the Volcano. * Volcano is enabled, verify the tile is not the Volcano.
*/ */
layout.corners.forEach((corner, cornerIndex) => { layout.corners.forEach((corner, cornerIndex) => {
const placement = game.placements.corners[cornerIndex]!; const placement = game.placements && game.placements.corners ? game.placements.corners[cornerIndex] : undefined;
if (!placement) {
// Treat a missing placement as unassigned (no owner)
// Continue processing using a falsy placement where appropriate
}
if (type) { if (type) {
if (placement.color === color && placement.type === type) { if (placement && placement.color === color && placement.type === type) {
limits.push(cornerIndex); limits.push(cornerIndex);
} }
return; return;
@ -97,7 +101,7 @@ const getValidCorners = (game: Game, color: PlayerColor = "unassigned", type?: C
// "unassigned" then it's occupied and should be skipped. // "unassigned" then it's occupied and should be skipped.
// Note: placement.color may be undefined (initial state), treat that // Note: placement.color may be undefined (initial state), treat that
// the same as unassigned. // the same as unassigned.
if (placement.color && placement.color !== "unassigned") { if (placement && placement.color && placement.color !== "unassigned") {
return; return;
} }
@ -119,29 +123,30 @@ const getValidCorners = (game: Game, color: PlayerColor = "unassigned", type?: C
} }
for (let r = 0; valid && r < (corner.roads || []).length; r++) { for (let r = 0; valid && r < (corner.roads || []).length; r++) {
if (!corner.roads) { if (!corner.roads) {
break; break;
} }
const ridx = corner.roads[r]; const ridx = corner.roads[r];
if (ridx == null || layout.roads[ridx] == null) { if (ridx == null || layout.roads[ridx] == null) {
continue;
}
const road = layout.roads[ridx];
for (let c = 0; valid && c < (road.corners || []).length; c++) {
/* This side of the road is pointing to the corner being validated.
* Skip it. */
if (road.corners[c] === cornerIndex) {
continue; continue;
} }
/* There is a settlement within one segment from this const road = layout.roads[ridx];
* corner, so it is invalid for settlement placement */ for (let c = 0; valid && c < (road.corners || []).length; c++) {
const cc = road.corners[c] as number; /* This side of the road is pointing to the corner being validated.
const ccColor = game.placements.corners[cc]!.color; * Skip it. */
if (ccColor && ccColor !== "unassigned") { if (road.corners[c] === cornerIndex) {
valid = false; continue;
}
/* There is a settlement within one segment from this
* corner, so it is invalid for settlement placement */
const cc = road.corners[c] as number;
const ccPlacement = game.placements && game.placements.corners ? game.placements.corners[cc] : undefined;
const ccColor = ccPlacement ? ccPlacement.color : undefined;
if (ccColor && ccColor !== "unassigned") {
valid = false;
}
} }
} }
}
if (valid) { if (valid) {
/* During initial placement, if volcano is enabled, do not allow /* During initial placement, if volcano is enabled, do not allow
* placement on a corner connected to the volcano (robber starts * placement on a corner connected to the volcano (robber starts