docs(01-foundation): create phase plan

This commit is contained in:
Dabit
2026-03-10 14:42:41 +01:00
parent 23f9c15213
commit 558fe20271
3 changed files with 671 additions and 2 deletions

View File

@@ -35,7 +35,11 @@
4. Ball angle changes based on where it hits the paddle (not purely geometric) 4. Ball angle changes based on where it hits the paddle (not purely geometric)
5. Ball speed increases gradually over the course of a match 5. Ball speed increases gradually over the course of a match
**Plans**: TBD **Plans:** 2 plans
Plans:
- [ ] 01-01-PLAN.md — HTML scaffold, HiDPI canvas, Renderer, GameLoop, Input, Player 1 paddle
- [ ] 01-02-PLAN.md — Ball physics: movement, wall bounce, zone-based deflection, speed increment
--- ---
@@ -133,7 +137,7 @@
| Phase | Plans Complete | Status | Completed | | Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------| |-------|----------------|--------|-----------|
| 1. Foundation | 0/? | Not started | — | | 1. Foundation | 0/2 | Planned | — |
| 2. Core Gameplay | 0/? | Not started | — | | 2. Core Gameplay | 0/? | Not started | — |
| 3. Complete Experience | 0/? | Not started | — | | 3. Complete Experience | 0/? | Not started | — |
| 4. Polish & Depth | 0/? | Not started | — | | 4. Polish & Depth | 0/? | Not started | — |
@@ -142,3 +146,4 @@
--- ---
*Roadmap created: 2026-03-10* *Roadmap created: 2026-03-10*
*Phase 1 planned: 2026-03-10*

View File

