diff --git a/src/components/GolfGame.module.css b/src/components/GolfGame.module.css index df298dd..ea51472 100644 --- a/src/components/GolfGame.module.css +++ b/src/components/GolfGame.module.css @@ -833,7 +833,7 @@ z-index: 100; animation: slideDownOverlay 0.5s ease-out; width: 90%; - max-width: 380px; + max-width: 900px; pointer-events: all; } @@ -1147,4 +1147,255 @@ background: rgba(255, 255, 255, 0.1); color: white; transform: rotate(90deg); +} + +/* Room Lobby Styles */ +.roomLobby { + display: flex; + flex-direction: column; + align-items: center; + min-height: 400px; + padding: 2rem; + max-width: 1200px; + margin: 0 auto; +} + +.roomHeader { + text-align: center; + margin-bottom: 2rem; +} + +.roomInfo { + color: rgba(255, 255, 255, 0.7); + font-size: 1.1rem; + margin-top: 0.5rem; +} + +.roomContent { + width: 100%; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2rem; +} + +@media (max-width: 768px) { + .roomContent { + grid-template-columns: 1fr; + gap: 1.5rem; + } +} + +.playersSection, +.gamesSection, +.gameCreationSection, +.gameHistorySection { + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + padding: 1.5rem; + border-radius: 1rem; +} + +.playersSection h3, +.gamesSection h3, +.gameCreationSection h3, +.gameHistorySection h3 { + color: #90ee90; + font-family: 'Lexend', sans-serif; + font-size: 1.2rem; + margin: 0 0 1rem 0; +} + +.roomPlayerList { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +.roomPlayer { + background: rgba(255, 255, 255, 0.05); + border-radius: 0.5rem; + padding: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); +} + +.playerName { + color: white; + font-weight: 500; + font-size: 1.1rem; + margin-bottom: 0.5rem; +} + +.playerStats { + display: flex; + gap: 1rem; + font-size: 0.9rem; + color: rgba(255, 255, 255, 0.7); +} + +.activeGamesList { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.gameCard { + background: rgba(255, 255, 255, 0.05); + border-radius: 0.5rem; + padding: 1rem; + border: 1px solid rgba(255, 255, 255, 0.1); + display: flex; + justify-content: space-between; + align-items: center; +} + +.gameInfo h4 { + color: white; + font-size: 1.1rem; + margin: 0 0 0.5rem 0; +} + +.gameInfo p { + color: rgba(255, 255, 255, 0.7); + font-size: 0.9rem; + margin: 0.25rem 0; +} + +.joinGameButton { + padding: 0.5rem 1rem; + font-size: 0.9rem; + font-family: 'Lexend', sans-serif; + background: #90ee90; + color: #0d5016; + border: none; + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.joinGameButton:hover:not(:disabled) { + background: #7dd87d; +} + +.joinGameButton:disabled { + opacity: 0.5; + cursor: not-allowed; +} + +.noGames { + color: rgba(255, 255, 255, 0.5); + font-style: italic; + text-align: center; + padding: 1rem; +} + +.gameCreationForm { + display: flex; + gap: 1rem; + align-items: center; +} + +.gameCreationForm input { + flex: 1; +} + +.gameHistory { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +.historyItem { + display: flex; + justify-content: space-between; + align-items: center; + background: rgba(255, 255, 255, 0.05); + padding: 0.75rem; + border-radius: 0.5rem; + font-size: 0.9rem; +} + +.gameId { + color: #90ee90; + font-weight: 500; +} + +.winner { + color: white; +} + +.completedAt { + color: rgba(255, 255, 255, 0.6); + font-size: 0.8rem; +} + +/* Game End Enhancements */ +.cumulativeScores { + background: rgba(255, 255, 255, 0.1); + border-radius: 1rem; + padding: 1.5rem; + margin: 1.5rem 0; +} + +.cumulativeTitle { + color: #90ee90; + font-family: 'Lexend', sans-serif; + font-size: 1.3rem; + margin: 0 0 1rem 0; + text-align: center; +} + +.cumulativeRow { + display: grid; + grid-template-columns: 2fr 1fr 1fr 1fr 1fr; + gap: 1rem; + padding: 0.75rem; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); + align-items: center; + font-size: 0.9rem; +} + +.cumulativeRow:last-child { + border-bottom: none; +} + +.bestAverage { + background: rgba(144, 238, 144, 0.1); + border-radius: 0.5rem; +} + +.cumulativeRow .playerName { + color: white; + font-weight: 500; +} + +.cumulativeRow .playerGames, +.cumulativeRow .playerWins, +.cumulativeRow .playerTotal, +.cumulativeRow .playerAverage { + color: rgba(255, 255, 255, 0.8); + text-align: center; +} + +.gameEndActions { + display: flex; + gap: 1rem; + justify-content: center; + flex-wrap: wrap; +} + +.tertiaryButton { + padding: 0.75rem 1.5rem; + font-size: 1rem; + font-family: 'Lexend', sans-serif; + background: transparent; + color: rgba(255, 255, 255, 0.6); + border: 1px solid rgba(255, 255, 255, 0.2); + border-radius: 0.5rem; + cursor: pointer; + transition: all 0.3s ease; +} + +.tertiaryButton:hover { + color: white; + border-color: rgba(255, 255, 255, 0.4); } \ No newline at end of file diff --git a/src/components/GolfGame.tsx b/src/components/GolfGame.tsx index 73002c4..3aa7640 100644 --- a/src/components/GolfGame.tsx +++ b/src/components/GolfGame.tsx @@ -7,6 +7,11 @@ interface Card { suit: string } +interface Player { + id: string + // Add other properties as needed +} + interface GolfGameProps { onGameIdChange: (id: string | null) => void @@ -18,26 +23,38 @@ interface GolfGameProps { const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConnectionChange }: GolfGameProps) => { const [showRules, setShowRules] = useState(false) + // Helper function to get display name (now just use the ID directly) + const getDisplayName = (player: Player | null) => { + // Since IDs are now whimsical names directly, just use the ID + return player?.id || '' + } + const { gameState, + roomState, roomCode, selectedCardIndex, isInLobby, + isInRoom, notification, currentPlayer, isMyTurn, peekCountdown, winner, + createRoom, createGame, + joinRoom, joinGame, startGame, + startNewGame, drawCard, takeFromDiscard, swapCard, discardDrawn, knock, handleCardClick, - setRoomCode + setRoomCode, + clearGameState } = useGolfGame({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConnectionChange }) const renderCard = (card: Card | null, index: number, isRevealed: boolean, isPlayer: boolean) => { @@ -76,8 +93,8 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn

Golf Card Game

-
OR
@@ -91,8 +108,8 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn className={styles.input} maxLength={6} /> -
@@ -179,6 +196,98 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn ) } + // Room lobby state - user is in room but not in a specific game + if (isInRoom && !gameState && roomState) { + return ( +
+
+

