fix: update matching game for UUID player system

- GamePhase: Convert Map to array, map numeric IDs to players
- GameCard: Use active players array for emoji lookup
- PlayerStatusBar: Fix Map.filter, fix duplicate className, fix type comparison
- ResultsPhase: Convert to array-based player lookup, add numericId mapping
- MemoryPairsContext: Create compatibility layer for numeric player IDs
- EmojiPicker: Update import path for PLAYER_EMOJIS
- EmojiPicker test: Update import path

Maintains backward compatibility with internal numeric player tracking
while using UUID-based players from GameModeContext
This commit is contained in:
Thomas Hallock
2025-10-04 17:06:55 -05:00
parent ae4e8fcb5a
commit 2e041ddc44
7 changed files with 65 additions and 60 deletions

View File

@@ -1,7 +1,7 @@
'use client'
import { useState, useMemo } from 'react'
import { PLAYER_EMOJIS } from '../../../../contexts/UserProfileContext'
import { PLAYER_EMOJIS } from '../../../../constants/playerEmojis'
import { css } from '../../../../../styled-system/css'
import emojiData from 'emojibase-data/en/data.json'
@@ -24,7 +24,7 @@ interface EmojiPickerProps {
currentEmoji: string
onEmojiSelect: (emoji: string) => void
onClose: () => void
playerNumber: 1 | 2 | 3 | 4
playerNumber: number
}
// Emoji group categories from emojibase (matching Unicode CLDR group IDs)

View File

