diff --git a/experimental/2nub/src/client/App.tsx b/experimental/2nub/src/client/App.tsx index 135b871..7ce4cb1 100644 --- a/experimental/2nub/src/client/App.tsx +++ b/experimental/2nub/src/client/App.tsx @@ -3,7 +3,7 @@ import { GameBoard } from './components/GameBoard'; import { CreateGame } from './components/CreateGame'; import { GameList } from './components/GameList'; import { useGameEvents } from './hooks/useGameEvents'; -import { GameState, GameStateClient, WebSocketMessage } from '../types'; +import { GameState, GameStateClient, WebSocketMessage, StateLobby } from '../types'; import { buildApiUrl } from './config/api'; import { sessionStorage } from './utils/sessionStorage'; @@ -86,7 +86,7 @@ const App: React.FC = () => { } }, [currentGame, currentPlayerId]); - const { isConnected, forceDisconnectPlayer, connect, authenticatePlayer } = useGameEvents({ + const { isConnected, forceDisconnectPlayer, connect, authenticatePlayer, updateRuleset } = useGameEvents({ onGameState, onGameCreated, onGameDeleted, @@ -289,6 +289,7 @@ const App: React.FC = () => { currentPlayerId={currentPlayerId} onLeave={leaveGame} onForceDisconnect={handleForceDisconnect} + updateRuleset={updateRuleset} /> )} diff --git a/experimental/2nub/src/client/components/GameBoard.tsx b/experimental/2nub/src/client/components/GameBoard.tsx index 7901df9..0c33b50 100644 --- a/experimental/2nub/src/client/components/GameBoard.tsx +++ b/experimental/2nub/src/client/components/GameBoard.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { GameStateClient } from '../../types'; +import { GameStateClient, StateLobby } from '../../types'; import { Onub } from './Onub'; interface GameBoardProps { @@ -8,9 +8,10 @@ interface GameBoardProps { currentPlayerId: string | null; onLeave: () => void; onForceDisconnect: (playerId: string) => void; + updateRuleset: (ruleset: StateLobby["ruleset"]) => void; } -export const GameBoard: React.FC = ({ game, gameId, currentPlayerId, onLeave, onForceDisconnect }) => { +export const GameBoard: React.FC = ({ game, gameId, currentPlayerId, onLeave, onForceDisconnect, updateRuleset }) => { console.debug("Rendering GameBoard with game:", game); const [disconnectingPlayerId, setDisconnectingPlayerId] = useState(null); @@ -230,7 +231,7 @@ export const GameBoard: React.FC = ({ game, gameId, currentPlaye )} - {currentPlayerId && } + {currentPlayerId && } ); }; \ No newline at end of file diff --git a/experimental/2nub/src/client/components/Onub.tsx b/experimental/2nub/src/client/components/Onub.tsx index 1110f7c..de90f52 100644 --- a/experimental/2nub/src/client/components/Onub.tsx +++ b/experimental/2nub/src/client/components/Onub.tsx @@ -1,13 +1,14 @@ import React, { useState } from 'react'; -import { GameStateClient } from '../../types'; +import { GameStateClient, StateLobby } from '../../types'; import { OnubLobby } from './OnubLobby'; interface OnubProps { game: GameStateClient; currentPlayerId: string; + updateRuleset: (ruleset: StateLobby["ruleset"]) => void; } -export const Onub: React.FC = ({ game, currentPlayerId }) => { +export const Onub: React.FC = ({ game, currentPlayerId, updateRuleset }) => { let content =

This is where the actual game would be implemented. The boilerplate provides: @@ -31,7 +32,7 @@ export const Onub: React.FC = ({ game, currentPlayerId }) => { switch (game.state.state) { case 'lobby': - content = ; + content = ; default: //throw new Error(`Onub component: Unsupported game state "${game.state.state}"`); } diff --git a/experimental/2nub/src/client/components/OnubLobby.tsx b/experimental/2nub/src/client/components/OnubLobby.tsx index 0188444..f479fc9 100644 --- a/experimental/2nub/src/client/components/OnubLobby.tsx +++ b/experimental/2nub/src/client/components/OnubLobby.tsx @@ -1,19 +1,24 @@ -import React, { useState } from 'react'; +import React from 'react'; import { GameStateClient, RoleId, ROLES, StateLobby } from '../../types'; interface OnubLobbyProps { stateLobby: StateLobby; playerCount: number; currentPlayerId: string; + updateRuleset: (ruleset: StateLobby["ruleset"]) => void; } -export const OnubLobby: React.FC = ({ stateLobby, playerCount, currentPlayerId }) => { +export const OnubLobby: React.FC = ({ stateLobby, playerCount, currentPlayerId, updateRuleset }) => { console.debug("Player count: ", playerCount); function makeRoleAddHandler(roleId: RoleId): React.MouseEventHandler { return (e: React.MouseEvent) => { e.preventDefault(); - //setRoles(roles => [...roles, roleId]); + const newRuleset = { + ...stateLobby.ruleset, + roleOrder: [...stateLobby.ruleset.roleOrder, roleId] + }; + updateRuleset(newRuleset); } } @@ -21,34 +26,40 @@ export const OnubLobby: React.FC = ({ stateLobby, playerCount, c return (e: React.MouseEvent) => { e.preventDefault(); if (index <= 0) return; - //setRoles(roles => { - // const newRoles = [...roles]; - // [newRoles[index - 1], newRoles[index]] = [newRoles[index], newRoles[index - 1]]; - // return newRoles; - //}); + const newRoleOrder = [...stateLobby.ruleset.roleOrder]; + [newRoleOrder[index - 1], newRoleOrder[index]] = [newRoleOrder[index], newRoleOrder[index - 1]]; + const newRuleset = { + ...stateLobby.ruleset, + roleOrder: newRoleOrder + }; + updateRuleset(newRuleset); } } function makeRoleMoveDownHandler(index: number): React.MouseEventHandler { return (e: React.MouseEvent) => { e.preventDefault(); - //setRoles(roles => { - // if (index >= roles.length - 1) return roles; - // const newRoles = [...roles]; - // [newRoles[index + 1], newRoles[index]] = [newRoles[index], newRoles[index + 1]]; - // return newRoles; - //}); + if (index >= stateLobby.ruleset.roleOrder.length - 1) return; + const newRoleOrder = [...stateLobby.ruleset.roleOrder]; + [newRoleOrder[index + 1], newRoleOrder[index]] = [newRoleOrder[index], newRoleOrder[index + 1]]; + const newRuleset = { + ...stateLobby.ruleset, + roleOrder: newRoleOrder + }; + updateRuleset(newRuleset); } } function makeRoleDeleteHandler(index: number): React.MouseEventHandler { return (e: React.MouseEvent) => { e.preventDefault(); - //setRoles(roles => { - // const newRoles = [...roles]; - // newRoles.splice(index, 1); - // return newRoles; - //}); + const newRoleOrder = [...stateLobby.ruleset.roleOrder]; + newRoleOrder.splice(index, 1); + const newRuleset = { + ...stateLobby.ruleset, + roleOrder: newRoleOrder + }; + updateRuleset(newRuleset); } } @@ -89,13 +100,17 @@ export const OnubLobby: React.FC = ({ stateLobby, playerCount, c type="checkbox" checked={stateLobby.ruleset.special.maybeAllTanners.enabled} onChange={e => { - //setSpecialRuleset(ruleset => ({ - // ...ruleset, - // maybeAllTanners: { - // ...ruleset.maybeAllTanners, - // enabled: e.target.checked, - // }, - //})) + const newRuleset = { + ...stateLobby.ruleset, + special: { + ...stateLobby.ruleset.special, + maybeAllTanners: { + ...stateLobby.ruleset.special.maybeAllTanners, + enabled: e.target.checked, + }, + } + }; + updateRuleset(newRuleset); }} /> Everyone is Tanner (with probability = ({ stateLobby, playerCount, c value={stateLobby.ruleset.special.maybeAllTanners.probability} min={0} max={1} + step={0.01} onChange={e => { const value = parseFloat(e.target.value); if (isNaN(value) || !isFinite(value)) return; - //setSpecialRuleset(ruleset => ({ - // ...ruleset, - // maybeAllTanners: { - // ...ruleset.maybeAllTanners, - // probability: parseFloat(e.target.value) || 0, - // }, - //})); + const newRuleset = { + ...stateLobby.ruleset, + special: { + ...stateLobby.ruleset.special, + maybeAllTanners: { + ...stateLobby.ruleset.special.maybeAllTanners, + probability: value, + }, + } + }; + updateRuleset(newRuleset); }} />) diff --git a/experimental/2nub/src/client/hooks/useGameEvents.ts b/experimental/2nub/src/client/hooks/useGameEvents.ts index 986bbaa..b8b53dd 100644 --- a/experimental/2nub/src/client/hooks/useGameEvents.ts +++ b/experimental/2nub/src/client/hooks/useGameEvents.ts @@ -1,6 +1,6 @@ import { useEffect, useCallback } from 'react'; import { useWebSocket } from './useWebSocket'; -import { GameState, GameStateClient, Player, ServerToClientEvents } from '../../types'; +import { GameState, GameStateClient, Player, ServerToClientEvents, StateLobby } from '../../types'; import { buildApiUrl } from '../config/api'; import { sessionStorage } from '../utils/sessionStorage'; @@ -24,6 +24,7 @@ interface UseGameEventsReturn { connect: () => void; disconnect: () => void; authenticatePlayer: (gameId: string, playerId: string) => void; + updateRuleset: (ruleset: StateLobby["ruleset"]) => void; } const createEventHandler = (eventName: string, handler: (data: T) => void) => { @@ -51,7 +52,7 @@ export function useGameEvents(options: UseGameEventsOptions): UseGameEventsRetur playerId } = options; - const { isConnected, socket, connect, disconnect, authenticatePlayer } = useWebSocket({ + const { isConnected, socket, connect, disconnect, authenticatePlayer, updateRuleset } = useWebSocket({ onConnect, onDisconnect, onConnectionError, @@ -106,5 +107,6 @@ export function useGameEvents(options: UseGameEventsOptions): UseGameEventsRetur connect, disconnect, authenticatePlayer, + updateRuleset, }; } \ No newline at end of file diff --git a/experimental/2nub/src/client/hooks/useWebSocket.ts b/experimental/2nub/src/client/hooks/useWebSocket.ts index d667260..e6f6e8f 100644 --- a/experimental/2nub/src/client/hooks/useWebSocket.ts +++ b/experimental/2nub/src/client/hooks/useWebSocket.ts @@ -109,6 +109,15 @@ export function useWebSocket(options: UseWebSocketOptions): UseWebSocketReturn { } }, [isConnected]); + const updateRuleset = useCallback((ruleset: StateLobby["ruleset"]) => { + if (socketRef.current && isConnected && gameIdRef.current) { + console.log(`Updating ruleset for game ${gameIdRef.current}`, ruleset); + socketRef.current.emit('updateRuleset', { gameId: gameIdRef.current, ruleset }); + } else { + console.error('Cannot update ruleset: socket not connected or no game ID'); + } + }, [isConnected]); + // Handle authentication when gameId/playerId change while connected useEffect(() => { if (isConnected && gameId && playerId) { @@ -129,5 +138,6 @@ export function useWebSocket(options: UseWebSocketOptions): UseWebSocketReturn { connect, disconnect, authenticatePlayer, + updateRuleset, }; } \ No newline at end of file diff --git a/experimental/2nub/src/server/gameState.ts b/experimental/2nub/src/server/gameState.ts index 8ded7c6..dcb69a5 100644 --- a/experimental/2nub/src/server/gameState.ts +++ b/experimental/2nub/src/server/gameState.ts @@ -180,6 +180,20 @@ class GameStateManager { return this.playerToSocket.has(playerId); } + updateRuleset(gameId: string, newRuleset: StateLobby["ruleset"]): GameState | null { + const game = this.games.get(gameId); + if (!game) return null; + + // Only allow updating ruleset if game is in lobby state + if (game.state.state !== 'lobby') return null; + + // Update the ruleset + (game.state as StateLobby).ruleset = newRuleset; + game.lastActivity = new Date(); + + return game; + } + } export const gameStateManager = new GameStateManager(); \ No newline at end of file diff --git a/experimental/2nub/src/server/websocket.ts b/experimental/2nub/src/server/websocket.ts index d7beec7..46406cd 100644 --- a/experimental/2nub/src/server/websocket.ts +++ b/experimental/2nub/src/server/websocket.ts @@ -45,6 +45,30 @@ export function setupSocketIO(io: Server { + const { gameId, ruleset } = data; + console.log(`Updating ruleset for game ${gameId} from socket ${socket.id}`); + + // Verify the socket is authenticated for this game + if (socket.gameId !== gameId) { + console.error(`Socket ${socket.id} not authenticated for game ${gameId}`); + socket.emit('error', { error: 'Not authenticated for this game' }); + return; + } + + // Update the ruleset via game state manager + const updatedGameState = gameStateManager.updateRuleset(gameId, ruleset); + + if (updatedGameState) { + // Broadcast updated game state to all players in the room + broadcastToGame(gameId, 'gameState', toGameStateClient(updatedGameState)); + console.log(`Ruleset updated and broadcasted for game ${gameId}`); + } else { + console.error(`Failed to update ruleset for game ${gameId}`); + socket.emit('error', { error: 'Failed to update ruleset' }); + } + }); + socket.on('disconnect', () => { console.log('Socket.io client disconnected:', socket.id); diff --git a/experimental/2nub/src/types.ts b/experimental/2nub/src/types.ts index 8e8b85d..1eeaec7 100644 --- a/experimental/2nub/src/types.ts +++ b/experimental/2nub/src/types.ts @@ -121,6 +121,7 @@ export type ServerToClientEvents = { export interface ClientToServerEvents { authenticatePlayer: (data: { gameId: string; playerId: string }) => void; + updateRuleset: (data: { gameId: string; ruleset: StateLobby["ruleset"] }) => void; } // Legacy WebSocket message interface (can be removed if not used elsewhere)