How it works
From homework to game
The project started as the second half of a Model-View-Controller assignment in Java: a model of shapes and snapshots, a command-file reader, and two views. The graphical view is a Swing window with Prev, Select and Next buttons. The web view writes one HTML file with an SVG per snapshot. Both still live in the repository, unchanged, and the albums they were written for are still on this site.
Lightson is the same model, played. The model is rebuilt with
current Java: records for positions, colours and shapes, a sealed Shape interface
with exactly two kinds, and an Album that keeps drawing order and freezes
immutable snapshots. The game adds a package of its own on top. Nothing in the model, the
parser, the renderer or the game imports Spring, and a test checks that.
The puzzle is linear algebra
Flipping a switch twice does nothing, and the order of flips never matters. So a whole
play-through boils down to a set of switches, and the windows that end up changed
are the XOR of what each of those switches reaches. Asking which sets light a tower is
solving A x = b over GF(2), the field with two elements, where
addition is XOR.
A tower has at most 64 windows, so a row of the matrix fits in one long and
adding two rows is a single instruction. This is the heart of the solver, Gauss-Jordan
elimination on machine words:
for (int col = 0; col < n && rank < n; col++) {
long bit = 1L << col;
int found = -1;
for (int r = rank; r < n; r++) {
if ((row[r] & bit) != 0) {
found = r;
break;
}
}
if (found < 0) {
continue; // a free switch: it will span the kernel
}
swap(row, rhs, found, rank);
for (int r = 0; r < n; r++) {
if (r != rank && (row[r] & bit) != 0) {
row[r] ^= row[rank]; // adding two rows is one XOR
rhs[r] ^= rhs[rank];
}
}
pivot[rank++] = col;
}
What comes out is one solution plus a basis of the kernel: the "quiet patterns", sets of switches that change nothing when flipped together. Every solution is the first one plus some combination of quiet patterns. The Walk-up has no quiet pattern and so exactly one answer. The Grand has four, which makes sixteen answers, and the best of them use four switches. A five-by-five tower has two, and its best answer takes fifteen.
- Par is the size of the smallest solution. The server walks all 2k combinations of the kernel in Gray-code order, one XOR per step, and keeps the minimum.
- A hint is one switch on the way to the par solution nearest to where you are. Switches you flipped that should not be come first, so a hint tidies up before it builds.
- A dark tower can always be lit. Every wiring here flips the switch's own window and is symmetric, and for exactly that case a theorem of Klaus Sutner (1989) guarantees a solution, whatever the shape of the tower. Jammed switches and scrambled starts are outside the theorem, so those towers are checked by the solver when the program starts.
A position is a URL
Because a position is just a set of switches, it fits in an address. In
/tower/walk-up/155 the last part is that set in hexadecimal, one bit per window.
Every window on the page is a link to the address with its own bit flipped. Playing is
following links:
- Your browser's Back button is undo, and Forward is redo.
- Any position can be bookmarked or sent to someone.
- The server keeps nothing about a game in progress. It reads the address, works out the lights, and draws.
Only two things change what the server remembers through a form post rather than a link: asking for a hint, because hints are counted, and restarting a tower. Results are kept in SQLite, one row per player and tower, under a random id from the site's only cookie. The cookie is issued when you first open a tower, and nothing is written until a request comes back carrying it, so crawlers and link previews leave no trace.
Nobody signs in here, so that id proves nothing, and the game does not pretend otherwise. A lit tower is only written down when your browser says the request came from one of the site's own pages, which is what clicking the last window looks like. Opening a link to somebody's finished tower shows it and changes nothing. One network address can only start so many new records an hour, which keeps a script from swelling the day's count. The stars are for fun; nothing here is worth cheating for.
Every picture is still an album
The night sky, the towers, the people behind the windows: all of it is rectangles and
ovals with a flat colour, built as a list of the original model's shapes. A window keeps the
same shapes whether it is lit or dark; only their colours change. That is deliberate. It
means one move in the game is a handful of color commands in the homework's
own language. This is the first switch of The Walk-up, top left, lighting three windows:
# flip the switch on floor 3, window 1
color w-0-0-glow 127 139 99
color w-0-0 238 245 255
color w-0-0-glass 255 207 92
color w-0-0-shine 255 234 160
color w-0-0-bar-v 238 245 255
color w-0-0-bar-h 238 245 255
color w-0-1-glow 127 139 99
color w-0-1-glass 255 207 92
color w-0-1-shine 255 234 160
color w-1-0-glow 127 139 99
color w-1-0-glass 255 207 92
color w-1-0-shine 255 234 160
snapshot 3 of 9 windows lit
CommandWriter is the parser run backwards, and a round trip through both gives
back the same shapes. Under any tower, "Keep these moves as a photo album" writes your game
out as a command file, one snapshot per switch, and opens it in the original viewer.
No JavaScript
The site's Content-Security-Policy header forbids scripts and inline styles outright, so everything that moves is plain HTML, SVG and one stylesheet:
- Windows are
<a>elements inside the SVG. - The preview you see when you hover a window is a group of outlines inside that link,
hidden until
:hoveror keyboard focus. - The server marks what the last move changed with class names, and CSS keyframes do the rest: a flicker for windows that came on, a fading after-image for windows that went off, a ripple and a moonrise when a tower is lit.
- Links end in
#game, which keeps the tower in the same place on screen from one page to the next. - Visitors who ask their system for reduced motion get the same game without animation.
What happens on a request
- Caddy terminates HTTPS at
lightson.7107labs.comand proxies to this process on a localhost port. - Spring Boot (embedded Tomcat on virtual threads) routes the URL to a controller method.
- TowerCatalog turns the id into a tower: one of the campaign's twenty-five, the tower of a given day, or a random one grown from the number in its name.
- Puzzle reads the position from the address, rejects anything that is not a real position of that tower, and works out which windows are lit.
- TowerView builds the scene out of model shapes and SvgRenderer draws it, switching over the sealed shape kinds.
- A Thymeleaf template wraps it in the window you see: an info bar, the canvas and a row of buttons, laid out like the Swing view the project began with.
The daily tower
Each day's tower is grown from the date with java.util.Random, whose
algorithm is part of the Java specification, so the same date gives the same tower on any
machine and after any restart. The generator starts from the solved picture and flips a few
switches, which makes the result solvable by construction, then keeps the first candidate
whose par is between three and six. It changes at midnight UTC, and earlier days stay
playable.
The command language
One command per line. Coordinates start at the top-left corner, as in SVG and Swing.
Lines starting with # are comments. The command word is case-insensitive.
You can write an album of your own with it.
| Command | Arguments | Meaning |
|---|---|---|
shape | id rectangle|oval x y w h r g b |
Create a shape on top of the others. For ovals, x y is the centre and
w h are the radii. Colours are 0 to 255. |
move | id x y | Move to an absolute position. |
color | id r g b | Change the colour. |
resize | id w h | New width and height, or radii. |
remove | id | Take the shape off the canvas. |
snapshot | [description] | Freeze the canvas as the next snapshot. |
Limits
| Windows in a tower | 64, one bit each |
| Results kept | 400 days after you last played that tower |
| Pasted commands | 64 KB |
| Shapes on the canvas | 5,000 |
| Snapshots per album | 200 |
| Canvas side | 100 to 2,000, default 1,000 |
| Pasted albums kept | 90 days after they were last opened, at most 5,000 |
Running it
Java 21 from Ubuntu's packages, one jar built with Maven, a systemd unit that restarts it
if it dies, and Caddy in front for TLS and headers. The service listens only on localhost.
Results and pasted albums go to one SQLite file through Spring's JdbcClient,
with write-ahead logging, so they outlive restarts and deploys. /healthz reports
the service's health, database included. The same code builds and tests on every push.