go_games_collection/checkers_tui_test.go
2026-07-16 09:53:25 +03:00

266 lines
9.4 KiB
Go
Raw Permalink 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 (
"strings"
"testing"
"github.com/charmbracelet/lipgloss"
"github.com/muesli/termenv"
)
// TestCheckersModelCursorVisibleOnLegalDestination проверяет тот же
// баг, что и в Шахматах: курсор не должен теряться под подсветкой
// легального хода.
func TestCheckersModelCursorVisibleOnLegalDestination(t *testing.T) {
oldProfile := lipgloss.ColorProfile()
lipgloss.SetColorProfile(termenv.ANSI256)
defer lipgloss.SetColorProfile(oldProfile)
m := NewCheckersModel(0, ColorChoiceA)
m.cursor = CheckersPos{Row: 5, Col: 2}
next, _ := m.Update(key("enter")) // выбираем шашку
m = next.(CheckersModel)
dests := m.legalDestinationSet()
var destPos CheckersPos
for pos := range dests {
destPos = pos
break
}
m.cursor = destPos
board := m.renderBoard()
cursorTag := checkersCursorStyle.Render("X")
start := strings.Index(cursorTag, "\x1b[")
end := strings.Index(cursorTag, "m")
cursorColorCode := cursorTag[start : end+1]
if !strings.Contains(board, cursorColorCode) {
t.Errorf("цвет курсора должен присутствовать в отрисовке доски даже на клетке легального хода")
}
}
// TestCheckersCursorOnPieceUsesOneCombinedStyle проверяет исправление
// более глубокого бага: раньше цвет фигуры применялся ВНУТРИ уже
// готового Render() вызова, что при последующем оборачивании в фон
// клетки (курсор/подсветка) давало вложенные ANSI-последовательности
// с преждевременным сбросом — фон обрывался до конца клетки (тот же
// класс бага, что раньше был в отрисовке карт Тонка).
func TestCheckersCursorOnPieceUsesOneCombinedStyle(t *testing.T) {
oldProfile := lipgloss.ColorProfile()
lipgloss.SetColorProfile(termenv.ANSI256)
defer lipgloss.SetColorProfile(oldProfile)
piece := &CheckersPiece{Color: CheckersBlack}
text := pieceGlyphChar(piece)
fg, ok := pieceForegroundColor(piece)
if !ok {
t.Fatalf("ожидался цвет для непустой клетки")
}
combined := checkersCursorStyle.Foreground(fg).Bold(true)
rendered := combined.Render(" " + text + " ")
resets := strings.Count(rendered, "\x1b[0m")
if resets != 1 {
t.Errorf("ожидался ровно один сброс ANSI на всю клетку (единый стиль), получено %d: %q", resets, rendered)
}
opens := strings.Count(rendered, "\x1b[")
if opens != 2 {
t.Errorf("ожидалось ровно 2 вхождения ESC (открытие+закрытие, без вложенности), получено %d: %q", opens, rendered)
}
}
func TestCheckersModelSelectAndMoveViaKeys(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceA)
m.cursor = CheckersPos{Row: 5, Col: 2}
next, _ := m.Update(key("enter"))
m = next.(CheckersModel)
if m.isError {
t.Fatalf("неожиданная ошибка выбора: %s", m.message)
}
if m.selected == nil {
t.Fatalf("ожидался выбор шашки")
}
m.cursor = CheckersPos{Row: 4, Col: 1}
next, _ = m.Update(key("enter"))
m = next.(CheckersModel)
if m.isError {
t.Fatalf("неожиданная ошибка хода: %s", m.message)
}
if m.game.Board[4][1] == nil {
t.Fatalf("шашка должна была переместиться")
}
if m.selected != nil {
t.Errorf("выбор должен был сброситься после хода")
}
}
func TestCheckersModelCursorMovement(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceA)
m.cursor = CheckersPos{Row: 4, Col: 4}
next, _ := m.Update(key("up"))
m = next.(CheckersModel)
if m.cursor.Row != 3 {
t.Errorf("курсор должен был сдвинуться вверх")
}
next, _ = m.Update(key("right"))
m = next.(CheckersModel)
if m.cursor.Col != 5 {
t.Errorf("курсор должен был сдвинуться вправо")
}
}
func TestCheckersModelEscClearsSelection(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceA)
m.cursor = CheckersPos{Row: 5, Col: 2}
next, _ := m.Update(key("enter"))
m = next.(CheckersModel)
if m.selected == nil {
t.Fatalf("ожидался выбор")
}
next, _ = m.Update(key("esc"))
m = next.(CheckersModel)
if m.selected != nil {
t.Errorf("esc должен был сбросить выбор")
}
}
func TestCheckersModelCannotSelectEmptySquare(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceA)
m.cursor = CheckersPos{Row: 3, Col: 2}
next, _ := m.Update(key("enter"))
m = next.(CheckersModel)
if !m.isError {
t.Fatalf("ожидалась ошибка выбора пустой клетки")
}
}
func TestCheckersModelQReturnsToMenu(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceA)
next, _ := m.Update(key("q"))
m = next.(CheckersModel)
if !m.backToMenu {
t.Fatalf("ожидался флаг backToMenu после 'q'")
}
if m.quitting {
t.Fatalf("'q' не должен закрывать всё приложение")
}
}
func TestCheckersModelNewGameAfterResult(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceA)
m.game.Result = &CheckersResult{Winner: CheckersWhite}
next, _ := m.Update(key("n"))
m = next.(CheckersModel)
if m.game.Result != nil {
t.Errorf("новая партия не должна была начаться с результатом")
}
}
// TestCheckersModelFullAutoPlaythrough прогоняет TUI-модель от
// начала до конца, где "человек" отыгрывает свои ходы через бота
// (небольшая глубина — ради скорости), проверяя, что весь путь
// через Update не паникует и партия завершается.
func TestCheckersModelFullAutoPlaythrough(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceA)
m.bot = CheckersBot{Depth: 2}
helperBot := CheckersBot{Depth: 2}
const maxSteps = 400
for i := 0; i < maxSteps && m.game.Result == nil; i++ {
if m.game.Turn != m.human {
next, _ := m.Update(botMoveMsg{})
m = next.(CheckersModel)
continue
}
mv, ok := helperBot.DecideMove(m.game)
if !ok {
break
}
m.cursor = mv.From
next, _ := m.Update(key("enter"))
m = next.(CheckersModel)
if m.isError {
t.Fatalf("неожиданная ошибка выбора хода человеком: %s", m.message)
}
m.cursor = mv.To
next, _ = m.Update(key("enter"))
m = next.(CheckersModel)
if m.isError {
t.Fatalf("неожиданная ошибка хода человеком: %s", m.message)
}
}
if m.game.Result == nil {
t.Fatalf("партия через TUI не завершилась за %d шагов", maxSteps)
}
}
func TestNewCheckersModelRespectsColorChoiceWhite(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceA)
if m.human != CheckersWhite {
t.Errorf("при выборе ColorChoiceA человек должен был играть белыми, получено %v", m.human)
}
}
func TestNewCheckersModelRespectsColorChoiceBlack(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceB)
if m.human != CheckersBlack {
t.Errorf("при выборе ColorChoiceB человек должен был играть чёрными, получено %v", m.human)
}
}
func TestNewCheckersModelRandomColorGivesValidColor(t *testing.T) {
seenWhite, seenBlack := false, false
for i := 0; i < 50 && !(seenWhite && seenBlack); i++ {
m := NewCheckersModel(0, ColorChoiceRandom)
if m.human == CheckersWhite {
seenWhite = true
} else {
seenBlack = true
}
}
if !seenWhite || !seenBlack {
t.Errorf("случайный выбор цвета должен был давать оба варианта за 50 попыток: белые=%v чёрные=%v", seenWhite, seenBlack)
}
}
func TestCheckersBoardNotFlippedForWhite(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceA)
if m.cursor.Row != 5 {
t.Errorf("за белых курсор должен был стартовать в ряду 5 (своя сторона), получено %d", m.cursor.Row)
}
view := m.renderBoard()
lines := strings.Split(view, "\n")
if !strings.HasPrefix(lines[8], "1 ") {
t.Errorf("за белых нижний ряд доски должен был быть подписан 1, получено: %q", lines[8])
}
}
func TestCheckersBoardFlippedForBlack(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceB)
if m.cursor.Row != 2 {
t.Errorf("за чёрных курсор должен был стартовать в ряду 2 (своя сторона), получено %d", m.cursor.Row)
}
view := m.renderBoard()
lines := strings.Split(view, "\n")
if !strings.HasPrefix(lines[8], "8 ") {
t.Errorf("за чёрных нижний ряд доски должен был быть подписан 8 (развёрнуто), получено: %q", lines[8])
}
}
func TestCheckersCursorMovementFlippedForBlack(t *testing.T) {
m := NewCheckersModel(0, ColorChoiceB)
m.game.Turn = m.human
startRow := m.cursor.Row
next, _ := m.Update(key("up"))
m2 := next.(CheckersModel)
if m2.cursor.Row != startRow+1 {
t.Errorf("за чёрных 'вверх' должен был увеличивать Row (развёрнутая доска), получено %d (было %d)", m2.cursor.Row, startRow)
}
}