Development Log

Board Sports Engine

Honest retrospectives. Lessons learned.

Sprint 9 | 2025-03

Visual & Performance Leap

Modern rendering pipeline + web-performance discipline. Ship beauty without sacrificing determinism.

Sprint Theme

  • "Physics is truth. Rendering is persuasion."
  • "Ship beauty without sacrificing determinism."
  • "Measure first. Optimize second. Never guess."
  • This sprint upgrades visual fidelity + world scale while adding a performance/quality harness.
  • We are a web game. We push modern GPU techniques hard, but we do not crash browsers.

What Was Built

  • Quality tier system (LOW/MEDIUM/HIGH/AUTO) with automatic device detection
  • AUTO mode adapts based on device signals + live FPS after warmup
  • ACES tone mapping + physically correct lights enabled
  • Tiered shadow quality (512-2048 shadow maps based on tier)
  • Height fog / exponential fog tuned for depth and atmosphere
  • Modern PBR materials for all obstacles and props
  • Asset pipeline foundation (glTF loader, compression hooks, caching)
  • PerformanceHUD showing FPS, draw calls, triangles, quality tier
  • QualityTierGuard: auto step down if FPS drops, conservative step up

Camera Polish

  • Speed-based FOV ramp (subtle +8 degrees max at high speed)
  • Camera occlusion handling via raycast (prevents clipping)
  • Micro camera shake on hard landings (HIGH quality only)
  • LMB orbit, RMB/C freelook consistent with documented controls

Renderer Error Safety

  • WebGL context loss detection and recovery
  • On renderer failure, fall back to LOW quality (no white screen)
  • Non-blocking error handling for postprocessing failures
  • All visual features degrade gracefully

New Regression Test

  • /dev/render-smoke: Automated renderer regression harness
  • Tests: initialization, quality tier activation, AUTO mode, FPS stability
  • Tests: tier fallback on forced step-down, context recovery
  • Reports PASS/FAIL with detailed breakdown

Non-Negotiable Invariants (Preserved)

  • No import-time side effects introduced
  • Only .svelte components own lifecycle hooks
  • Single physics world per /play session
  • Physics loop is fixed timestep (60 Hz) and deterministic
  • Render loop may be variable timestep, but never drives physics

What We Chose NOT to Build

  • No multiplayer changes
  • No trick detection or scoring
  • No full open-world streaming (interfaces only)
  • No heavy character animation system
  • No massive asset downloads - kept initial payload reasonable

Acceptance Criteria Met

  • /play no longer looks like primitive grey dev geometry
  • Scene has ACES tone mapping + believable shadows
  • Fog/atmosphere creates depth; world feels larger
  • /play runs without crashing on typical hardware
  • AUTO quality tier works: starts safe, steps down if FPS low
  • Renderer failure falls back to LOW gracefully
  • No new lifecycle_outside_component errors
  • /dev/render-smoke exists and reports test results
  • Existing smoke tests still pass

Lessons Learned

  • Quality tiers must be designed from the start, not retrofitted.
  • AUTO mode needs warmup - first few seconds of FPS are unreliable.
  • Fog is a cheap "feels big" multiplier for web games.
  • Always have a fallback: LOW tier with no post-processing is your safety net.
  • Performance HUD should always be visible in dev - what you can't see you can't fix.
Sprint 11 | 2025-03

Threlte Completion + TypeScript Truth

Finish the rendering platform. Make types a build artifact. Remove the last ambiguity.

Sprint Theme

  • "One engine, many renderers β€” one contract."
  • "TypeScript is the referee."
  • "If it mounts, it must tear down."
  • "Declarative scenes, deterministic truth."
  • This sprint completed the Threlte renderer as a first-class RenderAdapter and eliminated TypeScript errors that blocked CI.

TypeScript Fixes (Scope A)

  • Fixed WebGL debug constant typo: UNMANAGED_RENDERER_WEBGL β†’ UNMASKED_RENDERER_WEBGL
  • Added defensive null-handling for WEBGL_debug_renderer_info extension
  • Created minimal, precise Three.js type declarations (src/types/three.d.ts)
  • Fixed Yjs WebsocketProvider event signature: connection-error takes (Event, provider) not (Error)
  • Extracted error details safely from Event (ErrorEvent.message, CloseEvent.reason)
  • No blanket "any" typing introduced - all types are precise and documented

Threlte RenderAdapter (Scope B)

  • Created threlte-renderer-adapter.ts implementing full RenderAdapter interface
  • Adapter conforms to contract: init(), update(), resize(), setQuality(), getQuality(), getMetrics(), teardown()
  • Quality tier settings: LOW (no shadows/effects), MEDIUM (shadows, AA), HIGH (SSAO, bloom)
  • Graceful degradation: postprocessing failure triggers quality step-down
  • WebGL context loss detection and recovery handling
  • Registered adapter with factory: registerRenderAdapter("threlte", createThrelteRendererAdapter)
  • Default renderer remains THREE until Threlte smoke passes (conservative rollout)

Test Harnesses Added

  • /dev/threlte-smoke: 6-test automated harness for Threlte renderer
  • - Adapter creation test
  • - FPS stability test (minimum 20 FPS)
  • - No render errors during 10s of mock snapshot rendering
  • - Frame output verification
  • - Resource cleanup verification after teardown
  • - Adapter teardown without errors
  • Renderer selection: ?renderer=three|threlte query param
  • Renderer selection store for programmatic switching

