diff options
Diffstat (limited to 'internal/casino/video_poker_rules.go')
| -rw-r--r-- | internal/casino/video_poker_rules.go | 307 |
1 files changed, 307 insertions, 0 deletions
diff --git a/internal/casino/video_poker_rules.go b/internal/casino/video_poker_rules.go new file mode 100644 index 0000000..340815d --- /dev/null +++ b/internal/casino/video_poker_rules.go @@ -0,0 +1,307 @@ +package casino + +import ( + "strings" + "unicode" +) + +type pokerSchedule struct { + name string + pays map[string]int +} + +// pokerCategories is the fixed pay-table row order, top payout first. +var pokerCategories = []string{"Natural royal flush", "Royal flush", "Four deuces", "Five of a kind", "Wild royal flush", "Straight flush", "Four aces with 2-4 kicker", "Four 2s-4s with A-4 kicker", "Four aces", "Four 2s-4s", "Four 5s-Ks", "Four of a kind", "Full house", "Flush", "Straight", "Three of a kind", "Two pair", "Jacks or better", "Kings or better"} + +// pokerPayTable resolves the per-coin (five-coin) rates for a resolved +// game/pay-table pair. Every listed schedule is a published one; see +// building_guide/video_poker.md for the sources. +func pokerPayTable(c VideoPokerConfig) (pokerSchedule, bool) { + c = c.Normalize() + if c.Variant == "" { + return pokerSchedule{}, false + } + p := map[string]int{"Royal flush": 800, "Straight flush": 50, "Four of a kind": 25, "Straight": 4, "Three of a kind": 3, "Two pair": 2, "Jacks or better": 1} + name := variantTitle(c.Variant) + switch c.Variant { + case "jacks_or_better": + if !switchTwo(&p, c.PayTable, map[string][2]int{"9/6": {9, 6}, "9/5": {9, 5}, "8/6": {8, 6}, "8/5": {8, 5}, "7/5": {7, 5}}) { + return pokerSchedule{}, false + } + case "bonus_poker": + if !switchTwo(&p, c.PayTable, map[string][2]int{"8/5": {8, 5}, "7/5": {7, 5}, "6/5": {6, 5}}) { + return pokerSchedule{}, false + } + p["Four aces"], p["Four 2s-4s"], p["Four 5s-Ks"] = 80, 40, 25 + delete(p, "Four of a kind") + case "bonus_poker_deluxe": + if !switchTwo(&p, c.PayTable, map[string][2]int{"9/6": {9, 6}, "9/5": {9, 5}, "8/6": {8, 6}, "8/5": {8, 5}, "7/5": {7, 5}}) { + return pokerSchedule{}, false + } + p["Four of a kind"], p["Two pair"] = 80, 1 + case "double_bonus", "double_double_bonus": + p["Two pair"] = 1 + delete(p, "Four of a kind") + p["Four aces"], p["Four 2s-4s"], p["Four 5s-Ks"] = 160, 80, 50 + if c.Variant == "double_bonus" { + p["Straight"] = 5 + switch c.PayTable { + case "10/7": + p["Full house"], p["Flush"] = 10, 7 + case "9/7": + p["Full house"], p["Flush"] = 9, 7 + case "9/6/5": + p["Full house"], p["Flush"] = 9, 6 + case "9/6/4": + p["Full house"], p["Flush"], p["Straight"] = 9, 6, 4 + case "9/5/4": + p["Full house"], p["Flush"], p["Straight"] = 9, 5, 4 + case "8/5": + p["Full house"], p["Flush"], p["Straight"] = 8, 5, 4 + default: + return pokerSchedule{}, false + } + } else { + name = "Double Double Bonus" + p["Four aces with 2-4 kicker"], p["Four 2s-4s with A-4 kicker"] = 400, 160 + switch c.PayTable { + case "10/6": + p["Full house"], p["Flush"] = 10, 6 + case "9/6": + p["Full house"], p["Flush"] = 9, 6 + case "9/5": + p["Full house"], p["Flush"] = 9, 5 + case "8/6": + p["Full house"], p["Flush"] = 8, 6 + case "8/5": + p["Full house"], p["Flush"] = 8, 5 + default: + return pokerSchedule{}, false + } + } + case "deuces_wild": + name = "Deuces Wild" + p = map[string]int{"Natural royal flush": 800, "Four deuces": 200, "Wild royal flush": 25, "Five of a kind": 15, "Straight flush": 9, "Four of a kind": 5, "Full house": 3, "Flush": 2, "Straight": 2, "Three of a kind": 1} + switch c.PayTable { + case "25/15/9/5/3/2": + case "25/15/9/4/4/3": + p["Four of a kind"], p["Full house"], p["Flush"] = 4, 4, 3 + case "25/16/13/4/3/2": + p["Five of a kind"], p["Straight flush"], p["Four of a kind"] = 16, 13, 4 + case "20/12/10/4/4/3": + p["Wild royal flush"], p["Five of a kind"], p["Straight flush"], p["Four of a kind"], p["Full house"], p["Flush"] = 20, 12, 10, 4, 4, 3 + default: + return pokerSchedule{}, false + } + case "joker_poker": + if c.PayTable != "50/17/7/5/3" && c.PayTable != "50/16/7/5/3" && c.PayTable != "50/15/7/5/3" { + return pokerSchedule{}, false + } + name = "Joker Poker (Kings or Better)" + p = map[string]int{"Natural royal flush": 800, "Five of a kind": 200, "Wild royal flush": 100, "Straight flush": 50, "Four of a kind": 17, "Full house": 7, "Flush": 5, "Straight": 3, "Three of a kind": 2, "Two pair": 1, "Kings or better": 1} + if c.PayTable == "50/16/7/5/3" { + p["Four of a kind"] = 16 + } + if c.PayTable == "50/15/7/5/3" { + p["Four of a kind"] = 15 + } + default: + return pokerSchedule{}, false + } + return pokerSchedule{name + " / " + c.PayTable, p}, true +} + +// switchTwo applies a full-house/flush pair to the schedule map and reports +// whether the pay-table name was one of the listed editions. +func switchTwo(p *map[string]int, payTable string, editions map[string][2]int) bool { + if edition, ok := editions[payTable]; ok { + (*p)["Full house"], (*p)["Flush"] = edition[0], edition[1] + return true + } + return false +} + +func pokerRules(c VideoPokerConfig) string { + c = c.Normalize() + schedule, ok := pokerPayTable(c) + if !ok { + return "This cabinet is not configured with a playable game." + } + lines := []string{c.CabinetLabel() + " / " + schedule.name, + "One fresh deck per deal. bet 1-4 loads coins (up to five per hand);", + "bet max loads five. Bare bet repeats the previous selection. deal", + "charges the full wager and deals; numbers toggle holds; d draws.", + "denom 1|10|50 sets credits per coin. paytable shows this box again.", + "Payouts are total coins per hand (stake included), not profit odds.", + c.PairRule(), + } + if c.Feature != "" { + lines = append(lines, c.FeatureRules()) + } + lines = append(lines, "Only the highest-paying category pays on each line.") + return strings.Join(lines, "\n") +} + +// PairRule names the lowest paying hand family of the selected game. +func (c VideoPokerConfig) PairRule() string { + c = c.Normalize() + switch c.Variant { + case "deuces_wild": + return "Every 2 is wild. Three of a kind is the lowest win; only the highest category pays." + case "joker_poker": + return "One joker is wild. A pair of kings or aces is the lowest win; a wild royal is not a natural royal." + case "bonus_poker", "bonus_poker_deluxe", "double_bonus", "double_double_bonus": + return "A pair of jacks or better is the lowest win; four of a kind carries bonus rates by rank." + } + return "A pair of jacks or better is the lowest win." +} + +// ParsePokerDigits accepts the unordered digits of a hold selection: every +// distinct digit toggles that card once per command; spacing and order do not matter. +func ParsePokerDigits(input string) ([]int, bool) { + var digits []int + var seen [5]bool + for _, r := range input { + if unicode.IsSpace(r) { + continue + } + if r < '1' || r > '5' { + return nil, false + } + i := int(r - '1') + if !seen[i] { + digits = append(digits, i) + seen[i] = true + } + } + return digits, len(digits) > 0 +} + +func pokerRank(c card) int { + switch c.rank { + case "A": + return 14 + case "K": + return 13 + case "Q": + return 12 + case "J": + return 11 + } + return c.blackjackValue() +} + +// evaluatePoker scores one five-card line against the resolved schedule. Wild +// evaluation uses rank deficits instead of enumerating wild-card assignments. +func evaluatePoker(cards [5]card, cfg VideoPokerConfig) (string, int) { + cfg = cfg.Normalize() + schedule, ok := pokerPayTable(cfg) + if !ok { + return "No win", 0 + } + counts := [15]int{} + wild := 0 + suit := "" + flush := true + for _, c := range cards { + r := pokerRank(c) + if cfg.Variant == "deuces_wild" && r == 2 || cfg.Variant == "joker_poker" && c.rank == "JK" { + wild++ + continue + } + counts[r]++ + if suit == "" { + suit = c.suit + } else if suit != c.suit { + flush = false + } + } + straight, royal := false, false + for high := 5; high <= 14; high++ { + fits := true + for r := 2; r <= 14; r++ { + if counts[r] == 0 { + continue + } + in := r >= high-4 && r <= high + if high == 5 && r == 14 { + in = true + } + if !in || counts[r] > 1 { + fits = false + break + } + } + if fits { + straight = true + if high == 14 { + royal = true + } + } + } + maxCount, quad, kicker, pairs := 0, 0, 0, 0 + for r := 2; r <= 14; r++ { + if counts[r] > maxCount { + maxCount = counts[r] + } + if counts[r] == 4 { + quad = r + } + if counts[r] == 1 { + kicker = r + } + if counts[r] == 2 { + pairs++ + } + } + var candidates []string + add := func(ok bool, k string) { + if ok { + candidates = append(candidates, k) + } + } + if cfg.Variant == "deuces_wild" || cfg.Variant == "joker_poker" { + add(flush && royal && wild == 0, "Natural royal flush") + add(cfg.Variant == "deuces_wild" && wild == 4, "Four deuces") + add(flush && royal && wild > 0, "Wild royal flush") + add(maxCount+wild >= 5, "Five of a kind") + add(maxCount+wild >= 4, "Four of a kind") + if cfg.Variant == "joker_poker" { + add(pairs == 2 || pairs == 1 && wild == 1, "Two pair") + add(counts[13]+wild >= 2 || counts[14]+wild >= 2, "Kings or better") + } + } else { + add(flush && royal, "Royal flush") + add(quad > 0, "Four of a kind") + add(quad == 14, "Four aces") + add(quad >= 2 && quad <= 4, "Four 2s-4s") + add(quad >= 5 && quad <= 13, "Four 5s-Ks") + add(quad == 14 && kicker >= 2 && kicker <= 4, "Four aces with 2-4 kicker") + add(quad >= 2 && quad <= 4 && (kicker == 14 || kicker >= 2 && kicker <= 4), "Four 2s-4s with A-4 kicker") + add(pairs == 2, "Two pair") + add(counts[11] == 2 || counts[12] == 2 || counts[13] == 2 || counts[14] == 2, "Jacks or better") + } + full := false + for a := 2; a <= 14; a++ { + for b := 2; b <= 14; b++ { + if a == b || counts[a] > 3 || counts[b] > 2 { + continue + } + if counts[a]+counts[b]+wild == 5 { + full = true + } + } + } + add(flush && straight, "Straight flush") + add(full, "Full house") + add(flush, "Flush") + add(straight, "Straight") + add(maxCount+wild >= 3, "Three of a kind") + best, pay := "No win", 0 + for _, k := range candidates { + if schedule.pays[k] > pay { + best, pay = k, schedule.pays[k] + } + } + return best, pay +} |
