Escrevendo um Engine para Xadrez – parte 13 – Refactoring e melhorias em Board

Publicado em 18/05/2011

0


Olá pessoal, como estamos?

No post anterior, começamos o desenvolvimento do modelo que representa uma posição de jogo: a classe Board. Mais que isso, indiquei um caminho claro de transição entre uma posição e outra através do método MakeMove.

No post de hoje, mostro, passo-a-passo, como foi a conclusão desse método (incluindo todo refactoring realizado).

Se você não está acompanhando a série desde o início, considere dar uma olhada nos posts anteriores. Além disso, para facilitar, considere pegar os fontes em https://github.com/elemarjr/StrongChess.

Nosso ponto de partida: o método MakeMove

Encerrei o post anterior mostrando como implementar parte da lógica da movimentação do peão. O método havia ficado assim:

public Board MakeMove(Move move)
{
    Square? enpassant = null;

    var moving = (this.IsWhiteTurn ? White : Black);
    var notmoving = (this.IsWhiteTurn ? Black : White);

    if (moving.GetPieceAt(move.From) == ChessPieces.Pawn)
    {
        bool isPromotion = 
            (move.To.Rank.Index == 7 || move.To.Rank.Index == 0);

        if (move.Type == MoveTypes.Normal && isPromotion)
            throw new InvalidMoveException(move, this);
                
        var isvalid = moving.Pawns
            .GetAllMoves(~Occupation, notmoving.Occupation, 
            this.Enpassant)
            .Count(m => m.From == move.From && m.To == move.To)
            > 0;

        if (!isvalid)
            throw new InvalidMoveException(move, this);

        var locations = moving.Pawns.Locations
            & (~move.From.AsBoard);

        if (!isPromotion)
            locations |= move.To.AsBoard;

        IPawns pawns = IsWhiteTurn ? 
            (IPawns) new WhitePawns(locations) :
            (IPawns) new BlackPawns(locations);

        if (move.From.File == move.To.File && 
            Math.Abs(move.From.Rank - move.To.Rank) > 1)
            enpassant = new Square(
                (move.From.Rank + move.To.Rank) / 2,
                move.From.File
                );

        var queens = moving.Queens;
        if (move.Type == MoveTypes.PawnToQueenPromotion)
            queens = new PieceSet<Queen>(
                moving.Queens.Locations |
                move.To.AsBoard
                );

        var bishops = moving.Bishops;
        if (move.Type == MoveTypes.PawnToBishopPromotion)
            bishops = new PieceSet<Bishop>(
                moving.Bishops.Locations |
                move.To.AsBoard
                );

        var knights = moving.Knights;
        if (move.Type == MoveTypes.PawnToKnightPromotion)
            knights = new PieceSet<Knight>(
                moving.Knights.Locations |
                move.To.AsBoard
                );

        var rooks = moving.Rooks;
        if (move.Type == MoveTypes.PawnToRookPromotion)
            rooks = new PieceSet<Rook>(
                moving.Rooks.Locations |
                move.To.AsBoard
                );

        moving = new Side(
            moving.KingLocation,
            queens,
            bishops,
            knights,
            rooks,
            pawns
            );

        Bitboard negative;
        if (move.To != Enpassant)
            negative = ~move.To.AsBoard;
        else if (move.To.Rank == 6)
            negative = ~(new Square(5, move.To.File).AsBoard);
        else 
            negative = ~(new Square(4, move.To.File).AsBoard);

        notmoving = new Side(
            notmoving.KingLocation,
            new PieceSet<Queen>(notmoving.Queens.Locations & negative),
            new PieceSet<Bishop>(notmoving.Bishops.Locations & negative),
            new PieceSet<Knight>(notmoving.Knights.Locations & negative),
            new PieceSet<Rook>(notmoving.Rooks.Locations & negative),
            (IsWhiteTurn ?
                (IPawns)new BlackPawns(notmoving.Pawns.Locations & negative)
                :
                (IPawns)new WhitePawns(notmoving.Pawns.Locations & negative)
                )
            );
    }
    else
    {
        throw new NotImplementedException(
            string.Format("There is no support to {0} moves", moving.GetPieceAt(move.From))
            );
    }

    var white = (this.IsWhiteTurn ? moving : notmoving);
    var black = (this.IsWhiteTurn ? notmoving : moving);
            
    return new Board(white, black, 0, !IsWhiteTurn, enpassant);
}

Explicando um pouco as coisas:

  • o método começa transferindo informações de “white” ou “black” para “moving” e “notmoving”, conforme quem tem “a vez”;
  • como só há suporte para movimentos de pões implementado (parcialmente), disparo uma exception para qualquer outra peça;
  • verifico se o peão chegou na “última linha”. Nesse caso, deveria ter informação de “promoção” (regras do xadrez…);
  • gero todas as movimentações de peão possíveis na posição. Se a movimentação que estamos tentando não estiver entre essas, disparo um exception (o movimento seria ilegal);
  • crio uma cópia do bitboard com a posição dos peões e removo o peão da posição de origem;
  • se não estiver ocorrendo uma promoção, coloco um peão na posição de destino;
  • recrio a estrutura de peões;
  • defino ou não uma casa de enpassant;
  • em caso de promoção, coloco a “peça nova” na posição de destino;
  • defino a posição atacada pelo peão, em caso de “tomada”;
  • retiro qualquer peça adversária que estivesse naquela posição;
  • crio uma “nova posição” (Board) com informações atualizadas.

Ufa! Lindo, mas complexo. Hora de refactoring.

Refactoring 1 – Extraindo o método de movimento do peão

A primeira decisão de refactoring que resolvi adotar foi extrair de MakeMove o método MakePawnMove que deve conter a lógica de movimentação do peão. O resultado ficou assim:

public Board MakeMove(Move move)
{
    Square? enpassant = null;

    var moving = (this.IsWhiteTurn ? White : Black);
    var notmoving = (this.IsWhiteTurn ? Black : White);

    switch (moving.GetPieceAt(move.From))
    {
        case ChessPieces.Pawn:
            MakePawnMove(move, ref enpassant, ref moving, ref notmoving);
            break;
        case ChessPieces.King:
        case ChessPieces.Queen:
        case ChessPieces.Bishop:
        case ChessPieces.Knight:
        case ChessPieces.Rook:
        case ChessPieces.None:
        default:
            throw new NotImplementedException(
                string.Format("There is no support to {0} moves", 
                moving.GetPieceAt(move.From))
            );
    }

    var white = (this.IsWhiteTurn ? moving : notmoving);
    var black = (this.IsWhiteTurn ? notmoving : moving);
            
    return new Board(white, black, 0, !IsWhiteTurn, enpassant);
}

private void MakePawnMove(Move move, 
    ref Square? enpassant, 
    ref Side moving, 
    ref Side notmoving)
{
    bool isPromotion =
        (move.To.Rank.Index == 7 || move.To.Rank.Index == 0);

    if (move.Type == MoveTypes.Normal && isPromotion)
        throw new InvalidMoveException(move, this);

    var isvalid = moving.Pawns
        .GetAllMoves(~Occupation, notmoving.Occupation,
        this.Enpassant)
        .Count(m => m.From == move.From && m.To == move.To)
        > 0;

    if (!isvalid)
        throw new InvalidMoveException(move, this);

    var locations = moving.Pawns.Locations
        & (~move.From.AsBoard);

    if (!isPromotion)
        locations |= move.To.AsBoard;

    IPawns pawns = IsWhiteTurn ?
        (IPawns)new WhitePawns(locations) :
        (IPawns)new BlackPawns(locations);

    if (move.From.File == move.To.File &&
        Math.Abs(move.From.Rank - move.To.Rank) > 1)
        enpassant = new Square(
            (move.From.Rank + move.To.Rank) / 2,
            move.From.File
            );

    var queens = moving.Queens;
    if (move.Type == MoveTypes.PawnToQueenPromotion)
        queens = new PieceSet<Queen>(
            moving.Queens.Locations |
            move.To.AsBoard
            );

    var bishops = moving.Bishops;
    if (move.Type == MoveTypes.PawnToBishopPromotion)
        bishops = new PieceSet<Bishop>(
            moving.Bishops.Locations |
            move.To.AsBoard
            );

    var knights = moving.Knights;
    if (move.Type == MoveTypes.PawnToKnightPromotion)
        knights = new PieceSet<Knight>(
            moving.Knights.Locations |
            move.To.AsBoard
            );

    var rooks = moving.Rooks;
    if (move.Type == MoveTypes.PawnToRookPromotion)
        rooks = new PieceSet<Rook>(
            moving.Rooks.Locations |
            move.To.AsBoard
            );

    moving = new Side(
        moving.KingLocation,
        queens,
        bishops,
        knights,
        rooks,
        pawns
        );

    Bitboard negative;
    if (move.To != Enpassant)
        negative = ~move.To.AsBoard;
    else if (move.To.Rank == 6)
        negative = ~(new Square(5, move.To.File).AsBoard);
    else
        negative = ~(new Square(4, move.To.File).AsBoard);

    notmoving = new Side(
        notmoving.KingLocation,
        new PieceSet<Queen>(notmoving.Queens.Locations & negative),
        new PieceSet<Bishop>(notmoving.Bishops.Locations & negative),
        new PieceSet<Knight>(notmoving.Knights.Locations & negative),
        new PieceSet<Rook>(notmoving.Rooks.Locations & negative),
        (IsWhiteTurn ?
            (IPawns)new BlackPawns(notmoving.Pawns.Locations & negative)
            :
            (IPawns)new WhitePawns(notmoving.Pawns.Locations & negative)
            )
        );
}

