diff options
| author | historia <[not public]> | 2026-10-02 01:54:37 -0400 |
|---|---|---|
| committer | historia <[not public]> | 2026-10-02 01:54:37 -0400 |
| commit | d3a305d1d7962f82af2397760cf8008758bddd1d (patch) | |
| tree | f91f0deb342c20f2993e5366f772c3432648f426 /internal/casino/house_edge.go | |
| parent | 8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0 (diff) | |
| download | thehouseoficarus-d3a305d1d7962f82af2397760cf8008758bddd1d.tar.gz | |
feat: better 'list' bet table, multiple bets in baccarat
Diffstat (limited to 'internal/casino/house_edge.go')
| -rw-r--r-- | internal/casino/house_edge.go | 138 |
1 files changed, 138 insertions, 0 deletions
diff --git a/internal/casino/house_edge.go b/internal/casino/house_edge.go new file mode 100644 index 0000000..01fe7aa --- /dev/null +++ b/internal/casino/house_edge.go @@ -0,0 +1,138 @@ +package casino + +import ( + "fmt" + "math" + "strings" +) + +// HouseEdge is expected net loss / initial stake (not loss per roll, nor +// probability of losing). Parts keep grouped targets with different edges +// separate. Unknown analyses must never silently become a zero-percent edge. +// Values are unrounded fractions; only the presentation rounds percentages. +type HouseEdge struct { + Parts []EdgePart + Note string +} + +type EdgePart struct { + Label string + Loss float64 +} + +func edge(loss float64) HouseEdge { return HouseEdge{Parts: []EdgePart{{Loss: loss}}} } +func unknownEdge(reason string) HouseEdge { return HouseEdge{Note: "not verified: " + reason} } + +func (e HouseEdge) Text(precision int) string { + if precision != 2 { + precision = 0 + } + var parts []string + for _, p := range e.Parts { + // Avoid displaying negative zero while preserving genuine player edges. + percent := math.Round(p.Loss*100*math.Pow10(precision)) / math.Pow10(precision) + if percent == 0 { + percent = 0 + } + value := fmt.Sprintf("%.*f%%", precision, percent) + parts = append(parts, value) + } + if len(parts) == 0 { + return "" + } + return "House edge: " + strings.Join(parts, " / ") +} + +// HouseEdge dispatches through the game's registration. A new game must +// supply its own analysis; it never inherits another game's percentage. +// This is an off-the-top, pre-credit-rounding reference, not a card-counting +// aid or a prediction of the next outcome. Strategy assumptions and sources +// are documented alongside each game's calculation; Note is diagnostic only. +func (c TableConfig) HouseEdge(bet string) HouseEdge { + c = c.Normalize() + if def, ok := tableGameDefFor(c.Game); ok && def.houseEdge != nil { + return def.houseEdge(c, bet) + } + return unknownEdge("this game's rules have no house-edge analysis") +} + +// HouseEdge reads seat-specific settings under the game's lock. It does not +// alter game state, advance RNGs, or reveal cards from the current shoe. +func (t *Table) HouseEdge(name, bet string) HouseEdge { + if vp, ok := t.game.(*videoPokerTable); ok { + vp.mu.Lock() + defer vp.mu.Unlock() + if p := vp.players[name]; p != nil { + return pokerHouseEdge(p.config, bet, p.loaded, p.featureOn, p.hands) + } + } + if pg, ok := t.game.(*paiGowTable); ok && bet == "fortune" && t.Config.PaiGow.Envy { + pg.mu.Lock() + defer pg.mu.Unlock() + others, stake := 0, max(1, t.Config.MinBet) + for _, p := range pg.players { + if p.name == name { + if p.fortune.Total > 0 { + stake = p.fortune.Total + } + } else if p.main.Total > 0 { + others++ + } + } + e := t.Config.HouseEdge(bet) + e.Parts[0].Loss -= fortuneEnvyReturn(others, stake) + return e + } + return t.Config.HouseEdge(bet) +} + +func rouletteHouseEdge(c TableConfig, bet string) HouseEdge { + pockets := 37.0 + if c.Roulette.DoubleZero { + pockets = 38 + } + // Recognize the catalogue explicitly: a new target must not accidentally + // inherit the standard edge (the American basket is a counterexample). + for _, row := range (&rouletteTable{tableCore: newTableCore[*roulettePlayer](&tableBase{Config: c})}).wagerRows() { + if row.Bet == bet { + if bet == "basket" && c.Roulette.DoubleZero { + return edge(3 / pockets) + } + return edge((pockets - 36) / pockets) + } + } + return unknownEdge("unrecognized roulette target") +} + +func blackjackHouseEdge(c TableConfig, bet string) HouseEdge { + if bet == "insurance" { + // Given a dealer ace from a fresh shoe; NOT conditional on the player's + // visible cards or a running count. Remaining tens = 16 per deck. + b := c.Blackjack.Normalize() + e := edge(1 - float64(16*b.Decks)/float64(52*b.Decks-1)*(1+b.InsurancePayout)) + e.Note = "fresh shoe, dealer ace only; visible cards change this" + return e + } + switch bet { + case "main": + return edge(blackjackMainEdge(c.Blackjack)) + case "hit", "stand", "decline", "double", "split": + return HouseEdge{Note: "not a separate wager; decision EV depends on the hand"} + case "surrender": + return HouseEdge{Note: "returns half the original stake; not a new wager"} + } + return unknownEdge("unrecognized blackjack wager") +} + +func (c MachineConfig) HouseEdge() HouseEdge { + c = c.Normalize() + if c.Game != GameSlots { + return unknownEdge("unrecognized machine game") + } + if _, ok := slotVariants[c.Variant]; !ok { + return unknownEdge("unrecognized slot profile") + } + e := edge(1 - CalculateRTP(machineSlotProfile(c))) + e.Note = "includes free spins; before residual fractional-credit loss on leaving" + return e +} |
