Solstice — A Light-Routing Puzzle Game

작성자

카테고리:

← 피드로
DEV Community · vinny · 2026-06-22 개발(SW)

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

Screenshot 2026-06-15 202114

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.

solstice-game-pi.vercel.app

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&lt;br&gt;main.js&lt;br&gt;requestAnimationFrame&lt;br&gt;renderer.draw()&lt;br&gt;updateUI()\"]\n RT[\"RAY TRACER&lt;br&gt;raytracer.js&lt;br&gt;Dual-pass simulation&lt;br&gt;SUN + MOON paths&lt;br&gt;Cycle detection\"]\n RD[\"RENDERER&lt;br&gt;renderer.js&lt;br&gt;3-pass canvas draw&lt;br&gt;Glow + dashes + pulses&lt;br&gt;Particle integration\"]\n PS[\"PARTICLES&lt;br&gt;particles.js&lt;br&gt;emit() / burst()&lt;br&gt;update(dt) / draw()&lt;br&gt;Lifecycle management\"]\n end\n\n GL –&gt; RT –&gt; RD –&gt; 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 LOOP
main.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&lt;br&gt;input.js&lt;br&gt;Mouse + Touch&lt;br&gt;Long-press gesture&lt;br&gt;Coord mapping\"]\n GS[\"GAME STATE&lt;br&gt;game.js&lt;br&gt;8×8 grid state&lt;br&gt;Undo / redo history&lt;br&gt;Star rating (1-3)&lt;br&gt;Level load / reset&lt;br&gt;Save / load progress&lt;br&gt;Win detection\"]\n AU[\"AUDIO&lt;br&gt;audio.js&lt;br&gt;Web Audio synthesis&lt;br&gt;Osc + Gain nodes&lt;br&gt;5 sound effects&lt;br&gt;Zero audio files\"]\n ED[\"EDITOR&lt;br&gt;editor.js&lt;br&gt;Tool palette&lt;br&gt;Grid painting&lt;br&gt;JSON export\"]\n end\n\n IM –&gt; GS –&gt; AU\n GS –&gt; 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["INPUT
input.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.

원문에서 계속 ↗

코멘트

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다