package main import "testing" func dc(rank Rank, suit Suit) Card { return Card{Rank: rank, Suit: suit} } func TestNewDurakGameDealsSixEach(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C"}, -1) for i, p := range g.Players { if len(p.Hand) != 6 { t.Errorf("игрок %d: ожидалось 6 карт, получено %d", i, len(p.Hand)) } } // 36 - 18 в руках - 1 козырь на виду = 17 в талоне (козырь ещё // формально считается в талоне, так как он его последняя карта) if len(g.Talon) != 36-18 { t.Fatalf("ожидалось %d карт в талоне, получено %d", 36-18, len(g.Talon)) } if g.Talon[len(g.Talon)-1] != g.TrumpCard { t.Fatalf("последняя карта талона должна быть козырной картой") } } func TestNewDurakGameSixPlayersUnevenDeal(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C", "D", "E", "F"}, -1) total := 0 for _, p := range g.Players { total += len(p.Hand) } // 35 карт доступно для раздачи (36 минус зарезервированный // козырь), на 6 игроков по 6 не поделится ровно if total != 35 { t.Fatalf("ожидалось 35 розданных карт при 6 игроках, получено %d", total) } if len(g.Talon) != 1 { t.Fatalf("в талоне должна была остаться только козырная карта, получено %d карт", len(g.Talon)) } } func TestInitialAttackerHasLowestTrump(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C"}, -1) minTrumpRank := func(hand []Card) (int, bool) { best := 99 found := false for _, c := range hand { if c.Suit == g.TrumpSuit && durakRankOrder[c.Rank] < best { best = durakRankOrder[c.Rank] found = true } } return best, found } attackerMin, attackerHasTrump := minTrumpRank(g.Players[g.AttackerIdx].Hand) if !attackerHasTrump { return // допускаем случай, когда ни у кого нет козыря } for i, p := range g.Players { if i == g.AttackerIdx { continue } if otherMin, ok := minTrumpRank(p.Hand); ok && otherMin < attackerMin { t.Errorf("у игрока %d козырь младше (%d), чем у выбранного атакующего (%d)", i, otherMin, attackerMin) } } } // setupBout — тестовый хелпер: вручную задаёт роли, руки и козырь, // затем через startNewBout() корректно инициализирует attackCap, // ThrowOrder и Phase (вместо ручного дублирования этой логики в // каждом тесте). func setupBout(g *DurakGameState, attackerIdx, defenderIdx int, trump Suit, hands map[int][]Card) { g.AttackerIdx = attackerIdx g.DefenderIdx = defenderIdx g.TrumpSuit = trump for idx, hand := range hands { g.Players[idx].Hand = hand } g.startNewBout() } func TestThrowInAndDefendEntersThrowInPhase(t *testing.T) { // 3 игрока и запас карт у защищающегося (cap=3), чтобы после // первой защиты лимит атаки ещё не был исчерпан и раунд ушёл в // фазу подкидывания, а не сразу в отбой g := NewDurakGame([]string{"A", "B", "C"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs), dc(Nine, Hearts)}, 1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)}, 2: {dc(Eight, Diamonds)}, }) if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка подкидывания: %v", err) } if g.Phase != DurakPhaseDefend { t.Fatalf("ожидалась фаза защиты") } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка защиты: %v", err) } if g.Phase != DurakPhaseThrowIn { t.Fatalf("после успешной защиты (лимит атаки ещё не исчерпан) ожидалась фаза подкидывания, получено %v", g.Phase) } if len(g.Table) != 1 || g.Table[0].Defend == nil { t.Fatalf("карта на столе должна быть отбита") } // первый в очереди подкидывающих — атакующий раунда (игрок 0) if g.CurrentThrower() != 0 { t.Errorf("ожидался первый подкидывающий 0 (атакующий), получено %d", g.CurrentThrower()) } } func TestOtherPlayerCanThrowIn(t *testing.T) { // ключевая проверка полноценных правил: подкинуть карту может не // только атакующий, но и третий игрок (не защищающийся) g := NewDurakGame([]string{"A", "B", "C"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs)}, 1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)}, 2: {dc(Six, Hearts)}, // тот же ранг, что уже на столе }) if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Phase != DurakPhaseThrowIn { t.Fatalf("ожидалась фаза подкидывания") } // игрок 0 (атакующий) пасует — очередь переходит к игроку 2 if err := g.Pass(); err != nil { t.Fatalf("неожиданная ошибка паса: %v", err) } if g.CurrentThrower() != 2 { t.Fatalf("ожидался подкидывающий 2, получено %d", g.CurrentThrower()) } // игрок 2 (не атакующий и не защищающийся) подкидывает шестёрку червей if err := g.ThrowIn(dc(Six, Hearts)); err != nil { t.Fatalf("игрок 2 должен был иметь возможность подкинуть карту: %v", err) } if g.Phase != DurakPhaseDefend { t.Fatalf("ожидалась фаза защиты после подкидывания игроком 2") } if len(g.Table) != 2 { t.Fatalf("ожидалось 2 карты на столе, получено %d", len(g.Table)) } } func TestDefendRejectsWeakerCard(t *testing.T) { g := NewDurakGame([]string{"A", "B"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Eight, Clubs)}, 1: {dc(Six, Clubs)}, // младше по той же масти }) if err := g.ThrowIn(dc(Eight, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Six, Clubs)); err != ErrDurakInvalidDefend { t.Errorf("ожидалась ошибка ErrDurakInvalidDefend, получено: %v", err) } } func TestTrumpBeatsNonTrump(t *testing.T) { g := NewDurakGame([]string{"A", "B"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Ace, Clubs)}, 1: {dc(Six, Spades)}, // младший козырь бьёт туза не в масть }) if err := g.ThrowIn(dc(Ace, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Six, Spades)); err != nil { t.Errorf("младший козырь должен побить некозырного туза: %v", err) } } func TestNonTrumpCannotBeatTrump(t *testing.T) { g := NewDurakGame([]string{"A", "B"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Spades)}, // козырная атака 1: {dc(Ace, Clubs)}, }) if err := g.ThrowIn(dc(Six, Spades)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Ace, Clubs)); err != ErrDurakInvalidDefend { t.Errorf("некозырная карта не должна бить козырь, получено: %v", err) } } func TestThrowInRejectsMismatchedRank(t *testing.T) { g := NewDurakGame([]string{"A", "B"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs), dc(Nine, Hearts)}, 1: {dc(Seven, Clubs), dc(Ten, Diamonds)}, // 2 карты -> cap=2, не исчерпан после 1 карты }) if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Phase != DurakPhaseThrowIn { t.Fatalf("ожидалась фаза подкидывания, получено %v", g.Phase) } // девятка не совпадает по рангу ни с чем на столе (там только 6 и 7) if err := g.ThrowIn(dc(Nine, Hearts)); err != ErrDurakInvalidThrowIn { t.Errorf("ожидалась ошибка ErrDurakInvalidThrowIn, получено: %v", err) } } func TestHasThrowableCardFalseWithEmptyHand(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs)}, 1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)}, 2: {}, // пустая рука — подкидывать нечем чисто физически }) if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.hasThrowableCard(2) { t.Errorf("у игрока с пустой рукой не может быть карты для подкидывания") } } func TestHasThrowableCardFalseWithNoMatchingRank(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs)}, 1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)}, 2: {dc(Eight, Diamonds), dc(Nine, Hearts)}, // ни 8, ни 9 не совпадают с 6/7 на столе }) if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.hasThrowableCard(2) { t.Errorf("ни одна карта в руке не подходит по рангу — подкидывать нечего") } } func TestHasThrowableCardTrueWithMatchingRank(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs)}, 1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)}, 2: {dc(Six, Hearts), dc(Nine, Hearts)}, // шестёрка совпадает по рангу с 6♣ на столе }) if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if !g.hasThrowableCard(2) { t.Errorf("шестёрка в руке должна была засчитаться как подходящая для подкидывания") } } func TestHasThrowableCardFalseWhenAttackCapReached(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs)}, 1: {dc(Seven, Clubs), dc(Jack, Diamonds)}, 2: {dc(Six, Hearts)}, // шестёрка подошла бы по рангу, но лимит атаки уже исчерпан }) defendCard := dc(Seven, Clubs) g.Table = []durakTablePair{{Attack: dc(Six, Clubs), Defend: &defendCard}} g.attackCap = 1 // имитируем уже достигнутый лимит атаки напрямую if g.hasThrowableCard(2) { t.Errorf("при достигнутом лимите атаки подкидывать уже некуда, даже если ранг подходит") } } func TestPassingResolvesBito(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs)}, 1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)}, 2: {dc(Eight, Diamonds)}, // не подходит по рангу для подкидывания }) if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Phase != DurakPhaseThrowIn { t.Fatalf("ожидалась фаза подкидывания") } // оба потенциальных подкидывающих (0 и 2) пасуют по очереди if err := g.Pass(); err != nil { t.Fatalf("неожиданная ошибка первого паса: %v", err) } if g.Phase == DurakPhaseGameOver { t.Fatalf("партия не должна была завершиться") } if len(g.Table) == 0 { t.Fatalf("после первого паса стол ещё не должен был очиститься (ждём второй пас)") } if err := g.Pass(); err != nil { t.Fatalf("неожиданная ошибка второго паса: %v", err) } if len(g.Table) != 0 { t.Errorf("после второго паса стол должен был очиститься (бито)") } } func TestAutoResolvesWhenNoOneElseCanThrow(t *testing.T) { // 2 игрока: после защиты подкидывать может только сам атакующий; // если у него нет карт подходящего ранга — единственный пас в // ThrowOrder сразу должен разрешить раунд бито g := NewDurakGame([]string{"A", "B"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs), dc(Nine, Hearts)}, // девятка не подходит по рангу 1: {dc(Seven, Clubs), dc(Ten, Diamonds)}, }) if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Phase != DurakPhaseThrowIn { t.Fatalf("ожидалась фаза подкидывания") } if err := g.Pass(); err != nil { t.Fatalf("неожиданная ошибка паса: %v", err) } if len(g.Table) != 0 { t.Errorf("после единственного паса (2 игрока) стол должен был очиститься") } } func TestBitoRotatesRolesAndRefills(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs)}, 1: {dc(Seven, Clubs), dc(Ten, Diamonds), dc(Jack, Diamonds)}, 2: {dc(Eight, Diamonds)}, }) g.Talon = []Card{dc(Two, Hearts), dc(Three, Hearts)} // карты-заглушки, важен только факт добора if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Pass(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Pass(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Phase == DurakPhaseGameOver { t.Fatalf("партия не должна была завершиться в этом сценарии, результат: %+v", g.Result) } if len(g.Table) != 0 { t.Errorf("стол должен был очиститься после бито") } // атакующим должен стать следующий после старого защищающегося (1) -> 2 if g.AttackerIdx != 2 { t.Errorf("ожидался новый атакующий 2, получено %d", g.AttackerIdx) } if g.DefenderIdx != 0 { t.Errorf("ожидался новый защищающийся 0, получено %d", g.DefenderIdx) } if len(g.Talon) != 0 { t.Errorf("талон должен был опустеть, осталось: %v", g.Talon) } } func TestTakeGivesTableCardsToDefenderAndRotates(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Eight, Clubs), dc(Ten, Diamonds)}, // запасная карта, чтобы не выбыть при пустом талоне 1: {dc(Six, Hearts)}, // не бьёт и не козырь 2: {dc(Nine, Diamonds)}, }) g.Talon = nil if err := g.ThrowIn(dc(Eight, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Take(); err != nil { t.Fatalf("неожиданная ошибка взятия: %v", err) } if g.Phase == DurakPhaseGameOver { t.Fatalf("партия не должна была завершиться в этом сценарии, результат: %+v", g.Result) } wantHand := []Card{dc(Six, Hearts), dc(Eight, Clubs)} if len(g.Players[1].Hand) != len(wantHand) { t.Fatalf("защищающийся должен был получить карту со стола вдобавок к своей, рука: %v", g.Players[1].Hand) } for _, c := range wantHand { if !hasCard(g.Players[1].Hand, c) { t.Errorf("в руке защищающегося должна была быть карта %v, рука: %v", c, g.Players[1].Hand) } } if len(g.Table) != 0 { t.Errorf("стол должен был очиститься после взятия") } // новым атакующим должен стать следующий после защищающегося (1) -> 2 if g.AttackerIdx != 2 { t.Errorf("ожидался новый атакующий 2, получено %d", g.AttackerIdx) } if g.DefenderIdx != 0 { t.Errorf("ожидался новый защищающийся 0, получено %d", g.DefenderIdx) } } func TestGameOverWhenOnePlayerLeftWithCards(t *testing.T) { g := NewDurakGame([]string{"A", "B"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs)}, 1: {dc(Seven, Clubs), dc(Ten, Diamonds)}, // одна карта останется после защиты }) g.Talon = nil if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Pass(); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Phase != DurakPhaseGameOver { t.Fatalf("игрок 0 избавился от последней карты при пустом талоне — партия должна была завершиться") } if g.Result == nil || g.Result.NoLoser { t.Fatalf("ожидался единственный проигравший (игрок 1), получено: %+v", g.Result) } if g.Result.Loser != 1 { t.Errorf("ожидался проигравший 1 (у него осталась карта), получено %d", g.Result.Loser) } } func TestAttackCapLimitsThrowIn(t *testing.T) { g := NewDurakGame([]string{"A", "B"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs), dc(Six, Hearts), dc(Six, Diamonds)}, 1: {dc(Seven, Clubs)}, // всего 1 карта -> cap = 1 }) if g.attackCap != 1 { t.Fatalf("ожидался лимит атаки 1, получено %d", g.attackCap) } if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } // лимит атаки (1) уже достигнут -> раунд должен был автоматически // разрешиться бито, не дожидаясь паса if len(g.Table) != 0 { t.Errorf("при исчерпанном лимите атаки стол должен был очиститься сразу после защиты") } } func TestCapReachedResolvesBitoWithoutWaitingForPass(t *testing.T) { g := NewDurakGame([]string{"A", "B", "C"}, -1) setupBout(g, 0, 1, Spades, map[int][]Card{ 0: {dc(Six, Clubs)}, 1: {dc(Seven, Clubs)}, // 1 карта -> cap=1 2: {dc(Six, Hearts)}, }) if g.attackCap != 1 { t.Fatalf("ожидался лимит атаки 1, получено %d", g.attackCap) } if err := g.ThrowIn(dc(Six, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Defend(dc(Seven, Clubs)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Phase == DurakPhaseThrowIn { t.Fatalf("при исчерпанном лимите атаки фаза подкидывания не должна была наступить") } }