# ๐งฉ **Memory Pairs Challenge - Technical Implementation Plan**
## **๐ Executive Summary**
Port the sophisticated Matching Pairs Challenge from the static HTML games website to a modern React implementation in `apps/web`, following the established patterns from the existing Speed Memory Quiz implementation while maintaining all features and improving UX with TypeScript safety, comprehensive testing, and the existing `@soroban/abacus-react` component system.
## **๐ Architecture Analysis - Improved Component Composition**
After analyzing the existing Speed Memory Quiz implementation and identifying areas for improvement, I've designed a proper component composition approach that prioritizes maintainability, testability, and reusability:
### **Modern Architecture Patterns**
- **Component composition**: Small, focused, testable components
- **Context-based state management**: Centralized state with clean separation
- **useReducer pattern**: Complex state management with typed actions and reducer
- **Phase-based UI**: Setup โ Playing โ Results with conditional rendering
- **Panda CSS**: Using `css()` function for styling with design tokens
- **AbacusDisplayContext**: Integration with existing abacus configuration system
- **Proper separation of concerns**: Logic, UI, and state clearly separated
- **Testable components**: Each component focused on single responsibility
## **๐๏ธ Component Architecture**
### **File Structure** (Component Composition Approach)
```
src/app/games/matching/
โโโ page.tsx # Main page entry point (minimal)
โโโ components/
โ โโโ MemoryPairsGame.tsx # Main game orchestrator
โ โโโ SetupPhase.tsx # Game configuration UI
โ โโโ GamePhase.tsx # Active gameplay UI
โ โโโ ResultsPhase.tsx # Game results and statistics
โ โโโ GameCard.tsx # Individual card component
โ โโโ MemoryGrid.tsx # Grid layout manager
โ โโโ PlayerIndicator.tsx # Player status display
โ โโโ GameControls.tsx # Game action buttons
โโโ context/
โ โโโ MemoryPairsContext.tsx # Game state management
โ โโโ types.ts # TypeScript interfaces
โโโ hooks/
โ โโโ useMemoryPairsGame.tsx # Custom game logic hook
โ โโโ useCardMatching.tsx # Card matching logic
โ โโโ useGameTimer.tsx # Timer functionality
โโโ utils/
โโโ cardGeneration.ts # Card generation utilities
โโโ matchValidation.ts # Match validation logic
โโโ gameScoring.ts # Scoring calculations
src/lib/
โโโ memory-pairs-utils.ts # Shared game utilities
โโโ memory-pairs-utils.test.ts # Unit tests for utilities
โโโ [existing] memory-quiz-utils.ts # Reference implementation
```
### **State Management** (Following memory-quiz reducer pattern)
```typescript
interface MemoryPairsState {
// Core game data
cards: GameCard[];
gameCards: GameCard[];
flippedCards: GameCard[];
// Game configuration
gameMode: "single" | "two-player";
gameType: "abacus-numeral" | "complement-pairs";
difficulty: 6 | 8 | 12 | 15; // Number of pairs
turnTimer: number; // For two-player mode
// Game progression
currentPlayer: 1 | 2;
matchedPairs: number;
totalPairs: number;
moves: number;
scores: { player1: number; player2: number };
// UI state
gamePhase: "setup" | "playing" | "results";
gameStartTime: number | null;
timerInterval: NodeJS.Timeout | null;
celebrationAnimations: CelebrationAnimation[];
}
type MemoryPairsAction =
| { type: "SET_GAME_MODE"; mode: "single" | "two-player" }
| { type: "SET_GAME_TYPE"; gameType: "abacus-numeral" | "complement-pairs" }
| { type: "SET_DIFFICULTY"; difficulty: 6 | 8 | 12 | 15 }
| { type: "START_GAME"; cards: GameCard[] }
| { type: "FLIP_CARD"; cardId: string }
| { type: "MATCH_FOUND"; cardIds: [string, string] }
| { type: "MATCH_FAILED"; cardIds: [string, string] }
| { type: "SWITCH_PLAYER" }
| { type: "ADD_CELEBRATION"; animation: CelebrationAnimation }
| { type: "SHOW_RESULTS" }
| { type: "RESET_GAME" };
```
## **๐ฎ Core Features Implementation**
### **1. Card System Integration**
#### **GameCard Interface** (Following memory-quiz QuizCard pattern)
```typescript
interface GameCard {
id: string;
type: "abacus" | "number" | "complement";
number: number;
complement?: number; // For complement pairs
targetSum?: 5 | 10; // For complement pairs
matched: boolean;
matchedBy?: 1 | 2; // For two-player mode
element?: HTMLElement | null; // Following memory-quiz pattern
}
```
#### **AbacusReact Integration** (Following memory-quiz pattern)
```typescript
// In card rendering - following memory-quiz AbacusReact usage
```
### **2. Game Generation Logic**
#### **Abacus-Numeral Mode** (Similar to memory-quiz generateQuizCards)
```typescript
const generateAbacusNumeralCards = (
pairs: number,
appConfig: AbacusDisplayConfig,
): GameCard[] => {
// Generate unique numbers based on difficulty
const numbers = generateUniqueNumbers(pairs, { min: 1, max: 999 });
const cards: GameCard[] = [];
numbers.forEach((number) => {
// Abacus card
cards.push({
id: `abacus_${number}`,
type: "abacus",
number,
matched: false,
});
// Number card
cards.push({
id: `number_${number}`,
type: "number",
number,
matched: false,
});
});
return shuffleArray(cards);
};
```
#### **Complement Pairs Mode**
```typescript
const generateComplementCards = (pairs: number): GameCard[] => {
const complementPairs = [
[0, 5],
[1, 4],
[2, 3], // Friends of 5
[0, 10],
[1, 9],
[2, 8],
[3, 7],
[4, 6],
[5, 5], // Friends of 10
];
const selectedPairs = complementPairs.slice(0, pairs);
const cards: GameCard[] = [];
selectedPairs.forEach(([num1, num2]) => {
const targetSum = num1 + num2;
cards.push(
{
id: `comp1_${num1}_${num2}`,
type: "complement",
number: num1,
complement: num2,
targetSum: targetSum as 5 | 10,
matched: false,
},
{
id: `comp2_${num1}_${num2}`,
type: "complement",
number: num2,
complement: num1,
targetSum: targetSum as 5 | 10,
matched: false,
},
);
});
return shuffleArray(cards);
};
```
### **3. Match Validation Logic**
#### **Validation Utilities** (Following memory-quiz-utils pattern)
```typescript
// lib/memory-pairs-utils.ts
export function validateAbacusNumeralMatch(
card1: GameCard,
card2: GameCard,
): boolean {
return (
card1.number === card2.number &&
card1.type !== card2.type &&
card1.type !== "complement" &&
card2.type !== "complement"
);
}
export function validateComplementMatch(
card1: GameCard,
card2: GameCard,
): boolean {
return (
card1.type === "complement" &&
card2.type === "complement" &&
card1.number === card2.complement &&
card2.number === card1.complement &&
card1.targetSum === card2.targetSum
);
}
export function isValidMatch(card1: GameCard, card2: GameCard): boolean {
if (card1.type === "complement" || card2.type === "complement") {
return validateComplementMatch(card1, card2);
}
return validateAbacusNumeralMatch(card1, card2);
}
```
## **๐จ Visual Design & UX** (Following memory-quiz patterns)
### **Component Implementation Examples**
#### **Context Provider** (Centralized state management)
```typescript
// context/MemoryPairsContext.tsx
export function MemoryPairsProvider({ children }: { children: ReactNode }) {
const [state, dispatch] = useReducer(memoryPairsReducer, initialState)
const contextValue = {
state,
dispatch,
// Derived state
isGameActive: state.gamePhase === 'playing',
canFlipCard: (cardId: string) => canPlayerFlipCard(state, cardId),
// Actions
startGame: () => dispatch({ type: 'START_GAME' }),
flipCard: (cardId: string) => dispatch({ type: 'FLIP_CARD', cardId }),
resetGame: () => dispatch({ type: 'RESET_GAME' })
}
return (
{children}
)
}
```
#### **Setup Phase Component** (Clean separation of concerns)
```typescript
// components/SetupPhase.tsx
export function SetupPhase() {
const { state, dispatch } = useMemoryPairs()
return (
{/* Game mode selection */}
{['single', 'two-player'].map(mode => (
))}
{/* Additional setup controls... */}
)
}
```
### **Card Flip Animation** (Following memory-quiz 3D card patterns)
````typescript
#### **Game Card Component** (Focused single responsibility)
```typescript
// components/GameCard.tsx
export function GameCard({ card, isFlipped, isMatched, onClick }: GameCardProps) {
const appConfig = useAbacusConfig()
return (
{/* Card back */}
{card.type === 'abacus' ? '๐งฎ' : card.type === 'number' ? '๐ข' : '๐ค'}
{/* Card front */}
{card.type === 'abacus' ? (
) : card.type === 'number' ? (
{card.number}
) : (
{card.number}
{card.targetSum === 5 ? 'โ' : '๐'}
)}
)
}
````
### **Responsive Grid System** (Following memory-quiz adaptive grid patterns)
````typescript
#### **Memory Grid Component** (Layout management)
```typescript
// components/MemoryGrid.tsx
export function MemoryGrid() {
const { state, flipCard } = useMemoryPairs()
const getGridClass = (pairs: number) => {
if (pairs <= 6) return 'repeat(4, 1fr)' // 3x4 grid
if (pairs <= 8) return 'repeat(4, 1fr)' // 4x4 grid
if (pairs <= 12) return 'repeat(6, 1fr)' // 4x6 grid
return 'repeat(6, 1fr)' // 5x6 grid
}
const getCardSize = (pairs: number) => {
if (pairs <= 6) return { width: '140px', height: '180px' }
if (pairs <= 8) return { width: '120px', height: '160px' }
if (pairs <= 12) return { width: '100px', height: '140px' }
return { width: '90px', height: '120px' }
}
const gridClass = getGridClass(state.totalPairs)
const cardSize = getCardSize(state.totalPairs)
return (
{state.gameCards.map(card => (
c.id === card.id) || card.matched}
isMatched={card.matched}
onClick={() => onCardClick(card.id)}
/>
))}
)
}
````
## **๐ Advanced Features**
### **Two-Player System** (Enhanced from original)
```typescript
// Two-player logic in reducer
case 'MATCH_FOUND': {
const [card1Id, card2Id] = action.cardIds
const updatedCards = state.gameCards.map(card => {
if (card.id === card1Id || card.id === card2Id) {
return {
...card,
matched: true,
matchedBy: state.currentPlayer
}
}
return card
})
// Player keeps turn after successful match
const newScores = {
...state.scores,
[`player${state.currentPlayer}`]: state.scores[`player${state.currentPlayer}`] + 1
}
return {
...state,
gameCards: updatedCards,
matchedPairs: state.matchedPairs + 1,
scores: newScores,
flippedCards: []
}
}
case 'MATCH_FAILED': {
// Switch players after failed match in two-player mode
return {
...state,
flippedCards: [],
currentPlayer: state.gameMode === 'two-player'
? (state.currentPlayer === 1 ? 2 : 1)
: state.currentPlayer,
moves: state.moves + 1
}
}
```
### **Celebration Effects** (Following memory-quiz animation patterns)
```typescript
// Global animations (following memory-quiz global animations pattern)
const globalAnimations = `
@keyframes cardFlip {
0% { transform: rotateY(0deg); }
100% { transform: rotateY(180deg); }
}
@keyframes matchSuccess {
0% { transform: scale(1); }
50% { transform: scale(1.1); }
100% { transform: scale(1); }
}
@keyframes celebrationBurst {
0% {
opacity: 1;
transform: scale(0.5) rotate(0deg);
}
50% {
opacity: 1;
transform: scale(1.2) rotate(180deg);
}
100% {
opacity: 0;
transform: scale(1.5) rotate(360deg);
}
}
@keyframes invalidMove {
0%, 100% { transform: translateX(0); }
25% { transform: translateX(-5px); }
75% { transform: translateX(5px); }
}
`;
```
## **๐งช Testing Strategy** (Following memory-quiz test patterns)
### **1. Utility Function Tests**
```typescript
// lib/memory-pairs-utils.test.ts (following memory-quiz-utils.test.ts patterns)
describe("memory-pairs-utils", () => {
describe("validateAbacusNumeralMatch", () => {
it("should match same numbers with different types", () => {
const abacusCard: GameCard = {
id: "1",
type: "abacus",
number: 5,
matched: false,
};
const numberCard: GameCard = {
id: "2",
type: "number",
number: 5,
matched: false,
};
expect(validateAbacusNumeralMatch(abacusCard, numberCard)).toBe(true);
});
it("should reject same types", () => {
const card1: GameCard = {
id: "1",
type: "abacus",
number: 5,
matched: false,
};
const card2: GameCard = {
id: "2",
type: "abacus",
number: 5,
matched: false,
};
expect(validateAbacusNumeralMatch(card1, card2)).toBe(false);
});
});
describe("validateComplementMatch", () => {
it("should match friends of 5", () => {
const card1: GameCard = {
id: "1",
type: "complement",
number: 2,
complement: 3,
targetSum: 5,
matched: false,
};
const card2: GameCard = {
id: "2",
type: "complement",
number: 3,
complement: 2,
targetSum: 5,
matched: false,
};
expect(validateComplementMatch(card1, card2)).toBe(true);
});
it("should reject different target sums", () => {
const card1: GameCard = {
id: "1",
type: "complement",
number: 2,
complement: 3,
targetSum: 5,
matched: false,
};
const card2: GameCard = {
id: "2",
type: "complement",
number: 4,
complement: 6,
targetSum: 10,
matched: false,
};
expect(validateComplementMatch(card1, card2)).toBe(false);
});
});
});
```
### **2. Component Integration Tests**
```typescript
// __tests__/memory-pairs.integration.test.tsx
describe('Memory Pairs Game Integration', () => {
it('should complete a single-player abacus-numeral game', async () => {
render()
// Setup game
await user.click(screen.getByText('Abacus-Numeral'))
await user.click(screen.getByText('6 pairs'))
await user.click(screen.getByText('Start Game'))
// Verify game board
await waitFor(() => {
expect(screen.getAllByRole('button')).toHaveLength(12) // 6 pairs = 12 cards
})
// Play game logic...
})
it('should handle two-player mode correctly', async () => {
render()
// Setup two-player game
await user.click(screen.getByText('Two Players'))
await user.click(screen.getByText('Start Game'))
// Verify player indicators
expect(screen.getByText('Player 1')).toBeInTheDocument()
expect(screen.getByText('Score: 0')).toBeInTheDocument()
})
})
```
### **3. E2E Tests** (Following Playwright patterns)
```typescript
// e2e/memory-pairs.spec.ts
test.describe("Memory Pairs Game E2E", () => {
test("should complete full game flow", async ({ page }) => {
await page.goto("/games/matching");
// Game setup
await page.click('[data-testid="game-type-abacus"]');
await page.click('[data-testid="difficulty-6"]');
await page.click('[data-testid="start-game"]');
// Game interaction
const cards = page.locator(".memory-card");
await expect(cards).toHaveCount(12);
// Test card interactions...
});
});
```
## **๐ Documentation & Storybook**
### **Storybook Stories** (Following memory-quiz patterns)
```typescript
// stories/MemoryPairs.stories.tsx
export default {
title: 'Games/Memory Pairs',
component: MemoryPairsPage
} as Meta
export const SinglePlayerAbacus: Story = {
args: {
initialState: {
gameMode: 'single',
gameType: 'abacus-numeral',
difficulty: 6
}
}
}
export const TwoPlayerComplements: Story = {
args: {
initialState: {
gameMode: 'two-player',
gameType: 'complement-pairs',
difficulty: 8
}
}
}
export const CardStates: Story = {
render: () => (
)
}
```
## **๐ Implementation Phases**
### **Phase 1: Core Architecture (Week 1)**
- [ ] Create minimal `apps/web/src/app/games/matching/page.tsx` entry point
- [ ] Implement `MemoryPairsContext` with useReducer state management
- [ ] Create `types.ts` with comprehensive TypeScript interfaces
- [ ] Build `SetupPhase` component with game configuration UI
- [ ] Add utility functions in `utils/` directory with proper separation
### **Phase 2: Core Components (Week 2)**
- [ ] Implement `GameCard` component with flip animations
- [ ] Create `MemoryGrid` component with responsive layout
- [ ] Build `GamePhase` component orchestrating active gameplay
- [ ] Add `useCardMatching` hook for match validation logic
- [ ] Implement card generation utilities in `utils/cardGeneration.ts`
### **Phase 3: Game Logic & Polish (Week 3)**
- [ ] Create `ResultsPhase` component with game statistics
- [ ] Implement `PlayerIndicator` component for two-player mode
- [ ] Add `GameControls` component for game actions
- [ ] Build `useGameTimer` hook for timed gameplay features
- [ ] Add celebration effects and visual feedback
### **Phase 4: Advanced Features (Week 4)**
- [ ] Implement two-player turn-based gameplay logic
- [ ] Add scoring system in `utils/gameScoring.ts`
- [ ] Create `useMemoryPairsGame` hook for complex game orchestration
- [ ] Add AbacusReact integration with proper theming
- [ ] Implement complement pairs game mode
### **Phase 5: Testing & Documentation (Week 5)**
- [ ] Write comprehensive utility tests following memory-quiz test patterns
- [ ] Add component integration tests
- [ ] Create E2E test scenarios
- [ ] Write Storybook stories for all components
### **Phase 6: Final Polish & Integration (Week 6)**
- [ ] Performance optimization and code review
- [ ] Accessibility audit and improvements
- [ ] Cross-browser and mobile testing
- [ ] Integration with games directory
## **๐ Key Technical Decisions**
### **Improved Architecture Patterns**
1. **Component Composition**: Small, focused, testable components with single responsibilities
2. **Context-Based State**: Centralized state management with clean separation of concerns
3. **Custom Hooks**: Encapsulated logic in reusable, testable hooks
4. **Utility Modules**: Pure functions in focused utility files
5. **TypeScript Interfaces**: Comprehensive type safety with proper separation
6. **Panda CSS**: Use `css()` function with design tokens for consistent styling
### **Quality Standards**
- **TypeScript**: Strict mode with comprehensive type coverage
- **Testing**: Unit, integration, and E2E following memory-quiz test patterns
- **Performance**: React.memo, useMemo, useCallback for optimization
- **Accessibility**: WCAG 2.1 AA compliance with keyboard navigation
- **Code Quality**: ESLint + Prettier with existing project configuration
This plan ensures seamless integration with the existing codebase while delivering a sophisticated, well-tested implementation that maintains the quality and patterns established by the Speed Memory Quiz.