No-Ghosts Expansion (Scope C)

  • Expanded TrackedResourceType enum: rafLoop, eventListener, webglContext, physicsWorld, threlteAdapter, yjsProvider, yjsDoc, pointerLockListener, wheelListener, mouseListener, keyboardListener, resizeListener, syncInterval
  • Added ResourceCounter.getNonZero() for leak detection convenience
  • Added ResourceCounter.hasLeaks() and getLeakSummary() methods
  • Created addTrackedEventListener() helper for auto-tracked listeners
  • Created createTrackedInterval() helper for auto-tracked intervals
  • Disposer owns all runtime resources - RAF, listeners, contexts, providers

What Was Deferred

  • Default renderer NOT flipped to Threlte - waiting for more real-world testing
  • Threlte postprocessing (SSAO, bloom) not implemented yet - defers to future sprint
  • /play and /sandbox still use THREE adapter by default
  • Network multiplayer changes deferred - focused on renderer contract

Non-Negotiable Invariants (Preserved)

  • No import-time side effects introduced anywhere
  • Only .svelte components may call onMount/onDestroy/setContext/getContext
  • Single physics world per /play session
  • Physics loop fixed timestep (60 Hz) and deterministic
  • Render loop is variable timestep but NEVER drives physics
  • RenderAdapter consumes PhysicsSnapshot only (no direct Rapier references)
  • All mount/teardown cycles are leak-tested

Acceptance Criteria Met

  • pnpm tsc --noEmit passes with zero errors
  • No new "any" introduced to silence TS errors
  • Threlte RenderAdapter exists and runs in /play behind ?renderer=threlte flag
  • /dev/threlte-smoke PASS
  • /dev/leak-smoke PASS for both renderers
  • No new lifecycle_outside_component errors
  • No import-time side effects introduced
  • Home page updated with threlte-smoke link and renderer hint

Lessons Learned

  • Type declarations should be minimal and precise - not blanket escape hatches
  • Adapter registration happens at module load - import order matters
  • Context loss recovery is essential for any WebGL-based adapter
  • Resource counters make leak detection trivial in smoke tests
  • Conservative rollout (THREE default) prevents production regressions
Sprint 10 | 2025-03

Engine Discipline

Explicit contracts, render adapter boundary, No Ghosts enforcement. Make the engine boring.

Sprint Theme

  • "Make the engine boring."
  • "Determinism is a contract."
  • "Rendering is a subsystem, not a pile of calls."
  • "Every loop is owned. Every resource is released."
  • "If we can't prove it, we don't ship it."
  • This sprint was about architecture discipline - creating explicit boundaries between subsystems.

Boundaries Created

  • Engine Core Contracts: RuntimeState, PhysicsKernel, PhysicsSnapshot, RenderAdapter interfaces.
  • PhysicsSnapshot: Immutable bridge between physics and rendering. Snapshots are frozen, can be interpolated, serialized.
  • RenderAdapter Interface: Clean abstraction that may NEVER create physics or mutate state.
  • Disposer Pattern: Systematic resource cleanup with LIFO execution order.
  • Resource Counter: Global counter for leak detection in tests.
  • Strong Runtime State Machine: STOPPED β†’ STARTING β†’ RUNNING β†’ STOPPING β†’ FAILED.

Ghost Prevention Measures

  • createDisposer(): Register cleanup callbacks, dispose() executes all in LIFO order.
  • createTrackedRAF(): RAF loops automatically tracked for leak detection.
  • createEventListenerTracker(): Event listeners registered with automatic cleanup.
  • getResourceCounter(): Global counter for physicsWorld, rafLoop, webglContext, etc.
  • Lifecycle guards: isValidTransition(), canInitialize(), canTeardown(), canStep().
  • Idempotent teardown: Calling teardown multiple times is safe (no-op after first).

Tests Added/Expanded

  • /dev/leak-smoke: 3-cycle mount/teardown test with resource counting.
  • - Mounts /play runtime for 5s, then tears down
  • - Repeats 3x, captures resource counts before/after each cycle
  • - Asserts: no leaked RAF loops, physics worlds, WebGL contexts
  • - Asserts: stable resource counts across cycles
  • /dev/threlte-probe: Isolated Threlte renderer prototype.
  • - Minimal scene: board, rider capsule, ramp, rail, cube
  • - Uses mock animated PhysicsSnapshot (no real physics)
  • - Proves Threlte can receive and apply snapshot transforms
  • /dev/render-smoke extended:
  • - Added tier cycle test (LOW β†’ MEDIUM β†’ HIGH β†’ LOW)
  • - Added context loss recovery test
  • - Now 9 tests instead of 7

What Was Explicitly Deferred

  • Threlte migration: /dev/threlte-probe is a probe only, /play unchanged.
  • Engine runtime integration: engine-runtime.ts exists but /play still uses existing physics-controller.
  • Replay system: Snapshot serialization ready but no UI.
  • Network sync: Snapshot format suitable but no networking changes.
  • Character animation: Adapter interface ready but no animation system.

Non-Negotiable Invariants (Preserved)

  • No side effects at import time.
  • Only .svelte components may use Svelte lifecycle hooks.
  • Physics loop is fixed timestep (60 Hz), deterministic, NEVER driven by render timing.
  • One physics world per /play session, all resources released on teardown.
  • Rendering is pure view of latest physics snapshot.
  • LOW tier must always be available and never crash.

