kc4-checklist

star 0

Pre-implementation checklist for keyboard-command-4 sessions. Prints the coding rules most likely to cause bugs. Run this before writing any session code.

a1flecke By a1flecke schedule Updated 2/27/2026

name: kc4-checklist description: Pre-implementation checklist for keyboard-command-4 sessions. Prints the coding rules most likely to cause bugs. Run this before writing any session code.

Read the session file, then confirm each rule below before writing code.

Spec Code Has Bugs — Fix Before Implementing

Session specs often contain code samples that violate CLAUDE.md rules. Before using any code sample from the spec, scan it for these patterns and replace them:

Spec pattern Fix
style.display = 'none' / 'block' / 'flex' Use a CSS class instead (.active, .open, .hidden)
onclick="..." HTML attribute Remove attr; bind with addEventListener in JS
.onclick = () => ... Use addEventListener — no stacking risk
.sort(() => Math.random() - 0.5) Fisher-Yates (see Shuffling section)
color: #636e72 color: var(--text-secondary)
localStorage.getItem / .setItem SaveManager.load() / SaveManager.save() with key keyboard-command-4-save
window.x = window.x || new X() window.x = new X() in game.init() — always fresh
new AudioContext() in constructor Create lazily on first _getCtx() call; ctx.resume().then(() => schedule())
{ "type": "Full Restore", ... } item Use { "type": "health", "amount": 50 } — only health and weapon are engine-supported item types
{ ..., "combo": true } on a monster Remove — combo monsters are NOT implemented in the engine
{ ..., "mode": "key" } on a monster Remove — mode field is not read by the engine; display mode is set by monster type
"shortcut": ["cmd_a", "cmd_c"] on a monster Array shortcut fields are NOT implemented — use a single shortcutId per boss phase only
Boss phase with shortcutCategory or shortcutLevelRange Not supported — every boss phase needs a fixed shortcutId that exists in shortcuts.json

Level JSON Authoring Rules

Before writing any level JSON, verify these constraints against the engine:

