From b9d7523800162dd8f3e01fcecc4b8bc4f0242454 Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Tue, 7 Oct 2025 14:59:14 -0700 Subject: [PATCH] Continuing... --- client/src/PlayerList.tsx | 159 ++++++++++++++++----------------- server/routes/games.ts | 46 +++------- server/routes/games/helpers.ts | 146 +++++++++++++++++++++++------- 3 files changed, 202 insertions(+), 149 deletions(-) diff --git a/client/src/PlayerList.tsx b/client/src/PlayerList.tsx index 985222e..cdecb45 100644 --- a/client/src/PlayerList.tsx +++ b/client/src/PlayerList.tsx @@ -5,7 +5,6 @@ import "./PlayerList.css"; import { MediaControl, MediaAgent, Peer } from "./MediaControl"; import Box from "@mui/material/Box"; import { GlobalContext } from "./GlobalContext"; -import useWebSocket from "react-use-websocket"; type Player = { name: string; @@ -22,8 +21,8 @@ type Player = { }; const PlayerList: React.FC = () => { - const { session, socketUrl } = useContext(GlobalContext); - const [Players, setPlayers] = useState(null); + const { session, socketUrl, lastJsonMessage, sendJsonMessage } = useContext(GlobalContext); + const [players, setPlayers] = useState(null); const [peers, setPeers] = useState>({}); const sortPlayers = useCallback( @@ -55,82 +54,76 @@ const PlayerList: React.FC = () => { ); // Use the WebSocket hook for room events with automatic reconnection - const { sendJsonMessage } = useWebSocket(socketUrl, { - share: true, - shouldReconnect: (closeEvent) => true, // Auto-reconnect on connection loss - reconnectInterval: 5000, - onMessage: (event: MessageEvent) => { - if (!session) { - return; - } - const message = JSON.parse(event.data); - const data: any = message.data; - switch (message.type) { - case "room_state": { - type RoomStateData = { - participants: Player[]; - }; - const room_state = data as RoomStateData; - console.log(`Players - room_state`, room_state.participants); - room_state.participants.forEach((Player) => { - Player.local = Player.session_id === session.id; - }); - room_state.participants.sort(sortPlayers); - setPlayers(room_state.participants); - // Initialize peers with remote mute/video state - setPeers((prevPeers) => { - const updated: Record = { ...prevPeers }; - room_state.participants.forEach((Player) => { - // Only update remote peers, never overwrite local peer object - if (!Player.local && updated[Player.session_id]) { - updated[Player.session_id] = { - ...updated[Player.session_id], - muted: Player.muted ?? false, - video_on: Player.video_on ?? true, - }; - } - }); - return updated; - }); - break; - } - case "update_name": { - // Update local session name immediately - if (data && typeof data.name === "string") { - session.name = data.name; - } - break; - } - case "peer_state_update": { - // Update peer state in peers, but do not override local mute - setPeers((prevPeers) => { - const updated = { ...prevPeers }; - const peerId = data.peer_id; - if (peerId && updated[peerId]) { - updated[peerId] = { - ...updated[peerId], - muted: data.muted, - video_on: data.video_on, + useEffect(() => { + if (!lastJsonMessage) { + return; + } + const data: any = lastJsonMessage; + switch (data.type) { + case "players": { + type RoomStateData = { + participants: Player[]; + }; + const room_state = data as RoomStateData; + console.log(`Players - room_state`, room_state.participants); + room_state.participants.forEach((player) => { + player.local = player.session_id === session.id; + }); + room_state.participants.sort(sortPlayers); + setPlayers(room_state.participants); + // Initialize peers with remote mute/video state + setPeers((prevPeers) => { + const updated: Record = { ...prevPeers }; + room_state.participants.forEach((player) => { + // Only update remote peers, never overwrite local peer object + if (!player.local && updated[player.session_id]) { + updated[player.session_id] = { + ...updated[player.session_id], + muted: player.muted ?? false, + video_on: player.video_on ?? true, }; } - return updated; }); - break; - } - default: - break; + return updated; + }); + break; } - }, - }); + case "update_name": { + // Update local session name immediately + if (data && typeof data.name === "string") { + session.name = data.name; + } + break; + } + case "peer_state_update": { + // Update peer state in peers, but do not override local mute + setPeers((prevPeers) => { + const updated = { ...prevPeers }; + const peerId = data.peer_id; + if (peerId && updated[peerId]) { + updated[peerId] = { + ...updated[peerId], + muted: data.muted, + video_on: data.video_on, + }; + } + return updated; + }); + break; + } + default: + break; + } + }, [lastJsonMessage, session, sortPlayers, setPeers, setPlayers]); useEffect(() => { - if (Players !== null) { + if (players !== null) { return; } sendJsonMessage({ type: "list_Players", }); - }, [Players, sendJsonMessage]); + }, [players, sendJsonMessage]); return ( @@ -144,17 +137,17 @@ const PlayerList: React.FC = () => { > - {Players?.map((Player) => ( + {players?.map((player) => ( -
{Player.name ? Player.name : Player.session_id}
- {Player.protected && ( +
{player.name ? player.name : player.session_id}
+ {player.protected && (
{ 🔒
)} - {Player.bot_instance_id && ( + {player.bot_instance_id && (
🤖
)}
- {Player.name && !Player.live &&
} + {player.name && !player.live &&
}
- {Player.name && Player.live && peers[Player.session_id] && (Player.local || Player.has_media !== false) ? ( + {player.name && player.live && peers[player.session_id] && (player.local || player.has_media !== false) ? ( - ) : Player.name && Player.live && Player.has_media === false ? ( + ) : player.name && player.live && player.has_media === false ? (
{ return game; }; -const clearPlayer = (player: any) => { - const color = player.color; - for (let key in player) { - delete player[key]; - } - Object.assign(player, newPlayer(color)); -}; - -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; @@ -1317,7 +1308,6 @@ const setPlayerColor = (game: any, session: any, color: string): string | undefi sendUpdateToPlayers(game, update); return undefined; }; - 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 */ @@ -1804,21 +1794,7 @@ const offerToString = (offer: any): string => { ); }; -const setForRoadPlacement = (game: Game, limits: any): void => { - game.turn.actions = ["place-road"]; - game.turn.limits = { roads: limits }; -}; - -const setForCityPlacement = (game: Game, limits: any): void => { - game.turn.actions = ["place-city"]; - game.turn.limits = { corners: limits }; -}; - -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 }; -}; + router.put("/:id/:action/:value?", async (req, res) => { const { action, id } = req.params, diff --git a/server/routes/games/helpers.ts b/server/routes/games/helpers.ts index 1e3a1fa..5c4824b 100644 --- a/server/routes/games/helpers.ts +++ b/server/routes/games/helpers.ts @@ -1,5 +1,8 @@ -export const addActivity = (game: any, session: any, message: string): void => { +import type { Game, Session, Player } from "./types"; + +export const addActivity = (game: Game, session: Session | null, message: string): void => { let date = Date.now(); + if (!game.activities) game.activities = [] as any[]; if (game.activities.length && game.activities[game.activities.length - 1].date === date) { date++; } @@ -9,9 +12,10 @@ export const addActivity = (game: any, session: any, message: string): void => { } }; -export const addChatMessage = (game: any, session: any, message: string, isNormalChat?: boolean) => { +export const addChatMessage = (game: Game, session: Session | null, message: string, isNormalChat?: boolean) => { let now = Date.now(); let lastTime = 0; + if (!game.chat) game.chat = [] as any[]; if (game.chat.length) { lastTime = game.chat[game.chat.length - 1].date; } @@ -38,71 +42,151 @@ export const addChatMessage = (game: any, session: any, message: string, isNorma } }; -export const getColorFromName = (game: any, name: string): string => { +export const getColorFromName = (game: Game, name: string): string => { for (let id in game.sessions) { - if (game.sessions[id].name === name) { - return game.sessions[id].color; + const s = game.sessions[id]; + if (s && s.name === name) { + return s.color || ""; } } return ""; }; -export const getLastPlayerName = (game: any): string => { - let index = game.playerOrder.length - 1; +export const getLastPlayerName = (game: Game): string => { + const index = (game.playerOrder || []).length - 1; + const color = (game.playerOrder || [])[index]; + if (!color) return ""; for (let id in game.sessions) { - if (game.sessions[id].color === game.playerOrder[index]) { - return game.sessions[id].name; + const s = game.sessions[id]; + if (s && s.color === color) { + return s.name || ""; } } return ""; }; -export const getFirstPlayerName = (game: any): string => { - let index = 0; +export const getFirstPlayerName = (game: Game): string => { + const color = (game.playerOrder || [])[0]; + if (!color) return ""; for (let id in game.sessions) { - if (game.sessions[id].color === game.playerOrder[index]) { - return game.sessions[id].name; + const s = game.sessions[id]; + if (s && s.color === color) { + return s.name || ""; } } return ""; }; -export const getNextPlayerSession = (game: any, name: string): any => { - let color; +export const getNextPlayerSession = (game: Game, name: string): Session | undefined => { + let color: string | undefined; for (let id in game.sessions) { - if (game.sessions[id].name === name) { - color = game.sessions[id].color; + const s = game.sessions[id]; + if (s && s.name === name) { + color = s.color; break; } } + if (!color) return undefined; - let index = game.playerOrder.indexOf(color); - index = (index + 1) % game.playerOrder.length; - color = game.playerOrder[index]; + const order = game.playerOrder || []; + let index = order.indexOf(color); + if (index === -1) return undefined; + index = (index + 1) % order.length; + const nextColor = order[index]; for (let id in game.sessions) { - if (game.sessions[id].color === color) { - return game.sessions[id]; + const s = game.sessions[id]; + if (s && s.color === nextColor) { + return s; } } console.error(`getNextPlayerSession -- no player found!`); console.log(game.players); + return undefined; }; -export const getPrevPlayerSession = (game: any, name: string): any => { - let color; +export const getPrevPlayerSession = (game: Game, name: string): Session | undefined => { + let color: string | undefined; for (let id in game.sessions) { - if (game.sessions[id].name === name) { - color = game.sessions[id].color; + const s = game.sessions[id]; + if (s && s.name === name) { + color = s.color; break; } } - let index = game.playerOrder.indexOf(color); - index = (index - 1) % game.playerOrder.length; + if (!color) return undefined; + const order = game.playerOrder || []; + let index = order.indexOf(color); + if (index === -1) return undefined; + index = (index - 1 + order.length) % order.length; + const prevColor = order[index]; for (let id in game.sessions) { - if (game.sessions[id].color === game.playerOrder[index]) { - return game.sessions[id]; + const s = game.sessions[id]; + if (s && s.color === prevColor) { + return s; } } - console.error(`getNextPlayerSession -- no player found!`); + console.error(`getPrevPlayerSession -- no player found!`); console.log(game.players); + return undefined; +}; + +export const clearPlayer = (player: Player) => { + const color = player.color; + for (let key in player) { + // delete all runtime fields + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + delete (player as any)[key]; + } + + // Inline minimal newPlayer factory to avoid circular import at runtime + const base = { + roads: 15, + cities: 4, + settlements: 5, + points: 0, + status: "Not active", + lastActive: 0, + resources: 0, + order: 0, + stone: 0, + wheat: 0, + sheep: 0, + wood: 0, + brick: 0, + army: 0, + development: [], + color: color, + name: "", + totalTime: 0, + turnStart: 0, + ports: 0, + developmentCards: 0, + } as Player; + + Object.assign(player, base); +}; + +export const canGiveBuilding = (game: Game): 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; +}; + +export const setForRoadPlacement = (game: Game, limits: any): void => { + game.turn.actions = ["place-road"]; + game.turn.limits = { roads: limits }; +}; + +export const setForCityPlacement = (game: Game, limits: any): void => { + game.turn.actions = ["place-city"]; + game.turn.limits = { corners: limits }; +}; + +export const setForSettlementPlacement = (game: Game, limits: number[] | undefined, _extra?: any): void => { + game.turn.actions = ["place-settlement"]; + game.turn.limits = { corners: limits }; };