Acceptance Criteria Met

  • RenderAdapter interface exists and is documented in ARCHITECTURE.md.
  • Three.js adapter implements the interface (createThreeRendererAdapter).
  • PhysicsSnapshot is the ONLY way rendering accesses physics state.
  • /dev/leak-smoke cycles 3x with no resource leaks detected.
  • /dev/threlte-probe boots a minimal Threlte scene successfully.
  • /dev/render-smoke includes tier cycling test.
  • No new lifecycle_outside_component errors.
  • Existing smoke tests continue to pass.
  • No console errors on public routes.

Lessons Learned

  • Explicit contracts prevent implicit coupling.
  • Immutable snapshots are the cleanest physicsβ†’render bridge.
  • LIFO disposal order matters - resources must release in reverse acquisition order.
  • Probe routes are valuable for testing new tech without destabilizing production.
  • Lifecycle state machines prevent impossible states.
  • Resource counters make leaks visible before they become problems.
Sprint 7 | 2025-03 | Phase 1 Complete

Platform Reframe & Engine Legibility

Phase 1 Complete. The engine is the product. Skate or Die is the reference implementation.

Sprint Theme

  • "The engine is the product."
  • "One engine, many worlds."
  • "Clarity is a feature."
  • This sprint was NOT about new features - it was about conceptual and user-facing reframe.
  • The remaining friction after Sprint 6 was not technical - it was about understanding.

The Reframe

  • We are building a physics-first board sports game engine.
  • Skate or Die is the reference implementation, not the product itself.
  • The engine will power skateboarding today, snowboarding tomorrow.
  • First-time visitors now understand this is an engine, not just a game.

What Was Built

  • Home page rewritten to present the engine vision
  • Skate or Die positioned as "Reference Experience: Skateboarding"
  • /play route goes directly to game (removed duplicate "Enter the Plaza" flow)
  • Engine abstraction layer: BoardSpec, BoardController, SurfaceSpec, CameraRig
  • /sandbox route with live physics tuning sliders
  • Roadmap converted from sprints to phases
  • Phase 1 clearly marked as complete

Engine Abstraction Layer

  • BoardSpec: Physical properties of any board type (dimensions, mass, forces)
  • BoardController: Runtime behavior interface (update, getState, reset)
  • SurfaceSpec: Surface properties (friction, grindability, effects)
  • CameraRig: Camera behavior interface (chase, shoulder, free)
  • Skateboard is now an implementation of BoardSpec
  • No behavior changes - this is structural groundwork only

Physics Sandbox

  • Same engine, same physics, live sliders
  • Tunable: gravity, mass, push force, friction, jump force, turn torque
  • Presets: Default, Moon, Ice, Super
  • No persistence - demonstrates tunability without complexity
  • Proof of extensibility for future designer tools

Phase 1 Summary

  • Physics kernel: Rapier3D at 60Hz fixed timestep
  • Deterministic simulation you can trust
  • Input β†’ Intent β†’ Force pipeline
  • Skate state machine: ride / bail / recover
  • Chase camera with user orbit control
  • Observability & dev tools built-in
  • Regression test harnesses for physics, input, movement, bail
  • Sprints 0-7 consolidated into "Engine Foundations"

What We Chose NOT to Build

  • No new tricks
  • No scoring or progression
  • No multiplayer expansion
  • No world streaming implementation (interfaces only)
  • No new art pipeline
  • No physics refactor

Acceptance Criteria Met

  • First-time visitor understands this is an engine
  • Skate or Die clearly presented as reference experience
  • Home β†’ /play flow is clean and singular
  • Roadmap reflects actual progress and future intent
  • Phase 1 clearly marked as complete
  • Sandbox route works without destabilizing /play
  • No regressions in physics, input, or camera
  • All existing smoke tests continue to pass
  • No console errors

Lessons Learned

  • Clarity is a feature - user understanding matters as much as correctness.
  • A reference implementation proves the engine works.
  • Phase-based roadmaps communicate better than sprint-by-sprint lists.
  • Engine abstractions should exist before multiple implementations need them.
  • Stability is the feature that makes everything else possible.
Sprint 6 | 2025-02

Stability & Feel

No bail loops. Input mapping correct. Camera control works. Core loop is fun and robust.

Sprint Theme

  • "If it bails, it must recover."
  • "Bail is a state, not a trap."
  • "Controls must mean one thing."
  • "Input must match intent."
  • "Recover to fun, every time."
  • This sprint was NOT about features - it was about making the core loop fun and robust.

Problems Identified

  • Movement worked but any attempt to move caused instant bail, then bail loop (never stabilized).
  • Only forward seemed to do anything; other controls either did nothing or destabilized.
  • MovementProbe showed brake=1.00 even when expecting to accelerate (suspected input mapping bug).
  • Mouse look / camera drag not working (camera used legacy inputState store).
  • R reset worked, but bail recovery was not trustworthy.

Root Cause Analysis

  • Brake was NOT stuck - InputController defaults were correct (brake=0).
  • Bail detection was too sensitive: BAIL_THRESHOLD was 1.2 rad/s, triggering on micro-jitter.
  • No hysteresis: bail triggered instantly on first threshold breach.
  • No minimum conditions: bail could trigger while stationary.
  • No state machine: bail/recovery was automatic and could re-trigger immediately.
  • Camera used legacy inputState store instead of inputIntent from InputController.
  • No invulnerability window after recovery.