Repare que aproveitei para colocar um switch no lugar do If original. Isso, na hora, parecia fazer todo o sentido.

Refactoring 2 – Extraindo RemovePiece

Uma das coisas mais bacanas de praticar refactoring é dar mais coesão a nosso código. O primeiro refactoring que proponho é generaizar o código para retirar uma peça do tabuleiro. Vamos reproduzir o trecho original:

private void MakePawnMove(Move move, 
    ref Square? enpassant, 
    ref Side moving, 
    ref Side notmoving)
{
    // ...[ trecho não afetado removido 

    Bitboard negative;
    if (move.To != Enpassant)
        negative = ~move.To.AsBoard;
    else if (move.To.Rank == 6)
        negative = ~(new Square(5, move.To.File).AsBoard);
    else
        negative = ~(new Square(4, move.To.File).AsBoard);

    notmoving = new Side(
        notmoving.KingLocation,
        new PieceSet<Queen>(notmoving.Queens.Locations & negative),
        new PieceSet<Bishop>(notmoving.Bishops.Locations & negative),
        new PieceSet<Knight>(notmoving.Knights.Locations & negative),
        new PieceSet<Rook>(notmoving.Rooks.Locations & negative),
        (IsWhiteTurn ?
            (IPawns)new BlackPawns(notmoving.Pawns.Locations & negative)
            :
            (IPawns)new WhitePawns(notmoving.Pawns.Locations & negative)
            )
        );
}

Agora, depois do refactoring:

private void MakePawnMove(Move move, 
    ref Square? enpassant, 
    ref Side moving, 
    ref Side notmoving)
{
    // ...[ trecho não afetado removido 
    Square capturedSquare;
    if (move.To != Enpassant)
        capturedSquare = move.To;
    else if (move.To.Rank == 6)
        capturedSquare = new Square(5, move.To.File);
    else
        capturedSquare = new Square(4, move.To.File);

    notmoving = notmoving.RemovePieces(capturedSquare);
}

Extraí a retirada da peça para Side. Além disso, tornei mais “expressiva” a definição da casa com a peça a ser removida. Abaixo, a implementação de RemovingPieces.

public Side RemovePieces(IBoardUnit bu)
{
    var negative = ~bu.AsBoard;

    return  new Side(
        KingLocation,
        new PieceSet<Queen>(Queens.Locations & negative),
        new PieceSet<Bishop>(Bishops.Locations & negative),
        new PieceSet<Knight>(Knights.Locations & negative),
        new PieceSet<Rook>(Rooks.Locations & negative),
        (IsWhite ?
            (IPawns)new WhitePawns(Pawns.Locations & negative)
            :
            (IPawns)new BlackPawns(Pawns.Locations & negative)
            )
        );
}

Melhor!

Refactoring 2 – Promovendo a “retirada” da peça capturada para MakeMove

Considerando que sempre haverá necessidade de remover uma peça capturada, resolvi promover a retirada de peça, para o método principal. Observe como o método auxiliar GetCapturedSquare ajuda a ocultar complexidades desnecessárias do código.

