games: the rake you pay, and the rake the table lifts

They are different numbers and the felt was quoting the wrong one. Every raked
pot has chips lifted off it, whoever wins — that has to stay true or the table
stops balancing. But the bots' chips are not real, so a pot a bot wins costs
you nothing, and the counter under your stack was climbing anyway while you sat
there folding.

Rake is now every chip off the table (so the chips conserve) and Paid is the
part that came out of a pot you won, which is the only part that is money and
the only part worth telling you about. A chop costs you half of it. The audit
log takes Paid too: the house's income is what it made off the player, not what
it lifted off a bot.

Claude-Session: https://claude.ai/code/session_013M5nD7PgUboJXoDcYHzpuJ
This commit is contained in:
prosolis
2026-07-14 09:11:14 -07:00
parent e6c1bd3b54
commit 903c5accdb
4 changed files with 78 additions and 16 deletions

View File

@@ -131,19 +131,6 @@ func (s *State) payPot(pot Pot, live []ranked, evs *[]Event) {
return return
} }
amount := pot.Amount
if s.Flopped {
rake := int64(math.Floor(float64(amount) * s.Tier.RakePct))
if cap := s.Tier.BB * rakeCapBB; rake > cap {
rake = cap
}
if rake > 0 {
amount -= rake
s.Rake += rake
*evs = append(*evs, Event{Kind: "rake", Seat: -1, Amount: rake})
}
}
eligible := make(map[int]bool, len(pot.Eligible)) eligible := make(map[int]bool, len(pot.Eligible))
for _, seat := range pot.Eligible { for _, seat := range pot.Eligible {
eligible[seat] = true eligible[seat] = true
@@ -165,6 +152,30 @@ func (s *State) payPot(pot Pot, live []ranked, evs *[]Event) {
return return
} }
amount := pot.Amount
if s.Flopped {
rake := int64(math.Floor(float64(amount) * s.Tier.RakePct))
if cap := s.Tier.BB * rakeCapBB; rake > cap {
rake = cap
}
if rake > 0 {
amount -= rake
s.Rake += rake // every chip of it, so the table still balances
// But only the part that came out of *your* winnings is money the house
// actually made, and it is the only part the felt should quote you. The
// bots' chips are not real — the only real money at this table is yours —
// so raking a pot a bot won costs you nothing, and a counter that climbed
// while you folded would be telling you it had.
for _, w := range winners {
if w.seat == You {
s.Paid += rake / int64(len(winners))
}
}
*evs = append(*evs, Event{Kind: "rake", Seat: -1, Amount: rake})
}
}
share := amount / int64(len(winners)) share := amount / int64(len(winners))
odd := amount % int64(len(winners)) // the odd chip goes to the first seat left of the button odd := amount % int64(len(winners)) // the odd chip goes to the first seat left of the button
for i, w := range winners { for i, w := range winners {

View File

@@ -204,7 +204,19 @@ type State struct {
// table; Payout is the stack that goes home, and is only set once you're up. // table; Payout is the stack that goes home, and is only set once you're up.
BoughtIn int64 `json:"bought_in"` BoughtIn int64 `json:"bought_in"`
Payout int64 `json:"payout"` Payout int64 `json:"payout"`
Rake int64 `json:"rake"` // taken by the house across the whole session
// Two rakes, and they are different numbers.
//
// Rake is every chip the house has lifted off this table. It exists so the
// chips balance: a pot that is raked is a pot that pays out less than it holds,
// and the difference has to be somewhere.
//
// Paid is the part of that which came out of a pot *you* won — the only part
// that is real money, and the only part worth quoting you. Rake a pot a bot
// wins and you have paid nothing; a counter that climbed anyway while you sat
// folding would be lying to you.
Rake int64 `json:"rake"`
Paid int64 `json:"paid"`
// The seed rides in the state for the same reason it does in UNO: the bots // The seed rides in the state for the same reason it does in UNO: the bots
// choose and the deck is reshuffled every hand, so the engine needs randomness // choose and the deck is reshuffled every hand, so the engine needs randomness

View File

@@ -356,6 +356,43 @@ func TestTheRakeSurvivesTheConstructor(t *testing.T) {
} }
} }
// The house only makes money off you. A pot a bot wins is raked — that is what a
// pot is — but the chips it comes out of are not real, so it has cost you nothing
// and the number the felt quotes you must not move.
func TestYouOnlyPayRakeOnPotsYouWin(t *testing.T) {
s := State{Tier: Tiers[1], Flopped: true,
Seats: []Seat{{Name: "You"}, {Name: "Dice", Bot: true}}}
var evs []Event
// A bot takes it.
s.payPot(Pot{Amount: 400, Eligible: []int{1}}, []ranked{{seat: 1}}, &evs)
if s.Paid != 0 {
t.Errorf("you paid %d in rake on a pot a bot won", s.Paid)
}
if s.Rake != 20 {
t.Errorf("the table lifted %d off that pot, want 20 — the chips have to balance "+
"whoever won it", s.Rake)
}
// Now you take one.
s.payPot(Pot{Amount: 400, Eligible: []int{You}}, []ranked{{seat: You}}, &evs)
if s.Paid != 20 {
t.Errorf("you paid %d in rake on a 400 pot you won, want 20", s.Paid)
}
if s.Rake != 40 {
t.Errorf("the table has lifted %d in total, want 40", s.Rake)
}
// And a chop costs you half of it.
s.Paid, s.Rake = 0, 0
s.payPot(Pot{Amount: 400, Eligible: []int{0, 1}},
[]ranked{{seat: 0, rank: 9}, {seat: 1, rank: 9}}, &evs)
if s.Paid != 10 {
t.Errorf("you paid %d in rake on a chopped pot, want 10 — half the rake, "+
"because you won half the pot", s.Paid)
}
}
func TestASplitPotSplits(t *testing.T) { func TestASplitPotSplits(t *testing.T) {
s := State{Tier: Tiers[1], Seats: []Seat{{Name: "You"}, {Name: "A", Bot: true}}} s := State{Tier: Tiers[1], Seats: []Seat{{Name: "You"}, {Name: "A", Bot: true}}}
var evs []Event var evs []Event

View File

@@ -92,7 +92,7 @@ func viewHoldem(g holdem.State) holdemView {
Phase: string(g.Phase), Phase: string(g.Phase),
Stack: g.Seats[holdem.You].Stack, Stack: g.Seats[holdem.You].Stack,
BoughtIn: g.BoughtIn, BoughtIn: g.BoughtIn,
Rake: g.Rake, Rake: g.Paid, // the part you actually paid, not the part the table lifted
Payout: g.Payout, Payout: g.Payout,
} }
for _, p := range g.Side { for _, p := range g.Side {
@@ -329,7 +329,9 @@ func (s *Server) persistHoldem(w http.ResponseWriter, user string, g holdem.Stat
v, ok := s.commit(w, user, finished{ v, ok := s.commit(w, user, finished{
Game: gameHoldem, Blob: blob, Game: gameHoldem, Blob: blob,
Bet: g.BoughtIn, Payout: g.Payout, Rake: g.Rake, // Paid, not Rake: the audit log is the house's income, and the house only
// makes money off the player. What it lifts off a bot's pot is not income.
Bet: g.BoughtIn, Payout: g.Payout, Rake: g.Paid,
Outcome: outcome, Done: done, Outcome: outcome, Done: done,
Seed1: seed1, Seed2: seed2, Fresh: fresh, Seed1: seed1, Seed2: seed2, Fresh: fresh,
}) })