@@ -0,0 +1,335 @@
---
phase: 01-foundation
plan: 01
type: execute
wave: 1
depends_on: []
files_modified: [index.html]
autonomous: true
requirements: [VFX-05, CORE-07]
must_haves:
truths:
- "Canvas fills the entire browser window with no scrollbars or white space"
- "Canvas resizes immediately when the window is resized, maintaining minimum 4:3 aspect ratio"
- "Graphics render sharply on HiDPI/Retina displays (no blurry ball or paddle edges)"
- "Pressing W moves Player 1 paddle up; pressing S moves it down — responsively with no OS key-repeat lag"
- "Game loop runs continuously at device refresh rate; GameLoop, Renderer, Input objects exist in global scope"
artifacts:
- path: "index.html"
provides: "Complete HTML scaffold with embedded CSS, canvas element, and all four module objects"
contains: "GameLoop, Physics, Renderer, Input"
- path: "index.html"
provides: "HiDPI canvas setup — devicePixelRatio scaling applied on init and resize"
contains: "devicePixelRatio"
- path: "index.html"
provides: "Full-window canvas with aspect ratio enforcement and resize handler"
contains: "window.addEventListener('resize'"
- path: "index.html"
provides: "Keyboard input state tracking for W/S keys"
contains: "KeyW, KeyS"
key_links:
- from: "GameLoop.main()"
to: "requestAnimationFrame"
via: "window.requestAnimationFrame(this.main.bind(this))"
pattern: "requestAnimationFrame"
- from: "Renderer.init()"
to: "devicePixelRatio"
via: "canvas bitmap scale + ctx.scale(dpr, dpr)"
pattern: "devicePixelRatio"
- from: "Input.getVerticalInput()"
to: "GameLoop update cycle"
via: "Physics.update() queries Input.getVerticalInput() each frame"
pattern: "getVerticalInput"
---
<objective>
Create the complete HTML scaffold for Super Pong Next Gen: a single index.html with embedded CSS, a full-window HiDPI-aware canvas, and the four core module objects (GameLoop, Renderer, Input, Physics skeleton). After this plan, the canvas renders sharply on Retina displays, fills the window with correct aspect ratio on resize, the game loop is running, and Player 1 can move a visible paddle with W/S keys.
Purpose: Every subsequent phase builds on this foundation. The module structure established here is the architecture all later phases extend.
Output: index.html — runnable directly in a browser with no build step.
</objective>
<execution_context>
@/home/dabit/.claude/get-shit-done/workflows/execute-plan.md
@/home/dabit/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
</context>
<tasks>
<task type="auto">
<name>Task 1: HTML scaffold, HiDPI canvas, and full-window Renderer</name>
<files>index.html</files>
<action>
Create index.html from scratch. It must contain exactly:
**HTML structure:**
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Super Pong Next Gen</title>
<style>...</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<script>...</script>
</body>
</html>
```
**CSS (inside `<style>`):**
- `* { margin: 0; padding: 0; box-sizing: border-box; }`
- `body { background: #000; overflow: hidden; width: 100vw; height: 100vh; }`
- `#gameCanvas { display: block; background: #000; }`
**Renderer object (inside `<script>`):**
```javascript
const Renderer = {
canvas: null,
ctx: null,
logicalWidth: 0,
logicalHeight: 0,
init() {
this.canvas = document.getElementById('gameCanvas');
this.resize();
window.addEventListener('resize', () => this.resize());
},
resize() {
const dpr = window.devicePixelRatio || 1;
// Enforce minimum 4:3 aspect ratio — canvas fills window
const w = window.innerWidth;
const h = window.innerHeight;
const minAspect = 4 / 3;
const currentAspect = w / h;
// Always fill entire window; use logical coords for game
this.logicalWidth = w;
this.logicalHeight = h;
// Enforce minimum 4:3: if narrower than 4:3, treat as 4:3 height
// (window fills screen; minimum 4:3 means we never let height dominate width)
if (currentAspect < minAspect) {
// Too tall — game uses 4:3 minimum: constrain logical height
this.logicalHeight = Math.floor(w / minAspect);
}
// Step 1: Set CSS size to logical dimensions
this.canvas.style.width = this.logicalWidth + 'px';
this.canvas.style.height = this.logicalHeight + 'px';
// Step 2: Scale bitmap to device pixel ratio
this.canvas.width = Math.floor(this.logicalWidth * dpr);
this.canvas.height = Math.floor(this.logicalHeight * dpr);
// Step 3: Scale context so all draw calls use logical pixels
this.ctx = this.canvas.getContext('2d');
this.ctx.scale(dpr, dpr);
// Notify Physics of new dimensions (Physics may not exist yet)
if (typeof Physics !== 'undefined' && Physics.onResize) {
Physics.onResize(this.logicalWidth, this.logicalHeight);
}
},
clear() {
this.ctx.clearRect(0, 0, this.logicalWidth, this.logicalHeight);
this.ctx.fillStyle = '#000';
this.ctx.fillRect(0, 0, this.logicalWidth, this.logicalHeight);
},
drawRect(x, y, w, h, color) {
this.ctx.fillStyle = color || '#fff';
this.ctx.fillRect(Math.round(x), Math.round(y), Math.round(w), Math.round(h));
},
drawCircle(x, y, radius, color) {
this.ctx.beginPath();
this.ctx.arc(Math.round(x), Math.round(y), radius, 0, Math.PI * 2);
this.ctx.fillStyle = color || '#fff';
this.ctx.fill();
}
};
```
Place the full Renderer object in the script tag. Do not initialize yet (initialization happens at bottom of script after all objects are defined).
</action>
<verify>
<automated>Open index.html in browser — canvas fills window, background is black, no scrollbars. Resize window — canvas adjusts. No JS console errors.</automated>
</verify>
<done>index.html exists with valid HTML5 structure, black full-window canvas, Renderer object with HiDPI-aware resize() method using devicePixelRatio. Canvas fills browser window on load and on resize.</done>
</task>
<task type="auto">
<name>Task 2: GameLoop, Input module, and Player 1 paddle rendering</name>
<files>index.html</files>
<action>
Add to the existing `<script>` tag in index.html (after Renderer, before initialization code):
**GameState object** — holds all mutable game state:
```javascript
const GameState = {
paddle1: {
x: 0, y: 0, // Set on Physics.init()
width: 12, height: 80,
speed: 400, // Logical pixels per second
color: '#fff'
},
ball: {
x: 0, y: 0, // Set on Physics.init()
radius: 8,
vx: 0, vy: 0, // Set on Physics.init()
speed: 0, // Current scalar speed
color: '#fff'
}
};
```
**Input object:**
```javascript
const Input = {
keys: { w: false, s: false },
init() {
document.addEventListener('keydown', (e) => {
if (e.code === 'KeyW') { this.keys.w = true; e.preventDefault(); }
if (e.code === 'KeyS') { this.keys.s = true; e.preventDefault(); }
});
document.addEventListener('keyup', (e) => {
if (e.code === 'KeyW') this.keys.w = false;
if (e.code === 'KeyS') this.keys.s = false;
});
},
getVerticalInput() {
if (this.keys.w) return -1;
if (this.keys.s) return 1;
return 0;
}
};
```
**Physics object (skeleton — full physics in Plan 02):**
```javascript
const Physics = {
width: 0,
height: 0,
init(width, height) {
this.width = width;
this.height = height;
// Position paddle1 on left side, vertically centered
GameState.paddle1.x = 30;
GameState.paddle1.y = height / 2 - GameState.paddle1.height / 2;
// Ball positioned center — physics will drive it in Plan 02
GameState.ball.x = width / 2;
GameState.ball.y = height / 2;
},
onResize(width, height) {
this.width = width;
this.height = height;
},
update(deltaTime) {
// Move paddle1 based on input
const dir = Input.getVerticalInput();
const paddle = GameState.paddle1;
paddle.y += dir * paddle.speed * deltaTime;
// Clamp to canvas bounds
paddle.y = Math.max(0, Math.min(this.height - paddle.height, paddle.y));
}
};
```
**GameLoop object:**
```javascript
const GameLoop = {
stopHandle: null,
lastTime: 0,
start() {
this.lastTime = performance.now();
this.stopHandle = window.requestAnimationFrame(this.main.bind(this));
},
stop() {
if (this.stopHandle) cancelAnimationFrame(this.stopHandle);
},
main(currentTime) {
this.stopHandle = window.requestAnimationFrame(this.main.bind(this));
const deltaTime = Math.min((currentTime - this.lastTime) / 1000, 0.05); // Cap at 50ms
this.lastTime = currentTime;
Physics.update(deltaTime);
Renderer.clear();
// Draw paddle1
const p1 = GameState.paddle1;
Renderer.drawRect(p1.x, p1.y, p1.width, p1.height, p1.color);
// Draw ball placeholder (will move in Plan 02)
const b = GameState.ball;
Renderer.drawCircle(b.x, b.y, b.radius, b.color);
}
};
```
**Initialization block (at bottom of script):**
```javascript
// Initialize all modules
Renderer.init();
Physics.init(Renderer.logicalWidth, Renderer.logicalHeight);
Input.init();
GameLoop.start();
```
The deltaTime cap of 50ms prevents the physics from exploding if the tab loses focus and resumes (a single large delta would fling the ball off screen).
</action>
<verify>
<automated>Open index.html in browser — white paddle visible on left side, white ball dot visible at center. Press W: paddle moves up smoothly. Press S: paddle moves down smoothly. Paddle stops at top/bottom canvas edges. No console errors. Holding W while resizing window should not break anything.</automated>
</verify>
<done>index.html renders a visible paddle on the left side that responds immediately to W/S keyboard input with no OS key-repeat lag. Ball dot is visible at center. Game loop running at device refresh rate. All four objects (GameLoop, Physics, Renderer, Input) exist and are initialized. Canvas remains HiDPI-correct on resize.</done>
</task>
</tasks>
<verification>
Open index.html in a browser:
1. Canvas fills entire window, black background, no scrollbars
2. White paddle visible on left side (x~30), white ball dot at center
3. Press W — paddle moves up immediately (no lag)
4. Press S — paddle moves down immediately (no lag)
5. Paddle stops at top and bottom edges (clamped)
6. Resize window — canvas resizes, paddle stays visible
7. On Retina/HiDPI display — paddle and ball edges are crisp, not blurry
8. Browser console shows zero errors
</verification>
<success_criteria>
- index.html exists and opens without errors in Chrome/Firefox/Safari
- Canvas fills window, renders sharp on HiDPI (VFX-05)
- Player 1 W/S input moves paddle responsively (CORE-07)
- GameLoop, Renderer, Physics, Input, GameState objects all exist in script scope
- Resize does not break layout or throw errors
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md` documenting:
- What was built (objects created, their responsibilities)
- Key implementation decisions (deltaTime cap, HiDPI resize approach, logical vs bitmap coords)
- Patterns established for Phase 2 to follow
- Any deviations from the plan and why
</output>

View File

@@ -0,0 +1,329 @@
---
phase: 01-foundation
plan: 02
type: execute
wave: 2
depends_on: [01-01]
files_modified: [index.html]
autonomous: true
requirements: [CORE-01, CORE-02, CORE-03, CORE-04]
must_haves:
truths:
- "Ball moves continuously across the canvas — it never stops"
- "Ball bounces off the top wall and bottom wall predictably (angle of incidence = angle of reflection)"
- "Ball deflects off Player 1 paddle when they collide"
- "Ball deflects at different angles depending on which zone of the paddle it hits (top edge ~60 degrees, center ~5 degrees)"
- "Ball speed increases after each paddle hit and resets when it passes the left or right edge"
- "Ball serves automatically in a random direction on start and after each reset"
artifacts:
- path: "index.html"
provides: "Complete Physics.update() with ball movement, wall bounce, paddle collision, zone deflection, speed increment"
contains: "ball.vx *= -1, zone, angles, ball.speed"
- path: "index.html"
provides: "Ball serve function that initializes ball velocity"
contains: "serveBall"
- path: "index.html"
provides: "Speed increment constant tunable via GameConfig"
contains: "GameConfig"
key_links:
- from: "Physics.update()"
to: "ball position"
via: "ball.x += ball.vx * deltaTime; ball.y += ball.vy * deltaTime"
pattern: "ball\\.x.*deltaTime"
- from: "wall bounce logic"
to: "ball.vy"
via: "Math.abs(ball.vy) inversion on top/bottom boundary crossing"
pattern: "Math\\.abs.*vy"
- from: "zone deflection"
to: "ball.vx / ball.vy"
via: "relativeHitPos -> hitZone -> angle -> cos/sin decomposition"
pattern: "relativeHitPos|hitZone|getPaddleAngle"
- from: "speed increment"
to: "ball.speed"
via: "ball.speed += GameConfig.speedIncrement on each paddle hit"
pattern: "speedIncrement"
---
<objective>
Complete the physics simulation: ball moves continuously, bounces off walls and paddles, deflects at zone-based angles, and accelerates with each rally. After this plan, the full Phase 1 experience is playable — Player 1 can hit a moving ball with angle control and watch speed build up over a rally.
Purpose: Completes Phase 1. All 6 success criteria become verifiable.
Output: Fully updated index.html with working ball physics replacing the Plan 01 skeleton.
</objective>
<execution_context>
@/home/dabit/.claude/get-shit-done/workflows/execute-plan.md
@/home/dabit/.claude/get-shit-done/templates/summary.md
</execution_context>
<context>
@.planning/ROADMAP.md
@.planning/phases/01-foundation/01-CONTEXT.md
@.planning/phases/01-foundation/01-RESEARCH.md
@.planning/phases/01-foundation/01-01-SUMMARY.md
</context>
<interfaces>
<!-- Key contracts from Plan 01 that this plan extends -->
GameState (from Plan 01):
```javascript
GameState.ball = {
x, y, // current position (logical pixels)
radius: 8,
vx, vy, // velocity (logical pixels per second)
speed, // current scalar speed (px/s)
color: '#fff'
};
GameState.paddle1 = {
x, y, // current position
width: 12, height: 80,
speed: 400, // movement speed (px/s)
color: '#fff'
};
```
Physics object (skeleton from Plan 01 — replace update() and init() fully):
```javascript
Physics.init(width, height) // called once at startup
Physics.onResize(width, height) // called on window resize
Physics.update(deltaTime) // called every frame — REPLACE THIS
```
Renderer helpers available:
```javascript
Renderer.drawRect(x, y, w, h, color)
Renderer.drawCircle(x, y, radius, color)
Renderer.logicalWidth
Renderer.logicalHeight
```
</interfaces>
<tasks>
<task type="auto">
<name>Task 1: Ball physics — movement, wall bounce, serve, and GameConfig</name>
<files>index.html</files>
<action>
Replace the Physics.init() and Physics.update() skeleton from Plan 01 with the full implementation. Also add a GameConfig object and serveBall() function.
**Add GameConfig object** (place before GameState in the script):
```javascript
const GameConfig = {
initialBallSpeed: 220, // px/s — starting speed per serve
speedIncrement: 18, // px/s added per paddle hit
paddleSpeed: 400 // px/s — Player 1 paddle movement
};
```
**Update Physics.init()** — initialize ball with a served velocity:
```javascript
init(width, height) {
this.width = width;
this.height = height;
// Position paddle1 on left side, vertically centered
GameState.paddle1.x = 30;
GameState.paddle1.y = height / 2 - GameState.paddle1.height / 2;
GameState.paddle1.speed = GameConfig.paddleSpeed;
// Serve ball from center
this.serveBall();
},
```
**Add serveBall() method to Physics:**
```javascript
serveBall() {
const ball = GameState.ball;
ball.x = this.width / 2;
ball.y = this.height / 2;
ball.speed = GameConfig.initialBallSpeed;
// Random vertical angle between -45 and +45 degrees, always heading right
const angle = (Math.random() * 90 - 45) * Math.PI / 180;
// Randomly serve left or right
const dir = Math.random() < 0.5 ? 1 : -1;
ball.vx = Math.cos(angle) * ball.speed * dir;
ball.vy = Math.sin(angle) * ball.speed;
},
```
**Replace Physics.update()** with full ball physics:
```javascript
update(deltaTime) {
const ball = GameState.ball;
const paddle = GameState.paddle1;
// --- Move paddle1 ---
const dir = Input.getVerticalInput();
paddle.y += dir * paddle.speed * deltaTime;
paddle.y = Math.max(0, Math.min(this.height - paddle.height, paddle.y));
// --- Move ball ---
ball.x += ball.vx * deltaTime;
ball.y += ball.vy * deltaTime;
// --- Wall bounce (top and bottom) ---
if (ball.y - ball.radius < 0) {
ball.y = ball.radius;
ball.vy = Math.abs(ball.vy);
}
if (ball.y + ball.radius > this.height) {
ball.y = this.height - ball.radius;
ball.vy = -Math.abs(ball.vy);
}
// --- Ball out of bounds (left or right) → reset ---
if (ball.x + ball.radius < 0 || ball.x - ball.radius > this.width) {
this.serveBall();
return;
}
// --- Paddle collision (only check when ball moving left toward paddle1) ---
if (ball.vx < 0) {
this._checkPaddleCollision(ball, paddle);
}
},
```
**Add _checkPaddleCollision() method to Physics:**
```javascript
_checkPaddleCollision(ball, paddle) {
// AABB: check ball center against paddle bounds (with ball radius buffer)
const inX = ball.x - ball.radius < paddle.x + paddle.width &&
ball.x + ball.radius > paddle.x;
const inY = ball.y + ball.radius > paddle.y &&
ball.y - ball.radius < paddle.y + paddle.height;
if (!inX || !inY) return;
// Zone-based deflection: divide paddle into 5 zones
const relativeHitPos = (ball.y - paddle.y) / paddle.height;
const hitZone = Math.max(0, Math.min(4, Math.floor(relativeHitPos * 5)));
// Map zone to angle (degrees) — positive = downward
const anglesDeg = [
-60, // Zone 0: top edge — steeply upward
-30, // Zone 1: upper — angled upward
5, // Zone 2: center — nearly flat (slight downward to avoid infinite horizontal)
30, // Zone 3: lower — angled downward
60 // Zone 4: bottom edge — steeply downward
];
const angleRad = anglesDeg[hitZone] * Math.PI / 180;
// Increment speed
ball.speed += GameConfig.speedIncrement;
// Decompose into velocity components, always going right after hitting paddle1
ball.vx = Math.cos(angleRad) * ball.speed;
ball.vy = Math.sin(angleRad) * ball.speed;
// Push ball out of paddle to prevent double-collision next frame
ball.x = paddle.x + paddle.width + ball.radius + 1;
},
```
After implementing, also update the GameLoop.main() render block to draw the ball using `GameState.ball` (it already does this from Plan 01 — confirm it still works with the new state).
</action>
<verify>
<automated>Open index.html in browser — ball should immediately start moving from center. Observe: (1) ball bounces off top wall, (2) ball bounces off bottom wall, (3) when ball exits left or right edge it reappears at center with a new serve direction. Console.log not required but no errors should appear.</automated>
</verify>
<done>Ball moves continuously at ~220 px/s initial speed, bounces off top and bottom walls, and serves from center on load and after going out of bounds left/right. Browser console shows zero errors.</done>
</task>
<task type="auto">
<name>Task 2: Paddle-ball collision refinement and speed acceleration verification</name>
<files>index.html</files>
<action>
With ball physics running, verify and refine collision behavior:
**1. Test and tune zone deflection angles:**
Hit the paddle at the top edge — ball should deflect upward steeply (~60 degrees from horizontal).
Hit the paddle at center — ball should deflect nearly flat (~5 degrees).
Hit the paddle at the bottom edge — ball should deflect downward steeply (~60 degrees).
If angles feel wrong, adjust the `anglesDeg` array values within these constraints from CONTEXT.md:
- Top/bottom edge: ~60 degrees
- Upper/lower zone: ~30 degrees
- Center: ~5 degrees (not exactly 0 — avoids infinite horizontal loop)
**2. Add speed display for verification (debug only):**
In GameLoop.main(), after Renderer.clear() and before drawing objects, add:
```javascript
// Debug: show ball speed (remove after Phase 1 verification)
Renderer.ctx.fillStyle = 'rgba(255,255,255,0.4)';
Renderer.ctx.font = '14px monospace';
Renderer.ctx.fillText('speed: ' + Math.round(GameState.ball.speed), 10, 20);
```
This lets the tester confirm speed increases by reading the on-canvas number during play.
**3. Verify speed resets on serve:**
After ball exits bounds, `serveBall()` sets `ball.speed = GameConfig.initialBallSpeed`. Confirm the debug display resets to 220 (or the configured initial speed) after each out-of-bounds.
**4. Confirm no tunneling at high speed:**
After ~10 paddle hits the ball will be moving at ~220 + (10 * 18) = 400 px/s. At 60 FPS, that's ~6.7 px/frame. Paddle height is 80px — well within safe range (max ~4800 px/s before tunneling risk at 60 FPS). Document in code comment:
```javascript
// Speed is uncapped per CONTEXT.md. Tunneling risk begins when ball travels
// more than paddle.height px/frame. At 60fps, that's paddle.height * 60 px/s.
// With height=80: tunneling risk above ~4800 px/s. Current rallies max ~800px/s.
// If Phase 5 introduces higher speeds, add substep physics here.
```
Place this comment inside `_checkPaddleCollision()`.
**5. Remove debug speed display before final commit** — or keep it if the plan verifier needs to confirm speed increase. Keep it for Phase 1 verification; it costs nothing.
</action>
<verify>
<automated>
Open index.html in browser and perform these manual checks:
1. Ball bounces continuously without stopping — CORE-01
2. Move paddle into ball path — ball deflects off paddle — CORE-02
3. Hit ball at top of paddle — steep upward angle. Hit at center — flat. Hit at bottom — steep downward — CORE-03
4. Rally 10+ times — speed display number increases each hit, resets to 220 on out-of-bounds — CORE-04
5. Press W — paddle moves up immediately. Press S — paddle moves down immediately — CORE-07
6. On HiDPI display — ball edges are crisp — VFX-05
</automated>
</verify>
<done>
All Phase 1 success criteria verified:
- Canvas renders sharply on Retina/HiDPI (VFX-05)
- Ball moves continuously and bounces off top/bottom walls (CORE-01)
- Player 1 paddle deflects ball (CORE-02)
- Ball angle changes per paddle hit zone (CORE-03)
- Ball speed increases each paddle hit, resets on serve (CORE-04)
- W/S keys move paddle responsively (CORE-07)
</done>
</task>
</tasks>
<verification>
Open index.html in browser and verify all 5 Phase 1 success criteria:
1. Canvas renders sharply on HiDPI — no blurry edges on ball or paddle
2. Ball moves continuously from first serve — bounces off top and bottom walls without slowing
3. Player 1 paddle moves up on W, down on S — immediate response, no lag
4. Hit ball at top/center/bottom of paddle — observe three distinctly different deflection angles
5. Rally 10 times — on-canvas speed display shows increasing value; on serve, resets to initial speed
Also verify: window resize does not break ball trajectory or throw console errors.
</verification>
<success_criteria>
- Ball moves and bounces continuously (CORE-01)
- Paddle deflects ball correctly (CORE-02)
- Zone-based angle deflection produces 5 visually distinct trajectories (CORE-03)
- Speed increments per hit, resets per serve — confirmed via on-canvas debug display (CORE-04)
- All requirements CORE-01, CORE-02, CORE-03, CORE-04 met
- index.html runs as a single static file with no build step
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md` documenting:
- Physics implementation details (speed values, angle map, tunneling comment)
- GameConfig constants and their rationale
- How Phase 2 should extend Physics (add second paddle, scoring — without touching zone deflection logic)
- Any tuning done to angles or speed increment during verification
</output>