Skip to content

preview_play omits three game_state keys that score_hand reads — Loyalty Card's cycle freezes #9

Description

@idIing

Summary

playground/exact_score.py::preview_play builds a synthetic game_state for the engine's
score_hand, but never sets three keys that score_hand actually reads. Each defaults, so the
preview silently scores some boards differently from the engine that will actually run the hand.

GreedyTactical ranks every candidate play through preview_play
(src/jackhammer/playground/harness.py:195), so this reaches the reference agent's play
selection, not just a diagnostic path.

Verified against main @ c3a24a0, engine pin 4d6f19d.

The three keys

Set difference between what jackdaw/engine/scoring.py reads out of game_state and what
preview_play sets on synth (19 keys set, 19 read, three of them disjoint):

key read at consumer effect when it defaults
hands_played scoring.py:447hands_played_run Loyalty Card, jokers.py:1214 joker evaluated at run counter 0
skips scoring.py:458ctx.game.skips Throwback, jokers.py:1868 x = 1 + skips * extra collapses to x1
chips scoring.py:535,908 Mr. Bones rescue path round chips treated as 0

Loyalty Card — confirmed empirically

Loyalty Card's window is recomputed from the run-wide hands counter
(Balatro/card.lua:3633, config {Xmult = 4, every = 5} at Balatro/game.lua:393), and Jackdaw
models that faithfully. With hands_played absent the preview always evaluates at counter 0, so
whether it fires depends only on hands_played_at_create % 6 — constant for a whole run, rather
than cycling.

Measured on one real position at the same engine pin: run counter 17, joker created at 7.

  • engine: (5-1-(17-7)) % 6 = 0 → does not fire
  • preview: (5-1-(0-7)) % 6 = 5 → fires

Preview reported total=440 (mult=4.0) where stepping the engine added 110. Ratio exactly
4.00, reproduced on three of four plays in one hand; the fourth agreed because the engine
genuinely fired there too.

Why skips is the one I would fix first

skips is currently 0 in practice only because agents were never given the blind-skip
decision
. Protocol v2 removed the BLIND_SELECT auto-step, which is exactly the fix that makes
skips start incrementing. So this defect is dormant today and arms itself as a direct
consequence of the fidelity fix — on Throwback boards, silently, with no error.

Suggested check

A regression that asserts preview_play's total equals the engine's actual chip delta on a board
holding each of Loyalty Card, Throwback, and Mr. Bones. The existing bit-exactness gate over
recorded plays did not catch this, which suggests those three boards are simply absent from the
corpus — worth confirming rather than assuming.

What is not established here

I have not measured the magnitude of the effect on any published number. This report
establishes that the mechanism exists, that it reaches the reference agent's ranking, and that it
is live on main — not how far it moves the reported deltas. Treat the impact as unaudited.

Provenance

Found by Claude (Opus 5) during offline search experiments against this engine pin. The search
certifies each positive result by replaying its candidate line through the real engine; one line
previewed 5128 and replayed 3070, and tracing that disagreement produced this report. Worth noting
for the fix: a scorer that is never re-checked against the engine cannot surface this class of
bug, which is why a preview-vs-engine regression is the durable form of the check above.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions