aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/house_edge_blackjack.go
diff options
context:
space:
mode:
authorhistoria <[not public]>2026-10-02 01:54:37 -0400
committerhistoria <[not public]>2026-10-02 01:54:37 -0400
commitd3a305d1d7962f82af2397760cf8008758bddd1d (patch)
treef91f0deb342c20f2993e5366f772c3432648f426 /internal/casino/house_edge_blackjack.go
parent8eea0e4e06a4aaa6a18edabd9897d3bea1ea2ca0 (diff)
downloadthehouseoficarus-d3a305d1d7962f82af2397760cf8008758bddd1d.tar.gz
feat: better 'list' bet table, multiple bets in baccarat
Diffstat (limited to 'internal/casino/house_edge_blackjack.go')
-rw-r--r--internal/casino/house_edge_blackjack.go182
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
+}