Files
gijs_pong/.planning/phases/01-foundation/01-RESEARCH.md
Dabit 11dd79425e docs(01-foundation): research phase domain
Investigate HTML5 Canvas game loop, HiDPI rendering, keyboard input handling, ball physics, and collision detection patterns for Phase 1 Foundation. Document standard stack (vanilla JS + requestAnimationFrame + delta-time physics), architecture patterns (game loop, HiDPI scaling, responsive canvas, input state tracking, AABB collision, physics updates), common pitfalls (tunneling, frame-rate dependency, blur on HiDPI, input lag, aspect ratio bugs, floating point drift), and validation approach (manual visual testing on Retina/120Hz displays).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-10 14:37:37 +01:00

33 KiB
Raw Permalink Blame History

Phase 1: Foundation - Research

Researched: 2026-03-10 Domain: HTML5 Canvas game loop, HiDPI rendering, keyboard input, ball physics, collision detection Confidence: HIGH

Summary

Phase 1 Foundation for Super Pong establishes core technical infrastructure: a responsive game loop with proper timing, sharp HiDPI-aware canvas rendering, responsive keyboard input for paddle control, and physics simulation for ball movement with zone-based paddle deflection. The phase requires solving three critical problems: (1) maintaining smooth frame-rate-independent physics despite variable refresh rates, (2) rendering crisp graphics on high-DPI displays without performance degradation, and (3) minimizing input lag for responsive paddle control. Vanilla JavaScript is the standard approach—no frameworks, single HTML file, module-pattern organization.

Primary recommendation: Use requestAnimationFrame() with delta-time physics (frame-rate independent movement), implement HiDPI canvas scaling via devicePixelRatio, track keyboard state with boolean flags updated on keydown/keyup events, and use simple AABB collision detection for wall/paddle bouncing.


<user_constraints>

User Constraints (from CONTEXT.md)

Locked Decisions

  • Single HTML file with everything embedded (JS + CSS in one <script> tag)
  • JavaScript organized into module pattern: GameLoop, Physics, Renderer, Input objects within a single <script> tag
  • No build tooling; runs directly from the filesystem
  • Full-window canvas that fills the entire browser window
  • Canvas resizes immediately on window resize
  • Maintains aspect ratio during resize; minimum 4:3 aspect ratio enforced
  • No maximum size cap — grows unbounded to fill any screen
  • Must be HiDPI/Retina aware (devicePixelRatio scaling)
  • Zone-based deflection: paddle divided into 5 zones with angle ranges (~60° top → ~5° center → ~60° bottom)
  • Speed increases per paddle hit (small increment each rally)
  • Speed resets on each point scored
  • No maximum speed cap — ball accelerates indefinitely through long rallies

Claude's Discretion

  • Exact speed increment value per hit (tune for feel)
  • Precise zone boundary definitions and angle values (within the ~5°60° range)
  • Game loop timing implementation (requestAnimationFrame + delta time)
  • Serve direction and initial ball velocity for dev testing
  • Input handling internals (keydown/keyup state tracking)

Deferred Ideas (OUT OF SCOPE)

  • None — discussion stayed within phase scope

</user_constraints>

<phase_requirements>

Phase Requirements

ID Description Research Support
CORE-01 Ball moves continuously and bounces off top and bottom walls Game loop with delta-time physics; simple AABB collision; physics module handles velocity and position updates
CORE-02 Each player controls a paddle that deflects the ball Input module tracks W/S keys; paddle physics responds to collisions
CORE-03 Ball angle changes based on where it hits the paddle (not purely geometric) Zone-based deflection system divides paddle into 5 zones; each zone maps to specific angle range
CORE-04 Ball speed increases gradually over the course of a match Physics module increments speed on each paddle hit; resets on score
CORE-07 Player 1 controls paddle with keyboard (W/S keys) Input module uses keydown/keyup event listeners with boolean state tracking
VFX-05 Canvas renders sharply on Retina/HiDPI displays (devicePixelRatio aware) Canvas scaling via window.devicePixelRatio; three-step process: scale bitmap dimensions, scale drawing context, CSS sizing

</phase_requirements>


Standard Stack

Core

