7.9 KiB
Matching Game Stats Integration Guide
Quick Reference
Files to modify: src/arcade-games/matching/components/ResultsPhase.tsx
What we're adding: Call useRecordGameResult() when game completes to save per-player stats.
Current State Analysis
ResultsPhase.tsx (lines 9-29)
Already has all the data we need:
const { state, resetGame, activePlayers, gameMode, exitSession } =
useMatching();
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode();
const gameTime =
state.gameEndTime && state.gameStartTime
? state.gameEndTime - state.gameStartTime
: 0;
const analysis = getPerformanceAnalysis(state);
const multiplayerResult =
gameMode === "multiplayer"
? getMultiplayerWinner(state, activePlayers)
: null;
Available data:
- ✅
state.scores- scores by player ID - ✅
state.gameStartTime,state.gameEndTime- timing - ✅
state.matchedPairs,state.totalPairs- completion - ✅
state.moves- total moves - ✅
activePlayers- array of player IDs - ✅
multiplayerResult.winners- who won - ✅
analysis.statistics.accuracy- accuracy percentage
Implementation Steps
Step 1: Add state flag to prevent duplicate recording
Add recorded: boolean to MatchingState type:
// src/arcade-games/matching/types.ts (add to MatchingState interface)
export interface MatchingState extends GameState {
// ... existing fields ...
// Stats recording
recorded?: boolean; // ← ADD THIS
}
Step 2: Import the hook in ResultsPhase.tsx
// At top of src/arcade-games/matching/components/ResultsPhase.tsx
import { useEffect } from "react"; // ← ADD if not present
import { useRecordGameResult } from "@/hooks/useRecordGameResult";
import type { GameResult } from "@/lib/arcade/stats/types";
Step 3: Call the hook
// Inside ResultsPhase component, after existing hooks
export function ResultsPhase() {
const router = useRouter()
const { state, resetGame, activePlayers, gameMode, exitSession } = useMatching()
const { players: playerMap, activePlayers: activePlayerIds } = useGameMode()
// ← ADD THIS
const { mutate: recordGame, isPending: isRecording } = useRecordGameResult()
// ... existing code ...
Step 4: Record game result on mount
Add this useEffect after the hook declarations:
// Record game result once when entering results phase
useEffect(() => {
// Only record if we haven't already
if (state.phase === "results" && !state.recorded && !isRecording) {
const gameTime =
state.gameEndTime && state.gameStartTime
? state.gameEndTime - state.gameStartTime
: 0;
const analysis = getPerformanceAnalysis(state);
const multiplayerResult =
gameMode === "multiplayer"
? getMultiplayerWinner(state, activePlayers)
: null;
// Build GameResult
const gameResult: GameResult = {
gameType:
state.gameType === "abacus-numeral"
? "matching-abacus"
: "matching-complements",
completedAt: state.gameEndTime || Date.now(),
duration: gameTime,
playerResults: activePlayers.map((playerId) => {
const score = state.scores[playerId] || 0;
const won = multiplayerResult
? multiplayerResult.winners.includes(playerId)
: state.matchedPairs === state.totalPairs; // Solo = completed
// In multiplayer, calculate per-player accuracy from their score
// In single player, use overall accuracy
const playerAccuracy =
gameMode === "multiplayer"
? score / state.totalPairs // Their score as fraction of total pairs
: analysis.statistics.accuracy / 100; // Convert percentage to 0-1
return {
playerId,
won,
score,
accuracy: playerAccuracy,
completionTime: gameTime,
metrics: {
moves: state.moves,
matchedPairs: state.matchedPairs,
difficulty: state.difficulty,
},
};
}),
metadata: {
gameType: state.gameType,
difficulty: state.difficulty,
grade: analysis.grade,
starRating: analysis.starRating,
},
};
// Record to database
recordGame(gameResult, {
onSuccess: (updates) => {
console.log("✅ Stats recorded:", updates);
// Mark as recorded to prevent duplicate saves
// Note: This assumes Provider has a way to update state.recorded
// We'll need to add an action for this
},
onError: (error) => {
console.error("❌ Failed to record stats:", error);
},
});
}
}, [state.phase, state.recorded, isRecording /* ... deps */]);
Step 5: Add loading state UI (optional)
Show a subtle loading indicator while recording:
// At the top of the return statement in ResultsPhase
if (isRecording) {
return (
<div className={css({
textAlign: 'center',
padding: '20px',
})}>
<p>Saving results...</p>
</div>
)
}
Or keep it subtle and just disable buttons:
// On the "Play Again" button
<button
disabled={isRecording}
className={css({
// ... styles ...
opacity: isRecording ? 0.5 : 1,
cursor: isRecording ? 'not-allowed' : 'pointer',
})}
onClick={resetGame}
>
{isRecording ? '💾 Saving...' : '🎮 Play Again'}
</button>
Provider Changes Needed
The Provider needs an action to mark the game as recorded:
// src/arcade-games/matching/Provider.tsx
// Add to the context type
export interface MatchingContextType {
// ... existing ...
markAsRecorded: () => void; // ← ADD THIS
}
// Add to the reducer or state update logic
const markAsRecorded = useCallback(() => {
setState((prev) => ({ ...prev, recorded: true }));
}, []);
// Add to the context value
const contextValue: MatchingContextType = {
// ... existing ...
markAsRecorded,
};
Then in ResultsPhase useEffect:
onSuccess: (updates) => {
console.log("✅ Stats recorded:", updates);
markAsRecorded(); // ← Use this instead
};
Testing Checklist
Solo Game
- Play a game to completion
- Check console for "✅ Stats recorded"
- Refresh page
- Go to
/gamespage - Verify player's gamesPlayed incremented
- Verify player's totalWins incremented (if completed)
Multiplayer Game
- Activate 2+ players
- Play a game to completion
- Check console for stats for ALL players
- Go to
/gamespage - Verify each player's stats updated independently
- Winner should have +1 win
- All players should have +1 games played
Edge Cases
- Incomplete game (exit early) - should NOT record
- Play again from results - should NOT duplicate record
- Network error during save - should show error, not mark as recorded
Common Issues
Issue: Stats recorded multiple times
Cause: useEffect dependency array missing or incorrect
Fix: Ensure state.recorded is in deps and checked in condition
Issue: Can't read property 'id' of undefined
Cause: Player not found in playerMap Fix: Add null checks when mapping activePlayers
Issue: Accuracy is always 100% or 0%
Cause: Wrong calculation or unit (percentage vs decimal) Fix: Ensure accuracy is 0.0 - 1.0, not 0-100
Issue: Single player never "wins"
Cause: Wrong win condition for solo mode
Fix: Solo player wins if they complete all pairs (state.matchedPairs === state.totalPairs)
Next Steps After Integration
- ✅ Verify stats save correctly
- ✅ Update
/gamespage to fetch and display per-player stats - ✅ Test with different game modes and difficulties
- 🔄 Repeat this pattern for other arcade games
- 📊 Add stats visualization/charts (future)
Status: Ready for implementation Blocked by:
- Database schema (player_stats table)
- API endpoints (/api/player-stats/record-game)
- React hooks (useRecordGameResult)