public Board MakeMove(Move move)
{
    Square? enpassant = null;

    var moving = (this.IsWhiteTurn ? White : Black);
    var notmoving = (this.IsWhiteTurn ? Black : White);

    switch (moving.GetPieceAt(move.From))
    {
        case ChessPieces.Pawn:
            MakePawnMove(move, ref enpassant, ref moving, ref notmoving);
            break;
        case ChessPieces.King:
        case ChessPieces.Queen:
        case ChessPieces.Bishop:
        case ChessPieces.Knight:
        case ChessPieces.Rook:
        case ChessPieces.None:
        default:
            throw new NotImplementedException(
                string.Format("There is no support to {0} moves", 
                moving.GetPieceAt(move.From))
            );
    }

    notmoving = notmoving.RemovePieces(
        GetCapturedSquare(move)
        );

    var white = (this.IsWhiteTurn ? moving : notmoving);
    var black = (this.IsWhiteTurn ? notmoving : moving);
            
    return new Board(white, black, 0, !IsWhiteTurn, enpassant);
}

private Square GetCapturedSquare(Move move)
{
    if (move.To != Enpassant)
        return move.To;
            
    if (move.To.Rank == 6)
        return new Square(5, move.To.File);
            
    return new Square(4, move.To.File);
}

Refactoring 3 – Extraíndo a lógica para adicionar peças (em promoções)

Outro trecho de código bastante extenso em MakePawnMove é aquele que trata da promoção de uma peça. Observe:

private void MakePawnMove(Move move, 
    ref Square? enpassant, 
    ref Side moving, 
    ref Side notmoving)
{
	//...
    var queens = moving.Queens;
    if (move.Type == MoveTypes.PawnToQueenPromotion)
        queens = new PieceSet<Queen>(
            moving.Queens.Locations |
            move.To.AsBoard
            );

    var bishops = moving.Bishops;
    if (move.Type == MoveTypes.PawnToBishopPromotion)
        bishops = new PieceSet<Bishop>(
            moving.Bishops.Locations |
            move.To.AsBoard
            );

    var knights = moving.Knights;
    if (move.Type == MoveTypes.PawnToKnightPromotion)
        knights = new PieceSet<Knight>(
            moving.Knights.Locations |
            move.To.AsBoard
            );

    var rooks = moving.Rooks;
    if (move.Type == MoveTypes.PawnToRookPromotion)
        rooks = new PieceSet<Rook>(
            moving.Rooks.Locations |
            move.To.AsBoard
            );

    moving = new Side(
        moving.KingLocation,
        queens,
        bishops,
        knights,
        rooks,
        pawns
        );

  //...
}

Extraí toda essa lógica também para a Side. Repare como o código ficou:

private void MakePawnMove(Move move, 
    ref Square? enpassant, 
    ref Side moving, 
    ref Side notmoving)
{
    // ...
    bool isPromotion =
        (move.To.Rank.Index == 7 || move.To.Rank.Index == 0);

    if (isPromotion)
        moving = moving.AddPieces(move.Type, move.To.AsBoard);

    moving = new Side(
        moving.KingLocation,
        moving.Queens,
        moving.Bishops,
        moving.Knights,
        moving.Rooks,
        pawns
        );
    // ...
}

Mais inteligente, não? Segue implementação de AddPieces em Side:

public Side AddPieces(ChessPieces piece, Bitboard b)
{
    var queens = Queens;
    var bishops = Bishops;
    var knights = Knights;
    var rooks = Rooks;

    switch (piece)
    {
        case ChessPieces.Queen:
            queens = new PieceSet<Queen>(queens.Locations | b);
            break;
        case ChessPieces.Bishop:
            bishops = new PieceSet<Bishop>(bishops.Locations | b);
            break;
        case ChessPieces.Knight:
            knights = new PieceSet<Knight>(knights.Locations | b);
            break;
        case ChessPieces.Rook:
            rooks = new PieceSet<Rook>(rooks.Locations | b);
            break;
        default:
            throw new NotSupportedException();
    }

    return new Side(
        KingLocation,
        queens, bishops, knights,
        rooks, this.Pawns
        );
}

public Side AddPieces(MoveTypes type, Bitboard b)
{
    if (type == MoveTypes.PawnToQueenPromotion)
        return AddPieces(ChessPieces.Queen, b);

    if (type == MoveTypes.PawnToBishopPromotion)
        return AddPieces(ChessPieces.Bishop, b);
            
    if (type == MoveTypes.PawnToKnightPromotion)
        return AddPieces(ChessPieces.Knight, b);

    return AddPieces(ChessPieces.Rook, b);
}