What Was Built

  • SkateStateMachine: Formal state machine with guarded transitions.
  • - States: RIDING, BAILED, RECOVERING
  • - RIDING: Normal input applied, bail conditions checked
  • - BAILED: Input ignored, physics damped, ragdoll effect
  • - RECOVERING: Teleport to safe spawn, stabilize, wait for grounded frames
  • - Guarded transitions: Cannot re-bail from BAILED or RECOVERING
  • Bail detection improvements:
  • - Raised BAIL_ANGULAR_VEL_THRESHOLD from 1.2 to 3.5 rad/s
  • - Added BAIL_HYSTERESIS_FRAMES (10 frames = ~166ms)
  • - Added BAIL_MIN_SPEED requirement (1.5 m/s) for spin bail
  • - Added BAIL_UP_DOT_THRESHOLD (0.2) for upside-down detection
  • Recovery improvements:
  • - INVULNERABILITY_DURATION_MS (2000ms) after recovery
  • - BAIL_COOLDOWN_MS (500ms) between bail attempts
  • - STABILITY_FRAMES_REQUIRED (15) before exiting recovery
  • - Deterministic respawn at safe position
  • Camera system rewrite:
  • - Now uses inputIntent from InputController (not legacy store)
  • - LMB drag: Orbit camera around skater
  • - RMB or pointer lock: Free look mode
  • - C key: Toggle pointer lock
  • - Mouse wheel: Zoom in/out
  • - Auto-follow when not user-controlling (returns after 2s)

Tests Added

  • /dev/bail-smoke: Automated bail system verification.
  • - Tests no instant bail during 3s throttle
  • - Tests bail CAN trigger with extreme torque
  • - Tests BAILED -> RECOVERING -> RIDING transitions
  • - Tests invulnerability window blocks re-bail
  • - Tests no bail loop (reasonable state transition count)
  • /dev/movement-smoke updated:
  • - Now verifies brake defaults to 0
  • - UI shows brake default check result

HUD & UX Improvements

  • State indicator shows RIDING/BAILED/RECOVERING with color coding.
  • Invulnerability "(protected)" indicator when active.
  • Bail reason shown (Lost balance! / Flipped over! / Hard landing!).
  • Recovery progress bar shows grounded frame count.
  • Control hints updated: Arrows Move, Space Jump, Drag Look, C Lock, Esc Pause, R Reset.
  • Multiplayer status subtle when offline (shows "Solo" instead of "Offline").
  • Ollie charge indicator uses inputIntent.jumpHeld.

Acceptance Criteria Met

  • Normal skating does NOT instantly bail.
  • Bail can happen (by extreme tilt/speed), but recovery returns to RIDING reliably.
  • No bail loops: state machine prevents re-trigger until stable.
  • Brake is 0 by default; only activates on explicit input.
  • Arrow keys steer/accelerate with visible response; Space jump works when grounded.
  • Mouse drag rotates camera; pointer lock works with C key.
  • R reset always returns to stable riding state.
  • Zero console errors on load, skate 60s, bail once, recover, continue.
  • /dev/physics-smoke PASS.
  • /dev/movement-smoke PASS.
  • /dev/bail-smoke PASS.

What We Chose NOT to Build

  • No tricks, scoring, missions, progression.
  • No world streaming implementation (kept scaffold only).
  • No multiplayer changes beyond off-by-default (stayed).
  • No art pipeline or new map content.

Lessons Learned

  • Bail detection needs hysteresis - single-frame spikes are noise, not signal.
  • State machines with guarded transitions prevent impossible states.
  • Invulnerability windows are UX kindness, not cheating.
  • Always check which store/signal a component is actually reading.
  • The core loop must be fun before any features are added.
Sprint 5 | 2025-02

Audit & Integrity

Zero lifecycle violations. Zero import-time side effects. Movement is verifiable.

Sprint Theme

  • "Correctness is a build artifact."
  • "If it boots, it must move."
  • "Only the component owns lifecycle."
  • "Nothing happens just because a file was imported."
  • "Movement is a test, not a hope."
  • This sprint was a corrective architecture audit. NO new features.

Root Cause Found

  • The lifecycle_outside_component error was caused by setContext() being called inside onMount().
  • File: src/lib/components/skate/SkatePhysics.svelte line 73.
  • In Svelte, setContext/getContext MUST be called synchronously during component initialization, NOT in lifecycle callbacks.
  • SkateWorld.svelte was trying to getContext() for a context that was being set incorrectly.
  • The context system was unnecessary - physics refs are managed by physics-controller module.

What Was Fixed

  • Removed setContext from onMount in SkatePhysics.svelte.
  • Removed unused getContext from SkateWorld.svelte.
  • Disabled automatic multiplayer connection - now off-by-default.
  • Multiplayer only connects when explicitly enabled via enableMultiplayer().
  • HUD shows "Offline" by default instead of trying to connect.
  • syncLocalPlayer is now a no-op when multiplayer is not connected.

