2

Pitch: Minerva: desenvolvendo meu próprio motor de xadrez em Java

1. A ideia do projeto

Estou desenvolvendo meu próprio motor de xadrez (chess engine) em Java! Meu objetivo é, além de evoluir meus conhecimentos sobre a stack Java, melhorar minhas habilidades como programador, desafiando-me em um projeto que envolve diferentes aspectos da programação e exige o exercício de várias habilidades.

O projeto se chama Minerva e pode ser acompanhado pelo repositório no GitHub:

Acesse o repositório do Minerva

O nome foi inspirado em Minerva, deusa romana da sabedoria e da guerra, equivalente à deusa grega Atena.

Representação de Atena, contraparte grega de Minerva, deusa romana da sabedoria e da guerra

Imagem de referência: Imaginação Fértil.

O Minerva será desenvolvido por etapas. Neste post, vou apresentar como implementei a representação inicial do tabuleiro e das peças, além de um método rudimentar para localizar uma peça a partir da notação das casas do xadrez, como a1 e b2.

2. Representação do tabuleiro

A representação do tabuleiro é uma das bases de um motor de xadrez. Ela é responsável por armazenar a posição das peças e permitir que o programa trabalhe com o estado da partida.

A Chess Programming Wiki, em seu artigo Getting Started, destaca a importância dessa etapa:

A base de um motor de xadrez é a representação do tabuleiro. Esse é o back-end do motor, responsável por controlar como ele acompanha o tabuleiro e as regras do jogo. O primeiro passo para desenvolver um motor de xadrez é criar uma representação completa e livre de erros, que conheça todas as regras do xadrez.

Outro artigo da mesma fonte, Board Representation, explica que um programa de xadrez precisa de uma representação interna para manter as posições das peças durante as buscas, avaliações e partidas.

Exemplo de representação de um tabuleiro de xadrez no terminal do programa TSCP

Imagem de referência: captura do TSCP no terminal.

Para representar o tabuleiro do Minerva, escolhi uma estrutura baseada no conceito de Mailbox, por ser uma abordagem simples de compreender e implementar.

Segundo a Chess Programming Wiki, no artigo sobre Mailbox, essa representação é centrada nas casas do tabuleiro: cada casa possui uma posição correspondente na memória, geralmente em um array, que permite acessar seu conteúdo diretamente.

No meu projeto, comecei com uma matriz de 8 × 8, na qual cada posição representa uma das 64 casas do tabuleiro de xadrez.

O código inicial ficou assim:

public int[][] tabuleiro = new int[][] {
    {TORRE_PRETO, CAVALO_PRETO, BISPO_PRETO, RAINHA_PRETO, REI_PRETO, BISPO_PRETO, CAVALO_PRETO, TORRE_PRETO},
    {PEAO_PRETO, PEAO_PRETO, PEAO_PRETO, PEAO_PRETO, PEAO_PRETO, PEAO_PRETO, PEAO_PRETO, PEAO_PRETO},
    {VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO},
    {VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO},
    {VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO},
    {VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO, VAZIO},
    {PEAO_BRANCO, PEAO_BRANCO, PEAO_BRANCO, PEAO_BRANCO, PEAO_BRANCO, PEAO_BRANCO, PEAO_BRANCO, PEAO_BRANCO},
    {TORRE_BRANCO, CAVALO_BRANCO, BISPO_BRANCO, RAINHA_BRANCO, REI_BRANCO, BISPO_BRANCO, CAVALO_BRANCO, TORRE_BRANCO}
};

Como representar as peças e suas cores?

