Escrevendo um Engine para Xadrez – Parte 14 – Castling (Roque) Rules

Publicado em 19/11/2011

3


Olá pessoal. Como estamos?!

Depois de muito tempo sem tocar no código desse engine de Xadrez, estou retomando as atividades. Meu desejo era deixar o projeto parado até que o @juanplopes fizesse um refactoring/review. Entretanto, atendendo a uma “solicitação” do @leandronet resolvi reconsiderar minha posição.

Até aqui, trabalhamos bastante da representação de dados relacionados ao tabuleiro. Além disso, implementamos a lógica para geração de movimentos válidos, considerando, inclusive: xeques (descobertos), escapadas, enpassant.

Hoje, vamos implementar a verificação  para possibilidade de roque.

O código-fonte produzido até agora está em https://github.com/ElemarJR/StrongChess.

Se desejar consultar os posts anteriores, considere http://elemarjr.net/tag/xadrez.

Explicando o Roque

Roque, no xadrez, é uma jogada que envolve a movimentação de duas peças no mesmo lance. Sua função é, normalmente, proteger o Rei ao deslocá-lo para um dos cantos do tabuleiro e conectar as torres na primeira fileira.

Há dois tipos de roque:

Roque pequeno

No roque pequeno, o Rei movimenta-se duas casas em direção à Torre da ala do Rei (o Rei branco vai para a casa g1, e o Rei preto vai para a casa g8). Em seguida a Torre é colocada na casa que o Rei saltou (a Torre branca vai para f1, e a Torre preta vai para f8).

Roque grande

No roque grande, o Rei faz Roque com a Torre da ala da Dama (o Rei branco vai para a casa c1, e o Rei preto vai para a casa c8). A Torre vai para a casa que o Rei pulou (a Torre branca vai para a casa d1, e a Torre preta vai para a casa d8).

Condições para que ocorra o Roque

Para se poder efetuar um roque, as seguintes condições são necessárias:

  • Rei não foi movido desde o início do jogo;
  • Torre a usar no roque nunca foi movida;
  • Rei não está em Xeque;
  • Nenhuma das casas pelas quais o Rei irá passar ou ficar está sob ataque;
  • Casas entre o rei e a torre estão desocupadas.

Entendido!? Go code!

REGRA 1: O rei não foi movido desde o início do jogo

No nosso engine, temos uma struct, chamada Side (veja no Github), que contém a informação sobre a posição das peças de um “lado” no tabuleiro.

Para atender a essa regra no Xadrez, adiciono uma propriedade bool, somente leitura, nessa struct com a informação se o rei foi ou não movimentado. O valor (padrão) dessa propriedade é definido no construtor. Além disso, mesmo que a informação passada para o constutor indique que o rei não foi movimentado, se ele não estiver na posição original, deverá retornar false.

Começamos pelos testes.

[Test]
public void KingWasMoved_SideWithKingInE1AndNoMoveInformation_ReturnsFalse()
{
    var white = new Side("E1",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(),
        new WhitePawns());

    white.KingWasMoved.Should().Be(false);
}

Agora, o mesmo teste para peças pretas.

[Test]
public void KingWasMoved_SideWithBlackKingInE8AndNoMoveInformation_ReturnsFalse()
{
    var black = new Side("E8",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(),
        new BlackPawns());

    black.KingWasMoved.Should().Be(false);
}

Movendo os reis…

[Test]
public void KingWasMoved_SideWithWhiteKingInE2AndNoMoveInformation_ReturnsTrue()
{
    var white = new Side("E2",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(),
        new WhitePawns());

    white.KingWasMoved.Should().Be(true);
}


[Test]
public void KingWasMoved_SideWithBlackKingInE7AndNoMoveInformation_ReturnsTrue()
{
    var black = new Side("E7",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(),
        new BlackPawns());

    black.KingWasMoved.Should().Be(true);
}

Considerando a alteração do construtor para receber a informação de que a peça foi movimentada.

[Test]
public void KingWasMoved_SideWithWhiteKingInE1WhereKingWasMoved_ReturnsTrue()
{
    var white = new Side("E1",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(),
        new WhitePawns(),
        kingWasMoved: true);

    white.KingWasMoved.Should().Be(true);
}

[Test]
public void KingWasMoved_SideWithBlackKingInE8WhereKingWasMoved_ReturnsTrue()
{
    var black = new Side("E8",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(),
        new BlackPawns(),
        true);

    black.KingWasMoved.Should().Be(true);
}

Testes escritos, vamos a implementação. Primeiro a propriedade:

public bool _kingWasMoved;
public bool KingWasMoved
{
    get
    {
        if (_kingWasMoved) return true;
        if (this.IsWhite)
            return (KingLocation.AsBoard != Bitboard.With.E1.Build());
        else
            return (KingLocation.AsBoard != Bitboard.With.E8.Build());
    }
}

Repare como considero a “cor” do lado para acertar posição.

Agora, o construtor:

public Side(
    Square kingLocation,
    PieceSet<Queen> queens,
    PieceSet<Bishop> bishops,
    PieceSet<Knight> knights,
    PieceSet<Rook> rooks,
    IPawns pawns,
    bool kingWasMoved = false
    )
    : this()
{
    _King = new PieceSet<King>(kingLocation.AsBoard);
    Queens = queens;
    Rooks = rooks;
    Bishops = bishops;
    Knights = knights;
    Pawns = pawns;
    Occupation = queens.Locations | rooks.Locations | _King.Locations |
        bishops.Locations | knights.Locations | pawns.Locations;
    this._kingWasMoved = kingWasMoved;
}

Executando os testes:

image

Perfeito!

REGRA 2: Torre a usar no roque nunca foi movida

Resolvi aplicar lógica semelhante para controlar “o movimento” das torres. Primeiro os testes:

[Test]
public void KingCastleRookWasMoved_SideWithWhiteRookInH1AndNoMoveInformation_ReturnsFalse()
{
    var white = new Side("E1",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("H1")),
        new WhitePawns());

    white.KingCastleRookWasMoved.Should().Be(false);
}

[Test]
public void KingCastleRookWasMoved_SideWithWhiteRookInH1WhereRookWasMoved_ReturnsTrue()
{
    var white = new Side("E1",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("H1")),
        new WhitePawns(),
        kingCastleRookWasMoved: true);

    white.KingCastleRookWasMoved.Should().Be(true);
}

[Test]
public void KingCastleRookWasMoved_SideWithBlackRookInH8WhereRookWasMoved_ReturnsTrue()
{
    var black = new Side("E8",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("H8")),
        new BlackPawns(),
        kingCastleRookWasMoved: true);

    black.KingCastleRookWasMoved.Should().Be(true);
}

[Test]
public void KingCastleRookWasMoved_SideWithBlackRookInH8AndNoMoveInformation_ReturnsFalse()
{
    var black = new Side("E8",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("H8")),
        new BlackPawns());

    black.KingCastleRookWasMoved.Should().Be(false);
}

[Test]
public void KingCastleRookWasMoved_SideWithWhiteRookInH2AndNoMoveInformation_ReturnsTrue()
{
    var white = new Side("E2",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("H2")),
        new WhitePawns());

    white.KingCastleRookWasMoved.Should().Be(true);
}


[Test]
public void KingCastleRookWasMoved_SideWithBlackRookInH7AndNoMoveInformation_ReturnsTrue()
{
    var black = new Side("E7",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("H7")),
        new BlackPawns());

    black.KingCastleRookWasMoved.Should().Be(true);
}

Agora, a propriedade e o construtor:

public bool _kingCastleRookWasMoved;
public bool KingCastleRookWasMoved
{
    get
    {
        if (_kingCastleRookWasMoved) return true;
        if (this.IsWhite)
            return !this.Rooks.Locations.Contains(new Square("H1"));
        else
            return !this.Rooks.Locations.Contains(new Square("H8"));
    }
}

public Side(
    Square kingLocation,
    PieceSet<Queen> queens,
    PieceSet<Bishop> bishops,
    PieceSet<Knight> knights,
    PieceSet<Rook> rooks,
    IPawns pawns,
    bool kingWasMoved = false,
    bool kingCastleRookWasMoved = false
    )
    : this()
{
    _King = new PieceSet<King>(kingLocation.AsBoard);
    Queens = queens;
    Rooks = rooks;
    Bishops = bishops;
    Knights = knights;
    Pawns = pawns;
    Occupation = queens.Locations | rooks.Locations | _King.Locations |
        bishops.Locations | knights.Locations | pawns.Locations;
    this._kingWasMoved = kingWasMoved;
    this._kingCastleRookWasMoved = kingCastleRookWasMoved;
}

Perfeito! Agora, o lado da dama

[Test]
public void QueenCastleRookWasMoved_SideWithWhiteRookInA1AndNoMoveInformation_ReturnsFalse()
{
    var white = new Side("E1",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("A1")),
        new WhitePawns());

    white.QueenCastleRookWasMoved.Should().Be(false);
}

[Test]
public void QueenCastleRookWasMoved_SideWithWhiteRookInA1WhereRookWasMoved_ReturnsTrue()
{
    var white = new Side("E1",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("A1")),
        new WhitePawns(),
        queenCastleRookWasMoved: true);

    white.QueenCastleRookWasMoved.Should().Be(true);
}

[Test]
public void QueenCastleRookWasMoved_SideWithBlackRookInH8WhereQueenWasMoved_ReturnsTrue()
{
    var black = new Side("E8",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("A8")),
        new BlackPawns(),
        queenCastleRookWasMoved: true);

    black.QueenCastleRookWasMoved.Should().Be(true);
}

[Test]
public void QueenCastleRookWasMoved_SideWithBlackRookInA8AndNoMoveInformation_ReturnsFalse()
{
    var black = new Side("E8",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("A8")),
        new BlackPawns());

    black.QueenCastleRookWasMoved.Should().Be(false);
}