Repare que agora esse código pode ser reaproveitado em outros contextos.

Refactoring 4 – Extraíndo a lógica para computar a posição do enpassant e para validação do movimento.

Também resolvi tirar do corpo principal do método o código que calcula Enpassant. Observe:

private static Square? ComputeEnpassantSquare(Move move)
{
            
    bool isPawnFirstTwoSquaresMove =
        (move.From.File == move.To.File &&
        Math.Abs(move.From.Rank - move.To.Rank) > 1);

    if (!isPawnFirstTwoSquaresMove) return null;
                
    return new Square(
        (move.From.Rank + move.To.Rank) / 2,
        move.From.File
        );
}

´

Trata-se de uma boa quantidade de código que não precisa estar no corpo de MakePawnMove. Também optei por remover o parâmetro referência de MakePawnMove optando por tratar do enpassant no método principal.

De forma semelhante, extraí o código que valida o movimento. Observe:

private void EnsureValidPawnMove(Move move, Side moving, Side notmoving)
{
    if (move.Type == MoveTypes.Normal)
        if (move.To.Rank.Index == 7 || move.To.Rank.Index == 0)
            throw new InvalidMoveException(move, this);

    var isvalid = moving.Pawns
        .GetAllMoves(~Occupation, notmoving.Occupation,
        this.Enpassant)
        .Count(m => m.From == move.From && m.To == move.To)
        > 0;

    if (!isvalid)
        throw new InvalidMoveException(move, this);
}

Enfim … chegamos a um código para MakePawnMove mais enxuto e fácil de entender. Observe:

private void MakePawnMove(Move move, 
            ref Side moving, 
            Side notmoving)
{
    EnsureValidPawnMove(move, moving, notmoving);

    var locations = moving.Pawns.Locations
        & (~move.From.AsBoard);

    if (move.Type == MoveTypes.Normal)
        locations |= move.To.AsBoard;
    else
        moving = moving.AddPieces(move.Type, move.To.AsBoard);

    IPawns pawns = IsWhiteTurn ?
        (IPawns)new WhitePawns(locations) :
        (IPawns)new BlackPawns(locations);

    moving = new Side(
        moving.KingLocation,
        moving.Queens,
        moving.Bishops,
        moving.Knights,
        moving.Rooks,
        pawns
        );
}

Bonito, não!?

Adicionando suporte para movimento de cavalos em Board

Agora que Board já tem suporte completo para peões, adicionemos suporte a outras peças, começando pelo cavalo. Eis os testes que planejei para agora:

[Test]
public void MakeMove_G1F3_RemovesKnightFromG1()
{
    // arrange
    var board = Board.NewGame();
    // act
    var result = board.MakeMove(new Move("G1", "F3"));
    // assert
    result.White.Knights.Locations.Contains(
        new Square("G1")).Should().Be(false);
}

[Test]
public void MakeMove_G1F3_PutsKnightOnF3()
{
    // arrange
    var board = Board.NewGame();
    // act
    var result = board.MakeMove(new Move("G1", "F3"));
    // assert
    result.White.Knights.Locations.Contains(
        new Square("F3")).Should().Be(true);
}

[Test]
public void MakeMove_G1F3AndE7E5AndF3E5_KnightCapturesPawn()
{
    // arrange
    var board = Board.NewGame();
    // act
    var result = board
        .MakeMove("G1", "F3")
        .MakeMove("E7", "E5")
        .MakeMove("F3", "E5");
    // assert
    result.White.GetPieceAt("E5").Should().Be(ChessPieces.Knight);
    result.Black.Pawns.Locations.Should().Be
        (Bitboard.With.A7.B7.C7.D7.F7.G7.H7.Build());
}

[Test]
public void MakeMove_G1F4_ThrowsInvalidMoveException()
{
    // arrange
    var board = Board.NewGame();
    // act
    board.Executing(b => b.MakeMove("G1", "F4", MoveTypes.Normal))
        .Throws<InvalidMoveException>();
}

Graças ao reaproveitamento de muita lógica escrita para peões, preciso escrever bem pouco código para dar suporte a movimentação de cavalos. Observe:

public Board MakeMove(Move move)
{
    Square? enpassant = null;

    var moving = (this.IsWhiteTurn ? White : Black);
    var notmoving = (this.IsWhiteTurn ? Black : White);

    switch (moving.GetPieceAt(move.From))
    {
        case ChessPieces.Pawn:
            MakePawnMove(move, ref moving, notmoving);
            enpassant = ComputeEnpassantSquare(move);
            break;
        case ChessPieces.Knight:
            var isValid = moving.Knights.GetMoves(
                moving.Occupation,
                notmoving.Occupation
                )
                .Count(m =>
                    m.From == move.From &&
                    m.To == move.To &&
                    m.Type == move.Type
                ) > 0;


            if (!isValid)
                throw new InvalidMoveException(move, this);

            moving = moving
                .RemovePieces(move.From)
                .AddPieces(ChessPieces.Knight, move.To.AsBoard);
            break;
        default:
            throw new NotImplementedException(
                string.Format("There is no support to {0} moves", 
                moving.GetPieceAt(move.From))
            );
    }

    notmoving = notmoving.RemovePieces(
        GetCapturedSquare(move)
        );

    var white = (this.IsWhiteTurn ? moving : notmoving);
    var black = (this.IsWhiteTurn ? notmoving : moving);
            
    return new Board(white, black, 0, !IsWhiteTurn, 
        enpassant);
}

Fazendo overload dos operadores de comparação para Move

Considere o seguinte código extraído do método que apresentei acima:

var isValid = moving.Knights.GetMoves(
    moving.Occupation,
    notmoving.Occupation
    )
    .Count(m =>
        m.From == move.From &&
        m.To == move.To &&
        m.Type == move.Type
    ) > 0;

Muitas linhas para comparação. Implementando uma comparação para Move:

public static bool operator ==(Move move1, Move move2)
{
    return move1.From == move2.From &&
        move1.To == move2.To &&
        move1.Type == move2.Type;
}

public static bool operator !=(Move move1, Move move2)
{
    return move1.From != move2.From ||
        move1.To != move2.To ||
        move1.Type != move2.Type;
}

public override bool Equals(object obj)
{
    if (!(obj is Move)) return false;
    return this == (Move) obj;
}

Pronto! Podemos revisitar o código e melhorar nossas comparações.

Suporte para Bispo/Torre/Dama em Board

Agora, precisamos adicionar suporte para as outras peças.. O que acham? Eis os testes iniciais:

[Test]
public void MakeMove_E4andE5andF1C4_MovesBishopToC4()
{
    // arrange
    var board = Board.NewGame();
    // act
    var result = board
        .MakeMove("E2", "E4")
        .MakeMove("E7", "E5")
        .MakeMove("F1", "C4");
    // assert
    result.White.GetPieceAt("C4").Should().Be(ChessPieces.Bishop);
    result.White.GetPieceAt("F1").Should().Be(ChessPieces.None);
}

[Test]
public void MakeMove_A4andE5andA1A3_MovesRookToA3()
{
    // arrange
    var board = Board.NewGame();
    // act
    var result = board
        .MakeMove("A2", "A4")
        .MakeMove("E7", "E5")
        .MakeMove("A1", "A3");
    // assert
    result.White.GetPieceAt("A3").Should().Be(ChessPieces.Rook);
    result.White.GetPieceAt("A1").Should().Be(ChessPieces.None);
}

[Test]
public void MakeMove_E4andE5andD1F3_MovesQueenToF3()
{
    // arrange
    var board = Board.NewGame();
    // act
    var result = board
        .MakeMove("E2", "E4")
        .MakeMove("E7", "E5")
        .MakeMove("D1", "F3");
    // assert
    result.White.GetPieceAt("F3").Should().Be(ChessPieces.Queen);
    result.White.GetPieceAt("D1").Should().Be(ChessPieces.None);
}

[Test]
public void MakeMove_G1F4_ThrowsInvalidMoveException()
{
    // arrange
    var board = Board.NewGame();
    // act
    board.Executing(b => b.MakeMove("G1", "F4", MoveTypes.Normal))
        .Throws<InvalidMoveException>();
}

Repare a implementação agora:

