feat: integrate memory pairs game with arena champions and N-player support

- Update MemoryPairsContext to use GameMode arena champions instead of UserProfile
- Extend game support from 2 players to N players with dynamic player IDs
- Update PlayerScore interface from fixed structure to dynamic object mapping
- Modify GamePhase to display actual arena champion names, emojis, and colors
- Update ResultsPhase to show all arena champions with proper winner calculation
- Add getMultiplayerWinner utility function for N-player winner determination
- Fix TypeScript errors from GameMode type changes (single | multiplayer)
- Ensure proper score tracking and turn switching for any number of players

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Thomas Hallock
2025-09-27 18:44:37 -05:00
parent 9d23e82b5a
commit d9f07d7a4d
7 changed files with 155 additions and 107 deletions

View File

@@ -1,13 +1,17 @@
'use client'
import { useMemoryPairs } from '../context/MemoryPairsContext'
import { useUserProfile } from '../../../../contexts/UserProfileContext'
import { useGameMode } from '../../../../contexts/GameModeContext'
import { MemoryGrid } from './MemoryGrid'
import { css } from '../../../../../styled-system/css'
export function GamePhase() {
const { state, resetGame } = useMemoryPairs()
const { profile } = useUserProfile()
const { state, resetGame, activePlayers } = useMemoryPairs()
const { players } = 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))
return (
<div className={css({
@@ -73,7 +77,7 @@ export function GamePhase() {
</span>
</div>
{state.gameMode === 'two-player' && (
{state.gameMode === 'multiplayer' && (
<div className={css({
display: 'flex',
alignItems: 'center',
@@ -85,7 +89,7 @@ export function GamePhase() {
})}>
<span className={css({ fontSize: '20px' })}></span>
<span className={css({ fontWeight: 'bold', color: 'gray.700' })}>
Two Players
{activePlayers.length} Players
</span>
</div>
)}
@@ -129,8 +133,8 @@ export function GamePhase() {
</div>
</button>
{/* Timer (if two-player mode) */}
{state.gameMode === 'two-player' && (
{/* Timer (if multiplayer mode) */}
{state.gameMode === 'multiplayer' && (
<div className={css({
display: 'flex',
alignItems: 'center',
@@ -149,8 +153,8 @@ export function GamePhase() {
</div>
</div>
{/* Current Player Indicator (Two-Player Mode) */}
{state.gameMode === 'two-player' && (
{/* Current Player Indicator (Multiplayer Mode) */}
{state.gameMode === 'multiplayer' && currentPlayerData && (
<div className={css({
marginTop: '16px',
textAlign: 'center'
@@ -160,9 +164,7 @@ export function GamePhase() {
alignItems: 'center',
gap: '12px',
padding: '12px 24px',
background: state.currentPlayer === 1
? 'linear-gradient(135deg, #74b9ff, #0984e3)'
: 'linear-gradient(135deg, #fd79a8, #e84393)',
background: `linear-gradient(135deg, ${currentPlayerData.color}, ${currentPlayerData.color}dd)`,
color: 'white',
borderRadius: '20px',
fontSize: '18px',
@@ -170,11 +172,11 @@ export function GamePhase() {
boxShadow: '0 4px 12px rgba(0,0,0,0.2)'
})}>
<span className={css({ fontSize: '48px' })}>
{state.currentPlayer === 1 ? profile.player1Emoji : profile.player2Emoji}
{currentPlayerData.emoji}
</span>
<span>Your Turn</span>
<span>{currentPlayerData.name}'s Turn</span>
<span className={css({ fontSize: '24px' })}>
{state.currentPlayer === 1 ? '🎯' : '🎮'}
🎯
</span>
</div>
</div>
@@ -205,7 +207,7 @@ export function GamePhase() {
}
</p>
{state.gameMode === 'two-player' && (
{state.gameMode === 'multiplayer' && (
<p className={css({
fontSize: '14px',
color: 'gray.500',

View File

@@ -86,8 +86,8 @@ export function MemoryGrid() {
</div>
</div>
{/* Two-Player Scores */}
{state.gameMode === 'two-player' && (
{/* Multiplayer Scores */}
{state.gameMode === 'multiplayer' && (
<div className={css({ display: 'flex', alignItems: 'center', gap: '24px' })}>
<button
className={css({
@@ -115,7 +115,7 @@ export function MemoryGrid() {
{profile.player1Emoji}
</div>
<div className={css({ fontSize: '28px', fontWeight: 'bold', color: 'blue.600' })}>
{state.scores.player1}
{state.scores[1] || 0}
</div>
<div className={css({ fontSize: '12px', color: 'gray.600', marginTop: '4px' })}>
Click to change character
@@ -156,7 +156,7 @@ export function MemoryGrid() {
{profile.player2Emoji}
</div>
<div className={css({ fontSize: '28px', fontWeight: 'bold', color: 'red.600' })}>
{state.scores.player2}
{state.scores[2] || 0}
</div>
<div className={css({ fontSize: '12px', color: 'gray.600', marginTop: '4px' })}>
Click to change character

View File

@@ -1,20 +1,23 @@
'use client'
import { useMemoryPairs } from '../context/MemoryPairsContext'
import { useUserProfile } from '../../../../contexts/UserProfileContext'
import { formatGameTime, getTwoPlayerWinner, getPerformanceAnalysis } from '../utils/gameScoring'
import { useGameMode } from '../../../../contexts/GameModeContext'
import { formatGameTime, getMultiplayerWinner, getPerformanceAnalysis } from '../utils/gameScoring'
import { css } from '../../../../../styled-system/css'
export function ResultsPhase() {
const { state, resetGame } = useMemoryPairs()
const { profile } = useUserProfile()
const { state, resetGame, activePlayers } = useMemoryPairs()
const { players } = useGameMode()
// Get active player data
const activePlayerData = players.filter(p => activePlayers.includes(p.id))
const gameTime = state.gameEndTime && state.gameStartTime
? state.gameEndTime - state.gameStartTime
: 0
const analysis = getPerformanceAnalysis(state)
const twoPlayerResult = state.gameMode === 'two-player' ? getTwoPlayerWinner(state) : null
const multiplayerResult = state.gameMode === 'multiplayer' ? getMultiplayerWinner(state, activePlayers) : null
return (
<div className={css({
@@ -43,23 +46,31 @@ export function ResultsPhase() {
})}>
Congratulations on completing the memory challenge!
</p>
) : twoPlayerResult && (
) : multiplayerResult && (
<div className={css({ marginBottom: '20px' })}>
{twoPlayerResult.winner === 'tie' ? (
{multiplayerResult.isTie ? (
<p className={css({
fontSize: '24px',
color: 'purple.600',
fontWeight: 'bold'
})}>
🤝 It's a tie! Both players are memory champions!
🤝 It's a tie! All champions are memory masters!
</p>
) : (
) : multiplayerResult.winners.length === 1 ? (
<p className={css({
fontSize: '24px',
color: 'blue.600',
fontWeight: 'bold'
})}>
🏆 Player {twoPlayerResult.winner} Wins!
🏆 {activePlayerData.find(p => p.id === multiplayerResult.winners[0])?.name || `Player ${multiplayerResult.winners[0]}`} Wins!
</p>
) : (
<p className={css({
fontSize: '24px',
color: 'purple.600',
fontWeight: 'bold'
})}>
🏆 {multiplayerResult.winners.length} Champions tied for victory!
</p>
)}
</div>
@@ -154,51 +165,45 @@ export function ResultsPhase() {
</div>
</div>
{/* Two-Player Scores */}
{state.gameMode === 'two-player' && twoPlayerResult && (
{/* Multiplayer Scores */}
{state.gameMode === 'multiplayer' && multiplayerResult && (
<div className={css({
display: 'flex',
justifyContent: 'center',
gap: '20px',
marginBottom: '40px'
marginBottom: '40px',
flexWrap: 'wrap'
})}>
<div className={css({
background: twoPlayerResult.winner === 1 ? 'linear-gradient(135deg, #ffd700, #ff8c00)' : 'linear-gradient(135deg, #c0c0c0, #808080)',
color: 'white',
padding: '20px',
borderRadius: '16px',
textAlign: 'center',
minWidth: '150px'
})}>
<div className={css({ fontSize: '48px', marginBottom: '8px' })}>
{profile.player1Emoji}
</div>
<div className={css({ fontSize: '36px', fontWeight: 'bold' })}>
{state.scores.player1}
</div>
{twoPlayerResult.winner === 1 && (
<div className={css({ fontSize: '24px' })}>👑</div>
)}
</div>
{activePlayerData.map((player) => {
const score = multiplayerResult.scores[player.id] || 0
const isWinner = multiplayerResult.winners.includes(player.id)
<div className={css({
background: twoPlayerResult.winner === 2 ? 'linear-gradient(135deg, #ffd700, #ff8c00)' : 'linear-gradient(135deg, #c0c0c0, #808080)',
color: 'white',
padding: '20px',
borderRadius: '16px',
textAlign: 'center',
minWidth: '150px'
})}>
<div className={css({ fontSize: '48px', marginBottom: '8px' })}>
{profile.player2Emoji}
</div>
<div className={css({ fontSize: '36px', fontWeight: 'bold' })}>
{state.scores.player2}
</div>
{twoPlayerResult.winner === 2 && (
<div className={css({ fontSize: '24px' })}>👑</div>
)}
</div>
return (
<div key={player.id} className={css({
background: isWinner
? 'linear-gradient(135deg, #ffd700, #ff8c00)'
: 'linear-gradient(135deg, #c0c0c0, #808080)',
color: 'white',
padding: '20px',
borderRadius: '16px',
textAlign: 'center',
minWidth: '150px'
})}>
<div className={css({ fontSize: '48px', marginBottom: '8px' })}>
{player.emoji}
</div>
<div className={css({ fontSize: '14px', marginBottom: '4px', opacity: 0.9 })}>
{player.name}
</div>
<div className={css({ fontSize: '36px', fontWeight: 'bold' })}>
{score}
</div>
{isWinner && (
<div className={css({ fontSize: '24px' })}>👑</div>
)}
</div>
)
})}
</div>
)}

View File

@@ -33,14 +33,15 @@ export function SetupPhase() {
state,
setGameType,
setDifficulty,
dispatch
dispatch,
activePlayers
} = useMemoryPairs()
const { activePlayerCount, gameMode: globalGameMode } = useGameMode()
const handleStartGame = () => {
const cards = generateGameCards(state.gameType, state.difficulty)
dispatch({ type: 'START_GAME', cards })
dispatch({ type: 'START_GAME', cards, activePlayers })
}
const getButtonStyles = (isSelected: boolean, variant: 'primary' | 'secondary' | 'difficulty' = 'primary') => {

View File

@@ -10,7 +10,8 @@ import type {
MemoryPairsContextValue,
GameCard,
GameStatistics,
CelebrationAnimation
CelebrationAnimation,
PlayerScore
} from './types'
// Initial state (gameMode removed - now derived from global context)
@@ -31,7 +32,8 @@ const initialState: MemoryPairsState = {
matchedPairs: 0,
totalPairs: 6,
moves: 0,
scores: { player1: 0, player2: 0 },
scores: {},
activePlayers: [],
// Timing
gameStartTime: null,
@@ -71,6 +73,12 @@ function memoryPairsReducer(state: MemoryPairsState, action: MemoryPairsAction):
}
case 'START_GAME':
// Initialize scores for all active players
const scores: PlayerScore = {}
action.activePlayers.forEach(playerId => {
scores[playerId] = 0
})
return {
...state,
gamePhase: 'playing',
@@ -79,8 +87,9 @@ function memoryPairsReducer(state: MemoryPairsState, action: MemoryPairsAction):
flippedCards: [],
matchedPairs: 0,
moves: 0,
scores: { player1: 0, player2: 0 },
currentPlayer: 1,
scores,
activePlayers: action.activePlayers,
currentPlayer: action.activePlayers[0] || 1,
gameStartTime: Date.now(),
gameEndTime: null,
currentMoveStartTime: Date.now(),
@@ -123,8 +132,7 @@ function memoryPairsReducer(state: MemoryPairsState, action: MemoryPairsAction):
const newMatchedPairs = state.matchedPairs + 1
const newScores = {
...state.scores,
[`player${state.currentPlayer}` as keyof typeof state.scores]:
state.scores[`player${state.currentPlayer}` as keyof typeof state.scores] + 1
[state.currentPlayer]: (state.scores[state.currentPlayer] || 0) + 1
}
// Check if game is complete
@@ -141,7 +149,7 @@ function memoryPairsReducer(state: MemoryPairsState, action: MemoryPairsAction):
gamePhase: isGameComplete ? 'results' : 'playing',
gameEndTime: isGameComplete ? Date.now() : null,
isProcessingMove: false
// Note: Player keeps turn after successful match in two-player mode
// Note: Player keeps turn after successful match in multiplayer mode
}
}
@@ -157,11 +165,15 @@ function memoryPairsReducer(state: MemoryPairsState, action: MemoryPairsAction):
}
}
case 'SWITCH_PLAYER':
case 'SWITCH_PLAYER': {
// Cycle through all active players
const currentIndex = state.activePlayers.indexOf(state.currentPlayer)
const nextIndex = (currentIndex + 1) % state.activePlayers.length
return {
...state,
currentPlayer: state.currentPlayer === 1 ? 2 : 1
currentPlayer: state.activePlayers[nextIndex] || state.activePlayers[0]
}
}
case 'ADD_CELEBRATION':
return {
@@ -221,10 +233,13 @@ const MemoryPairsContext = createContext<MemoryPairsContextValue | null>(null)
// Provider component
export function MemoryPairsProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(memoryPairsReducer, initialState)
const { activePlayerCount } = useGameMode()
const { activePlayerCount, players } = useGameMode()
// Get active players from GameMode context
const activePlayers = players.filter(player => player.isActive).map(player => player.id)
// Derive game mode from active player count
const gameMode = activePlayerCount > 1 ? 'two-player' : 'single'
const gameMode = activePlayerCount > 1 ? 'multiplayer' : 'single'
// Handle card matching logic when two cards are flipped
useEffect(() => {
@@ -240,8 +255,8 @@ export function MemoryPairsProvider({ children }: { children: ReactNode }) {
dispatch({ type: 'MATCH_FOUND', cardIds: [card1.id, card2.id] })
} else {
dispatch({ type: 'MATCH_FAILED', cardIds: [card1.id, card2.id] })
// Switch player only in two-player mode
if (gameMode === 'two-player') {
// Switch player only in multiplayer mode
if (gameMode === 'multiplayer') {
dispatch({ type: 'SWITCH_PLAYER' })
}
}
@@ -292,7 +307,7 @@ export function MemoryPairsProvider({ children }: { children: ReactNode }) {
// Action creators
const startGame = () => {
const cards = generateGameCards(state.gameType, state.difficulty)
dispatch({ type: 'START_GAME', cards })
dispatch({ type: 'START_GAME', cards, activePlayers })
}
const flipCard = (cardId: string) => {
@@ -325,7 +340,8 @@ export function MemoryPairsProvider({ children }: { children: ReactNode }) {
resetGame,
setGameType,
setDifficulty,
gameMode // Expose derived gameMode
gameMode, // Expose derived gameMode
activePlayers // Expose active players
}
return (

View File

@@ -1,11 +1,11 @@
// TypeScript interfaces for Memory Pairs Challenge game
export type GameMode = 'single' | 'two-player'
export type GameMode = 'single' | 'multiplayer'
export type GameType = 'abacus-numeral' | 'complement-pairs'
export type GamePhase = 'setup' | 'playing' | 'results'
export type CardType = 'abacus' | 'number' | 'complement'
export type Difficulty = 6 | 8 | 12 | 15 // Number of pairs
export type Player = 1 | 2
export type Player = number // Now supports any player ID
export type TargetSum = 5 | 10 | 20
export interface GameCard {
@@ -20,8 +20,7 @@ export interface GameCard {
}
export interface PlayerScore {
player1: number
player2: number
[playerId: number]: number
}
export interface CelebrationAnimation {
@@ -59,6 +58,7 @@ export interface MemoryPairsState {
totalPairs: number
moves: number
scores: PlayerScore
activePlayers: Player[] // Track active player IDs
// Timing
gameStartTime: number | null
@@ -77,7 +77,7 @@ export type MemoryPairsAction =
| { type: 'SET_GAME_TYPE'; gameType: GameType }
| { type: 'SET_DIFFICULTY'; difficulty: Difficulty }
| { type: 'SET_TURN_TIMER'; timer: number }
| { type: 'START_GAME'; cards: GameCard[] }
| { type: 'START_GAME'; cards: GameCard[]; activePlayers: Player[] }
| { type: 'FLIP_CARD'; cardId: string }
| { type: 'MATCH_FOUND'; cardIds: [string, string] }
| { type: 'MATCH_FAILED'; cardIds: [string, string] }
@@ -99,6 +99,7 @@ export interface MemoryPairsContextValue {
canFlipCard: (cardId: string) => boolean
currentGameStatistics: GameStatistics
gameMode: GameMode // Derived from global context
activePlayers: Player[] // Active player IDs from arena
// Actions
startGame: () => void

View File

@@ -65,7 +65,7 @@ export interface Achievement {
earned: boolean
}
export function getAchievements(state: MemoryPairsState, gameMode: 'single' | 'two-player'): Achievement[] {
export function getAchievements(state: MemoryPairsState, gameMode: 'single' | 'multiplayer'): Achievement[] {
const { matchedPairs, totalPairs, moves, scores, gameStartTime, gameEndTime } = state
const accuracy = moves > 0 ? (matchedPairs / moves) * 100 : 0
const gameTime = gameStartTime && gameEndTime ? gameEndTime - gameStartTime : 0
@@ -112,17 +112,17 @@ export function getAchievements(state: MemoryPairsState, gameMode: 'single' | 't
name: 'Two-Player Triumph',
description: 'Win a two-player game',
icon: '👥',
earned: gameMode === 'two-player' && matchedPairs === totalPairs &&
(scores.player1 > scores.player2 || scores.player2 > scores.player1)
earned: gameMode === 'multiplayer' && matchedPairs === totalPairs &&
Object.keys(scores).length > 1 && Math.max(...Object.values(scores)) > 0
},
{
id: 'shutout_victory',
name: 'Shutout Victory',
description: 'Win a two-player game without opponent scoring',
icon: '🛡️',
earned: gameMode === 'two-player' && matchedPairs === totalPairs &&
((scores.player1 === totalPairs && scores.player2 === 0) ||
(scores.player2 === totalPairs && scores.player1 === 0))
earned: gameMode === 'multiplayer' && matchedPairs === totalPairs &&
Object.values(scores).some(score => score === totalPairs) &&
Object.values(scores).some(score => score === 0)
},
{
id: 'comeback_kid',
@@ -259,26 +259,49 @@ export function getTwoPlayerWinner(state: MemoryPairsState): {
} {
const { scores } = state
if (scores.player1 > scores.player2) {
if (scores[1] > scores[2]) {
return {
winner: 1,
winnerScore: scores.player1,
loserScore: scores.player2,
margin: scores.player1 - scores.player2
winnerScore: scores[1],
loserScore: scores[2],
margin: scores[1] - scores[2]
}
} else if (scores.player2 > scores.player1) {
} else if (scores[2] > scores[1]) {
return {
winner: 2,
winnerScore: scores.player2,
loserScore: scores.player1,
margin: scores.player2 - scores.player1
winnerScore: scores[2],
loserScore: scores[1],
margin: scores[2] - scores[1]
}
} else {
return {
winner: 'tie',
winnerScore: scores.player1,
loserScore: scores.player2,
winnerScore: scores[1],
loserScore: scores[2],
margin: 0
}
}
}
// Get multiplayer game winner (supports N players)
export function getMultiplayerWinner(state: MemoryPairsState, activePlayers: Player[]): {
winners: Player[]
winnerScore: number
scores: { [playerId: number]: number }
isTie: boolean
} {
const { scores } = state
// Find the highest score
const maxScore = Math.max(...activePlayers.map(playerId => scores[playerId] || 0))
// Find all players with the highest score
const winners = activePlayers.filter(playerId => (scores[playerId] || 0) === maxScore)
return {
winners,
winnerScore: maxScore,
scores,
isTie: winners.length > 1
}
}