diff --git a/client/src/Board.tsx b/client/src/Board.tsx index e87e84a..7361686 100644 --- a/client/src/Board.tsx +++ b/client/src/Board.tsx @@ -106,7 +106,7 @@ const Board: React.FC = ({ animations }) => { const [signature, setSignature] = useState(""); const [generated, setGenerated] = useState(""); const [robber, setRobber] = useState(-1); - const [robberName, setRobberName] = useState([]); + const [robberName, setRobberName] = useState(""); const [pips, setPips] = useState(undefined); // Keep as any for now, complex structure const [pipOrder, setPipOrder] = useState(undefined); const [borders, setBorders] = useState(undefined); @@ -147,86 +147,123 @@ const Board: React.FC = ({ animations }) => { return; } 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) { case "game-update": - console.log(`board - game update`, data.update); - if ("robber" in data.update && data.update.robber !== robber) { - setRobber(data.update.robber); - } + handleUpdate(data.update || {}); + break; - if ("robberName" in data.update && data.update.robberName !== robberName) { - setRobberName(data.update.robberName); - } + case "initial-game": + // 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) { - 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); + handleUpdate(initialUpdate); } break; default: @@ -508,10 +545,6 @@ const Board: React.FC = ({ animations }) => { console.log(`board - Generate pip, border, and tile elements`); const Pip: React.FC = ({ pip, className }) => { const onPipClicked = (pip) => { - if (!ws) { - console.error(`board - sendPlacement - ws is NULL`); - return; - } sendJsonMessage({ type: "place-robber", index: pip.index, @@ -928,7 +961,7 @@ const Board: React.FC = ({ animations }) => { const el = document.querySelector(`.Pip[data-index="${robber}"]`); if (el) { el.classList.add("Robber"); - el.classList.add(robberName); + if (robberName) el.classList.add(robberName); } } }); diff --git a/client/src/MediaControl.tsx b/client/src/MediaControl.tsx index f323e71..a55ffc6 100644 --- a/client/src/MediaControl.tsx +++ b/client/src/MediaControl.tsx @@ -14,6 +14,7 @@ import { useContext } from "react"; import WebRTCStatus from "./WebRTCStatus"; import Moveable from "react-moveable"; import { flushSync } from "react-dom"; +import { SxProps, Theme } from "@mui/material"; const debug = true; // 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; remoteAudioMuted?: boolean; remoteVideoOff?: boolean; + sx?: SxProps; } const MediaControl: React.FC = ({ @@ -1313,6 +1315,7 @@ const MediaControl: React.FC = ({ sendJsonMessage, remoteAudioMuted, remoteVideoOff, + sx, }) => { const [muted, setMuted] = useState(peer?.muted || false); const [videoOn, setVideoOn] = useState(peer?.video_on !== false); @@ -1591,6 +1594,7 @@ const MediaControl: React.FC = ({ alignItems: "center", minWidth: "200px", minHeight: "100px", + ...sx, }} >
{ [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 useEffect(() => { if (!lastJsonMessage) { @@ -179,6 +193,7 @@ const PlayerList: React.FC = () => { {player.name && player.live && peers[player.session_id] && (player.local || player.has_media !== false) ? ( <> = {}; const loadAudio = (src: string) => { const audio = document.createElement("audio") as AudioEffect; audio.src = audioFiles[src]; - console.log("Loading audio:", audio.src); audio.setAttribute("preload", "auto"); audio.setAttribute("controls", "none"); audio.style.display = "none"; document.body.appendChild(audio); audio.load(); audio.addEventListener("error", (e) => console.error("Audio load error:", e, audio.src)); - audio.addEventListener("canplay", () => console.log("Audio can play:", audio.src)); return audio; }; diff --git a/original/birds.png b/original/birds.png deleted file mode 100755 index 8f58063..0000000 Binary files a/original/birds.png and /dev/null differ diff --git a/original/birds.xcf b/original/birds.xcf deleted file mode 100755 index c1979ce..0000000 Binary files a/original/birds.xcf and /dev/null differ diff --git a/original/borders-1.6.jpg b/original/borders-1.6.jpg deleted file mode 100644 index cb2e377..0000000 Binary files a/original/borders-1.6.jpg and /dev/null differ diff --git a/original/borders-1.6.png b/original/borders-1.6.png deleted file mode 100644 index a64d5d4..0000000 Binary files a/original/borders-1.6.png and /dev/null differ diff --git a/original/borders-2.1.jpg b/original/borders-2.1.jpg deleted file mode 100644 index 1a7c380..0000000 Binary files a/original/borders-2.1.jpg and /dev/null differ diff --git a/original/borders-2.1.png b/original/borders-2.1.png deleted file mode 100644 index 840bc74..0000000 Binary files a/original/borders-2.1.png and /dev/null differ diff --git a/original/borders-3.2.jpg b/original/borders-3.2.jpg deleted file mode 100644 index 2176672..0000000 Binary files a/original/borders-3.2.jpg and /dev/null differ diff --git a/original/borders-3.2.png b/original/borders-3.2.png deleted file mode 100644 index 8a969d2..0000000 Binary files a/original/borders-3.2.png and /dev/null differ diff --git a/original/borders-4.3.jpg b/original/borders-4.3.jpg deleted file mode 100644 index 0003707..0000000 Binary files a/original/borders-4.3.jpg and /dev/null differ diff --git a/original/borders-4.3.png b/original/borders-4.3.png deleted file mode 100644 index 4f94e3c..0000000 Binary files a/original/borders-4.3.png and /dev/null differ diff --git a/original/borders-5.4.jpg b/original/borders-5.4.jpg deleted file mode 100644 index fde9bfb..0000000 Binary files a/original/borders-5.4.jpg and /dev/null differ diff --git a/original/borders-5.4.png b/original/borders-5.4.png deleted file mode 100644 index fdfefd3..0000000 Binary files a/original/borders-5.4.png and /dev/null differ diff --git a/original/borders-6.5.jpg b/original/borders-6.5.jpg deleted file mode 100644 index 6ccbfed..0000000 Binary files a/original/borders-6.5.jpg and /dev/null differ diff --git a/original/borders-6.5.png b/original/borders-6.5.png deleted file mode 100644 index 31d1110..0000000 Binary files a/original/borders-6.5.png and /dev/null differ diff --git a/original/card-army-1.png b/original/card-army-1.png deleted file mode 100644 index e31b395..0000000 Binary files a/original/card-army-1.png and /dev/null differ diff --git a/original/card-army-10.png b/original/card-army-10.png deleted file mode 100644 index c9b4d79..0000000 Binary files a/original/card-army-10.png and /dev/null differ diff --git a/original/card-army-11.png b/original/card-army-11.png deleted file mode 100644 index f473c34..0000000 Binary files a/original/card-army-11.png and /dev/null differ diff --git a/original/card-army-12.png b/original/card-army-12.png deleted file mode 100644 index 8bc21f0..0000000 Binary files a/original/card-army-12.png and /dev/null differ diff --git a/original/card-army-13.png b/original/card-army-13.png deleted file mode 100644 index 4eb68f7..0000000 Binary files a/original/card-army-13.png and /dev/null differ diff --git a/original/card-army-14.png b/original/card-army-14.png deleted file mode 100644 index f9f4db8..0000000 Binary files a/original/card-army-14.png and /dev/null differ diff --git a/original/card-army-2.png b/original/card-army-2.png deleted file mode 100644 index 075477a..0000000 Binary files a/original/card-army-2.png and /dev/null differ diff --git a/original/card-army-3.png b/original/card-army-3.png deleted file mode 100644 index 1ae04c8..0000000 Binary files a/original/card-army-3.png and /dev/null differ diff --git a/original/card-army-4.png b/original/card-army-4.png deleted file mode 100644 index 1980779..0000000 Binary files a/original/card-army-4.png and /dev/null differ diff --git a/original/card-army-5.png b/original/card-army-5.png deleted file mode 100644 index 0d5b099..0000000 Binary files a/original/card-army-5.png and /dev/null differ diff --git a/original/card-army-6.png b/original/card-army-6.png deleted file mode 100644 index ce5bd5c..0000000 Binary files a/original/card-army-6.png and /dev/null differ diff --git a/original/card-army-7.png b/original/card-army-7.png deleted file mode 100644 index 0421ae5..0000000 Binary files a/original/card-army-7.png and /dev/null differ diff --git a/original/card-army-8.png b/original/card-army-8.png deleted file mode 100644 index a85e233..0000000 Binary files a/original/card-army-8.png and /dev/null differ diff --git a/original/card-army-9.png b/original/card-army-9.png deleted file mode 100644 index e2a262f..0000000 Binary files a/original/card-army-9.png and /dev/null differ diff --git a/original/card-brick.png b/original/card-brick.png deleted file mode 100644 index a32ac09..0000000 Binary files a/original/card-brick.png and /dev/null differ diff --git a/original/card-monopoly.png b/original/card-monopoly.png deleted file mode 100644 index 76bfec3..0000000 Binary files a/original/card-monopoly.png and /dev/null differ diff --git a/original/card-road-1.png b/original/card-road-1.png deleted file mode 100644 index edf4fa8..0000000 Binary files a/original/card-road-1.png and /dev/null differ diff --git a/original/card-road-2.png b/original/card-road-2.png deleted file mode 100644 index 6ab8f17..0000000 Binary files a/original/card-road-2.png and /dev/null differ diff --git a/original/card-sheep.png b/original/card-sheep.png deleted file mode 100644 index 652618e..0000000 Binary files a/original/card-sheep.png and /dev/null differ diff --git a/original/card-stone.png b/original/card-stone.png deleted file mode 100644 index 53f8d91..0000000 Binary files a/original/card-stone.png and /dev/null differ diff --git a/original/card-vp-library.png b/original/card-vp-library.png deleted file mode 100644 index 1af2088..0000000 Binary files a/original/card-vp-library.png and /dev/null differ diff --git a/original/card-vp-market.png b/original/card-vp-market.png deleted file mode 100644 index 05a78f7..0000000 Binary files a/original/card-vp-market.png and /dev/null differ diff --git a/original/card-vp-palace.png b/original/card-vp-palace.png deleted file mode 100644 index 042e93d..0000000 Binary files a/original/card-vp-palace.png and /dev/null differ diff --git a/original/card-vp-university.png b/original/card-vp-university.png deleted file mode 100644 index ec2fe32..0000000 Binary files a/original/card-vp-university.png and /dev/null differ diff --git a/original/card-wheat.png b/original/card-wheat.png deleted file mode 100644 index e20d4c4..0000000 Binary files a/original/card-wheat.png and /dev/null differ diff --git a/original/card-wood.png b/original/card-wood.png deleted file mode 100644 index 315b035..0000000 Binary files a/original/card-wood.png and /dev/null differ diff --git a/original/extra-cards.xcf b/original/extra-cards.xcf deleted file mode 100755 index 038eb77..0000000 Binary files a/original/extra-cards.xcf and /dev/null differ diff --git a/original/pieces-blue.jpg b/original/pieces-blue.jpg deleted file mode 100644 index 833a9a7..0000000 Binary files a/original/pieces-blue.jpg and /dev/null differ diff --git a/original/pieces-orange.jpg b/original/pieces-orange.jpg deleted file mode 100644 index cbc917f..0000000 Binary files a/original/pieces-orange.jpg and /dev/null differ diff --git a/original/pieces-red.jpg b/original/pieces-red.jpg deleted file mode 100644 index 8ab9386..0000000 Binary files a/original/pieces-red.jpg and /dev/null differ diff --git a/original/pieces-white.jpg b/original/pieces-white.jpg deleted file mode 100644 index 62b6045..0000000 Binary files a/original/pieces-white.jpg and /dev/null differ diff --git a/original/pieces.jpg b/original/pieces.jpg deleted file mode 100644 index 729d38c..0000000 Binary files a/original/pieces.jpg and /dev/null differ diff --git a/original/pip-numbers.png b/original/pip-numbers.png deleted file mode 100644 index 1e77843..0000000 Binary files a/original/pip-numbers.png and /dev/null differ diff --git a/original/pip-ships.png b/original/pip-ships.png deleted file mode 100644 index 59d270f..0000000 Binary files a/original/pip-ships.png and /dev/null differ diff --git a/original/placard-blue.png b/original/placard-blue.png deleted file mode 100644 index 25b7b07..0000000 Binary files a/original/placard-blue.png and /dev/null differ diff --git a/original/placard-largest-army.png b/original/placard-largest-army.png deleted file mode 100644 index 2dcf62f..0000000 Binary files a/original/placard-largest-army.png and /dev/null differ diff --git a/original/placard-longest-road.png b/original/placard-longest-road.png deleted file mode 100644 index d2e3789..0000000 Binary files a/original/placard-longest-road.png and /dev/null differ diff --git a/original/placard-orange.png b/original/placard-orange.png deleted file mode 100644 index 0c8079f..0000000 Binary files a/original/placard-orange.png and /dev/null differ diff --git a/original/placard-red.png b/original/placard-red.png deleted file mode 100644 index 845d12a..0000000 Binary files a/original/placard-red.png and /dev/null differ diff --git a/original/placard-white.png b/original/placard-white.png deleted file mode 100644 index 16f2d4a..0000000 Binary files a/original/placard-white.png and /dev/null differ diff --git a/original/sheep-alpha.xcf b/original/sheep-alpha.xcf deleted file mode 100755 index 346169f..0000000 Binary files a/original/sheep-alpha.xcf and /dev/null differ diff --git a/original/sheep.png b/original/sheep.png deleted file mode 100755 index ff93c3b..0000000 Binary files a/original/sheep.png and /dev/null differ diff --git a/original/tabletop.png b/original/tabletop.png deleted file mode 100644 index ab168d4..0000000 Binary files a/original/tabletop.png and /dev/null differ diff --git a/original/tiles-brick.png b/original/tiles-brick.png deleted file mode 100644 index e3922ac..0000000 Binary files a/original/tiles-brick.png and /dev/null differ diff --git a/original/tiles-desert.png b/original/tiles-desert.png deleted file mode 100644 index e1ed070..0000000 Binary files a/original/tiles-desert.png and /dev/null differ diff --git a/original/tiles-sheep.png b/original/tiles-sheep.png deleted file mode 100644 index e0b20dc..0000000 Binary files a/original/tiles-sheep.png and /dev/null differ diff --git a/original/tiles-stone.png b/original/tiles-stone.png deleted file mode 100644 index d56e739..0000000 Binary files a/original/tiles-stone.png and /dev/null differ diff --git a/original/tiles-volcano.xcf b/original/tiles-volcano.xcf deleted file mode 100755 index 074d6f1..0000000 Binary files a/original/tiles-volcano.xcf and /dev/null differ diff --git a/original/tiles-wheat.png b/original/tiles-wheat.png deleted file mode 100644 index 58f1ce6..0000000 Binary files a/original/tiles-wheat.png and /dev/null differ diff --git a/original/tiles-wood.png b/original/tiles-wood.png deleted file mode 100644 index 517dd76..0000000 Binary files a/original/tiles-wood.png and /dev/null differ diff --git a/original/uncut/army.png b/original/uncut/army.png deleted file mode 100644 index af9058a..0000000 Binary files a/original/uncut/army.png and /dev/null differ diff --git a/original/uncut/borders.jpg b/original/uncut/borders.jpg deleted file mode 100644 index ca94395..0000000 Binary files a/original/uncut/borders.jpg and /dev/null differ diff --git a/original/uncut/cards.jpg b/original/uncut/cards.jpg deleted file mode 100644 index 27589d7..0000000 Binary files a/original/uncut/cards.jpg and /dev/null differ diff --git a/original/uncut/cards.xcf b/original/uncut/cards.xcf deleted file mode 100644 index 8c464bb..0000000 Binary files a/original/uncut/cards.xcf and /dev/null differ diff --git a/original/uncut/pips.jpg b/original/uncut/pips.jpg deleted file mode 100644 index ed066ee..0000000 Binary files a/original/uncut/pips.jpg and /dev/null differ diff --git a/original/uncut/placards.jpg b/original/uncut/placards.jpg deleted file mode 100644 index 337efd3..0000000 Binary files a/original/uncut/placards.jpg and /dev/null differ diff --git a/original/uncut/tiles.png b/original/uncut/tiles.png deleted file mode 100644 index 20f5c2d..0000000 Binary files a/original/uncut/tiles.png and /dev/null differ diff --git a/server/ai/app.ts b/server/ai/app.ts index 75ba4e7..96d5cb8 100644 --- a/server/ai/app.ts +++ b/server/ai/app.ts @@ -300,7 +300,7 @@ const bestRoadPlacement = (game) => { return; } const placedRoad = game.placements.roads[roadIndex]; - if (placedRoad.color) { + if (!placedRoad || placedRoad.color) { return; } attempt = roadIndex; diff --git a/server/ai/longest-road.ts b/server/ai/longest-road.ts index ed3e13f..f09912d 100644 --- a/server/ai/longest-road.ts +++ b/server/ai/longest-road.ts @@ -16,6 +16,9 @@ const processCorner = (game: any, color: string, cornerIndex: number, placedCorn let longest = 0; layout.corners[cornerIndex].roads.forEach((roadIndex: number) => { const placedRoad = game.placements.roads[roadIndex]; + if (!placedRoad) { + return; + } if (placedRoad.walking) { return; } @@ -42,11 +45,12 @@ const buildCornerGraph = (game: any, color: string, cornerIndex: number, placedC if (placedCorner.walking) { return; } - + placedCorner.walking = true; /* Calculate the longest road branching from both corners */ layout.corners[cornerIndex].roads.forEach((roadIndex: number) => { const placedRoad = game.placements.roads[roadIndex]; + if (!placedRoad) return; 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 => { /* If this road isn't assigned to the walking color, skip it */ if (placedRoad.color !== color) { - return 0; + return 0; } /* 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; /* Calculate the longest road branching from both corners */ let roadLength = 1; - layout.roads[roadIndex].corners.forEach(cornerIndex => { + layout.roads[roadIndex].corners.forEach((cornerIndex) => { const placedCorner = game.placements.corners[cornerIndex]; + if (!placedCorner) return; if (placedCorner.walking) { return; } @@ -89,21 +94,26 @@ const buildRoadGraph = (game: any, color: string, roadIndex: number, placedRoad: placedRoad.walking = true; set.push(roadIndex); /* 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]; - buildCornerGraph(game, color, cornerIndex, placedCorner, set) + if (!placedCorner) return; + buildCornerGraph(game, color, cornerIndex, placedCorner, set); }); }; const clearRoadWalking = (game: any) => { /* Clear out walk markers on roads */ 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 */ 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 = []; layout.roads.forEach((_, roadIndex) => { const placedRoad = game.placements.roads[roadIndex]; + if (!placedRoad) return; if (placedRoad.color === color) { let set = []; buildRoadGraph(game, color, roadIndex, placedRoad, set); @@ -133,13 +144,13 @@ const calculateRoadLengths = (game: any) => { let final = { segments: 0, - index: -1 + index: -1, }; clearRoadWalking(game); - graphs.forEach(graph => { + graphs.forEach((graph) => { graph.longestRoad = 0; - graph.set.forEach(roadIndex => { + graph.set.forEach((roadIndex) => { const placedRoad = game.placements.roads[roadIndex]; clearRoadWalking(game); 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; }; diff --git a/server/routes/games.ts b/server/routes/games.ts index 90d7268..ddabf0a 100755 --- a/server/routes/games.ts +++ b/server/routes/games.ts @@ -17,8 +17,8 @@ import { PlayerColor, PLAYER_COLORS, RESOURCE_TYPES, + ResourceType, } from "./games/types"; -import { normalizeIncoming } from "./games/utils"; import { audio, join as webrtcJoin, @@ -54,6 +54,7 @@ import { transientState } from "./games/sessionState"; import { createGame, resetGame, setBeginnerGame } from "./games/gameFactory"; import { getVictoryPointRule, setRules, supportedRules } from "./games/rules"; import { pickRobber } from "./games/robber"; +import { IncomingMessage } from "./games/types"; const processTies = (players: Player[]): boolean => { /* Sort the players into buckets based on their @@ -256,6 +257,8 @@ const processVolcano = (game: Game, session: Session, dice: number[]) => { dice: game.dice, placements: game.placements, players: getFilteredPlayers(game), + longestRoad: game.longestRoad, + longestRoadLength: game.longestRoadLength, }); }; @@ -327,6 +330,7 @@ interface ResourceCount { wheat: number; stone: number; desert: number; + bank: number; } type Received = Record; @@ -361,6 +365,13 @@ const distributeResources = (game: Game, roll: number): void => { 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 */ matchedTiles.forEach((tile: Tile) => { @@ -379,18 +390,21 @@ const distributeResources = (game: Game, roll: number): void => { return; } tileLayout.corners.forEach((cornerIndex: number) => { + // corners may refer to undefined slots in placements; guard against that const active = game.placements.corners[cornerIndex]; - if (!active) { + if (!active || !active.color) { return; } + const count = active.type === "settlement" ? 1 : 2; if (!tile.robber) { - if (resource.type) { - try { - receives[active.color][resource.type]! += count; - } catch (e) { - console.error("Error incrementing resources", { receives, active, resource, count }); + if (resource && resource.type) { + // ensure receives entry for this color exists + if (!receives[active.color]) { + receives[active.color] = {} as ResourceCount; + RESOURCE_TYPES.forEach((t) => (receives[active.color]![t] = 0)); } + receives[active.color]![resource.type] = (receives[active.color]![resource.type] || 0) + count; } } else { const victim = game.players[active.color]; @@ -400,10 +414,19 @@ const distributeResources = (game: Game, roll: number): void => { null, `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 { - trackTheft(game, active.color, "robber", resource.type, count); - if (resource.type) receives.robber[resource.type] += count; + // If resource.type is falsy, skip. + 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") { s = sessionFromColor(game, color); if (s && s.player) { - s.player[type] += entry[type]; - s.player.resources += entry[type]; + switch (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}`); } } else { @@ -1188,7 +1222,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und message = `${name} has rejoined the lobby.`; } 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); } } else { @@ -1211,10 +1245,7 @@ const setPlayerName = (game: Game, session: Session, name: string): string | und } if (session.ws && session.hasAudio) { - webrtcJoin(audio[game.id], session, { - hasVideo: session.video ? true : false, - hasAudio: session.audio ? true : false, - }); + webrtcJoin(audio[game.id], session); } console.log(`${info}: ${message}`); addChatMessage(game, null, message); @@ -1576,7 +1607,7 @@ const calculateRoadLengths = (game: Game, session: Session): void => { if (debug.road) console.log( "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) { @@ -1610,7 +1641,7 @@ const calculateRoadLengths = (game: Game, session: Session): void => { if (debug.road) console.log( "Post update:", - game.placements.roads.filter((road: any) => road.color) + game.placements.roads.filter((road: any) => road && road.color) ); let checkForTies = false; @@ -2277,6 +2308,8 @@ const placeRobber = (game: Game, session: Session, robber: number | string): str robber: game.robber, robberName: game.robberName, activities: game.activities, + longestRoad: game.longestRoad, + longestRoadLength: game.longestRoadLength, }); sendUpdateToPlayer(game, session, { 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 => { if (!session.player) return `You are not playing a player.`; - const player: any = session.player; - const anyGame: any = game as any; + const player: Player = session.player; if (typeof index === "string") index = parseInt(index); 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... */ - if ( - !anyGame.placements || - anyGame.placements.corners === undefined || - anyGame.placements.corners[index] === undefined - ) { + if (!game.placements || game.placements.corners === undefined || game.placements.corners[index] === undefined) { return `You have requested to place a settlement illegally!`; } @@ -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) { return `You tried to cheat! You should not try to break the rules.`; } - const corner = anyGame.placements.corners[index]; - if (corner.color) { + const corner = game.placements.corners[index]!; + if (corner.color && corner.color !== "unassigned") { const owner = game.players && game.players[corner.color]; const ownerName = owner ? owner.name : "unknown"; 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.wheat = (player.wheat || 0) - 1; player.sheep = (player.sheep || 0) - 1; - player.resources = 0; - ["wheat", "brick", "sheep", "stone", "wood"].forEach((resource) => { - player.resources += player[resource] || 0; - }); + player.resources -= 4; } delete game.turn.free; @@ -2641,8 +2666,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 = anyGame.borderOrder[Math.floor(bank / 3)], - type = anyGame.borders?.[border]?.[bank % 3]; + const border = game.borderOrder[Math.floor(bank / 3)]!, + type: ResourceType = staticData.borders?.[border]?.[bank % 3]!; console.log(`${session.short}: Bank ${bank} = ${type}`); if (!type) { console.log(`${session.short}: Bank ${bank}`); @@ -2656,11 +2681,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, 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; + console.log(`Checking port-of-call`, player.ports, game.mostPorts); + if (player.ports >= 3 && (!game.mostPorts || player.ports > game.mostPortCount)) { + if (game.mostPorts !== session.color) { + game.mostPorts = session.color; + game.mostPortCount = player.ports; addChatMessage(game, session, `${session.name} now has the most ports (${player.ports})!`); } } @@ -2676,7 +2701,7 @@ const placeSettlement = (game: Game, session: Session, index: number | string): } calculateRoadLengths(game, session); } else if (game.state === "initial-placement") { - if (anyGame.direction && anyGame.direction === "backward") { + if (game.direction && game.direction === "backward") { (session as any).initialSettlement = index; } corner.color = session.color || ""; @@ -2685,8 +2710,8 @@ const placeSettlement = (game: Game, session: Session, index: number | string): const banks2 = layout.corners?.[index]?.banks; if (banks2 && banks2.length) { banks2.forEach((bank: any) => { - const border = anyGame.borderOrder[Math.floor(bank / 3)], - type = anyGame.borders?.[border]?.[bank % 3]; + const border = game.borderOrder[Math.floor(bank / 3)]!, + type = staticData.borders?.[border]?.[bank % 3]; console.log(`${session.short}: Bank ${bank} = ${type}`); if (!type) { 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.`); player.brick--; player.wood--; - player.resources = 0; - ["wheat", "brick", "sheep", "stone", "wood"].forEach((resource) => { - player.resources += player[resource]; - }); + player.resources -= 2; } delete game.turn.free; } @@ -2890,6 +2912,8 @@ const placeRoad = (game: Game, session: Session, index: number): string | undefi players: getFilteredPlayers(game), state: game.state, direction: (game as any)["direction"], + longestRoad: game.longestRoad, + longestRoadLength: game.longestRoadLength, }); return undefined; }; @@ -3279,6 +3303,8 @@ const placeCity = (game: any, session: any, index: any): string | undefined => { chat: game.chat, activities: game.activities, players: getFilteredPlayers(game), + longestRoad: game.longestRoad, + longestRoadLength: game.longestRoadLength, }); return undefined; }; @@ -3623,6 +3649,23 @@ const gotoLobby = (game: any, session: any): string | 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) => { console.log("New WebSocket connection"); if (!req.cookies || !(req.cookies as any)["player"]) { @@ -3683,7 +3726,7 @@ router.ws("/ws/:id", async (ws, req) => { /* ignore logging errors */ } 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`); } else { 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 // reliably access the payload without repeated defensive checks. const incoming = normalizeIncoming(message); - if (!incoming.type) { + if (!incoming || !incoming.type) { // If we couldn't parse or determine the type, log and ignore the // message to preserve previous behavior. try { @@ -3856,7 +3899,7 @@ router.ws("/ws/:id", async (ws, req) => { } return; } - const data = (incoming.data as any) || {}; + const data = incoming.data; const game = await loadGame(gameId); const _session = getSession( game, @@ -3925,7 +3968,7 @@ router.ws("/ws/:id", async (ws, req) => { // Accept either legacy `config`, newer `data`, or flat payloads where // the client sent fields at the top level (normalizeIncoming will // populate `data` with the parsed object in that case). - webrtcJoin(audio[gameId], session, data.config || data.data || data || {}); + webrtcJoin(audio[gameId], session); break; case "part": @@ -3937,7 +3980,7 @@ router.ws("/ws/:id", async (ws, req) => { // Delegate to the webrtc signaling helper (it performs its own checks) // Accept either config/data or a flat payload (data). const cfg = data.config || data.data || data || {}; - handleRelayICECandidate(gameId, cfg, session, undefined, debug); + handleRelayICECandidate(gameId, cfg, session, debug); } break; @@ -3945,7 +3988,7 @@ router.ws("/ws/:id", async (ws, req) => { { // Accept either config/data or a flat payload (data). const cfg = data.config || data.data || data || {}; - handleRelaySessionDescription(gameId, cfg, session, undefined, debug); + handleRelaySessionDescription(gameId, cfg, session, debug); } break; @@ -3977,7 +4020,7 @@ router.ws("/ws/:id", async (ws, req) => { { // Accept either config/data or a flat payload (data). const cfg = data.config || data.data || data || {}; - broadcastPeerStateUpdate(gameId, cfg, session, undefined); + broadcastPeerStateUpdate(gameId, cfg, session); } break; @@ -4069,7 +4112,6 @@ router.ws("/ws/:id", async (ws, req) => { case "longestRoadLength": case "robber": case "robberName": - case "pips": case "tileOrder": case "active": case "largestArmy": @@ -4086,6 +4128,14 @@ router.ws("/ws/:id", async (ws, req) => { case "tiles": batchedUpdate.tiles = staticData.tiles; 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": batchedUpdate[field] = game.rules ? game.rules : {}; break; @@ -4519,6 +4569,18 @@ const getFilteredGameForPlayer = (game: any, session: any) => { sessions: reducedSessions, layout: layout, 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, }); }; diff --git a/server/routes/games/gameFactory.ts b/server/routes/games/gameFactory.ts index c4ac4c9..89e1c04 100644 --- a/server/routes/games/gameFactory.ts +++ b/server/routes/games/gameFactory.ts @@ -250,6 +250,8 @@ export const createGame = async (id: string | null = null) => { B: newPlayer("B"), W: newPlayer("W"), }, + mostPorts: null, + mostPortCount: 0, sessions: {}, unselected: [], placements: { diff --git a/server/routes/games/helpers.ts b/server/routes/games/helpers.ts index 88a2ab3..3a1380e 100644 --- a/server/routes/games/helpers.ts +++ b/server/routes/games/helpers.ts @@ -535,8 +535,16 @@ export const getFilteredPlayers = (game: Game): Record => { } player.resources = 0; RESOURCE_TYPES.forEach((resource) => { - player.resources += player[resource]; - delete player[resource]; + switch (resource) { + case "wood": + case "brick": + case "sheep": + case "wheat": + case "stone": + player.resources += player[resource]; + player[resource] = 0; + break; + } }); player.development = []; } diff --git a/server/routes/games/playerFactory.ts b/server/routes/games/playerFactory.ts index 853c165..ce8e41d 100644 --- a/server/routes/games/playerFactory.ts +++ b/server/routes/games/playerFactory.ts @@ -32,6 +32,7 @@ export const newPlayer = (color: PlayerColor): Player => { live: true, turnNotice: "", longestRoad: 0, + banks: [], }; }; diff --git a/server/routes/games/store.ts b/server/routes/games/store.ts index 7053be1..9bc1e25 100644 --- a/server/routes/games/store.ts +++ b/server/routes/games/store.ts @@ -52,7 +52,7 @@ export const gameDB: GameDB = { type: db.Sequelize.QueryTypes.SELECT, }); if (rows && rows.length) { - const r = rows[0] as any; + const r = rows[0]; // state may be stored as text or JSON if (typeof r.state === "string") { try { diff --git a/server/routes/games/types.ts b/server/routes/games/types.ts index b8573aa..cc9fbe0 100644 --- a/server/routes/games/types.ts +++ b/server/routes/games/types.ts @@ -25,7 +25,9 @@ export interface Player { stone: number; brick: number; wood: number; + army: number; points: number; + ports: number; resources: number; lastActive: number; live: boolean; @@ -35,7 +37,7 @@ export interface Player { turnNotice: string; turnStart: number; totalTime: number; - [key: string]: any; // allow incremental fields until fully typed + banks: ResourceType[]; } export type CornerType = "settlement" | "city" | "none"; @@ -46,7 +48,6 @@ export interface CornerPlacement { type: "settlement" | "city" | "none"; walking?: boolean; longestRoad?: number; - [key: string]: any; } export type RoadType = "road" | "ship"; @@ -60,8 +61,8 @@ export interface RoadPlacement { } export interface Placements { - corners: CornerPlacement[]; - roads: RoadPlacement[]; + corners: Array; + roads: Array; } export interface Turn { @@ -124,8 +125,8 @@ export interface Offer { [key: string]: any; } -export type ResourceType = "wood" | "brick" | "sheep" | "wheat" | "stone" | "desert"; -export const RESOURCE_TYPES: 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", "bank"]; export interface Tile { robber: boolean; @@ -166,10 +167,11 @@ export interface Game { turns: number; longestRoad?: string | false; longestRoadLength?: number; - borderOrder?: number[]; + borderOrder: number[]; largestArmy?: string | false; largestArmySize?: number; - mostPorts?: string | false; + mostPorts: PlayerColor | null; + mostPortCount: number; mostDeveloped?: string | false; private?: boolean; created?: number; diff --git a/server/routes/games/utils.ts b/server/routes/games/utils.ts index 564e32f..1183f4f 100644 --- a/server/routes/games/utils.ts +++ b/server/routes/games/utils.ts @@ -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(array: T[]): T[] { - let currentIndex = array.length, temporaryValue: T | undefined, randomIndex: number; + let currentIndex = array.length, + temporaryValue: T | undefined, + randomIndex: number; while (0 !== currentIndex) { randomIndex = Math.floor(Math.random() * currentIndex); currentIndex -= 1; - // use non-null assertions because we're swapping indices that exist - temporaryValue = array[currentIndex] as T; - array[currentIndex] = array[randomIndex] as T; - array[randomIndex] = temporaryValue as T; + // use non-null assertions because we're swapping indices that exist + temporaryValue = array[currentIndex] as T; + array[currentIndex] = array[randomIndex] as T; + array[randomIndex] = temporaryValue as T; } return array; } diff --git a/server/routes/webrtc-signaling.ts b/server/routes/webrtc-signaling.ts index fc65535..ef72dbb 100644 --- a/server/routes/webrtc-signaling.ts +++ b/server/routes/webrtc-signaling.ts @@ -1,19 +1,26 @@ /* WebRTC signaling helpers extracted from games.ts * Exports: * - audio: map of gameId -> peers - * - join(peers, session, config, safeSend) - * - part(peers, session, safeSend) - * - handleRelayICECandidate(gameId, cfg, session, safeSend, debug) - * - handleRelaySessionDescription(gameId, cfg, session, safeSend, debug) - * - broadcastPeerStateUpdate(gameId, cfg, session, safeSend) + * - join(peers, session, config) + * - part(peers, session) + * - handleRelayICECandidate(gameId, cfg, session, debug) + * - handleRelaySessionDescription(gameId, cfg, session, debug) + * - broadcastPeerStateUpdate(gameId, cfg, session) */ +import { Session } from "./games/types"; + export const audio: Record = {}; // Default send helper used when caller doesn't provide a safeSend implementation. const defaultSend = (targetOrSession: any, message: any): boolean => { 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; target.send(typeof message === "string" ? message : JSON.stringify(message)); return true; @@ -22,13 +29,8 @@ const defaultSend = (targetOrSession: any, message: any): boolean => { } }; -export const join = ( - peers: any, - 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; +export const join = (peers: any, session: any): void => { + const send = defaultSend; const ws = session.ws; if (!session.name) { @@ -43,23 +45,18 @@ export const join = ( console.log(`${session.short}: <- join - ${session.name}`); - // Determine media capability - prefer has_media if provided - const peerHasMedia = has_media !== undefined ? has_media : hasVideo || hasAudio; - - if (session.name in peers) { - console.log(`${session.short}:${session.name} - Already joined to Audio, updating WebSocket reference.`); + // Use session.id as the canonical peer key + if (session.id in peers) { + console.log(`${session.short}:${session.id} - Already joined to Audio, updating WebSocket reference.`); try { - const prev = peers[session.name] && peers[session.name].ws; + const prev = peers[session.id] && peers[session.id].ws; if (prev && prev._pingInterval) { clearInterval(prev._pingInterval); } } catch (e) { /* ignore */ } - peers[session.name].ws = ws; - peers[session.name].has_media = peerHasMedia; - peers[session.name].hasAudio = hasAudio; - peers[session.name].hasVideo = hasVideo; + peers[session.id].ws = ws; send(ws, { type: "join_status", @@ -67,34 +64,30 @@ export const join = ( message: "Reconnected", }); - for (const peer in peers) { - if (peer === session.name) continue; + // Tell the reconnecting client about existing peers + for (const peerId in peers) { + if (peerId === session.id) continue; send(ws, { type: "addPeer", data: { - peer_id: peer, - peer_name: peer, - has_media: peers[peer].has_media, + peer_id: peerId, + peer_name: peers[peerId].name || peerId, should_create_offer: true, - hasAudio: peers[peer].hasAudio, - hasVideo: peers[peer].hasVideo, }, }); } - for (const peer in peers) { - if (peer === session.name) continue; + // Tell existing peers about the reconnecting client + for (const peerId in peers) { + if (peerId === session.id) continue; - send(peers[peer].ws, { + send(peers[peerId].ws, { type: "addPeer", data: { - peer_id: session.name, + peer_id: session.id, peer_name: session.name, - has_media: peerHasMedia, should_create_offer: false, - hasAudio, - hasVideo, }, }); } @@ -102,37 +95,32 @@ export const join = ( return; } - for (let peer in peers) { - send(peers[peer].ws, { + for (let peerId in peers) { + // notify existing peers about the new client + send(peers[peerId].ws, { type: "addPeer", data: { - peer_id: session.name, + peer_id: session.id, peer_name: session.name, - has_media: peers[session.name]?.has_media ?? peerHasMedia, should_create_offer: false, - hasAudio, - hasVideo, }, }); + // tell the new client about existing peers send(ws, { type: "addPeer", data: { - peer_id: peer, - peer_name: peer, - has_media: peers[peer].has_media, + peer_id: peerId, + peer_name: peers[peerId].name || peerId, 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, - hasAudio, - hasVideo, - has_media: peerHasMedia, + name: session.name, }; 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 send = safeSend ? safeSend : defaultSend; + const send = defaultSend; if (!session.name) { console.error(`${session.id}: <- part - No name set yet. Audio not available.`); return; } - if (!(session.name in peers)) { + if (!(session.id in peers)) { console.log(`${session.short}: <- ${session.name} - Does not exist in game audio.`); 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}: -> removePeer - ${session.name}`); - delete peers[session.name]; + // Remove this peer + delete peers[session.id]; - for (let peer in peers) { - send(peers[peer].ws, { + for (let peerId in peers) { + send(peers[peerId].ws, { type: "removePeer", data: { - peer_id: session.name, + peer_id: session.id, peer_name: session.name, }, }); send(ws, { type: "removePeer", data: { - peer_id: peer, - peer_name: peer, + peer_id: peerId, + peer_name: peers[peerId].name || peerId, }, }); } }; -export const handleRelayICECandidate = ( - gameId: string, - cfg: any, - session: any, - safeSend?: (targetOrSession: any, message: any) => boolean, - debug?: any -) => { - const send = safeSend ? safeSend : defaultSend; +export const handleRelayICECandidate = (gameId: string, cfg: any, session: Session, debug?: any) => { + const send = defaultSend; const ws = session && session.ws; if (!cfg) { @@ -200,12 +183,13 @@ export const handleRelayICECandidate = ( return; } 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({ type: "iceCandidate", data: { - peer_id: session.name, + peer_id: session.id, peer_name: session.name, candidate, }, @@ -221,14 +205,8 @@ export const handleRelayICECandidate = ( } }; -export const handleRelaySessionDescription = ( - gameId: string, - cfg: any, - session: any, - safeSend?: (targetOrSession: any, message: any) => boolean, - debug?: any -) => { - const send = safeSend ? safeSend : defaultSend; +export const handleRelaySessionDescription = (gameId: string, cfg: any, session: any, debug?: any) => { + const send = defaultSend; const ws = session && session.ws; if (!cfg) { @@ -253,7 +231,7 @@ export const handleRelaySessionDescription = ( const message = JSON.stringify({ type: "sessionDescription", data: { - peer_id: session.name, + peer_id: session.id, peer_name: session.name, session_description, }, @@ -268,19 +246,22 @@ export const handleRelaySessionDescription = ( } }; -export const broadcastPeerStateUpdate = (gameId: string, cfg: any, session: any, safeSend?: (targetOrSession: any, message: any) => boolean) => { - const send = safeSend - ? safeSend - : (targetOrSession: any, message: any) => { - try { - const target = targetOrSession && typeof targetOrSession.send === "function" ? targetOrSession : targetOrSession && targetOrSession.ws ? targetOrSession.ws : null; - if (!target) return false; - target.send(typeof message === "string" ? message : JSON.stringify(message)); - return true; - } catch (e) { - return false; - } - }; +export const broadcastPeerStateUpdate = (gameId: string, cfg: any, session: any) => { + const send = (targetOrSession: any, message: any) => { + try { + const target = + targetOrSession && typeof targetOrSession.send === "function" + ? targetOrSession + : targetOrSession && targetOrSession.ws + ? targetOrSession.ws + : null; + if (!target) return false; + target.send(typeof message === "string" ? message : JSON.stringify(message)); + return true; + } catch (e) { + return false; + } + }; if (!(gameId in 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({ type: "peer_state_update", data: { - peer_id: session.name, + peer_id: session.id, peer_name: session.name, muted, video_on, }, }); - for (const other in audio[gameId]) { - if (other === session.name) continue; + for (const otherId in audio[gameId]) { + if (otherId === session.id) continue; try { - const tgt = audio[gameId][other] as any; + const tgt = audio[gameId][otherId] as any; 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)) { - 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) { - console.warn(`Failed sending peer_state_update to ${other}:`, e); + console.warn(`Failed sending peer_state_update to ${otherId}:`, e); } } }; diff --git a/server/util/layout.ts b/server/util/layout.ts index 8068ef2..f070e5d 100644 --- a/server/util/layout.ts +++ b/server/util/layout.ts @@ -317,13 +317,13 @@ const staticData = { { roll: 7, pips: 0 } /* Robber is at the end or indexing gets off */, ], borders: [ - ["bank", undefined, "sheep"], - [undefined, "bank", undefined], - ["bank", undefined, "brick"], - [undefined, "wood", undefined], - ["bank", undefined, "wheat"], - [undefined, "stone", undefined], - ], + ["bank", "none", "sheep"], + ["none", "bank", "none"], + ["bank", "none", "brick"], + ["none", "wood", "none"], + ["bank", "none", "wheat"], + ["none", "stone", "none"], + ] as ResourceType[][], }; export { diff --git a/server/util/validLocations.ts b/server/util/validLocations.ts index 5946b42..8469b5a 100644 --- a/server/util/validLocations.ts +++ b/server/util/validLocations.ts @@ -85,9 +85,13 @@ const getValidCorners = (game: Game, color: PlayerColor = "unassigned", type?: C * Volcano is enabled, verify the tile is not the Volcano. */ 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 (placement.color === color && placement.type === type) { + if (placement && placement.color === color && placement.type === type) { limits.push(cornerIndex); } return; @@ -97,7 +101,7 @@ const getValidCorners = (game: Game, color: PlayerColor = "unassigned", type?: C // "unassigned" then it's occupied and should be skipped. // Note: placement.color may be undefined (initial state), treat that // the same as unassigned. - if (placement.color && placement.color !== "unassigned") { + if (placement && placement.color && placement.color !== "unassigned") { return; } @@ -119,29 +123,30 @@ const getValidCorners = (game: Game, color: PlayerColor = "unassigned", type?: C } for (let r = 0; valid && r < (corner.roads || []).length; r++) { - if (!corner.roads) { - break; - } - const ridx = corner.roads[r]; - 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) { + if (!corner.roads) { + break; + } + const ridx = corner.roads[r]; + if (ridx == null || layout.roads[ridx] == null) { 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 ccColor = game.placements.corners[cc]!.color; - if (ccColor && ccColor !== "unassigned") { - valid = false; + 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; + } + /* 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) { /* During initial placement, if volcano is enabled, do not allow * placement on a corner connected to the volcano (robber starts