Supported fields

  • Items: type is "health" (+ amount: number) or "weapon" (+ weaponId: number). Nothing else.
  • Monsters: only type, depth, offsetX. No mode, combo, shortcut array.
  • Boss phases: shortcutId (fixed, must exist in shortcuts.json), instruction (must match that shortcut's action field exactly), taunt (unique across all levels).

Item after_room safety — items near boss corridors are silently dropped

Find the first room with a boss field (first_boss_id). Then:

  • If room first_boss_id - 1 has isBonus: truesafe max = first_boss_id - 3
  • Otherwise → safe max = first_boss_id - 2

Items with after_room > safe_max are swallowed by startBossTransition and never delivered.

Level structure example Safe max
5 rooms, boss at r5, no bonus 5-2 = 3
6 rooms, boss at r6, no bonus 6-2 = 4
6 rooms, bonus at r5, boss at r6 6-3 = 3
7 rooms, bonus at r6, boss at r7 7-3 = 4
6 rooms, first boss at r4 (multi-boss) 4-2 = 2

Other invariants

  • Boss hp must equal phases.length (hp is display metadata but must match)
  • No two monsters in the same wave may share offsetX
  • Mage depth must be ≤ 0.15 (mages are stationary/ranged)
  • All monster depths in range 0.0–0.8

Run /verify-kc4-levels after writing all level JSON files


Canvas Performance Rules

This is a Canvas-rendered game targeting 60fps on iPad Safari. Every frame counts.

Offscreen Caching

  • Room backgrounds are static — render once to an offscreen canvas on room load. Each frame just drawImage() the cached background.
  • Monster sprites — pre-render each type + state to offscreen canvases on level load. Never draw from primitives each frame.
  • HUD text — cache static elements. Only re-render score/health text when values change (dirty flag pattern).

No DOM During Gameplay

  • All gameplay rendering is on Canvas — no createElement, innerHTML, classList, or textContent during the game loop.
  • HUD overlay elements (health bar, score) are DOM but updated only on value change, not every frame.

Particle Pool

  • Pre-allocate a pool of 50 particle objects. Reuse rather than create/destroy.
  • Each particle: {x, y, vx, vy, life, color, size, active}.

RAF Delta Time

  • Always use requestAnimationFrame with delta time for movement/animation.
  • If delta > 20ms (below 50fps), reduce particle count and skip non-essential animations.
  • Never use setInterval for the game loop.

Timer Lifecycle Pattern

Every manager that uses setTimeout or setInterval must follow this exact pattern. Missing any part is a bug.

class SomeManager {
    constructor() {
        this._mainTimer = null;   // declare ALL timer IDs as null
        this._shakeTimer = null;  // even short cosmetic timers
        this.onComplete = null;
    }

    cancel() {
        clearTimeout(this._mainTimer);  this._mainTimer = null;
        clearTimeout(this._shakeTimer); this._shakeTimer = null;
        this.onComplete = null;
    }

    complete() {
        const cb = this.onComplete;  // save BEFORE cancel() nulls it
        this.cancel();
        if (cb) cb();
    }

    start(data, onComplete) {
        this.cancel(); // defensive reset on re-entry
        this.onComplete = onComplete;
        // ... initialize fresh state ...
    }
}

Key rules:

  • Every setTimeout(...) → store return value in this._somethingTimer
  • Short cosmetic timers (screen shake, hit flash) still need IDs — they fire on detached DOM
  • cancel() is the single source of truth for cleanup
  • start() always calls cancel() first

Input System Rules

Modifier Key Routing

The input system must distinguish between shortcut attempts and game controls:

  • If e.metaKey || e.altKey || e.ctrlKey → route as shortcut attempt (compare against shortcut database)
  • If bare number key (1-0) with NO modifiers → route as weapon select
  • Tab / Shift+Tab → target cycling (NOT browser tab navigation — must preventDefault())
  • Escape → pause menu
  • Space → advance dialogue
  • H → shortcut journal

preventDefault() Scope

  • preventDefault() on ALL key events during gameplay state — prevents browser/OS shortcuts
  • In menu states (TITLE, LEVEL_SELECT, RESULTS), only prevent game-bound keys
  • Some system shortcuts cannot be intercepted (Cmd+H, Cmd+Space on iPadOS) — these use "Knowledge Monster" variant

700ms Fire Lock

After a correct shortcut fires the weapon:

  1. Set this._inputLocked = true
  2. this._lockTimer = setTimeout(() => { this._inputLocked = false; }, 700)
  3. During lock, ignore all shortcut input (but allow Tab for target cycling)
  4. Store _lockTimer and clear in cancel()

Web Audio API (iOS Safari)

  • AudioContext must be created lazily inside a user-gesture handler — never in a constructor
  • ctx.resume() returns a Promise — chain scheduling inside .then():
    _getCtx() {
        if (!this._ctx) this._ctx = new AudioContext();
        return this._ctx;
    }
    
    playSound(type) {
        const ctx = this._getCtx();
        ctx.resume().then(() => {
            // schedule oscillator nodes HERE, not before .then()
        });
    }
    
  • Never schedule oscillator nodes immediately after ctx.resume() — they may not play on iOS

Screen Visibility

  • Screens.active class
  • Overlays (pause, settings, journal) → .open class
  • Any element toggled visible/hidden → a semantic CSS class — NOT style.display

Modal Dialogs (Pause, Settings, Journal)

  • role="dialog", aria-modal="true", aria-label="..."
  • First focusable element = close button
  • Focus trap: Tab/Shift-Tab cycles within overlay
  • Escape to close (with .contains('open') guard)
  • On open: focus close button, setAttribute('aria-hidden', 'false') on overlay
  • On close: setAttribute('aria-hidden', 'true'), return focus to trigger element
  • aria-hidden always explicit 'true'/'false' — never removeAttribute

State Machine

TITLE → LEVEL_SELECT → GAMEPLAY → TRANSITION → GAMEPLAY → ...
                                → BOSS_FIGHT → LEVEL_COMPLETE → LEVEL_SELECT
                                → PAUSE (overlay)
                                → JOURNAL (overlay)
                                → GAME_OVER → respawn or LEVEL_SELECT
  • State transitions must be clean — cancel all active timers, animations, and input locks
  • showLevelSelect() must call cancel() on every active manager
  • Guard timer callbacks with null-checks: if (!window.someManager) return;
  • Timer callbacks must tolerate PAUSED state. Use state !== 'GAMEPLAY' && state !== 'PAUSED' — NOT just !== 'GAMEPLAY'. The user can pause during any delay (room-clear, wave spawn). Guarding only !== 'GAMEPLAY' causes permanent stalls.
  • When adding a new state, document: which states enter/exit it, what the game loop does during it, what input is accepted/blocked

Single RAF Chain — game.js Owns the Loop

IMPORTANT: Only game.js may call requestAnimationFrame. No manager may start its own animation loop.

Any class that renders to the game canvas must expose passive methods called by the game loop:

  • update(dt) or updateX(now) — advance internal state
  • draw(ctx, w, h) or drawX(ctx, w, h, now) — render to canvas
// WRONG — manager owns its own RAF chain → flickering
class SomeManager {
    start() { this._raf = requestAnimationFrame(() => this._tick()); }
}

// CORRECT — game loop drives the manager
if (this.state === 'TRANSITION') {
    this._levelManager.updateTransition(now);
    this._levelManager.drawTransition(ctx, w, h, now);
}

Batch SaveManager Operations

When updating 2+ save fields at once, do a single load() → modify → save():

// WRONG — 3x redundant JSON.parse + JSON.stringify
SaveManager.saveLevelResult(levelId, stats);
SaveManager.unlockWeapon(weaponId);
SaveManager.updateSettings('hints', 'always');

// CORRECT — single round trip
const data = SaveManager.load();
data.levels[String(levelId)] = { /* ... */ };
if (!data.weaponsUnlocked.includes(weaponId)) data.weaponsUnlocked.push(weaponId);
data.settings.hints = 'always';
SaveManager.save(data);

Timing Constants Must Be Synchronized

These durations are coupled across files. Changing one without the other is a bug:

Constant Value Files
Input lock after fire 700ms input.js, weapons.js
Monster death animation 500ms monsters.js, renderer.js
Corridor transition 500ms levels.js
Boss corridor 800ms levels.js
Boss title card 2000ms levels.js

Session Spec Validation

Before implementing any session plan, check these:

  1. Default settings must have a rationale. Every user-facing default (hints, volume, speed) needs justification. hints: 'always' = "new players can learn"; not just a value with no context.
  2. Canvas rendering ownership must be stated. If the plan says "render X to canvas", confirm it says game.js calls the render method — not "the new class renders to canvas" (which implies its own RAF).
  3. New state machine states must document transitions. Which states enter it, which states it exits to, game loop behavior, input handling.
  4. Scan spec code samples against the pattern table above. Specs routinely contain style.display, .onclick, independent RAF chains, new AudioContext() in constructors.

Monster Depth System

  • Depth 0.0 = back of room (small, far away)
  • Depth 1.0 = front of room (large, at attack range)
  • Render order: back-to-front (lower depth first)
  • Scale factor: 0.3 + depth * 0.7 (30% at back, 100% at front)
  • Y position: maps depth to Canvas Y coordinate (higher on screen = further away)

Save System

  • One key: keyboard-command-4-save — always through SaveManager
  • Never call localStorage.getItem/setItem outside SaveManager
  • Auto-save after: room clear, boss defeat, settings change
  • SaveManager._defaults() must include ALL keys that game.js reads from save data

Shuffling

Never array.sort(() => Math.random() - 0.5) — biased. Use Fisher-Yates:

for (let i = arr.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [arr[i], arr[j]] = [arr[j], arr[i]];
}

WCAG Non-Negotiables

  • Never user-scalable=no in viewport meta
  • Touch targets >= 44x44px on all interactive elements (menu buttons, level cards)
  • Font: OpenDyslexic via CDN <link>, Comic Sans MS fallback — never @import in CSS
  • Min font size: 16px, scalable (16/18/22px)
  • Background: cream #F5F0E8, text: #2C2416, secondary: #595143
  • WCAG AA contrast 4.5:1 minimum — never use #666, #888, #999 on cream
  • No flashing/strobing effects — death animations use fades
  • Screen shake: 100ms maximum, subtle

CSS ID Specificity

ID selectors (#foo) always beat class selectors (.bar). Never put display: in a base ID rule when a class like .screen controls visibility:

/* BAD — #screen-title display:flex overrides .screen { display:none } */
#screen-title { display: flex; }

/* GOOD — only active screens get display:flex */
#screen-title.active { display: flex; }
Install via CLI
npx skills add https://github.com/a1flecke/games-for-my-kids --skill kc4-checklist
Repository Details
star Stars 0
call_split Forks 0
navigation Branch main
article Path SKILL.md
More from Creator