Photon Maze is a polished, browser-based geometric-optics puzzle game. Players place, move, and rotate mirrors, triangular prisms, convex lenses, and concave lenses to guide live simulated light into spectral receivers. The complete local campaign contains 12 experiments across Reflection, Refraction, and Focus, with progressive hints, stars, persistent records, responsive touch controls, procedural audio, and accessibility preferences.
- 12 data-driven levels in three chapters, each with three progressive hints.
- Verified authored solutions for every level; untouched levels are also checked against accidental completion.
- Deterministic, UI-independent TypeScript optics engine.
- Finite double-sided mirrors with reflectivity loss.
- Convex polygon prisms using Snell's law, spectral dispersion, and total internal reflection.
- Positive and negative thin lenses with bounded paraxial behavior.
- Finite-aperture RGB receivers driven only by traced ray contributions.
- Continuous beam retracing during movement and rotation.
- Dragging, rotation ring, wheel, keyboard nudging, inventory placement, return, reset, undo, and redo.
- Complete title, level select, manual, settings, pause, completion, and campaign-finale states.
- Local progress, stars, best move counts, best times, and settings persistence.
- Procedural Web Audio ambience and cues with independent master, ambience, and effects levels.
- Reduced motion, high contrast, color assistance, visible focus, keyboard navigation, and large touch controls.
- Developer author mode with optical IDs, authored-solution application, and current-state JSON export.
- Node.js 20.19+ or 22.12+ (Node.js 24 is also supported)
- npm 10+
- A modern browser with Canvas or WebGL support
npm install
npm run devVite prints the local development URL. No backend, login, API key, remote font, or network service is required after dependencies are installed.
npm run dev # Start the Vite development server
npm run build # Type-check and create the production build
npm run preview # Preview the production build
npm run test # Run Vitest in watch mode
npm run test:run # Run all tests once
npm run typecheck # Run strict TypeScript checks
npm run lint # Run ESLint with zero warnings allowed
npm run format # Format the repository with Prettier
npm run format:check # Check formatting without writing| Action | Input |
|---|---|
| Select or move | Click / drag an optical element |
| Continuous rotation | Drag the selected element's rotation ring |
| Rotate by 5° | Mouse wheel, Q, or E |
| Snap rotation to 15° | Hold Shift while using Q / E or the ring |
| Nudge | Arrow keys |
| Larger nudge | Shift + arrow keys |
| Undo / redo | Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, or Ctrl/Cmd+Y |
| Return inventory item | Delete / Backspace |
| Reset | R or the reset button |
| Hint | H or the hint button |
| Pause / close | Escape |
Bench-element buttons provide a keyboard-accessible way to select placed optics.
Tap an inventory item to place it, drag elements directly on the board, and use the visible Rotate left, Rotate right, and Return controls after selection. Interactive HTML controls are at least 44 CSS pixels. On narrow screens the board remains fitted while the receiver, inventory, and selected-instrument panels move below it.
The simulation lives under src/optics/ and does not depend on Phaser or the DOM. Renderer pixels never determine receiver state.
- Every ray carries a normalized direction, RGB spectral band, intensity, refractive medium, stable ID, and depth.
- Every segment records its start and end, collision type and ID, band, intensity, and termination reason.
- Nearest-hit selection uses stable type/ID tie-breaking.
- Epsilon offsets, a 48-interaction depth cap, intensity cutoff, quantized loop detection, and invalid-vector checks guarantee bounded traces.
- Mirrors are finite and double-sided. Reflection uses the standard vector law and 98% reflectivity.
- White light is represented by three co-located RGB bands. They overlap visually as white and separate when refracted.
- Prisms use oriented-edge Snell refraction and total internal reflection. The refractive indices are deliberately exaggerated to
1.49 / 1.52 / 1.56for red, green, and blue so dispersion remains readable on a compact board. This is a gameplay model, not precision color science. - Lenses change direction at their center plane using
thetaOut = thetaIn - height / focalLength. Angles and very short focal lengths are bounded to keep the paraxial approximation stable. - Receivers accumulate actual ray contributions by band, apply a small numerical tolerance, and require both spectral and total-intensity thresholds.
src/
app/
PhotonMazeApp.ts HTML UI, state transitions, completion flow
persistence.ts Versioned save loading and safe migration
game/
audio/AudioEngine.ts Procedural Web Audio
rendering/LabScene.ts Phaser board/input/rendering layer
GameSession.ts Optical state, history, moves, validation
levels/
campaign.ts 12 typed campaign definitions
schema.ts Validation and state-to-scene conversion
optics/
geometry.ts Pure vector and intersection helpers
intersections.ts Deterministic world collision ordering
reflection.ts Mirror reflection
refraction.ts Snell refraction and TIR
thinLens.ts Bounded paraxial lens transform
receivers.ts Spectral accumulation and evaluation
tracer.ts Bounded deterministic ray tracer
ui/
icons.ts Inline SVG icon system
styles.css Responsive visual system and access modes
GameSession is the explicit boundary between authored content, the pure tracer, Phaser rendering, and the HTML interface. Optical changes retrace immediately; unchanged frames reuse the previous result.
Each LevelDefinition includes metadata, a fixed board, emitters, fixed and movable objects, inventory, par moves, three hints, an elegance goal, and a test-only authored solution. validateCampaign() runs when the campaign module loads and reports useful level IDs and validation messages.
To inspect author mode, start the game with:
http://localhost:5173/?debug=1
Author mode shows object IDs and exposes Apply solution and Copy state JSON controls. Copy the formatted element state into a level's solution data, then run npm run test:run to confirm every receiver is activated by real simulation. The authored solution is never shown in normal play.
When adding a level:
- Add a typed entry to
src/levels/campaign.tswith stable object IDs. - Keep placement clearances and receiver apertures forgiving enough for human manipulation.
- Add three reasoning-focused hints.
- Add a reachable
solutionstate and reasonableparMoves. - Run campaign solution tests and manually inspect the level.
Progress is stored as versioned JSON under the photon-maze-save local-storage key. The save includes the highest unlocked level, onboarding state, level records, and settings. Values are type-checked and clamped while loading; corrupt or incompatible content falls back safely to a fresh save. Settings includes a clearly confirmed Reset Progress action.
The game remains playable if local storage is blocked; persistence simply becomes session-only.
- Native buttons, inputs, headings, labels, dialogs, and live status regions.
- Visible keyboard focus and keyboard-only gameplay manipulation.
- High-contrast mode.
- Reduced-motion mode that collapses animated transitions.
- Color assistance with persistent numerical R/G/B receiver labels.
- No receiver requirement is represented by rendered color alone.
- Touch targets are at least 44 CSS pixels.
- No rapid flashing; screen shake is off by default.
- Settings and help overlays preserve the underlying puzzle state.
The Vite base is relative, so the production directory can be hosted at a domain root or subdirectory:
npm run buildUpload the contents of dist/ to any static host, such as GitHub Pages, Netlify, Cloudflare Pages, an object-storage static site, or a local HTTP server. No server-side routing or environment variables are required.
The test suite covers vector math, finite ray/segment and ray/circle intersections, stable nearest-hit ordering, mirror angles, bidirectional Snell refraction, total internal reflection, positive/negative/rotated/reverse thin-lens travel, receiver truth, wall absorption, save migration, campaign schema, and all 12 authored level solutions.
The final project was also inspected in a real browser at desktop and 390×844 mobile viewports. The title, tutorial, Level 1 completion, Level 2 inventory placement, touch controls, undo, redo, reset, settings persistence, save reload, level unlocking, and browser console were checked.
- Phaser 3 — game loop, scenes, input, scaling, and rendering.
- Vite — development and production build tooling.
- TypeScript — strict typed implementation.
- Vitest — unit and campaign validation tests.
All game visuals, SVG icons, interface styling, level data, and audio are original or procedural. No third-party art or audio assets are included.

