Files
gijs_pong/.planning/phases/01-foundation/01-01-PLAN.md
2026-03-10 14:42:41 +01:00

11 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-foundation 01 execute 1
index.html
true
VFX-05
CORE-07
truths artifacts key_links
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
path provides contains
index.html Complete HTML scaffold with embedded CSS, canvas element, and all four module objects GameLoop, Physics, Renderer, Input
path provides contains
index.html HiDPI canvas setup — devicePixelRatio scaling applied on init and resize devicePixelRatio
path provides contains
index.html Full-window canvas with aspect ratio enforcement and resize handler window.addEventListener('resize'
path provides contains
index.html Keyboard input state tracking for W/S keys KeyW, KeyS
from to via pattern
GameLoop.main() requestAnimationFrame window.requestAnimationFrame(this.main.bind(this)) requestAnimationFrame
from to via pattern
Renderer.init() devicePixelRatio canvas bitmap scale + ctx.scale(dpr, dpr) devicePixelRatio
from to via pattern
Input.getVerticalInput() GameLoop update cycle Physics.update() queries Input.getVerticalInput() each frame getVerticalInput
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.

<execution_context> @/home/dabit/.claude/get-shit-done/workflows/execute-plan.md @/home/dabit/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-RESEARCH.md Task 1: HTML scaffold, HiDPI canvas, and full-window Renderer index.html Create index.html from scratch. It must contain exactly:

HTML structure:

<!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>):

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). Open index.html in browser — canvas fills window, background is black, no scrollbars. Resize window — canvas adjusts. No JS console errors. 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.

Task 2: GameLoop, Input module, and Player 1 paddle rendering index.html Add to the existing `<script>` tag in index.html (after Renderer, before initialization code):

GameState object — holds all mutable game state:

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:

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):

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:

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):

// 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). 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. 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.

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

<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>
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