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:447 → hands_played_run |
Loyalty Card, jokers.py:1214 |
joker evaluated at run counter 0 |
skips |
scoring.py:458 → ctx.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.
Summary
playground/exact_score.py::preview_playbuilds a syntheticgame_statefor the engine'sscore_hand, but never sets three keys thatscore_handactually reads. Each defaults, so thepreview silently scores some boards differently from the engine that will actually run the hand.
GreedyTacticalranks every candidate play throughpreview_play(
src/jackhammer/playground/harness.py:195), so this reaches the reference agent's playselection, not just a diagnostic path.
Verified against
main@c3a24a0, engine pin4d6f19d.The three keys
Set difference between what
jackdaw/engine/scoring.pyreads out ofgame_stateand whatpreview_playsets onsynth(19 keys set, 19 read, three of them disjoint):hands_playedscoring.py:447→hands_played_runjokers.py:1214skipsscoring.py:458→ctx.game.skipsjokers.py:1868x = 1 + skips * extracollapses to x1chipsscoring.py:535,908Loyalty Card — confirmed empirically
Loyalty Card's window is recomputed from the run-wide hands counter
(
Balatro/card.lua:3633, config{Xmult = 4, every = 5}atBalatro/game.lua:393), and Jackdawmodels that faithfully. With
hands_playedabsent the preview always evaluates at counter 0, sowhether it fires depends only on
hands_played_at_create % 6— constant for a whole run, ratherthan cycling.
Measured on one real position at the same engine pin: run counter 17, joker created at 7.
(5-1-(17-7)) % 6 = 0→ does not fire(5-1-(0-7)) % 6 = 5→ firesPreview reported
total=440(mult=4.0) where stepping the engine added110. Ratio exactly4.00, reproduced on three of four plays in one hand; the fourth agreed because the enginegenuinely fired there too.
Why
skipsis the one I would fix firstskipsis currently 0 in practice only because agents were never given the blind-skipdecision. Protocol v2 removed the
BLIND_SELECTauto-step, which is exactly the fix that makesskipsstart incrementing. So this defect is dormant today and arms itself as a directconsequence 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 boardholding 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.