1
0

TypeScript conversion mostly completed

This commit is contained in:
James Ketr 2025-10-07 14:47:27 -07:00
parent 45e01d5e89
commit f1580970f9
12 changed files with 508 additions and 473 deletions

View File

@ -7,13 +7,6 @@ import "./Actions.css";
import { PlayerName } from "./PlayerName";
import { GlobalContext } from "./GlobalContext";
type LocalGlobalContext = {
ws?: WebSocket | null;
gameId?: string | null;
name?: string | undefined;
sendJsonMessage?: (message: any) => void;
};
type PrivateData = {
orderRoll?: boolean;
resources?: number;
@ -50,25 +43,28 @@ const Actions: React.FC<ActionsProps> = ({
houseRulesActive,
setHouseRulesActive,
}) => {
console.log("Actions component rendered");
const ctx = useContext(GlobalContext) as LocalGlobalContext;
const ws = ctx.ws ?? null;
const gameId = ctx.gameId ?? null;
const name = ctx.name ?? undefined;
const { lastJsonMessage, sendJsonMessage, name, roomName } = useContext(GlobalContext);
const [state, setState] = useState<string>("lobby");
const [color, setColor] = useState<string | undefined>(undefined);
const [priv, setPriv] = useState<PrivateData | undefined>(undefined);
const [turn, setTurn] = useState<TurnData>({});
const [edit, setEdit] = useState<string | undefined>(name);
console.log("Actions: name =", name, "edit =", edit);
const [active, setActive] = useState<number>(0);
const [players, setPlayers] = useState<Record<string, PlayerData>>({});
const [alive, setAlive] = useState<number>(0);
const fields = useMemo(() => ["state", "turn", "private", "active", "color", "players"], []);
const onWsMessage = (event: MessageEvent) => {
const data = JSON.parse(event.data);
useEffect(() => {
sendJsonMessage({ type: "get", fields });
}, [sendJsonMessage, fields]);
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`actions - game update`, data.update);
@ -99,39 +95,7 @@ const Actions: React.FC<ActionsProps> = ({
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ctx.ws) {
return;
}
const cbMessage = (e: MessageEvent) => refWsMessage.current(e);
ctx.ws.addEventListener("message", cbMessage as EventListener);
return () => {
ctx.ws.removeEventListener("message", cbMessage as EventListener);
};
}, [ctx.ws, refWsMessage]);
useEffect(() => {
if (!ctx.sendJsonMessage) {
return;
}
ctx.sendJsonMessage({ type: "get", fields });
}, [ctx.sendJsonMessage, fields]);
const sendMessage = useCallback(
(data: Record<string, unknown>) => {
if (!ctx.sendJsonMessage) {
console.warn(`No sendJsonMessage`);
} else {
ctx.sendJsonMessage(data);
}
},
[ctx.sendJsonMessage]
);
}, [lastJsonMessage, state, color, edit, turn, active, players, priv]);
const buildClicked = () => {
setBuildActive(!buildActive);
@ -151,7 +115,7 @@ const Actions: React.FC<ActionsProps> = ({
const setName = (update: string) => {
if (update !== name) {
sendMessage({ type: "player-name", name: update });
sendJsonMessage({ type: "player-name", name: update });
}
setEdit(name);
if (buildActive) setBuildActive(false);
@ -171,47 +135,47 @@ const Actions: React.FC<ActionsProps> = ({
discards[t] = (discards[t] || 0) + 1;
nodes[i].classList.remove("Selected");
}
sendMessage({ type: "discard", discards });
sendJsonMessage({ type: "discard", discards });
if (buildActive) setBuildActive(false);
};
const newTableClick = () => {
sendMessage({ type: "shuffle" });
sendJsonMessage({ type: "shuffle" });
if (buildActive) setBuildActive(false);
};
const tradeClick = () => {
if (!tradeActive) {
setTradeActive(true);
sendMessage({ type: "trade" });
sendJsonMessage({ type: "trade" });
} else {
setTradeActive(false);
sendMessage({ type: "trade", action: "cancel", offer: undefined });
sendJsonMessage({ type: "trade", action: "cancel", offer: undefined });
}
if (buildActive) setBuildActive(false);
};
const rollClick = () => {
sendMessage({ type: "roll" });
sendJsonMessage({ type: "roll" });
if (buildActive) setBuildActive(false);
};
const passClick = () => {
sendMessage({ type: "pass" });
sendJsonMessage({ type: "pass" });
if (buildActive) setBuildActive(false);
};
const houseRulesClick = () => {
setHouseRulesActive(!houseRulesActive);
};
const startClick = () => {
sendMessage({ type: "set", field: "state", value: "game-order" });
sendJsonMessage({ type: "set", field: "state", value: "game-order" });
if (buildActive) setBuildActive(false);
};
const resetGame = () => {
sendMessage({ type: "clear-game" });
sendJsonMessage({ type: "clear-game" });
if (buildActive) setBuildActive(false);
};
if (!gameId) {
if (!roomName) {
return <Paper className="Actions" />;
}
@ -265,22 +229,8 @@ const Actions: React.FC<ActionsProps> = ({
disableRoll = true;
}
console.log("actions - ", {
disableRoll,
robberActions,
turn,
inGame,
isTurn,
hasRolled,
volcanoActive,
inGameOrder,
hasGameOrderRolled,
});
const disableDone = volcanoActive || placeRoad || robberActions || !isTurn || !hasRolled;
console.log("Actions render: edit =", edit, "name =", name);
return (
<Paper className="Actions">
{edit === "" && <PlayerName name={name} setName={setName} />}

View File

@ -1,229 +1,7 @@
body {
font-family: 'Droid Sans', 'Arial Narrow', Arial, sans-serif;
overflow: hidden;
width: 100dvw;
height: 100dvh;
}
#root {
width: 100vw;
/* height: 100vh; breaks on mobile -- not needed */
}
.Table {
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0;
flex-direction: row;
background-image: url("./assets/tabletop.png");
}
.Table .Dialogs {
z-index: 10000;
display: flex;
justify-content: space-around;
align-items: center;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.Table .Dialogs .Dialog {
display: flex;
position: absolute;
flex-shrink: 1;
flex-direction: column;
padding: 0.25rem;
left: 0;
right: 0;
top: 0;
bottom: 0;
justify-content: space-around;
align-items: center;
z-index: 60000;
}
.Table .Dialogs .Dialog > div {
display: flex;
padding: 1rem;
flex-direction: column;
}
.Table .Dialogs .Dialog > div > div:first-child {
padding: 1rem;
}
.Table .Dialogs .TurnNoticeDialog {
background-color: #7a680060;
}
.Table .Dialogs .ErrorDialog {
background-color: #40000060;
}
.Table .Dialogs .WarningDialog {
background-color: #00000060;
}
.Table .Game {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.Table .Board {
display: flex;
position: relative;
flex-grow: 1;
z-index: 500;
}
.Table .PlayersStatus {
z-index: 500; /* Under Hand */
}
.Table .PlayersStatus.ActivePlayer {
z-index: 1500; /* On top of Hand */
}
.Table .Hand {
display: flex;
position: relative;
height: 11rem;
z-index: 10000;
}
.Table .Sidebar {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 25rem;
max-width: 25rem;
overflow: hidden;
z-index: 5000;
}
.Table .Sidebar .Chat {
display: flex;
position: relative;
flex-grow: 1;
}
.Table .Trade {
display: flex;
position: relative;
z-index: 25000;
align-self: center;
}
.Table .Dialogs {
position: absolute;
display: flex;
top: 0;
bottom: 0;
right: 0;
left: 0;
justify-content: space-around;
align-items: center;
z-index: 20000;
pointer-events: none;
}
.Table .Dialogs > * {
pointer-events: all;
}
.Table .ViewCard {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.Table .Winner {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.Table .HouseRules {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.Table .ChooseCard {
display: flex;
position: relative;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.Table button {
margin: 0.25rem;
background-color: white;
border: 1px solid black; /* why !important */
}
.Table .MuiButton-text {
padding: 0.25rem 0.55rem;
}
.Table button:disabled {
opacity: 0.5;
border: 1px solid #ccc; /* why !important */
}
.Table .ActivitiesBox {
display: flex;
flex-direction: column;
position: absolute;
left: 1em;
top: 1em;
}
.Table .DiceRoll {
display: flex;
flex-direction: column;
position: relative;
/*
left: 1rem;
top: 5rem;*/
flex-wrap: wrap;
justify-content: left;
align-items: left;
z-index: 1000;
}
.Table .DiceRoll div:not(:last-child) {
border: 1px solid black;
background-color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.Table .DiceRoll div:last-child {
display: flex;
flex-direction: row;
}
.Table .DiceRoll .Dice {
margin: 0.25rem;
width: 2.75rem;
height: 2.75rem;
border-radius: 0.5rem;
}

View File

@ -95,7 +95,7 @@ const clearTooltip = () => {
};
const Board: React.FC<BoardProps> = ({ animations }) => {
const { ws, sendJsonMessage } = useContext(GlobalContext);
const { sendJsonMessage, lastJsonMessage } = useContext(GlobalContext);
const board = useRef();
const [transform, setTransform] = useState(1);
const [pipElements, setPipElements] = useState<React.ReactElement[]>([]);
@ -142,11 +142,11 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
[]
);
const onWsMessage = (event) => {
if (ws && ws !== event.target) {
console.error(`Disconnect occur?`);
useEffect(() => {
if (!lastJsonMessage) {
return;
}
const data = JSON.parse(event.data);
const data = lastJsonMessage;
switch (data.type) {
case "game-update":
console.log(`board - game update`, data.update);
@ -232,23 +232,7 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
default:
break;
}
};
const refWsMessage = useRef(onWsMessage);
useEffect(() => {
refWsMessage.current = onWsMessage;
});
useEffect(() => {
if (!ws) {
return;
}
console.log("board - bind");
const cbMessage = (e) => refWsMessage.current(e);
ws.addEventListener("message", cbMessage);
return () => {
console.log("board - unbind");
ws.removeEventListener("message", cbMessage);
};
}, [ws]);
}, [lastJsonMessage, robber, robberName]);
useEffect(() => {
if (!sendJsonMessage) {
return;
@ -305,10 +289,6 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
onResize();
useEffect(() => {
if (!ws) {
return;
}
console.log(`Generating static corner data... should only occur once per reload or socket reconnect.`);
const onCornerClicked = (event, corner) => {
let type;
@ -317,12 +297,10 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
} else {
type = "place-settlement";
}
ws.send(
JSON.stringify({
type,
index: corner.index,
})
);
sendJsonMessage({
type,
index: corner.index,
});
};
const Corner: React.FC<CornerProps> = ({ corner }) => {
return (
@ -411,27 +389,17 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
};
setCornerElements(generateCorners());
}, [ws, setCornerElements]);
}, [setCornerElements]);
useEffect(() => {
if (!ws) {
return;
}
console.log(`Generating static road data... should only occur once per reload or socket reconnect.`);
const Road: React.FC<RoadProps> = ({ road }) => {
const onRoadClicked = (road) => {
console.log(`Road clicked: ${road.index}`);
if (!ws) {
console.error(`board - onRoadClicked - ws is NULL`);
return;
}
ws.send(
JSON.stringify({
type: "place-road",
index: road.index,
})
);
sendJsonMessage({
type: "place-road",
index: road.index,
});
};
return (
@ -533,13 +501,10 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
return corners;
};
setRoadElements(generateRoads());
}, [ws, setRoadElements]);
}, [setRoadElements]);
/* Generate Pip, Tile, and Border elements */
useEffect(() => {
if (!ws) {
return;
}
console.log(`board - Generate pip, border, and tile elements`);
const Pip: React.FC<PipProps> = ({ pip, className }) => {
const onPipClicked = (pip) => {
@ -547,12 +512,10 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
console.error(`board - sendPlacement - ws is NULL`);
return;
}
ws.send(
JSON.stringify({
type: "place-robber",
index: pip.index,
})
);
sendJsonMessage({
type: "place-robber",
index: pip.index,
});
};
return (
@ -830,7 +793,6 @@ const Board: React.FC<BoardProps> = ({ animations }) => {
tiles,
tileOrder,
animationSeeds,
ws,
state,
rules,
animations,

View File

@ -68,6 +68,8 @@ const base = baseCandidate;
const assetsPath = base;
const gamesPath = `${base}`;
const ws_base: string = `${window.location.protocol === "https:" ? "wss" : "ws"}://${window.location.host}${base}/ws`;
const ws_base: string = `${window.location.protocol === "https:" ? "wss" : "ws"}://${
window.location.host
}${base}/api/v1/games/ws`;
export { base, ws_base, assetsPath, gamesPath };

View File

@ -5,15 +5,21 @@ export type GlobalContextType = {
name?: string;
sendJsonMessage?: (message: any) => void;
chat?: Array<unknown>;
socketUrl?: string;
session?: Session;
lastJsonMessage?: any;
};
const global: GlobalContextType = {
roomName: undefined,
name: "",
socketUrl: undefined,
chat: [],
session: undefined,
lastJsonMessage: undefined,
};
const GlobalContext = createContext<GlobalContextType>(global);
const GlobalContext = createContext(global);
/**
* RoomModel
@ -132,4 +138,4 @@ export type Session = Omit<SessionResponse, "name"> & {
has_media?: boolean; // Whether this session provides audio/video streams
};
export { GlobalContext, global };
export { GlobalContext };

View File

@ -1081,8 +1081,6 @@ const MediaAgent = (props: MediaAgentProps) => {
const handleWebSocketMessage = useCallback(
(data: any) => {
console.log(`media-agent - WebSocket message received:`, data.type, data.data);
switch (data.type) {
case "join_status":
setJoinStatus({ status: data.status, message: data.message });

View File

@ -1,22 +1,25 @@
import React, { useState, KeyboardEvent, useRef } from "react";
import { Input, Button, Box, Typography, Tooltip, Dialog, DialogTitle, DialogContent, DialogActions } from "@mui/material";
import { Session } from "./GlobalContext";
import React, { useState, KeyboardEvent, useRef, useContext } from "react";
import {
Input,
Button,
Box,
Typography,
Tooltip,
Dialog,
DialogTitle,
DialogContent,
DialogActions,
} from "@mui/material";
import { GlobalContext, Session } from "./GlobalContext";
interface NameSetterProps {
session: Session;
sendJsonMessage: (message: any) => void;
onNameSet?: () => void;
initialName?: string;
initialPassword?: string;
}
const NameSetter: React.FC<NameSetterProps> = ({
session,
sendJsonMessage,
onNameSet,
initialName = "",
initialPassword = "",
}) => {
const NameSetter: React.FC<NameSetterProps> = ({ onNameSet, initialName = "", initialPassword = "" }) => {
const { session, sendJsonMessage } = useContext(GlobalContext);
const [editName, setEditName] = useState<string>(initialName);
const [editPassword, setEditPassword] = useState<string>(initialPassword);
const [showDialog, setShowDialog] = useState<boolean>(!session.name);
@ -28,7 +31,7 @@ const NameSetter: React.FC<NameSetterProps> = ({
const setName = (name: string) => {
setIsSubmitting(true);
sendJsonMessage({
type: "set_name",
type: "player-name",
data: { name, password: editPassword ? editPassword : undefined },
});
if (onNameSet) {
@ -98,19 +101,15 @@ const NameSetter: React.FC<NameSetterProps> = ({
{/* Dialog for name change */}
<Dialog open={showDialog} onClose={handleCloseDialog} maxWidth="sm" fullWidth>
<DialogTitle>
{session.name ? "Change Your Name" : "Enter Your Name"}
</DialogTitle>
<DialogTitle>{session.name ? "Change Your Name" : "Enter Your Name"}</DialogTitle>
<DialogContent>
<Box sx={{ display: "flex", flexDirection: "column", gap: 2, pt: 1 }}>
<Typography variant="body2" color="text.secondary">
{session.name
? "Enter a new name to change your current name."
: "Enter your name to join the lobby."
}
{session.name ? "Enter a new name to change your current name." : "Enter your name to join the lobby."}
</Typography>
<Typography variant="caption" color="text.secondary">
You can optionally set a password to reserve this name; supply it again to takeover the name from another client.
You can optionally set a password to reserve this name; supply it again to takeover the name from another
client.
</Typography>
<Input
@ -151,7 +150,7 @@ const NameSetter: React.FC<NameSetterProps> = ({
disabled={!canSubmit}
color={hasNameChanged ? "primary" : "inherit"}
>
{isSubmitting ? "Changing..." : (session.name ? "Change Name" : "Join")}
{isSubmitting ? "Changing..." : session.name ? "Change Name" : "Join"}
</Button>
</DialogActions>
</Dialog>
@ -159,4 +158,4 @@ const NameSetter: React.FC<NameSetterProps> = ({
);
};
export default NameSetter;
export default NameSetter;

View File

@ -1,10 +1,10 @@
import React, { useState, useEffect, useCallback } from "react";
import React, { useState, useEffect, useCallback, useContext } from "react";
import Paper from "@mui/material/Paper";
import List from "@mui/material/List";
import "./PlayerList.css";
import { MediaControl, MediaAgent, Peer } from "./MediaControl";
import Box from "@mui/material/Box";
import { Session, Room } from "./GlobalContext";
import { GlobalContext } from "./GlobalContext";
import useWebSocket from "react-use-websocket";
type Player = {
@ -21,14 +21,8 @@ type Player = {
video_on?: boolean;
};
type PlayerListProps = {
socketUrl: string;
session: Session;
roomId: string;
};
const PlayerList: React.FC<PlayerListProps> = (props: PlayerListProps) => {
const { socketUrl, session, roomId } = props;
const PlayerList: React.FC = () => {
const { session, socketUrl } = useContext(GlobalContext);
const [Players, setPlayers] = useState<Player[] | null>(null);
const [peers, setPeers] = useState<Record<string, Peer>>({});

229
client/src/RoomView.css Normal file
View File

@ -0,0 +1,229 @@
body {
font-family: 'Droid Sans', 'Arial Narrow', Arial, sans-serif;
overflow: hidden;
}
#root {
width: 100vw;
/* height: 100vh; breaks on mobile -- not needed */
}
.RoomView {
display: flex;
position: absolute;
top: 0;
left: 0;
width: 100%;
bottom: 0;
flex-direction: row;
background-image: url("./assets/tabletop.png");
}
.RoomView .Dialogs {
z-index: 10000;
display: flex;
justify-content: space-around;
align-items: center;
position: absolute;
top: 0;
left: 0;
bottom: 0;
right: 0;
}
.RoomView .Dialogs .Dialog {
display: flex;
position: absolute;
flex-shrink: 1;
flex-direction: column;
padding: 0.25rem;
left: 0;
right: 0;
top: 0;
bottom: 0;
justify-content: space-around;
align-items: center;
z-index: 60000;
}
.RoomView .Dialogs .Dialog > div {
display: flex;
padding: 1rem;
flex-direction: column;
}
.RoomView .Dialogs .Dialog > div > div:first-child {
padding: 1rem;
}
.RoomView .Dialogs .TurnNoticeDialog {
background-color: #7a680060;
}
.RoomView .Dialogs .ErrorDialog {
background-color: #40000060;
}
.RoomView .Dialogs .WarningDialog {
background-color: #00000060;
}
.RoomView .Game {
position: relative;
display: flex;
flex-direction: column;
flex-grow: 1;
}
.RoomView .Board {
display: flex;
position: relative;
flex-grow: 1;
z-index: 500;
}
.RoomView .PlayersStatus {
z-index: 500; /* Under Hand */
}
.RoomView .PlayersStatus.ActivePlayer {
z-index: 1500; /* On top of Hand */
}
.RoomView .Hand {
display: flex;
position: relative;
height: 11rem;
z-index: 10000;
}
.RoomView .Sidebar {
display: flex;
flex-direction: column;
justify-content: space-between;
width: 25rem;
max-width: 25rem;
overflow: hidden;
z-index: 5000;
}
.RoomView .Sidebar .Chat {
display: flex;
position: relative;
flex-grow: 1;
}
.RoomView .Trade {
display: flex;
position: relative;
z-index: 25000;
align-self: center;
}
.RoomView .Dialogs {
position: absolute;
display: flex;
top: 0;
bottom: 0;
right: 0;
left: 0;
justify-content: space-around;
align-items: center;
z-index: 20000;
pointer-events: none;
}
.RoomView .Dialogs > * {
pointer-events: all;
}
.RoomView .ViewCard {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.RoomView .Winner {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.RoomView .HouseRules {
display: flex;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.RoomView .ChooseCard {
display: flex;
position: relative;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
.RoomView button {
margin: 0.25rem;
background-color: white;
border: 1px solid black; /* why !important */
}
.RoomView .MuiButton-text {
padding: 0.25rem 0.55rem;
}
.RoomView button:disabled {
opacity: 0.5;
border: 1px solid #ccc; /* why !important */
}
.RoomView .ActivitiesBox {
display: flex;
flex-direction: column;
position: absolute;
left: 1em;
top: 1em;
}
.RoomView .DiceRoll {
display: flex;
flex-direction: column;
position: relative;
/*
left: 1rem;
top: 5rem;*/
flex-wrap: wrap;
justify-content: left;
align-items: left;
z-index: 1000;
}
.RoomView .DiceRoll div:not(:last-child) {
border: 1px solid black;
background-color: white;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
}
.RoomView .DiceRoll div:last-child {
display: flex;
flex-direction: row;
}
.RoomView .DiceRoll .Dice {
margin: 0.25rem;
width: 2.75rem;
height: 2.75rem;
border-radius: 0.5rem;
}

View File

@ -1,11 +1,11 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useContext } from "react";
import { useParams } from "react-router-dom";
import useWebSocket, { ReadyState } from "react-use-websocket";
import Paper from "@mui/material/Paper";
import Button from "@mui/material/Button";
import { GlobalContext } from "./GlobalContext";
import { GlobalContext, GlobalContextType } from "./GlobalContext";
import { PlayerList } from "./PlayerList";
import { Chat } from "./Chat";
import { Board } from "./Board";
@ -25,7 +25,7 @@ import { Dice } from "./Dice";
import { assetsPath } from "./Common";
import { Session, Room } from "./GlobalContext";
// history replaced by react-router's useNavigate
import "./App.css";
import "./RoomView.css";
import equal from "fast-deep-equal";
import itsYourTurnAudio from "./assets/its-your-turn.mp3";
@ -33,6 +33,7 @@ import robberAudio from "./assets/robber.mp3";
import knightsAudio from "./assets/the-knights-who-say-ni.mp3";
import volcanoAudio from "./assets/volcano-eruption.mp3";
import { ConnectionStatus } from "./ConnectionStatus";
import NameSetter from "./NameSetter";
const audioFiles: Record<string, string> = {
"its-your-turn.mp3": itsYourTurnAudio,
@ -64,11 +65,10 @@ type RoomProps = {
setError: React.Dispatch<React.SetStateAction<string | null>>;
};
const RoomView: React.FC<RoomProps> = (props: RoomProps) => {
const RoomView = (props: RoomProps) => {
const { session, setSession, setError } = props;
const [socketUrl, setSocketUrl] = useState<string | null>(null);
const { roomName = "default" } = useParams<{ roomName: string }>();
const [creatingRoom, setCreatingRoom] = useState<boolean>(false);
const [reconnectAttempt, setReconnectAttempt] = useState<number>(0);
const [name, setName] = useState<string>("");
@ -88,7 +88,6 @@ const RoomView: React.FC<RoomProps> = (props: RoomProps) => {
const [cardActive, setCardActive] = useState<unknown>(undefined);
const [houseRulesActive, setHouseRulesActive] = useState<boolean>(false);
const [winnerDismissed, setWinnerDismissed] = useState<boolean>(false);
const [global, setGlobal] = useState<Record<string, unknown>>({});
const [count, setCount] = useState<number>(0);
const [audio, setAudio] = useState<boolean>(
localStorage.getItem("audio") ? JSON.parse(localStorage.getItem("audio") as string) : false
@ -206,18 +205,6 @@ const RoomView: React.FC<RoomProps> = (props: RoomProps) => {
}
}, [lastJsonMessage, session, setError, setSession]);
useEffect(() => {
console.log("app - WebSocket connection status: ", readyState);
}, [readyState]);
if (global.name !== name || global.roomName !== roomName) {
setGlobal({
name,
roomName,
sendJsonMessage,
});
}
useEffect(() => {
if (state === "volcano") {
if (!audioEffects.volcano) {
@ -292,11 +279,17 @@ const RoomView: React.FC<RoomProps> = (props: RoomProps) => {
}
}, [volume]);
if (readyState !== ReadyState.OPEN || !session) {
return <ConnectionStatus readyState={readyState} reconnectAttempt={reconnectAttempt} />;
}
return (
<GlobalContext.Provider value={global}>
<div className="Room">
{readyState !== ReadyState.OPEN || !session ? (
<ConnectionStatus readyState={readyState} reconnectAttempt={reconnectAttempt} />
<GlobalContext.Provider value={{ socketUrl, session, name, roomName, sendJsonMessage, lastJsonMessage }}>
<div className="RoomView">
{!name ? (
<Paper>
<NameSetter />
</Paper>
) : (
<>
<div className="ActivitiesBox">
@ -400,7 +393,7 @@ const RoomView: React.FC<RoomProps> = (props: RoomProps) => {
/>
</Paper>
)}
<PlayerList socketUrl={socketUrl} session={session} roomId={roomName} />
{name && <PlayerList />}
{/* Trade is an untyped JS component; assert its type to avoid `any` */}
{(() => {
const TradeComponent = Trade as unknown as React.ComponentType<{

View File

@ -17,16 +17,16 @@
}*/
html {
height: 100%;
width: 100%;
height: 100dvh;
width: 100dvw;
margin: 0;
padding: 0;
}
body {
position: relative;
height: 100%;
width: 100%;
display: flex;
height: 100dvh;
width: 100dvw;
margin: 0;
padding: 0;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
@ -35,8 +35,3 @@ body {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}

View File

@ -153,7 +153,7 @@ const processGameOrder = (game: any, player: any, dice: number): any => {
name: players[0].name,
color: players[0].color,
};
setForSettlementPlacement(game, getValidCorners(game), undefined);
setForSettlementPlacement(game, getValidCorners(game, ""));
addActivity(game, null, `${game.robberName} Robber Robinson entered the scene as the nefarious robber!`);
addChatMessage(game, null, `Initial settlement placement has started!`);
addChatMessage(game, null, `It is ${game.turn.name}'s turn to place a settlement.`);
@ -169,8 +169,8 @@ const processGameOrder = (game: any, player: any, dice: number): any => {
};
const processVolcano = (game: Game, session: Session, dice: number[]): any => {
const player = session.player,
name = session.name ? session.name : "Unnamed";
const name = session.name ? session.name : "Unnamed";
void session.player;
const volcano = layout.tiles.findIndex((_tile, index) => {
const tileIndex = game.tileOrder ? game.tileOrder[index] : undefined;
@ -234,12 +234,11 @@ const roll = (game: any, session: any, dice?: number[] | undefined): any => {
dice = [Math.ceil(Math.random() * 6), Math.ceil(Math.random() * 6)];
}
switch (game.state) {
case "lobby"
/* currently not available as roll is only after color is
* set for players */:
addChatMessage(game, session, `${name} rolled ${dice[0]}.`);
case "lobby":
/* currently not available as roll is only after color is
* set for players */ addChatMessage(game, session, `${name} rolled ${dice[0]}.`);
sendUpdateToPlayers(game, { chat: game.chat });
return;
return undefined;
case "game-order":
game.startTime = Date.now();
@ -776,17 +775,19 @@ const clearPlayer = (player: any) => {
Object.assign(player, newPlayer(color));
};
const canGiveBuilding = (game: any): string | void => {
const canGiveBuilding = (game: any): string | undefined => {
if (!game.turn.roll) {
return `Admin cannot give a building until the dice have been rolled.`;
}
if (game.turn.actions && game.turn.actions.length !== 0) {
return `Admin cannot give a building while other actions in play: ${game.turn.actions.join(", ")}.`;
}
return undefined;
};
const adminCommands = (game: any, action: string, value: string, query: any): any => {
let color: string | undefined, parts: RegExpMatchArray | null, session: any, corners: any, corner: any, error: any;
void color;
switch (action) {
case "rules":
@ -897,7 +898,7 @@ const adminCommands = (game: any, action: string, value: string, query: any): an
return `There are no valid locations for ${game.turn.name} to place a settlement.`;
}
game.turn.free = true;
setForSettlementPlacement(game, corners, undefined);
setForSettlementPlacement(game, corners);
addChatMessage(
game,
null,
@ -1183,6 +1184,7 @@ const setPlayerName = (game: any, session: any, name: string): string | undefine
});
/* Now that a name is set, send the full game to the player */
sendGameToPlayer(game, session);
return undefined;
};
const colorToWord = (color: string): string => {
@ -1211,7 +1213,7 @@ const getActiveCount = (game: any): number => {
return active;
};
const setPlayerColor = (game: any, session: any, color: string): string | void => {
const setPlayerColor = (game: any, session: any, color: string): string | undefined => {
/* Selecting the same color is a NO-OP */
if (session.color === color) {
return;
@ -1313,6 +1315,7 @@ const setPlayerColor = (game: any, session: any, color: string): string | void =
private: session.player,
});
sendUpdateToPlayers(game, update);
return undefined;
};
const addActivity = (game: any, session: any, message: string): void => {
@ -1405,6 +1408,9 @@ const getNextPlayerSession = (game: any, name: string): any => {
console.log(game.players);
};
// Keep some helper symbols present for external use or tests; reference them
// as no-ops so TypeScript/linters do not mark them as unused.
const getPrevPlayerSession = (game: any, name: string): any => {
let color;
for (let id in game.sessions) {
@ -1424,6 +1430,12 @@ const getPrevPlayerSession = (game: any, name: string): any => {
console.log(game.players);
};
// Prevent 'declared but never used' warnings for public helpers that may be used externally
void getColorFromName;
void getLastPlayerName;
void getFirstPlayerName;
void getPrevPlayerSession;
const processCorner = (game: Game, color: string, cornerIndex: number, placedCorner: CornerPlacement): number => {
/* If this corner is allocated and isn't assigned to the walking color, skip it */
if (placedCorner.color && placedCorner.color !== color) {
@ -1530,14 +1542,14 @@ const buildRoadGraph = (game: Game, color: string, roadIndex: number, placedRoad
const clearRoadWalking = (game: Game): void => {
/* Clear out walk markers on roads */
layout.roads.forEach((item, itemIndex) => {
layout.roads.forEach((_item, itemIndex) => {
if (game.placements?.roads?.[itemIndex]) {
delete game.placements.roads[itemIndex].walking;
}
});
/* Clear out walk markers on corners */
layout.corners.forEach((item, itemIndex) => {
layout.corners.forEach((_item, itemIndex) => {
if (game.placements?.corners?.[itemIndex]) {
delete game.placements.corners[itemIndex].walking;
}
@ -1872,20 +1884,26 @@ const setGameFromSignature = (game: any, border: string, pip: string, tile: stri
pips = [],
tiles = [];
for (let i = 0; i < 6; i++) {
borders[i] = parseInt(border.slice(i * 2, i * 2 + 2), 16) ^ salt;
if (borders[i] > 6) {
const parsed = parseInt(border.slice(i * 2, i * 2 + 2), 16);
if (Number.isNaN(parsed)) return false;
borders[i] = parsed ^ salt;
if (borders[i]! > 6) {
return false;
}
}
for (let i = 0; i < 19; i++) {
pips[i] = parseInt(pip.slice(i * 2, i * 2 + 2), 16) ^ salt ^ (salt * i) % 256;
if (pips[i] > 18) {
const parsed = parseInt(pip.slice(i * 2, i * 2 + 2), 16);
if (Number.isNaN(parsed)) return false;
pips[i] = parsed ^ salt ^ (salt * i) % 256;
if (pips[i]! > 18) {
return false;
}
}
for (let i = 0; i < 19; i++) {
tiles[i] = parseInt(tile.slice(i * 2, i * 2 + 2), 16) ^ salt ^ (salt * i) % 256;
if (tiles[i] > 18) {
const parsed = parseInt(tile.slice(i * 2, i * 2 + 2), 16);
if (Number.isNaN(parsed)) return false;
tiles[i] = parsed ^ salt ^ (salt * i) % 256;
if (tiles[i]! > 18) {
return false;
}
}
@ -1913,7 +1931,7 @@ const setForCityPlacement = (game: Game, limits: any): void => {
game.turn.limits = { corners: limits };
};
const setForSettlementPlacement = (game: Game, limits?: number[] | undefined, _extra?: any): void => {
const setForSettlementPlacement = (game: Game, limits: number[] | undefined, _extra?: any): void => {
// limits: array of valid corner indices
game.turn.actions = ["place-settlement"];
game.turn.limits = { corners: limits };
@ -1948,7 +1966,7 @@ router.put("/:id/:action/:value?", async (req, res) => {
return res.status(400).send(error);
});
const startTrade = (game: any, session: any): string | void => {
const startTrade = (game: any, session: any): string | undefined => {
/* Only the active player can begin trading */
if (game.turn.name !== session.name) {
return `You cannot start trading negotiations when it is not your turn.`;
@ -1965,9 +1983,10 @@ const startTrade = (game: any, session: any): string | void => {
delete game.players[key].offerRejected;
}
addActivity(game, session, `${session.name} requested to begin trading negotiations.`);
return undefined;
};
const cancelTrade = (game: any, session: any): string | void => {
const cancelTrade = (game: any, session: any): string | undefined => {
/* TODO: Perhaps 'cancel' is how a player can remove an offer... */
if (game.turn.name !== session.name) {
return `Only the active player can cancel trading negotiations.`;
@ -1975,9 +1994,10 @@ const cancelTrade = (game: any, session: any): string | void => {
game.turn.actions = [];
game.turn.limits = {};
addActivity(game, session, `${session.name} has cancelled trading negotiations.`);
return undefined;
};
const processOffer = (game: any, session: any, offer: any): string | void => {
const processOffer = (game: any, session: any, offer: any): string | undefined => {
let warning = checkPlayerOffer(game, session.player, offer);
if (warning) {
return warning;
@ -2015,6 +2035,7 @@ const processOffer = (game: any, session: any, offer: any): string | void => {
}
addActivity(game, session, `${session.name} submitted an offer to give ${offerToString(offer)}.`);
return undefined;
};
const rejectOffer = (game: any, session: any, offer: any): void => {
@ -2031,7 +2052,7 @@ const rejectOffer = (game: any, session: any, offer: any): void => {
addActivity(game, session, `${session.name} rejected ${other.name}'s offer.`);
};
const acceptOffer = (game: any, session: any, offer: any): string | void => {
const acceptOffer = (game: any, session: any, offer: any): string | undefined => {
const name = session.name,
player = session.player;
@ -2135,6 +2156,7 @@ const acceptOffer = (game: any, session: any, offer: any): string | void => {
});
}
game.turn.actions = [];
return undefined;
};
const trade = (game: any, session: any, action: string, offer: any) => {
@ -2171,7 +2193,7 @@ const trade = (game: any, session: any, action: string, offer: any) => {
}
};
const clearTimeNotice = (game: any, session: any) => {
const clearTimeNotice = (game: any, session: any): string | undefined => {
if (!session.player.turnNotice) {
/* benign state; don't alert the user */
//return `You have not been idle.`;
@ -2180,6 +2202,7 @@ const clearTimeNotice = (game: any, session: any) => {
sendUpdateToPlayer(game, session, {
private: session.player,
});
return undefined;
};
const startTurnTimer = (game: any, session: any) => {
@ -2216,9 +2239,10 @@ const stopTurnTimer = (game: any): void => {
clearTimeout(game.turnTimer);
game.turnTimer = 0;
}
return undefined;
};
const shuffle = (game: any, session: any): string | void => {
const shuffle = (game: any, session: any): string | undefined => {
if (game.state !== "lobby") {
return `Game no longer in lobby (${game.state}). Can not shuffle board.`;
}
@ -2237,9 +2261,10 @@ const shuffle = (game: any, session: any): string | void => {
signature: game.signature,
animationSeeds: game.animationSeeds,
});
return undefined;
};
const pass = (game: any, session: any): string | void => {
const pass = (game: any, session: any): string | undefined => {
const name = session.name;
if (game.turn.name !== name) {
return `You cannot pass when it isn't your turn.`;
@ -2282,9 +2307,10 @@ const pass = (game: any, session: any): string | void => {
activities: game.activities,
dice: game.dice,
});
return undefined;
};
const placeRobber = (game: any, session: any, robber: any): string | void => {
const placeRobber = (game: any, session: any, robber: any): string | undefined => {
const name = session.name;
if (typeof robber === "string") {
robber = parseInt(robber);
@ -2357,9 +2383,10 @@ const placeRobber = (game: any, session: any, robber: any): string | void => {
sendUpdateToPlayer(game, session, {
private: session.player,
});
return undefined;
};
const stealResource = (game: any, session: any, color: any): string | void => {
const stealResource = (game: any, session: any, color: any): string | undefined => {
if (game.turn.actions.indexOf("steal-resource") === -1) {
return `You can only steal a resource when it is valid to do so!`;
}
@ -2427,6 +2454,7 @@ const stealResource = (game: any, session: any, color: any): string | void => {
activities: game.activities,
players: getFilteredPlayers(game),
});
return undefined;
};
const buyDevelopment = (game: any, session: any): string | undefined => {
@ -2484,7 +2512,11 @@ const buyDevelopment = (game: any, session: any): string | undefined => {
if (game.mostDeveloped !== session.color) {
game.mostDeveloped = session.color;
game.mostPortCount = player.developmentCards;
addChatMessage(game, session, `${session.name} now has the most development cards (${player.developmentCards})!`);
addChatMessage(
game,
session,
`${session.name} now has the most development cards (${player.developmentCards})!`
);
}
}
}
@ -2637,7 +2669,7 @@ const playCard = (game: any, session: any, card: any): string | undefined => {
return undefined;
};
const placeSettlement = (game: any, session: any, index: any): string | void => {
const placeSettlement = (game: any, session: any, index: any): string | undefined => {
const player = session.player;
if (typeof index === "string") index = parseInt(index);
@ -2786,6 +2818,76 @@ const placeSettlement = (game: any, session: any, index: any): string | void =>
return undefined;
};
const placeRoad = (game: any, session: any, index: any): string | undefined => {
const player = session.player;
if (typeof index === "string") index = parseInt(index);
if (!game || !game.turn) {
return `Invalid game state.`;
}
if (session.color !== game.turn.color) {
return `It is not your turn! It is ${game.turn.name}'s turn.`;
}
if (game.placements.roads[index] === undefined) {
return `You have requested to place a road illegally!`;
}
if (!game.turn.limits || !game.turn.limits.roads || game.turn.limits.roads.indexOf(index) === -1) {
return `You tried to cheat! You should not try to break the rules.`;
}
const road = game.placements.roads[index];
if (road.color) {
return `This location already has a road belonging to ${game.players[road.color].name}!`;
}
if (game.state === "normal") {
if (!game.turn.free) {
if (player.brick < 1 || player.wood < 1) {
return `You have insufficient resources to build a road.`;
}
}
if (player.roads < 1) {
return `You have already built all of your roads.`;
}
if (!game.turn.free) {
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];
});
}
delete game.turn.free;
}
road.color = session.color;
road.type = "road";
player.roads--;
game.turn.actions = [];
game.turn.limits = {};
calculateRoadLengths(game, session);
sendUpdateToPlayer(game, session, {
private: session.player,
});
sendUpdateToPlayers(game, {
placements: game.placements,
turn: game.turn,
chat: game.chat,
activities: game.activities,
players: getFilteredPlayers(game),
});
return undefined;
};
const getVictoryPointRule = (game: any): number => {
const minVP = 10;
if (!isRuleEnabled(game, "victory-points") || !("points" in game.rules["victory-points"])) {
@ -2793,7 +2895,7 @@ const getVictoryPointRule = (game: any): number => {
}
return game.rules["victory-points"].points;
};
const supportedRules: Record<string, (game: any, session: any, rule: any, rules: any) => string | void> = {
const supportedRules: Record<string, (game: any, session: any, rule: any, rules: any) => string | void | undefined> = {
"victory-points": (game: any, session: any, rule: any, rules: any) => {
if (!("points" in rules[rule])) {
return `No points specified for victory-points`;
@ -2803,6 +2905,7 @@ const supportedRules: Record<string, (game: any, session: any, rule: any, rules:
} else {
addChatMessage(game, null, `${getName(session)} set the minimum Victory Points to ` + `${rules[rule].points}`);
}
return undefined;
},
"roll-double-roll-again": (game: any, session: any, rule: any, rules: any) => {
addChatMessage(
@ -2810,6 +2913,7 @@ const supportedRules: Record<string, (game: any, session: any, rule: any, rules:
null,
`${getName(session)} has ${rules[rule].enabled ? "en" : "dis"}abled the Roll Double, Roll Again house rule.`
);
return undefined;
},
volcano: (game: any, session: any, rule: any, rules: any) => {
if (!rules[rule].enabled) {
@ -2839,6 +2943,7 @@ const supportedRules: Record<string, (game: any, session: any, rule: any, rules:
}
}
},
"twelve-and-two-are-synonyms": (game: any, session: any, rule: any, rules: any) => {
addChatMessage(
game,
@ -2918,7 +3023,7 @@ const setRules = (game: any, session: any, rules: any): string | undefined => {
return undefined;
};
const discard = (game: any, session: any, discards: Record<string, any>): string | void => {
const discard = (game: any, session: any, discards: Record<string, any>): string | undefined => {
const player = session.player;
if (game.turn.roll !== 7) {
@ -2983,9 +3088,10 @@ const discard = (game: any, session: any, discards: Record<string, any>): string
chat: game.chat,
turn: game.turn,
});
return undefined;
};
const buyRoad = (game: any, session: any): string | void => {
const buyRoad = (game: any, session: any): string | undefined => {
const player = session.player;
if (game.state !== "normal") {
@ -3019,10 +3125,12 @@ const buyRoad = (game: any, session: any): string | void => {
chat: game.chat,
activities: game.activities,
});
return undefined;
};
const selectResources = (game: any, session: any, cards: string[]): string | void => {
const selectResources = (game: any, session: any, cards: string[]): string | undefined => {
const player = session.player;
void player;
if (!game || !game.turn || !game.turn.actions || game.turn.actions.indexOf("select-resources") === -1) {
return `Please, let's not cheat. Ok?`;
}
@ -3168,6 +3276,7 @@ const selectResources = (game: any, session: any, cards: string[]): string | voi
activities: game.activities,
players: getFilteredPlayers(game),
});
return undefined;
};
const buySettlement = (game: any, session: any): string | undefined => {
@ -3196,7 +3305,7 @@ const buySettlement = (game: any, session: any): string | undefined => {
if (corners.length === 0) {
return `There are no valid locations for you to place a settlement.`;
}
setForSettlementPlacement(game, corners, undefined);
setForSettlementPlacement(game, corners);
addActivity(game, session, `${game.turn.name} is considering placing a settlement.`);
sendUpdateToPlayers(game, {
turn: game.turn,
@ -3206,7 +3315,7 @@ const buySettlement = (game: any, session: any): string | undefined => {
return undefined;
};
const buyCity = (game: any, session: any): string | void => {
const buyCity = (game: any, session: any): string | undefined => {
const player = session.player;
if (game.state !== "normal") {
return `You cannot purchase a development card unless the game is active (${game.state}).`;
@ -3239,9 +3348,10 @@ const buyCity = (game: any, session: any): string | void => {
chat: game.chat,
activities: game.activities,
});
return undefined;
};
const placeCity = (game: any, session: any, index: any): string | void => {
const placeCity = (game: any, session: any, index: any): string | undefined => {
const player = session.player;
if (typeof index === "string") index = parseInt(index);
if (game.state !== "normal") {
@ -3300,6 +3410,7 @@ const placeCity = (game: any, session: any, index: any): string | void => {
activities: game.activities,
players: getFilteredPlayers(game),
});
return undefined;
};
const ping = (session: any) => {
@ -3320,6 +3431,7 @@ const ping = (session: any) => {
};
const wsInactive = (game: any, req: any) => {
void game; // referenced for API completeness
const playerCookie = req.cookies && (req.cookies as any)["player"] ? String((req.cookies as any)["player"]) : "";
const session = getSession(game, playerCookie || "");
@ -3346,7 +3458,10 @@ const wsInactive = (game: any, req: any) => {
}
};
const setGameState = (game: any, session: any, state: any): string | void => {
// keep a void reference so linters/typecheckers don't complain about unused declarations
void wsInactive;
const setGameState = (game: any, session: any, state: any): string | undefined => {
if (!state) {
return `Invalid state.`;
}
@ -3385,9 +3500,11 @@ const setGameState = (game: any, session: any, state: any): string | void => {
});
break;
}
return undefined;
};
const resetDisconnectCheck = (game: any, req: any): void => {
const resetDisconnectCheck = (_game: any, req: any): void => {
void _game;
if (req.disconnectCheck) {
clearTimeout(req.disconnectCheck);
}
@ -3543,7 +3660,7 @@ const saveGame = async (game: any): Promise<void> => {
}
};
const departLobby = (game: any, session: any, color?: string): void => {
const departLobby = (game: any, session: any, _color?: string): void => {
const update: any = {};
update.unselected = getFilteredUnselected(game);
@ -3938,7 +4055,8 @@ const calculatePoints = (game: any, update: any): void => {
}
};
const clearGame = (game: any, session: any): void => {
const clearGame = (game: any, _session: any): string | undefined => {
void _session;
resetGame(game);
addChatMessage(
game,
@ -3946,6 +4064,7 @@ const clearGame = (game: any, session: any): void => {
`The game has been reset. You can play again with this board, or ` + `click 'New Table' to mix things up a bit.`
);
sendGameToPlayers(game);
return undefined;
};
const gotoLobby = (game: any, session: any): string | undefined => {
@ -3992,6 +4111,7 @@ const gotoLobby = (game: any, session: any): string | undefined => {
};
router.ws("/ws/:id", async (ws, req) => {
console.log("New WebSocket connection");
if (!req.cookies || !(req.cookies as any)["player"]) {
// If the client hasn't established a session cookie, they cannot
// participate in a websocket-backed game session. Log the request
@ -4028,10 +4148,12 @@ router.ws("/ws/:id", async (ws, req) => {
const gameId = id;
if (!gameId) {
console.log("Missing game id");
try {
ws.send(JSON.stringify({ type: "error", error: "Missing game id" }));
} catch (e) {}
try {
console.log("Missing game id");
ws.close && ws.close(1008, "Missing game id");
} catch (e) {}
return;
@ -4241,8 +4363,6 @@ router.ws("/ws/:id", async (ws, req) => {
switch (incoming.type) {
case "join":
// Accept either legacy `config` or newer `data` field from clients
join(audio[gameId], session, data.config || data.data || {});
break;
@ -4326,7 +4446,7 @@ router.ws("/ws/:id", async (ws, req) => {
}
const cfg = data.config || data.data || {};
const { peer_id, muted, video_on } = cfg;
const { muted, video_on } = cfg;
if (!session.name) {
console.error(`${session.id}: peer_state_update - unnamed session`);
return;
@ -4543,7 +4663,6 @@ router.ws("/ws/:id", async (ws, req) => {
}
processed = true;
const _priorSession = session;
switch (incoming.type) {
case "roll":
@ -5236,15 +5355,25 @@ const shuffleBoard = (game: any): void => {
[7, 3, 0, 1, 2, 6, 11, 15, 18, 17, 16, 12, 8, 4, 5, 10, 14, 13, 9],
];
const sequence = order[Math.floor(Math.random() * order.length)];
if (!sequence || !Array.isArray(sequence)) {
// Defensive: should not happen, but guard for TS strictness
return;
}
game.pipOrder = [];
game.animationSeeds = [];
for (let i = 0, p = 0; i < sequence.length; i++) {
const target = sequence[i];
if (typeof target !== "number") {
continue;
}
/* If the target tile is the desert (18), then set the
* pip value to the robber (18) otherwise set
* the target pip value to the currently incremeneting
* pip value. */
if (game.tiles[game.tileOrder[target]].type === "desert") {
const tileIdx = typeof game.tileOrder?.[target] === "number" ? game.tileOrder[target] : undefined;
const tileType = typeof tileIdx === "number" && game.tiles?.[tileIdx] ? game.tiles[tileIdx].type : undefined;
if (!game.pipOrder) game.pipOrder = [];
if (tileType === "desert") {
game.robber = target;
game.pipOrder[target] = 18;
} else {
@ -5320,7 +5449,7 @@ router.post("/:id?", async (req, res /*, next*/) => {
} else {
console.log(`[${playerId.substring(0, 8)}]: Creating new game.`);
}
const game = await loadGame(id); /* will create game if it doesn't exist */
const game = await loadGame(String(id || "")); /* will create game if it doesn't exist */
console.log(`[${playerId.substring(0, 8)}]: ${game.id} loaded.`);
return res.status(200).send({ id: game.id });