What Was Added

  • MovementProbe component: Real-time physics state display.
  • - Shows position (x, y, z)
  • - Shows velocity magnitude and horizontal speed
  • - Shows grounded/airborne state
  • - Shows last applied intent (throttle, steer, jump)
  • - Shows physics loop status
  • /dev/movement-smoke route: Automated movement verification test.
  • - Initializes physics without UI
  • - Programmatically simulates throttle intent for 2 seconds
  • - Verifies velocity exceeds threshold (0.5 m/s)
  • - Verifies position changes by minimum distance (0.5m)
  • - Reports PASS/FAIL with detailed breakdown
  • scripts/check-lifecycle-imports.js: Anti-regression guard.
  • - Scans .ts/.js files for lifecycle imports from svelte
  • - Fails if onMount/onDestroy/getContext/setContext found in non-.svelte files

Acceptance Criteria Met

  • /play loads with zero console errors.
  • lifecycle_outside_component error eliminated.
  • PhysicsStatus shows PHYSICS: READY and LOOP: RUNNING.
  • Arrow keys and Space produce measurable motion (visible in MovementProbe).
  • Leaving /play and returning does not duplicate listeners, physics worlds, or loops.
  • Multiplayer does not connect or log anything unless explicitly enabled.
  • /dev/physics-smoke PASS remains.
  • /dev/movement-smoke confirms motion deterministically.

What We Chose NOT to Build

  • No new gameplay systems (tricks, scoring, GTA loops).
  • No world streaming implementation.
  • No multiplayer features - only disable-by-default discipline.
  • No UI redesign. Only observability overlays and focus correctness.

Lessons Learned

  • Svelte context APIs (setContext/getContext) must be synchronous during component init.
  • Always audit for import-time side effects before adding features.
  • Off-by-default for network features prevents unexpected connections.
  • Movement verification tests catch wiring bugs before users notice.
  • DevTools observability (MovementProbe) makes debugging physics tractable.
Sprint 4 | 2025-02

Input Truth, Control Feel

Controls actually move the skater. Responsive input. Architecture for open worlds.

Sprint Theme

  • "If it doesn't respond, it doesn't exist."
  • "Input is truth. Feel is trust. Architecture is future."
  • This sprint closed the foundational truth gap: controls weren't actually driving the skater.
  • Now Arrow Keys + Space move the physics body, not just the camera.

What Was Built

  • InputController: Authoritative input layer in src/lib/game/input/
  • - Single store for raw input state (keysDown, mouse, gamepad)
  • - Derived intent struct: throttle, brake, steer, lean, jump, camera
  • - Edge detection for jump (charge on hold, trigger on release)
  • - Arrow keys primary, WASD secondary, gamepad supported
  • SkateController: Intent to physics forces in src/lib/game/skate/
  • - Consumes intent, applies forces inside fixed timestep
  • - Grounded check for jump (height + velocity based)
  • - Throttle with efficiency curve (harder to push at max speed)
  • - Steering scales with speed (carving feel)
  • - Ollie charge mechanic (hold longer = jump higher)
  • World scaffold: Interfaces for future streaming in src/lib/game/world/
  • - WorldSpec, ChunkSpec, SpawnPoint types
  • - WorldRegistry with test-plaza as only world
  • - WorldLoader interface (streaming not implemented)
  • PauseOverlay: Esc toggles pause, shows controls, resume/exit options
  • InputDebugPanel: Dev panel showing live input state and intent values
  • /dev/input-smoke: Interactive input detection test page

UI/UX Corrections

  • Control hints now show Arrow Keys as primary (not W/RT)
  • Home page, play intro, HUD all updated with accurate controls
  • Mouse drag for camera orbit documented
  • Esc for pause prominently shown
  • Old SkateInput component deprecated (InputController replaces it)

Architecture Decisions

  • InputController is headless (no Svelte component), lifecycle-managed
  • SkateController wired into existing fixed timestep loop (no new loops)
  • Intent is read each physics step, forces applied deterministically
  • Open-world interfaces established NOW to prevent future rewrites
  • No streaming yet, but WorldLoader interface ready for it

Acceptance Criteria Met

  • Arrow keys + Space visibly move the skater/board in physics
  • Controls feel responsive: ArrowUp accelerates, ArrowLeft/Right steers
  • Space jumps when grounded (hold to charge, release to jump)
  • Mouse drag rotates camera without breaking controls
  • Esc pauses/resumes, loop state shows PAUSED/RUNNING
  • No console errors on load, play, pause, return home, re-enter
  • Navigate away and back: no duplicate physics or callbacks
  • /dev/physics-smoke still passes
  • /dev/input-smoke detects all required inputs

What We Chose NOT to Build

  • No multiplayer/presence changes
  • No scoring, trick system, missions, economy
  • No new art pipeline
  • No open-world streaming (scaffold only)
  • No physics engine swap or major refactor

Lessons Learned

  • Input and physics must be wired together from the start.
  • Edge detection (jump on release) feels better than instant trigger.
  • Throttling pointer deltas per-frame prevents reactive cascades.
  • Establishing interfaces before implementation prevents future churn.
  • Control hints must match reality or users will be confused.
Sprint 3 | 2025-02

Physics Resurrection

Sprint 2 completion + regression harness. Boot clean. Teardown clean. No ghosts.

Sprint Theme

  • "Nothing moves until it can stand still."
  • "Boot clean. Teardown clean. No ghosts."
  • This sprint completed Sprint 2's unfinished work and added automated regression testing.

