package main import "testing" func cp(r, c int) ChessPos { return ChessPos{Row: r, Col: c} } func TestNewChessGameSetup(t *testing.T) { g := NewChessGame() whiteCount, blackCount := 0, 0 for r := 0; r < 8; r++ { for c := 0; c < 8; c++ { p := g.Board[r][c] if p == nil { continue } if p.Color == ChessWhite { whiteCount++ } else { blackCount++ } } } if whiteCount != 16 || blackCount != 16 { t.Errorf("ожидалось по 16 фигур, получено white=%d black=%d", whiteCount, blackCount) } if g.Turn != ChessWhite { t.Errorf("первый ход должен быть за белыми") } if g.Board[7][4].Type != ChessKing || g.Board[7][3].Type != ChessQueen { t.Errorf("король и ферзь белых должны стоять на своих клетках") } } func TestPawnDoubleStepAndSingleStep(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[6][4] = &ChessPiece{Color: ChessWhite, Type: ChessPawn} if err := g.Move(cp(6, 4), cp(4, 4)); err != nil { t.Fatalf("неожиданная ошибка хода пешкой на 2: %v", err) } if g.EnPassantTarget == nil || *g.EnPassantTarget != cp(5, 4) { t.Errorf("ожидалась цель взятия на проходе (5,4)") } } func TestPawnCannotJumpOverPiece(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[6][4] = &ChessPiece{Color: ChessWhite, Type: ChessPawn} g.Board[5][4] = &ChessPiece{Color: ChessBlack, Type: ChessPawn} if err := g.Move(cp(6, 4), cp(4, 4)); err != ErrChessInvalidMove { t.Errorf("пешка не должна перепрыгивать через фигуру, получено: %v", err) } } func TestPawnCapturesDiagonally(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[6][4] = &ChessPiece{Color: ChessWhite, Type: ChessPawn} g.Board[5][5] = &ChessPiece{Color: ChessBlack, Type: ChessPawn} if err := g.Move(cp(6, 4), cp(5, 5)); err != nil { t.Fatalf("неожиданная ошибка взятия по диагонали: %v", err) } if g.Board[5][5].Color != ChessWhite { t.Errorf("белая пешка должна была оказаться на (5,5)") } } func TestEnPassantCapture(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[3][4] = &ChessPiece{Color: ChessWhite, Type: ChessPawn} g.Board[1][3] = &ChessPiece{Color: ChessBlack, Type: ChessPawn} g.Turn = ChessBlack if err := g.Move(cp(1, 3), cp(3, 3)); err != nil { t.Fatalf("неожиданная ошибка хода чёрной пешкой на 2: %v", err) } if g.EnPassantTarget == nil || *g.EnPassantTarget != cp(2, 3) { t.Fatalf("ожидалась цель взятия на проходе (2,3)") } if err := g.Move(cp(3, 4), cp(2, 3)); err != nil { t.Fatalf("неожиданная ошибка взятия на проходе: %v", err) } if g.Board[3][3] != nil { t.Errorf("взятая на проходе пешка должна была исчезнуть с (3,3)") } if g.Board[2][3] == nil || g.Board[2][3].Color != ChessWhite { t.Errorf("белая пешка должна была оказаться на (2,3)") } } func TestEnPassantOnlyAvailableImmediately(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[3][4] = &ChessPiece{Color: ChessWhite, Type: ChessPawn} g.Board[1][3] = &ChessPiece{Color: ChessBlack, Type: ChessPawn} g.Board[6][0] = &ChessPiece{Color: ChessWhite, Type: ChessPawn} g.Board[1][0] = &ChessPiece{Color: ChessBlack, Type: ChessPawn} g.Turn = ChessBlack if err := g.Move(cp(1, 3), cp(3, 3)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Move(cp(6, 0), cp(5, 0)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if err := g.Move(cp(1, 0), cp(2, 0)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } opts := g.LegalMovesFrom(cp(3, 4)) for _, o := range opts { if o.IsEnPassant { t.Errorf("право на взятие на проходе должно было сгореть") } } } func TestPromotionToQueen(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[1][0] = &ChessPiece{Color: ChessWhite, Type: ChessPawn} g.Board[0][7] = &ChessPiece{Color: ChessBlack, Type: ChessKing} g.Board[7][7] = &ChessPiece{Color: ChessWhite, Type: ChessKing} if err := g.Move(cp(1, 0), cp(0, 0)); err != nil { t.Fatalf("неожиданная ошибка превращения: %v", err) } if g.Board[0][0] == nil || g.Board[0][0].Type != ChessQueen { t.Errorf("пешка должна была превратиться в ферзя") } } func TestKnightMovesInLShape(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[4][4] = &ChessPiece{Color: ChessWhite, Type: ChessKnight} opts := g.LegalMovesFrom(cp(4, 4)) want := map[ChessPos]bool{ cp(2, 3): true, cp(2, 5): true, cp(3, 2): true, cp(3, 6): true, cp(5, 2): true, cp(5, 6): true, cp(6, 3): true, cp(6, 5): true, } if len(opts) != 8 { t.Fatalf("ожидалось 8 ходов коня из центра доски, получено %d", len(opts)) } for _, o := range opts { if !want[o.To] { t.Errorf("неожиданный ход коня на %v", o.To) } } } func TestBishopBlockedByOwnPiece(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[4][4] = &ChessPiece{Color: ChessWhite, Type: ChessBishop} g.Board[2][2] = &ChessPiece{Color: ChessWhite, Type: ChessPawn} opts := g.LegalMovesFrom(cp(4, 4)) for _, o := range opts { if o.To == cp(2, 2) || o.To == cp(1, 1) { t.Errorf("слон не должен проходить через свою же фигуру или вставать на неё") } } } func TestRookCapturesEnemyButStops(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[4][4] = &ChessPiece{Color: ChessWhite, Type: ChessRook} g.Board[4][6] = &ChessPiece{Color: ChessBlack, Type: ChessPawn} opts := g.LegalMovesFrom(cp(4, 4)) foundCapture, foundBeyond := false, false for _, o := range opts { if o.To == cp(4, 6) { foundCapture = true } if o.To == cp(4, 7) { foundBeyond = true } } if !foundCapture { t.Errorf("ладья должна была иметь возможность взять вражескую пешку") } if foundBeyond { t.Errorf("ладья не должна проходить сквозь взятую фигуру") } } func TestCastlingKingsideWhenClear(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing} g.Board[7][7] = &ChessPiece{Color: ChessWhite, Type: ChessRook} g.Board[0][4] = &ChessPiece{Color: ChessBlack, Type: ChessKing} if err := g.Move(cp(7, 4), cp(7, 6)); err != nil { t.Fatalf("неожиданная ошибка короткой рокировки: %v", err) } if g.Board[7][6] == nil || g.Board[7][6].Type != ChessKing { t.Errorf("король должен был оказаться на g1") } if g.Board[7][5] == nil || g.Board[7][5].Type != ChessRook { t.Errorf("ладья должна была оказаться на f1") } if g.Board[7][7] != nil { t.Errorf("исходная клетка ладьи должна была опустеть") } } func TestCastlingQueensideWhenClear(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing} g.Board[7][0] = &ChessPiece{Color: ChessWhite, Type: ChessRook} g.Board[0][4] = &ChessPiece{Color: ChessBlack, Type: ChessKing} if err := g.Move(cp(7, 4), cp(7, 2)); err != nil { t.Fatalf("неожиданная ошибка длинной рокировки: %v", err) } if g.Board[7][2] == nil || g.Board[7][2].Type != ChessKing { t.Errorf("король должен был оказаться на c1") } if g.Board[7][3] == nil || g.Board[7][3].Type != ChessRook { t.Errorf("ладья должна была оказаться на d1") } } func TestCastlingForbiddenAfterKingMoved(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing} g.Board[7][7] = &ChessPiece{Color: ChessWhite, Type: ChessRook} g.Board[0][4] = &ChessPiece{Color: ChessBlack, Type: ChessKing} g.WhiteKingMoved = true opts := g.LegalMovesFrom(cp(7, 4)) for _, o := range opts { if o.IsCastleKingside { t.Errorf("рокировка должна быть запрещена, если король уже ходил") } } } func TestCastlingForbiddenWhileInCheck(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing} g.Board[7][7] = &ChessPiece{Color: ChessWhite, Type: ChessRook} g.Board[0][4] = &ChessPiece{Color: ChessBlack, Type: ChessRook} // держит короля под шахом по вертикали e opts := g.LegalMovesFrom(cp(7, 4)) for _, o := range opts { if o.IsCastleKingside || o.IsCastleQueenside { t.Errorf("рокировка должна быть запрещена под шахом") } } } func TestCastlingForbiddenThroughAttackedSquare(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing} g.Board[7][7] = &ChessPiece{Color: ChessWhite, Type: ChessRook} g.Board[0][5] = &ChessPiece{Color: ChessBlack, Type: ChessRook} // бьёт f1 (клетка, через которую проходит король) opts := g.LegalMovesFrom(cp(7, 4)) for _, o := range opts { if o.IsCastleKingside { t.Errorf("рокировка должна быть запрещена, если король проходит через атакованную клетку") } } } func TestCannotMoveIntoCheck(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing} g.Board[0][3] = &ChessPiece{Color: ChessBlack, Type: ChessRook} if err := g.Move(cp(7, 4), cp(7, 3)); err != ErrChessInvalidMove { t.Errorf("нельзя вставать королём под шах, ожидалась ошибка, получено: %v", err) } } func TestPinnedPieceCannotMove(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing} g.Board[6][4] = &ChessPiece{Color: ChessWhite, Type: ChessRook} g.Board[0][4] = &ChessPiece{Color: ChessBlack, Type: ChessRook} opts := g.LegalMovesFrom(cp(6, 4)) for _, o := range opts { if o.To.Col != 4 { t.Errorf("связанная ладья не должна иметь возможность сойти с вертикали e, ход на %v", o.To) } } } func TestFoolsMateCheckmate(t *testing.T) { g := NewChessGame() moves := []struct{ from, to ChessPos }{ {cp(6, 5), cp(5, 5)}, // f2-f3 {cp(1, 4), cp(3, 4)}, // e7-e5 {cp(6, 6), cp(4, 6)}, // g2-g4 {cp(0, 3), cp(4, 7)}, // Qd8-h4# } for i, mv := range moves { if err := g.Move(mv.from, mv.to); err != nil { t.Fatalf("ход %d неожиданно не прошёл: %v", i, err) } } if g.Result == nil || !g.Result.Checkmate { t.Fatalf("ожидался мат ('детский мат'), получено: %+v", g.Result) } if g.Result.Winner != ChessBlack { t.Errorf("ожидался победитель — чёрные") } } func TestStalemateDraw(t *testing.T) { g := &ChessGameState{Turn: ChessBlack} g.Board[0][7] = &ChessPiece{Color: ChessBlack, Type: ChessKing} // h8 g.Board[2][6] = &ChessPiece{Color: ChessWhite, Type: ChessQueen} // g6 g.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing} // e1, далеко и не мешает if g.inCheck(ChessBlack) { t.Fatalf("в этой тестовой позиции чёрный король не должен быть под шахом") } if g.hasAnyLegalMove(ChessBlack) { t.Fatalf("в этой тестовой позиции у чёрных не должно быть легальных ходов") } g.checkGameOver() if g.Result == nil || !g.Result.Draw || g.Result.DrawReason != "stalemate" { t.Fatalf("ожидался пат, получено: %+v", g.Result) } } func TestInsufficientMaterialKingVsKing(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[0][0] = &ChessPiece{Color: ChessBlack, Type: ChessKing} g.Board[7][7] = &ChessPiece{Color: ChessWhite, Type: ChessKing} if !g.hasInsufficientMaterial() { t.Errorf("король против короля — недостаточно материала для мата") } } func TestSufficientMaterialWithRook(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[0][0] = &ChessPiece{Color: ChessBlack, Type: ChessKing} g.Board[7][7] = &ChessPiece{Color: ChessWhite, Type: ChessKing} g.Board[4][4] = &ChessPiece{Color: ChessWhite, Type: ChessRook} if g.hasInsufficientMaterial() { t.Errorf("с ладьёй на доске материала достаточно для мата") } } func TestChessCannotMoveOpponentPiece(t *testing.T) { g := &ChessGameState{Turn: ChessWhite} g.Board[1][4] = &ChessPiece{Color: ChessBlack, Type: ChessPawn} if err := g.Move(cp(1, 4), cp(2, 4)); err != ErrChessNoPiece { t.Errorf("ожидалась ошибка хода чужой фигурой, получено: %v", err) } } func TestNoProgressDrawLimit(t *testing.T) { g := &ChessGameState{Turn: ChessWhite, HalfmoveClock: maxChessHalfmovesWithoutProgress - 1} g.Board[7][4] = &ChessPiece{Color: ChessWhite, Type: ChessKing} g.Board[7][0] = &ChessPiece{Color: ChessWhite, Type: ChessRook} g.Board[0][4] = &ChessPiece{Color: ChessBlack, Type: ChessKing} g.Board[0][7] = &ChessPiece{Color: ChessBlack, Type: ChessRook} if err := g.Move(cp(7, 0), cp(6, 0)); err != nil { t.Fatalf("неожиданная ошибка: %v", err) } if g.Result == nil || !g.Result.Draw || g.Result.DrawReason != "no_progress" { t.Fatalf("ожидалась ничья по правилу отсутствия прогресса, получено: %+v", g.Result) } }