mirror of
https://github.com/tiennm99/gsd-framework.git
synced 2026-05-30 08:22:15 +00:00
docs(01-core-foundation): add phase research document
This commit is contained in:
@@ -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>
|
||||
## 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.
|
||||
</user_constraints>
|
||||
|
||||
<phase_requirements>
|
||||
## 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 |
|
||||
</phase_requirements>
|
||||
|
||||
## 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<T extends Record<string, unknown>> {
|
||||
private emitter = new EventEmitter();
|
||||
|
||||
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): this {
|
||||
this.emitter.on(event as string, listener);
|
||||
return this;
|
||||
}
|
||||
|
||||
emit<K extends keyof T>(event: K, data: T[K]): boolean {
|
||||
return this.emitter.emit(event as string, data);
|
||||
}
|
||||
|
||||
off<K extends keyof T>(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)
|
||||
Reference in New Issue
Block a user