diff --git a/client/src/Actions.tsx b/client/src/Actions.tsx index b671f39..9692bb8 100644 --- a/client/src/Actions.tsx +++ b/client/src/Actions.tsx @@ -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 = ({ 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("lobby"); const [color, setColor] = useState(undefined); const [priv, setPriv] = useState(undefined); const [turn, setTurn] = useState({}); const [edit, setEdit] = useState(name); - console.log("Actions: name =", name, "edit =", edit); const [active, setActive] = useState(0); const [players, setPlayers] = useState>({}); const [alive, setAlive] = useState(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 = ({ 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) => { - 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 = ({ 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 = ({ 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 ; } @@ -265,22 +229,8 @@ const Actions: React.FC = ({ 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 ( {edit === "" && } diff --git a/client/src/App.css b/client/src/App.css index 1e87788..85e66e6 100755 --- a/client/src/App.css +++ b/client/src/App.css @@ -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; -} \ No newline at end of file diff --git a/client/src/Board.tsx b/client/src/Board.tsx index 89f2c34..085bce5 100644 --- a/client/src/Board.tsx +++ b/client/src/Board.tsx @@ -95,7 +95,7 @@ const clearTooltip = () => { }; const Board: React.FC = ({ animations }) => { - const { ws, sendJsonMessage } = useContext(GlobalContext); + const { sendJsonMessage, lastJsonMessage } = useContext(GlobalContext); const board = useRef(); const [transform, setTransform] = useState(1); const [pipElements, setPipElements] = useState([]); @@ -142,11 +142,11 @@ const Board: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ animations }) => { } else { type = "place-settlement"; } - ws.send( - JSON.stringify({ - type, - index: corner.index, - }) - ); + sendJsonMessage({ + type, + index: corner.index, + }); }; const Corner: React.FC = ({ corner }) => { return ( @@ -411,27 +389,17 @@ const Board: React.FC = ({ 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 = ({ 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 = ({ 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 = ({ pip, className }) => { const onPipClicked = (pip) => { @@ -547,12 +512,10 @@ const Board: React.FC = ({ 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 = ({ animations }) => { tiles, tileOrder, animationSeeds, - ws, state, rules, animations, diff --git a/client/src/Common.ts b/client/src/Common.ts index 620e6f2..d8f9cd4 100644 --- a/client/src/Common.ts +++ b/client/src/Common.ts @@ -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 }; diff --git a/client/src/GlobalContext.ts b/client/src/GlobalContext.ts index 79c27cb..cc96dc3 100644 --- a/client/src/GlobalContext.ts +++ b/client/src/GlobalContext.ts @@ -5,15 +5,21 @@ export type GlobalContextType = { name?: string; sendJsonMessage?: (message: any) => void; chat?: Array; + socketUrl?: string; + session?: Session; + lastJsonMessage?: any; }; const global: GlobalContextType = { roomName: undefined, name: "", + socketUrl: undefined, chat: [], + session: undefined, + lastJsonMessage: undefined, }; -const GlobalContext = createContext(global); +const GlobalContext = createContext(global); /** * RoomModel @@ -132,4 +138,4 @@ export type Session = Omit & { has_media?: boolean; // Whether this session provides audio/video streams }; -export { GlobalContext, global }; +export { GlobalContext }; diff --git a/client/src/MediaControl.tsx b/client/src/MediaControl.tsx index 14fd06d..4ccc53d 100644 --- a/client/src/MediaControl.tsx +++ b/client/src/MediaControl.tsx @@ -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 }); diff --git a/client/src/NameSetter.tsx b/client/src/NameSetter.tsx index 9ff0bb3..5d94cf9 100644 --- a/client/src/NameSetter.tsx +++ b/client/src/NameSetter.tsx @@ -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 = ({ - session, - sendJsonMessage, - onNameSet, - initialName = "", - initialPassword = "", -}) => { +const NameSetter: React.FC = ({ onNameSet, initialName = "", initialPassword = "" }) => { + const { session, sendJsonMessage } = useContext(GlobalContext); const [editName, setEditName] = useState(initialName); const [editPassword, setEditPassword] = useState(initialPassword); const [showDialog, setShowDialog] = useState(!session.name); @@ -28,7 +31,7 @@ const NameSetter: React.FC = ({ 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 = ({ {/* Dialog for name change */} - - {session.name ? "Change Your Name" : "Enter Your Name"} - + {session.name ? "Change Your Name" : "Enter Your Name"} - {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."} - 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. = ({ disabled={!canSubmit} color={hasNameChanged ? "primary" : "inherit"} > - {isSubmitting ? "Changing..." : (session.name ? "Change Name" : "Join")} + {isSubmitting ? "Changing..." : session.name ? "Change Name" : "Join"} @@ -159,4 +158,4 @@ const NameSetter: React.FC = ({ ); }; -export default NameSetter; \ No newline at end of file +export default NameSetter; diff --git a/client/src/PlayerList.tsx b/client/src/PlayerList.tsx index 4a7ca3b..985222e 100644 --- a/client/src/PlayerList.tsx +++ b/client/src/PlayerList.tsx @@ -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 = (props: PlayerListProps) => { - const { socketUrl, session, roomId } = props; +const PlayerList: React.FC = () => { + const { session, socketUrl } = useContext(GlobalContext); const [Players, setPlayers] = useState(null); const [peers, setPeers] = useState>({}); diff --git a/client/src/RoomView.css b/client/src/RoomView.css new file mode 100644 index 0000000..b804702 --- /dev/null +++ b/client/src/RoomView.css @@ -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; +} \ No newline at end of file diff --git a/client/src/RoomView.tsx b/client/src/RoomView.tsx index b376af1..acb7fbc 100644 --- a/client/src/RoomView.tsx +++ b/client/src/RoomView.tsx @@ -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 = { "its-your-turn.mp3": itsYourTurnAudio, @@ -64,11 +65,10 @@ type RoomProps = { setError: React.Dispatch>; }; -const RoomView: React.FC = (props: RoomProps) => { +const RoomView = (props: RoomProps) => { const { session, setSession, setError } = props; const [socketUrl, setSocketUrl] = useState(null); const { roomName = "default" } = useParams<{ roomName: string }>(); - const [creatingRoom, setCreatingRoom] = useState(false); const [reconnectAttempt, setReconnectAttempt] = useState(0); const [name, setName] = useState(""); @@ -88,7 +88,6 @@ const RoomView: React.FC = (props: RoomProps) => { const [cardActive, setCardActive] = useState(undefined); const [houseRulesActive, setHouseRulesActive] = useState(false); const [winnerDismissed, setWinnerDismissed] = useState(false); - const [global, setGlobal] = useState>({}); const [count, setCount] = useState(0); const [audio, setAudio] = useState( localStorage.getItem("audio") ? JSON.parse(localStorage.getItem("audio") as string) : false @@ -206,18 +205,6 @@ const RoomView: React.FC = (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 = (props: RoomProps) => { } }, [volume]); + if (readyState !== ReadyState.OPEN || !session) { + return ; + } + return ( - -
- {readyState !== ReadyState.OPEN || !session ? ( - + +
+ {!name ? ( + + + ) : ( <>
@@ -400,7 +393,7 @@ const RoomView: React.FC = (props: RoomProps) => { /> )} - + {name && } {/* Trade is an untyped JS component; assert its type to avoid `any` */} {(() => { const TradeComponent = Trade as unknown as React.ComponentType<{ diff --git a/client/src/index.css b/client/src/index.css index e474130..5fd920b 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -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; -} diff --git a/server/routes/games.ts b/server/routes/games.ts index 7dbab08..56a8610 100755 --- a/server/routes/games.ts +++ b/server/routes/games.ts @@ -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 | void> = { +const supportedRules: Record 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 { addChatMessage( @@ -2810,6 +2913,7 @@ const supportedRules: Record { if (!rules[rule].enabled) { @@ -2839,6 +2943,7 @@ const supportedRules: Record { 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 | void => { +const discard = (game: any, session: any, discards: Record): string | undefined => { const player = session.player; if (game.turn.roll !== 7) { @@ -2983,9 +3088,10 @@ const discard = (game: any, session: any, discards: Record): 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 => { } }; -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 });