aboutsummaryrefslogtreecommitdiff
path: root/internal/casino/pai_gow_rules.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/casino/pai_gow_rules.go')
-rw-r--r--internal/casino/pai_gow_rules.go96
1 files changed, 72 insertions, 24 deletions
diff --git a/internal/casino/pai_gow_rules.go b/internal/casino/pai_gow_rules.go
index 8a9842c..ced7c47 100644
--- a/internal/casino/pai_gow_rules.go
+++ b/internal/casino/pai_gow_rules.go
@@ -22,8 +22,14 @@ type PaiGowConfig struct {
Envy bool `yaml:"envy,omitempty"`
PushAceHigh string `yaml:"push_ace_high,omitempty"` // Face Up only: 5_15_40 or 5_12_40
ActionTicks int `yaml:"action_ticks,omitempty"`
+ // A2345SecondHighest makes the wheel the second-highest straight, below
+ // Broadway and above a six-high straight. It defaults to true; only an
+ // explicit false demotes A2345 to the lowest straight.
+ A2345SecondHighest *bool `yaml:"a2345_second_highest,omitempty"`
}
+func (c PaiGowConfig) A2345High() bool { return ruleBool(c.A2345SecondHighest, true) }
+
func (c PaiGowConfig) Normalize() PaiGowConfig {
if c.Variant == "" {
c.Variant = PaiGowClassic
@@ -85,7 +91,11 @@ func (c PaiGowConfig) Label() string {
}
func paiGowRules(c TableConfig) string {
r := c.PaiGow.Normalize()
- s := r.Label() + "\nSeven cards from a fresh 53-card deck. Joker is an ace unless completing a straight, flush or straight flush. A2345 is the second-highest straight.\nSet two cards low and five high: 24 chooses low cards 2/4; 13456 chooses the high hand. High must strictly outrank low. Copies go to the dealer.\nBoth hands win: win; both lose: loss; one wins: push."
+ wheel := "A2345 is the second-highest straight."
+ if !r.A2345High() {
+ wheel = "A2345 is the lowest straight."
+ }
+ s := r.Label() + "\nSeven cards from a fresh 53-card deck. Joker is an ace unless completing a straight, flush or straight flush. " + wheel + "\nSet two cards low and five high: 24 chooses low cards 2/4; 13456 chooses the high hand. High must strictly outrank low. Copies go to the dealer.\nBoth hands win: win; both lose: loss; one wins: push."
switch r.Variant {
case PaiGowClassic:
s += " Wins pay 0.95:1 (profit rounded down, minimum one credit)."
@@ -138,7 +148,14 @@ func pgCompare(a, b pgRank) int {
}
return 0
}
-func pgNatural(cards []card) pgRank {
+// pgNatural keeps the default-rules entry point used by tests and callers that
+// do not thread a table configuration. Production evaluation goes through
+// pgNaturalRules.
+//
+//nolint:unused // default-rules entry point
+func pgNatural(cards []card) pgRank { return pgNaturalRules(cards, true) }
+
+func pgNaturalRules(cards []card, a2345High bool) pgRank {
counts := [15]int{}
flush := len(cards) == 5
for _, c := range cards {
@@ -162,7 +179,11 @@ func pgNatural(cards []card) pgRank {
straight = sorted[4]
}
if sorted[0] == 2 && sorted[1] == 3 && sorted[2] == 4 && sorted[3] == 5 && sorted[4] == 14 {
- straight = 15
+ if a2345High {
+ straight = 15
+ } else {
+ straight = 5
+ }
}
if straight == 14 {
straight = 16
@@ -196,7 +217,9 @@ func pgNatural(cards []card) pgRank {
copy(out[1:], ranks)
return out
}
-func pgEvaluate(cards []card) pgRank {
+func pgEvaluate(cards []card) pgRank { return pgEvaluateRules(cards, true) }
+
+func pgEvaluateRules(cards []card, a2345High bool) pgRank {
j := -1
for i, c := range cards {
if c.rank == "JK" {
@@ -205,11 +228,11 @@ func pgEvaluate(cards []card) pgRank {
}
}
if j < 0 {
- return pgNatural(cards)
+ return pgNaturalRules(cards, a2345High)
}
copyCards := append([]card(nil), cards...)
copyCards[j] = card{"A", "joker"}
- best := pgNatural(copyCards)
+ best := pgNaturalRules(copyCards, a2345High)
if len(cards) != 5 {
return best
}
@@ -226,7 +249,7 @@ func pgEvaluate(cards []card) pgRank {
continue
}
copyCards[j] = replacement
- v := pgNatural(copyCards)
+ v := pgNaturalRules(copyCards, a2345High)
if (v[0] == 4 || v[0] == 5 || v[0] == 8) && pgCompare(v, best) > 0 {
best = v
}
@@ -252,28 +275,32 @@ func pgSet(cards []card, digits []int) pgSplit {
}
return s
}
-func (s pgSplit) valid() bool {
- return len(s.high) == 5 && len(s.low) == 2 && pgCompare(pgEvaluate(s.high), pgEvaluate(s.low)) > 0
+func (s pgSplit) valid() bool { return s.validRules(true) }
+
+func (s pgSplit) validRules(a2345High bool) bool {
+ return len(s.high) == 5 && len(s.low) == 2 && pgCompare(pgEvaluateRules(s.high, a2345High), pgEvaluateRules(s.low, a2345High)) > 0
}
-func pgHouse(cards []card) pgSplit {
+func pgHouse(cards []card) pgSplit { return pgHouseRules(cards, true) }
+
+func pgHouseRules(cards []card, a2345High bool) pgSplit {
var options []pgSplit
best := pgRank{}
for i := 0; i < 7; i++ {
for j := i + 1; j < 7; j++ {
s := pgSet(cards, []int{i, j})
- if !s.valid() {
+ if !s.validRules(a2345High) {
continue
}
options = append(options, s)
- if pgCompare(pgEvaluate(s.high), best) > 0 {
- best = pgEvaluate(s.high)
+ if pgCompare(pgEvaluateRules(s.high, a2345High), best) > 0 {
+ best = pgEvaluateRules(s.high, a2345High)
}
}
}
var chosen pgSplit
var low, high pgRank
for _, s := range options {
- h, l := pgEvaluate(s.high), pgEvaluate(s.low)
+ h, l := pgEvaluateRules(s.high, a2345High), pgEvaluateRules(s.low, a2345High)
eligible := h[0] == best[0]
// Split two pair/full house only for a paired front with at least a pair behind.
if (best[0] == 2 || best[0] == 6) && l[0] == 1 && h[0] >= 1 {
@@ -340,7 +367,9 @@ func pgSevenFlush(cards []card) bool {
}
return false
}
-func pgFortune(cards []card, envy bool) (FortuneRow, bool) {
+func pgFortune(cards []card, envy bool) (FortuneRow, bool) { return pgFortuneRules(cards, envy, true) }
+
+func pgFortuneRules(cards []card, envy, a2345High bool) (FortuneRow, bool) {
rows := FortunePayTable(envy)
joker := false
counts := [15]int{}
@@ -361,7 +390,7 @@ func pgFortune(cards []card, envy bool) (FortuneRow, bool) {
for i := 0; i < 7; i++ {
for j := i + 1; j < 7; j++ {
s := pgSet(cards, []int{i, j})
- v := pgEvaluate(s.high)
+ v := pgEvaluateRules(s.high, a2345High)
index := len(rows)
switch v[0] {
case 9:
@@ -408,11 +437,18 @@ func pgFortune(cards []card, envy bool) (FortuneRow, bool) {
}
return rows[bestIndex], true
}
-func pgBest(cards []card) pgRank {
+// pgBest keeps the default-rules entry point used by tests and callers that do
+// not thread a table configuration. Production evaluation goes through
+// pgBestRules.
+//
+//nolint:unused // default-rules entry point
+func pgBest(cards []card) pgRank { return pgBestRules(cards, true) }
+
+func pgBestRules(cards []card, a2345High bool) pgRank {
var best pgRank
for i := 0; i < 7; i++ {
for j := i + 1; j < 7; j++ {
- v := pgEvaluate(pgSet(cards, []int{i, j}).high)
+ v := pgEvaluateRules(pgSet(cards, []int{i, j}).high, a2345High)
if pgCompare(v, best) > 0 {
best = v
}
@@ -424,10 +460,14 @@ func pgBest(cards []card) pgRank {
// Automatic pushes use the best of all seven dealer cards, not just the
// chosen high hand. Pair/straight/flush hands headed by an ace do not qualify.
func pgDealerPush(cards []card, variant string) string {
+ return pgDealerPushRules(cards, variant, true)
+}
+
+func pgDealerPushRules(cards []card, variant string, a2345High bool) string {
if variant != PaiGowEZ && variant != PaiGowFaceUp {
return ""
}
- v := pgBest(cards)
+ v := pgBestRules(cards, a2345High)
if v[0] == 0 {
if variant == PaiGowEZ && v[1] == 12 {
return "push (dealer queen-high)"
@@ -439,11 +479,15 @@ func pgDealerPush(cards []card, variant string) string {
return ""
}
func pgMainResult(player, dealer pgSplit, dealerCards []card, variant string, tie [2]card) string {
- if push := pgDealerPush(dealerCards, variant); push != "" {
+ return pgMainResultRules(player, dealer, dealerCards, variant, tie, true)
+}
+
+func pgMainResultRules(player, dealer pgSplit, dealerCards []card, variant string, tie [2]card, a2345High bool) string {
+ if push := pgDealerPushRules(dealerCards, variant, a2345High); push != "" {
return push
}
- high := pgCompare(pgEvaluate(player.high), pgEvaluate(dealer.high)) > 0
- low := pgCompare(pgEvaluate(player.low), pgEvaluate(dealer.low)) > 0
+ high := pgCompare(pgEvaluateRules(player.high, a2345High), pgEvaluateRules(dealer.high, a2345High)) > 0
+ low := pgCompare(pgEvaluateRules(player.low, a2345High), pgEvaluateRules(dealer.low, a2345High)) > 0
if high && low {
return "win"
}
@@ -475,7 +519,11 @@ type pgPushAce struct {
}
func pgPushAceClass(dealer, player []card, rates [3]int) pgPushAce {
- if pgBest(dealer)[0] != 0 || pgBest(dealer)[1] != 14 {
+ return pgPushAceClassRules(dealer, player, rates, true)
+}
+
+func pgPushAceClassRules(dealer, player []card, rates [3]int, a2345High bool) pgPushAce {
+ if pgBestRules(dealer, a2345High)[0] != 0 || pgBestRules(dealer, a2345High)[1] != 14 {
return pgPushAce{klass: "none"}
}
for _, c := range dealer {
@@ -483,7 +531,7 @@ func pgPushAceClass(dealer, player []card, rates [3]int) pgPushAce {
return pgPushAce{klass: "joker", rate: rates[1], desc: "dealer ace-high with Joker"}
}
}
- if pgBest(player)[0] == 0 && pgBest(player)[1] == 14 {
+ if pgBestRules(player, a2345High)[0] == 0 && pgBestRules(player, a2345High)[1] == 14 {
return pgPushAce{klass: "both", rate: rates[2], desc: "both ace-high"}
}
return pgPushAce{klass: "plain", rate: rates[0], desc: "dealer ace-high without Joker"}