Skip to content

preview_play's synthetic game_state is too thin for the Hook discard path — three reproducible divergences #12

Description

@idIing

preview_play (src/jackhammer/playground/exact_score.py) sets every key the engine's score_hand
reads directly, and protocol v2.1 repaired the four it had wrong. What it does not do is mirror
the live state faithfully enough for the other engine code that runs against it — specifically
_press_play, which fires Blind:press_play and, for The Hook, runs discard handlers against the
synthetic dict.

Three reproducible divergences, all on Hook boards, all at the current pin:

board wrong scorer input preview total engine total
Hook + Mail-In Rebate + Bull money 4, not 14 24 44
Hook + Castle no discard-time suit target 16 19
Hook destroys a negative Ramen + Stencil joker_slots 6, not 5 96 80

Two distinct causes:

  1. The synthetic dict is too thin for the discard path. preview_play builds synth with four
    keys (hand, discard_pile, dollars, jokers) before calling _press_play. The Hook's
    discard triggers joker handlers that read state that dict does not carry, so effects that pay out
    or retarget in the engine silently do nothing in the preview.
  2. joker_slots is read from the pre-press_play live state. It is assigned after
    _press_play but sourced from gs, so a joker destroyed during the press does not shrink it,
    where in the engine the destruction handler decrements the real gs["joker_slots"] before
    score_hand reads it.

Scope. Neither is new — both predate the published v1 numbers. Neither is reached by any
position the frozen 240-seed battery visits, which is why the differential gates in
tests/test_exact_score.py do not catch them and why no published number is affected. They matter
for an agent that meets a Hook board holding one of those jokers, and GreedyTactical ranks every
candidate play through this function, so the effect would be a silently mis-ranked play rather than
an error.

Suggested fix. Key-by-key reconstruction is the root cause; each new engine key that
_press_play or score_hand learns to read is a fresh instance of this bug. The durable shape is
to derive synth from the live state (a shallow copy with the objects scoring can mutate replaced
by defensive copies) rather than build it from an enumerated list, so an unmodelled key is inherited
rather than defaulted. That is a bigger change than v2.1 wanted to carry and wants its own
before/after differential over the battery.

Found by adversarial review (GPT-6-Astra) of #11.

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