package casino import ( "strings" "testing" ) func pokerHand(text string) [5]card { var cards [5]card suits := map[byte]string{'c': "clubs", 'd': "diamonds", 'h': "hearts", 's': "spades"} for i, s := range strings.Fields(text) { if s == "JK" { cards[i] = card{rank: "JK", suit: "joker"} continue } cards[i] = card{rank: s[:len(s)-1], suit: suits[s[len(s)-1]]} } return cards } func TestPokerEvaluation(t *testing.T) { tests := []struct { variant, table, hand, category string pay int }{ {"jacks_or_better", "9/6", "Ah Kh Qh Jh 10h", "Royal flush", 800}, {"jacks_or_better", "9/6", "Ah Ac As Ad 10h", "Four of a kind", 25}, {"jacks_or_better", "9/6", "Ah 2h 3h 4h 5h", "Straight flush", 50}, {"jacks_or_better", "9/6", "Ah 2c 3h 4h 5h", "Straight", 4}, {"jacks_or_better", "9/6", "Qh Kh Ac 2h 3h", "No win", 0}, {"jacks_or_better", "9/6", "10h 10c 4s 6h 8c", "No win", 0}, {"jacks_or_better", "9/6", "Jh Jc 4s 6h 8c", "Jacks or better", 1}, {"jacks_or_better", "9/6", "Jh Jc Js 6h 6c", "Full house", 9}, {"jacks_or_better", "8/5", "Jh Jc Js 6h 6c", "Full house", 8}, {"jacks_or_better", "8/5", "Jh 3h 8h 6h 9h", "Flush", 5}, {"jacks_or_better", "9/6", "Jh Jc 4s 4h 8c", "Two pair", 2}, {"bonus_poker", "8/5", "Ah Ac As Ad 2c", "Four aces", 80}, {"bonus_poker", "8/5", "3h 3c 3s 3d Ac", "Four 2s-4s", 40}, {"bonus_poker", "8/5", "Kh Kc Ks Kd Ac", "Four 5s-Ks", 25}, {"double_bonus", "10/7", "Ah Ac As Ad 2c", "Four aces", 160}, {"double_bonus", "10/7", "3h 3c 3s 3d Ac", "Four 2s-4s", 80}, {"double_bonus", "10/7", "Kh Kc Ks Kd Ac", "Four 5s-Ks", 50}, {"double_bonus", "10/7", "Ah 2c 3h 4h 5h", "Straight", 5}, {"double_bonus", "9/6/4", "Ah 2c 3h 4h 5h", "Straight", 4}, {"double_double_bonus", "10/6", "Ah Ac As Ad 2c", "Four aces with 2-4 kicker", 400}, {"double_double_bonus", "10/6", "Ah Ac As Ad 5c", "Four aces", 160}, {"double_double_bonus", "9/6", "4h 4c 4s 4d Ac", "Four 2s-4s with A-4 kicker", 160}, {"double_double_bonus", "8/5", "2h 2c 2s 2d 4c", "Four 2s-4s with A-4 kicker", 160}, {"double_double_bonus", "10/6", "4h 4c 4s 4d 5c", "Four 2s-4s", 80}, {"double_double_bonus", "10/6", "Jh Jc Js 6h 6c", "Full house", 10}, {"deuces_wild", "25/15/9/5/3/2", "Ah Kh Qh Jh 10h", "Natural royal flush", 800}, {"deuces_wild", "25/15/9/5/3/2", "2h 2c 2s 2d Ac", "Four deuces", 200}, {"deuces_wild", "25/15/9/5/3/2", "Ah Kh Qh 2c 10h", "Wild royal flush", 25}, {"deuces_wild", "25/15/9/5/3/2", "Ah Kh Qc 2c 10h", "Straight", 2}, {"deuces_wild", "25/15/9/5/3/2", "Ah Ac As 2c 2h", "Five of a kind", 15}, {"deuces_wild", "25/15/9/5/3/2", "3h 4h 5h 2c 2h", "Straight flush", 9}, {"deuces_wild", "25/15/9/5/3/2", "3h 3c 3s 2c 7h", "Four of a kind", 5}, {"deuces_wild", "25/15/9/5/3/2", "3h 3c 7s 2c 7h", "Full house", 3}, {"deuces_wild", "25/15/9/5/3/2", "3h 4h 8h 2c Jh", "Flush", 2}, {"deuces_wild", "20/12/10/4/4/3", "3h 4h 8h 2c Jh", "Flush", 3}, {"deuces_wild", "20/12/10/4/4/3", "Ah Kh Qh 2c 10h", "Wild royal flush", 20}, {"deuces_wild", "25/15/9/5/3/2", "3h 3c 8s 2c Jh", "Three of a kind", 1}, {"deuces_wild", "25/15/9/5/3/2", "3h 3c 8s 8c Jh", "No win", 0}, {"deuces_wild", "25/15/9/5/3/2", "Ah Ac 8s 5c Jh", "No win", 0}, {"joker_poker", "50/17/7/5/3", "Ah Kh Qh Jh 10h", "Natural royal flush", 800}, {"joker_poker", "50/17/7/5/3", "Ah Kh Qh JK 10h", "Wild royal flush", 100}, {"joker_poker", "50/17/7/5/3", "Ah Ac As Ad JK", "Five of a kind", 200}, {"joker_poker", "50/17/7/5/3", "Kh 3c 5s 8d JK", "Kings or better", 1}, {"joker_poker", "50/17/7/5/3", "Qh Qc 5s 8d 9h", "No win", 0}, {"joker_poker", "50/17/7/5/3", "Qh Qc 5s 5d 9h", "Two pair", 1}, {"joker_poker", "50/17/7/5/3", "Kh Kc 5s 5d JK", "Full house", 7}, {"bonus_poker_deluxe", "9/6", "2h 2c 2s 2d Ac", "Four of a kind", 80}, {"bonus_poker_deluxe", "9/6", "Jh Jc 4s 4h 8c", "Two pair", 1}, } for _, tt := range tests { t.Run(tt.variant+"/"+tt.hand, func(t *testing.T) { got, pay := evaluatePoker(pokerHand(tt.hand), VideoPokerConfig{Variant: tt.variant, PayTable: tt.table}) if got != tt.category || pay != tt.pay { t.Fatalf("got %s/%d, want %s/%d", got, pay, tt.category, tt.pay) } }) } } func TestPokerHoldDigits(t *testing.T) { for _, input := range []string{"531", "5 3 1", " 5\t3\n1 "} { digits, ok := ParsePokerDigits(input) if !ok || len(digits) != 3 { t.Fatalf("%q: %v %v", input, digits, ok) } } for _, input := range []string{"", " ", "0", "6", "123456", "1,3", "-1", "1x3"} { if _, ok := ParsePokerDigits(input); ok { t.Fatalf("accepted %q", input) } } } func TestPokerCabinets(t *testing.T) { // Every cabinet type normalizes to its fixed feature and default game. cfg := TableConfig{ID: "vp", Game: GameVideoPoker, VideoPoker: VideoPokerConfig{Cabinet: CabinetUXMulti}}.Normalize() if err := cfg.Validate(); err != nil { t.Fatal(err) } if cfg.VideoPoker.Feature != PokerUltimateX || cfg.VideoPoker.Payout != "full" || cfg.VideoPoker.Variant != "jacks_or_better" { t.Fatalf("UX normalize: %+v", cfg.VideoPoker) } if cfg.VideoPoker.PayTable != "8/6" { t.Fatalf("UX multi schedule %q", cfg.VideoPoker.PayTable) } single := TableConfig{ID: "vp", Game: GameVideoPoker, VideoPoker: VideoPokerConfig{Cabinet: CabinetUXSingle}}.Normalize() if single.VideoPoker.PayTable != "9/6" { t.Fatalf("UX single schedule %q", single.VideoPoker.PayTable) } hr := TableConfig{ID: "vp", Game: GameVideoPoker, VideoPoker: VideoPokerConfig{Cabinet: CabinetHotRoll}}.Normalize() if hr.VideoPoker.PayTable != "9/6" || hr.VideoPoker.Feature != PokerHotRoll { t.Fatalf("hot roll: %+v", hr.VideoPoker) } // Authored overrides are rejected; tiers must be known values. for _, c := range []VideoPokerConfig{{Cabinet: "unknown"}, {Payout: "high"}, {Feature: "ux_stp"}} { if (TableConfig{ID: "vp", Game: GameVideoPoker, VideoPoker: c}).Validate() == nil { t.Fatalf("accepted %+v", c) } } // Low/normal tiers resolve verified short-pay schedules on classic cabinets. tiers := map[string]string{"full": "9/6", "normal": "8/5", "low": "7/5"} for tier, want := range tiers { cfg := TableConfig{ID: "vp", Game: GameVideoPoker, VideoPoker: VideoPokerConfig{Payout: tier}}.Normalize() if err := cfg.Validate(); err != nil { t.Fatal(err) } if cfg.VideoPoker.PayTable != want { t.Fatalf("%s: %q", tier, cfg.VideoPoker.PayTable) } } // The pay table carries across games: tier survives a game switch. mid := TableConfig{ID: "vp", Game: GameVideoPoker, VideoPoker: VideoPokerConfig{Payout: "normal", Variant: "deuces_wild"}}.Normalize() if mid.VideoPoker.PayTable != "25/15/9/4/4/3" || mid.VideoPoker.Feature != "" { t.Fatalf("mid deuces: %+v", mid.VideoPoker) } if (TableConfig{ID: "bj", Game: GameBlackjack, VideoPoker: VideoPokerConfig{Cabinet: CabinetClassic, Payout: "full", Variant: "jacks_or_better"}}).Validate() == nil { t.Fatal("foreign block accepted") } }