diff --git a/internal/games/holdem/eval.go b/internal/games/holdem/eval.go index 3ef0e56..4d26360 100644 --- a/internal/games/holdem/eval.go +++ b/internal/games/holdem/eval.go @@ -131,19 +131,6 @@ func (s *State) payPot(pot Pot, live []ranked, evs *[]Event) { 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)) for _, seat := range pot.Eligible { eligible[seat] = true @@ -165,6 +152,30 @@ func (s *State) payPot(pot Pot, live []ranked, evs *[]Event) { 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)) odd := amount % int64(len(winners)) // the odd chip goes to the first seat left of the button for i, w := range winners { diff --git a/internal/games/holdem/holdem.go b/internal/games/holdem/holdem.go index 89dcb10..3b8b61e 100644 --- a/internal/games/holdem/holdem.go +++ b/internal/games/holdem/holdem.go @@ -204,7 +204,19 @@ type State struct { // table; Payout is the stack that goes home, and is only set once you're up. BoughtIn int64 `json:"bought_in"` 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 // choose and the deck is reshuffled every hand, so the engine needs randomness diff --git a/internal/games/holdem/holdem_test.go b/internal/games/holdem/holdem_test.go index cc984e4..0707567 100644 --- a/internal/games/holdem/holdem_test.go +++ b/internal/games/holdem/holdem_test.go @@ -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) { s := State{Tier: Tiers[1], Seats: []Seat{{Name: "You"}, {Name: "A", Bot: true}}} var evs []Event diff --git a/internal/web/games_holdem.go b/internal/web/games_holdem.go index fa28f6b..d60c31b 100644 --- a/internal/web/games_holdem.go +++ b/internal/web/games_holdem.go @@ -92,7 +92,7 @@ func viewHoldem(g holdem.State) holdemView { Phase: string(g.Phase), Stack: g.Seats[holdem.You].Stack, BoughtIn: g.BoughtIn, - Rake: g.Rake, + Rake: g.Paid, // the part you actually paid, not the part the table lifted Payout: g.Payout, } 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{ 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, Seed1: seed1, Seed2: seed2, Fresh: fresh, })