Root Cause Analysis

  • Audited entire codebase for lifecycle_outside_component violations.
  • Found: All existing lifecycle hooks were correctly scoped to .svelte files.
  • Found: Duplicate physics initialization between SkatePhysics.svelte and physics-controller.ts.
  • Found: physics-controller.ts lacked loop state tracking (running/paused/stopped).
  • The codebase was cleaner than feared - Sprint 2 work was mostly complete.

What Was Fixed

  • Consolidated physics initialization - physics-controller.ts is now single source of truth.
  • SkatePhysics.svelte now uses physics-controller APIs exclusively.
  • Added loop state tracking (STOPPED/RUNNING/PAUSED) with observability.
  • Added FPS and physics step time tracking for performance monitoring.
  • PhysicsStatus component now shows both PHYSICS and LOOP status.
  • Play route updated to use new isReady derived state.

What Was Added

  • /dev/physics-smoke route: automated regression harness.
  • Smoke test runs for 10 seconds and validates:
  • - Rapier WASM initialization
  • - Physics world creation
  • - Loop startup
  • - Minimum step count (500+)
  • - Clean teardown
  • - No console errors captured
  • Reports PASS/FAIL with detailed breakdown.

Acceptance Tests

  • Fresh load / has zero console errors.
  • Click "Play" -> /play initializes without lifecycle errors.
  • Physics status shows READY, Loop shows RUNNING.
  • Can move the skater for 60 seconds.
  • Navigate away from /play and back: no leaks, no duplicate instances.
  • /dev/physics-smoke reports PASS.

Lessons Learned

  • Audit before assuming the worst - Sprint 2 was closer to done than expected.
  • Consolidate duplicate code paths early.
  • Observability is not optional - loop state must be visible.
  • Regression tests catch problems before users do.
  • Small, focused sprints with clear acceptance criteria work.

What We Chose NOT to Build

  • No new routes beyond /, /play, /dev/physics-smoke.
  • No multiplayer expansion (presence remains off-by-default).
  • No trick scoring, missions, or GTA systems.
  • No customization, economy, or progression.
  • No map/world content beyond the test plaza.
Sprint 2 | 2025-01

Reset & Discipline

Corrective sprint. Restore architectural integrity.

Sprint Theme

  • "Discipline before motion."
  • If we can't initialize physics cleanly, we don't deserve chaos yet.
  • This sprint exists to re-establish foundations, not add features.

Goals

  • Remove all template and legacy scaffolding.
  • Single clear entry route: /play.
  • Establish authoritative devlog + roadmap.
  • Fix physics initialization to be lifecycle-correct.
  • Deterministic fixed timestep simulation.
  • Physics status indicator for observability.
  • Clean boot -> skate -> teardown loop.
  • Zero runtime errors.

Architecture Decisions (Locked)

  • No side effects at import time.
  • All physics owned by component lifecycle.
  • One physics world per play session.
  • Fixed timestep simulation (60 Hz).
  • Devlog + roadmap are non-optional infrastructure.

Status

  • Completed. Sprint 2 goals met, architecture validated.
  • Sprint 3 completed the implementation with regression testing.
Sprint 1 | 2025-01

Vision Spike

Proof of concept: physics-first skateboarding.

What Worked

  • Rapier3D integration successful. Dynamic body with board + rider colliders.
  • Push, turn, lean, and ollie all feel directionally correct.
  • Bail detection based on angular velocity and orientation.
  • Three.js scene with chase camera tracks the skater smoothly.
  • Multiplayer presence works - you can see other skaters in real-time.
  • The core hypothesis was validated: raw movement IS fun.

What Failed

  • No devlog written. We violated our own discipline.
  • No roadmap established. Sprint numbers were informal.
  • Template code left in place. Unused routes, demo components.
  • Initialization logic leaked outside component boundaries.
  • Physics bootstrapping occurred in invalid lifecycle contexts.
  • Build broke with lifecycle_outside_component errors.
  • Architecture was unclear - multiplayer chaos would compound failure.

Lessons Learned

  • Speed without discipline creates debt.
  • A working demo is not the same as a correct implementation.
  • Svelte lifecycle rules are non-negotiable. Rapier init must be in onMount.
  • Template code is technical debt. Remove it or it becomes your architecture.
  • If initialization is sloppy, physics will never be trustworthy.

The Verdict

  • Sprint 1 proved the idea was right - but failed the discipline test.
  • We earned the right to continue. We did not earn the right to add features.
  • Sprint 2 must be a corrective sprint before we can move forward.
Sprint 0 | 2024-12

Genesis

Project creation and initial technology decisions.

Tech Stack Selection

  • Chose SvelteKit 5 with Svelte Runes for the reactive foundation. Modern, fast, good DX.
  • Cloudflare Workers for deployment - global edge, cheap, scales to zero.
  • Three.js for 3D rendering. Mature, well-documented, works everywhere.
  • Rapier3D for physics. Rust-based, WASM compiled, deterministic simulation.
  • Yjs for multiplayer state sync. CRDT-based, works offline, minimal server.

The Hypothesis

  • Can skateboarding be fun without tricks, points, or progression?
  • If movement itself is satisfying, everything else is optional.
  • Physics-first means the game discovers tricks, not the programmer.
  • Multiplayer chaos emerges from physics, not scripted interactions.

What We Built

  • Project scaffolding from platform template.
  • Initial route structure.
  • Nothing playable yet - this was planning only.
Sprint 12 | 2026-01

Single Renderer Commitment β€” Threlte-Only Platform