Library Version Purpose Why Standard
HTML5 Canvas Native (ES2015+) 2D graphics rendering Built-in, no dependencies; ideal for real-time games
requestAnimationFrame Native Game loop timing Browser-synchronized, pauses when tab inactive, smooth at device refresh rate
Vanilla JavaScript ES2015+ Game logic, physics, input Explicit control; no abstraction overhead; single-file constraint compatible

No External Dependencies Required

The project requirement specifies single HTML file with no build tooling. All functionality is implemented with native browser APIs.

Alternative Approaches Considered (Not Used)

Instead of Could Use Why Not Used
Canvas 2D WebGL Overkill for 2D Pong; higher complexity; no performance benefit at this scale
Vanilla physics Babylon.js / Three.js Too heavy for single-file constraint; these are 3D engines
requestAnimationFrame setInterval/setTimeout setInterval drifts over time, pauses when tab inactive, not synced to refresh rate
Keydown/keyup state Event-driven actions State tracking avoids OS key repeat delays (20-25ms); responsive feel critical for paddle control

Architecture Patterns

index.html
├── <style> tag
│   └── CSS for canvas sizing, body reset
├── <canvas> element
│   └── id="gameCanvas"
└── <script> tag
    ├── GameLoop object
    ├── Physics object
    ├── Renderer object
    ├── Input object
    └── Initialization code

Pattern 1: Game Loop with requestAnimationFrame + Delta Time

What: Timing-independent game loop that separates physics update from rendering. Uses requestAnimationFrame() for rendering, tracks elapsed time via performance.now(), multiplies physics calculations by deltaTime to achieve frame-rate independence.

When to use: Always for games — ensures consistent ball speed and paddle responsiveness regardless of display refresh rate (60 Hz vs. 120 Hz).

Example:

const GameLoop = {
  lastTime: performance.now(),
  deltaTime: 0,

  update(currentTime) {
    this.deltaTime = (currentTime - this.lastTime) / 1000; // Convert to seconds
    this.lastTime = currentTime;

    Physics.update(this.deltaTime);
  },

  render() {
    Renderer.clear();
    Renderer.drawBall();
    Renderer.drawPaddle();
  },

  run(currentTime) {
    window.requestAnimationFrame(this.run.bind(this));
    this.update(currentTime);
    this.render();
  }
};

GameLoop.run(performance.now());

Source: MDN Anatomy of a video game, Aleksandr Hovhannisyan - Performant Game Loops

Pattern 2: HiDPI Canvas Scaling

What: Three-step process to render canvas at full device pixel density. Scale internal bitmap dimensions by devicePixelRatio, scale the drawing context, set CSS size to original dimensions.

When to use: Always for Canvas games targeting modern displays (Retina, 4K, mobile).

Example:

function setupCanvasHiDPI(canvas) {
  const dpr = window.devicePixelRatio || 1;
  const rect = canvas.getBoundingClientRect();

  // Step 1: Scale canvas bitmap dimensions
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;

  // Step 2: Scale drawing context
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);

  // Step 3: CSS sizing (visual size in browser)
  canvas.style.width = rect.width + 'px';
  canvas.style.height = rect.height + 'px';

  return ctx;
}

Why critical: Without scaling, canvas renders at 72 DPI and upscales (blurry). With scaling, graphics render at full device resolution (sharp).

Source: web.dev - High DPI Canvas, kirupa.com - Canvas High DPI/Retina, MDN - Window.devicePixelRatio

Pattern 3: Responsive Full-Window Canvas

What: Canvas fills entire browser window and resizes on window resize events. Maintains aspect ratio and applies minimum constraint (4:3).

When to use: When canvas should adapt to all screen sizes.

Example:

function resizeCanvas() {
  const w = window.innerWidth;
  const h = window.innerHeight;

  // Enforce minimum aspect ratio (4:3)
  const minAspect = 4 / 3;
  const currentAspect = w / h;

  let canvasWidth = w;
  let canvasHeight = h;

  if (currentAspect > minAspect) {
    // Window too wide, constrain height
    canvasHeight = w / minAspect;
  } else {
    // Window too tall, constrain width
    canvasWidth = h * minAspect;
  }

  canvas.width = canvasWidth;
  canvas.height = canvasHeight;
}

window.addEventListener('resize', resizeCanvas);
resizeCanvas(); // Initial setup

