THings are improving
@ -106,7 +106,7 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
|
||||
const [signature, setSignature] = useState<string>("");
|
||||
const [generated, setGenerated] = useState<string>("");
|
||||
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 [pipOrder, setPipOrder] = useState<any>(undefined);
|
||||
const [borders, setBorders] = useState<any>(undefined);
|
||||
@ -147,86 +147,123 @@ const Board: React.FC<BoardProps> = ({ 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<BoardProps> = ({ animations }) => {
|
||||
console.log(`board - Generate pip, border, and tile elements`);
|
||||
const Pip: React.FC<PipProps> = ({ 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<BoardProps> = ({ 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -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<Theme>;
|
||||
}
|
||||
|
||||
const MediaControl: React.FC<MediaControlProps> = ({
|
||||
@ -1313,6 +1315,7 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
||||
sendJsonMessage,
|
||||
remoteAudioMuted,
|
||||
remoteVideoOff,
|
||||
sx,
|
||||
}) => {
|
||||
const [muted, setMuted] = useState<boolean>(peer?.muted || false);
|
||||
const [videoOn, setVideoOn] = useState<boolean>(peer?.video_on !== false);
|
||||
@ -1591,6 +1594,7 @@ const MediaControl: React.FC<MediaControlProps> = ({
|
||||
alignItems: "center",
|
||||
minWidth: "200px",
|
||||
minHeight: "100px",
|
||||
...sx,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
|
@ -64,6 +64,20 @@ const PlayerList: React.FC = () => {
|
||||
[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) ? (
|
||||
<>
|
||||
<MediaControl
|
||||
sx={{ border: "3px solid blue" }}
|
||||
className="Medium"
|
||||
key={player.session_id}
|
||||
peer={peers[player.session_id]}
|
||||
|
@ -48,14 +48,12 @@ const audioEffects: Record<string, AudioEffect | undefined> = {};
|
||||
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;
|
||||
};
|
||||
|
||||
|
Before Width: | Height: | Size: 107 KiB |
Before Width: | Height: | Size: 323 KiB |
Before Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 305 KiB |
Before Width: | Height: | Size: 933 KiB |
Before Width: | Height: | Size: 339 KiB |
Before Width: | Height: | Size: 998 KiB |
Before Width: | Height: | Size: 316 KiB |
Before Width: | Height: | Size: 952 KiB |
Before Width: | Height: | Size: 306 KiB |
Before Width: | Height: | Size: 945 KiB |
Before Width: | Height: | Size: 338 KiB |
Before Width: | Height: | Size: 921 KiB |
Before Width: | Height: | Size: 412 KiB |
Before Width: | Height: | Size: 432 KiB |
Before Width: | Height: | Size: 442 KiB |
Before Width: | Height: | Size: 444 KiB |
Before Width: | Height: | Size: 433 KiB |
Before Width: | Height: | Size: 442 KiB |
Before Width: | Height: | Size: 434 KiB |
Before Width: | Height: | Size: 434 KiB |
Before Width: | Height: | Size: 428 KiB |
Before Width: | Height: | Size: 428 KiB |
Before Width: | Height: | Size: 436 KiB |
Before Width: | Height: | Size: 458 KiB |
Before Width: | Height: | Size: 458 KiB |
Before Width: | Height: | Size: 448 KiB |
Before Width: | Height: | Size: 438 KiB |
Before Width: | Height: | Size: 477 KiB |
Before Width: | Height: | Size: 484 KiB |
Before Width: | Height: | Size: 488 KiB |
Before Width: | Height: | Size: 446 KiB |
Before Width: | Height: | Size: 423 KiB |
Before Width: | Height: | Size: 468 KiB |
Before Width: | Height: | Size: 433 KiB |
Before Width: | Height: | Size: 462 KiB |
Before Width: | Height: | Size: 466 KiB |
Before Width: | Height: | Size: 452 KiB |
Before Width: | Height: | Size: 435 KiB |
Before Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 30 KiB |
Before Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 123 KiB |
Before Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 920 KiB |
Before Width: | Height: | Size: 983 KiB |
Before Width: | Height: | Size: 1.4 MiB |
Before Width: | Height: | Size: 1.2 MiB |
Before Width: | Height: | Size: 1.1 MiB |
Before Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 1014 KiB |
Before Width: | Height: | Size: 42 KiB |
Before Width: | Height: | Size: 1.3 MiB |
Before Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 453 KiB |
Before Width: | Height: | Size: 1.9 MiB |
Before Width: | Height: | Size: 1.8 MiB |
Before Width: | Height: | Size: 2.5 MiB |
Before Width: | Height: | Size: 2.3 MiB |
Before Width: | Height: | Size: 6.3 MiB |
Before Width: | Height: | Size: 2.2 MiB |
Before Width: | Height: | Size: 3.4 MiB |
Before Width: | Height: | Size: 1.0 MiB |
Before Width: | Height: | Size: 1.7 MiB |
Before Width: | Height: | Size: 14 MiB |
@ -300,7 +300,7 @@ const bestRoadPlacement = (game) => {
|
||||
return;
|
||||
}
|
||||
const placedRoad = game.placements.roads[roadIndex];
|
||||
if (placedRoad.color) {
|
||||
if (!placedRoad || placedRoad.color) {
|
||||
return;
|
||||
}
|
||||
attempt = roadIndex;
|
||||
|
@ -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;
|
||||
};
|
||||
|
@ -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<PlayerColor | "robber", ResourceCount>;
|
||||
@ -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,
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -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: {
|
||||
|
@ -535,8 +535,16 @@ export const getFilteredPlayers = (game: Game): Record<string, Player> => {
|
||||
}
|
||||
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 = [];
|
||||
}
|
||||
|
@ -32,6 +32,7 @@ export const newPlayer = (color: PlayerColor): Player => {
|
||||
live: true,
|
||||
turnNotice: "",
|
||||
longestRoad: 0,
|
||||
banks: [],
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -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 {
|
||||
|
@ -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<CornerPlacement | undefined>;
|
||||
roads: Array<RoadPlacement | undefined>;
|
||||
}
|
||||
|
||||
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;
|
||||
|
@ -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[] {
|
||||
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;
|
||||
}
|
||||
|
@ -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<string, any> = {};
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -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 {
|
||||
|
@ -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
|
||||
|