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 }