tags: dev-challenge, game-challenge, gamedev
This is a submission for the June Solstice Game Jam
What I Built
Solstice is a puzzle game where you place mirrors to guide light beams across a grid. Each level has two modes — SUN and MOON. You toggle between them to route both beams to their targets. Both need to be lit at the same time to win.
Click a cell to place a / mirror, click again for \, again to remove. Right-click removes it. Ctrl+Z to undo. That’s the whole mechanic — but the levels get hard fast.
There are 18 levels split into 7 tiers. It starts simple (one mirror, one beam) and builds up to puzzles where you have fewer than 10 free cells and every placement has to count. I verified every level with a Python solver to make sure they’re all solvable at the stated par.
The game has a terminal with a Bletchley Park theme. You get 3 questions, the AI responds like a trapped wartime consciousness, and at the end you decide if it was human or machine. It connects to the Gemini API if you have a key, otherwise it uses pre-written responses.
VIDEO DEMO
Code
Solstice
A light-routing puzzle game with a custom ray-tracing engine, dual-spectrum beam physics, 18 levels across 7 tiers, a CRT terminal narrative, undo/redo, and a star-based rating system.
Built with vanilla JavaScript, rendered on HTML Canvas 2D, with audio synthesis via the Web Audio API and optional AI-powered dialogue through Google Gemini. Deployed as a static site on the Vercel CDN.
Architecture
Each subsystem is a standalone ES module communicating through a central game state, orchestrated by a requestAnimationFrame game loop.
Core Rendering Pipeline
<div dir="auto" data-json="{"data":"flowchart LR\n subgraph Pipeline[Core Pipeline]\n direction LR\n GL[\"GAME LOOP<br>main.js<br>requestAnimationFrame<br>renderer.draw()<br>updateUI()\"]\n RT[\"RAY TRACER<br>raytracer.js<br>Dual-pass simulation<br>SUN + MOON paths<br>Cycle detection\"]\n RD[\"RENDERER<br>renderer.js<br>3-pass canvas draw<br>Glow + dashes + pulses<br>Particle integration\"]\n PS[\"PARTICLES<br>particles.js<br>emit() / burst()<br>update(dt) / draw()<br>Lifecycle management\"]\n end\n\n GL –> RT –> RD –> PS\n\n style GL fill:#1a1a3a,stroke:#ffd700,color:#ffd700\n style RT fill:#1a1a3a,stroke:#4a90d9,color:#4a90d9\n style RD fill:#1a1a3a,stroke:#7b68ee,color:#7b68ee\n style PS fill:#1a1a3a,stroke:#00ff88,color:#00ff88\n style Pipeline fill:#0a0a1a,stroke:#333,color:#888\n"}" data-plain="flowchart LR subgraph Pipeline[Core Pipeline] direction LR GL["GAME LOOPmain.js
requestAnimationFrame
renderer.draw()
updateUI()"] RT["RAY TRACER
raytracer.js
Dual-pass simulation
SUN + MOON paths
Cycle detection"] RD["RENDERER
renderer.js
3-pass canvas draw
Glow + dashes + pulses
Particle integration"] PS["PARTICLES
particles.js
emit() / burst()
update(dt) / draw()
Lifecycle management"] end GL –> RT –> RD –> PS style GL fill:#1a1a3a,stroke:#ffd700,color:#ffd700 style RT fill:#1a1a3a,stroke:#4a90d9,color:#4a90d9 style RD fill:#1a1a3a,stroke:#7b68ee,color:#7b68ee style PS fill:#1a1a3a,stroke:#00ff88,color:#00ff88 style Pipeline fill:#0a0a1a,stroke:#333,color:#888 “>
flowchart LR
subgraph Pipeline[Core Pipeline]
direction LR
GL["GAME LOOP<br>main.js<br>requestAnimationFrame<br>renderer.draw()<br>updateUI()"]
RT["RAY TRACER<br>raytracer.js<br>Dual-pass simulation<br>SUN + MOON paths<br>Cycle detection"]
RD["RENDERER<br>renderer.js<br>3-pass canvas draw<br>Glow + dashes + pulses<br>Particle integration"]
PS["PARTICLES<br>particles.js<br>emit() / burst()<br>update(dt) / draw()<br>Lifecycle management"]
end
GL --> RT --> RD --> PS
style GL fill:#1a1a3a,stroke:#ffd700,color:#ffd700
style RT fill:#1a1a3a,stroke:#4a90d9,color:#4a90d9
style RD fill:#1a1a3a,stroke:#7b68ee,color:#7b68ee
style PS fill:#1a1a3a,stroke:#00ff88,color:#00ff88
style Pipeline fill:#0a0a1a,stroke:#333,color:#888
Loading
Supporting Systems
<div dir="auto" data-json="{"data":"flowchart TB\n subgraph Systems[Supporting Systems]\n direction TB\n IM[\"INPUT<br>input.js<br>Mouse + Touch<br>Long-press gesture<br>Coord mapping\"]\n GS[\"GAME STATE<br>game.js<br>8×8 grid state<br>Undo / redo history<br>Star rating (1-3)<br>Level load / reset<br>Save / load progress<br>Win detection\"]\n AU[\"AUDIO<br>audio.js<br>Web Audio synthesis<br>Osc + Gain nodes<br>5 sound effects<br>Zero audio files\"]\n ED[\"EDITOR<br>editor.js<br>Tool palette<br>Grid painting<br>JSON export\"]\n end\n\n IM –> GS –> AU\n GS –> ED\n\n style IM fill:#1a1a3a,stroke:#ffd700,color:#ffd700\n style GS fill:#1a1a3a,stroke:#00ff41,color:#00ff41\n style AU fill:#1a1a3a,stroke:#ff6b35,color:#ff6b35\n style ED fill:#1a1a3a,stroke:#4a90d9,color:#4a90d9\n style Systems fill:#0a0a1a,stroke:#333,color:#888\n"}" data-plain="flowchart TB subgraph Systems[Supporting Systems] direction TB IM["INPUTinput.js
Mouse + Touch
Long-press gesture
Coord mapping"] GS["GAME STATE
game.js
8×8 grid state
Undo / redo history
Star rating (1-3)
Level load / reset
Save / load progress
Win detection"] AU["AUDIO
audio.js
Web Audio synthesis
Osc + Gain nodes
5 sound effects
Zero audio files"] ED["EDITOR
editor.js
Tool palette
Grid painting
JSON export"] end IM –> GS –> AU GS –> ED style IM fill:#1a1a3a,stroke:#ffd700,color:#ffd700 style GS fill:#1a1a3a,stroke:#00ff41,color:#00ff41 style AU fill:#1a1a3a,stroke:#ff6b35,color:#ff6b35 style ED fill:#1a1a3a,stroke:#4a90d9,color:#4a90d9 style Systems fill:#0a0a1a,stroke:#333,color:#888 ” data-identity=”1c1bd2ec-e510-4db7-92f1-92ac1a02bbb5″ data-host=”https://viewscreen.githubusercontent.com” data-src=”https://viewscreen.githubusercontent.com/markdown/mermaid?docs_host=https%3A%2F%2Fdocs.github.com” data-type=”mermaid” aria-label=”mermaid rendered output container”>
flowchart TB
subgraph Systems[Supporting Systems]
direction TB
IM["INPUT<br>input.js<br>Mouse +…
Play it here: https://solstice-game-pi.vercel.app
HOW I BUILT IT
Vanilla JavaScript with HTML Canvas for rendering. No frameworks, no libraries — just ES modules and the Canvas 2D API.
The ray tracer uses grid-based ray marching. Each frame runs twice — once for SUN, once for MOON. Mirrors transform the direction vector: / does [dx, dy] → [-dy, -dx], \ does [dx, dy] → [dy, dx]. Cycle detection prevents infinite loops from mirror pairs facing each other.
Audio is all Web Audio API oscillators — no sound files at all. The terminal uses the Gemini API when configured, with a system prompt that keeps the Bletchley persona consistent.
Levels are hand-designed with increasing wall density. Levels 11-18 went through multiple redesigns after my Python solver kept finding unintended shortcuts.
Prize Category
BEST ODE TO ALAN TURING ;
The terminal is a Turing Test. You talk to something that believes it was a code-breaker at Bletchley Park in 1945. Three questions, then you judge. The dialogue touches on Enigma, ration books, the hum of valves. The ending asks the same question Turing posed — can you tell the difference?
Best Google AI Usage
The terminal connects to the Gemini API for dynamic responses. The AI stays in character as a wartime consciousness. Without a key it falls back to pre-written dialogue, but with Gemini every conversation is unique.
답글 남기기