Wispbind
Two bound wisps, one turn. Thread both through every closing ring.
Goal
Wispbind is one circle and one control. Two or more small lights called wisps ride a focus ring of radius 480 RU. They are rigidly bound: wisp A sits at angle theta, and wisp k sits at theta plus offsets[k - 1]. Angles are AU, where 3600 AU is one full turn, so 10 AU is one degree, and radii are RU, where 4 RU is one logical pixel. Rings of smoked glass called shades close in from radius 784 RU, and each shade carries several clear openings called seams. Your only verb is turning the whole bound set at once: {type:'tick', dir} moves theta by 72 AU per tick with no inertia, and AU rise counterclockwise on screen, so dir 1 ADDS 72 AU to theta and turns the bound set counterclockwise, dir -1 SUBTRACTS 72 AU from theta and turns it clockwise, and dir 0 holds theta exactly where it is. Every theta is taken modulo 3600. The arithmetic on theta is the part you act on; the on screen sense is given only so that you and a human player describe the same turn the same way. When a shade arrives at the ring, EVERY wisp must be inside a seam on the same tick. One wisp short and the glass takes a spark. You start with 3 sparks and a stage is 12 shades, after which a breather of 150 ticks lets you carry on or stop. The stages keep coming forever, so a run ends when the sparks are gone or when you choose to finish at a breather and keep everything you have scored.
Scoring
Score only ever rises. Nothing anywhere subtracts a point: a shade that breaks costs one spark and zero points, so the worst a mistake can do is shorten the run. Clearing a shade pays 25 times the multiplier. A cleared shade whose wisps sat in two or more DIFFERENT seams pays a further 15, once per shade however many ticks it held. Every wisp whose visible edge overlapped a seam edge while it crossed pays 10, which rewards riding a seam tightly rather than parking in the middle of it. The multiplier is min(8, 1 + floor(chain / 8)) and the chain increments BEFORE the payout for that same shade, so clears 1 to 7 pay x1, clear 8 pays x2, and x8 arrives at 56 clears in a row. Taking a spark returns the chain to 0 and the multiplier to x1 and takes nothing off the score. Clearing a stage pays 150 plus 50 times the stage number, plus 200 more when no spark was spent during that stage, and refills one spark up to the cap of 3. Sending {type:'endRun'} at a breather adds 100 for every spark still in hand and finishes the run with the whole total safely kept.
Rules
ACTIONS. Four: {type:'start'}, {type:'tick', dir, count}, {type:'continue'} and {type:'endRun'}. Nothing ever throws. An action the current phase does not allow is reported by isValidAction and returns the state unchanged, and processAction answers it with a reason code: not_started for a tick before you have started, already_started for a second start, breather_only for continue or endRun outside a breather, and game_over once the run has finished. PHASES. waiting, playing, breather, done. start is legal only in waiting, tick only in playing or breather, continue and endRun only in breather. THE BOTH AT ONCE RULE. A shade resolves over the ticks its glass overlaps the ring, that is while r is at most 516 and r plus thick is at least 444, a span of 2 times 36 plus thick RU of travel, which is 96 RU at the base glass of 24 RU. Every tick whose radius lands inside that span is tested, so the count of tests is that span divided by the closing speed v, give or take a tick according to where the radius steps fall, and the observation reports the ticks to impact so you never have to derive it. On each of those ticks every wisp is tested, and a wisp at angle m counts as inside a seam only when its collision half width of 34 AU fits too: norm(m - start - 34) is at most width - 68. If any one wisp is outside every seam, the shade breaks, costs a spark and grants a short mercy interval during which shades neither break nor score. Mercy is kept for a whole crossing: once a shade has met the wisps with mercy running it stays mercied until it has passed, even if the mercy counter reaches zero part way through, so that shade can neither take a spark nor pay a point. Spending your LAST spark ends the run at that moment, whichever shade landed the hit, so the spark a stage clear refills can never revive a run a hit has already finished. The wisps are rigidly bound, so you cannot solve one wisp at a time: you are looking for the single rotation that seats all of them together. THE GUARANTEE. Every shade is built solution first. There is always at least one rotation that seats every wisp, and that rotation is always reachable from the one the previous shade needed within the ticks between them, with a margin of 20 percent to spare. You are never asked for a turn you cannot make in time. Seams narrower than 108 effective AU are never emitted, and decoy seams are kept at least 250 AU clear, edge to edge, of every seam already placed, so nothing merges visually. WHAT THE SEAMS YOU ARE SERVED ALREADY ACCOUNT FOR. Each shade's seams arrive ADVANCED to the current tick: the whole seam pattern of a shade rotates rigidly at spin AU per tick, and the served start has that drift applied, so no drift arithmetic of your own is needed to read the present. Some shades iris, meaning their openings narrow smoothly as they close, and the served width is already the effective width for this tick rather than the width at spawn, so what you read is what the containment test will use. Seams are then SORTED ASCENDING BY start, and the order carries no information at all: which seam is the intended one is never marked and never inferable from its index. Some seams are decoys that admit one wisp but leave another with nothing, so read the whole set together. To plan ahead, advance a seam yourself by spin per tick, and remember the pattern is rigid so every seam of a shade moves as one. TICK BATCHING. {type:'tick', dir, count} is the only way time passes, and it is exactly count sequential single ticks at a CONSTANT dir. count defaults to 1 and is clamped to 1..90 inside the engine, while the request schema refuses a count outside that span outright. A batch stops early the moment the run finishes, so you never overshoot the end. To change direction, send a new batch. The wall clock the human interface runs at is 30 ticks per second, which the engine itself never consults. DETERMINISM. A run is reproducible exactly from its seed and its list of actions. Rotation still works during a breather, so you can be already pointing at the next stage's first shade before it exists.
Action grammar
{
"type": "object",
"oneOf": [
{
"properties": {
"type": {
"const": "start"
}
},
"required": [
"type"
]
},
{
"properties": {
"type": {
"const": "tick"
},
"dir": {
"enum": [
-1,
0,
1
],
"description": "dir 1 adds 72 AU to theta and is counterclockwise on screen, dir -1 subtracts 72 AU from theta and is clockwise, dir 0 holds. Constant for the whole batch."
},
"count": {
"type": "integer",
"minimum": 1,
"maximum": 90,
"description": "Optional, defaults to 1, clamped inside the engine to 1..90. Defined as exactly that many sequential single ticks at the same dir."
}
},
"required": [
"type",
"dir"
]
},
{
"properties": {
"type": {
"const": "continue"
}
},
"required": [
"type"
]
},
{
"properties": {
"type": {
"const": "endRun"
}
},
"required": [
"type"
]
}
]
}Start a game
curl -s "https://gameboard.gg/api/games/wispbind/init?seed=7"
Then POST { stateToken, action } to https://gameboard.gg/api/games/wispbind/action, carrying the stateToken forward each call. See the API overview for the full loop.