aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/house_edge_baccarat.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/house_edge_baccarat.go')
-rw-r--r--internal/casino/house_edge_baccarat.go131
1 files changed, 131 insertions, 0 deletions
diff --git a/internal/casino/house_edge_baccarat.go b/internal/casino/house_edge_baccarat.go
new file mode 100644
index 0000000..1598fd6
--- /dev/null
+++ b/internal/casino/house_edge_baccarat.go
@@ -0,0 +1,131 @@
+package casino
+
+import (
+ "strings"
+ "sync"
+)
+
+type baccaratProbabilities struct {
+ player, banker, tie, dragon, panda, six float64
+}
+
+var baccaratEdgeCache sync.Map // deck count -> baccaratProbabilities
+
+func baccaratHouseEdge(cfg TableConfig, bet string) HouseEdge {
+ c := cfg.Baccarat.Normalize()
+ spot := strings.Fields(bet)
+ if len(spot) == 0 {
+ return unknownEdge("unrecognized baccarat target")
+ }
+ p := baccaratFreshProbabilities(c.Decks)
+ var e HouseEdge
+ switch spot[0] {
+ case "player":
+ e = edge(p.banker - p.player)
+ case "banker":
+ loss := p.player - c.BankerPayout*p.banker
+ if c.Variant == BaccaratSuper6 {
+ loss += .5 * p.six
+ }
+ if c.Variant == BaccaratEZ {
+ loss += p.dragon
+ }
+ e = edge(loss)
+ case "tie":
+ e = edge(1 - (1+c.TiePayout)*p.tie)
+ case "player_pair", "banker_pair":
+ e = edge(1 - 12*float64(4*c.Decks-1)/float64(52*c.Decks-1))
+ case "dragon_7":
+ e = edge(1 - 41*p.dragon)
+ case "panda_8":
+ e = edge(1 - 26*p.panda)
+ case "super_6":
+ e = edge(1 - 13*p.six)
+ default:
+ return unknownEdge("unrecognized baccarat target")
+ }
+ e.Note = "fresh shoe; before fractional-credit rounding"
+ return e
+}
+
+// Enumerate value sequences WITHOUT replacement from the configured shoe.
+// Tens/J/Q/K coalesce into value zero; rank-pair probability is separately
+// exact by symmetry. At most 10^6 branches, cached by deck count. Uses the
+// same banker tableau as playTableau; no sampled or generic casino rates.
+func baccaratFreshProbabilities(decks int) baccaratProbabilities {
+ if v, ok := baccaratEdgeCache.Load(decks); ok {
+ return v.(baccaratProbabilities)
+ }
+ var out baccaratProbabilities
+ counts := [10]int{}
+ for i := range counts {
+ counts[i] = 4 * decks
+ }
+ counts[0] = 16 * decks
+ var cards [6]int
+ var deal func(int, float64)
+ finish := func(pt, bt, pn, bn int, prob float64) {
+ switch {
+ case pt > bt:
+ out.player += prob
+ if pn == 3 && pt == 8 {
+ out.panda += prob
+ }
+ case bt > pt:
+ out.banker += prob
+ if bn == 3 && bt == 7 {
+ out.dragon += prob
+ }
+ if bt == 6 {
+ out.six += prob
+ }
+ default:
+ out.tie += prob
+ }
+ }
+ deal = func(n int, prob float64) {
+ if n >= 4 {
+ pt, bt := (cards[0]+cards[1])%10, (cards[2]+cards[3])%10
+ if pt >= 8 || bt >= 8 {
+ finish(pt, bt, 2, 2, prob)
+ return
+ }
+ playerDraw := pt <= 5
+ if playerDraw && n == 4 { // draw Player's third below
+ } else {
+ third, pn := -1, 2
+ if playerDraw {
+ third = cards[4]
+ pt = (pt + third) % 10
+ pn = 3
+ }
+ bankerDraw := (third < 0 && bt <= 5) || (third >= 0 && bankerDraws(bt, third))
+ if !bankerDraw {
+ finish(pt, bt, pn, 2, prob)
+ return
+ }
+ needed := 5
+ if playerDraw {
+ needed = 6
+ }
+ if n == needed {
+ finish(pt, (bt+cards[n-1])%10, pn, 3, prob)
+ return
+ }
+ }
+ }
+ remaining := float64(52*decks - n)
+ for value, count := range counts {
+ if count == 0 {
+ continue
+ }
+ counts[value]--
+ cards[n] = value
+ deal(n+1, prob*float64(count)/remaining)
+ counts[value]++
+ }
+ }
+ deal(0, 1)
+ baccaratEdgeCache.Store(decks, out)
+ return out
+}