266 lines
9.4 KiB
Go
266 lines
9.4 KiB
Go
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)
|
||
}
|
||
}
|