docs(01): create Phase 1 Core Foundation plans

This commit is contained in:
2026-03-10 23:31:02 +07:00
parent 7a0d675209
commit d30a1c5180
4 changed files with 1055 additions and 5 deletions
+6 -5
View File
@@ -30,12 +30,12 @@ Decimal phases appear between their surrounding integers in numeric order.
2. Game loop runs at 60fps using requestAnimationFrame with delta time tracking
3. Event system allows components to subscribe to and emit typed events
4. Basic Tile model exists with properties for id, type, position, and cleared state
**Plans**: TBD
**Plans**: 3 plans
Plans:
- [ ] 01-01: Project scaffolding with Vite + TypeScript + Canvas
- [ ] 01-02: Game loop with delta time and event emitter
- [ ] 01-03: Tile model and game configuration constants
- [ ] 01-01-PLAN.md — Project scaffolding with Vite + TypeScript + Canvas, config constants, and shared types
- [ ] 01-02-PLAN.md — Game loop with delta time, typed event emitter, and Tile model class
- [ ] 01-03-PLAN.md — Game orchestrator class, main entry point, and human verification
### Phase 2: Grid and Input
**Goal**: Players can see a grid of Pokemon tiles and interact with them via mouse and touch
@@ -126,7 +126,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6
| Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------|
| 1. Core Foundation | 0/3 | Not started | - |
| 1. Core Foundation | 0/3 | Planning complete | - |
| 2. Grid and Input | 0/3 | Not started | - |
| 3. Core Matching Mechanics | 0/3 | Not started | - |
| 4. Game State Management | 0/3 | Not started | - |
@@ -136,3 +136,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6
---
*Roadmap created: 2026-03-10*
*Granularity: standard*
*Last updated: 2026-03-10 after Phase 1 planning*
@@ -0,0 +1,289 @@
---
phase: 01-core-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified:
- package.json
- tsconfig.json
- vite.config.ts
- vitest.config.ts
- index.html
- src/main.ts
- src/config.ts
- src/types/index.ts
- src/vite-env.d.ts
autonomous: true
requirements: [CORE-01]
user_setup: []
must_haves:
truths:
- "Developer can run npm run dev without errors"
- "Developer sees a Canvas element rendered in the browser"
- "Configuration constants are defined and typed"
- "Shared TypeScript types exist for Tile, Position, and Events"
artifacts:
- path: "package.json"
provides: "Project dependencies and scripts"
contains: '"vite", "typescript", "vitest"'
- path: "index.html"
provides: "HTML entry with Canvas element"
contains: '<canvas id="game">'
- path: "src/config.ts"
provides: "Game constants"
min_lines: 20
- path: "src/types/index.ts"
provides: "Shared type definitions"
exports: ["TilePosition", "Tile", "GameEvents"]
key_links:
- from: "src/main.ts"
to: "index.html"
via: "DOM canvas element reference"
pattern: "getElementById.*game"
---
<objective>
Create the project scaffolding with Vite + TypeScript + Canvas, establish game configuration constants, and define shared type interfaces.
Purpose: Establish the foundational project structure that all subsequent code builds upon. Without this, no game code can run.
Output: Working Vite dev server with Canvas, typed configuration, and shared type definitions.
</objective>
<execution_context>
@./.claude/get-shit-done/workflows/execute-plan.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-core-foundation/01-CONTEXT.md
@.planning/phases/01-core-foundation/01-RESEARCH.md
</context>
<interfaces>
<!-- Contracts defined in this plan - executors implement against these -->
```typescript
// src/types/index.ts - Type definitions that downstream plans will use
export interface TilePosition {
row: number;
col: number;
}
export interface Tile {
id: string;
type: number; // 0-15 for emoji index
position: TilePosition;
cleared: boolean;
}
export interface GameEvents {
'game:start': void;
'game:tick': { deltaTime: number };
'tile:selected': { tile: Tile; row: number; col: number };
'tile:cleared': { tile: Tile };
'game:score': { points: number };
'game:over': { won: boolean };
'error': Error;
}
```
```typescript
// src/config.ts - Configuration constants
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;
```
</interfaces>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create Vite project with TypeScript and Canvas</name>
<files>package.json, tsconfig.json, vite.config.ts, vitest.config.ts, index.html, src/main.ts, src/vite-env.d.ts</files>
<behavior>
- Test 1: npm run dev starts without errors
- Test 2: Browser displays Canvas element with colored background
- Test 3: npm run test runs vitest successfully (even with no tests)
</behavior>
<action>
1. Initialize Vite project with vanilla-ts template:
- Run: `npm create vite@latest . -- --template vanilla-ts` (use current directory)
- If prompted about non-empty directory, proceed with overwrite
2. Install dependencies:
- Run: `npm install`
- Run: `npm install -D vitest @vitest/coverage-v8 @types/node`
3. Create vite.config.ts with basic configuration:
- No plugins needed for vanilla TS
- Add test configuration for vitest
4. Create vitest.config.ts:
```typescript
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
globals: true,
environment: 'node',
},
});
```
5. Update package.json scripts:
- Add: `"test": "vitest"`, `"test:run": "vitest run"`
6. Modify index.html:
- Replace default Vite content with:
- A single `<canvas id="game"></canvas>` element
- Basic centered styling
- Keep the `<script type="module" src="/src/main.ts"></script>`
7. Update src/main.ts as temporary placeholder:
- Get canvas element by id 'game'
- Get 2D context
- Set canvas size (use CONFIG.grid dimensions when available)
- Fill with background color from CONFIG.colors when available
- For now, use hardcoded: canvas 832x528 (16 cols * 52, 10 rows * 52), background '#1a1a2e'
8. Ensure tsconfig.json has strict mode enabled.
DO NOT use Phaser, PixiJS, or any game framework - native Canvas only.
DO NOT add complex build configurations - keep Vite defaults.
</action>
<verify>
<automated>npm run test -- --run 2>&1 | head -20</automated>
</verify>
<done>
- `npm run dev` starts Vite dev server without errors
- Browser at localhost:5173 shows Canvas with dark blue background
- `npm run test` runs without configuration errors
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Create game configuration constants</name>
<files>src/config.ts</files>
<behavior>
- Test 1: CONFIG.grid.rows equals 10
- Test 2: CONFIG.grid.cols equals 16
- Test 3: CONFIG.emojis has exactly 16 emojis
- Test 4: CONFIG.tile.size and gap are positive numbers
</behavior>
<action>
Create src/config.ts with all game constants as a single exported CONFIG object.
Include these sections (use `as const` for type inference):
- grid: { rows: 10, cols: 16, totalTiles: 160, pairsPerType: 10 }
- tile: { size: 48, gap: 4, cornerRadius: 8 }
- emojis: array of 16 nature emojis (from RESEARCH.md)
- colors: { background, tile, tileHover, selection, text }
Emoji set (exactly these 16):
🌟 ⭐ 💫 ✨ 🌙 ☀️ 🔥 💧 🌿 ⚡ 🧊 🪨 🌸 🍃 🌊 🍄
Color values:
- background: '#1a1a2e'
- tile: '#16213e'
- tileHover: '#0f3460'
- selection: '#e94560'
- text: '#eaeaea'
DO NOT add runtime validation - these are compile-time constants.
DO NOT export individual constants - use the CONFIG namespace.
</action>
<verify>
<automated>npm run test -- --run src/__tests__/config.test.ts 2>&1 || echo "Test file will be created in task"</automated>
</verify>
<done>
- src/config.ts exports CONFIG object
- CONFIG.emojis.length === 16
- CONFIG.grid.rows * CONFIG.grid.cols === CONFIG.grid.totalTiles
- All color values are valid hex strings
</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: Create shared type definitions</name>
<files>src/types/index.ts</files>
<behavior>
- Test 1: TilePosition type has row and col as numbers
- Test 2: Tile interface has id, type, position, cleared
- Test 3: GameEvents type defines event names and payloads
</behavior>
<action>
Create src/types/index.ts with TypeScript interfaces and types.
Define these exports:
1. TilePosition interface: { row: number; col: number }
2. Tile interface: { id: string; type: number; position: TilePosition; cleared: boolean }
3. GameEvents type mapping event names to payload types:
- 'game:start': void
- 'game:tick': { deltaTime: number }
- 'tile:selected': { tile: Tile; row: number; col: number }
- 'tile:cleared': { tile: Tile }
- 'game:score': { points: number }
- 'game:over': { won: boolean }
- 'error': Error
DO NOT add implementation code - this file is types only.
DO NOT import from other modules - types should be self-contained.
</action>
<verify>
<automated>npx tsc --noEmit src/types/index.ts 2>&1 | head -10</automated>
</verify>
<done>
- src/types/index.ts exports TilePosition, Tile, GameEvents
- TypeScript compiles without errors
- Types are properly exported for use in other modules
</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. Run `npm run dev` - should start without errors
2. Open browser to localhost:5173 - should see Canvas with dark background
3. Run `npm run test` - should pass (even if empty test suite)
4. Run `npx tsc --noEmit` - should compile without errors
</verification>
<success_criteria>
- [ ] Vite dev server starts with `npm run dev`
- [ ] Canvas element renders in browser with colored background
- [ ] CONFIG object exports all required constants
- [ ] TilePosition, Tile, and GameEvents types are defined
- [ ] TypeScript compiles without errors
- [ ] Vitest runs without configuration errors
</success_criteria>
<output>
After completion, create `.planning/phases/01-core-foundation/01-01-SUMMARY.md`
</output>
@@ -0,0 +1,334 @@
---
phase: 01-core-foundation
plan: 02
type: execute
wave: 2
depends_on: [01-01]
files_modified:
- src/game/GameLoop.ts
- src/game/EventEmitter.ts
- src/models/Tile.ts
- src/__tests__/GameLoop.test.ts
- src/__tests__/EventEmitter.test.ts
- src/__tests__/Tile.test.ts
autonomous: true
requirements: [CORE-01]
user_setup: []
must_haves:
truths:
- "Game loop runs at 60fps using requestAnimationFrame"
- "Event system allows typed subscribe and emit operations"
- "Tile model has id, type, position, and cleared properties"
- "All core classes have passing unit tests"
artifacts:
- path: "src/game/GameLoop.ts"
provides: "requestAnimationFrame game loop"
exports: ["GameLoop"]
- path: "src/game/EventEmitter.ts"
provides: "Typed event emitter"
exports: ["TypedEventEmitter"]
- path: "src/models/Tile.ts"
provides: "Tile data model"
exports: ["Tile"]
- path: "src/__tests__/GameLoop.test.ts"
provides: "GameLoop unit tests"
min_lines: 20
- path: "src/__tests__/EventEmitter.test.ts"
provides: "EventEmitter unit tests"
min_lines: 20
- path: "src/__tests__/Tile.test.ts"
provides: "Tile unit tests"
min_lines: 20
key_links:
- from: "src/game/GameLoop.ts"
to: "requestAnimationFrame"
via: "browser API"
pattern: "requestAnimationFrame"
- from: "src/game/EventEmitter.ts"
to: "src/types/index.ts"
via: "GameEvents type"
pattern: "GameEvents"
- from: "src/models/Tile.ts"
to: "src/config.ts"
via: "emoji lookup"
pattern: "CONFIG.emojis"
- from: "src/models/Tile.ts"
to: "src/types/index.ts"
via: "TilePosition, Tile types"
pattern: "TilePosition"
---
<objective>
Implement the core game infrastructure: game loop with delta time tracking, typed event emitter for component communication, and Tile model class.
Purpose: These are the foundational systems that all game logic builds upon. The game loop drives frame updates, the event system decouples components, and the Tile model represents game state.
Output: Working GameLoop, TypedEventEmitter, and Tile classes with unit tests.
</objective>
<execution_context>
@./.claude/get-shit-done/workflows/execute-plan.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-core-foundation/01-CONTEXT.md
@.planning/phases/01-core-foundation/01-RESEARCH.md
@.planning/phases/01-core-foundation/01-01-PLAN.md
</context>
<interfaces>
<!-- Types from Plan 01 that this plan uses -->
From src/types/index.ts:
```typescript
export interface TilePosition {
row: number;
col: number;
}
export interface Tile {
id: string;
type: number;
position: TilePosition;
cleared: boolean;
}
export interface GameEvents {
'game:start': void;
'game:tick': { deltaTime: number };
'tile:selected': { tile: Tile; row: number; col: number };
'tile:cleared': { tile: Tile };
'game:score': { points: number };
'game:over': { won: boolean };
'error': Error;
}
```
From src/config.ts:
```typescript
export const CONFIG = {
emojis: [
'🌟', '⭐', '💫', '✨', '🌙', '☀️', '🔥', '💧',
'🌿', '⚡', '🧊', '🪨', '🌸', '🍃', '🌊', '🍄'
],
// ... other config
} as const;
```
</interfaces>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Implement GameLoop class</name>
<files>src/game/GameLoop.ts, src/__tests__/GameLoop.test.ts</files>
<behavior>
- Test 1: start() begins the loop and calls update callback
- Test 2: stop() cancels the animation frame
- Test 3: Delta time is calculated and passed to update callback
- Test 4: Multiple ticks accumulate correct time
</behavior>
<action>
Create src/game/GameLoop.ts and src/__tests__/GameLoop.test.ts following TDD pattern.
GameLoop class requirements:
1. Constructor accepts an update callback: `(deltaTime: number) => void`
2. `start()` method begins the loop using requestAnimationFrame
3. `stop()` method cancels the animation frame
4. Target 60fps (tickLength = 1000/60 ms)
5. Accumulate delta time and call update with correct deltaTime value
6. Store the rAF ID to enable stopping (prevent memory leak)
Implementation pattern (from RESEARCH.md):
```typescript
export class GameLoop {
private readonly tickLength: number = 1000 / 60;
private lastTick: number = 0;
private rafId: number = 0;
private running: boolean = false;
constructor(private update: (deltaTime: number) => void) {}
start(): void { /* start loop */ }
stop(): void { /* cancel rAF */ }
private main = (timestamp: number): void => { /* rAF callback */ }
}
```
Test file should cover:
- start() sets running to true
- stop() sets running to false and cancels rAF
- update callback is called with deltaTime
- Loop can be started and stopped multiple times
DO NOT use setInterval - must use requestAnimationFrame.
DO NOT forget to store rAF ID for cleanup.
</action>
<verify>
<automated>npm run test -- --run src/__tests__/GameLoop.test.ts</automated>
</verify>
<done>
- GameLoop class exports from src/game/GameLoop.ts
- All tests in GameLoop.test.ts pass
- start() begins loop, stop() cancels loop
- Delta time is correctly calculated
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Implement TypedEventEmitter class</name>
<files>src/game/EventEmitter.ts, src/__tests__/EventEmitter.test.ts</files>
<behavior>
- Test 1: on() registers a listener for an event
- Test 2: emit() calls all registered listeners with payload
- Test 3: off() removes a specific listener
- Test 4: Listeners receive correctly typed payloads
- Test 5: 'error' event is handled without throwing
</behavior>
<action>
Create src/game/EventEmitter.ts and src/__tests__/EventEmitter.test.ts following TDD pattern.
TypedEventEmitter class requirements:
1. Generic class: `TypedEventEmitter<T extends Record<string, unknown>>`
2. Uses Node's EventEmitter internally (import from 'events')
3. `on<K extends keyof T>(event: K, listener: (data: T[K]) => void): this`
4. `emit<K extends keyof T>(event: K, data: T[K]): boolean`
5. `off<K extends keyof T>(event: K, listener: (data: T[K]) => void): this`
6. Type-safe: TypeScript infers payload types from event name
Implementation pattern (from RESEARCH.md):
```typescript
import { EventEmitter } from 'events';
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;
}
}
```
Test file should:
- Create emitter with a test event map
- Verify on/emit/off work correctly
- Test multiple listeners for same event
- Verify type inference works
DO NOT implement custom event system - wrap Node's EventEmitter.
DO NOT allow untyped event names - use keyof T constraint.
</action>
<verify>
<automated>npm run test -- --run src/__tests__/EventEmitter.test.ts</automated>
</verify>
<done>
- TypedEventEmitter class exports from src/game/EventEmitter.ts
- All tests in EventEmitter.test.ts pass
- on(), emit(), off() methods work with type safety
- Uses Node's EventEmitter internally
</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: Implement Tile model class</name>
<files>src/models/Tile.ts, src/__tests__/Tile.test.ts</files>
<behavior>
- Test 1: Tile constructor sets id, type, position, cleared
- Test 2: emoji getter returns correct emoji from CONFIG
- Test 3: isAdjacent() returns true for adjacent tiles, false otherwise
- Test 4: cleared property defaults to false
</behavior>
<action>
Create src/models/Tile.ts and src/__tests__/Tile.test.ts following TDD pattern.
Tile class requirements:
1. Implements the Tile interface from src/types/index.ts
2. Constructor: `constructor(id: string, type: number, position: TilePosition)`
3. `cleared` property defaults to false
4. Getter `emoji` returns `CONFIG.emojis[this.type]`
5. Method `isAdjacent(other: Tile): boolean` - returns true if tiles are orthogonally adjacent (not diagonal)
Implementation pattern (from RESEARCH.md):
```typescript
import { TilePosition } from '../types';
import { CONFIG } from '../config';
export class Tile {
public cleared: boolean = false;
constructor(
public readonly id: string,
public readonly type: number,
public readonly position: TilePosition
) {}
get emoji(): string {
return CONFIG.emojis[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);
}
}
```
Test file should cover:
- Constructor assigns all properties correctly
- emoji getter returns correct emoji for each type 0-15
- isAdjacent returns true for (0,0) and (0,1)
- isAdjacent returns false for (0,0) and (1,1) (diagonal)
- isAdjacent returns false for (0,0) and (0,2) (not adjacent)
- cleared defaults to false
DO NOT add methods not specified - keep Tile model minimal.
DO NOT mutate position after construction - it's readonly.
</action>
<verify>
<automated>npm run test -- --run src/__tests__/Tile.test.ts</automated>
</verify>
<done>
- Tile class exports from src/models/Tile.ts
- All tests in Tile.test.ts pass
- Tile implements Tile interface correctly
- emoji getter returns correct emoji
- isAdjacent() correctly identifies adjacent tiles
</done>
</task>
</tasks>
<verification>
After all tasks complete:
1. Run `npm run test -- --run` - all 3 test files pass
2. Run `npx tsc --noEmit` - TypeScript compiles without errors
3. Verify all classes export correctly from their files
</verification>
<success_criteria>
- [ ] GameLoop class with start/stop methods exists
- [ ] GameLoop uses requestAnimationFrame (not setInterval)
- [ ] TypedEventEmitter class with on/emit/off methods exists
- [ ] TypedEventEmitter provides type-safe event handling
- [ ] Tile class with id, type, position, cleared exists
- [ ] Tile.emoji getter returns correct emoji
- [ ] Tile.isAdjacent() correctly identifies orthogonal neighbors
- [ ] All unit tests pass
</success_criteria>
<output>
After completion, create `.planning/phases/01-core-foundation/01-02-SUMMARY.md`
</output>
@@ -0,0 +1,426 @@
---
phase: 01-core-foundation
plan: 03
type: execute
wave: 3
depends_on: [01-02]
files_modified:
- src/game/Game.ts
- src/main.ts
- src/__tests__/config.test.ts
autonomous: false
requirements: [CORE-01]
user_setup: []
must_haves:
truths:
- "Developer runs npm run dev and sees a Canvas with colored background"
- "Game loop is running at 60fps (verifiable via console)"
- "Event system is wired and functional"
- "All tests pass including new config tests"
artifacts:
- path: "src/game/Game.ts"
provides: "Main game orchestrator class"
exports: ["Game"]
- path: "src/main.ts"
provides: "Application entry point"
min_lines: 15
- path: "src/__tests__/config.test.ts"
provides: "Configuration validation tests"
min_lines: 20
key_links:
- from: "src/main.ts"
to: "src/game/Game.ts"
via: "import and instantiation"
pattern: "new Game"
- from: "src/game/Game.ts"
to: "src/game/GameLoop.ts"
via: "composition"
pattern: "new GameLoop"
- from: "src/game/Game.ts"
to: "src/game/EventEmitter.ts"
via: "composition"
pattern: "TypedEventEmitter<GameEvents>"
- from: "src/game/Game.ts"
to: "index.html canvas"
via: "CanvasRenderingContext2D"
pattern: "getElementById.*game"
---
<objective>
Create the Game class that orchestrates all components and wire everything together in main.ts. Add configuration tests and perform human verification of the complete foundation.
Purpose: Integrate all Phase 1 components into a working application that can be verified visually and via tests.
Output: Working game foundation with Canvas rendering, game loop, and event system - ready for Phase 2.
</objective>
<execution_context>
@./.claude/get-shit-done/workflows/execute-plan.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-core-foundation/01-CONTEXT.md
@.planning/phases/01-core-foundation/01-RESEARCH.md
@.planning/phases/01-core-foundation/01-02-PLAN.md
</context>
<interfaces>
<!-- Types and classes from prior plans that this plan uses -->
From src/types/index.ts:
```typescript
export interface GameEvents {
'game:start': void;
'game:tick': { deltaTime: number };
'tile:selected': { tile: Tile; row: number; col: number };
'tile:cleared': { tile: Tile };
'game:score': { points: number };
'game:over': { won: boolean };
'error': Error;
}
```
From src/game/GameLoop.ts:
```typescript
export class GameLoop {
constructor(update: (deltaTime: number) => void);
start(): void;
stop(): void;
}
```
From src/game/EventEmitter.ts:
```typescript
export class TypedEventEmitter<T extends Record<string, unknown>> {
on<K extends keyof T>(event: K, listener: (data: T[K]) => void): this;
emit<K extends keyof T>(event: K, data: T[K]): boolean;
off<K extends keyof T>(event: K, listener: (data: T[K]) => void): this;
}
```
From src/config.ts:
```typescript
export const CONFIG = {
grid: { rows: 10, cols: 16, totalTiles: 160, pairsPerType: 10 },
tile: { size: 48, gap: 4, cornerRadius: 8 },
emojis: string[], // 16 emojis
colors: { background: '#1a1a2e', tile: '#16213e', ... },
} as const;
```
</interfaces>
<tasks>
<task type="auto" tdd="true">
<name>Task 1: Create Game class orchestrator</name>
<files>src/game/Game.ts</files>
<behavior>
- Test 1: Game constructor initializes canvas, context, loop, and events
- Test 2: Game.start() starts the game loop
- Test 3: Game.render() clears canvas with background color
- Test 4: Game emits 'game:tick' events during update
</behavior>
<action>
Create src/game/Game.ts that orchestrates all components.
Game class requirements:
1. Constructor:
- Gets canvas element by id 'game'
- Gets 2D rendering context
- Sets up canvas size based on CONFIG.grid and CONFIG.tile
- Creates GameLoop instance with this.update as callback
- Creates TypedEventEmitter<GameEvents> instance
- Handles device pixel ratio for sharp rendering
2. Properties:
- `canvas: HTMLCanvasElement`
- `ctx: CanvasRenderingContext2D`
- `loop: GameLoop`
- `events: TypedEventEmitter<GameEvents>`
3. Methods:
- `start()`: Starts the game loop, emits 'game:start'
- `stop()`: Stops the game loop
- `update(deltaTime: number)`: Called by loop, emits 'game:tick'
- `render()`: Clears canvas with CONFIG.colors.background
4. Canvas setup:
- Calculate width: cols * (size + gap) + gap
- Calculate height: rows * (size + gap) + gap
- Handle devicePixelRatio for sharp rendering
- Scale context by dpr
Implementation skeleton:
```typescript
import { GameLoop } from './GameLoop';
import { TypedEventEmitter } from './EventEmitter';
import { GameEvents } from '../types';
import { CONFIG } from '../config';
export class Game {
readonly canvas: HTMLCanvasElement;
readonly ctx: CanvasRenderingContext2D;
readonly loop: GameLoop;
readonly events: TypedEventEmitter<GameEvents>;
constructor() {
this.canvas = document.getElementById('game') as HTMLCanvasElement;
this.ctx = this.canvas.getContext('2d')!;
this.events = new TypedEventEmitter<GameEvents>();
this.setupCanvas();
this.loop = new GameLoop(this.update.bind(this));
}
private setupCanvas(): void {
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);
}
start(): void {
this.events.emit('game:start', undefined as never);
this.loop.start();
}
stop(): void {
this.loop.stop();
}
private update(deltaTime: number): void {
this.events.emit('game:tick', { deltaTime });
this.render();
}
private render(): void {
this.ctx.fillStyle = CONFIG.colors.background;
this.ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
}
}
```
DO NOT add tile rendering - that's Phase 2.
DO NOT add input handling - that's Phase 2.
</action>
<verify>
<automated>npm run test -- --run 2>&1 | tail -20</automated>
</verify>
<done>
- Game class exports from src/game/Game.ts
- Game initializes canvas, loop, and events
- Game.start() begins the loop
- Game.render() clears canvas with background color
</done>
</task>
<task type="auto" tdd="true">
<name>Task 2: Wire main.ts entry point</name>
<files>src/main.ts</files>
<behavior>
- Test 1: main.ts imports and instantiates Game
- Test 2: Game.start() is called on DOMContentLoaded
- Test 3: Error handling logs to console
</behavior>
<action>
Update src/main.ts to be the application entry point.
Requirements:
1. Import Game class
2. Wait for DOMContentLoaded event
3. Create Game instance
4. Call game.start()
5. Add error event listener on game.events
6. Log initialization to console (helps verify 60fps in dev tools)
Implementation:
```typescript
import { Game } from './game/Game';
let game: Game | null = null;
document.addEventListener('DOMContentLoaded', () => {
try {
game = new Game();
// Handle errors
game.events.on('error', (err) => {
console.error('Game error:', err);
});
// Log tick events in development (comment out in production)
game.events.on('game:tick', ({ deltaTime }) => {
// Uncomment for debugging: console.log('Tick:', deltaTime.toFixed(2), 'ms');
});
game.start();
console.log('Game initialized - Canvas should show with background color');
} catch (err) {
console.error('Failed to initialize game:', err);
}
});
```
DO NOT add complex initialization logic.
DO NOT add hot module replacement handling.
</action>
<verify>
<automated>npm run test -- --run 2>&1 | tail -10</automated>
</verify>
<done>
- src/main.ts imports and instantiates Game
- Game starts on DOMContentLoaded
- Error events are logged to console
- Application runs without errors
</done>
</task>
<task type="auto" tdd="true">
<name>Task 3: Add configuration tests</name>
<files>src/__tests__/config.test.ts</files>
<behavior>
- Test 1: CONFIG.grid.rows equals 10
- Test 2: CONFIG.grid.cols equals 16
- Test 3: CONFIG.grid.totalTiles equals rows * cols (160)
- Test 4: CONFIG.emojis has exactly 16 unique emojis
- Test 5: CONFIG.tile.size and gap are positive
- Test 6: All colors are valid hex strings
</behavior>
<action>
Create src/__tests__/config.test.ts to validate configuration constants.
Test cases:
```typescript
import { describe, it, expect } from 'vitest';
import { CONFIG } from '../config';
describe('CONFIG', () => {
describe('grid', () => {
it('should have 10 rows', () => {
expect(CONFIG.grid.rows).toBe(10);
});
it('should have 16 columns', () => {
expect(CONFIG.grid.cols).toBe(16);
});
it('should have correct total tiles', () => {
expect(CONFIG.grid.totalTiles).toBe(CONFIG.grid.rows * CONFIG.grid.cols);
});
it('should have 10 pairs per type', () => {
expect(CONFIG.grid.pairsPerType).toBe(10);
});
});
describe('tile', () => {
it('should have positive size', () => {
expect(CONFIG.tile.size).toBeGreaterThan(0);
});
it('should have positive gap', () => {
expect(CONFIG.tile.gap).toBeGreaterThan(0);
});
it('should have positive corner radius', () => {
expect(CONFIG.tile.cornerRadius).toBeGreaterThan(0);
});
});
describe('emojis', () => {
it('should have exactly 16 emojis', () => {
expect(CONFIG.emojis).toHaveLength(16);
});
it('should have all unique emojis', () => {
const unique = new Set(CONFIG.emojis);
expect(unique.size).toBe(16);
});
});
describe('colors', () => {
it('should have valid hex background color', () => {
expect(CONFIG.colors.background).toMatch(/^#[0-9a-fA-F]{6}$/);
});
it('should have valid hex tile color', () => {
expect(CONFIG.colors.tile).toMatch(/^#[0-9a-fA-F]{6}$/);
});
it('should have valid hex selection color', () => {
expect(CONFIG.colors.selection).toMatch(/^#[0-9a-fA-F]{6}$/);
});
});
});
```
DO NOT add tests for values that may change during tuning.
DO test invariants (counts, formats, positivity).
</action>
<verify>
<automated>npm run test -- --run src/__tests__/config.test.ts</automated>
</verify>
<done>
- src/__tests__/config.test.ts exists with all tests
- All configuration tests pass
- Grid dimensions are validated
- Emoji count and uniqueness verified
- Color formats validated
</done>
</task>
<task type="checkpoint:human-verify" gate="blocking">
<name>Task 4: Human verification of Phase 1 foundation</name>
<files></files>
<action>Human verification checkpoint</action>
<what-built>Complete Phase 1 foundation: Vite project with Canvas, game loop at 60fps, typed event system, Tile model, and all tests passing.</what-built>
<how-to-verify>
1. Run `npm run dev` in terminal
2. Open browser to http://localhost:5173
3. Verify you see a Canvas with dark blue background (#1a1a2e)
4. Open browser DevTools (F12) -> Console
5. Verify "Game initialized" message appears
6. Run `npm run test -- --run` in a new terminal
7. Verify all tests pass (config, Tile, GameLoop, EventEmitter)
</how-to-verify>
<verify>
<automated>echo "Manual verification required - see how-to-verify steps"</automated>
</verify>
<done>Human has verified Canvas rendering, console output, and all tests pass</done>
<resume-signal>Type "approved" if all verifications pass, or describe any issues found.</resume-signal>
</task>
</tasks>
<verification>
After all automated tasks complete:
1. Run `npm run test -- --run` - all tests pass
2. Run `npx tsc --noEmit` - TypeScript compiles without errors
3. Run `npm run dev` - dev server starts
4. Open browser - Canvas with background color visible
5. Console shows "Game initialized" message
</verification>
<success_criteria>
- [ ] Game class orchestrates loop, events, and canvas
- [ ] main.ts entry point initializes and starts game
- [ ] All configuration tests pass
- [ ] All prior tests still pass (GameLoop, EventEmitter, Tile)
- [ ] `npm run dev` shows Canvas with colored background
- [ ] Console shows game initialization message
- [ ] Human verification checkpoint approved
</success_criteria>
<output>
After completion, create `.planning/phases/01-core-foundation/01-03-SUMMARY.md`
</output>