package main import ( "strings" "testing" "github.com/charmbracelet/lipgloss" "github.com/muesli/termenv" ) func TestChessModelSelectAndMoveViaKeys(t *testing.T) { m := NewChessModel(0) m.cursor = ChessPos{Row: 6, Col: 4} next, _ := m.Update(key("enter")) m = next.(ChessModel) if m.isError { t.Fatalf("неожиданная ошибка выбора: %s", m.message) } if m.selected == nil { t.Fatalf("ожидался выбор фигуры") } m.cursor = ChessPos{Row: 4, Col: 4} next, _ = m.Update(key("enter")) m = next.(ChessModel) if m.isError { t.Fatalf("неожиданная ошибка хода: %s", m.message) } if m.game.Board[4][4] == nil { t.Fatalf("пешка должна была переместиться на e4") } if m.selected != nil { t.Errorf("выбор должен был сброситься после хода") } } func TestChessModelCastlingViaKeys(t *testing.T) { m := NewChessModel(0) m.game = &ChessGameState{Turn: ChessWhite} m.game.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing} m.game.Board[7][7] = &ChessPiece{Color: ChessWhite, Type: ChessRook} m.game.Board[0][4] = &ChessPiece{Color: ChessBlack, Type: ChessKing} m.cursor = ChessPos{Row: 7, Col: 4} next, _ := m.Update(key("enter")) m = next.(ChessModel) if m.isError { t.Fatalf("неожиданная ошибка выбора короля: %s", m.message) } m.cursor = ChessPos{Row: 7, Col: 6} next, _ = m.Update(key("enter")) m = next.(ChessModel) if m.isError { t.Fatalf("неожиданная ошибка рокировки: %s", m.message) } if m.game.Board[7][6] == nil || m.game.Board[7][6].Type != ChessKing { t.Errorf("король должен был оказаться на g1") } if m.game.Board[7][5] == nil || m.game.Board[7][5].Type != ChessRook { t.Errorf("ладья должна была оказаться на f1") } } // TestChessModelCursorVisibleOnLegalDestination проверяет исправление // бага: раньше подсветка "легальный ход" рендерилась поверх курсора, // делая его невидимым, стоило навести курсор на клетку, куда можно // сходить (именно эта клетка обычно и нужна пользователю). // TestChessCursorOnPieceUsesOneCombinedStyle — тот же баг и та же // проверка, что и в Шашках: цвет фигуры и фон клетки должны // составлять один комбинированный стиль, а не вложенные Render(). func TestChessCursorOnPieceUsesOneCombinedStyle(t *testing.T) { oldProfile := lipgloss.ColorProfile() lipgloss.SetColorProfile(termenv.ANSI256) defer lipgloss.SetColorProfile(oldProfile) piece := &ChessPiece{Color: ChessBlack, Type: ChessKnight} text := chessGlyphChar(piece) fg, ok := chessPieceForegroundColor(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 TestChessModelCursorVisibleOnLegalDestination(t *testing.T) { oldProfile := lipgloss.ColorProfile() lipgloss.SetColorProfile(termenv.ANSI256) defer lipgloss.SetColorProfile(oldProfile) m := NewChessModel(0) m.cursor = ChessPos{Row: 6, Col: 4} next, _ := m.Update(key("enter")) // выбираем пешку e2 m = next.(ChessModel) m.cursor = ChessPos{Row: 4, Col: 4} // e4 — легальный ход этой пешки dests := m.legalDestinationSet() if !dests[m.cursor] { t.Fatalf("тест предполагает, что курсор наведён именно на легальный ход") } board := m.renderBoard() cursorTag := checkersCursorStyle.Render("X") // вытаскиваем именно код фона курсора из отрендеренной "X" start := strings.Index(cursorTag, "\x1b[") end := strings.Index(cursorTag, "m") cursorColorCode := cursorTag[start : end+1] if !strings.Contains(board, cursorColorCode) { t.Errorf("цвет курсора должен присутствовать в отрисовке доски даже на клетке легального хода") } } func TestChessModelCursorMovement(t *testing.T) { m := NewChessModel(0) m.cursor = ChessPos{Row: 4, Col: 4} next, _ := m.Update(key("up")) m = next.(ChessModel) if m.cursor.Row != 3 { t.Errorf("курсор должен был сдвинуться вверх") } next, _ = m.Update(key("right")) m = next.(ChessModel) if m.cursor.Col != 5 { t.Errorf("курсор должен был сдвинуться вправо") } } func TestChessModelEscClearsSelection(t *testing.T) { m := NewChessModel(0) m.cursor = ChessPos{Row: 6, Col: 4} next, _ := m.Update(key("enter")) m = next.(ChessModel) if m.selected == nil { t.Fatalf("ожидался выбор") } next, _ = m.Update(key("esc")) m = next.(ChessModel) if m.selected != nil { t.Errorf("esc должен был сбросить выбор") } } func TestChessModelCannotSelectEmptySquare(t *testing.T) { m := NewChessModel(0) m.cursor = ChessPos{Row: 4, Col: 4} next, _ := m.Update(key("enter")) m = next.(ChessModel) if !m.isError { t.Fatalf("ожидалась ошибка выбора пустой клетки") } } func TestChessModelQReturnsToMenu(t *testing.T) { m := NewChessModel(0) next, _ := m.Update(key("q")) m = next.(ChessModel) if !m.backToMenu { t.Fatalf("ожидался флаг backToMenu после 'q'") } if m.quitting { t.Fatalf("'q' не должен закрывать всё приложение") } } func TestChessModelNewGameAfterResult(t *testing.T) { m := NewChessModel(0) m.game.Result = &ChessResult{Winner: ChessWhite, Checkmate: true} next, _ := m.Update(key("n")) m = next.(ChessModel) if m.game.Result != nil { t.Errorf("новая партия не должна была начаться с результатом") } } // TestChessModelFullAutoPlaythrough прогоняет TUI-модель от начала // до конца, где "человек" отыгрывает свои ходы через бота // (небольшая глубина — ради скорости), проверяя, что весь путь // через Update не паникует и партия завершается. func TestChessModelFullAutoPlaythrough(t *testing.T) { m := NewChessModel(1) helperBot := ChessBot{Depth: 1} const maxSteps = 600 for i := 0; i < maxSteps && m.game.Result == nil; i++ { if m.game.Turn != m.human { next, _ := m.Update(botMoveMsg{}) m = next.(ChessModel) continue } mv, ok := helperBot.DecideMove(m.game) if !ok { break } m.cursor = mv.From next, _ := m.Update(key("enter")) m = next.(ChessModel) if m.isError { t.Fatalf("неожиданная ошибка выбора хода человеком: %s", m.message) } m.cursor = mv.To next, _ = m.Update(key("enter")) m = next.(ChessModel) if m.isError { t.Fatalf("неожиданная ошибка хода человеком: %s", m.message) } } if m.game.Result == nil { t.Fatalf("партия через TUI не завершилась за %d шагов", maxSteps) } }