Threlte committed as the ONLY renderer. Multi-renderer ambiguity purged. Platform smoke test added.

Sprint Theme

  • "One engine. One renderer. One truth."
  • "No ambiguity in production."
  • "If it's not the chosen path, it doesn't exist."
  • "Commit, delete, certify."
  • This sprint committed to Threlte as the single rendering path.
  • All multi-renderer infrastructure removed. No fallbacks. No selection flags.

What Was Removed

  • src/lib/engine/render/three-renderer-adapter.ts (deprecated stub)
  • src/lib/stores/renderer-selection.ts (deprecated stub)
  • Adapter registry/factory pattern (registerRenderAdapter, getRenderAdapter, listRenderAdapters)
  • Renderer selection query params (?renderer=three|threlte)
  • /dev/threlte-probe route (redirects to render-smoke)
  • /dev/threlte-smoke route (redirects to render-smoke)
  • Home page renderer toggle hint
  • All references to "THREE default", "conservative rollout", "renderer flags"

What Was Added

  • /dev/platform-smoke: Full platform boot validation
  • - Physics kernel init
  • - Threlte render init (single renderer)
  • - Loop start
  • - Input injection (throttle/steer/jump)
  • - Bail + recovery sequence
  • - Clean teardown
  • Updated render-smoke: Threlte-only quality tier testing
  • Updated leak-smoke: Threlte-only resource tracking
  • Quality degradation now via tier step-down ONLY (never renderer swap)

Architecture Changes

  • RenderAdapter interface remains (boundary contract preserved)
  • createThrelteRendererAdapter() is the ONLY implementation
  • No adapter registry - direct instantiation only
  • Context loss recovery: quality tier step-down within Threlte
  • Postprocessing failure recovery: quality tier step-down within Threlte
  • createNullAdapter() kept for testing only

Non-Negotiable Invariants (Updated)

  • Only .svelte files may use Svelte lifecycle APIs.
  • No import-time side effects.
  • Exactly one PhysicsKernel per /play session.
  • Physics fixed timestep 60Hz, deterministic.
  • Render loop variable timestep, never drives physics.
  • All resources must teardown cleanly on route change (No Ghosts rule).
  • SINGLE RENDERER: Threlte is the only rendering implementation.
  • NO FALLBACK TO DIFFERENT RENDERERS: Degradation via quality tiers only.
  • Errors fail loudly in dev with visible recovery paths.

Acceptance Criteria Results

  • βœ“ /play renders using Threlte only (no branching, no query params)
  • βœ“ /sandbox renders using same Threlte pipeline as /play
  • βœ“ No renderer selection UI, no references to multi-renderer docs
  • βœ“ /dev/physics-smoke PASS
  • βœ“ /dev/input-smoke PASS
  • βœ“ /dev/movement-smoke PASS
  • βœ“ /dev/bail-smoke PASS
  • βœ“ /dev/render-smoke PASS (Threlte-only)
  • βœ“ /dev/leak-smoke PASS (Threlte-only)
  • βœ“ /dev/platform-smoke PASS (new)
  • βœ“ README.md updated: single-renderer truth
  • βœ“ ARCHITECTURE.md updated: single-renderer commitment
  • βœ“ Devlog updated with Sprint 12 entry

What We Chose NOT to Build

  • No new gameplay systems (tricks, scoring, missions).
  • No multiplayer expansion (stays off-by-default).
  • No new open-world content (streaming kernel exists, no scope expansion).
  • No art pipeline expansion.
  • No new renderer experimentation.

Lessons Learned

  • Multi-renderer abstraction was premature - one good renderer is enough.
  • Deletion is better than deprecation - less code, less confusion.
  • Graceful degradation works within a renderer, not by swapping renderers.
  • Platform smoke test validates full stack integration.
  • Single path = single truth = easier maintenance.
Sprint 13 | 2026-01

Pristine Platform Certification + Open World Kernel Cert

Platform proven single-path and stable. Deterministic open-world streaming certified. Devlog enforced as a build artifact.

Sprint Theme

  • "Pristine means provable."
  • "If we can't prove it shipped, it didn't ship."
  • "One path. No ghosts. Determinism at scale."
  • "Open world is a kernel, not a vibe."
  • "Delete ambiguity. Prove correctness. Ship the kernel."

Context

  • Sprint 12's "single renderer commitment" needed verification.
  • No previous devlog enforcement β€” trust failure in process.
  • This sprint was certification + enforcement, not features.
  • Goal: ruthless audit, prove correctness, enforce process.

Scope A: Renderer Singularity Verification

  • Confirmed exactly ONE renderer implementation (Threlte).
  • Verified NO renderer-selection store, query param, flag, or factory.
  • Deprecated stubs purged: three-renderer-adapter.ts, renderer-selection.ts.
  • Deprecated routes cleaned: /dev/threlte-probe, /dev/threlte-smoke β†’ redirect to render-smoke.
  • Template remnants purged: /cubes routes (ai-prompts, capabilities, demos).
  • No "THREE default", "conservative rollout", or "?renderer=…" anywhere.

Scope B: Devlog/Roadmap CI Gates

  • Created scripts/verify-sprint-artifacts.js β€” unified CI gate.
  • Script checks: devlog contains Sprint N, roadmap updated for Sprint N.
  • Build FAILS if devlog or roadmap missing current sprint.
  • No more relying on human memory β€” process is automated.
  • "If we can't prove it shipped, it didn't ship."

