368 lines
14 KiB
Go
368 lines
14 KiB
Go
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)
|
||
}
|
||
}
|