games: no mercy, and the multiples nobody re-measured

No Mercy UNO as a rules dial on the existing tier, not a fourth table: 168 cards,
draw-until-playable, draw-stacking, and the twenty-five card mercy kill. Six
tiers now; a normal game never runs a line of the new code.

The engine is the whole of it so far — the felt hasn't been touched, so there is
no way to play this in a browser yet.

Two things worth knowing.

The normal tiers were mispriced, and had been for a while. They were set against
a naive win rate of 43/32/27%; it now measures 40.3/29.2/23.3%. The bots got
better at some point after the multiples were written down and nobody re-ran the
measurement — which the plan explicitly warns about, because the bots and the
tiers are a pair. Table and Full House had been charging an 18–19% house edge
instead of the 8% they were meant to. All six tiers are repriced off a fresh
measurement, and TestTheMultiplesAreStillPriced now fails the build if they
drift again. It is the test the normal tiers never had, which is how they drifted.

And No Mercy is *easier* than UNO, at every table size, so it pays less. The
mercy rule does not 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 turns out to be merciless mostly to the table.

The rake test used to assert a payout of 214, which was the 2.2x duel written
down as a number. It failed on a rake that was entirely correct. It derives the
arithmetic from the tier now: the rule is that the house takes its cut of the
profit and never touches the stake, and that holds at any multiple.

Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
prosolis
2026-07-14 10:07:55 -07:00
parent 4bc38859d4
commit aca523e511
5 changed files with 1092 additions and 61 deletions

View File

@@ -39,8 +39,11 @@ func census(s State) map[Card]int {
}
for _, c := range s.Discard {
// A wild is stamped with the colour it was played as while it sits on the
// pile, so it counts as the wild it really is.
if c.Value == WildCard || c.Value == WildDrawFour {
// pile, so it counts as the wild it really is. This asks the face, rather
// than listing the wilds: No Mercy prints four more of them, and a census
// that didn't know about them would count a played roulette as a red card
// and report a deck that balances while the cards don't.
if c.Value.Wild() {
c.Color = Wild
}
m[c]++
@@ -478,22 +481,37 @@ func TestQuoteIsThePayout(t *testing.T) {
}
// The rake comes out of the winnings, never the stake.
//
// The arithmetic is derived from the tier rather than written down. It used to be
// written down — payout 214, rake 6 — and those numbers were the 2.2× duel. When
// the tiers were re-measured and repriced, this test failed on a rake that was
// perfectly correct, which is a test asserting a *price* while claiming to assert
// a *rule*. The rule is: the house takes its cut of the profit and never touches
// the stake. That holds at any multiple.
func TestRakeIsOnWinningsOnly(t *testing.T) {
s := rig([][]Card{{{Red, Three}}, {{Green, Five}, {Green, Six}}}, Card{Red, Nine}, Red)
s.Tier = duel() // 2.2x on 100: 220 back, 120 of it profit, 6 of that to the house
s.Tier = duel()
s.Bet = 100
gross := int64(float64(s.Bet) * s.Tier.Base) // what the tier pays back, before the house
profit := gross - s.Bet
wantRake := int64(float64(profit) * rake)
wantPayout := s.Bet + profit - wantRake
next, _, err := ApplyMove(s, Move{Kind: MovePlay, Index: 0})
if err != nil {
t.Fatalf("go out: %v", err)
}
if next.Payout != 214 {
t.Errorf("payout %d, want 214 (100 stake + 120 winnings - 6 rake)", next.Payout)
if next.Payout != wantPayout {
t.Errorf("payout %d, want %d (%d stake + %d winnings - %d rake)",
next.Payout, wantPayout, s.Bet, profit, wantRake)
}
if next.Rake != 6 {
t.Errorf("rake %d, want 6", next.Rake)
if next.Rake != wantRake {
t.Errorf("rake %d, want %d — and never a penny of the %d stake",
next.Rake, wantRake, s.Bet)
}
if next.Net() != 114 {
t.Errorf("net %d, want 114", next.Net())
if next.Net() != wantPayout-s.Bet {
t.Errorf("net %d, want %d", next.Net(), wantPayout-s.Bet)
}
}