Skip to content

exchanging with one tile in the bag#96

Merged
andy-k merged 2 commits into
mainfrom
exchanging-with-one-tile-in-the-bag
Jul 14, 2026
Merged

exchanging with one tile in the bag#96
andy-k merged 2 commits into
mainfrom
exchanging-with-one-tile-in-the-bag

Conversation

@andy-k

@andy-k andy-k commented Jul 14, 2026

Copy link
Copy Markdown
Owner

Summary

when only one tile is left in the bag, some configs (Spanish among them) allow that tile to be exchanged, and the previous PEG-1 solver declined every such config because its enumeration modeled only plays and passes. this teaches the one-in-bag solver to handle the exchange. with one tile in the bag an exchange is a one-for-one swap: return a rack tile, draw the lone bag tile, and the returned tile becomes the new fully-known one-tile bag with the opponent to move -- so the bag stays size one, the position stays fully known, and the game is still finite and exactly solvable. one_in_bag_minimax_ex now enumerates each distinct rack tile as the returned tile alongside the plays and the pass, and the decline is narrowed to only the configs that genuinely cannot be solved this way. the second commit documents all of this in lab/endgame.txt.

Changes

  • solve the one-in-bag exchange as a one-for-one swap (return a rack tile, draw the lone bag tile, returned tile becomes the new one-tile bag), keeping the bag at size one and the position fully known.
  • because a pass leaves both racks untouched, a run of passes resolves to the same leftover point margin (the plain solver's two-pass shortcut); an exchange changes a rack, so that shortcut no longer holds and the search tracks consecutive-pass and consecutive-zero counts, ending the game by the config's num_passes_to_end / num_zeros_to_end rule (a pass advances both counts; an exchange counts as a zero, resetting the pass count and advancing the zero count).
  • memoize on (mover, both racks, bag tile, pass count, zero count); the board never changes in the bag phase so it stays out of the key.
  • narrow the decline to configs where an exchange is legal but scoreless turns can never force an end, so an exchange chain need not terminate -- an honest "not solvable" instead of a wrong number.
  • document the swap, the pass-collapse-but-not-exchange reasoning, the memo key, the clairvoyance assumption, and the remaining declined case in lab/endgame.txt.

Correctness

the empty-bag solver and the no-exchange path are untouched, so every English and empty-bag value is byte-identical. the result is exact given the same per-hypothesis clairvoyance the PEG enumeration already assumes (within each guessed bag tile the mover is treated as knowing it), an upper bound on what a real player can guarantee and the same assumption every pre-endgame solver makes. tests add a Spanish-style config (English tiles, one-tile exchange, short scoreless end) and check the fast search against an independent un-memoized reference for both movers and several bag tiles; a hand-checkable position where swapping a lone Q for a bag A beats passing; and that a solvable config is answered while an unsolvable one is declined.

andy-k added 2 commits July 13, 2026 04:43
The previous PEG-1 commit declined any config where an exchange is
legal against the one-tile bag, because the enumeration modeled only
plays and passes. Spanish is exactly that case (its exchange_tile_limit
is 1, so a lone bag tile can still be exchanged), so it was declined
rather than solved. Solve it now, and narrow the decline to the few
configs that genuinely cannot be solved this way.

With one tile in the bag an exchange is a one-for-one swap: return a
rack tile, draw the lone bag tile, and the returned tile becomes the
new (still fully known) one-tile bag with the opponent to move. So the
bag stays size one and the position stays fully known -- the game is
finite and exactly solvable. one_in_bag_minimax_ex enumerates each
distinct rack tile as the returned tile alongside the plays and the
pass.

A pass leaves both racks untouched, so a run of passes resolves to the
same leftover point margin no matter how many -- the plain solver's
two-pass shortcut. An exchange CHANGES a rack, so that shortcut is no
longer valid: the game only ends once enough scoreless turns pile up.
This tracks the consecutive-pass and consecutive-zero counts and ends
the game by the config's num_passes_to_end / num_zeros_to_end rule (a
pass advances both counts; an exchange, which counts as a zero here,
resets the pass count and advances the zero count). A node's value now
depends on how close that end already is, so (mover, both racks, bag
tile, pass count, zero count) is memoized; the board never changes in
the bag phase, so it stays out of the key.

The empty-bag solver and the no-exchange path are untouched, so every
English and empty-bag value is byte-identical. The decline now fires
only when an exchange is legal but scoreless turns can never force an
end (so an exchange chain need not terminate) -- an honest "not
solvable" instead of a wrong number.

This is exact GIVEN the same per-hypothesis clairvoyance the PEG
enumeration already assumes: within each guessed bag tile the mover is
treated as knowing it. Exchange fits that model unchanged.

Tests add a Spanish-style config (English tiles, one-tile exchange,
short scoreless end) and check the fast search against an independent
un-memoized reference for both movers and several bag tiles; a
hand-checkable position where swapping a lone Q for a bag A beats
passing; and that a solvable config is answered while an unsolvable one
is declined.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Document the one-in-bag exchange support: the one-for-one swap that
keeps the bag at a single known tile, why passes can be collapsed to
their leftover margin but exchanges cannot (so the search counts
scoreless turns and ends by the config's rule), what the memo key
holds, and the per-hypothesis clairvoyance the value assumes -- an
upper bound on what a real player can guarantee, the same assumption
every pre-endgame solver makes. Also note the one case still declined:
an exchange-legal config whose scoreless turns never force an end.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@andy-k
andy-k merged commit e94367a into main Jul 14, 2026
3 checks passed
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