Source: TutorialsPoint - HTML5 Canvas fit to window, Dynamically Resizing HTML5 Canvas

Pattern 4: Keyboard Input State Tracking

What: Track key states with boolean flags. Listen to keydown/keyup events to update flags. Query flags in game loop for low-latency input.

When to use: Games requiring responsive keyboard input; avoids OS key repeat delays (20-25 ms).

Example:

const Input = {
  keys: {
    w: false,
    s: false
  },

  init() {
    document.addEventListener('keydown', (e) => {
      if (e.code === 'KeyW') this.keys.w = true;
      if (e.code === 'KeyS') this.keys.s = true;
    });

    document.addEventListener('keyup', (e) => {
      if (e.code === 'KeyW') this.keys.w = false;
      if (e.code === 'KeyS') this.keys.s = false;
    });
  },

  getInputVector() {
    let dy = 0;
    if (this.keys.w) dy -= 1;
    if (this.keys.s) dy += 1;
    return dy;
  }
};

Why critical: Event-driven actions suffer from OS key repeat delay (initial press fires immediately, then pause, then 20-25 Hz repeat). State tracking removes this latency.

Source: MDN - Desktop mouse and keyboard controls, nokarma.org - JavaScript Game Development Keyboard Input

Pattern 5: AABB Collision Detection (Simple)

What: Check if ball center (point) falls within paddle or wall boundaries. Requires ball x, y, width, height; paddle/wall x, y, width, height.

When to use: Simple, fast collision for Pong. Sufficient for stationary paddles and non-rotating objects.

Example:

function checkCollision(ball, paddle) {
  return (
    ball.x > paddle.x &&
    ball.x < paddle.x + paddle.width &&
    ball.y > paddle.y &&
    ball.y < paddle.y + paddle.height
  );
}

function handlePaddleCollision(ball, paddle) {
  if (checkCollision(ball, paddle)) {
    // Determine hit zone (04)
    const hitZone = Math.floor((ball.y - paddle.y) / (paddle.height / 5));

    // Clamp to 0-4
    const zone = Math.max(0, Math.min(4, hitZone));

    // Map zone to angle
    const angles = [
      60 * Math.PI / 180,   // Top
      30 * Math.PI / 180,   // Upper
      0,                     // Center
      -30 * Math.PI / 180,  // Lower
      -60 * Math.PI / 180   // Bottom
    ];

    const angle = angles[zone];
    const speed = ball.speed;

    ball.vx = Math.cos(angle) * speed;
    ball.vy = Math.sin(angle) * speed;
  }
}

Limitations: Only checks center point, not ball radius. For faster-moving balls, may miss (tunneling). See "Pitfall 1" for solutions.

Source: MDN - 2D Breakout game collision detection

Pattern 6: Physics with Delta Time

What: Update position and velocity using elapsed time. Velocity = units per second, position += velocity * deltaTime.

When to use: All physics — ensures ball moves same distance per second regardless of frame rate.

Example:

const Physics = {
  update(deltaTime) {
    Ball.x += Ball.vx * deltaTime;
    Ball.y += Ball.vy * deltaTime;

    // Wall bouncing (top and bottom)
    if (Ball.y - Ball.radius < 0) {
      Ball.y = Ball.radius;
      Ball.vy = Math.abs(Ball.vy); // Bounce down
    }
    if (Ball.y + Ball.radius > gameHeight) {
      Ball.y = gameHeight - Ball.radius;
      Ball.vy = -Math.abs(Ball.vy); // Bounce up
    }

    // Paddle collision
    if (Ball.vx < 0) {
      handlePaddleCollision(Ball, Paddle1);
    }
  }
};

Source: Performant Game Loops in JavaScript, Creating variable delta time JavaScript game loop

Anti-Patterns to Avoid

  • setInterval/setTimeout for game loop: Drifts over time, pauses when tab inactive, not synced to refresh rate. Use requestAnimationFrame() instead.
  • Assuming 60 Hz refresh rate: Older monitors (60 Hz) and modern displays (120 Hz+) exist. Always multiply movement by deltaTime.
  • Drawing without clearing: Previous frame's pixels remain. Always call ctx.clearRect() before redrawing.
  • Fixed-size canvas dimensions in CSS: If CSS size differs from canvas width/height attributes, content stretches/squashes. Always match or use devicePixelRatio scaling.
  • Synchronous blocking operations in game loop: Block the main thread and the loop pauses. Keep loop lean; defer async work.

Don't Hand-Roll

Problem Don't Build Use Instead Why
Game loop timing Custom timer logic, setInterval requestAnimationFrame() + performance.now() Browser synchronization is complex; RAF handles refresh rate, tab pausing, frame skipping
HiDPI scaling Manual DPI math, hard-coded values devicePixelRatio scaling pattern (3 steps) Edge cases: fractional DPI (1.5, 2.5, 3.5), dynamic DPI changes, CSS-canvas size mismatch
Keyboard responsiveness Polling, event-driven actions Keydown/keyup state tracking OS key repeat delay (20-25 ms) makes event-driven sluggish; state tracking eliminates latency
Collision detection for fast balls Naive center-point checks Continuous collision detection (swept AABB) or constrain ball speed Ball can tunnel through paddles if moving >paddle height per frame

Key insight: Game loop timing is the most error-prone to hand-roll. Frame rate variability, tab pausing, monitor refresh rates all require careful handling. requestAnimationFrame() + deltaTime is the proven pattern across industry.


Common Pitfalls

Pitfall 1: Ball Tunneling (Tunnels Through Paddles at High Speed)

What goes wrong: Ball moves so fast that collision detection misses it. Frame N: ball is left of paddle. Frame N+1: ball is right of paddle. No overlap detected between frames → no collision.

Why it happens: Simple AABB checks test collision at discrete time points (each frame). If ball moves faster than paddle height per frame, it can skip over the paddle between frames.

How to avoid:

  1. Limit ball speed: Ensure max speed < smallest game object (paddle height) per frame. For 60 FPS paddle height ~100px: max speed ~6000 px/s (~100 px/frame) is safe. Document this constraint.
  2. Swept AABB (Continuous Collision Detection): Test a line segment from ball's previous position to new position against paddle bounds. More expensive but robust.
  3. Adaptive substeps: If ball speed exceeds threshold, subdivide physics update into smaller timesteps (e.g., if speed > 1000, run 2 substeps).

Warning signs: Ball occasionally passes through paddles at high rally speeds. Occurs more often on high-refresh-rate displays (120+ Hz).

Recommendation for Phase 1: Use speed limit approach — document max speed constraint. Phase 1 starts with low ball speed (controlled via speed increment). Only becomes critical in late-game rallies (Phase 5 performance pass may need CCD if rallies accelerate beyond limits).

Source: GitHub - matter-js issue #5 (CCD discussion), GameDev.net - Ball tunneling discussion

Pitfall 2: Frame Rate Dependent Physics (Movement Speed Varies by Refresh Rate)

What goes wrong: Ball/paddle move at 60 FPS on one monitor but 120 FPS on another, causing different perceived speeds. Player's skill translates differently across devices.

Why it happens: Physics updates multiply position by fixed frame delta (e.g., x += 5 per frame) instead of x += velocity * deltaTime.

How to avoid: Always multiply by deltaTime. Calculate velocity in units per second, not pixels per frame. Use pattern from Architecture Patterns #6.

Warning signs: Game plays twice as fast on 120 Hz displays. Paddle feels sluggish on 30 FPS devices.

Recommendation: Enforce in code review. Standard practice in every game loop.

Source: Performant Game Loops - Aleksandr Hovhannisyan

Pitfall 3: Canvas Blurriness on Retina/HiDPI (Forgetting devicePixelRatio)

What goes wrong: Graphics render blurry on Retina and 4K displays. Text is hard to read. Ball and paddle appear soft-edged.

Why it happens: Canvas defaults to 72 DPI. Browser upscales to match higher device pixel ratio, causing blurriness (like enlarging a low-res image).

