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