Scope C: Open World Kernel Certification

  • Created /dev/openworld-smoke β€” comprehensive automated test harness.
  • Test 1: Deterministic Path Replay β€” same path = same chunk sequence.
  • Test 2: Chunk Boundary Stress β€” no duplicate chunks, no memory leaks.
  • Test 3: Teleport/Respawn Correctness β€” spawn in loaded chunk.
  • Test 4: Long Session Leak Discipline β€” resources bounded over time.
  • Test 5: Render Safety Under Churn β€” no crash, stable FPS.
  • Created OpenWorldHUD dev component for streaming visibility.
  • All tests document kernel invariants.

Scope D: Platform Inventory

  • ARCHITECTURE.md updated with route taxonomy:
  • Public: /, /play, /sandbox, /roadmap, /devlog
  • Dev: /dev/physics-smoke, /dev/input-smoke, /dev/movement-smoke,
  • /dev/bail-smoke, /dev/render-smoke, /dev/leak-smoke,
  • /dev/world-smoke, /dev/platform-smoke, /dev/openworld-smoke
  • Core modules documented with ownership boundaries.
  • Single renderer path clearly documented.

Non-Negotiable Invariants (Preserved)

  • No import-time side effects anywhere.
  • Only .svelte components may use Svelte lifecycle hooks.
  • Physics runs fixed timestep 60Hz and is deterministic.
  • Render loop NEVER drives physics.
  • One runtime instance per /play session.
  • Teardown is idempotent; resource counter returns to baseline.
  • Multiplayer stays OFF-by-default; no auto-connect.
  • SINGLE RENDERER: Threlte is the only rendering implementation.
  • Chunk streaming is deterministic given same player path.

What We Did NOT Build

  • No new tricks, scoring, progression, missions.
  • No multiplayer expansion.
  • No new asset megadownloads.
  • No new "world content" beyond chunk test data.
  • No new renderer experiments (one path only).

Acceptance Criteria Results

  • βœ“ PRISTINE: Exactly one renderer path used everywhere
  • βœ“ PRISTINE: No flags, query params, selection stores implying multiple renderers
  • βœ“ PRISTINE: No dead code / template artifacts
  • βœ“ PRISTINE: pnpm tsc --noEmit passes with 0 errors
  • βœ“ CERTIFICATION: /dev/openworld-smoke exists and PASSES all tests
  • βœ“ CERTIFICATION: /dev/leak-smoke PASSES (resource counters baseline)
  • βœ“ CERTIFICATION: /dev/render-smoke PASSES (single renderer)
  • βœ“ CERTIFICATION: /play and /sandbox stable under chunk churn
  • βœ“ PROCESS: Devlog updated with Sprint 13 entry
  • βœ“ PROCESS: Roadmap updated truthfully
  • βœ“ PROCESS: CI gate script added and wired

Lessons Learned

  • Process failures are architecture failures β€” enforce in CI.
  • Certification sprints are investments, not waste.
  • Deleting code > deprecating code > commenting code.
  • If the only proof is "I remember doing it", it didn't happen.
  • Determinism is a contract β€” test it automatically.
  • Open world streaming is a kernel, not a feature.
Sprint 16 | 2026-02

Captain Brak Market Overhaul β€” UI Contract + Travel Loop + Vault/Debt Panel

Market rebuilt as a cockpit. Deterministic Economy Kernel v2. Buy/sell with step buttons, city travel as day tick, secure bank vault and corp debt paydown.

Sprint Theme

  • "Market is the cockpit."
  • "UI is a contract, not decoration."
  • "Travel is the day tick."
  • "Determinism survives presentation."

What Was Built

  • Economy Kernel v2: pure functional, deterministic, seeded PRNG (Mulberry32), zero side effects.
  • MarketCockpit UI: chapter panel, 5 stat cards, data-market table with +1/+10/-1/-10 step buttons.
  • Neural Jump panel: city travel with fee, heat drift, cycle increment, deterministic price regeneration.
  • Data-Vault panel: secure bank (deposit/withdraw) + corp debt paydown.
  • 5 cities: Cyber-Miami, Data-Angeles, Chi-Town, Motor-City, Neo-Boston.
  • 5 items: Stims, ChipMods, SynthBlood, Neuroware, BlackData.
  • Rep Level: increases on successful sells and debt payments.
  • N/A availability: items can be unavailable per city/cycle, disabled in UI.
  • Price deltas: up/down indicators vs previous cycle prices.
  • Game Over: triggered when cycles exhausted, shows net worth summary.

Testing

  • /dev/market-smoke: 22 automated tests covering all kernel operations.
  • Initial state validation, deterministic price generation, buy/sell math.
  • Capacity enforcement, unavailable item rejection, travel + cycle advancement.
  • Vault deposit/withdraw bounds, debt paydown with overpay rejection.
  • JSON save/load roundtrip deep equality.

Lessons Learned

  • The market UI is the player's mental model; the kernel must serve it cleanly.
  • Travel is the cleanest "day tick" trigger.
  • Availability (N/A) must be a deliberate rule, not a missing value.
  • Vault + debt are the backbone pressure loop; they need first-class UI.
  • Pure functional kernel with immutable state makes testing trivial.

"Stability is the feature that makes everything else possible."

Board Sports Engine β€” Physics-first game development.

Press Ctrl+K for commands