go_games_collection/poker_game_test.go
2026-07-10 09:12:06 +03:00

287 lines
11 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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("дилер должен был смениться на следующей раздаче")
}
}