go_games_collection/chess_game_test.go

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