291 lines
11 KiB
Go
291 lines
11 KiB
Go
package main
|
||
|
||
import "testing"
|
||
|
||
func TestDurakModelDealsCorrectPlayerCount(t *testing.T) {
|
||
for n := 2; n <= 6; n++ {
|
||
m := NewDurakModel(n)
|
||
if len(m.game.Players) != n {
|
||
t.Errorf("ожидалось %d игроков, получено %d", n, len(m.game.Players))
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestDurakModelClampsPlayerCount(t *testing.T) {
|
||
m := NewDurakModel(1)
|
||
if len(m.game.Players) != 2 {
|
||
t.Errorf("ожидалось ограничение снизу до 2 игроков, получено %d", len(m.game.Players))
|
||
}
|
||
m = NewDurakModel(10)
|
||
if len(m.game.Players) != 6 {
|
||
t.Errorf("ожидалось ограничение сверху до 6 игроков, получено %d", len(m.game.Players))
|
||
}
|
||
}
|
||
|
||
func TestDurakModelAutoPassesHumanWithNothingToThrow(t *testing.T) {
|
||
m := NewDurakModel(3)
|
||
m.humanIdx = 2 // человек — третий игрок (индекс 2), сам атакующий
|
||
setupBout(m.game, 2, 1, Spades, map[int][]Card{
|
||
0: {dc(Ten, Diamonds)}, // сторонний бот, роли не играет
|
||
1: {dc(Seven, Clubs), dc(Jack, Diamonds)}, // защищающийся
|
||
2: {dc(Six, Clubs), dc(Nine, Hearts)}, // атакует шестёркой, девятка не подходит по рангу
|
||
})
|
||
if err := m.game.ThrowIn(dc(Six, Clubs)); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if err := m.game.Defend(dc(Seven, Clubs)); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if m.game.Phase != DurakPhaseThrowIn || m.game.CurrentThrower() != m.humanIdx {
|
||
t.Fatalf("тест предполагает, что сейчас очередь человека подкидывать (атакующий ходит первым в очереди)")
|
||
}
|
||
|
||
m.maybeScheduleBot()
|
||
|
||
if m.game.CurrentThrower() == m.humanIdx && m.game.Phase == DurakPhaseThrowIn {
|
||
t.Errorf("человека без подходящих карт должно было автоматически пропустить (пас), а не оставить ждать ввода")
|
||
}
|
||
}
|
||
|
||
func TestDurakModelDoesNotAutoPassHumanWithThrowableCard(t *testing.T) {
|
||
m := NewDurakModel(3)
|
||
m.humanIdx = 2
|
||
setupBout(m.game, 2, 1, Spades, map[int][]Card{
|
||
0: {dc(Ten, Diamonds)},
|
||
1: {dc(Seven, Clubs), dc(Jack, Diamonds)},
|
||
2: {dc(Six, Clubs), dc(Six, Hearts)}, // вторая шестёрка подходит по рангу
|
||
})
|
||
if err := m.game.ThrowIn(dc(Six, Clubs)); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if err := m.game.Defend(dc(Seven, Clubs)); err != nil {
|
||
t.Fatalf("неожиданная ошибка: %v", err)
|
||
}
|
||
if m.game.Phase != DurakPhaseThrowIn || m.game.CurrentThrower() != m.humanIdx {
|
||
t.Fatalf("тест предполагает, что сейчас очередь человека подкидывать")
|
||
}
|
||
|
||
m.maybeScheduleBot()
|
||
|
||
if m.game.CurrentThrower() != m.humanIdx || m.game.Phase != DurakPhaseThrowIn {
|
||
t.Errorf("у человека есть подходящая карта — его очередь не должна была автоматически пропуститься")
|
||
}
|
||
}
|
||
|
||
func TestDurakModelThrowInViaKeys(t *testing.T) {
|
||
m := NewDurakModel(2)
|
||
setupBout(m.game, m.humanIdx, 1-m.humanIdx, m.game.TrumpSuit, map[int][]Card{
|
||
m.humanIdx: {c(Six, Clubs)},
|
||
})
|
||
m.cursor = 0
|
||
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(DurakModel)
|
||
if m.isError {
|
||
t.Fatalf("неожиданная ошибка подкидывания: %s", m.message)
|
||
}
|
||
if m.game.Phase != DurakPhaseDefend {
|
||
t.Fatalf("ожидалась фаза защиты после подкидывания")
|
||
}
|
||
if len(m.game.Table) != 1 {
|
||
t.Fatalf("ожидалась 1 карта на столе")
|
||
}
|
||
}
|
||
|
||
func TestDurakModelDefendViaKeys(t *testing.T) {
|
||
m := NewDurakModel(2)
|
||
setupBout(m.game, 1-m.humanIdx, m.humanIdx, Spades, map[int][]Card{
|
||
1 - m.humanIdx: {c(Six, Clubs)},
|
||
m.humanIdx: {c(Seven, Clubs), c(Ten, Diamonds)}, // запасная карта, чтобы cap>1
|
||
})
|
||
if err := m.game.ThrowIn(c(Six, Clubs)); err != nil {
|
||
t.Fatalf("не удалось подготовить стол: %v", err)
|
||
}
|
||
m.cursor = 0
|
||
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(DurakModel)
|
||
if m.isError {
|
||
t.Fatalf("неожиданная ошибка защиты: %s", m.message)
|
||
}
|
||
if len(m.game.Table) == 0 || m.game.Table[0].Defend == nil {
|
||
t.Fatalf("карта на столе должна быть отбита")
|
||
}
|
||
}
|
||
|
||
func TestDurakModelTakeViaKeys(t *testing.T) {
|
||
m := NewDurakModel(2)
|
||
setupBout(m.game, 1-m.humanIdx, m.humanIdx, Spades, map[int][]Card{
|
||
1 - m.humanIdx: {c(Eight, Clubs)},
|
||
m.humanIdx: {c(Six, Hearts)}, // не бьёт
|
||
})
|
||
if err := m.game.ThrowIn(c(Eight, Clubs)); err != nil {
|
||
t.Fatalf("не удалось подготовить стол: %v", err)
|
||
}
|
||
|
||
next, _ := m.Update(key("t"))
|
||
m = next.(DurakModel)
|
||
if m.isError {
|
||
t.Fatalf("неожиданная ошибка взятия: %s", m.message)
|
||
}
|
||
if !hasCard(m.game.Players[m.humanIdx].Hand, c(Eight, Clubs)) {
|
||
t.Fatalf("после взятия карта со стола должна была оказаться в руке")
|
||
}
|
||
}
|
||
|
||
func TestDurakModelPassViaKeys(t *testing.T) {
|
||
m := NewDurakModel(2)
|
||
// у человека (игрока 0) достаточно карт на руке защищающегося,
|
||
// чтобы после защиты лимит атаки не был исчерпан и наступила
|
||
// фаза подкидывания, где единственный потенциальный подкидывающий
|
||
// (при 2 игроках) — сам атакующий, то есть человек
|
||
setupBout(m.game, m.humanIdx, 1-m.humanIdx, Spades, map[int][]Card{
|
||
m.humanIdx: {c(Six, Clubs), c(Nine, Hearts)},
|
||
1 - m.humanIdx: {c(Seven, Clubs), c(Ten, Diamonds)},
|
||
})
|
||
if err := m.game.ThrowIn(c(Six, Clubs)); err != nil {
|
||
t.Fatalf("ошибка подготовки: %v", err)
|
||
}
|
||
if err := m.game.Defend(c(Seven, Clubs)); err != nil {
|
||
t.Fatalf("ошибка подготовки: %v", err)
|
||
}
|
||
if m.game.Phase != DurakPhaseThrowIn {
|
||
t.Fatalf("ожидалась фаза подкидывания, получено %v", m.game.Phase)
|
||
}
|
||
|
||
next, _ := m.Update(key("p"))
|
||
m = next.(DurakModel)
|
||
if m.isError {
|
||
t.Fatalf("неожиданная ошибка паса: %s", m.message)
|
||
}
|
||
if len(m.game.Table) != 0 {
|
||
t.Fatalf("стол должен был очиститься после единственного паса (2 игрока)")
|
||
}
|
||
}
|
||
|
||
func TestDurakModelOtherPlayerThrowInViaKeys(t *testing.T) {
|
||
// человек — третий игрок (не атакующий и не защищающийся),
|
||
// проверяем, что именно ему через TUI дают возможность подкинуть
|
||
m := NewDurakModel(3)
|
||
humanIdx := m.humanIdx
|
||
var otherIdx int
|
||
for i := range m.game.Players {
|
||
if i != humanIdx {
|
||
otherIdx = i
|
||
break
|
||
}
|
||
}
|
||
var thirdIdx int
|
||
for i := range m.game.Players {
|
||
if i != humanIdx && i != otherIdx {
|
||
thirdIdx = i
|
||
break
|
||
}
|
||
}
|
||
setupBout(m.game, otherIdx, thirdIdx, Spades, map[int][]Card{
|
||
otherIdx: {c(Six, Clubs)},
|
||
thirdIdx: {c(Seven, Clubs), c(Ten, Diamonds), c(Jack, Diamonds)},
|
||
humanIdx: {c(Six, Hearts)},
|
||
})
|
||
if err := m.game.ThrowIn(c(Six, Clubs)); err != nil {
|
||
t.Fatalf("ошибка подготовки: %v", err)
|
||
}
|
||
if err := m.game.Defend(c(Seven, Clubs)); err != nil {
|
||
t.Fatalf("ошибка подготовки: %v", err)
|
||
}
|
||
if m.game.CurrentThrower() != otherIdx {
|
||
t.Fatalf("первым в очереди подкидывающих должен быть атакующий раунда")
|
||
}
|
||
|
||
// атакующий (бот) пасует программно, чтобы дошла очередь до человека
|
||
if err := m.game.Pass(); err != nil {
|
||
t.Fatalf("ошибка подготовки паса: %v", err)
|
||
}
|
||
if m.game.CurrentThrower() != humanIdx {
|
||
t.Fatalf("ожидалось, что очередь дошла до человека (%d), получено %d", humanIdx, m.game.CurrentThrower())
|
||
}
|
||
|
||
m.cursor = 0
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(DurakModel)
|
||
if m.isError {
|
||
t.Fatalf("неожиданная ошибка подкидывания человеком: %s", m.message)
|
||
}
|
||
if len(m.game.Table) != 2 {
|
||
t.Fatalf("ожидалось 2 карты на столе после подкидывания человеком")
|
||
}
|
||
}
|
||
|
||
func TestDurakModelQReturnsToMenu(t *testing.T) {
|
||
m := NewDurakModel(2)
|
||
next, _ := m.Update(key("q"))
|
||
m = next.(DurakModel)
|
||
if !m.backToMenu {
|
||
t.Fatalf("ожидался флаг backToMenu после 'q'")
|
||
}
|
||
if m.quitting {
|
||
t.Fatalf("'q' не должен закрывать всё приложение")
|
||
}
|
||
}
|
||
|
||
// TestDurakModelFullAutoPlaythrough прогоняет TUI-модель Дурака от
|
||
// начала до конца, где "человек" отыгрывает свои ходы через
|
||
// реальные нажатия клавиш (используя ту же логику, что и DurakBot),
|
||
// а не напрямую через DurakGameState — проверяем весь путь через
|
||
// Update целиком для нескольких вариантов числа игроков.
|
||
func TestDurakModelFullAutoPlaythrough(t *testing.T) {
|
||
for n := 2; n <= 6; n++ {
|
||
m := NewDurakModel(n)
|
||
const maxSteps = 10000
|
||
steps := 0
|
||
for m.game.Phase != DurakPhaseGameOver && steps < maxSteps {
|
||
steps++
|
||
if m.currentActor() != m.humanIdx {
|
||
next, _ := m.Update(botMoveMsg{})
|
||
m = next.(DurakModel)
|
||
continue
|
||
}
|
||
|
||
selectCard := func(card Card) {
|
||
for i, hc := range m.currentHand() {
|
||
if hc == card {
|
||
m.cursor = i
|
||
break
|
||
}
|
||
}
|
||
}
|
||
|
||
switch m.game.Phase {
|
||
case DurakPhaseAttack:
|
||
card, _ := m.bot.DecideAttack(m.game)
|
||
selectCard(card)
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(DurakModel)
|
||
case DurakPhaseDefend:
|
||
if card, ok := m.bot.DecideDefend(m.game); ok {
|
||
selectCard(card)
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(DurakModel)
|
||
} else {
|
||
next, _ := m.Update(key("t"))
|
||
m = next.(DurakModel)
|
||
}
|
||
case DurakPhaseThrowIn:
|
||
if card, ok := m.bot.DecideThrowOrPass(m.game); ok {
|
||
selectCard(card)
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(DurakModel)
|
||
} else {
|
||
next, _ := m.Update(key("p"))
|
||
m = next.(DurakModel)
|
||
}
|
||
}
|
||
}
|
||
if steps >= maxSteps {
|
||
t.Fatalf("игроков %d: партия через TUI не завершилась за %d шагов", n, steps)
|
||
}
|
||
if m.game.Result == nil {
|
||
t.Fatalf("игроков %d: партия завершилась без результата", n)
|
||
}
|
||
}
|
||
}
|