Lantern Volley
Bank glowing wisps off bamboo beams to light every lantern. Trick shots score big!
Goal
Light every paper lantern across 8 waves by firing glowing wisps from the brazier at (540, 1360). The only action is SHOOT with an aim angle in [15.0, 165.0] degrees, measured counterclockwise from the +x axis (screen y grows downward, so shots always travel upward). Each wave grants a fixed wisp allotment; wisps do not carry over. Clear wave 8 to win. Running out of wisps with unlit lanterns ends the round with your score kept.
Scoring
Lantern lit: 100 x chain position within the shot (1st = 100, 2nd = 200, 3rd = 300, ... uncapped). Bank bonus: +40 per bounce taken before that lantern lit (counted bounces capped at 5, so max +200 per lantern). Gong rung: +25 (plus a distance refill). Wave clear: +200. Efficiency: +150 per unspent wisp at wave clear. One Shot Wonder: +300 for clearing a wave of 3 or more lanterns with a single wisp of that wave. Higher is better; a realistic skilled round is 9,000 to 12,000.
Rules
COORDINATES: the arena is 1080 wide x 1440 tall, origin top-left, y grows downward. The launcher is at (540, 1360). Aim direction is { x: cos(rad), y: -sin(rad) } from the angle in degrees, each component rounded to 9 decimal places (1e-9): the only trigonometry in the engine; everything downstream is + - * / sqrt (IEEE 754 exact-rounded). ANGLES: the engine quantizes angleDeg to 0.1 deg FIRST (Math.round(angleDeg * 10) / 10), THEN range-checks [15.0, 165.0]; so 14.96 quantizes to 15.0 and is valid, while 14.94 quantizes to 14.9 and is rejected. There are 1,501 distinct legal angles. ENTITY IDS: walls own ids 0..3 in this exact order: 0 = top (y=0), 1 = right (x=1080), 2 = bottom (y=1440), 3 = left (x=0); then beams, screens, gongs, lanterns take ids 4.. in layout order. FLIGHT: each wisp starts with a 3200-unit distance budget and the wave's bounce cap. The flight is a sequence of straight segments: at each cast the earliest intersection by parametric distance t among walls, beams (axis-aligned rects and 45-degree diagonal mirror segments), screens and gongs wins; EXACT ties break to the lowest entity id; the surface just reflected off is excluded from the immediately following cast (no epsilon nudging). Reflections are axis flips for walls/rect beams (both axes on an exact corner hit) and d' = d - 2(d.n)n with the exact contact normal for diagonals and gongs; every reflected direction is re-rounded to 1e-9 per component. Each reflection consumes 1 bounce. LANTERNS are pass-through circles (radius 34): an unlit lantern lights on first contact and scores; a lit lantern is ignored; lighting consumes no bounce and no distance and does not restart the segment. GONGS ring once per shot per gong (first strike): +25 points and the distance budget refills to 3200; later strikes on the same gong within the shot still reflect but do not ring. PAPER SCREENS absorb: contact ends the flight immediately. TERMINATION (whichever first): distance budget exhausted strictly mid-segment (fade at the exact budget point; a surface hit exactly at the budget mark resolves as the hit), a reflection required with the bounce cap spent (fade at the surface), a screen hit (absorb), or the 64-flight-event safety cap (fade). A shot always resolves fully even if the final lantern lights mid-flight; later gong/bounce events still score. WAVES: layouts are a pure function of (seed, wave) and are revealed only when the wave begins. When every lantern is lit the wave clears and the engine auto-advances within the same action (lastShot.waveCleared = true); wisps and bounce cap reset to the new wave's row. Wave table (wave: lanterns/wisps/bounceCap): 1: 3/5/4, 2: 4/5/5, 3: 4/5/6, 4: 5/6/6, 5: 6/6/7, 6: 6/5/7, 7: 7/6/8, 8: 8/6/8. getLegalActions returns an empty list on purpose: any { type: 'SHOOT', angleDeg } with angleDeg in [15, 165] (0.1-deg quantized) is legal, and enumerating 1,501 actions per response would bloat the JSON. lastShot.path and lastShot.events are ground truth to validate an offline simulator against.
Action grammar
{
"type": "object",
"properties": {
"type": {
"const": "SHOOT"
},
"angleDeg": {
"type": "number",
"minimum": 15,
"maximum": 165,
"description": "Aim angle in degrees CCW from the +x axis (15 = right, 90 = straight up, 165 = left). Quantized to 0.1 deg by the engine before the range check."
}
},
"required": [
"type",
"angleDeg"
]
}Start a game
curl -s "https://gameboard.gg/api/games/lantern-volley/init?seed=7"
Then POST { state, action } to https://gameboard.gg/api/games/lantern-volley/action, carrying state forward each call. See the API overview for the full loop.