games: no mercy on the felt, and the bill that went to the wrong window
The engine has been able to play No Mercy since aca523e. Now a browser can.
The switch is a switch, not a fourth table: the tier is still the table size,
because that is what you are paid for, and the deck is the other dial. Six faces
the normal box does not print, sized by the card's own vars and never by the box
they sit in. The stack says what the bill is on the felt, in the turn line and on
the button, and under it the deck is dead — you cannot draw your way out of a
bill somebody has run up and pointed at you.
The wild draws glow. That started as decoration and turned out to be doing work:
No Mercy prints a coloured +4 right beside the wild one, and in a hand of twenty
the glow is what tells them apart.
A buried seat is not an empty one, which is the whole trap here — a seat killed
at twenty-five holds no cards, and neither does a seat that just went out and
won. The view asks the engine which it is instead of counting to zero, so the
winner is never the corpse.
Two bugs, both found in a browser and neither findable anywhere else:
The felt's stack bill was writing into the chip bar. It was [data-pending], and
so is the bar's "your chips are still coming" readout — and the bar lives inside
the table's own root and comes first in the document. A stack quietly overwrote
the escrow message and never appeared on the felt at all. A table's attributes
are not a private namespace.
And hold'em, re-driven on the 20M-hand policy (six hands, got up 61 ahead of a
100 buy-in, money conserved to the chip — Phase 4 closed), let you click a button
that did nothing: Deal, Leave and Top up stayed alive through the whole deal
animation, where send() drops the click on purpose. The lock is on the buttons
now, not only in the variable.
Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
@@ -12,6 +12,33 @@ This plan reuses that seam wholesale and does not invent a second one.
|
||||
|
||||
A multi-session build. This section is the handover; read it before anything else.
|
||||
|
||||
### Start here (next session)
|
||||
|
||||
**Everything builds, everything is played, and nothing since blackjack is
|
||||
deployed.** Six games (seven, counting the No Mercy dial) sit on main and the live
|
||||
casino is still blackjack alone. Deploying is the next job — see "Next, in order".
|
||||
|
||||
**The money finding, and the thing to actually remember:** the *normal* UNO tiers
|
||||
had been mispriced for a while, and it wasn't No Mercy's fault. They were set
|
||||
against a naive win rate of 43/32/27%; re-measuring says 40.3/29.2/23.3%. The bots
|
||||
improved at some point after the multiples were written down and nobody re-ran the
|
||||
measurement — which §0 already warned about ("the bots and the tiers are a pair").
|
||||
Table and Full House had quietly been charging an **18–19% house edge instead of
|
||||
8%**. All six tiers are repriced off a fresh measurement, and
|
||||
**`TestTheMultiplesAreStillPriced`** now fails the build if any of them drift out
|
||||
of a 2–14% band. It is the test the normal tiers never had, which is precisely how
|
||||
they drifted. *Any change to the bots, the deck, or a rule re-opens this.*
|
||||
|
||||
And the counterintuitive one, which would have shipped wrong if it had been
|
||||
guessed: **No Mercy is easier than UNO at every table size** (naive wins 46.7% vs
|
||||
40.3% heads-up), so it **pays less** — 2.0/3.1/3.8 against the normal deck's new
|
||||
2.4/3.3/4.1. The mercy rule doesn't care whose hand hits twenty-five; it kills
|
||||
*bots* too, and every bot it buries is one fewer seat that can beat you to the last
|
||||
card. A deck built to be merciless is merciless mostly to the table.
|
||||
|
||||
Still un-deployed: **only blackjack is live.** The other five games (six, now, if
|
||||
you count the No Mercy dial) are on main and have never been deployed.
|
||||
|
||||
### Decisions taken (these close §9's open questions)
|
||||
|
||||
- **Chips are 1:1 with euros.** No second denomination.
|
||||
@@ -400,6 +427,68 @@ A multi-session build. This section is the handover; read it before anything els
|
||||
in.**
|
||||
|
||||
|
||||
- **No Mercy, on the felt, and it plays.** *(2026-07-14. The engine landed in
|
||||
`aca523e`; this is the half you can see. Driven in a browser, and that is where
|
||||
both bugs were.)*
|
||||
- **It is one switch, not a fourth table.** The betting panel grows a two-way
|
||||
segmented control (UNO / No Mercy) that swaps the three tier cards for the other
|
||||
three. The table size stays the tier — it is what you're paid for — and the deck
|
||||
is the other dial. A settled game leaves the panel on the table you just played,
|
||||
because the commonest thing anybody wants after a hand of No Mercy is another one.
|
||||
- **The six faces the normal box doesn't print.** `FACES` in `uno.js` is one table
|
||||
of {middle mark, corner mark, size}: the long ones ("DISCARD ALL", "SKIP ALL")
|
||||
wrap across the tilted oval and carry a short mark for the corners, exactly as a
|
||||
real deck does. Sized by a *font class*, never a box — `--uno-h`/`--uno-w` remain
|
||||
the only thing that decides how big a card is, which is the rule the phone bug
|
||||
taught this table.
|
||||
- **The wild draws glow** (the user asked, and it turned out to be load-bearing).
|
||||
A rainbow aura behind the card plus a slow shine across it, on any wild that
|
||||
makes somebody draw. It is the one thing that tells the *wild* +4 apart from No
|
||||
Mercy's **coloured** +4 sitting next to it in the same hand. It is a sibling
|
||||
behind the card, not a shadow on it, because a box-shadow there would fight the
|
||||
one that says which colour a played wild was named as.
|
||||
- **The stack says what the bill is, in three places**: a red pill on the felt, the
|
||||
turn line ("+6 — stack it, or take it"), and the button itself ("Take 6"). Under
|
||||
a stack the hand only lights the cards that answer it, the deck is dead (you
|
||||
cannot draw your way out of a bill), and the hint line changes to say so.
|
||||
- **A buried seat is not an empty one.** This is the whole trap of the mercy rule
|
||||
in the view layer: a seat killed at twenty-five holds zero cards, and so does a
|
||||
seat that just went out and *won*. The view asks the engine (`State.Live`) rather
|
||||
than inferring from a count of zero, so the winner is never the corpse.
|
||||
`TestABuriedSeatIsNotTheWinner` is that, and it also covers the ending only this
|
||||
deck has — you can win by **outliving** the table, with a hand still in front of
|
||||
you ("Last one standing!"), and you can lose without anybody going out at all.
|
||||
- **Two bugs, and a browser found both.**
|
||||
1. **The bill was writing into the chip bar.** The felt's pending pill was
|
||||
`[data-pending]` — and so is the chip bar's "chips still in flight from a
|
||||
buy-in" readout, and the chip bar lives *inside* `[data-uno]` and comes first
|
||||
in the document. So `root.querySelector` found the wrong one: a stack quietly
|
||||
overwrote the escrow message and never appeared on the felt at all. It is
|
||||
`[data-bill]` now. **A table's own attributes are not a private namespace —
|
||||
the chip bar is in every one of these roots.**
|
||||
2. **Hold'em let you click a button that did nothing** (found while re-driving
|
||||
poker, below). `send()` drops a click that arrives while a move is in flight,
|
||||
which is right — the board on screen during a script is a board the server has
|
||||
already moved past — but the *between-hands* buttons (Deal, Leave, Top up)
|
||||
stayed enabled through the whole deal animation. Clicking Leave while the
|
||||
cards were still flying did nothing: no move, no message, no reason. The lock
|
||||
lives on the buttons now (`lock()`), not only in the `busy` variable.
|
||||
- **Driven in a browser, 2026-07-14.** A Full House game stacked a +6 onto us and
|
||||
the felt said so; taking it worked; a bot went to twenty-five and the seat went
|
||||
grey and said "Buried"; a Duel was won by outliving the table and paid 1,950 on a
|
||||
1,000 stake (2.0× is 1,000 of winnings, less the 5% rake — the quote and the
|
||||
payout agreed, as `Pays()` requires). A 21-card hand at 390px wraps to five rows
|
||||
with every card painted and no sideways scroll. The normal deck still plays and
|
||||
still pays (a Table win on 1,200 paid 3,822). Console silent.
|
||||
|
||||
- **Hold'em, re-driven on the 20M-hand policy.** *(2026-07-14, and it closes Phase
|
||||
4.)* The policy is a data file, so the check was whether the bots still play a real
|
||||
game off it and the money still conserves: sat down for 100 at the 1/2 table, six
|
||||
hands (won a pot with a straight, lost one at showdown, folded the rest), got up
|
||||
with 61, and 4,900 + 61 came back as 4,961 — conserved to the chip. The re-drive is
|
||||
also what turned up the dead-button bug above, which is the argument for the rule:
|
||||
**re-drive after a policy change even though "only data" moved.**
|
||||
|
||||
- **Hold'em, and it is a cash game.** *(2026-07-14. Built, tested, and driven in a
|
||||
browser. The bots had to be retrained from scratch — see below, it is the whole
|
||||
story of this phase.)*
|
||||
@@ -479,48 +568,16 @@ A multi-session build. This section is the handover; read it before anything els
|
||||
size, with chips scaled to match. The bet total hangs *below* the ring
|
||||
(`.pete-spot-total`), which is the existing rule for exactly this reason.
|
||||
|
||||
### Pick this up here — a 20M-hand policy is still training
|
||||
|
||||
The `policy.gob` on main is a **300,000-hand run** — a placeholder. It works (95%
|
||||
heads-up hit rate, and the bots play a real game off it), but it is thin: 4,129
|
||||
nodes. A **20,000,000-hand run** was started on millenia on 2026-07-14 and needs
|
||||
collecting:
|
||||
|
||||
```sh
|
||||
ssh reala@192.168.1.212 'tail -2 ~/pete-train/train.log' # is it done?
|
||||
scp reala@192.168.1.212:~/pete-train/policy-new.gob internal/games/holdem/policy.gob
|
||||
go test ./internal/games/holdem/ -run TestTheBotsAreActuallyTrained -v # hit rate must hold ≥60%
|
||||
```
|
||||
|
||||
Then re-drive the table in a browser and commit it. If the run was lost, just do it
|
||||
again — it is one command and about an hour:
|
||||
|
||||
```sh
|
||||
rsync -az --delete --exclude .git --exclude node_modules ./ reala@192.168.1.212:~/pete-train/
|
||||
ssh reala@192.168.1.212 'cd ~/pete-train && go build ./cmd/holdem-train && \
|
||||
nohup ./holdem-train -iterations 20000000 -workers 30 -out ~/pete-train/policy-new.gob \
|
||||
> ~/pete-train/train.log 2>&1 &'
|
||||
```
|
||||
|
||||
millenia (`reala@192.168.1.212`) has 32 cores and does ~250k hands a minute. The
|
||||
local box does ~110k. Nothing about the *code* is waiting on this — the policy is a
|
||||
data file, and a better one only makes the bots harder.
|
||||
|
||||
### Next, in order
|
||||
|
||||
1. **Deploy.** Hangman, solitaire, trivia, UNO and hold'em are all played and all
|
||||
five are sitting on main un-deployed — the live casino is blackjack and nothing
|
||||
else. The server runs `StartTriviaBank`, so trivia's bank fills itself once the
|
||||
binary is out there, but the first player to try a ladder in the first minute
|
||||
after a deploy gets the 503.
|
||||
2. **No Mercy UNO.** The plan's header line has always promised "UNO (normal +
|
||||
no-mercy)" and only normal was ever built. gogobee has the rules
|
||||
(`uno_nomercy.go`: a 168-card deck, draw-stacking, elimination at 25 cards,
|
||||
sudden-death point scoring). It is a *rules dial orthogonal to the table-size
|
||||
tier*, so the lobby becomes 3 sizes × 2 rule sets — and **the multiples have to be
|
||||
re-measured**, because the current 2.2×/2.9×/3.6× are priced off a measured
|
||||
go-out-first rate (43/32/27%) that draw-stacking and mercy elimination change
|
||||
completely. Shipping No Mercy on the regular tier's prices would misprice it.
|
||||
1. **Deploy.** Hangman, solitaire, trivia, UNO (both rule sets) and hold'em are all
|
||||
played and all of them are sitting on main un-deployed — the live casino is
|
||||
blackjack and nothing else. The server runs `StartTriviaBank`, so trivia's bank
|
||||
fills itself once the binary is out there, but the first player to try a ladder in
|
||||
the first minute after a deploy gets the 503.
|
||||
2. **Nothing else is queued.** Every game in the header line is built. What's left is
|
||||
the open list at the bottom of this section: hold'em has no multiway policy, and
|
||||
blackjack still has no split.
|
||||
|
||||
Still open on hold'em, none of it blocking: the policy is **heads-up**, so a
|
||||
six-handed table is an approximation of it (the hit rate falls from 95% to about 17%
|
||||
|
||||
Reference in New Issue
Block a user