279 lines
9.5 KiB
Go
279 lines
9.5 KiB
Go
package main
|
||
|
||
import (
|
||
"testing"
|
||
|
||
tea "github.com/charmbracelet/bubbletea"
|
||
)
|
||
|
||
// key — маленький хелпер для конструирования tea.KeyMsg, которые
|
||
// соответствуют строкам, разбираемым в handleActionKey/handleDrawKey.
|
||
func key(s string) tea.KeyMsg {
|
||
switch s {
|
||
case "left":
|
||
return tea.KeyMsg{Type: tea.KeyLeft}
|
||
case "right":
|
||
return tea.KeyMsg{Type: tea.KeyRight}
|
||
case "up":
|
||
return tea.KeyMsg{Type: tea.KeyUp}
|
||
case "down":
|
||
return tea.KeyMsg{Type: tea.KeyDown}
|
||
case "esc":
|
||
return tea.KeyMsg{Type: tea.KeyEsc}
|
||
case " ":
|
||
return tea.KeyMsg{Type: tea.KeySpace}
|
||
case "enter":
|
||
return tea.KeyMsg{Type: tea.KeyEnter}
|
||
case "ctrl+c":
|
||
return tea.KeyMsg{Type: tea.KeyCtrlC}
|
||
default:
|
||
return tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune(s)}
|
||
}
|
||
}
|
||
|
||
func TestTUIDrawAndDiscardCycle(t *testing.T) {
|
||
m := NewModel()
|
||
// делаем раунд предсказуемым: у человека маленькая рука без
|
||
// готовых комбинаций
|
||
m.game.Players[0].Hand = []Card{c(Two, Clubs), c(Nine, Hearts), c(King, Spades)}
|
||
m.game.CurrentPlayer = 0
|
||
m.game.Phase = PhaseDraw
|
||
|
||
next, _ := m.Update(key("d"))
|
||
m = next.(Model)
|
||
if m.game.Phase != PhaseAction {
|
||
t.Fatalf("после взятия карты ожидалась фаза PhaseAction, получена %v", m.game.Phase)
|
||
}
|
||
if len(m.game.current().Hand) != 4 {
|
||
t.Fatalf("ожидалось 4 карты после взятия, получено %d", len(m.game.current().Hand))
|
||
}
|
||
|
||
next, _ = m.Update(key("enter"))
|
||
m = next.(Model)
|
||
if m.game.CurrentPlayer == m.humanIdx && m.game.Phase != PhaseGameOver {
|
||
t.Fatalf("после сброса ход должен был перейти к боту")
|
||
}
|
||
}
|
||
|
||
func TestTUILayMeldViaKeys(t *testing.T) {
|
||
m := NewModel()
|
||
m.game.Players[0].Hand = []Card{
|
||
c(Seven, Clubs), c(Seven, Hearts), c(Seven, Spades), c(Two, Diamonds),
|
||
}
|
||
m.game.CurrentPlayer = 0
|
||
m.game.Phase = PhaseAction
|
||
|
||
// отмечаем три семёрки пробелом, двигая курсор
|
||
for i := 0; i < 3; i++ {
|
||
next, _ := m.Update(key(" "))
|
||
m = next.(Model)
|
||
if i < 2 {
|
||
next, _ = m.Update(key("right"))
|
||
m = next.(Model)
|
||
}
|
||
}
|
||
if len(m.staged) != 3 {
|
||
t.Fatalf("ожидалось 3 отмеченные карты, получено %d", len(m.staged))
|
||
}
|
||
|
||
next, _ := m.Update(key("m"))
|
||
m = next.(Model)
|
||
if m.isError {
|
||
t.Fatalf("неожиданная ошибка при выкладке комбинации: %s", m.message)
|
||
}
|
||
if len(m.game.Table) != 1 {
|
||
t.Fatalf("ожидалась 1 комбинация на столе, получено %d", len(m.game.Table))
|
||
}
|
||
if len(m.game.current().Hand) != 1 {
|
||
t.Fatalf("в руке должна была остаться 1 карта, получено %d", len(m.game.current().Hand))
|
||
}
|
||
if len(m.staged) != 0 {
|
||
t.Fatalf("отметки должны сброситься после выкладки комбинации")
|
||
}
|
||
}
|
||
|
||
func TestTUILayMeldRejectsFewerThanThree(t *testing.T) {
|
||
m := NewModel()
|
||
m.game.Players[0].Hand = []Card{c(Seven, Clubs), c(Seven, Hearts), c(Two, Diamonds)}
|
||
m.game.CurrentPlayer = 0
|
||
m.game.Phase = PhaseAction
|
||
|
||
next, _ := m.Update(key(" "))
|
||
m = next.(Model)
|
||
next, _ = m.Update(key("m"))
|
||
m = next.(Model)
|
||
if !m.isError {
|
||
t.Fatalf("ожидалась ошибка при попытке выложить меньше 3 карт")
|
||
}
|
||
}
|
||
|
||
func TestTUIAddToMeld(t *testing.T) {
|
||
m := NewModel()
|
||
// комбинацию на столе готовит бот (игрок 1) — напрямую через
|
||
// GameState, это лишь подготовка сцены для теста
|
||
m.game.Players[1].Hand = []Card{c(Seven, Clubs), c(Seven, Hearts), c(Seven, Spades)}
|
||
m.game.CurrentPlayer = 1
|
||
m.game.Phase = PhaseAction
|
||
if err := m.game.LayNewMeld(m.game.Players[1].Hand); err != nil {
|
||
t.Fatalf("не удалось подготовить стол: %v", err)
|
||
}
|
||
|
||
// а вот подкладывает карту уже человек — через реальный путь
|
||
// TUI (Update + нажатие клавиши), именно это здесь и проверяем
|
||
m.game.Players[0].Hand = []Card{c(Seven, Diamonds)}
|
||
m.game.CurrentPlayer = 0
|
||
m.game.Phase = PhaseAction
|
||
m.cursor = 0
|
||
next, _ := m.Update(key("1"))
|
||
m = next.(Model)
|
||
if m.isError {
|
||
t.Fatalf("неожиданная ошибка подкладки: %s", m.message)
|
||
}
|
||
if len(m.game.Table[0].Cards) != 4 {
|
||
t.Fatalf("ожидалось 4 карты в комбинации, получено %d", len(m.game.Table[0].Cards))
|
||
}
|
||
}
|
||
|
||
func TestTUIDropSucceeds(t *testing.T) {
|
||
m := NewModel()
|
||
m.game.Players[0].Hand = []Card{c(Two, Clubs)}
|
||
m.game.Players[1].Hand = []Card{c(King, Hearts)}
|
||
m.game.Players[2].Hand = []Card{c(Queen, Spades)}
|
||
m.game.Players[3].Hand = []Card{c(King, Diamonds)}
|
||
m.game.CurrentPlayer = 0
|
||
m.game.Phase = PhaseDraw
|
||
|
||
next, _ := m.Update(key("k"))
|
||
m = next.(Model)
|
||
if m.isError {
|
||
t.Fatalf("неожиданная ошибка дропа: %s", m.message)
|
||
}
|
||
if m.game.Phase != PhaseGameOver || !m.game.Result.DropCall || m.game.Result.DropCaught {
|
||
t.Fatalf("ожидался успешный дроп: %+v", m.game.Result)
|
||
}
|
||
}
|
||
|
||
func TestTUIDropCaught(t *testing.T) {
|
||
m := NewModel()
|
||
m.game.Players[0].Hand = []Card{c(King, Hearts)}
|
||
m.game.Players[1].Hand = []Card{c(Ace, Clubs)} // реально ниже
|
||
m.game.Players[2].Hand = []Card{c(King, Spades)}
|
||
m.game.Players[3].Hand = []Card{c(King, Diamonds)}
|
||
m.game.CurrentPlayer = 0
|
||
m.game.Phase = PhaseDraw
|
||
|
||
next, _ := m.Update(key("k"))
|
||
m = next.(Model)
|
||
if m.isError {
|
||
t.Fatalf("неожиданная ошибка дропа: %s", m.message)
|
||
}
|
||
if !m.game.Result.DropCaught {
|
||
t.Fatalf("ожидался неудачный дроп (попался)")
|
||
}
|
||
}
|
||
|
||
func TestTUIDropOnlyInDrawPhase(t *testing.T) {
|
||
m := NewModel()
|
||
m.game.CurrentPlayer = 0
|
||
m.game.Phase = PhaseAction
|
||
|
||
next, _ := m.Update(key("k"))
|
||
m = next.(Model)
|
||
if m.game.Phase == PhaseGameOver {
|
||
t.Fatalf("дроп после взятия карты должен быть запрещён — клавиша 'k' здесь не должна ни на что влиять")
|
||
}
|
||
}
|
||
|
||
func TestTUIDeclareTonk(t *testing.T) {
|
||
m := NewModel()
|
||
m.game.Players[0].Hand = []Card{c(Ace, Clubs), c(Two, Hearts)}
|
||
m.game.CurrentPlayer = 0
|
||
m.game.Phase = PhaseAction
|
||
|
||
next, _ := m.Update(key("t"))
|
||
m = next.(Model)
|
||
if m.isError {
|
||
t.Fatalf("неожиданная ошибка объявления Тонка: %s", m.message)
|
||
}
|
||
if m.game.Phase != PhaseGameOver || !m.game.Result.TonkCall {
|
||
t.Fatalf("раунд должен был завершиться объявлением Тонка")
|
||
}
|
||
}
|
||
|
||
func TestTUINewRoundCarriesOverBalance(t *testing.T) {
|
||
m := NewModel()
|
||
m.game.Players[1].Hand = []Card{c(Ace, Clubs)}
|
||
m.game.CurrentPlayer = 1
|
||
m.game.Phase = PhaseAction
|
||
if err := m.game.Discard(c(Ace, Clubs)); err != nil {
|
||
t.Fatalf("не удалось завершить раунд для теста: %v", err)
|
||
}
|
||
if m.game.Phase != PhaseGameOver {
|
||
t.Fatalf("раунд должен был завершиться")
|
||
}
|
||
balanceAfterRound := m.game.Players[0].Balance
|
||
|
||
next, _ := m.Update(key("n"))
|
||
m = next.(Model)
|
||
if m.game.Phase == PhaseGameOver {
|
||
t.Fatalf("новый раунд должен был начаться")
|
||
}
|
||
// капитал переносится, но в новом раунде заново списывается
|
||
// анте — итоговый капитал должен быть меньше на размер ставки
|
||
want := balanceAfterRound - m.game.AnteAmount
|
||
if m.game.Players[0].Balance != want {
|
||
t.Fatalf("капитал должен был перенестись с учётом нового анте: ожидалось %d, получено %d",
|
||
want, m.game.Players[0].Balance)
|
||
}
|
||
}
|
||
|
||
// TestTUIFullAutoPlaythrough прогоняет TUI-модель от начала до
|
||
// конца, но подменяет человека ботом на уровне вызовов Update:
|
||
// вместо реальных нажатий клавиш скрипт запрашивает у SimpleBot
|
||
// решение и переводит его в нажатия клавиш. Так мы проверяем, что
|
||
// весь путь через Update (а не только через GameState напрямую)
|
||
// стабильно доводит партию до конца без паники и зависаний.
|
||
func TestTUIFullAutoPlaythrough(t *testing.T) {
|
||
m := NewModel()
|
||
|
||
const maxSteps = 2000
|
||
steps := 0
|
||
for m.game.Phase != PhaseGameOver && steps < maxSteps {
|
||
steps++
|
||
if m.game.CurrentPlayer != m.humanIdx {
|
||
next, _ := m.Update(botMoveMsg{})
|
||
m = next.(Model)
|
||
continue
|
||
}
|
||
|
||
// "человек" в этом тесте играет как простой бот, но через
|
||
// реальные нажатия клавиш, а не напрямую через GameState
|
||
switch m.game.Phase {
|
||
case PhaseDraw:
|
||
top, ok := m.game.PeekDiscard()
|
||
k := "d"
|
||
if ok && wouldFormMeld(m.game.current().Hand, top) {
|
||
k = "p"
|
||
}
|
||
next, _ := m.Update(key(k))
|
||
m = next.(Model)
|
||
case PhaseAction:
|
||
discard := chooseDiscardSimple(m.game.current().Hand)
|
||
for i, cardInHand := range m.game.current().Hand {
|
||
if cardInHand == discard {
|
||
m.cursor = i
|
||
break
|
||
}
|
||
}
|
||
next, _ := m.Update(key("enter"))
|
||
m = next.(Model)
|
||
}
|
||
}
|
||
|
||
if steps >= maxSteps {
|
||
t.Fatalf("партия через TUI не завершилась за %d шагов", maxSteps)
|
||
}
|
||
if m.game.Result == nil {
|
||
t.Fatalf("партия завершилась без результата")
|
||
}
|
||
}
|