go_games_collection/durak_game_test.go
2026-07-04 13:51:04 +03:00

427 lines
18 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 "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 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("при исчерпанном лимите атаки фаза подкидывания не должна была наступить")
}
}