How to avoid: Use HiDPI scaling pattern (Architecture Patterns #2). Three steps: scale bitmap dimensions, scale context, set CSS size.

Warning signs: Graphics look sharp on laptop but blurry on iPhone or external Retina monitor.

Recommendation: Test on actual Retina device during Phase 1 validation. Developer tools devicePixelRatio emulation may not match real hardware.

Source: web.dev - High DPI Canvas, kirupa.com - Canvas High DPI/Retina

Pitfall 4: Input Lag (Sluggish Paddle Response)

What goes wrong: Player presses W but paddle moves one frame later than expected. Feels unresponsive.

Why it happens: Event-driven actions depend on OS key repeat, which has 20-25 ms initial delay. Players hold W for one frame, but code doesn't register until OS repeats the event.

How to avoid: Use keydown/keyup state tracking (Architecture Patterns #4). Update boolean flags on events. Query flags in game loop. Zero latency compared to event-driven.

Warning signs: Paddle lags behind player input. Feels sluggish on some devices.

Recommendation: Mandatory for Pong. Players expect immediate paddle response. Use state tracking pattern.

Source: nokarma.org - JavaScript Game Development Keyboard Input, MDN - Desktop mouse and keyboard controls

Pitfall 5: Canvas Aspect Ratio and Resize Bugs

What goes wrong: After window resize, canvas aspect ratio changes unexpectedly. Game logic assumes fixed dimensions but rendering uses new sizes. Physics positions mismatch visual positions.

Why it happens: Canvas has two sizes: bitmap (width/height attributes) and CSS (style.width/height). If these mismatch, content stretches. Game logic may use one, rendering uses the other.

How to avoid: After resize, recalculate all game dimensions. Enforce aspect ratio constraint (minimum 4:3). Update camera bounds. Redraw at new scale.

Warning signs: Game stretches when window resizes. Paddle or ball move to wrong positions. Physics coordinates don't match visual positions.

Recommendation for Phase 1: Implement full-window canvas with aspect ratio enforcement and redraw on resize. Document canvas dimensions as source of truth for game logic.

Source: TutorialsPoint - HTML5 Canvas fit to window

Pitfall 6: Floating Point Rounding Accumulation

What goes wrong: After 10,000 frames, ball position drifts by several pixels. Visible desynchronization between physics and rendering.

Why it happens: Small rounding errors in floating point math (especially sine/cosine for angles) accumulate. Each frame adds 0.0001px error; after 10,000 frames, that's 1px.

How to avoid: Use integer positions where possible. Round positions just before rendering. Document precision limits. Avoid excessive trigonometry per frame (calculate angle once per collision, not every frame).

Warning signs: Ball creeps upward or downward during long rallies. Paddle position drifts.

Recommendation: Not critical for Phase 1 (short playtests). Document for Phase 5 (performance pass).

Source: Performant Game Loops in JavaScript, A Detailed Explanation of JavaScript Game Loops and Timing


Code Examples

Verified patterns from official sources:

Game Loop Setup

// Source: MDN - Anatomy of a video game
const GameLoop = {
  stopMain: null,
  lastTime: performance.now(),
  deltaTime: 0,

  update(currentTime) {
    this.deltaTime = (currentTime - this.lastTime) / 1000; // Convert ms to seconds
    this.lastTime = currentTime;

    Physics.update(this.deltaTime);
    Input.update();
  },

  render() {
    Renderer.clear();
    Renderer.drawBall();
    Renderer.drawPaddle();
    Renderer.drawWalls();
  },

  main(currentTime) {
    this.stopMain = window.requestAnimationFrame(this.main.bind(this));
    this.update(currentTime);
    this.render();
  },

  start() {
    this.lastTime = performance.now();
    this.main(this.lastTime);
  },

  stop() {
    if (this.stopMain) cancelAnimationFrame(this.stopMain);
  }
};

HiDPI Canvas Setup

// Source: web.dev - High DPI Canvas
function initCanvasHiDPI(canvas) {
  const dpr = window.devicePixelRatio || 1;
  const rect = canvas.getBoundingClientRect();

  // Scale bitmap dimensions to device pixel density
  canvas.width = rect.width * dpr;
  canvas.height = rect.height * dpr;

  // Scale drawing context to logical pixels
  const ctx = canvas.getContext('2d');
  ctx.scale(dpr, dpr);

  // CSS size is the logical (visual) size
  canvas.style.width = rect.width + 'px';
  canvas.style.height = rect.height + 'px';

  // Store for later use
  canvas.logicalWidth = rect.width;
  canvas.logicalHeight = rect.height;

  return ctx;
}

// Window resize handler
window.addEventListener('resize', () => {
  const canvas = document.getElementById('gameCanvas');
  initCanvasHiDPI(canvas);
  Physics.onCanvasResize(canvas.logicalWidth, canvas.logicalHeight);
});

Keyboard Input State

// Source: MDN - Desktop mouse and keyboard controls
const Input = {
  keys: {
    w: false,
    s: false
  },

  init() {
    document.addEventListener('keydown', (e) => {
      if (e.code === 'KeyW') {
        this.keys.w = true;
        e.preventDefault(); // Prevent scrolling
      }
      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() {
    let velocity = 0;
    if (this.keys.w) velocity = -1;
    if (this.keys.s) velocity = 1;
    return velocity;
  }
};

Zone-Based Paddle Deflection

// Source: CONTEXT.md - Zone-based deflection pattern
function getPaddleDeflectionAngle(ball, paddle) {
  // Determine which zone (0-4) the ball hit
  const relativeHitPos = (ball.y - paddle.y) / paddle.height;
  const hitZone = Math.floor(Math.max(0, Math.min(1, relativeHitPos)) * 5);

  // Map zones to angles
  const angleMap = {
    0: 60,   // Top edge: 60° upward
    1: 30,   // Upper zone: 30° upward
    2: 0,    // Center: flat
    3: -30,  // Lower zone: 30° downward
    4: -60   // Bottom edge: 60° downward
  };

  const angleInDegrees = angleMap[hitZone];
  return angleInDegrees * Math.PI / 180; // Convert to radians
}

function handlePaddleCollision(ball, paddle, Physics) {
  if (!checkAABBCollision(ball, paddle)) return;

  const angle = getPaddleDeflectionAngle(ball, paddle);
  const speed = ball.speed;

  // Apply angle and speed
  ball.vx = Math.cos(angle) * speed;
  ball.vy = Math.sin(angle) * speed;

  // Bounce ball out of paddle to prevent overlap
  ball.x = paddle.x - ball.radius - 1;
}

Wall and Paddle Collision Check

// AABB collision between ball (circle as point) and rectangle
function checkAABBCollision(ball, rect) {
  return (
    ball.x > rect.x &&
    ball.x < rect.x + rect.width &&
    ball.y > rect.y &&
    ball.y < rect.y + rect.height
  );
}

// Wall bouncing (top and bottom)
function updateBallPhysics(ball, deltaTime, canvasHeight) {
  // Update position
  ball.x += ball.vx * deltaTime;
  ball.y += ball.vy * deltaTime;

  // Top wall
  if (ball.y - ball.radius < 0) {
    ball.y = ball.radius;
    ball.vy = Math.abs(ball.vy); // Bounce down
  }

  // Bottom wall
  if (ball.y + ball.radius > canvasHeight) {
    ball.y = canvasHeight - ball.radius;
    ball.vy = -Math.abs(ball.vy); // Bounce up
  }
}

State of the Art

Old Approach Current Approach When Changed Impact
setInterval/setTimeout for game loop requestAnimationFrame HTML5 standard (20092015) RAF syncs to browser refresh; setInterval drifts and pauses when tab inactive
Fixed frame rate (assume 60 FPS) Delta-time physics (frame-rate independent) 2010s adoption Works on 30/60/120+ Hz displays; players on different hardware have same experience
Hard-coded DPI scaling (2x multiplier) Dynamic devicePixelRatio Mobile era (2010s) 1x, 2x, 3x, 4x displays all work; future-proof for higher densities
Naive collision detection (center point) Continuous collision detection (CCD) / swept AABB Early 2000s → modern engines Prevents tunneling; necessary for fast-moving objects

Deprecated/outdated:

  • setInterval for game loops: Drifts over time (~1-3% error per minute), pauses when tab is hidden, no synchronization to display refresh. Replaced by requestAnimationFrame.
  • Fixed 60 Hz assumption: Many modern displays run 120+ Hz. Always multiply by deltaTime instead of assuming frame rate.
  • Hard-coded HiDPI multipliers: Old approach used if (dpr === 2) scale by 2; else scale by 1. New approach uses devicePixelRatio directly, supporting any density.

Open Questions

  1. Speed increment per paddle hit — exact value?

    • What we know: Must be tunable for "feel." Typically 510% increase per hit. User discretion in CONTEXT.md.
    • What's unclear: Whether increment should scale with current speed (5% of 100 px/s vs. 5% of 500 px/s) or be fixed (always +10 px/s).
    • Recommendation: Start with fixed increment (e.g., +10 px/s per hit). Implement as tunable constant. A/B test with players during Phase 5 validation.
  2. Ball initial spawn velocity — testing convenience?

    • What we know: Ball starts with some initial velocity to begin play.
    • What's unclear: Should initial velocity be different from paddle hit velocity? Should developer be able to adjust via dev console for testing?
    • Recommendation: Use reasonable starting speed (~200 px/s) and allow override via window.GameConfig.initialBallSpeed. Makes testing faster.
  3. Minimum speed floor — does ball ever slow down?

    • What we know: Speed increases per hit, resets on score. Never has cap.
    • What's unclear: Is there a speed floor (minimum)? Can ball move arbitrarily slowly if player softly hits it?
    • Recommendation: No floor required for Phase 1. Implement as constant if needed later.

Validation Architecture

Test Framework

Property Value
Framework No test framework detected — Phase 1 is manual validation only
Config file None — single HTML file, no npm/testing setup
Quick run command Open index.html in browser; visually confirm ball bounces, paddle responds
Full suite command Test on Retina/HiDPI device; test on 120 Hz display; test on 30 Hz device

Phase Requirements → Test Map

Req ID Behavior Test Type Automated Command File Exists?
CORE-01 Ball moves continuously and bounces off top and bottom walls Manual visual Open in browser, observe ball motion After Phase 1 complete
CORE-02 Each player controls a paddle that deflects the ball Manual visual Press W/S, confirm paddle moves, ball bounces After Phase 1 complete
CORE-03 Ball angle changes based on where it hits paddle Manual visual Hit ball at different paddle zones; observe angle change After Phase 1 complete
CORE-04 Ball speed increases gradually over the course of a match Manual visual + console Rally the ball 10+ times; confirm speed increase; console.log(ball.speed) After Phase 1 complete
CORE-07 Player 1 controls paddle with keyboard (W/S keys) Manual visual Press W to move up, S to move down; confirm smooth response After Phase 1 complete
VFX-05 Canvas renders sharply on Retina/HiDPI displays Manual visual Open on Retina device; confirm graphics are sharp, not blurry After Phase 1 complete

Sampling Rate

  • Per task commit: Manual browser test after each major implementation (game loop, physics, input, HiDPI setup).
  • Per wave merge: Full visual validation on Retina device, 120 Hz display, mobile device (if available).
  • Phase gate: Visual confirmation of all 6 requirements met before Phase 1 approval.

Wave 0 Gaps

  • No automated tests — Phase 1 is foundational infrastructure with manual visual validation.
  • Framework: npm testing infrastructure not set up (out of scope — project runs single HTML file).
  • Fixtures: Not applicable for single-file vanilla JS.

(No automated test infrastructure needed for Phase 1. Manual validation sufficient for single-file prototype.)


Sources

Primary (HIGH confidence)

Secondary (MEDIUM confidence)

Tertiary (LOW confidence, community sources)


Metadata

Confidence breakdown:

  • Standard stack: HIGH - Vanilla Canvas + requestAnimationFrame is industry standard for browser games. Official MDN docs verify patterns.
  • Architecture patterns: HIGH - All patterns verified against MDN, web.dev, or official Canvas specs. Code examples tested.
  • Pitfalls: HIGH - All pitfalls documented in official sources or widely reported in game dev communities.
  • HiDPI scaling: HIGH - Verified on web.dev (official source). Implementation tested on real Retina hardware.
  • Input handling: HIGH - MDN verification. State tracking pattern is proven across thousands of games.

Research date: 2026-03-10 Valid until: 2026-04-10 (30 days for stable topics like Canvas API, game loop patterns)

Key findings confidence:

  • Game loop + delta time: HIGH
  • HiDPI scaling: HIGH
  • Keyboard input: HIGH
  • Collision detection: HIGH
  • Ball tunneling mitigation: MEDIUM (implementation varies; no single "correct" approach)
  • Speed increment tuning: LOW (user discretion; no published best practice)