Skip to content

fix: enforce must-use-both-dice and must-use-larger-die rules#129

Merged
nodots merged 3 commits into
developmentfrom
fix/must-use-both-dice-rule
May 31, 2026
Merged

fix: enforce must-use-both-dice and must-use-larger-die rules#129
nodots merged 3 commits into
developmentfrom
fix/must-use-both-dice-rule

Conversation

@nodots

@nodots nodots commented May 31, 2026

Copy link
Copy Markdown
Owner

Problem

In production game 01727ded-7a6a-4125-8729-02f44a6b60db, a player rolled [5,2] and was allowed to play only the 5 (cc7→cc2), recording the 2 as a no-move — illegal. A legal sequence using both dice existed: play the 2 first (cc7→cc5), then the 5 (cc23→cc18). The rules require using as many dice as legally possible.

Root cause: Play.pureMove validated against the post-move state (result.newBoard/result.newPlay), where the stranded die was already consumed. The alternative-sequence explorer only inspects ready moves and found none, so it always reported the single-die play as valid.

Fix

  • Play.initialize computes the maximum dice any legal ordering can use from the turn-start board (reusing testSequenceDiceUsage) and persists it on the play (maxDiceUsable, dieValuesPlayableAtStart). Covers doubles.
  • Play.pureMove enforces it once no ready moves remain: MustUseBothDiceError when fewer dice were used than possible; MustUseLargerDieError for the either-but-not-both case.

Incidental fixes surfaced by the enforcement

  • Sim engine made rule-compliant (try candidates best-first, fall back on rule rejection) so simulations only play legal moves.
  • Sim LCG sign bug: (s & 0xffffffff) is signed in JS and produced negative values → invalid die values once the seeded RNG was wired into Dice. Fixed normalization to s / 0x100000000.
  • Dice gained an optional injectable random source (default = crypto randomInt, unchanged) so simulations are deterministic per seed; this makes the previously-flaky simulate-until-win reliable.

Tests

  • New must-use-both-dice-rule.test.ts: the exact production position (both directions), doubles, and larger-die — 9 tests.
  • Full core suite: 443 passed, 0 failed; sim tests now deterministic.

Depends on

nodots/backgammon-types PR #66 (adds the two BackgammonPlay fields).

nodots added 3 commits May 31, 2026 10:37
A player could complete a turn using fewer dice than the rules require
(e.g. roll [5,2], play only the 5 and strand the 2 even though playing the
2 first then the 5 uses both). pureMove validated against the post-move
state, where the stranded die was already consumed, so the alternative-
sequence search always found nothing.

Compute the maximum dice any legal ordering can use from the turn-start
board in Play.initialize, persist it on the play (maxDiceUsable,
dieValuesPlayableAtStart), and enforce it in pureMove once no ready moves
remain: throw MustUseBothDiceError when fewer dice were used than possible,
and MustUseLargerDieError for the either-but-not-both case.

Also:
- Make the Sim engine rule-compliant (try candidates best-first, fall back
  on rule rejection) so simulations only play legal moves.
- Fix the Sim LCG normalization: (s & 0xffffffff) is signed in JS and
  produced negative values, yielding invalid die values once the seeded RNG
  was wired into Dice for deterministic runs.
- Add an optional injectable random source to Dice (default unchanged) so
  simulations are deterministic per seed.
The must-use-both-dice enforcement reads play.maxDiceUsable /
dieValuesPlayableAtStart, added in backgammon-types 1.0.3. Lockfile
regeneration is deferred until 1.0.3 is published to npm.
….0.4

Resolves the pre-existing npm ci drift (lock pinned rc.1/rc.3 against ^1.0.0
ranges) and points at the published backgammon-types 1.0.3 that carries the
maxDiceUsable fields.
@nodots
nodots merged commit e716005 into development May 31, 2026
1 check failed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant