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
-
- Create New Game
+
+ Create New Room
OR
@@ -91,8 +108,8 @@ const GolfGame = ({ onGameIdChange, onPlayerIdChange, onPlayerNameChange, onConn
className={styles.input}
maxLength={6}
/>
-
- Join Game
+
+ Join Room
@@ -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}
+
+
joinGame(gameId)}
+ className={styles.joinGameButton}
+ disabled={game.players.length >= 4 || game.gamePhase !== 'waiting'}
+ >
+ {game.gamePhase === 'waiting' ? 'Join Game' : 'Game In Progress'}
+
+
+ ))}
+
+ ) : (
+
No active games in room
+ )}
+
+
+
+
Start New Game
+
+ createGame()} className={styles.primaryButton}>
+ Create & Join 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
+
+
+ ))}
+
+ )}
+
window.location.reload()}
+ onClick={startNewGame}
className={styles.primaryButton}
>
- Back to Home
+ Start New Game
- Play Again
+ Back to Room
+
+ window.location.reload()}
+ className={styles.tertiaryButton}
+ >
+ Leave Room
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())
}