Concept. Barrel Toss is a reversed Donkey Kong (Nintendo, arcade 1981, 25m girder board): the player is Donkey Kong at the top of the structure, choosing when and how to heave barrels, while an AI opponent — the Runner (Jumpman/Mario) — climbs the girders and ladders in real time, exactly as a skilled arcade player would. The player wins by landing a barrel or fireball on him before he reaches Pauline. Where the original asks one player to survive the climb, the reversal asks the other to make the climb unsurvivable.
Design lineage. The candidate was approved under the near-perfect F2 standard and carries the open spike "window lethality vs a route-planning bot" (selection spec §9). This design is grounded in the arcade ROM itself, not memory: the barrel engine was verified against a pixel-comparison-verified Z80 decompilation (qarl/arcade-js, mirrored at computerarcheology.com) and Don Hodges' 2008 ladder-routine disassembly. Every mechanic below exists in the original; the reversal changes only who decides.
Goals (observable outcomes).
| Element | Definition |
|---|---|
| Primary user | A casual player in a browser; solo only; full keyboard and touch support |
| Trigger | Opening the HTML file; boots to TITLE with no external resources |
| Core job | Heave barrels (roll / hurl / blue) that kill the Runner during his climb |
| Completion (kill) | The Runner is hit by a barrel or fireball, or his bonus clock reaches zero. The kill is scored and the tier attempt ends at SUMMARY |
| Failure | None by design (mirror of the original's absent win state for DK). If the Runner reaches Pauline he escapes: the board resets with a fresh Runner, the escape is counted, and play continues in the same attempt. Quitting is always available. |
| Trust boundaries | Fully local: no network calls, no external assets, no accounts; best scores and settings only in localStorage |
| Object | Canonical definition |
|---|---|
| The Site (25m) | The arcade's first board, portrait orientation (224×256): six sloped truss girders zigzagging from the oil drum at bottom-left up to the top, Pauline's short platform (88→150, sloping down-left) at the very top, and DK standing on his own ramp (girder 1, x≈52) beside his barrel piles — as in the arcade screen. Ladders sit on the native X mod 8 = 3 grid, including the twin pair at the top (75 partial / 91 full) and the short right ladder (139) up to Pauline. Geometry replicates the arcade reference screenshot, not an invented layout. |
| The Tosser (player = DK) | Stationary on his ramp beside his barrel piles. One verb family — heave — with seven native release options across three families (SR-2). Between heaves he plays his native idle chest-pound animation. He never moves or aims. |
| The Runner (AI = Jumpman/Mario) | The opponent. Runs girders, waits at ladder bases, climbs, and jumps with fixed arcs — a finite-state router over the girder/ladder graph, never a generic platforming AI (FR-05). Starts at the ROM's native spawn point; reaching Pauline is an escape. |
| Roll family (ROLL / DEEP ROLL / CHASE ROLL) | Orange barrel set onto the top girder via the ROM's three-waypoint release string — identical physics for all three modes. They differ only on the ladder-descent axis: ROLL runs the native SR-4 gate chain at every ladder head (difficulty-scaled likelihood steered by the Runner's own X position and movement — the barrel is natively "interested" in him); DEEP never descends (full zigzag to the drum); CHASE descends at every head it passes. All three are regimes the ROM's own latch/state machine can produce (SR-2, R-6). |
| HURL barrel | The ROM's direct drop, shipped as the arcade's high hurl ("sometimes Kong throws them directly down the building" — attract-mode text): a 14-frame grab at the launch point (60,74), then a ballistic arc at HURL_VX = 3.0 px/f under ROM gravity — it clears DK's own ramp and lands on a girder below (fall contact never re-lands on the ledge it left), where the native bounce reseed takes over. Mode NONE: its rolling never descends a ladder. Rarest release in the ROM (1-in-16 gate) → longest cooldown. Single form. |
| Blue family (BLUE / DEEP BLUE / CHASE BLUE) | Alternate-kind barrel on the roll path — same three descent modes as the roll family, same physics. Every blue retires at the oil drum, ignites it, and arms exactly one fireball emission (~1 measured per blue barrel; native 64-frame arm; SR-4). The player's area-pressure weapons. |
| Fireball | Spawned by an ignited drum (native cap: 5 fire records). Roams the girders and climbs ladders under its native motion tables; its post-contact velocity uses the same reseed family as bouncing barrels, so on high native difficulty fireballs also steer toward the Runner. Kills on contact. |
| Bonus clock | The Runner's native board timer (L1 = 5000 display points). On 25m the ROM does not tick it per second — it decrements one unit per barrel released. ~50 thrown barrels drain a fresh clock; at zero the Runner dies the native time-up death. His clock is therefore the player's throw economy made visible, and it is refilled on every escape (SR-1). |
| Heave cycle | Each press commits DK to the native grab–raise–heave animation for a flat HEAVE_FRAMES = 26 (~0.43 s) lockout shared by all release types — presses during it are ignored, never queued (SR-2). The ROLL string advances one waypoint every WP_STEP = 14 frames (~0.23 s): three points, hands → slight arc → ramp contact (~0.47 s total); HURL holds its launch point 14 frames, then goes ballistic. |
| Cooldown | Per-release-option lockout in real seconds, ticking only during PLAY. Every deck button shows its remaining cooldown live (sweep + seconds). Tier-scaled: Beginner gets the richest supply, Expert the leanest (SR-3). |
| Escape | The Runner reaches Pauline's row: a visible cutscene plays for ESCAPE_FRAMES = 90 — he walks to her, three hearts rise, and a blinking localized "HE ESCAPED!" banner shows with the running escape count — then a fresh Runner spawns at the native floor spawn with a refilled bonus, in-flight barrels retire, and the escape counter ticks. The attempt is not over — only a kill ends it. Escapes are visible, counted failures of pressure, not losses. |
| Hammer P1 | Two fixed props. While held (~5 s native) the Runner smashes barrels he contacts but cannot climb, jump, or change plan — a stall that drains his own clock. Spike-gated; see R-5. |
There is no LOSS state by design (§2). The only terminal state of an attempt is KILLED; escape is a sub-loop of PLAY, and passive play produces endless escapes at zero score.
localStorage; blocked storage → session-only, silently.| Aspect | Rule |
|---|---|
| Ready phase | ~1.5 s: empty girders, Runner at native spawn, drum unlit, bonus clock full, all cooldowns zero. Input ignored. |
| Heave | Player presses a release key/taps a deck button that is off cooldown. Cooldown starts immediately; DK plays the native animation for that release; the barrel enters its fixed waypoint/ballistic path (SR-4). No aiming, no queueing, no take-backs — the moment of the press IS the decision. |
| In-flight economy | Maximum 10 active barrels (native object table). If the table is full the press is refused with a brief "pile full" deck shake (a native limit, vanishingly rare under cooldowns). Every released barrel ticks the Runner's bonus clock one unit. |
| Kill paths | (1) barrel contact while the Runner is on a ladder — he cannot jump mid-climb; (2) contact during a jump arc whose landing is covered by a second barrel — the double bind; (3) fireball contact; (4) bonus clock reaching zero — the native time-up death. Kills play the native death animation. |
| Escape | Runner reaches Pauline's row → escape animation (~1.5 s) → fresh Runner, refilled bonus, all active barrels retire naturally/are cleared, fireballs despawn, escape counter +1, play resumes without a READY pause. Thrown-barrel total keeps accumulating (escalation is the score pressure). |
| No lose state | The attempt continues until the kill or the player quits (Q → TIER SELECT, nothing recorded). Idle play yields endless escapes: no throw, no bonus drain, no risk to the Runner — inaction is self-penalizing through score, exactly like Saboteur/Stacked. |
| Replay | SUMMARY offers retry (R) and TIER SELECT (Esc). |
| Exceptions | Tab hidden / window blurred during PLAY → auto-PAUSED (SR-5). Kill and escape completing on the same frame: kill wins (attribution to the last throw is preserved). |
Purpose. One verb family, one panel: choose the release option; the option and the moment are the entire decision. The deck is a 3×3 numpad grid — columns are barrel families, rows are descent intensity — matching the Saboteur dealer-bar convention so on-screen position equals physical numpad position.
touch-action: manipulation plus a touchend fast path (zero double-tap zoom, zero synthetic delay). Presses on cooling cells or during DK's heave animation are ignored — never queued.Tap a release or press its numpad key (1/4/7 · 2 · 3/6/9). Watch the live cooldown sweep per button. Demo cooldowns are shortened for illustration; game cooldowns are SR-2/SR-3 anchors finalized in §9.
Purpose. A skilled arcade runner who is nonetheless killable through the information and commitment windows built into the original. This is the design's concentrated risk (R-1, R-4): the generic "clumsy platformer bot" is a killed category in this project (selection §7), and 25m is deliberately board-like — flat girders, straight ladders, fixed-arc jumps, no pits — so the Runner is implemented as a finite-state router over a graph, not as a physics agent.
RUN (along a girder toward the chosen next ladder) → WAIT (at the ladder base, evaluate threats) → CLIMB (irreversible until the next girder, no jumping possible) → back to RUN; JUMP interrupts RUN/WAIT only, fixed arcade arc (a jump clears three framework sections), landing pre-committed; DEAD is terminal. He always progresses toward Pauline; the bonus clock forbids turtling.PLAN_HORIZON = 240 f (≈ 4 s — no tier-scaled cap), sampled every 8 f, into time-indexed occupancy risk (native-lottery descents contribute expected risk; DEEP/CHASE barrels are deterministic facts he routes around exactly, and a CHASE predicted to take a ladder head closes that shaft outright). Each reachable ladder is scored by collision risk over the whole run+climb route plus a wait price: a closure is wait-able when it will clear within DESC_WAIT_CAP — a descent still in the column when he would reach the base, a stream crossing his base or his landing head, an inbound roll, or a sealed runway on the girder above (continuation seal). While a priced wait stands, he parks at a standoff P = 12 px up-hill of the base (DESC_PARK_OFF); the park is vetoed (park-spot nuke) if any predicted barrel point would sweep through it during the stand, and a stream re-crossing the base right after the wait extends it (walk-back window). A route sealed with no priceable wait is never walked to — he holds position instead (closed-route hold). Cooldown state and DK's heave animation are visible inputs — exactly what a watching human has. Pair gate (round 3): two corridor crossings closer than a full jump arc (JUMP_PAIR_GAP) are an unjumpable moving wall — the route reads not-enterable rather than merely risky, and he holds instead of pushing into a bunched stream he cannot outplay. Safest stand (round 3): when every route is closed with no priced wait, he walks to the spot on his girder that maximizes the earliest predicted contact (safestStandX over his current spot, both deck ends, and all full-ladder bases, HOLD_HORIZON = 180 f deep) — a chosen pocket, never a freeze in place.JUMP_TRIGGER = 92 px — in front or behind — is tracked with a per-barrel reaction latch; among all live threats he engages the most urgent by time-to-contact, never the first in memory order (the multi-barrel freeze bug). Barrels that retire (drum band) or leave the board before they can ever reach him are phantom threats and are skipped by the same arrivalAtX math the planner uses — but the phantom read is intent-aware (round 4): a barrel that cannot reach his current spot still latches the instant his own run or his chosen route would carry him into it (meeting-point check), so the reaction budget is spent at full sighting distance instead of discovering the barrel at point-blank range (the drum-mouth deaths). A barrel he has seen but not yet processed still closes in — the sighting brake (round 5) holds his feet and the planner while latency burns, so the takeoff window is never eaten by his own walk (the round-5 ladder-base death). Answers, in order: a timed-takeoff forward jump on the low native arc (SR-4) whose exact sim (arcClear — real integration, edge clamp, girder slant, barrel-first tick interleave) is the takeoff trigger, gated by the headClear ceiling veto and an uncovered landing; a neutral jump (the native straight-up move) under the same exact read with zero drift when the forward landing is covered; a step-back bind/retreat only while his own spot stays uncovered; and the marginal forward arc as a wall-pinned last resort. A committed jump is never second-guessed. Fireballs are never jumped (the low arc cannot clear their thicker band) — he gives ground instead (fire-flee, SR-4).headClear refuses the ceiling) and no stand/retreat pocket reads safe, he does not freeze or pray — he flees backward to a hide ladder and parks on the rungs, the skilled-human cluster answer. escapeLadder offers the nearest full ladder based on his deck (up-hide: climb LETHAL_CLIMB_PX + slope lift out of the deck band) or topped on his deck (down-hide: slide SURFACE_BAND_PX + slope lift under the head band), and only while every gate reads exact-clean: no barrel or fire reaches the mouth before he is off the deck (walk + hide-transit window on arrivalAtX), no corridor barrel rolls at him en route, no CHASE roll owns the shaft mouth road (the chaser dives in after him — the hide becomes the trap), and no descent squats the shaft. Shaft rules are the exact static-safety test hideShaftSafe (feet clear of the lower band and head clear of the upper, both slope-aware): down-hides live or die by it; up-hides on shorter shafts (≥ HIDE_MIN_SHAFT_UP = 24) are offered as marginal hides — the head pokes ~1 px into the upper band, so the upper head must additionally read clean for the whole expected hold (mouth-crossing horizon + slack) with no CHASE inbound up there. The flee is a persistent commitment: each frame the window is re-read; if the road clears he releases and resumes the plan, if the window closes he falls back to the generic layer. On the rungs he holds at the safe depth until no barrel can arrive during his descent, then leaves — the release clears the commitment outright (a kept commit re-drags him to the hold depth every frame: the oscillation bug). All band-clearance math is slope-exact (round-6 slopeLift ruling: the kill band tracks the girder surface, so flat-ground depths come up short by |slope|·(BARREL_R+3) on sloped spans — a parked up-hide on g6's rising span died by 0.03 px before this).LETHAL_CLIMB_PX of climb + CLIMB_ESCAPE_PAD). Surface pause — inside the hold depth below the upper deck (SURFACE_HOLD_DEPTH = 16 px) he recomputes the remaining surface transit every frame; while any barrel can cross the ladder head during it he waits on the rungs just below the contact band (depth > SURFACE_BAND_PX = 14), or backs down out of the band if a fresh throw heats the window after he entered it. Surfacing into a roll-over is never a stale-plan accident.react, SR-3) before jumps and ladder grabs. The commitment window remains the native kill lever: a barrel released after he commits to a climb cannot be answered — CLIMB is irreversible until the next girder — and that is information-window play, not a bug.ontouchstart / maxTouchPoints, flagged html.is-touch) get tappable title/READY advancement, an on-screen pause button, and RESUME/QUIT/RETRY/TIER-SELECT buttons in overlays (shipped padding 14 px 22 px, ≥40 px tap target) — the entire state machine completable without a keyboard. Keyboard-hint copy swaps to touch copy via the standard .kbd/.touch span pair.gcd / cfg.gcd fill) and every release cell dims (.cell.gcd) — one glance separates "this cell is cooling" from "the whole deck is winding up". Hidden entirely when idle; no text.title_brand + " — " + title_suffix, both localized per language), a hook line with the translated tier name and barrel count, the throw timeline rendered as an emoji row (💥 kill, 💨 jumped clean over, 🛢️ miss / retired into drum, capped at 12 symbols with …), and a call-to-action line with the page URL.public/shared/share.js. Clicking SHARE RESULT always opens a retro CRT preview modal first — the player sees the exact card before anything leaves the device. From the modal: COPY (clipboard + execCommand legacy fallback + retro toast), native SHARE via the Web Share API sheet where available (a dismissed sheet is not an error), CLOSE. Modal button labels are localized in all 8 languages by the engine's own dictionary. Purely additive: no server, no account, no change to the match loop.share_btn, share_copied, share_hook, share_cta dictionary keys in all 8 languages; card assembly (buildShareCard) is a pure function pinned byte-for-byte by the 06-share test suite.The match is real-time at the arcade fixed step. The atomic economic event is the release: one press, one heave animation, one barrel, one bonus-clock decrement. Cooldowns are denominated in real seconds (not barrels), tick only while state === 'play', and freeze on pause. The bonus clock starts at the native L1 value (5000 display = 50 internal units → ~50 releases per climb) and refills on every escape. The native DIFFICULTY byte advances on elapsed board time (~1 per 33 s, capped at 5) independent of tier; it rescales barrel speed and the reseed arms (SR-4), so an attempt that runs long escalates itself.
The original resolved DK's decision layer with dice and latches only because no human controlled his side of the cabinet. The reversal hands that layer to the player: the ROM's own barrel behaviors become seven selectable releases, each a regime the ROM itself can produce, with a cooldown as its cost. Physics and trajectories are identical for every option — an option decides when a barrel changes planes, never how it moves.
| Key | Release | Descent mode | Native grounding | Starting anchors (Beginner / Intermediate / Expert) |
|---|---|---|---|---|
| 1 | ROLL | Native lottery — the SR-4 gate chain decides at every ladder head | The shipped arcade behavior (15/16 release gate) | 2.7 / 3.0 / 3.3 s |
| 4 | DEEP ROLL | Never descends — rolls the full zigzag to the drum | The pre-latch regime / first-barrel behavior (R-6) | 5.2 / 6.8 / 8.2 s |
| 7 | CHASE ROLL | Descends at every ladder head it passes | The pre-latch regime read literally (latch arm of startBarrelDescentAtLadder) | 9 / 13.5 / 18 s |
| 2 | HURL | — single form; ballistic arc, never descends (mode NONE) | high hurl across the board, then native bounce reseed | 9 / 15 / 21 s |
| 3 | BLUE | Native lottery + drum ignition | Alternate-kind bit (~12.8% measured) on the roll path | 12 / 18 / 24 s |
| 6 | DEEP BLUE | Never descends + drum ignition | Kind bit × pre-latch regime | 18 / 27 / 36 s |
| 9 | CHASE BLUE | Descends at every ladder head + drum ignition | Kind bit × pre-latch regime | 27 / 39 / 51 s |
Cooldown starts on press (the release is final when committed — same spirit as Saboteur). A press during an active heave, a winding master cadence, or on a cooling cell is ignored. The 10-barrel native table is a hard cap independent of cooldowns. Ordering guardrail: within a family, cooldown must order base < DEEP < CHASE at every tier.
Master release cadence (round 7): on top of the per-type cooldowns, one global timer gates every release — the cabinet's release-opportunity pulse (SR-4 Release scheduling) made player-facing. After any throw, no type fires for gcd seconds (tier anchors 0.9 / 1.1 / 1.3 s, Beginner / Intermediate / Expert). The cadence strictly extends the heave lockout (gcd ≥ HEAVE/60) and stays below the ROLL cooldown, so single-type spam is never gated by it — it throttles only cross-type weaving and the opening volley (the round-7 fix: all seven types could previously be emptied inside ~3 s of an attempt start, a volley even the Expert Runner cannot survive). The first throw of an attempt is ungated; the timer ticks only in PLAY, freezes on pause, is untouched by the escape reset, and a press refused by it consumes nothing. The anchors are sim-locked: Expert burst loses kills from 1.4 s up (§9.4 round-7), so the ladder sits under that cliff.
Balance — certainty cuts both ways: DEEP and CHASE barrels are deterministic, so the Runner's planner treats them as hard facts and routes around them exactly (FR-05); only the native-lottery releases contribute expected risk he cannot resolve. Each option therefore has a distinct role: DEEP = patient zone pressure and guaranteed drum delivery (blues), CHASE = shaft pursuit that punishes misroutes, lottery = unresolved threat everywhere at the lowest cost. The §9 F6 checks must confirm no option is dominated and no single-option spam approaches the human-policy kill rate. The anchors deliberately compress authentic ROM frequencies for playability (a literal 1/16 gate at ~2 s cadence ≈ 32 s between hurls), the way Saboteur compresses its academic results into playable beats; the §9 sim decides the final values. Only cooldown lengths may be tuned — no new rule, meter, or ammo type may be introduced to fix balance.
| Tier | Reaction latency (react) | Jump-timing jitter | Body speed | Player cooldowns (all seven, SR-2) | Intended matchup |
|---|---|---|---|---|---|
| Beginner | 18 f | 6 | 0.8× arcade-native run/climb | Rich (SR-2 Beginner anchors) | Full vision, slow hands and feet. A fair first kill while learning the deck — joy of winning first, then graduate. |
| Intermediate | 11 f | 3 | 0.9× | Mixed (SR-2 Intermediate anchors) | Quicker feet, quicker reads; kills need prediction, not reaction throws. |
| Expert | 6 f | 1 | 1.0× (arcade-native) | Lean (SR-2 Expert anchors) | Near-perfect vision and reflexes — the F2 near-perfect standard. Only predicted spawns, double binds, and fire pincers kill him. |
The Runner's decision model is identical at every tier — full-vision planning over the shared horizon, no foresight cap, no slips, no second-best picks (2026-10-17 play-test ruling: maximum theoretical strength first). Tiers rescale only the body (reaction latency, jump jitter, speed) and the player's economy; the native DIFFICULTY ramp (SR-1) and all SR-4 physics are identical across tiers. Named constants grouped by category (NATIVE_* ROM constants, TUNE_TIERS tier parameters), each with a descriptive comment; no magic numbers in logic. If post-ship play-testing finds a tier's Runner too strong, the documented response is body-scaling (lowered speed or reaction, Ghost-Squad style) — never decision sabotage.
These are the arcade constants and decision routines, reproduced rather than reinvented. Addresses refer to the verified Z80 decompilation; implementation copies the behavior and keeps the labels in comments for auditability.
| Mechanic | Native rule |
|---|---|
| Release scheduling | Native release cadence is gated by an every-32-frames phase check against the DIFFICULTY countdown; fire rate rises with difficulty. In the reversal the player schedules releases (subject to SR-2); the same animation pipeline renders them. |
| Release-type decision | The ROM draw is a timer-seeded RNG low nibble: nonzero (15/16) → ROLL, zero (1/16) → direct drop; the first scheduled drop of a board is also forced deterministically. The orange/blue kind is an independent bit (~12.8% alternates measured). The reversal replaces both draws with the player's explicit type choice — the only place we change a decision from system to player. |
| ROLL path | Three fixed waypoints (60,74) → (72,72) → (84,84), WP_STEP = 14 frames per step; the barrel lands on the top girder (girder 1) and zigzags the sloped girders under native rolling velocity. Shipped edge behavior: rolling off a girder end drops straight down (horizontal velocity zeroed at the lip) and lands on the first girder surface below the one it left — never re-lands on its own ledge, never drifts past the next girder's catch range. |
| HURL path | A 14-frame grab pinned at the launch point (60,74), then a ballistic throw: HURL_VX = 3.0 px/f horizontal under ROM gravity at 16t+8 subpixels/frame² (terminal cap 3.5) — the arcade's high hurl. The arc clears DK's own ramp and lands on a girder below (fall contact starts checking one girder below the one it left), where the native bounce reseed takes over; mode NONE means its rolling never descends a ladder. |
| Bounce reseed | On each girder contact the barrel's horizontal velocity is re-seeded by routine family 0x22CB, selected by state: (a) before the first barrel retires to the drum — fixed magnitude by board level; (b) native difficulty 1–2 — random magnitude; (c) difficulty 3–4 — random magnitude, direction toward the Runner's X; (d) difficulty 5 — magnitude proportional to the X offset to the Runner, sign toward him. Bottom girder: fixed −1 px/f +176 up-launch, rolls into the drum (ROM detail — shipped barrels simply retire in the drum band, see Retirement below). This reseed is the ONLY native "aiming" in the game and belongs to the bounce, not the release. |
| Ladder descent | A rolling barrel tests the ladder-endpoint table every 8th pixel of travel (X mod 8 = 3) and re-decides descent fresh at every ladder head — the barrel record carries no behavior-type field; nothing is fixed at spawn. Gate chain (startBarrelDescentAtLadder): (1) a one-way global latch (0x6348) flipped by the first barrel retirement into the drum (any kind) separates the pre-latch from the graded regime (resolved — R-6: pre-latch ships as never-descends, the observed first-barrel behavior; the decomp's literal unconditional reading ships separately as the CHASE mode); (2) never when the Runner is at/above the ladder head (Mario Y − 4 vs head row + 5): descent only chases downward; (3) difficulty-scaled 2-bit draw — pass while (RNG & 3) < (DIFFICULTY >> 1) + 1, i.e. 25% → 75% across the native ramp; (4) Runner steering: exactly on the ladder column → descend at once; else moving toward the column and within sight of it (STEER_SIGHT = 24 px — barrels only dive toward a Runner they can practically reach; the live joystick in the original — this is the documented "barrel steering" technique; the Runner FSM's run direction in the reversal) → descend; else a 1-in-4 tail gate (RNG & 0x18 = 0). Replicated verbatim — the ROLL is natively homing through ladder heads, steered by the Runner's own position and direction. |
| Retirement / drum | Barrels retire in the bottom-left drum band (X 30–43, Y ≥ 228). A BLUE barrel reaching the drum writes fire-arm state 3: after a 16-count ÷ 4 prescale (~64 frames) exactly one fireball spawns at the drum mouth (x 44, floor girder), then the arm drops to state 1 (drum lit, no more emissions) until the next blue barrel. Up to 5 fireball records exist. |
| Fireball motion | Fireballs roam girders and climb ladders under native motion; post-contact velocity reuses the 0x22CB family, so they wander early and steer toward the Runner at high difficulty. Despawn follows native edge/range retirement. |
| Bonus clock | 25m decrements the bonus once per released barrel (ROM 0x2CDA), not per second; L1 start = 50 internal units (5000 display). At zero the Runner dies the time-up death. Refilled on escape. |
| Ladder table | Fifteen ladders, all on the native X mod 8 = 3 grid, pixel-verified against the 25m screenshot (girder top → bottom; broken climb-proof for the Runner but still barrel descents): 131 full (0→1, Pauline's platform ↔ DK's ramp); 67, 83 tall sky decoys ending mid-air (top y 24; perchable, the arcade's famous safe spot); 187 full, 91 broken (1→2); 35, 75 full, 171 broken (2→3); 115, 187 full, 67 broken (3→4); 35, 99 full (4→5); 187 full, 83 broken (5→6). 11 full + 4 broken. |
| Jump geometry | The Runner's fixed-arc jump clears three framework sections (frame-data correction over the cabinet sticker's "two"). Shipped reflex model (round 5 — the low native arc): JUMP_VY0 = −1.85, JUMP_GRAV = 0.12, JUMP_AIR = 30 f, apex −13.35 px — the arc skims a barrel top (−8) with ~5 px to spare, exactly like the original, so his head stays below the floor above except near a girder's low-ceiling end (floor gap < ~27 px), where the headClear sim vets the arc against barrels riding the upper deck and vetoes the jump like a covered landing. Road-level frames sit outside [JUMP_SAFE_T1, JUMP_SAFE_T2] = [9, 21] (kill contact needs him below −11 px of arc height). A visible oncoming barrel inside JUMP_TRIGGER = 92 px starts his reaction (tier latency ± jitter, SR-3); he plants and the exact arc-vs-barrel sim (arcClear — real integration, edge clamp, girder slant, and the tick's barrel-first interleave) owns the takeoff trigger: the analytic go (speed-aware closing window) only bounds the evaluation zone, and a failed read inside the zone is classified too-early vs too-late by an exact lookahead that steps the barrel ahead (wait-sep — hold the plant while the window is still approaching). A barrel first sighted too late for the window falls back — neutral hop (same exact read with zero drift) / stand-base / retreat — with the marginal forward arc only as a wall-pinned last resort. Fireballs are never jumped: their thicker contact band (body-center ±8) outlasts the low arc's safe frames at every speed, so he gives ground (fire-flee) — fires only re-steer every FIRE_STEER_FRAMES, making sustained lateral distance and ladders the honest answer. A jump whose arc and landing are both covered is the double-bind kill; he otherwise always jumps cleanly (FR-05 guardrails). |
| Object cap | 10 barrel slots (stride 0x20), active/claimed/empty flags per slot. Full-table presses are refused (FR-03). |
Evidence base: qarl/arcade-js games/dkong/ (idiomatic decompilation, pixel-diff verified against MAME 0.288; the project itself lists the airborne continuation at ROM 0x20EC as its open issue #1 and the eight unlogged dropping barrels as #17 — see R-6), Computer Archaeology annotated Z80 dump (ROM tables 0x39B9–39CE), and Don Hodges' Controlling the barrels in Donkey Kong (2008) for the ladder-head routine. Open reconciliation work is part of the build, not a license to invent.
Esc toggles PAUSED; the fixed-step clock, heaves, all cooldowns, barrel physics, fireballs, the bonus clock, and the Runner freeze. Window blur or tab hidden forces PAUSED. PAUSED dims the board and shows touch-capable controls; no state is lost.
Stored in localStorage: per-tier best kill score (barrels thrown; tiebreak time), mute preference, last played tier. Storage unavailable → session-only, no error surfaced.
| Case | Behavior |
|---|---|
| Player never throws (idle) | Runner climbs untouched, escapes, reclimbs forever. Bonus never drains (it is per-release on 25m). No score, no kill, no unlock — endless escape loop until the player engages or quits (FR-03). Shipped cadence (browser verification): an untouched Beginner Runner completes a climb in ≈4–5 s — 5 escapes in ~20 s idle. |
| Press during a heave animation or on a cooling cell | Ignored, never queued; the cell shows its sweep state. No "buffered smart throw". |
| 10 barrels already active | Press refused with a brief deck shake; cooldown not consumed (no barrel was released). |
| Kill and escape complete on the same frame | Kill wins; attribution goes to the fatal release. |
| Bonus reaches zero during an escape animation | Escape wins the frame (he touched Pauline); bonus refills. Edge case pinned by 03-flow tests. |
| HURL lands with no Runner anywhere near its lane | A whiff: it bounces the native chain and retires. Whiffs are the cooldown's cost — this is required, never silently corrected toward him. |
| Fireball cap (5) reached when a BLUE retires | Drum ignites/relights and the emission is consumed natively with no spawn (ROM table full), exactly as arcade behavior. |
| Small viewport / portrait phone | Mobile layout (FR-08); below a usable minimum, proportional board scaling and a compact strip — reject only below ~300 px width. |
| Autoplay-blocked audio | Engine initializes on first gesture; game fully playable silent. |
localStorage blocked | Session-only bests; no error surfaced (SR-6). |
title_brand only.The tier numbers are not guessed before implementation: the game is built instrumented, then the same three-layer discipline shipped with the other titles is run. All logic tests load the real index.html in-memory with stubbed browser APIs; the file on disk is untouched.
tests/barrel-toss/harness.js): loads the real HTML, stubs DOM/canvas/Web Audio/rAF, and exposes a deterministic control hook (window.__BT) with: fixed-step frame advance, seeded RNG pin (barrel ladder rolls, bounce reseeds, slip draws), release injection by type, full internal state (Runner FSM + target ladder + risk estimate, all barrels with type/path-state, fireballs, bonus, cooldowns, DIFFICULTY byte, stats), and a deterministic policy socket for both the Runner channels and thrower policies.tests/barrel-toss/run.js): per-file-isolation Node runner, same pattern as the shipped games. Run: node tests/barrel-toss/run.js.| File | Focus | Scenarios |
|---|---|---|
01-barrel-physics.test.js | SR-4 fidelity | ROLL follows 4-waypoint string at 24 f/step · HURL follows 1 waypoint then 16t+8 gravity on the fixed column · all four bounce-reseed arms select correctly by pre-drum/difficulty state · ladder-head test every 8 px with the never-above-Runner gate, X/steer bias, and 25% fallback · all three descent modes gate correctly (lottery per SR-4; DEEP never starts a descent; CHASE starts at every head) · retirement band only · BLUE arms exactly one fire after ~64 frames, then state 1 · 5-fire cap · bonus decrements exactly one per release and refills on escape · 10-slot cap |
02-runner.test.js | FR-05 behavior | FSM transitions legal; CLIMB cannot JUMP · always jumps a single visible barrel on open ground · never walks into a visible barrel · commits only to reachable ladders (never broken) · planner foresight is the shared full-vision horizon at every tier (no tier cap) · determinism: identical seeds reproduce the Runner exactly · death only via the native kill classes · all-shafts-busy siege: never motionless without a priced wait or holdX, never eats a barrel · commit gate: never enters a shaft while a barrel beats him to the safe height · surface pause: holds below the deck while a barrel crosses the ladder head · converging barrels: most-urgent reflex prevents freeze on stacked streams · zero bonus → time-up death · reaches Pauline → escape · round 4: single-barrel geometry sweep (3 tiers × 5 girders × positions × 3 speeds × both approaches): never dies to one readable barrel, forward takeoffs always cross inside the high-arc window unless wall-pinned, neutral hops always clear out before touchdown · drum-side retiree latched on the approach (intent-aware phantom read), never discovered point-blank · late sighting: no road-level forward takeoff while retreat room exists · round 5: sweep/late-sighting takeoff invariants re-pinned on the exact arcClear sim at the decision-read positions (runner pre-step, barrel post-step — the tick steps barrels first) · converging-pair regression geometry widened beyond one full arc (a 30 px gap at vx 1.0 equals JUMP_AIR — provably lethal for any player on open ground) · round 6: cluster with no jump window flees to the floor ladder, hides UP on the rungs, holds while the stream passes underneath, releases and resumes (never jumps, never dies) · slow stream with both landings covered flees and hides DOWN the ladder, climbs back once the deck above clears · escape refused while a descent occupies the shaft (falls back to the generic layer, never freezes) · escape refused against an inbound CHASE stream (the chaser would dive in after him) |
03-flow.test.js | State machine & no-auto-action | READY ignores releases · escape resets Runner/bonus/barrels but keeps barrel total and escape count · kill vs escape same-frame tie → kill · pause freezes physics/cooldowns/bonus · blur auto-pause · quit records nothing · best never worsens · no code path auto-throws or auto-selects a release type (Stacked auto-place guardrail) · press during heave ignored · cooling press ignored and not consumed · round 7: press during the master cadence ignored, never queued |
04-economy.test.js | SR-2/SR-3 | Cooldown starts on press, ticks real seconds in PLAY only, frozen in pause, floor ≥ animation length · all three tier configs load and differ only in the documented parameters · named-constants audit (no unnamed magic numbers in tuned logic) · deck state matches internal cooldowns · full-table refusal does not consume cooldown · round 7: master cadence gates every type independent of per-type cooldowns, ticks in PLAY only / frozen in pause, first throw of an attempt ungated, anchors 0.9/1.1/1.3 with the ≥ heave-animation floor · wind-up rail starts exactly full and hides on expiry, all seven cells dim while winding |
05-touch-layout.test.js | FR-08 | Breakpoint switch at 900 px: board-left/panel-right vs board-top/deck-bottom · deck key positions match numpad (1/2/3 bottom, 4/6 middle, 7/9 top) · tap targets ≥46 px · cooling/ready/committed states differ by sweep+border, not color alone · full match completable touch-only in the E2E layer |
| Policy | Definition | Role |
|---|---|---|
| Idle | Never throws | Sanity: Runner escapes indefinitely; bonus full; no accidental kills |
| Arcade-auto | Mimics the original ROM itself: releases on the native cadence with the native 15/16 + 1/16 and 12.8% draws | Lower bound. The 1981 CPU opponent. A Runner that dies to this often is too weak — the arcade game is beatable by humans, so the lower bound is "escapes, reliably" |
| Roll-spam | Native-lottery ROLL the instant off cooldown; never DEEP/CHASE/HURL/BLUE | F6: uniform streams must be jumpable/ladderevable |
| Hurl-camp | HURL the instant off cooldown during any climb; ROLL never | F6: fixed-lane camping must whiff at high rate against routing |
| Blue-spam | Native-lottery BLUE off cooldown; otherwise idle | F6: area pressure alone must not close climbs |
| Chase-spam | CHASE ROLL the instant off cooldown; otherwise idle | F6: deterministic chasers are fully legible to the planner — shaft avoidance must hold at high rate |
| Oracle/prediction | Evaluates every legal release moment by forward-simulating SR-4 against the Runner's actual policy; takes the moment with highest kill probability; two-beat setups allowed | Upper bound on achievable lethality |
| Human-like | Same evaluation as oracle but top-3 moment/type choice weighted (≈0.6/0.3/0.1), with deliberate reaction delay and no access to the Runner's slip RNG | The real-player proxy; win bands apply to this policy |
Each attempt sim caps at 5 escapes (attempt failure for measurement only — the shipped game has no cap). Seeded batches (≥60 at Beginner, ≥40 at Intermediate, ≥20 at Expert per policy), pinned RNG, telemetry per release: type, moment, target lane, result (whiff/jumped/ladder-descended/retired/fire/kill), and for every kill the throw-vs-commit timing gap that proves attribution. Shipped harness: tests/barrel-toss/sim.js — spam/mix/burst × 3 tiers × 12 seeded attempts on one loaded game instance (startAttempt reseeds per attempt), printing kills/N + median kill time per cell and re-running the first non-kill seed with the decision-trace tail; a full sweep runs in seconds.
| Tier | Human-like kill rate within 5 escapes | Arcade-auto lower bound | Oracle upper bound | F6 guard |
|---|---|---|---|---|
| Beginner | ≥ 70% target; median kill ≤ ~30 throws | Escapes ≥ 90% of climbs (not trivially killable) | ≥ 90% (the kill exists) | Best degenerate policy's kill rate ≤ 60% of the human-like policy at that tier; no single release option accounts for >70% of human-policy kills at Beginner/Intermediate, and no option is dominated — the human policy must find a use for all seven (Expert HURL share may rise but lane-avoidance must remain measurable) |
| Intermediate | 40–65% | Escapes ≥ 95% | ≥ 75% | |
| Expert | 10–30% | Escapes ≥ 98% | ≥ 50% |
Round-2 revision (2026-10-17 play-test): shipped tuning supersedes the Expert band above — the Runner ships at maximum theoretical strength at every tier (body-only scaling, SR-3). Measured with the shipped script policies (roll-spam / learning-rotation / reactive-burst, 12 seeded boards each): Beginner and Intermediate fall 12/12 to every policy; Expert falls to spam and mix and holds the reactive-burst script to 0/12 — its escapes require spawn prediction, which is precisely the player-side skill the game teaches. If post-ship play-testing finds him too strong, the response is body-scaling per tier (SR-3 note), never decision sabotage.
Round-5 revision (2026-10-19 play-test, low native arc): the round-4 arc retuned to the original's barrel-skimming jump (SR-4 Jump geometry); all decision reads are exact sims. Measured (same policies): mix 12/12 and burst 12/12 at every tier (median ~10–20 s) — the forced-win path is intact; spam 12/12 Beginner, 11/12 + 1 escape Intermediate, 11/12 + 1 escape Expert. The two spam escapes are the accepted consequence of fixing the reported jump bugs: round-4's spam kills were partly parasitic on his own mistimed takeoffs, and a clean Runner does not die to a uniform-speed conga line — killing him requires varying releases, which still lands in ~10 s. No timeouts in any cell (no freeze regressions).
Round-6 revision (2026-10-20 play-test, escape-hatch hides): the cluster answer shipped (FR-05 escape hatch) — he flees unjumpable binds to a hide ladder and holds on the rungs at the slope-exact safe depth. Measured (same policies, 12 seeded boards per cell): mix 12/12 at every tier (median 12.8 / 9.9 / 10.0 s) and burst 12/12 at every tier (19.5 / 10.4 / 10.5 s) — the forced-win path is intact with the hide live; spam 12/12 Beginner, 11/12 + 1 escape Intermediate, 11/12 + 1 escape Expert (the same accepted round-5 spam outcome; CHASE natively punishes ladder play, so the hide does not leak lethality). No timeouts in any cell.
Round-7 revision (2026-10-21 play-test, master release cadence): the SR-2 global cadence shipped (gcd 0.9 / 1.1 / 1.3 s) — the opening alpha-strike (all seven types inside ~3 s) is gone; every release now waits the cabinet's opportunity pulse. The pre-chosen 1.2 / 1.4 / 1.6 ladder failed the forced-win invariant at Expert (burst 11/12 at 1.4–1.5, 5/12 at 1.6 — the spaced-out follow-ups let him thread the stream and escape), so the anchors were sim-probed down under the 1.4 s cliff. Measured with the shipped script policies (12 seeded boards per cell): mix 12/12 at every tier (median 10.7 / 9.9 / 8.8 s), burst 12/12 at every tier (11.3 / 11.0 / 20.9 s), spam unchanged (12/12 Beginner, 11/12 + 1 escape Intermediate, 11/12 + 1 escape Expert — cadence < ROLL cooldown by construction, so single-type spam is never gated). No timeouts in any cell. Browser: zero game console errors; wind-up rail + all-cell dim verified in the live DOM (rail on, fill draining at exactly gcd/cfg.gcd, refused press consumes nothing).
Tuning protocol: tune Beginner to its band first (joy of winning), then Intermediate, then Expert. Allowed knobs, in order: (1) the three Runner body parameters (SR-3), (2) the seven cooldown lengths and the master cadence (SR-2). Never allowed: changing physics, adding weapons/rules, making the Runner miss jumps he can see, or aiming releases. After every change re-run the full policy matrix, not just the failing tier, and append a tuning-history table to this spec (Saboteur §9.5 precedent). R-1's failure response applies if no knob setting lands the bands.
| # | Scenario | Asserts |
|---|---|---|
| 1 | Boots to TITLE | Title overlay, zero console errors |
| 2 | Advance to TIER SELECT; all three unlocked | Three cards, bests empty |
| 3 | Start Beginner: READY → PLAY | Runner spawns at native point, bonus full, board pixels match reference crop |
| 4 | Key 1 / tap ROLL | Heave animation plays, barrel on ROLL waypoints, bonus −1, cooldown sweep animates |
| 5 | HURL timing + cooldown | Ballistic arc lands below DK's ramp; press during cooldown ignored; seconds readout matches elapsed |
| 6 | BLUE → drum | Ignition, one fireball after ~64 frames, drum relit state |
| 7 | Esc pause/resume | Physics, cooldowns, bonus frozen across a measured interval |
| 8 | Runner reaches top → escape loop | Escape count increments, barrel total kept, bonus refills, play resumes |
| 9 | Instrumented ladder-catch kill | KILLED → SUMMARY with correct kill class, throw type, stats; best persisted |
| 10 | Touch-only match in mobile layout | Deck bottom, ≥46 px cells, tap throw → kill path reachable, overlay buttons tappable |
| 11 | Mute persists; reload keeps bests | localStorage round-trip |
| 12 | Zero console errors across the sweep | No exceptions, no warnings |
Barrel Toss uses the series' shared engine public/shared/i18n.js plus the per-page dictionary public/shared/lang/barrel-toss.js, namespace barrel_toss. Offline-safe, dependency-free, fixed top-right switcher.
en, zh, es, de, fr, ru, ja, ko. English is a first-class dictionary block (en, first in the file), never hardcoded except last-resort pre-init/harness fallback.RG_I18N.init('barrel_toss') resolves localStorage['rg.lang'] → navigator.languages/navigator.language (2-letter prefix match) → en. Switcher writes the override; choice persists.data-i18n="barrel_toss.<key>"; original innerHTML captured once as last-resort fallback (<kbd>, entities preserved). Dynamic strings via local T(key, englishDefault).title_brand is the first key in every language block (title-screen wordmark); doc_title is derived as title_brand + " — " + descriptor, and the header wordmark derives from the same field — renaming in one place propagates to the tab, title screen, and header.RG_I18N.onChange(...) for JS-built DOM: deck cells, tier cards, SUMMARY stats.The harness evals only the bare inline game script; external engine/dictionary tags are invisible to it, so window.RG_I18N is undefined there. The boot is guarded (if (window.RG_I18N) …) and T() returns its English default in the harness — deterministic tests and offline play are unaffected by localization.