You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The dealer can now deal 7-card (formerly basic), 10-card, or heads-up gin. The options depend on the seat count: 2 seats get all three, 3 seats get 7-card and 10-card, and 4 seats get 7-card only. In every variant the ace runs high or low but never wraps around.
Best-meld search (arrange.h): finds each hand's least-deadwood melds, with lay-offs. It is checked against a brute force and against a corpus generated by an independent Python oracle (testdata/arrange_corpus.json). The UI replays the same corpus.
GinState:
The upcard round.
A knock at 10 deadwood or less.
Gin and undercut scoring.
A drawn deal once the stock is down to 2.
Its own v1 JSON serialization, which refuses rows that couldn't be played.
Table: TableState holds Deal = variant<GameState, GinState>. The hub goes through the deal* helpers.
Wire:
New pass and knock moves.
New canDrawDiscard field on the view.
A new upcard stage.
lastDeal.gin carries the arranged hands, the lay-offs and the ending.
Stats: gin deals are recorded as gin; 7-card and 10-card stay rummy. otel_contract now also pins recordedName's words against the stats and deja readers.
Storage: 7-card tables are still stored as basic; every other variant is stored under its wire word.
Docs: CLAUDE.md and WORKING_AGREEMENT.md now say never to schedule check-ins on a PR. This rides here at the owner's request.
Rollout: deploy muchq.github.io#390 first; it works against the current hub. A hub rolled back past this PR still reads 7-card tables but drops tables with 10-card or gin deals.
Verified locally: the rummy lib, games_hub (including the PG suites against Postgres), otel_contract, stats and deja are green. A gin deal was played end to end in a browser against #390.
Review: my own four-lens panel and Cursor's review ran. Their findings are fixed in this PR's later commits. Known gaps left as-is:
Lay-offs and the draw ending are covered in the lib but not on the wire.
When two arrangements tie on deadwood, the engine, not the player, picks which melds the knocker shows.
Four lenses + Bugbot. Bugbot found no bugs. Gin rules / variant dispatch / store-basic↔wire-7-card / arrange corpus look sound on the happy path. Author panel fold-ins and known deferrals (lay-offs lib-covered with empty wire goldens; engine-picked tie arrangements) accepted. build-and-test / sanitize still running at post time.
Verdict: LGTM with fold-ins. Sharpest gap is gin serde lagging GameState’s hostile-row bar; rest is docs/pins.
Survivors (verified):
Correctness / resource — deserializeGinState accepts playing rows with no legal move, and unbounded hands.game_state_serde rejects stuck playing rows ("a playing row with no move to make"). Gin has no twin: a restored phase=playing / stage=stock|draw with empty stock wedges (canDrawStock/canDrawDiscard false; pass refused). Separately, readCards allows up to 52; arrange assumes uint32_t bitmasks (1 << hand.size() UB at ≥32) and runs under hub mu_ on knock / leave / abandoned deadwood. Cap hands at gin’s 10/11 and add the stuck-row guard; pin both in RejectsWhatTheEngineCouldNotPlay.
Docs — cards/rummy/README.md misstates storage. Still says game_state_serde is what the hub stores a table in. Hub stores table_serde v2 (nested deal; 7-card as "basic"; gin via gin_serde). Fold the README to match.
Tests — gin deal → event word "gin" unpinned.recordedName(Gin)=="gin" is unit-pinned; otel scrapes it. FinishedOf / EveryEventsLineIsTextWithNothingToEscape only finish SevenCard → "rummy" (allow-list still omits "gin"). Hub e2e comment still says deals are “recorded as rummy whatever the deal's variant.” Add a gin FinishedOf (and escape-loop) case; fix the comment.
Tests (soft) — wire never pins lastDeal.gin.ending: "draw". Engine has the stock-floor draw; smithy documents it; wire goldens only gin / undercut. Author’s empty-laidOff gap stays intentional.
Altitude (soft) — basicDeal() naming residue. Storage basic means SevenCard only; basicDeal() returns GameState* for both 7- and 10-card. Prefer rummyDeal() / gameDeal(). Process tip (WORKING_AGREEMENT / CLAUDE.md) is fine folded — name it in the PR body.
Done. AGinDealPlayedOutIsCompletedAsGin pins the event word gin, and the escape loop now covers gin. A mutant that records gin as rummy is killed. I also fixed the hub e2e comment.
Not done. The draw ending (draw) is pinned in the engine (GinStock.ADiscardLeavingTwoInTheStockDrawsTheDeal), but not on the wire. With the unshuffled test deck, getting there takes about 28 scripted turns. The hub maps endings through the same switch that the gin and undercut wire goldens already exercise.
Done. basicDeal() is renamed to rummyDeal().
The docs commit (no scheduled check-ins on PRs, in CLAUDE.md and WORKING_AGREEMENT.md) is part of this PR at the owner's request.
Gin serde — hands capped at 11; stuck playing rows refused (!canDrawStock && !canDrawDiscard / empty hand at play); four cases in RejectsWhatTheEngineCouldNotPlay.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1610.
The dealer can now deal
7-card(formerlybasic),10-card, or heads-upgin. The options depend on the seat count: 2 seats get all three, 3 seats get 7-card and 10-card, and 4 seats get 7-card only. In every variant the ace runs high or low but never wraps around.arrange.h): finds each hand's least-deadwood melds, with lay-offs. It is checked against a brute force and against a corpus generated by an independent Python oracle (testdata/arrange_corpus.json). The UI replays the same corpus.GinState:TableStateholdsDeal = variant<GameState, GinState>. The hub goes through thedeal*helpers.passandknockmoves.canDrawDiscardfield on the view.upcardstage.lastDeal.gincarries the arranged hands, the lay-offs and the ending.gin; 7-card and 10-card stayrummy.otel_contractnow also pinsrecordedName's words against the stats and deja readers.basic; every other variant is stored under its wire word.CLAUDE.mdandWORKING_AGREEMENT.mdnow say never to schedule check-ins on a PR. This rides here at the owner's request.Rollout: deploy muchq.github.io#390 first; it works against the current hub. A hub rolled back past this PR still reads 7-card tables but drops tables with 10-card or gin deals.
Verified locally: the rummy lib, games_hub (including the PG suites against Postgres), otel_contract, stats and deja are green. A gin deal was played end to end in a browser against #390.
Review: my own four-lens panel and Cursor's review ran. Their findings are fixed in this PR's later commits. Known gaps left as-is: