From 522d06848a1deabe8487bbaae2afd767b6530992 Mon Sep 17 00:00:00 2001 From: tiennm99 Date: Tue, 10 Mar 2026 23:21:54 +0700 Subject: [PATCH] docs(01-core-foundation): add phase research document --- .../phases/01-core-foundation/01-RESEARCH.md | 463 ++++++++++++++++++ 1 file changed, 463 insertions(+) create mode 100644 .planning/phases/01-core-foundation/01-RESEARCH.md diff --git a/.planning/phases/01-core-foundation/01-RESEARCH.md b/.planning/phases/01-core-foundation/01-RESEARCH.md new file mode 100644 index 0000000..e6971b8 --- /dev/null +++ b/.planning/phases/01-core-foundation/01-RESEARCH.md @@ -0,0 +1,463 @@ +# Phase 1: Core Foundation - Research + +**Researched:** 2026-03-10 +**Domain:** Vite + TypeScript + HTML5 Canvas game development, game loop architecture, event systems +**Confidence:** HIGH + +## Summary + +This phase establishes the foundational architecture for a tile-matching puzzle game using vanilla TypeScript with HTML5 Canvas rendering. The stack consists of Vite 6.x for build tooling, TypeScript 5.x for type safety, and native Canvas API for rendering - no game framework required for this scope. + +The core deliverables are: project scaffolding, game loop with `requestAnimationFrame`, typed event emitter for game communication, basic Tile model, and centralized configuration. This foundation will support all subsequent phases for matching mechanics, pathfinding, and UI. + +**Primary recommendation:** Use Vite's vanilla-ts template as the starting point, implement a fixed-timestep game loop with `requestAnimationFrame`, and extend Node's EventEmitter pattern for type-safe game events. + + +## User Constraints (from CONTEXT.md) + +### Locked Decisions +- Grid size: **16 columns x 10 rows** (160 tiles, 80 pairs) +- 16 unique tile types, each appearing 10 times +- **Emoji tiles** using 16 nature element emojis: + - 🌟 ⭐ 💫 ✨ 🌙 ☀️ 🔥 💧 🌿 ⚡ 🧊 🪨 🌸 🍃 🌊 🍄 +- **Card style** tiles: big emoji in center, rounded corners, subtle shadow +- **Highlight ring** to show tile selection +- All game constants in **src/config.ts** as typed TypeScript +- Configurable items: + - Grid dimensions (rows, columns) + - Tile size and gap/spacing + - Emoji set (array of 16 emojis) + - Color palette (background, selection, etc.) + +### Claude's Discretion +- Exact project folder structure +- Event emitter implementation details +- Game loop implementation specifics +- Tile model exact properties + +### Deferred Ideas (OUT OF SCOPE) +None - discussion stayed within phase scope. + + + +## Phase Requirements + +| ID | Description | Research Support | +|----|-------------|-----------------| +| CORE-01 | Game displays a grid of Pokemon tiles arranged in rows and columns | Canvas rendering patterns, Tile model, Grid layout algorithms, Configuration system | + + +## Standard Stack + +### Core +| Library | Version | Purpose | Why Standard | +|---------|---------|---------|--------------| +| Vite | 6.x+ | Build tool and dev server | Fast HMR, native ESM, TypeScript support out of box | +| TypeScript | 5.x+ | Type safety | Industry standard, excellent IDE support | +| Canvas API | Native | 2D rendering | No dependencies, full control, perfect for tile games | + +### Supporting +| Library | Version | Purpose | When to Use | +|---------|---------|---------|-------------| +| Vitest | 3.x | Unit testing | For all test files, integrates with Vite config | +| @types/node | 22.x | Node.js type definitions | Required for EventEmitter typing | + +### Alternatives Considered +| Instead of | Could Use | Tradeoff | +|------------|-----------|----------| +| Native Canvas | Phaser.js | Phaser adds complexity and bundle size for simple tile rendering | +| Native Canvas | PixiJS | PixiJS is WebGL-first, overkill for 2D tile game | +| Custom EventEmitter | eventemitter3 | eventemitter3 is smaller but Node's EventEmitter has better TypeScript support | +| Vite | webpack | webpack is slower and more complex to configure | + +**Installation:** +```bash +npm create vite@latest pikachu-match -- --template vanilla-ts +cd pikachu-match +npm install +npm install -D vitest @types/node +``` + +## Architecture Patterns + +### Recommended Project Structure +``` +src/ +├── config.ts # Game constants (grid, tiles, colors) +├── main.ts # Entry point, initializes game +├── game/ +│ ├── Game.ts # Main game class, owns game loop +│ ├── GameLoop.ts # requestAnimationFrame wrapper +│ └── EventEmitter.ts # Typed event emitter +├── models/ +│ └── Tile.ts # Tile data model +├── renderer/ +│ └── CanvasRenderer.ts # Canvas drawing operations +└── types/ + └── index.ts # Shared type definitions +``` + +### Pattern 1: Fixed-Timestep Game Loop +**What:** Game loop that updates at a fixed rate (e.g., 60Hz) independent of render rate +**When to use:** All games that need consistent physics/timing regardless of frame rate +**Example:** +```typescript +// Source: MDN Game Anatomy + industry best practices +// https://developer.mozilla.org/en-US/docs/Games/Anatomy + +class GameLoop { + private readonly tickLength: number = 1000 / 60; // 60 FPS + private lastTick: number = performance.now(); + private stopMain: number = 0; + + start() { + this.lastTick = performance.now(); + this.main(performance.now()); + } + + private main = (tFrame: number) => { + this.stopMain = requestAnimationFrame(this.main); + const nextTick = this.lastTick + this.tickLength; + let numTicks = 0; + + if (tFrame > nextTick) { + const timeSinceTick = tFrame - this.lastTick; + numTicks = Math.floor(timeSinceTick / this.tickLength); + } + + this.queueUpdates(numTicks); + this.render(tFrame); + } + + private queueUpdates(numTicks: number) { + for (let i = 0; i < numTicks; i++) { + this.lastTick += this.tickLength; + this.update(this.lastTick); + } + } + + stop() { + cancelAnimationFrame(this.stopMain); + } +} +``` + +### Pattern 2: Typed Event Emitter +**What:** Event emitter with full TypeScript type inference for event names and payloads +**When to use:** Decoupling game components (input, rendering, game state) +**Example:** +```typescript +// Source: Node.js EventEmitter with TypeScript enhancements +// https://nodejs.org/api/events.html + +import { EventEmitter } from 'events'; + +type EventMap = { + 'tile:selected': { tile: Tile; row: number; col: number }; + 'tile:cleared': { tile: Tile }; + 'game:score': { points: number }; + 'game:over': { won: boolean }; +}; + +export class TypedEventEmitter> { + private emitter = new EventEmitter(); + + on(event: K, listener: (data: T[K]) => void): this { + this.emitter.on(event as string, listener); + return this; + } + + emit(event: K, data: T[K]): boolean { + return this.emitter.emit(event as string, data); + } + + off(event: K, listener: (data: T[K]) => void): this { + this.emitter.off(event as string, listener); + return this; + } +} +``` + +### Pattern 3: Tile Model with Position +**What:** Immutable tile data with grid position and type information +**When to use:** Representing game state in a grid-based game +**Example:** +```typescript +// Tile model for grid-based matching game +export interface TilePosition { + row: number; + col: number; +} + +export class Tile { + constructor( + public readonly id: string, + public readonly type: number, // 0-15 for emoji index + public readonly position: TilePosition + ) {} + + get emoji(): string { + return EMOJI_SET[this.type]; + } + + isAdjacent(other: Tile): boolean { + const rowDiff = Math.abs(this.position.row - other.position.row); + const colDiff = Math.abs(this.position.col - other.position.col); + return (rowDiff === 1 && colDiff === 0) || (rowDiff === 0 && colDiff === 1); + } +} +``` + +### Anti-Patterns to Avoid +- **Polluting global scope:** Don't attach game to `window` object - use proper module exports +- **Mixed timing strategies:** Don't mix `setTimeout` with `requestAnimationFrame` - pick one pattern +- **Mutable tile state:** Don't mutate tile objects directly - create new instances for state changes +- **Canvas context per frame:** Don't call `getContext('2d')` repeatedly - cache the context + +## Don't Hand-Roll + +| Problem | Don't Build | Use Instead | Why | +|---------|-------------|-------------|-----| +| Event system | Custom pub/sub | Node EventEmitter | Battle-tested, typed, handles edge cases | +| Build system | Custom scripts | Vite | HMR, tree-shaking, TypeScript support | +| Test runner | Custom assertion library | Vitest | Jest-compatible API, Vite integration | +| Grid data structure | 2D array class | Nested arrays | Native, performant, easy to debug | + +**Key insight:** The game is simple enough that custom abstractions add complexity without value. Use native features and established patterns. + +## Common Pitfalls + +### Pitfall 1: Canvas Resolution vs CSS Size +**What goes wrong:** Blurry canvas rendering because canvas pixel dimensions don't match display size +**Why it happens:** Canvas width/height attributes define internal resolution, CSS defines display size +**How to avoid:** Set canvas width/height to actual pixel dimensions, use CSS for responsive sizing +**Warning signs:** Text looks blurry, edges are fuzzy, lines aren't crisp + +```typescript +// Correct approach +const canvas = document.getElementById('game') as HTMLCanvasElement; +const dpr = window.devicePixelRatio || 1; +canvas.width = displayWidth * dpr; +canvas.height = displayHeight * dpr; +canvas.style.width = `${displayWidth}px`; +canvas.style.height = `${displayHeight}px`; +ctx.scale(dpr, dpr); +``` + +### Pitfall 2: Game Loop Memory Leak +**What goes wrong:** Multiple game loops running simultaneously, consuming CPU and causing visual glitches +**Why it happens:** Forgetting to cancel previous `requestAnimationFrame` before starting new loop +**How to avoid:** Always store the rAF ID and call `cancelAnimationFrame` when stopping +**Warning signs:** Game speeds up over time, CPU usage increases, animations become erratic + +### Pitfall 3: EventEmitter 'error' Event Unhandled +**What goes wrong:** Game crashes silently when an error event is emitted +**Why it happens:** Node's EventEmitter throws if 'error' is emitted with no listeners +**How to avoid:** Always register an 'error' listener on event emitters +**Warning signs:** Game exits unexpectedly, no error message in console + +### Pitfall 4: TypeScript Strict Mode Issues +**What goes wrong:** Type errors when accessing DOM elements or canvas context +**Why it happens:** Strict null checks catch potential null references +**How to avoid:** Use non-null assertions only when certain, or add proper null checks +**Warning signs:** Red squiggly lines on `document.getElementById()`, `getContext()` calls + +## Code Examples + +Verified patterns from official sources: + +### Vite + TypeScript Project Setup +```bash +# Source: https://vite.dev/guide/ +npm create vite@latest pikachu-match -- --template vanilla-ts +cd pikachu-match +npm install +``` + +### Configuration File Pattern +```typescript +// src/config.ts +// All game constants in one place, easily tunable + +export const CONFIG = { + grid: { + rows: 10, + cols: 16, + totalTiles: 160, + pairsPerType: 10, + }, + tile: { + size: 48, + gap: 4, + cornerRadius: 8, + }, + emojis: [ + '🌟', '⭐', '💫', '✨', '🌙', '☀️', '🔥', '💧', + '🌿', '⚡', '🧊', '🪨', '🌸', '🍃', '🌊', '🍄' + ], + colors: { + background: '#1a1a2e', + tile: '#16213e', + tileHover: '#0f3460', + selection: '#e94560', + text: '#eaeaea', + }, +} as const; +``` + +### Canvas Renderer Class +```typescript +// Source: MDN Canvas tutorials + game development best practices +export class CanvasRenderer { + private ctx: CanvasRenderingContext2D; + private canvas: HTMLCanvasElement; + + constructor(canvasId: string) { + this.canvas = document.getElementById(canvasId) as HTMLCanvasElement; + this.ctx = this.canvas.getContext('2d')!; + this.setupCanvas(); + } + + private setupCanvas() { + const { cols, rows } = CONFIG.grid; + const { size, gap } = CONFIG.tile; + + const dpr = window.devicePixelRatio || 1; + const width = cols * (size + gap) + gap; + const height = rows * (size + gap) + gap; + + this.canvas.width = width * dpr; + this.canvas.height = height * dpr; + this.canvas.style.width = `${width}px`; + this.canvas.style.height = `${height}px`; + this.ctx.scale(dpr, dpr); + } + + clear() { + this.ctx.fillStyle = CONFIG.colors.background; + this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height); + } + + drawTile(tile: Tile, isSelected: boolean = false) { + const { size, gap, cornerRadius } = CONFIG.tile; + const x = tile.position.col * (size + gap) + gap; + const y = tile.position.row * (size + gap) + gap; + + // Draw rounded rectangle + this.ctx.fillStyle = isSelected ? CONFIG.colors.tileHover : CONFIG.colors.tile; + this.roundRect(x, y, size, size, cornerRadius); + + // Draw emoji + this.ctx.font = `${size * 0.6}px sans-serif`; + this.ctx.textAlign = 'center'; + this.ctx.textBaseline = 'middle'; + this.ctx.fillText(tile.emoji, x + size / 2, y + size / 2); + + // Draw selection ring + if (isSelected) { + this.ctx.strokeStyle = CONFIG.colors.selection; + this.ctx.lineWidth = 3; + this.roundRect(x, y, size, size, cornerRadius, false); + } + } + + private roundRect( + x: number, y: number, w: number, h: number, r: number, fill: boolean = true + ) { + this.ctx.beginPath(); + this.ctx.moveTo(x + r, y); + this.ctx.lineTo(x + w - r, y); + this.ctx.quadraticCurveTo(x + w, y, x + w, y + r); + this.ctx.lineTo(x + w, y + h - r); + this.ctx.quadraticCurveTo(x + w, y + h, x + w - r, y + h); + this.ctx.lineTo(x + r, y + h); + this.ctx.quadraticCurveTo(x, y + h, x, y + h - r); + this.ctx.lineTo(x, y + r); + this.ctx.quadraticCurveTo(x, y, x + r, y); + this.ctx.closePath(); + if (fill) { + this.ctx.fill(); + } else { + this.ctx.stroke(); + } + } +} +``` + +## State of the Art + +| Old Approach | Current Approach | When Changed | Impact | +|--------------|------------------|--------------|--------| +| Webpack for builds | Vite | ~2020 | 10-100x faster dev server startup | +| `setInterval` for game loops | `requestAnimationFrame` | ~2014 | Smooth 60fps, VSync-aware | +| JavaScript | TypeScript | ~2020+ | Type safety, better IDE support | +| Separate config files | Unified Vite config | ~2020 | Single source of truth for build | + +**Deprecated/outdated:** +- `setInterval`/`setTimeout` for game loops: Use `requestAnimationFrame` instead +- Global namespace pollution: Use ES modules +- String-based event names without types: Use typed event maps + +## Open Questions + +1. **Responsive canvas sizing** + - What we know: Canvas should handle different screen sizes + - What's unclear: Whether to scale tiles or change grid layout on mobile + - Recommendation: Phase 6 (UX) addresses this - Phase 1 uses fixed dimensions + +2. **Tile animation system** + - What we know: Phase 6 requires tile animations + - What's unclear: Whether to prepare animation infrastructure in Phase 1 + - Recommendation: Keep Phase 1 minimal - add animation system in Phase 6 + +## Validation Architecture + +### Test Framework +| Property | Value | +|----------|-------| +| Framework | Vitest 3.x | +| Config file | vitest.config.ts (or vite.config.ts with test property) | +| Quick run command | `npm run test` | +| Full suite command | `npm run test -- --run` | + +### Phase Requirements -> Test Map +| Req ID | Behavior | Test Type | Automated Command | File Exists? | +|--------|----------|-----------|-------------------|-------------| +| CORE-01 | Grid displays tiles in rows/cols | unit | `vitest run src/__tests__/Grid.test.ts` | Wave 0 | + +### Sampling Rate +- **Per task commit:** `npm run test` +- **Per wave merge:** `npm run test -- --run` +- **Phase gate:** Full suite green before `/gsd:verify-work` + +### Wave 0 Gaps +- [ ] `src/__tests__/config.test.ts` - verifies config constants are valid +- [ ] `src/__tests__/Tile.test.ts` - verifies tile model behavior +- [ ] `src/__tests__/GameLoop.test.ts` - verifies loop start/stop/tick +- [ ] `src/__tests__/EventEmitter.test.ts` - verifies typed emit/on +- [ ] `vitest.config.ts` - test framework configuration +- [ ] Framework install: `npm install -D vitest @types/node` + +## Sources + +### Primary (HIGH confidence) +- [Vite Official Docs](https://vite.dev/guide/) - Vite 6.x setup, TypeScript support, project scaffolding +- [MDN Game Anatomy](https://developer.mozilla.org/en-US/docs/Games/Anatomy) - Game loop patterns, requestAnimationFrame best practices +- [Node.js Events Documentation](https://nodejs.org/api/events.html) - EventEmitter API, TypeScript integration +- [Vitest Official Docs](https://vitest.dev/guide/) - Test framework setup, Vite integration + +### Secondary (MEDIUM confidence) +- Context from existing project structure and conventions + +### Tertiary (LOW confidence) +- None - all recommendations verified against official sources + +## Metadata + +**Confidence breakdown:** +- Standard stack: HIGH - All tools are current stable releases with active maintenance +- Architecture: HIGH - Patterns derived from MDN official game development guides +- Pitfalls: HIGH - Common issues well-documented in Canvas and game development resources + +**Research date:** 2026-03-10 +**Valid until:** 30 days (stable tooling, low risk of breaking changes)