[Test]
public void QueenCastleRookWasMoved_SideWithWhiteRookInA2AndNoMoveInformation_ReturnsTrue()
{
    var white = new Side("E2",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("A2")),
        new WhitePawns());

    white.QueenCastleRookWasMoved.Should().Be(true);
}


[Test]
public void QueenCastleRookWasMoved_SideWithBlackRookInA7AndNoMoveInformation_ReturnsTrue()
{
    var black = new Side("E7",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("A7")),
        new BlackPawns());

    black.QueenCastleRookWasMoved.Should().Be(true);
}

Propriedade:

public bool _queenCastleRookWasMoved;
public bool QueenCastleRookWasMoved
{
    get
    {
        if (_queenCastleRookWasMoved) return true;
        if (this.IsWhite)
            return !this.Rooks.Locations.Contains(new Square("A1"));
        else
            return !this.Rooks.Locations.Contains(new Square("A8"));
    }
}

Feito!

REGRA 3: Rei não está em xeque

Já temos suporte em código para validar essa regra. No passado, escrevemos a lógica para verificar se uma “casa” está sendo atacada. Hoje, vamos apenas deixar as coisas mais “explícitas”. Eis o método que escrevi:

public bool IsInCheck(Side enemy)
{
    return enemy.Attacks(this.KingLocation, this.Occupation);
}

Dois testes básicos, para “tranquilidade”.

[Test]
public void KingInCheck_InCheckScenario_ReturnsTrue()
{
    var black = new Side("E7",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("A7")),
        new BlackPawns());

    var white = new Side("A2",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(),
        new BlackPawns());

    white.IsInCheck(black).Should().Be(true);
}

[Test]
public void KingInCheck_NotInCheckScenario_ReturnsFalse()
{
    var black = new Side("E7",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(new Square("A7")),
        new BlackPawns());

    var white = new Side("B2",
        new PieceSet<Queen>(),
        new PieceSet<Bishop>(),
        new PieceSet<Knight>(),
        new PieceSet<Rook>(),
        new BlackPawns());

    white.IsInCheck(black).Should().Be(false);
}

REGRA 4: Nenhuma das casas pelas quais o Rei irá passar ou ficar está sob ataque

A lógica aqui é bem simples, também. Afinal, já temos o método para verificar se uma casa está sob ataque. Veja:

public bool IsKingCastlePathUnderAttack(Side enemy)
{
    if (this.IsWhite)
    {
        return
            enemy.Attacks("E1", this.Occupation) ||
            enemy.Attacks("F1", this.Occupation) ||
            enemy.Attacks("G1", this.Occupation);
    }
    else
    {
        return
            enemy.Attacks("E8", this.Occupation) ||
            enemy.Attacks("F8", this.Occupation) ||
            enemy.Attacks("G8", this.Occupation);
    }
}

public bool IsQueenCastlePathUnderAttack(Side enemy)
{
    if (this.IsWhite)
    {
        return
            enemy.Attacks("E1", this.Occupation) ||
            enemy.Attacks("D1", this.Occupation) ||
            enemy.Attacks("C1", this.Occupation);
    }
    else
    {
        return
            enemy.Attacks("E8", this.Occupation) ||
            enemy.Attacks("D8", this.Occupation) ||
            enemy.Attacks("C8", this.Occupation);
    }
}

Não escrevi testes para esses métodos!

REGRA 5: Casas entre o rei e a torre estão desocupadas

Enfim, a última regra: Casas desocupadas. Novamente, temos o trabalho simplificado pelo que já fizemos nos posts anteriores. Observe:

public bool IsKingCastlePathClear()
{
    if (this.IsWhite)
    {
        return
            this.Occupation.IsClear(Bitboard.With.F1.G1.Build());
    }
    else
    {
        return
            this.Occupation.IsClear(Bitboard.With.F8.G8.Build());
    }
}

public bool IsQueenCastlePathClear()
{
    if (this.IsWhite)
    {
        return
            this.Occupation.IsClear(Bitboard.With.D1.C1.B1.Build());
    }
    else
    {
        return
            this.Occupation.IsClear(Bitboard.With.D8.C8.B8.Build());
    }
}

Combinando tudo

Pronto! Agora que já temos toda lógica para cada uma das regras do roque implementadas, vamos escrever os métodos que verificam todas as regras. Observe:

public bool CanPerformKingCastle(Side enemy)
{
    return
        !this.KingWasMoved &&
        !this.KingCastleRookWasMoved &&
        !this.IsInCheck(enemy) &&
        !this.IsKingCastlePathUnderAttack(enemy) &&
        this.IsKingCastlePathClear();
}

public bool CanPerformQueenCastle(Side enemy)
{
    return
        !this.KingWasMoved &&
        !this.QueenCastleRookWasMoved &&
        !this.IsInCheck(enemy) &&
        !this.IsQueenCastlePathUnderAttack(enemy) &&
        this.IsQueenCastlePathClear();
}

Pronto!

Smiley piscando

Etiquetado:
Publicado em: Post