Room: {roomState.id}

+

+ {roomState.players.length} player{roomState.players.length !== 1 ? 's' : ''} in room +

+
+ +
+
+

Players in Room

+
+ {roomState.players.map(player => ( +
+
+ {getDisplayName(player)} {player.isConnected ? '🟢' : '🔴'} +
+
+ Games: {player.gamesPlayed} + Wins: {player.gamesWon} + Total Score: {player.totalScore} +
+
+ ))} +
+
+ +
+

Games in Room

+ {Object.keys(roomState.games).length > 0 ? ( +
+ {Object.entries(roomState.games).map(([gameId, game]) => ( +
+
+

{gameId}

+

{game.players.length}/4 players

+

Status: {game.gamePhase}

+
+ +
+ ))} +
+ ) : ( +

No active games in room

+ )} +
+ +
+

Start New Game

+
+ +
+
+ + {roomState.gameHistory.length > 0 && ( +
+

Recent Games

+
+ {roomState.gameHistory.slice(-3).reverse().map((result, _index) => ( +
+ {result.gameId} + Winner: {result.winner} + + {new Date(result.completedAt).toLocaleTimeString()} + +
+ ))} +
+
+ )} +
+ + {notification && ( +
+ {notification} +
+ )} +
+ ) + } + if (!gameState) { return
Loading...
} @@ -193,7 +302,7 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn key={player.id} className={`${styles.playerInfo} ${index === gameState.currentPlayerIndex ? styles.active : ''}`} > - {player.name} + {getDisplayName(player)} {gameState.gamePhase === 'ended' && ( Score: {player.score} )} @@ -218,18 +327,22 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
- 🏆 + {currentPlayer && getDisplayName(currentPlayer) === winner ? ( + 🏆 + ) : ( + 😢 + )}

