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

642 lines
27 KiB
Go
Raw Permalink 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 "testing"
func TestOneOhOneDealsCorrectly(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B", "C"}, -1)
for i, p := range g.Players {
want := 5
if i == g.DealerIdx {
want = 4
}
if len(p.Hand) != want {
t.Errorf("игрок %d: ожидалось %d карт, получено %d", i, want, len(p.Hand))
}
}
if len(g.Discard) != 1 {
t.Fatalf("ожидалась 1 карта в сбросе (стартовая), получено %d", len(g.Discard))
}
if len(g.Stock) != 36-14-1 {
t.Fatalf("ожидалось %d карт в колоде добора, получено %d", 36-14-1, len(g.Stock))
}
if g.CurrentPlayerIdx != g.nextActive(g.DealerIdx) {
t.Errorf("первым должен ходить игрок слева от сдающего")
}
}
// --- Дама: играется на что угодно + объявление масти ------------------
func TestQueenPlayableRegardlessOfSuitOrRank(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Queen, Hearts), c(Two, Diamonds)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Queen, Hearts)); err != nil {
t.Fatalf("дама должна была сыграться независимо от масти и ранга: %v", err)
}
if g.Phase != OneOhOnePhaseChooseSuit {
t.Fatalf("после дамы должна была начаться фаза выбора масти, получено %v", g.Phase)
}
}
func TestChooseSuitSetsDeclaredSuitAndAdvancesTurn(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Queen, Hearts), c(Two, Diamonds)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
g.PlayCard(c(Queen, Hearts))
if err := g.ChooseSuit(Spades); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.DeclaredSuit == nil || *g.DeclaredSuit != Spades {
t.Errorf("ожидалась объявленная масть Spades, получено %v", g.DeclaredSuit)
}
if g.Phase != OneOhOnePhasePlay {
t.Errorf("после выбора масти должна возобновиться обычная фаза игры")
}
if g.CurrentPlayerIdx != 1 {
t.Errorf("ход должен был перейти следующему игроку, получено %d", g.CurrentPlayerIdx)
}
}
func TestChooseSuitRejectedOutsidePhase(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Phase = OneOhOnePhasePlay
if err := g.ChooseSuit(Spades); err != ErrOOOWrongPhase {
t.Errorf("ожидалась ошибка недопустимой фазы, получено: %v", err)
}
}
func TestChooseSuitRejectsInvalidSuit(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Phase = OneOhOnePhaseChooseSuit
if err := g.ChooseSuit(Suit(99)); err != ErrOOOInvalidSuit {
t.Errorf("ожидалась ошибка недопустимой масти, получено: %v", err)
}
}
func TestDeclaredSuitGovernsNextPlayThenClears(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Queen, Hearts), c(Two, Clubs)} // вторая карта — чтобы рука не опустела после дамы
g.Players[1].Hand = []Card{c(Five, Spades), c(Six, Diamonds)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Queen, Hearts)); err != nil {
t.Fatalf("неожиданная ошибка при игре дамы: %v", err)
}
if err := g.ChooseSuit(Spades); err != nil {
t.Fatalf("неожиданная ошибка при выборе масти: %v", err)
}
if !g.canPlay(c(Five, Spades)) {
t.Fatalf("пятёрка пик должна была подойти под объявленную масть")
}
if g.canPlay(c(Six, Diamonds)) {
t.Fatalf("шестёрка бубён не должна была подойти — не та объявленная масть и не совпадает с дамой по рангу")
}
if err := g.PlayCard(c(Five, Spades)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.DeclaredSuit != nil {
t.Errorf("объявленная масть должна была очиститься после следующего хода")
}
}
// --- Восьмёрка: обязательное покрытие ----------------------------------
func TestEightCreatesPendingObligation(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Diamonds)} // совпадает по масти с восьмёркой бубён
g.PendingEightSuit = nil
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Eight, Diamonds), c(Two, Spades)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Eight, Diamonds)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.PendingEightSuit == nil || *g.PendingEightSuit != Diamonds {
t.Fatalf("ожидалось висящее требование покрытия по бубнам, получено %v", g.PendingEightSuit)
}
}
func TestPendingEightOnlySameSuitOrAnotherEightValid(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
suit := Diamonds
g.PendingEightSuit = &suit
if !g.canPlay(c(Two, Diamonds)) {
t.Errorf("карта той же масти, что и висящая восьмёрка, должна быть годной")
}
if !g.canPlay(c(Eight, Clubs)) {
t.Errorf("любая другая восьмёрка должна быть годной для покрытия")
}
if g.canPlay(c(King, Hearts)) {
t.Errorf("карта другой масти (не восьмёрка) не должна подходить, пока висит требование покрытия")
}
if g.canPlay(c(Queen, Hearts)) {
t.Errorf("дама НЕ должна автоматически покрывать висящую восьмёрку — приоритет у требования покрытия")
}
}
func TestPendingEightChainContinuesWithNewSuit(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Diamonds)}
g.PendingEightSuit = nil
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Eight, Diamonds), c(Two, Clubs)} // вторая карта — чтобы рука не опустела после восьмёрки
g.Players[1].Hand = []Card{c(Eight, Spades), c(Two, Hearts)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Eight, Diamonds)); err != nil {
t.Fatalf("неожиданная ошибка первого хода: %v", err)
}
if err := g.PlayCard(c(Eight, Spades)); err != nil {
t.Fatalf("другая восьмёрка должна была покрыть первую: %v", err)
}
if g.PendingEightSuit == nil || *g.PendingEightSuit != Spades {
t.Errorf("требование покрытия должно было переключиться на масть новой восьмёрки (пики), получено %v", g.PendingEightSuit)
}
}
func TestPendingEightClearedByMatchingSuitCover(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Diamonds)}
g.PendingEightSuit = nil
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Eight, Diamonds), c(Two, Clubs)} // вторая карта — чтобы рука не опустела после восьмёрки
g.Players[1].Hand = []Card{c(Two, Diamonds), c(Five, Hearts)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Eight, Diamonds)); err != nil {
t.Fatalf("неожиданная ошибка первого хода: %v", err)
}
if err := g.PlayCard(c(Two, Diamonds)); err != nil {
t.Fatalf("карта той же масти должна была покрыть восьмёрку: %v", err)
}
if g.PendingEightSuit != nil {
t.Errorf("требование покрытия должно было сняться после покрытия обычной картой той же масти")
}
}
func TestPendingEightForcesRepeatedDraw(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Diamonds)}
g.PendingEightSuit = nil
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Eight, Diamonds), c(Two, Clubs)} // вторая карта — чтобы рука не опустела после восьмёрки
g.Players[1].Hand = []Card{c(Two, Hearts)} // не подходит под требование покрытия
g.Stock = []Card{c(Three, Diamonds), c(Four, Clubs)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Eight, Diamonds)); err != nil {
t.Fatalf("неожиданная ошибка первого хода: %v", err)
}
if g.CurrentPlayerIdx != 1 {
t.Fatalf("тест предполагает, что ход перешёл ко второму игроку")
}
if err := g.Draw(); err != nil {
t.Fatalf("неожиданная ошибка при доборе: %v", err)
}
if len(g.Players[1].Hand) != 1 {
t.Fatalf("взятая тройка бубён подходит под покрытие и должна была сразу сыграться, рука: %v", g.Players[1].Hand)
}
if g.PendingEightSuit != nil {
t.Errorf("требование покрытия должно было сняться после автосыгранной тройки бубён")
}
}
func TestOpeningEightCardCreatesInitialObligation(t *testing.T) {
found := false
for attempt := 0; attempt < 300 && !found; attempt++ {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
if g.topDiscard().Rank != Eight {
continue
}
found = true
wantSuit := g.topDiscard().Suit
if g.PendingEightSuit == nil || *g.PendingEightSuit != wantSuit {
t.Fatalf("стартовая карта — восьмёрка (%v), но требование покрытия не выставилось: %v", wantSuit, g.PendingEightSuit)
}
}
if !found {
t.Skip("за 300 попыток стартовая карта ни разу не оказалась восьмёркой — статистически возможно, но маловероятно")
}
}
func TestPlayCardMatchingSuit(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Ten, Clubs)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Ten, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.topDiscard() != (c(Ten, Clubs)) {
t.Errorf("верхняя карта сброса должна была смениться")
}
}
func TestPlayCardMatchingRank(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Nine, Hearts)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Nine, Hearts)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
}
func TestPlayCardRejectsMismatch(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Ten, Hearts)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Ten, Hearts)); err != ErrOOOInvalidPlay {
t.Errorf("ожидалась ошибка ErrOOOInvalidPlay, получено: %v", err)
}
}
func TestDrawRejectedWhenPlayableCardExists(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Nine, Hearts)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.Draw(); err != ErrOOOMustPlay {
t.Errorf("ожидалась ошибка ErrOOOMustPlay, получено: %v", err)
}
}
func TestDrawAutoPlaysWhenDrawnCardMatches(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Ten, Hearts)} // не подходит
g.Stock = []Card{c(Nine, Diamonds), c(Two, Clubs)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.Draw(); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.topDiscard() != (c(Nine, Diamonds)) {
t.Errorf("взятая подходящая карта должна была сразу сыграться")
}
if hasCard(g.Players[0].Hand, c(Nine, Diamonds)) {
t.Errorf("сыгранной карты не должно остаться в руке")
}
}
func TestAceSkipsNextPlayer(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B", "C"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Ace, Clubs), c(Two, Diamonds)}
g.Players[1].Hand = []Card{c(Ten, Hearts)}
g.Players[2].Hand = []Card{c(Jack, Spades)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Ace, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.CurrentPlayerIdx != 2 {
t.Errorf("туз должен был пропустить игрока 1, ожидался ход игрока 2, получено %d", g.CurrentPlayerIdx)
}
if len(g.Players[1].Hand) != 1 {
t.Errorf("пропущенный игрок не должен был брать штрафные карты")
}
}
func TestSevenGivesTwoPenaltyCardsAndSkips(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B", "C"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Seven, Clubs), c(Two, Diamonds)}
g.Players[1].Hand = []Card{c(Ten, Hearts)}
g.Players[2].Hand = []Card{c(Jack, Spades)}
g.Stock = []Card{c(Three, Hearts), c(Four, Hearts), c(Five, Hearts)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Seven, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if len(g.Players[1].Hand) != 3 {
t.Errorf("игрок 1 должен был взять 2 штрафные карты (1+2=3), получено %d карт", len(g.Players[1].Hand))
}
if g.CurrentPlayerIdx != 2 {
t.Errorf("ход должен был перейти к игроку 2, получено %d", g.CurrentPlayerIdx)
}
}
func TestKingOfSpadesGivesFourPenaltyCards(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Spades)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(King, Spades), c(Two, Diamonds)}
g.Players[1].Hand = []Card{c(Ten, Hearts)}
g.Stock = make([]Card, 0, 4)
for i := 0; i < 4; i++ {
g.Stock = append(g.Stock, c(Rank(2+i), Hearts))
}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(King, Spades)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if len(g.Players[1].Hand) != 5 {
t.Errorf("игрок 1 должен был взять 4 штрафные карты (1+4=5), получено %d", len(g.Players[1].Hand))
}
}
func TestOtherKingsHaveNoSpecialEffect(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B", "C"}, -1)
g.Discard = []Card{c(Nine, Hearts)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(King, Hearts), c(Two, Diamonds)}
g.Players[1].Hand = []Card{c(Ten, Hearts)}
g.Players[2].Hand = []Card{c(Jack, Spades)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(King, Hearts)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.CurrentPlayerIdx != 1 {
t.Errorf("некозырной король не должен ничего пропускать, ожидался ход игрока 1, получено %d", g.CurrentPlayerIdx)
}
if len(g.Players[1].Hand) != 1 {
t.Errorf("игрок 1 не должен был брать штрафные карты")
}
}
func TestRoundEndScoresRemainingCards(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Ten, Clubs)}
g.Players[1].Hand = []Card{c(King, Hearts), c(Ace, Diamonds)} // 4+11=15
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Ten, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.Phase != OneOhOnePhaseRoundOver {
t.Fatalf("раунд должен был завершиться победой игрока 0")
}
if g.Result.Winner != 0 {
t.Fatalf("ожидался победитель 0, получен %d", g.Result.Winner)
}
if g.Result.ScoreDeltas[0] != 0 {
t.Errorf("победитель без дамы должен получить 0 очков за раунд, получено %d", g.Result.ScoreDeltas[0])
}
if g.Result.ScoreDeltas[1] != 15 {
t.Errorf("ожидалось 15 штрафных очков у проигравшего, получено %d", g.Result.ScoreDeltas[1])
}
if g.Players[1].Score != 15 {
t.Errorf("общий счёт игрока 1 должен был стать 15, получено %d", g.Players[1].Score)
}
}
func TestQueenBonusOnWin(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Queen, Clubs)}
g.Players[1].Hand = []Card{c(Ten, Hearts)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Queen, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if !g.Result.QueenBonus {
t.Fatalf("ожидался бонус за даму")
}
if g.Result.ScoreDeltas[0] != -20 {
t.Errorf("ожидался бонус -20 за некозырную даму, получено %d", g.Result.ScoreDeltas[0])
}
}
func TestQueenOfHeartsGivesDoubleBonus(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Hearts)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Queen, Hearts)}
g.Players[1].Hand = []Card{c(Ten, Hearts)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Queen, Hearts)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.Result.ScoreDeltas[0] != -40 {
t.Errorf("ожидался бонус -40 за червовую даму, получено %d", g.Result.ScoreDeltas[0])
}
}
func TestExactly101ResetsToZero(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Players[1].Score = 90 // 90+11(туз)=101
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Ten, Clubs)}
g.Players[1].Hand = []Card{c(Ace, Hearts)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Ten, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.Players[1].Score != 0 {
t.Errorf("ровно 101 должно было обнулить счёт, получено %d", g.Players[1].Score)
}
if len(g.Result.Reset) != 1 || g.Result.Reset[0] != 1 {
t.Errorf("ожидался Reset=[1], получено %v", g.Result.Reset)
}
if g.Players[1].Out {
t.Errorf("игрок с обнулённым счётом не должен выбывать")
}
}
func TestOver101Eliminates(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B", "C"}, -1)
g.Players[1].Score = 95 // 95+11=106 > 101
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Ten, Clubs)}
g.Players[1].Hand = []Card{c(Ace, Hearts)}
g.Players[2].Hand = []Card{c(Jack, Spades)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Ten, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if !g.Players[1].Out {
t.Errorf("игрок с 106 очками должен был выбыть")
}
if g.Result.MatchOver {
t.Errorf("партия не должна была завершиться (остались 2 активных игрока)")
}
}
func TestMatchEndsWhenOneActivePlayerRemains(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Players[1].Score = 95
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Ten, Clubs)}
g.Players[1].Hand = []Card{c(Ace, Hearts)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Ten, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if !g.Result.MatchOver {
t.Fatalf("партия должна была завершиться (остался 1 активный игрок)")
}
if g.Result.MatchWinner != 0 {
t.Errorf("ожидался победитель партии 0, получено %d", g.Result.MatchWinner)
}
}
func TestNextRoundDealerIsRoundWinner(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B", "C"}, -1)
g.Discard = []Card{c(Nine, Clubs)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Players[0].Hand = []Card{c(Ten, Clubs)}
g.Players[1].Hand = []Card{c(Ten, Hearts)}
g.Players[2].Hand = []Card{c(Jack, Spades)}
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if err := g.PlayCard(c(Ten, Clubs)); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if g.DealerIdx != 0 {
t.Fatalf("сдающим следующего раунда должен стать победитель (0), получено %d", g.DealerIdx)
}
g.NextRound()
if g.Phase != OneOhOnePhasePlay {
t.Fatalf("новый раунд должен был начаться")
}
for i, p := range g.Players {
want := 5
if i == g.DealerIdx {
want = 4
}
if len(p.Hand) != want {
t.Errorf("игрок %d: ожидалось %d карт в новом раунде, получено %d", i, want, len(p.Hand))
}
}
}
func TestStockReshufflesFromDiscardWhenEmpty(t *testing.T) {
g := NewOneOhOneGame([]string{"A", "B"}, -1)
g.Discard = []Card{c(Nine, Clubs), c(Ten, Diamonds), c(Jack, Diamonds)}
g.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи
g.DeclaredSuit = nil
g.Stock = nil
g.Players[0].Hand = []Card{c(Eight, Clubs)} // не подходит ни по масти (clubs vs diamonds), ни по рангу (8 vs J)
g.CurrentPlayerIdx = 0
g.Phase = OneOhOnePhasePlay
if g.stockExhausted() {
t.Fatalf("колода не должна считаться исчерпанной — под верхней картой сброса есть ещё карты")
}
if err := g.Draw(); err != nil {
t.Fatalf("неожиданная ошибка: %v", err)
}
if len(g.Discard) > 2 {
t.Errorf("после перетасовки в сбросе не должно накопиться старых карт, получено %d карт", len(g.Discard))
}
}
// TestPendingEightInvariantHoldsAcrossManyFullGames — регрессионный
// стресс-тест: проверяет через множество полных партий, что
// canPlay всегда точно соответствует правилу "пока висит непокрытая
// восьмёрка, годится только карта её масти или другая восьмёрка".
// Написан после сообщения о подозрении, что требование покрытия
// не работает — сама логика оказалась корректной (проблема была в
// том, что интерфейс никак не показывал это требование игроку, см.
// oneohone.pending_eight_line), но тест оставлен как постоянная
// защита от регрессии этого конкретного правила.
func TestPendingEightInvariantHoldsAcrossManyFullGames(t *testing.T) {
bot := OneOhOneBot{}
for game := 0; game < 200; game++ {
g := NewOneOhOneGame([]string{"A", "B", "C"}, -1)
for turns := 0; turns < 2000 && g.Phase != OneOhOnePhaseRoundOver; turns++ {
if g.Phase == OneOhOnePhaseChooseSuit {
g.ChooseSuit(bot.DecideSuit(g))
continue
}
if pending := g.PendingEightSuit; pending != nil {
for _, card := range g.current().Hand {
want := card.Suit == *pending || card.Rank == Eight
if g.canPlay(card) != want {
t.Fatalf("партия %d, ход %d: canPlay(%v) при требовании покрыть масть %v — получено %v, ожидалось %v",
game, turns, card, *pending, g.canPlay(card), want)
}
}
}
if card, ok := bot.DecideAction(g); ok {
if err := g.PlayCard(card); err != nil {
t.Fatalf("партия %d, ход %d: неожиданная ошибка розыгрыша %v: %v", game, turns, card, err)
}
} else {
if err := g.Draw(); err != nil {
t.Fatalf("партия %d, ход %d: неожиданная ошибка добора: %v", game, turns, err)
}
}
}
}
}