package main import ( "fmt" "strings" "testing" ) func TestOneOhOneModelDealsCorrectPlayerCount(t *testing.T) { for n := 2; n <= 6; n++ { m := NewOneOhOneModel(n) if len(m.game.Players) != n { t.Errorf("ожидалось %d игроков, получено %d", n, len(m.game.Players)) } } } func TestOneOhOneModelClampsPlayerCount(t *testing.T) { m := NewOneOhOneModel(1) if len(m.game.Players) != 2 { t.Errorf("ожидалось ограничение снизу до 2, получено %d", len(m.game.Players)) } m = NewOneOhOneModel(10) if len(m.game.Players) != 6 { t.Errorf("ожидалось ограничение сверху до 6, получено %d", len(m.game.Players)) } } func TestOneOhOneModelPlayCardViaKeys(t *testing.T) { m := NewOneOhOneModel(2) m.game.Discard = []Card{c(Nine, Clubs)} m.game.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи m.game.DeclaredSuit = nil m.game.Players[m.humanIdx].Hand = []Card{c(Ten, Clubs)} m.game.CurrentPlayerIdx = m.humanIdx m.game.Phase = OneOhOnePhasePlay m.cursor = 0 next, _ := m.Update(key("enter")) m = next.(OneOhOneModel) if m.isError { t.Fatalf("неожиданная ошибка: %s", m.message) } } func TestOneOhOneModelPendingEightVisibleInView(t *testing.T) { m := NewOneOhOneModel(3) suit := Diamonds m.game.PendingEightSuit = &suit view := m.View() if !strings.Contains(view, suit.String()) { t.Errorf("требование покрыть восьмёрку должно быть видно на экране, получено:\n%s", view) } } func TestOneOhOneModelNoPendingEightLineWhenNil(t *testing.T) { m := NewOneOhOneModel(3) m.game.PendingEightSuit = nil view := m.View() if strings.Contains(view, "покрыть восьмёрку") { t.Errorf("строка требования покрытия не должна показываться, когда PendingEightSuit == nil") } } func TestOneOhOneModelDeclaredSuitVisibleInView(t *testing.T) { m := NewOneOhOneModel(3) suit := Hearts m.game.DeclaredSuit = &suit view := m.View() if !strings.Contains(view, suit.String()) { t.Errorf("объявленная масть должна быть видна на экране, получено:\n%s", view) } } func TestOneOhOneModelNoDeclaredSuitLineWhenNil(t *testing.T) { m := NewOneOhOneModel(3) m.game.DeclaredSuit = nil view := m.View() if strings.Contains(view, "назвала масть") { t.Errorf("строка объявленной масти не должна показываться, когда DeclaredSuit == nil") } } func TestOneOhOneModelBotDeclaredSuitVisibleAfterQueen(t *testing.T) { m := NewOneOhOneModel(3) // подстраиваем ситуацию: сейчас ход бота, у него на руке дама, // и сразу после розыгрыша должна открыться фаза выбора масти botIdx := m.game.CurrentPlayerIdx if botIdx == m.humanIdx { botIdx = (botIdx + 1) % len(m.game.Players) m.game.CurrentPlayerIdx = botIdx } m.game.Players[botIdx].Hand = []Card{c(Queen, Spades), c(Two, Clubs)} m.game.Discard = []Card{c(King, Spades)} m.game.PendingEightSuit = nil m.game.DeclaredSuit = nil next, _ := m.Update(botMoveMsg{}) m = next.(OneOhOneModel) if m.game.Phase != OneOhOnePhaseChooseSuit { t.Fatalf("ожидалась фаза выбора масти после розыгрыша дамы, получено %v", m.game.Phase) } next, _ = m.Update(botMoveMsg{}) m = next.(OneOhOneModel) if m.game.DeclaredSuit == nil { t.Fatalf("бот должен был назвать масть") } view := m.View() if !strings.Contains(view, m.game.DeclaredSuit.String()) { t.Errorf("названная ботом масть должна быть видна человеку на экране, получено:\n%s", view) } } func TestOneOhOneModelDrawViaKeys(t *testing.T) { m := NewOneOhOneModel(2) m.game.Discard = []Card{c(Nine, Clubs)} m.game.PendingEightSuit = nil // сбрасываем возможное наследие случайной начальной раздачи m.game.DeclaredSuit = nil m.game.Players[m.humanIdx].Hand = []Card{c(Ten, Hearts)} // не подходит m.game.Stock = []Card{c(Two, Diamonds)} m.game.CurrentPlayerIdx = m.humanIdx m.game.Phase = OneOhOnePhasePlay next, _ := m.Update(key("d")) m = next.(OneOhOneModel) if m.isError { t.Fatalf("неожиданная ошибка: %s", m.message) } if len(m.game.Players[m.humanIdx].Hand) != 2 { t.Fatalf("рука должна была вырасти на 1 карту, получено %d карт", len(m.game.Players[m.humanIdx].Hand)) } } func TestOneOhOneModelQReturnsToMenu(t *testing.T) { m := NewOneOhOneModel(2) next, _ := m.Update(key("q")) m = next.(OneOhOneModel) if !m.backToMenu { t.Fatalf("ожидался флаг backToMenu после 'q'") } if m.quitting { t.Fatalf("'q' не должен закрывать всё приложение") } } // TestOneOhOneModelFullAutoPlaythrough прогоняет TUI-модель от // начала до конца матча, где "человек" отыгрывает свои ходы через // реальные нажатия клавиш (используя ту же логику, что и бот). func TestOneOhOneModelFullAutoPlaythrough(t *testing.T) { for n := 2; n <= 4; n++ { m := NewOneOhOneModel(n) const maxRounds = 100 round := 0 for ; round < maxRounds; round++ { const maxSteps = 200000 steps := 0 for m.game.Phase != OneOhOnePhaseRoundOver && steps < maxSteps { steps++ if m.game.CurrentPlayerIdx != m.humanIdx { next, _ := m.Update(botMoveMsg{}) m = next.(OneOhOneModel) continue } if m.game.Phase == OneOhOnePhaseChooseSuit { suit := m.bot.DecideSuit(m.game) next, _ := m.Update(key(fmt.Sprintf("%d", int(suit)+1))) m = next.(OneOhOneModel) continue } if card, ok := m.bot.DecideAction(m.game); ok { for i, hc := range m.currentHand() { if hc == card { m.cursor = i break } } next, _ := m.Update(key("enter")) m = next.(OneOhOneModel) } else { next, _ := m.Update(key("d")) m = next.(OneOhOneModel) } } if steps >= maxSteps { t.Fatalf("игроков %d, раунд %d: раунд через TUI не завершился за %d шагов", n, round, maxSteps) } if m.game.Result == nil { t.Fatalf("игроков %d, раунд %d: раунд завершился без результата", n, round) } if m.game.Result.MatchOver { break } next, _ := m.Update(key("n")) m = next.(OneOhOneModel) } if round >= maxRounds { t.Fatalf("игроков %d: матч не завершился за %d раундов", n, maxRounds) } } }