@@ -2,13 +2,18 @@
import { AbacusReact } from '@soroban/abacus-react'
import { useAbacusConfig } from '@soroban/abacus-react'
import { useUserProfile } from '../../../../contexts/UserProfileContext'
import { useGameMode } from '../../../../contexts/GameModeContext'
import type { GameCardProps } from '../context/types'
import { css } from '../../../../../styled-system/css'
export function GameCard({ card, isFlipped, isMatched, onClick, disabled = false }: GameCardProps) {
const appConfig = useAbacusConfig()
const { profile } = useUserProfile()
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
// Get active players array for mapping numeric IDs to actual players
const activePlayers = Array.from(activePlayerIds)
.map(id => playerMap.get(id))
.filter((p): p is NonNullable<typeof p> => p !== undefined)
const cardBackStyles = css({
position: 'absolute',
@@ -71,11 +76,9 @@ export function GameCard({ card, isFlipped, isMatched, onClick, disabled = false
const getCardBackIcon = () => {
if (isMatched) {
// Show player emoji for matched cards in two-player mode
if (card.matchedBy === 1) {
return profile.player1Emoji
} else if (card.matchedBy === 2) {
return profile.player2Emoji
// Show player emoji for matched cards in multiplayer mode
if (card.matchedBy && card.matchedBy <= activePlayers.length) {
return activePlayers[card.matchedBy - 1]?.emoji || '✓'
}
return '✓' // Default checkmark for single player
}
@@ -227,7 +230,9 @@ export function GameCard({ card, isFlipped, isMatched, onClick, disabled = false
animation: 'emojiBlast 0.8s cubic-bezier(0.68, -0.55, 0.265, 1.55) 0.4s both',
filter: 'drop-shadow(0 0 8px rgba(255,255,255,0.8))'
})}>
{card.matchedBy === 1 ? profile.player1Emoji : profile.player2Emoji}
{card.matchedBy && card.matchedBy <= activePlayers.length
? activePlayers[card.matchedBy - 1]?.emoji || '✓'
: '✓'}
</span>
</div>

View File

@@ -9,11 +9,17 @@ import { pluralizeWord } from '../../../../utils/pluralization'
export function GamePhase() {
const { state, resetGame, activePlayers } = useMemoryPairs()
const { players } = useGameMode()
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
// Get the current player from the arena champions
const currentPlayerData = players.find(p => p.id === state.currentPlayer)
const activePlayerData = players.filter(p => activePlayers.includes(p.id))
// Convert Map to array and create mapping from numeric index to player
const playersArray = Array.from(playerMap.values())
const activePlayersArray = Array.from(activePlayerIds)
.map(id => playerMap.get(id))
.filter((p): p is NonNullable<typeof p> => p !== undefined)
// Map numeric player ID (1, 2, 3...) to actual player data
const currentPlayerData = activePlayersArray[state.currentPlayer - 1]
const activePlayerData = activePlayersArray
return (
<div className={css({

View File

@@ -11,24 +11,23 @@ interface PlayerStatusBarProps {
}
export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
const { players } = useGameMode()
const { profile } = useUserProfile()
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
const { state } = useMemoryPairs()
// Get active players with their profile data
const activePlayers = players
.filter(player => player.isActive)
.map(player => ({
...player,
displayName: player.id === 1 ? profile.player1Name :
player.id === 2 ? profile.player2Name :
player.name,
displayEmoji: player.id === 1 ? profile.player1Emoji :
player.id === 2 ? profile.player2Emoji :
player.emoji,
score: state.scores[player.id] || 0,
consecutiveMatches: state.consecutiveMatches?.[player.id] || 0
}))
// Get active players array
const activePlayersData = Array.from(activePlayerIds)
.map(id => playerMap.get(id))
.filter((p): p is NonNullable<typeof p> => p !== undefined)
// Map active players to display data with scores
// State uses numeric player IDs (1, 2, 3...), so we map by index
const activePlayers = activePlayersData.map((player, index) => ({
...player,
displayName: player.name,
displayEmoji: player.emoji,
score: state.scores[index + 1] || 0,
consecutiveMatches: state.consecutiveMatches?.[index + 1] || 0
}))
// Get celebration level based on consecutive matches
const getCelebrationLevel = (consecutiveMatches: number) => {
@@ -41,7 +40,7 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
if (activePlayers.length <= 1) {
// Simple single player indicator
return (
<div className={css({
<div className={`${css({
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
@@ -52,8 +51,7 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
borderColor: 'blue.200',
mb: { base: '2', md: '3' },
boxShadow: '0 2px 4px rgba(0,0,0,0.1)'
})}
className={className}>
})} ${className || ''}`}>
<div className={css({
display: 'flex',
alignItems: 'center',
@@ -85,15 +83,14 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
// For multiplayer, show competitive status bar
return (
<div className={css({
<div className={`${css({
background: 'linear-gradient(135deg, #f8fafc, #e2e8f0)',
rounded: 'xl',
p: { base: '2', md: '3' },
border: '2px solid',
borderColor: 'gray.200',
mb: { base: '3', md: '4' }
})}
className={className}>
})} ${className || ''}`}>
<div className={css({
display: 'grid',
gridTemplateColumns: activePlayers.length <= 2
@@ -105,7 +102,7 @@ export function PlayerStatusBar({ className }: PlayerStatusBarProps) {
alignItems: 'center'
})}>
{activePlayers.map((player, index) => {
const isCurrentPlayer = player.id === state.currentPlayer
const isCurrentPlayer = (index + 1) === state.currentPlayer
const isLeading = player.score === Math.max(...activePlayers.map(p => p.score)) && player.score > 0
const celebrationLevel = getCelebrationLevel(player.consecutiveMatches)

View File

@@ -9,21 +9,18 @@ import { css } from '../../../../../styled-system/css'
export function ResultsPhase() {
const router = useRouter()
const { state, resetGame, activePlayers } = useMemoryPairs()
const { players } = useGameMode()
const { profile } = useUserProfile()
const { state, resetGame, activePlayers, gameMode } = useMemoryPairs()
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
// Get active player data with profile information
const activePlayerData = players
.filter(p => activePlayers.includes(p.id))
.map(player => ({
// Get active player data array
const activePlayerData = Array.from(activePlayerIds)
.map(id => playerMap.get(id))
.filter((p): p is NonNullable<typeof p> => p !== undefined)
.map((player, index) => ({
...player,
displayName: player.id === 1 ? profile.player1Name :
player.id === 2 ? profile.player2Name :
player.name,
displayEmoji: player.id === 1 ? profile.player1Emoji :
player.id === 2 ? profile.player2Emoji :
player.emoji
displayName: player.name,
displayEmoji: player.emoji,
numericId: index + 1 // For compatibility with state.scores
}))
const gameTime = state.gameEndTime && state.gameStartTime
@@ -31,7 +28,7 @@ export function ResultsPhase() {
: 0
const analysis = getPerformanceAnalysis(state)
const multiplayerResult = state.gameMode === 'multiplayer' ? getMultiplayerWinner(state, activePlayers) : null
const multiplayerResult = gameMode === 'multiplayer' ? getMultiplayerWinner(state, activePlayers) : null
return (
<div className={css({
@@ -52,7 +49,7 @@ export function ResultsPhase() {
🎉 Game Complete! 🎉
</h2>
{state.gameMode === 'single' ? (
{gameMode === 'single' ? (
<p className={css({
fontSize: '24px',
color: 'gray.700',
@@ -76,7 +73,7 @@ export function ResultsPhase() {
color: 'blue.600',
fontWeight: 'bold'
})}>
🏆 {activePlayerData.find(p => p.id === multiplayerResult.winners[0])?.displayName || `Player ${multiplayerResult.winners[0]}`} Wins!
🏆 {activePlayerData.find(p => p.numericId === multiplayerResult.winners[0])?.displayName || `Player ${multiplayerResult.winners[0]}`} Wins!
</p>
) : (
<p className={css({
@@ -180,7 +177,7 @@ export function ResultsPhase() {
</div>
{/* Multiplayer Scores */}
{state.gameMode === 'multiplayer' && multiplayerResult && (
{gameMode === 'multiplayer' && multiplayerResult && (
<div className={css({
display: 'flex',
justifyContent: 'center',
@@ -189,8 +186,8 @@ export function ResultsPhase() {
flexWrap: 'wrap'
})}>
{activePlayerData.map((player) => {
const score = multiplayerResult.scores[player.id] || 0
const isWinner = multiplayerResult.winners.includes(player.id)
const score = multiplayerResult.scores[player.numericId] || 0
const isWinner = multiplayerResult.winners.includes(player.numericId)
return (
<div key={player.id} className={css({

View File

@@ -1,7 +1,7 @@
import { render, screen, fireEvent } from '@testing-library/react'
import { describe, test, expect, beforeEach, vi } from 'vitest'
import { EmojiPicker } from '../EmojiPicker'
import { PLAYER_EMOJIS } from '../../../../../contexts/UserProfileContext'
import { PLAYER_EMOJIS } from '../../../../../constants/playerEmojis'
// Mock the emoji keywords function for testing
vi.mock('emojibase-data/en/data.json', () => ({

View File

@@ -251,10 +251,10 @@ const MemoryPairsContext = createContext<MemoryPairsContextValue | null>(null)
// Provider component
export function MemoryPairsProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(memoryPairsReducer, initialState)
const { activePlayerCount, players } = useGameMode()
const { activePlayerCount, activePlayers: activePlayerIds } = useGameMode()
// Get active players from GameMode context
const activePlayers = players.filter(player => player.isActive).map(player => player.id)
// Get active player IDs as numbers (convert from string IDs for now to maintain compatibility)
const activePlayers = Array.from(activePlayerIds).map((id, index) => index + 1)
// Derive game mode from active player count
const gameMode = activePlayerCount > 1 ? 'multiplayer' : 'single'