exposure
What a betafite crystal does to a photographic plate, in arithmetic. The `Game` component runs it every tick; the board draws what it writes.
game.rules.exposure — the physics of the plate
What a betafite crystal does to a photographic plate, in arithmetic. The
Game component runs it every tick; the board draws what it writes.
local Exposure = require("game.rules.exposure")
The kernel
A crystal deposits Core.EXPOSURE.rate per second on its own cell, times a
weight that falls with the SQUARE of the distance in cells:
w(d) = 0.25 ^ (d*d) while d <= Core.EXPOSURE.radius, else 0
Which makes an exposure TIME into a SHAPE — the thing the whole game is played on:
| distance | weight | per second | past clearMax | reaches darkAt |
|---|---|---|---|---|
| 0 — the crystal's own cell | 1.0000 | 0.340 | 1.0 s | 2.9 s |
| 1 — a side | 0.2500 | 0.085 | 4.1 s | 11.8 s |
| 1.41 — a diagonal | 0.0625 | 0.021 | 16.4 s | 47.1 s |
| 2 | 0.0039 | 0.0013 | 264 s | 754 s |
| beyond 2.2 | 0 | — | never | never |
- ~3 s → a dot. The crystal's own cell alone; its four sides sit at 0.26,
under
clearMax, so nothing around it is spoiled. - ~12 s → a plus. The cell and its four sides. The diagonals sit at 0.26, still clear. This is the workhorse mark.
- ~47 s → a block. The full 3x3, and expensive in both dose and night.
Nothing is spoiled by a single well-timed mark. A frisket is what you reach
for when two marks stand close enough that their skirts ADD past clearMax,
or when a plus has an arm you did not want — which is what makes the masks a
puzzle rather than a tax.
The surface
| call | what it does |
|---|---|
newCells() | a fresh plate: 70 cells, nothing exposed |
applyTarget(cells, target) | write a plate's #/. target into the cells |
applyMasks(cells, pieces) | recompute which cells stand under a frisket |
deposit(cells, pieces, dt) | run the crystals for dt seconds; returns how many stood |
crystalCount(pieces) | how many crystals are out — what doses the apprentice |
score(cells) | share of cells reading right, 0..1 |
audit(cells) | the same count with the missed and spoiled cells named |
render(cells) | ten strings: # dark, : spoiled, - masked, . clear |
weight(dc, dr) / kernel() | the kernel itself, for anything that wants to draw it |
Simulation
A plate is authored by writing down how it is SOLVED and reading the target off the result, so no plate can ship that cannot be played to 100%.
local sim = Exposure.simulate(plate.target, plate.solution)
--> { cells, match, audit, crystalSeconds, seconds, peakCrystals }
Exposure.simulateUnmasked(plate.target, plate.solution) -- the same crystals, no masks
Exposure.trayDemand(plate.solution) -- what the tray has to hold
A solution is a list of phases; each states the whole board — the friskets standing and the crystals lying — and how many seconds it is held. Between phases the player lifts and lays; the glass keeps what it has.
Scoped to this part · feeds back into the world's score.