Escrevendo um Engine para Xadrez – parte 12 – A classe Board

Publicado em 17/05/2011

1


Olá pessoal, como estamos?

Se você está chegando agora, estamos desenvolvendo um engine de Xadrez que será realmente forte. O código-fonte está disponível em https://github.com/elemarjr/StrongChess. Os posts anteriores estão disponíveis aqui.

Depois de algum tempo sem encostar nesse projeto, resolvi dar sequência aos trabalhos. A pausa foi intencional. Queria um refactoring no código feito pelo juanplopes que, infelizmente, não ocorreu (o cara está sem tempo).

Hoje, apresento algumas implementações importantes. No centro, está o início de implementação para Board: um tipo para representar uma posição completa no tabuleiro.

Que peça está em E4 (e outras questões desse tipo)?

Nos décimo post, apresentei Side (uma representação para a colocação das peças de um dos jogadores). Com os dados desse tipo, conseguimos responder a pergunta acima. A resposta ocorre por um enum. Observe:

[Flags]
public enum ChessPieces
{
    None = 0,
    King = 1,
    Queen = 2,
    Bishop = 4,
    Knight = 8,
    Rook = 16,
    Pawn = 32
}

Para fazer testes abrangentes, optei por usar algum T4. Observe:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace StrongChess.Model.Tests.Sets
{
	using NUnit.Framework;
	using SharpTestsEx;
	using StrongChess.Model.Sets;
	using StrongChess.Model.Pieces;

	[TestFixture]
	class SideTests_GetPieceAt
	{
		<#
		var pieces = new [] { "Rook", "Knight", "Bishop", 
						"Queen", "King",
						"Bishop", "Knight", "Rook" };
			
		var files = new [] {"A", "B", "C", "D", "E", "F", "G", "H"};
		for (int i = 0; i < 8; i++)
		{
		#>
		
		[Test]
        public void 
		GetPieceAt_<#= files[i] #>2InWhiteInitialPosition_ReturnsPawn()
        {
            // arrange
            var side = Side.WhiteInitialPosition;
            // act
            var piece = side.GetPieceAt("<#= files[i] #>2");
            // assert
            piece.Should().Be(ChessPieces.Pawn);
        }
		
		[Test]
        public void 
		GetPieceAt_<#= files[i] #>7InWhiteInitialPosition_ReturnsPawn()
        {
            // arrange
            var side = Side.BlackInitialPosition;
            // act
            var piece = side.GetPieceAt("<#= files[i] #>7");
            // assert
            piece.Should().Be(ChessPieces.Pawn);
        }
		
		[Test]
        public void 
		GetPieceAt_<#= files[i] #>1InWhiteInitialPosition_Returns<#= pieces[i] #>()
        {
            // arrange
            var side = Side.WhiteInitialPosition;
            // act
            var piece = side.GetPieceAt("<#= files[i] #>1");
            // assert
            piece.Should().Be(ChessPieces.<#= pieces[i] #>);
        }
		
		[Test]
		public void 
		GetPieceAt_<#= files[i] #>8InBlackInitialPosition_Returns<#= pieces[i] #>()
        {
            // arrange
            var side = Side.BlackInitialPosition;
            // act
            var piece = side.GetPieceAt("<#= files[i] #>8");
            // assert
            piece.Should().Be(ChessPieces.<#= pieces[i] #>);
        }
        <# } #>
		
		[Test]
        public void GetPieceAt_E4InWhiteInitialPosition_ReturnsNone()
        {
            // arrange
            var side = Side.WhiteInitialPosition;
            // act
            var piece = side.GetPieceAt("E4");
            // assert
            piece.Should().Be(ChessPieces.None);
        }
	}
}

Como você pode ver, nos testes, utilizou um novo método da classe Side. Trata-se de GetPieceAt, que recebe um Square como argumento. Eis sua implementação:

public ChessPieces GetPieceAt(Square sq)
{
    if (!this.Occupation.Contains(sq))
        return ChessPieces.None;
    else if (this.Pawns.Locations.Contains(sq))
        return ChessPieces.Pawn;
    else if (this.Queens.Locations.Contains(sq))
        return ChessPieces.Queen;
    else if (this.Bishops.Locations.Contains(sq))
        return ChessPieces.Bishop;
    else if (this.Knights.Locations.Contains(sq))
        return ChessPieces.Knight;
    else if (this.Rooks.Locations.Contains(sq))
        return ChessPieces.Rook;
            
    return ChessPieces.King;
}

Como pode ver, trata-se de uma implementação muito simples. Basicamente, verifico qual Bitboard (conceito fundamental explicado na parte 1) possui o bit correspondente a casa ligada. Bacana, não?

Side – representando uma posição do jogo

Até aqui, temos tipos para representar regras de movimentação, para representar ocupações em um tabuleiro, mas não de uma posição propriamente dita.. esse é o propósito da classe Board. Observe:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace StrongChess.Model
{
    using StrongChess.Model.Sets;
    using StrongChess.Model.Pieces;
    using StrongChess.Model.Exceptions;

    public struct Board
    {
        public Side White { get; private set; }
        public Side Black { get; private set; }
        public int NoPawnMovesCount { get; private set; }
        public bool IsWhiteTurn { get; private set; }
        public Square? Enpassant { get; private set; }

        public Bitboard Occupation
        {
            get
            {
                return White.Occupation | Black.Occupation;
            }
        }

        public Board(Side white, Side black, 
            int noPawnMovesCount = 0, 
            bool isWhiteTurn = true,
            Square? enpassant = null)
            : this()
        {
            this.White = white;
            this.Black = black;
            this.NoPawnMovesCount = noPawnMovesCount;
            this.IsWhiteTurn = isWhiteTurn;
            this.Enpassant = enpassant;
        }
        
        public static Board NewGame()
        {
            //...
        }

        public Board MakeMove(Move move)
        {
			//..
        }
    }
}

Esse tipo é bem simples … Temos

  • White e Black – para representar cada um dos lados;
  • NoPownMovesCount - para contar quantos lances foram realizados sem que um peão tenha sido movimentado (regra dos cinquenta lances);
  • IsWhiteTurn – uma que indica quem deve jogar;
  • Enpassant – correspondência com a casa do Enpassant (caso exista uma).

Há dois métodos:

  • NewGame – configura a posição inicial para um jogo novo;
  • MakeMove – que altera o estado “do tabuleiro”, criando uma nova posição.

Implementando NewGame

Antes de escrever código, escrevemos testes. Observe os testes desenvolvidos durante durante a escrita de NewGame:

[Test]
public void NewGame_WhiteInInitialPosition()
{
    // arrange
    var b = Board.NewGame();
    //act
    // assert
    b.White.Should().Be(Side.WhiteInitialPosition);
}

[Test]
public void NewGame_BlackInInitialPosition()
{
    // arrange
    var b = Board.NewGame();
    //act
    // assert
    b.Black.Should().Be(Side.BlackInitialPosition);
}

[Test]
public void NewGame_IsWhiteTurn_ReturnsTrue()
{
    // arrange
    var b = Board.NewGame();
    //act
    // assert
    b.IsWhiteTurn.Should().Be(true);
}

[Test]
public void NewGame_NoPawnMovesCount_Returns0()
{
    // arrange
    var b = Board.NewGame();
    //act
    // assert
    b.NoPawnMovesCount.Should().Be(0);
}

[Test]
public void Occupation_InitialPostion_ReturnsBlackAndWhiteOccupation()
{
    // arrange
    var b = Board.NewGame();
    // act

    // assert
    b.Occupation.Should().Be((Bitboard) 
        (b.White.Occupation | b.Black.Occupation));
}

Bonito .. agora a implementação:

public static Board NewGame()
{
    var result = new Board();

    result.White = Side.WhiteInitialPosition;
    result.Black = Side.BlackInitialPosition;

    result.IsWhiteTurn = true;
    result.NoPawnMovesCount = 0;
            
    return result;
}

Bacana!

Implementando (os testes para) movimentos simples de peões

Movimentar peões deve ser muito fácil. Além de atualizar a posição, devemos atualizar a posição do enpassant.

[Test]
public void MakeMove_InitialPositionE2E4()
{
    // arrange
    var board = Board.NewGame();

    // act
    var result = board.MakeMove(new Move("E2", "E4"));

    // assert
    result.White.Pawns.Locations.Should().Be(
        Bitboard.With.A2.B2.C2.D2.E4.F2.G2.H2
            .Build()
        );
}

[Test]
public void MakeMove_InitialPositionE2E4_EnpassantShouldBeE3()
{
    // arrange
    var board = Board.NewGame();
    Square enpassant = new Square("E3");
    // act
    var result = board.MakeMove(new Move("E2", "E4"));

    // assert
    result.Enpassant.Value.Should().Be(enpassant);
    //Assert.Fail(result.Enpassant.ToString());
}

[Test]
public void MakeMove_InitialPositionE2E4AndE7E5_EnpassantShouldBeE6()
{
    // arrange
    var board = Board.NewGame();
    Square enpassant = new Square("E6");
    // act
    var result = board
        .MakeMove(new Move("E2", "E4"))
        .MakeMove(new Move("E7", "E5"));

    // assert
    result.Enpassant.Value.Should().Be(enpassant);
    //Assert.Fail(result.Enpassant.ToString());
}

[Test]
public void MakeMove_InitialPositionE2E4AndE7E6_EnpassantShouldBeNull()
{
    // arrange
    var board = Board.NewGame();
    // act
    var result = board
        .MakeMove(new Move("E2", "E4"))
        .MakeMove(new Move("E7", "E6"));

    // assert
    Assert.IsTrue(result.Enpassant == null);
    //Assert.Fail(result.Enpassant.ToString());
}

Acredito que o código se exmplica sozinho.

Implementando (os testes) para movimentos de captura por peões

Capturas por pões são bacanas (na verdade, movimentos simples). Alguns testes evidentes são indicados abaixo:

[Test]
public void MakeMove_InitialPositionE2E4AndD7D5_E4D5ShouldBePossible()
{
    // arrange
    var board = Board.NewGame();
    Move m = new Move("E4", "D5");
    // act
    var result = board
        .MakeMove(new Move("E2", "E4"))
        .MakeMove(new Move("D7", "D5"));


    var c = result.White.Pawns.GetAllMoves(~result.Occupation,
        result.Black.Occupation, "D6")
        .Where(move => move.From == m.From && move.To == m.To);

    c.Count().Should().Be(1);
}

[Test]
public void MakeMove_InitialPositionE2E4AndD7D5AndE4D5_WhitePawnOccupiesD5()
{
    // arrange
    var board = Board.NewGame();
    Square s = "D5";
    // act
    var result = board
        .MakeMove(new Move("E2", "E4"))
        .MakeMove(new Move("D7", "D5"))
        .MakeMove(new Move("E4", "D5"));

    // assert
    result.White.Pawns.Locations.Contains(s).Should().Be(true);
}

[Test]
public void MakeMove_InitialPositionE2E4AndD7D5AndE4D5_BlackPawnLeavesTheBoard()
{
    // arrange
    var board = Board.NewGame();
            
    // act
    var result = board
        .MakeMove(new Move("E2", "E4"))
        .MakeMove(new Move("D7", "D5"))
        .MakeMove(new Move("E4", "D5"));

    // assert
    result.Black.Pawns.Locations.Should().Be(
        Bitboard.With.H7.G7.F7.E7.C7.B7.A7.Build());
}

Implementando (os testes) para movimentos de enpassant

Enpassant é uma forma de captura diferente no Xadrez. Precisa de testes diferentes também. Observe:

[Test]
public void MakeMove_InitialPositionE2E4AndD7D5AndE4D5AndF7F5AndE5F6_BlackPawnLeavesTheBoard()
{
    // arrange
    var board = Board.NewGame();

    // act
    var result = board
        .MakeMove(new Move("E2", "E4"))
        .MakeMove(new Move("D7", "D5"))
        .MakeMove(new Move("E4", "E5"))
        .MakeMove(new Move("F7", "F5"))
        .MakeMove(new Move("E5", "F6"));


    // assert
    result.Black.Pawns.Locations.Should().Be(
        Bitboard.With.H7.G7.E7.D5.C7.B7.A7.Build());
}

Movimentos inválidos… Exception…

Se um movimento inválido for executado, uma exception deverá ser disparada. Observe:

[Test]
public void MakeMove_InitialPositionE2E5_ShouldThrowInvalidMoveException()
{
    // arrange
    var board = Board.NewGame();
    var move = new Move("E2", "E5");
    // act
    board.Executing((b) => b.MakeMove(move))
        .Throws<InvalidMoveException>();
}

Uma implementação suficiente para passar nos testes

Inicialmente, não estou tomando cuidado com a solução. Desejo apenas passar nos testes. Observe:

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)
    {
        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)
            | 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
                );


        moving = new Side(
            moving.KingLocation,
            moving.Queens,
            moving.Bishops,
            moving.Knights,
            moving.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);
}

Grande e feio! Mas funciona… Nos próximos posts partimos para um refactoring.

Por hoje, era isso.

Smiley piscando

Etiquetado:,
Publicado em: Post