public Board MakeMove(Move move)
{
    Square? enpassant = null;

    var moving = (this.IsWhiteTurn ? White : Black);
    var notmoving = (this.IsWhiteTurn ? Black : White);

    if (AvaliableMoves.Count(m => m == move) == 0)
        throw new InvalidMoveException(move, this);

    var piece = moving.GetPieceAt(move.From);
    switch (piece)
    {
        case ChessPieces.Pawn:
            MakePawnMove(move, ref moving, notmoving);
            enpassant = ComputeEnpassantSquare(move);
            break;
        case ChessPieces.Queen:
        case ChessPieces.Rook:
        case ChessPieces.Bishop:
        case ChessPieces.Knight:
            moving = moving
                .RemovePieces(move.From)
                .AddPieces(piece, move.To.AsBoard);
            break;
        default:
            throw new NotImplementedException(
                string.Format("There is no support to {0} moves", 
                moving.GetPieceAt(move.From))
            );
    }

    notmoving = notmoving.RemovePieces(
        GetCapturedSquare(move)
        );

    if (notmoving.Attacks(
        moving.KingLocation, 
        moving.Occupation.AsBoard
        ))
        throw new InvalidMoveException(move, this);

    var white = (this.IsWhiteTurn ? moving : notmoving);
    var black = (this.IsWhiteTurn ? notmoving : moving);
            
    return new Board(white, black, 0, !IsWhiteTurn, 
        enpassant);
}

Repare como melhorei a validação do movimento através da propriedade local AvaliableMoves. No lugar de computar os movimentos para a peça sendo movimentada, gero todos os movimentos na inicialização de Board. (Pegue os fontes para ver os detalhes).

Observe também que coloco uma verificação adicional no fim do método. Verifico se o movimento deixa o rei em cheque, em caso positivo: Exception.

Suporte para Rei em board

Já estamos quase concluindo nossa implementação de MakeMove. Agora vamos implementar suporte a movimentação do rei.

Teste:

[Test]
public void MakeMove_MovingKingE1E2()
{
    // arrange
    var board = new Board(
        new Side(
            "E1",
            new PieceSet<Queen>(Bitboard.Empty),
            new PieceSet<Bishop>(Bitboard.Empty),
            new PieceSet<Knight>(Bitboard.Empty),
            new PieceSet<Rook>(Bitboard.Empty),
            new WhitePawns(Bitboard.Empty)
            ),
            new Side(
            "E8",
            new PieceSet<Queen>(Bitboard.Empty),
            new PieceSet<Bishop>(Bitboard.Empty),
            new PieceSet<Knight>(Bitboard.Empty),
            new PieceSet<Rook>(Bitboard.Empty),
            new WhitePawns(Bitboard.Empty)
            ));

    // act
    var result = board.MakeMove("E1", "E2");
            
    // assert
    result.White.KingLocation.Should().Be(
        new Square("E2")
        );
}

Novo MakeMove:

public Board MakeMove(Move move)
{
    Square? enpassant = null;

    var moving = (this.IsWhiteTurn ? White : Black);
    var notmoving = (this.IsWhiteTurn ? Black : White);

    if (AvaliableMoves.Count(m => m == move) == 0)
        throw new InvalidMoveException(move, this);

    var piece = moving.GetPieceAt(move.From);
    switch (piece)
    {
        case ChessPieces.Pawn:
            MakePawnMove(move, ref moving, notmoving);
            enpassant = ComputeEnpassantSquare(move);
            break;
        case ChessPieces.King:
            moving = new Side(
                move.To,
                moving.Queens,
                moving.Bishops,
                moving.Knights,
                moving.Rooks,
                moving.Pawns
                );
            break;
        default:
            moving = moving
                .RemovePieces(move.From)
                .AddPieces(piece, move.To.AsBoard);
            break;
    }

    notmoving = notmoving.RemovePieces(
        GetCapturedSquare(move)
        );

    if (notmoving.Attacks(
        moving.KingLocation, 
        moving.Occupation.AsBoard
        ))
        throw new InvalidMoveException(move, this);

    var white = (this.IsWhiteTurn ? moving : notmoving);
    var black = (this.IsWhiteTurn ? notmoving : moving);
            
    return new Board(white, black, 0, !IsWhiteTurn, 
        enpassant);
}

Repare como o switch foi otimizado.

Por hoje, era isso!

Smiley piscando

Etiquetado:,
Publicado em: Post