Para decidir como representar as peças, consultei também a implementação do TSCP (Tom Kerrigan's Simple Chess Program).

Nessa implementação, são utilizados dois arrays de 64 elementos: um armazena o tipo da peça, enquanto o outro armazena sua cor. Assim, as duas informações ficam separadas.

A estrutura é apresentada na Chess Programming Wiki, na seção dedicada ao TSCP:

int color[64];  /* LIGHT, DARK, or EMPTY */
int piece[64];  /* PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING, or EMPTY */

O exemplo também apresenta os arrays preenchidos com a posição inicial das peças:

int piece[64] = {
    ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK,
    PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN,
    EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
    EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
    EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
    EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
    PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN, PAWN,
    ROOK, KNIGHT, BISHOP, QUEEN, KING, BISHOP, KNIGHT, ROOK
};

int color[64] = {
    BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,
    BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK, BLACK,
    EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
    EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
    EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
    EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY, EMPTY,
    WHITE, WHITE, WHITE, WHITE, WHITE, WHITE, WHITE, WHITE,
    WHITE, WHITE, WHITE, WHITE, WHITE, WHITE, WHITE, WHITE
};

Essa é uma referência de outra implementação. No Minerva, optei por uma abordagem diferente: cada combinação de peça e cor possui um valor inteiro próprio, enquanto as casas vazias são representadas por um valor específico.

Tive algumas dificuldades para definir a arquitetura do motor e implementar métodos que permitissem localizar as peças no tabuleiro. Para começar, organizei essa representação em uma classe chamada Pecas:

public class Pecas {
    int PEAO_BRANCO = 0;
    int CAVALO_BRANCO = 1;
    int BISPO_BRANCO = 2;
    int TORRE_BRANCO = 3;
    int RAINHA_BRANCO = 4;
    int REI_BRANCO = 5;

    int PEAO_PRETO = 6;
    int CAVALO_PRETO = 7;
    int BISPO_PRETO = 8;
    int TORRE_PRETO = 9;
    int RAINHA_PRETO = 10;
    int REI_PRETO = 11;

    int VAZIO = 12;
}

Dessa forma, cada peça pode ser identificada pelo valor armazenado na matriz, incluindo sua cor, de acordo com as constantes definidas no projeto.

3. Imprimindo o tabuleiro no terminal com ASCII (CLI)

Depois de implementar a representação inicial do tabuleiro, precisei criar uma maneira de visualizar sua posição no terminal.

Para isso, desenvolvi um método de impressão utilizando caracteres ASCII. A implementação pode ser entendida em duas etapas.

3.1. Representação das peças por caracteres

Cada posição do array letras corresponde ao valor numérico utilizado para representar uma peça.

Por exemplo, PEAO_BRANCO possui o valor 0. Portanto, ao acessar letras[0], o programa obtém o caractere 'p'. O valor 12, utilizado para representar uma casa vazia, corresponde ao caractere '.'.

O código ficou assim:

// Cada posição do array corresponde ao valor de uma peça.
// Ex.: letras[0] = 'p' -> PEAO_BRANCO = 0.
char[] letras = {
    'p', 'n', 'b', 'r', 'q', 'k',
    'P', 'N', 'B', 'R', 'Q', 'K', '.'
};

Assim, em vez de imprimir apenas os números armazenados na matriz, posso utilizar o array letras para exibir o caractere correspondente a cada posição.

3.2. Percorrendo a matriz e imprimindo o tabuleiro

Com o mapeamento dos caracteres pronto, basta percorrer as linhas e colunas da matriz.

Em cada posição, o programa utiliza o valor armazenado na matriz como índice do array letras e imprime o caractere correspondente. Ao terminar uma linha, utiliza System.out.println() para iniciar a próxima.

for (int i = 0; i < 8; i++) {
    for (int j = 0; j < 8; j++) {
        System.out.printf("%c ", letras[arrayTabuleiro[i][j]]);

        // System.out.printf("%d ", arrayTabuleiro[i][j]);
    }

    System.out.println();
}

Dessa maneira, consigo visualizar o conteúdo da matriz no terminal usando caracteres para representar as peças e as casas vazias.

4. Implementação inicial para localizar uma peça pela notação algébrica

Por fim, comecei a implementar um método para encontrar uma peça a partir da notação de uma casa do tabuleiro, como a1, b2 e assim por diante.

Essa implementação ainda é rudimentar. Por enquanto, vou apresentar o código do main e o método responsável pela busca, para mostrar como essa funcionalidade está funcionando nesta etapa do projeto.

4.1. Método main

No main, crio um objeto da classe Tabuleiro, imprimo o tabuleiro no terminal e recebo a notação digitada pelo usuário.

Em seguida, envio a matriz, a letra e o número da casa para o método posicaoPeca().

import java.util.Scanner;
import pacotes.*;

public class Main {
    public static void main(String[] args) {
        Scanner entrada = new Scanner(System.in);
        Tabuleiro t1 = new Tabuleiro();

        System.out.println("// Tabuleiro //");
        t1.imprimir(t1.tabuleiro);

        String notacaoPeca = entrada.nextLine();

        t1.posicaoPeca(
            t1.tabuleiro,
            notacaoPeca.charAt(0),
            Character.getNumericValue(notacaoPeca.charAt(1))
        );
    }
}

4.2. Método posicaoPeca()

O método recebe a matriz do tabuleiro, a letra da casa e o número correspondente.

Primeiro, percorre um array com as letras de a a h para descobrir o índice da coluna correspondente à letra informada.

Depois, percorre a matriz e compara os índices para localizar a posição desejada. Quando encontra a casa correspondente, imprime o valor numérico armazenado nela e seus índices na matriz.

// Notação das casas do xadrez: a1, b2 etc.
public void posicaoPeca(int[][] arrayTabuleiro, char fileira, int coluna) {
    char[] letras = {'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'};
    int fileiraNumero = 0;

    for (int i = 0; i < 8; i++) {
        if (fileira == letras[i]) {
            fileiraNumero = i;
            break;
        }
    }

    // System.out.printf("Fileira: %d\nColuna: %d\n\n", fileiraNumero, coluna);

    for (int i = 0; i < 8; i++) {
        for (int j = 0; j < 8; j++) {
            if (j == fileiraNumero && i == 7 - (coluna - 1)) {
                System.out.printf(
                    "Peça achada: %d\nlocal: [%d][%d]\n",
                    arrayTabuleiro[i][j],
                    i,
                    j
                );
                break;
            }
        }
    }
}

Essa é a versão inicial do método de localização. Ainda há espaço para evoluir essa funcionalidade conforme o desenvolvimento do Minerva avançar.

Conclusão

Esse é o estado atual de algumas das primeiras etapas do Minerva: a representação inicial do tabuleiro e das peças, a impressão do tabuleiro no terminal e uma primeira implementação para localizar uma peça a partir da notação de uma casa.

O projeto será desenvolvido por partes, e pretendo continuar aprimorando meus conhecimentos de Java e minhas habilidades de programação durante esse processo.

Espero que tenham gostado de acompanhar esse começo! Se puderem dar uma olhada no repositório do Minerva no GitHub, deixar uma estrela, contribuir ou compartilhar algum feedback, ficarei muito agradecido.

Valeu!

Carregando publicação patrocinada...
2

projeto massa pra aprender. uma dica antes de ir muito longe na geração de lances: o 8x8 obriga a checar se saiu do tabuleiro em toda direção, e isso enche o código de if. Muita engine começa com mailbox 10x12 (uma borda de casas inválidas em volta) ou com 0x88, onde um & 0x88 já diz se a casa existe. Os dois estão na própria Chess Programming Wiki.

E quando chegar nos lances, monta o teste de perft cedo. Comparar a contagem de nós com os números conhecidos da posição inicial e da Kiwipete pega roque, en passant e promoção errados bem antes de você chegar na busca.

1

Obrigado pelas dicas, meu nobre! Optei por um tabuleiro 8x8 justamente para facilitar o início do projeto, dado que tive dificuldades antes com mailbox 10x12 numa tentativa passada em fazer esse motor em C. Pretendo na próxima etapa aplicar mailbox 10x12 justamente pelo seu ponto. Sobre PERFT, já dei uma lida e irei implementar no motor quando for o momento certo. Contudo, obrigado pelos pontos e por ter visto meu post.

1

faz sentido começar pelo 8x8, ainda mais depois de ter apanhado do 10x12 em C. e se o perft já estiver pronto na hora da troca fica tranquilo, roda a mesma contagem nas duas versões e se bater tá certo. boa sorte com o Minerva!

1
1
2

Vou conhecer mais sobre o projeto no link! Parece bem legal!
Eu também programo jogo de xadrez, cara. É um jogo incrível, com muita regra... programar ele é um desafio mesmo.

😀

3

Muito obrigado, meu nobre, por conhecer meu projeto. Realmente, programar uma engine de xadrez é um grande desafio e um fascínio.

Conteúdo excluído
1