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. 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 }