Ten-pin bowling for Roblox — the frame-scored sports genre rebuilt as pure property-tested scoring laws: exact strike/spare lookahead, a 300-perfect-game invariant, tenth-frame bonus-roll rules, and standing- pin legality, over real physics lanes with two-sweep launch meters.
- Every player gets their own lane with 10 physics pins.
- Click BOWL → the POWER meter sweeps; click to lock it. The ANGLE sweep follows; click again to launch. Server converts fractions into ball velocity and lets physics do the rest.
- Pins count as fallen when they tip past 60° or drift off their spot; legality law clamps knockdowns to pins actually standing.
- Classic ten-frame rules: strikes/spares chain bonuses, the tenth grants extra rolls on strike or spare, and the game ends exactly when the tenth completes. Chase the perfect 300.
- Frame law: open frames sum; SPARE = 10 + EXACTLY the next one roll; STRIKE = 10 + EXACTLY the next two — shared bonus rolls between consecutive strikes count once for each role, never double-counted.
- Unscoreable law: a strike with fewer than two future rolls adds NOTHING yet (0), then scores once the rolls arrive.
- Perfect-game law: twelve strikes = EXACTLY 300; eleven strikes + a nine = 299 (270 through frame nine + 29 in the tenth).
- Tenth-frame law: open tenth ends after two rolls; a spare grants exactly one bonus (5/5/7 → 17); a strike grants exactly two (10/3/4 → 17); a second tenth strike RESETS the rack for legality purposes.
- Legality law: pinsStandingForNextRoll tracks frames, spares, and tenth resets exactly ({6}→4, {6,4}→10, {10}→10).
- End law: not over at 9 strikes, not over when the tenth starts, over precisely on completion; position() tracks frame/roll exactly.
- Corpus: the canonical Wikipedia example scores its published 133; all gutters score 0 and end the game.
Two genuine engine bugs caught by the corpus: strike bonuses read the
strike itself plus ONE future roll instead of the actual next two
(off-by-one on flat[start]), and the tenth-frame sum used
table.unpack inside a compound assignment — which silently takes only
the FIRST value. Three spec expectations also had hand-arithmetic
errors where bonus rolls legitimately re-score as the following frame's
own content (e.g. {10,7,2} totals 28, not 19). Plus the recurring
pre-declaration nil-global (isFallen) and a duplicated meter state
machine in the HUD.
src/shared/ Config + BowlingMath (frame/perfect/tenth/legality/end laws)
src/server/ Remotes + BowlingService (private lanes, physics pins,
tip-or-drift fallen detection, settle windows)
src/client/ Network + two-sweep power/angle meter + roll history
tests/ lune headless specs (17) incl. published-score corpus
lune run tests/run_tests.luau # unit tests
lune run tests/syntax_check.luau # compile-check all sources
rojo build -o strike-zone.rbxlx # validate project mapping
rojo serve # live-sync into StudioCI runs tests, syntax checks, and a Rojo build on every push.
- No gutter walls — balls that drift far enough fall to the floor
- No spin/hook mechanic
- No Studio playtest yet (human dependency)