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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user