diff options
Diffstat (limited to 'internal/casino/house_edge_blackjack.go')
| -rw-r--r-- | internal/casino/house_edge_blackjack.go | 182 |
1 files changed, 182 insertions, 0 deletions
diff --git a/internal/casino/house_edge_blackjack.go b/internal/casino/house_edge_blackjack.go new file mode 100644 index 0000000..884cf75 --- /dev/null +++ b/internal/casino/house_edge_blackjack.go @@ -0,0 +1,182 @@ +package casino + +import "math" + +// A small infinite-shoe decision model supplies the effect of payout, H17, +// DAS and the game's unusual doubled-split-21 rule. Deck count and resplitting +// use the published basic-strategy rule adjustments at: +// https://wizardofodds.com/games/blackjack/rule-variations/ +// This is a reference estimate, not a finite-shoe/composition strategy solver. +func blackjackMainEdge(r BlackjackConfig) float64 { + r = r.Normalize() + base := BlackjackConfig{}.Normalize() + no := false + base.DoubleSplitBlackjack = &no + loss := .0043 - (blackjackReferenceEV(r) - blackjackReferenceEV(base)) + // Improvements relative to eight decks; interpolate uncommon deck counts. + decks := []float64{0, .0048, .0019, .00125, .0006, .0003, .0002, .0001, 0} + if r.Decks < len(decks) { + loss -= decks[r.Decks] + } else { + loss += .0006 * (1 - 8/float64(r.Decks)) + } + if r.MaxSplitHands == 2 { + loss += .001 + } else if r.MaxSplitHands == 3 { + loss += .0001 + } + if r.MaxSplitHands > 1 && r.AllowsResplitAces() { + loss -= .0008 + } + return loss +} + +// The split model allows one split; published adjustments above account for +// the configured resplit limit. Unlike-ten splitting does not change normal +// optimal play (tens are stood on). Shuffle penetration is not an off-top rule. +func blackjackReferenceEV(r BlackjackConfig) float64 { + probability := func(v int) float64 { + if v == 10 { + return 4.0 / 13 + } + return 1.0 / 13 + } + add := func(total, soft, v int) (int, int) { + if v == 1 { + total += 11 + soft++ + } else { + total += v + } + for total > 21 && soft > 0 { + total -= 10 + soft-- + } + return total, soft + } + result := 0.0 + for up := 1; up <= 10; up++ { + // Outcomes 17..21 and bust (22), conditional on no dealer natural. + type state struct{ total, soft int } + memoDealer := map[state][23]float64{} + var dealer func(int, int) [23]float64 + dealer = func(total, soft int) [23]float64 { + key := state{total, soft} + if v, ok := memoDealer[key]; ok { + return v + } + var out [23]float64 + if total > 21 { + out[22] = 1 + } else if total >= 17 && !(total == 17 && soft > 0 && r.DealerHitsSoft17()) { + out[total] = 1 + } else { + for v := 1; v <= 10; v++ { + t, s := add(total, soft, v) + child := dealer(t, s) + for i, p := range child { + out[i] += probability(v) * p + } + } + } + memoDealer[key] = out + return out + } + natural := 0.0 + if up == 1 { + natural = probability(10) + } + if up == 10 { + natural = probability(1) + } + var outcomes [23]float64 + for hole := 1; hole <= 10; hole++ { + if up+hole == 11 && (up == 1 || hole == 1) { + continue + } + t, s := add(0, 0, up) + t, s = add(t, s, hole) + dist := dealer(t, s) + for i, p := range dist { + outcomes[i] += probability(hole) * p / (1 - natural) + } + } + stand := func(total int) float64 { + if total > 21 { + return -1 + } + ev := outcomes[22] + for d := 17; d <= 21; d++ { + if total > d { + ev += outcomes[d] + } else if total < d { + ev -= outcomes[d] + } + } + return ev + } + memoPlayer := map[state]float64{} + var play func(int, int) float64 + play = func(total, soft int) float64 { + if total >= 21 { + return stand(total) + } + key := state{total, soft} + if v, ok := memoPlayer[key]; ok { + return v + } + hit := 0.0 + for v := 1; v <= 10; v++ { + t, s := add(total, soft, v) + hit += probability(v) * play(t, s) + } + ev := math.Max(stand(total), hit) + memoPlayer[key] = ev + return ev + } + hand := func(a, b int, split bool) float64 { + total, soft := add(0, 0, a) + total, soft = add(total, soft, b) + if split && a == 1 { + return stand(total) + } // one card on split aces + ev := play(total, soft) + if total < 21 && (!split || r.AllowsDoubleAfterSplit()) { + doubled := 0.0 + for v := 1; v <= 10; v++ { + t, _ := add(total, soft, v) + value := stand(t) + if split && t == 21 && r.CountsDoubleSplitBlackjack() { + value = r.BlackjackPayout + } + doubled += 2 * probability(v) * value + } + ev = math.Max(ev, doubled) + } + if !split && r.AllowsSurrender() { + ev = math.Max(ev, -.5) + } + return ev + } + for a := 1; a <= 10; a++ { + for b := 1; b <= 10; b++ { + ev := 0.0 + if a+b == 11 && (a == 1 || b == 1) { + ev = (1 - natural) * r.BlackjackPayout + } else { + ev = hand(a, b, false) + if a == b && r.MaxSplitHands > 1 { + split := 0.0 + for v := 1; v <= 10; v++ { + split += 2 * probability(v) * hand(a, v, true) + } + ev = math.Max(ev, split) + } + ev = (1-natural)*ev - natural + } + result += probability(up) * probability(a) * probability(b) * ev + } + } + } + return result +} |