Game Over!

{winner && (
- {currentPlayer?.name === winner ? ( + {currentPlayer && getDisplayName(currentPlayer) === winner ? (
🎉 🎊 🎉
) : (
)}
- {currentPlayer?.name === winner ? ( + {currentPlayer && getDisplayName(currentPlayer) === winner ? ( <> Congratulations! You've won with the lowest score! @@ -253,7 +366,7 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn {index === 0 ? '👑' : `#${index + 1}`} - {player.name} + {getDisplayName(player)} {player.score} pts
))} @@ -261,18 +374,43 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
Lower scores win • Pairs cancel out
+ {roomState && ( +
+

Room Totals

+ {roomState.players + .sort((a, b) => (a.gamesPlayed > 0 ? a.totalScore / a.gamesPlayed : 0) - (b.gamesPlayed > 0 ? b.totalScore / b.gamesPlayed : 0)) + .map((player, index) => ( +
0 ? styles.bestAverage : ''}`}> + {getDisplayName(player)} + {player.gamesPlayed} games + {player.gamesWon} wins + {player.totalScore} total + + {player.gamesPlayed > 0 ? (player.totalScore / player.gamesPlayed).toFixed(1) : 'N/A'} avg + +
+ ))} +
+ )} +
+
diff --git a/src/hooks/useGolfGame.ts b/src/hooks/useGolfGame.ts index bee30bb..0246fe7 100644 --- a/src/hooks/useGolfGame.ts +++ b/src/hooks/useGolfGame.ts @@ -1,5 +1,5 @@ import { useState, useRef, useCallback, useEffect } from 'react' -import type { GameState, Player } from '@/types/golf' +import type { GameState, Player, Room } from '@/types/golf' import { GolfNetworkAdapter } from '@/utils/networkAdapter' interface UseGolfGameProps { @@ -12,10 +12,12 @@ interface UseGolfGameProps { interface UseGolfGameReturn { // State gameState: GameState | null + roomState: Room | null playerId: string roomCode: string selectedCardIndex: number | null isInLobby: boolean + isInRoom: boolean notification: string isConnected: boolean peekCountdown: number | null @@ -23,9 +25,12 @@ interface UseGolfGameReturn { finalScores: Array<{ playerName: string; score: number }> | null // Actions - createGame: () => void - joinGame: () => void + createRoom: () => void + createGame: (roomId?: string) => void + joinRoom: () => void + joinGame: (gameId?: string) => void startGame: () => void + startNewGame: () => void peekCard: (index: number) => void drawCard: () => void takeFromDiscard: () => void @@ -34,6 +39,7 @@ interface UseGolfGameReturn { knock: () => void handleCardClick: (index: number) => void setRoomCode: (code: string) => void + clearGameState: () => void // Computed currentPlayer: Player | undefined @@ -47,10 +53,12 @@ export const useGolfGame = ({ onConnectionChange }: UseGolfGameProps = {}): UseGolfGameReturn => { const [gameState, setGameState] = useState(null) + const [roomState, setRoomState] = useState(null) const [playerId, setPlayerId] = useState('') const [roomCode, setRoomCode] = useState('') const [selectedCardIndex, setSelectedCardIndex] = useState(null) const [isInLobby, setIsInLobby] = useState(true) + const [isInRoom, setIsInRoom] = useState(false) const [notification, setNotification] = useState('') const [isConnected, setIsConnected] = useState(false) const [peekCountdown, setPeekCountdown] = useState(null) @@ -72,15 +80,28 @@ export const useGolfGame = ({ }, []) // Game actions - const createGame = useCallback(() => { + const createRoom = useCallback(() => { if (!networkAdapterRef.current) { showNotification('Not connected to server') return } - networkAdapterRef.current.createGame() + networkAdapterRef.current.createRoom() }, [showNotification]) - const joinGame = useCallback(() => { + const createGame = useCallback((roomId?: string) => { + const actualRoomId = roomId || roomState?.id + if (!actualRoomId) { + showNotification('Must be in a room to create a game') + return + } + if (!networkAdapterRef.current) { + showNotification('Not connected to server') + return + } + networkAdapterRef.current.createGame(actualRoomId) + }, [showNotification, roomState?.id]) + + const joinRoom = useCallback(() => { if (!roomCode.trim()) { showNotification('Please enter a room code') return @@ -89,9 +110,30 @@ export const useGolfGame = ({ showNotification('Not connected to server') return } - networkAdapterRef.current.joinGame(roomCode.trim()) + // Join room lobby (no specific game) + networkAdapterRef.current.joinRoom(roomCode.trim()) }, [roomCode, showNotification]) + const joinGame = useCallback((gameId?: string) => { + if (!gameId) { + showNotification('Please enter a game code') + return + } + if (!roomState?.id) { + showNotification('Must be in a room first') + return + } + if (!gameId.trim()) { + showNotification('Please enter a game code') + return + } + if (!networkAdapterRef.current) { + showNotification('Not connected to server') + return + } + networkAdapterRef.current.joinGame(roomState.id, gameId) + }, [roomState?.id, showNotification]) + const startGame = useCallback(() => { if (!networkAdapterRef.current) { showNotification('Not connected to server') @@ -100,6 +142,14 @@ export const useGolfGame = ({ networkAdapterRef.current.startGame() }, [showNotification]) + const startNewGame = useCallback(() => { + if (!networkAdapterRef.current) { + showNotification('Not connected to server') + return + } + networkAdapterRef.current.startNewGame() + }, [showNotification]) + const peekCard = useCallback((index: number) => { if (!networkAdapterRef.current) { showNotification('Not connected to server') @@ -169,6 +219,13 @@ export const useGolfGame = ({ } }, [gameState, playerId, peekCard, peekCountdown]) + const clearGameState = useCallback(() => { + setGameState(null) + setWinner(null) + setFinalScores(null) + setSelectedCardIndex(null) + }, []) + // Computed values const currentPlayer = gameState?.players.find(p => p.id === playerId) const isMyTurn = gameState?.players[gameState.currentPlayerIndex]?.id === playerId @@ -177,6 +234,26 @@ export const useGolfGame = ({ useEffect(() => { // Create network adapter with callbacks const adapter = new GolfNetworkAdapter({ + onRoomJoined: (newPlayerId, newRoomState) => { + setPlayerId(newPlayerId) + setRoomState(newRoomState) + setIsInRoom(true) + setIsInLobby(false) + onGameIdChange?.(newRoomState.id) + onPlayerIdChange?.(newPlayerId) + const player = newRoomState.players.find(p => p.id === newPlayerId) + onPlayerNameChange?.(player?.name || null) + showNotification('Joined room successfully!') + }, + onRoomStateUpdate: (newRoomState) => { + setRoomState(newRoomState) + // Check if any games ended and clear game state if current game is gone + if (gameState && !newRoomState.games[gameState.id]) { + setGameState(null) + setWinner(null) + setFinalScores(null) + } + }, onGameJoined: (newPlayerId, newGameState) => { setPlayerId(newPlayerId) setGameState(newGameState) @@ -223,7 +300,9 @@ export const useGolfGame = ({ clearTimeout(notificationTimeoutRef.current) } } + // eslint-disable-next-line react-hooks/exhaustive-deps }, [onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConnectionChange, showNotification]) + // gameState intentionally omitted from dependencies to prevent network adapter recreation // Handle peek countdown when all players have peeked useEffect(() => { @@ -275,10 +354,12 @@ export const useGolfGame = ({ return { // State gameState, + roomState, playerId, roomCode, selectedCardIndex, isInLobby, + isInRoom, notification, isConnected, peekCountdown, @@ -286,9 +367,12 @@ export const useGolfGame = ({ finalScores, // Actions + createRoom, createGame, + joinRoom, joinGame, startGame, + startNewGame, peekCard, drawCard, takeFromDiscard, @@ -297,6 +381,7 @@ export const useGolfGame = ({ knock, handleCardClick, setRoomCode, + clearGameState, // Computed currentPlayer, diff --git a/src/plugins/golfNetworkPlugin.ts b/src/plugins/golfNetworkPlugin.ts index b0983bb..0a243a9 100644 --- a/src/plugins/golfNetworkPlugin.ts +++ b/src/plugins/golfNetworkPlugin.ts @@ -5,69 +5,86 @@ import type { MessageHandlerMap, NetworkContext } from '@/types/network' -import type { Player, GameState as GolfGameState } from '@/types/golf' +import type { Player, GameState as GolfGameState, Room, FinalScore } from '@/types/golf' // Golf-specific message types interface GolfMessage extends BaseNetworkMessage { - type: 'createGame' | 'joinGame' | 'gameJoined' | 'gameState' | 'error' | - 'gameStarted' | 'turnChanged' | 'playerKnocked' | 'gameEnded' | + type: 'createRoom' | 'joinRoom' | 'createGame' | 'joinGame' | 'roomJoined' | 'roomStateUpdate' | 'gameState' | 'error' | + 'gameStarted' | 'turnChanged' | 'playerKnocked' | 'gameEnded' | 'newGameStarted' | 'startGame' | 'peekCard' | 'drawCard' | 'takeFromDiscard' | - 'swapCard' | 'discardDrawn' | 'knock' | 'hideCards' + 'swapCard' | 'discardDrawn' | 'knock' | 'hideCards' | 'startNewGame' // Request fields + roomId?: string gameId?: string cardIndex?: number // Response fields playerId?: string gameState?: GolfGameState + roomState?: Room message?: string winner?: string - finalScores?: Array<{ playerName: string; score: number }> + finalScores?: FinalScore[] + previousGameId?: string } // Golf plugin state interface GolfPluginState { playerId: string | null gameState: GolfGameState | null + roomState: Room | null + gameContext: { roomId: string; gameId: string } | null isInLobby: boolean } export class GolfNetworkPlugin implements GameNetworkPlugin { gameType = 'golf' + private onRoomJoined?: (playerId: string, roomState: Room) => void private onGameJoined?: (playerId: string, gameState: GolfGameState) => void private onGameStateUpdate?: (gameState: GolfGameState) => void + private onRoomStateUpdate?: (roomState: Room) => void private onNotification?: (message: string) => void - private onGameEnded?: (winner: string, finalScores: Array<{ playerName: string; score: number }>) => void + private onGameEnded?: (winner: string, finalScores: FinalScore[]) => void + private onNewGameStarted?: (gameId: string, previousGameId?: string) => void constructor(callbacks?: { + onRoomJoined?: (playerId: string, roomState: Room) => void onGameJoined?: (playerId: string, gameState: GolfGameState) => void onGameStateUpdate?: (gameState: GolfGameState) => void + onRoomStateUpdate?: (roomState: Room) => void onNotification?: (message: string) => void - onGameEnded?: (winner: string, finalScores: Array<{ playerName: string; score: number }>) => void + onGameEnded?: (winner: string, finalScores: FinalScore[]) => void + onNewGameStarted?: (gameId: string, previousGameId?: string) => void }) { if (callbacks) { + this.onRoomJoined = callbacks.onRoomJoined this.onGameJoined = callbacks.onGameJoined this.onGameStateUpdate = callbacks.onGameStateUpdate + this.onRoomStateUpdate = callbacks.onRoomStateUpdate this.onNotification = callbacks.onNotification this.onGameEnded = callbacks.onGameEnded + this.onNewGameStarted = callbacks.onNewGameStarted } } getMessageHandlers(): MessageHandlerMap { return { + 'roomJoined': (msg, ctx) => this.handleRoomJoined(msg as GolfMessage, ctx), + 'roomStateUpdate': (msg, ctx) => this.handleRoomStateUpdate(msg as GolfMessage, ctx), 'gameJoined': (msg, ctx) => this.handleGameJoined(msg as GolfMessage, ctx), 'gameState': (msg, ctx) => this.handleGameState(msg as GolfMessage, ctx), 'error': (msg, ctx) => this.handleError(msg as GolfMessage, ctx), 'gameStarted': (msg, ctx) => this.handleGameStarted(msg as GolfMessage, ctx), 'turnChanged': (msg, ctx) => this.handleTurnChanged(msg as GolfMessage, ctx), 'playerKnocked': (msg, ctx) => this.handlePlayerKnocked(msg as GolfMessage, ctx), - 'gameEnded': (msg, ctx) => this.handleGameEnded(msg as GolfMessage, ctx) + 'gameEnded': (msg, ctx) => this.handleGameEnded(msg as GolfMessage, ctx), + 'newGameStarted': (msg, ctx) => this.handleNewGameStarted(msg as GolfMessage, ctx) } } validateMessage(message: BaseNetworkMessage): boolean { const validTypes = [ - 'gameJoined', 'gameState', 'error', 'gameStarted', - 'turnChanged', 'playerKnocked', 'gameEnded' + 'roomJoined', 'roomStateUpdate', 'gameJoined', 'gameState', 'error', + 'gameStarted', 'turnChanged', 'playerKnocked', 'gameEnded', 'newGameStarted' ] return validTypes.includes(message.type) } @@ -76,6 +93,8 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { return { playerId: null, gameState: null, + roomState: null, + gameContext: null, isInLobby: true } } @@ -91,11 +110,51 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { ...s, playerId: null, gameState: null, + roomState: null, + gameContext: null, isInLobby: true })) } // Message handlers + private handleRoomJoined(message: GolfMessage, context: NetworkContext): void { + if (!message.playerId || !message.roomState) { + console.error('Room joined message missing required fields') + return + } + + context.updateGameState(s => ({ + ...s, + playerId: message.playerId!, + roomState: message.roomState!, + isInLobby: false + })) + + console.log(`🎉 Joined room ${message.roomState.id} as player ${message.playerId}`) + + if (this.onRoomJoined) { + this.onRoomJoined(message.playerId, message.roomState) + } + + this.notify('Joined room successfully!', context) + } + + private handleRoomStateUpdate(message: GolfMessage, context: NetworkContext): void { + if (!message.roomState) { + console.error('Room state update message missing roomState field') + return + } + + context.updateGameState(s => ({ + ...s, + roomState: message.roomState! + })) + + if (this.onRoomStateUpdate) { + this.onRoomStateUpdate(message.roomState) + } + } + private handleGameJoined(message: GolfMessage, context: NetworkContext): void { if (!message.playerId || !message.gameState) { console.error('Game joined message missing required fields') @@ -173,6 +232,18 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { } } + private handleNewGameStarted(message: GolfMessage, context: NetworkContext): void { + const gameId = message.gameId + const previousGameId = message.previousGameId + + console.log(`🆕 New game started in room! Game ID: ${gameId}${previousGameId ? `, Previous: ${previousGameId}` : ''}`) + this.notify('New game started!', context) + + if (this.onNewGameStarted && gameId) { + this.onNewGameStarted(gameId, previousGameId) + } + } + // Helper method to send notifications private notify(message: string, _context: NetworkContext): void { if (this.onNotification) { @@ -181,21 +252,31 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { } // Public methods for the game to use - createGame(context: NetworkContext): void { + createRoom(context: NetworkContext): void { + context.send({ + type: 'createRoom', + timestamp: Date.now() + }) + console.log('📤 Sent create room request') + } + + createGame(roomId: string, context: NetworkContext): void { context.send({ type: 'createGame', + roomId: roomId, timestamp: Date.now() }) - console.log('📤 Sent create game request') + console.log(`📤 Sent create game request for room ${roomId}`) } - joinGame(gameId: string, context: NetworkContext): void { + joinGame(roomId: string, gameId: string, context: NetworkContext): void { context.send({ type: 'joinGame', + roomId: roomId, gameId: gameId, timestamp: Date.now() }) - console.log(`📤 Sent join game request for room ${gameId}`) + console.log(`📤 Sent join game request for room ${roomId}, game ${gameId}`) } startGame(context: NetworkContext): void { @@ -264,6 +345,23 @@ export class GolfNetworkPlugin implements GameNetworkPlugin { console.log('📤 Sent hideCards request') } + joinRoom(roomId: string, context: NetworkContext): void { + context.send({ + type: 'joinRoom', + roomId: roomId, + timestamp: Date.now() + }) + console.log(`📤 Sent join room request for room ${roomId}`) + } + + startNewGame(context: NetworkContext): void { + context.send({ + type: 'startNewGame', + timestamp: Date.now() + }) + console.log('📤 Sent start new game request') + } + // Helper to check if it's the player's turn isMyTurn(context: NetworkContext): boolean { const state = context.getGameState() diff --git a/src/types/golf.ts b/src/types/golf.ts index 8042674..56669d3 100644 --- a/src/types/golf.ts +++ b/src/types/golf.ts @@ -4,6 +4,7 @@ export interface Card { } export interface Player { + // Game-specific fields id: string name: string cards: (Card | null)[] @@ -11,6 +12,14 @@ export interface Player { revealedCards: number[] isReady: boolean hasPeeked: boolean + + // Room/persistence fields + clientId: string + totalScore: number // Running total across all games + gamesPlayed: number + gamesWon: number + isConnected: boolean + joinedAt: string } export interface GameState { @@ -25,6 +34,33 @@ export interface GameState { allPlayersPeeked: boolean } +// New types for room-based architecture +export interface Room { + id: string + players: Player[] + games: Record // Active games mapped by game ID + gameHistory: GameResult[] + createdAt: string + lastActivity: string +} + +export interface GameResult { + gameId: string + winner: string + finalScores: FinalScore[] + completedAt: string +} + +export interface FinalScore { + playerName: string + score: number +} + +export interface GameContext { + roomId: string + gameId: string +} + export const CARD_VALUES: Record = { 'A': 1, '2': 2, '3': 3, '4': 4, '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, '10': 10, 'J': 0, 'Q': 10, 'K': 10 diff --git a/src/utils/networkAdapter.ts b/src/utils/networkAdapter.ts index 1679692..bda209d 100644 --- a/src/utils/networkAdapter.ts +++ b/src/utils/networkAdapter.ts @@ -3,7 +3,7 @@ import { NetworkManager } from './networkManager' import { ThoughtsNetworkPlugin } from '@/plugins/thoughtsNetworkPlugin' import { GolfNetworkPlugin } from '@/plugins/golfNetworkPlugin' import type { GameState } from '@/types/game' -import type { GameState as GolfGameState } from '@/types/golf' +import type { GameState as GolfGameState, Room, FinalScore } from '@/types/golf' import { ConnectionState, BaseNetworkMessage } from '@/types/network' // Factory function to create a network manager with pre-registered plugins @@ -102,12 +102,16 @@ export class GolfNetworkAdapter { private _playerId: string | null = null private _gameState: GolfGameState | null = null + private _roomState: Room | null = null + constructor(callbacks?: { + onRoomJoined?: (playerId: string, roomState: Room) => void onGameJoined?: (playerId: string, gameState: GolfGameState) => void onGameStateUpdate?: (gameState: GolfGameState) => void + onRoomStateUpdate?: (roomState: Room) => void onNotification?: (message: string) => void onConnectionChange?: (connected: boolean) => void - onGameEnded?: (winner: string, finalScores: Array<{ playerName: string; score: number }>) => void + onGameEnded?: (winner: string, finalScores: FinalScore[]) => void }) { // Create manager with connection state callback this.manager = new NetworkManager({ @@ -120,6 +124,11 @@ export class GolfNetworkAdapter { // Create plugin with game callbacks this.plugin = new GolfNetworkPlugin({ + onRoomJoined: (playerId, roomState) => { + this._playerId = playerId + this._roomState = roomState + callbacks?.onRoomJoined?.(playerId, roomState) + }, onGameJoined: (playerId, gameState) => { this._playerId = playerId this._gameState = gameState @@ -129,6 +138,10 @@ export class GolfNetworkAdapter { this._gameState = gameState callbacks?.onGameStateUpdate?.(gameState) }, + onRoomStateUpdate: (roomState) => { + this._roomState = roomState + callbacks?.onRoomStateUpdate?.(roomState) + }, onNotification: callbacks?.onNotification, onGameEnded: callbacks?.onGameEnded }) @@ -163,6 +176,10 @@ export class GolfNetworkAdapter { return this._gameState } + get roomState(): Room | null { + return this._roomState + } + // Create context helper private getContext() { return { @@ -178,12 +195,20 @@ export class GolfNetworkAdapter { } // Delegate game actions to plugin - createGame(): void { - this.plugin.createGame(this.getContext()) + createRoom(): void { + this.plugin.createRoom(this.getContext()) } - joinGame(gameId: string): void { - this.plugin.joinGame(gameId, this.getContext()) + createGame(roomId: string): void { + this.plugin.createGame(roomId, this.getContext()) + } + + joinRoom(roomId: string): void { + this.plugin.joinRoom(roomId, this.getContext()) + } + + joinGame(roomId: string, gameId: string): void { + this.plugin.joinGame(roomId, gameId, this.getContext()) } startGame(): void { @@ -218,6 +243,10 @@ export class GolfNetworkAdapter { this.plugin.hideCards(this.getContext()) } + startNewGame(): void { + this.plugin.startNewGame(this.getContext()) + } + isMyTurn(): boolean { return this.plugin.isMyTurn(this.getContext()) }