package main import ( "math/rand" "testing" ) func newTestPokerGame(numPlayers, stack int) *PokerGameState { rnd := rand.New(rand.NewSource(1)) return NewPokerGame(numPlayers, stack, 10, 20, rnd) } func TestNewPokerGamePostsBlindsCorrectly(t *testing.T) { g := newTestPokerGame(3, 1000) sbIdx := g.nextActiveTournamentSeat(g.DealerIdx) bbIdx := g.nextActiveTournamentSeat(sbIdx) if g.Players[sbIdx].CurrentBet != 10 { t.Errorf("малый блайнд должен был поставить 10, получено %d", g.Players[sbIdx].CurrentBet) } if g.Players[bbIdx].CurrentBet != 20 { t.Errorf("большой блайнд должен был поставить 20, получено %d", g.Players[bbIdx].CurrentBet) } if g.Players[sbIdx].Stack != 990 || g.Players[bbIdx].Stack != 980 { t.Errorf("стеки после блайндов неверны: SB=%d BB=%d", g.Players[sbIdx].Stack, g.Players[bbIdx].Stack) } } func TestHeadsUpDealerPostsSmallBlindHimself(t *testing.T) { g := newTestPokerGame(2, 1000) other := g.nextActiveTournamentSeat(g.DealerIdx) if g.Players[g.DealerIdx].CurrentBet != g.SmallBlind { t.Errorf("в игре один на один дилер должен сам поставить малый блайнд, получено %d", g.Players[g.DealerIdx].CurrentBet) } if g.Players[other].CurrentBet != g.BigBlind { t.Errorf("в игре один на один второй игрок должен поставить большой блайнд, получено %d", g.Players[other].CurrentBet) } if g.CurrentPlayerIdx != g.DealerIdx { t.Errorf("в игре один на один префлоп-ход первым должен ходить дилер (малый блайнд), получено игрока %d", g.CurrentPlayerIdx) } } func TestEachPlayerGetsTwoHoleCards(t *testing.T) { g := newTestPokerGame(4, 1000) seen := map[Card]bool{} for _, p := range g.Players { if p.HoleCards[0] == p.HoleCards[1] { t.Fatalf("две карты в руке одного игрока не должны совпадать: %v", p.HoleCards) } for _, c := range p.HoleCards { if seen[c] { t.Fatalf("карта %v оказалась у двух разных игроков", c) } seen[c] = true } } } func TestPreflopActionStartsAfterBigBlind(t *testing.T) { g := newTestPokerGame(3, 1000) bbIdx := g.nextActiveTournamentSeat(g.nextActiveTournamentSeat(g.DealerIdx)) want := g.nextToAct(bbIdx) if g.CurrentPlayerIdx != want { t.Errorf("первый ход префлопа должен быть у игрока после большого блайнда, ожидалось %d, получено %d", want, g.CurrentPlayerIdx) } } func TestCheckRejectedWhenBetIsOutstanding(t *testing.T) { g := newTestPokerGame(3, 1000) if err := g.Check(); err != ErrPokerCannotCheck { t.Errorf("ожидалась ошибка недопустимого чека против ставки большого блайнда, получено: %v", err) } } func TestCallMatchesHighestBet(t *testing.T) { g := newTestPokerGame(3, 1000) actor := g.CurrentPlayerIdx if err := g.Call(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Players[actor].CurrentBet != 20 { t.Errorf("после колла ставка должна была стать 20, получено %d", g.Players[actor].CurrentBet) } } func TestBetBelowMinimumRejected(t *testing.T) { g := newTestPokerGame(3, 1000) if err := g.Bet(25); err != ErrPokerBetTooLow { t.Errorf("ожидалась ошибка слишком маленькой ставки (минимум 40 = 20+20), получено: %v", err) } } func TestBetAboveStackRejected(t *testing.T) { g := newTestPokerGame(3, 1000) if err := g.Bet(100000); err != ErrPokerNotEnoughChips { t.Errorf("ожидалась ошибка нехватки фишек, получено: %v", err) } } func TestCallWithInsufficientStackGoesAllIn(t *testing.T) { g := newTestPokerGame(3, 15) actor := g.CurrentPlayerIdx if err := g.Call(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if !g.Players[actor].AllIn { t.Errorf("колл с недостаточным стеком должен был перевести игрока в олл-ин") } if g.Players[actor].Stack != 0 { t.Errorf("после олл-ина стек должен был обнулиться, получено %d", g.Players[actor].Stack) } } func TestFoldToOnePlayerEndsHandImmediately(t *testing.T) { g := newTestPokerGame(3, 1000) for g.Phase != PokerPhaseShowdown && g.countInHand() > 1 { if err := g.Fold(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } } if g.Phase != PokerPhaseShowdown { t.Fatalf("раздача должна была завершиться после того, как остался один игрок") } if g.Result == nil || g.Result.WentToShowdown { t.Errorf("завершение сбросами не должно доходить до шоудауна с показом карт") } } func TestBettingRoundAdvancesToFlopWhenAllCall(t *testing.T) { g := newTestPokerGame(3, 1000) for g.Phase == PokerPhasePreflop { if err := g.Call(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } } if g.Phase != PokerPhaseFlop { t.Fatalf("после того как все уравняли, должен был начаться флоп, получено %v", g.Phase) } if len(g.Board) != 3 { t.Errorf("на флопе должно быть 3 общие карты, получено %d", len(g.Board)) } } func TestFullHandProgressesThroughAllStreets(t *testing.T) { g := newTestPokerGame(2, 1000) steps := 0 for g.Phase != PokerPhaseShowdown && steps < 100 { steps++ if err := g.Check(); err != nil { g.Call() } } if g.Phase != PokerPhaseShowdown { t.Fatalf("раздача не дошла до шоудауна за %d шагов", steps) } if len(g.Board) != 5 { t.Errorf("на шоудауне должно быть 5 общих карт, получено %d", len(g.Board)) } } func TestAllInSkipsRemainingBettingRounds(t *testing.T) { g := newTestPokerGame(2, 100) if err := g.AllIn(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Call(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Phase != PokerPhaseShowdown { t.Fatalf("после олл-ина и колла (никто больше не может действовать) должен был сразу настать шоудаун, получено %v", g.Phase) } if len(g.Board) != 5 { t.Errorf("на доске должно было оказаться все 5 карт (доигранных без торгов), получено %d", len(g.Board)) } } func TestShowdownAwardsPotToBestHand(t *testing.T) { g := newTestPokerGame(2, 1000) g.Players[0].HoleCards = [2]Card{c(Ace, Spades), c(Ace, Hearts)} g.Players[1].HoleCards = [2]Card{c(Two, Clubs), c(Seven, Diamonds)} g.Board = []Card{c(Ace, Clubs), c(King, Spades), c(Queen, Hearts), c(Four, Diamonds), c(Nine, Clubs)} g.Players[0].TotalContributed = 100 g.Players[1].TotalContributed = 100 g.resolveShowdown() if g.Result.Winners[0] <= 0 { t.Errorf("игрок 0 с тройкой тузов должен был выиграть банк, получено %+v", g.Result.Winners) } if _, lost := g.Result.Winners[1]; lost { t.Errorf("игрок 1 не должен был получить долю банка") } } func TestShowdownSplitsPotOnTie(t *testing.T) { g := newTestPokerGame(2, 1000) g.Players[0].HoleCards = [2]Card{c(Two, Clubs), c(Three, Diamonds)} g.Players[1].HoleCards = [2]Card{c(Two, Hearts), c(Three, Spades)} g.Board = []Card{c(Ace, Clubs), c(King, Spades), c(Queen, Hearts), c(Jack, Diamonds), c(Nine, Clubs)} g.Players[0].TotalContributed = 100 g.Players[1].TotalContributed = 100 g.resolveShowdown() if g.Result.Winners[0] != 100 || g.Result.Winners[1] != 100 { t.Errorf("одинаковые по силе руки (игра по доске) должны были поровну разделить банк 200, получено %+v", g.Result.Winners) } } func TestSidePotsWithTwoDifferentAllInAmounts(t *testing.T) { players := []*PokerPlayer{ {TotalContributed: 50}, {TotalContributed: 100}, {TotalContributed: 100}, } pots := computeSidePots(players) if len(pots) != 2 { t.Fatalf("ожидалось 2 банка (основной + один сайд-пот), получено %d: %+v", len(pots), pots) } if pots[0].Amount != 150 { t.Errorf("основной банк должен быть 150 (50 от каждого из трёх), получено %d", pots[0].Amount) } if len(pots[0].EligiblePlayers) != 3 { t.Errorf("на основной банк претендуют все трое, получено %d", len(pots[0].EligiblePlayers)) } if pots[1].Amount != 100 { t.Errorf("сайд-пот должен быть 100 (по 50 от двоих, кто пошёл дальше), получено %d", pots[1].Amount) } if len(pots[1].EligiblePlayers) != 2 { t.Errorf("на сайд-пот претендуют только двое, получено %d", len(pots[1].EligiblePlayers)) } } func TestSidePotsExcludeFoldedPlayersFromEligibilityButKeepTheirMoney(t *testing.T) { players := []*PokerPlayer{ {TotalContributed: 100, Folded: true}, {TotalContributed: 100}, {TotalContributed: 100}, } pots := computeSidePots(players) if len(pots) != 1 { t.Fatalf("ожидался один общий банк, получено %d", len(pots)) } if pots[0].Amount != 300 { t.Errorf("деньги сбросившего игрока должны были остаться в банке, ожидалось 300, получено %d", pots[0].Amount) } if len(pots[0].EligiblePlayers) != 2 { t.Errorf("сбросивший игрок не должен претендовать на банк, ожидалось 2 претендента, получено %d", len(pots[0].EligiblePlayers)) } } func TestPlayerEliminatedWhenStackHitsZero(t *testing.T) { g := newTestPokerGame(2, 1000) g.Players[0].HoleCards = [2]Card{c(Two, Clubs), c(Seven, Diamonds)} g.Players[1].HoleCards = [2]Card{c(Ace, Spades), c(Ace, Hearts)} g.Board = []Card{c(Ace, Clubs), c(King, Spades), c(Queen, Hearts), c(Four, Diamonds), c(Nine, Clubs)} g.Players[0].Stack = 0 g.Players[0].TotalContributed = 1000 g.Players[1].TotalContributed = 1000 g.resolveShowdown() if !g.Players[0].Eliminated { t.Errorf("проигравший всё игрок должен был выбыть из турнира") } } func TestTournamentOverWhenOnePlayerRemains(t *testing.T) { rnd := rand.New(rand.NewSource(1)) g := newTestPokerGame(2, 1000) g.Players[0].Eliminated = true g.Players[0].Stack = 0 g.Phase = PokerPhaseShowdown g.NextHand(rnd) if g.Phase != PokerPhaseTournamentOver { t.Errorf("турнир должен был завершиться при одном оставшемся игроке, получено %v", g.Phase) } } func TestDealerButtonRotatesEachHand(t *testing.T) { rnd := rand.New(rand.NewSource(1)) g := newTestPokerGame(3, 1000) firstDealer := g.DealerIdx for g.Phase != PokerPhaseShowdown { g.Fold() } g.NextHand(rnd) if g.DealerIdx == firstDealer { t.Errorf("дилер должен был смениться на следующей раздаче") } }