Ch355 - Protótipo
Vamos começar como o código de 2015. Ou melhor: o resultado do meu esforço até o ano de 2015, visto que foi iniciado alguns anos antes.
A mágica toda está no arquivo chess-engine.js. Primeiramente eu fui atrás de caracteres Unicode que representassem as peças do Xadrez. Dessa forma eu não precisaria manipular imagens ou renderizar gráficos, tudo que eu tinha que fazer era exibir os caracteres com um zoom gigante.
wK = String.fromCharCode(0x2654); //white King
wQ = String.fromCharCode(0x2655); //white Queen
wR = String.fromCharCode(0x2656); //white Rook
wB = String.fromCharCode(0x2657); //white Bishop
wN = String.fromCharCode(0x2658); //white Knight
wp = String.fromCharCode(0x2659); //white Pawn
white = [wK, wQ, wR, wB, wN, wp]; //white group
bK = String.fromCharCode(0x265a); //black King
bQ = String.fromCharCode(0x265b); //black Queen
bR = String.fromCharCode(0x265c); //black Rook
bB = String.fromCharCode(0x265d); //black Bishop
bN = String.fromCharCode(0x265e); //black Knight
bp = String.fromCharCode(0x265f); //black Pawn
black = [bK, bQ, bR, bB, bN, bp]; //black group
São 6 desenhos diferentes nas cores preto + branco. Daí eu criei um array white[] e outro black[] com as seis peças de cada cor. Na sequência é preciso mapear a posição de cada peça no tabuleiro, representado em Notação Algébrica -- que curiosamente não tem relação com álgebra --, o sistema internacional oficial de coordenadas que identifica cada bloco e descreve os movimentos das peças. Ele é utilizado em livros há séculos e em competições globais organizadas pela Federação Internacional de Xadrez. O ASCII art abaixo tenta ilustrar isso.
/*** Board ***
Algebraic Notation [columns == files(letters); lines == ranks(numbers)]
-------------------------
8 a8 b8 c8 d8 e8 f8 g8 h8 | black rank
7 a7 b7 c7 d7 e7 f7 g7 h7 | black rank
6 a6 b6 c6 d6 e6 f6 g6 h6 |
5 a5 b5 c5 d5 e5 f5 g5 h5 |
4 a4 b4 c4 d4 e4 f4 g4 h4 |
3 a3 b3 c3 d3 e3 f3 g3 h3 |
2 a2 b2 c2 d2 e2 f2 g2 h2 | white rank
1 a1 b1 c1 d1 e1 f1 g1 h1 | white rank
-------------------------
a b c d e f g h ***/
Cada rank (fileira/linha horizontal) é representada por um número 1 – 8, enquanto o file (coluna vertical) é representado pelas letras de a – h. O protocolo UCI reutiliza alguns princípios da Notação Algébrica nos computadores para que seja possível interagir com os motores/engines. Mas foram coisas criadas para propósitos distintos. Até aqui só precisávamos entender que as coordenadas são herança da Notação Algébrica e de agora em diante só vamos abordar o UCI.
Voltando ao código de 2015, eu imaginei o tabuleiro como um array multidimensional. Além das casas 8x8 eu criei alguns atributos dentro do mesmo objeto board que precisam ser checados quando o usuário interage com uma das peças. Por exemplo: cada peão pode optar por avançar 2 casas no movimento inicial e estar sujeito à captura En passant, então é preciso checar se é o primeiro movimento para destacar os destinos permitidos (avanço de 1 ou 2 casas). O Roque (Castling) é outro bom exemplo: há uma propriedade board.wCastling = true;
que continuará assim somente enquanto a Torre e o Rei brancos não se moverem. O mesmo vale para as peças pretas na propriedade board.bCastling
: caso o time preto mova uma das duas peças da regra, esse atributo passa a ser falso.
Existe uma função reset()
que coloca todas as peças e atributos no estado inicial da partida. Já a função newGame()
é responsável por montar uma <table> HTML 8x8 já pensando nas coordenadas como atributos ID. Esta função também adiciona eventos onclick e classes para aplicar CSS com as clássicas cores alternadas do xadrez. Aliás, essa função deu um trabalhão pra criar, mas foi muito satisfatório quando eu consegui finalizar. Pega a lógica:
function newGame() {
var table = ce('table');
for (var i=7;i>=0;i--) {
var rank = ce('tr');
for (var j=0x61;j<=0x68;j++) {
var square = ce('td');
square.setAttribute('id', String.fromCharCode(j) + (i+1));
square.setAttribute('onclick', 'checkState(\'' + String.fromCharCode(j) + (i+1) + '\')');
if (!((i+1)%2)) {
if (!(j%2))
square.setAttribute('class', 'gray');
}
else {
if (j%2)
square.setAttribute('class', 'gray');
}
rank.appendChild(square);
}
table.appendChild(rank);
}
get('container').appendChild(table);
reset();
}
Nessa função eu introduzo outro conceito: os shorten methods (métodos abreviados). É só uma forma de não ficar escrevendo toda vez o famoso document.getElementById(id)
, por exemplo. Por pura preguiça eu criei uma função get()
que retorna esse textão aí pra capturar um elemento pelo ID. E o ce()
é o document.createElement()
.
No primeiro for
eu começo do rank 7 e desço até o rank 0, então são as nossas 8 fileiras (lembre-se que em diversas linguagens de programação, arrays iniciam em 0). Se você já brincou com hexadecimal, deve se lembrar que o 0x61
é o código pra letra a e o 0x68
é o da letra h, então dentro das condições do segundo loop for
eu tô dizendo pra ele varrer de a até h, ou seja, estou passando por cada file do tabuleiro. Daí em cada square eu coloco o ID dele e atribuo a função checkState(id)
(já falaremos dela!) no evento onclick. Ah, e tem a classe gray que é atribuída usando a seguinte lógica: se o rank é ímpar, somente o file ímpar é cinza; Se o rank é par, somente o file par pode ser cinza. Pra isso funcionar direitinho eu tive que somar 1 ao índice do array pra termos os IDs dos ranks corretos, claro. Por fim a função procura por um elemento com ID = container pra jogar a tabela bonitinha lá dentro. Se você reparar no source do arquivo HTML, ele está lá. E também há um evento onload no body chamando a função newGame()
. O resto é com o CSS que aplica a cor cinza nos elementos identificados com class = gray.

O CSS formata a tabela dessa forma
O array board só existe em memória. Para que o estado dele seja sincronizado com o que está renderizado na tela pelo HTML, é preciso chamar a função refresh()
. Para o tabuleiro aparecer como na imagem acima o refresh()
foi chamado de dentro do reset()
que estava dentro da newGame()
. Isso quer dizer que em todos os eventos importantes a gente vai atualizar board em memória e na sequência chamar refresh()
.
Como comentado anteriormente, a funçãocheckState()
foi atribuída em eventos onclick de cada square, junto de sua coordenada. Quando o usuário clica em uma peça do tabuleiro, essa função liga o atributo moving e com o ID resgata a peça contida nesse endereço para mostrar os possíveis movimentos. Estes "possíveis" movimentos são calculados pela função mais complexa até agora: showLegalMoves()
. Esse foi o estado mais próximo que eu consegui chegar de uma engine. Apesar de não tomar decisões ou classificar a melhor estratégia naquele turno, esta função checa a peça atual e mostra para onde ela pode se mover e quais peças ela pode capturar. Parece simples, mas envolve bastante matemática e checagens de peças espalhadas pelo plano cartesiano. Imagine que a peça selecionada é o ponto 0,0 e essa função precisa varrer os quadrantes identificando se há peças inimigas para captura ou amigas barrando sua passagem. Como temos 6 tipos diferentes de peças, existem 6 leis de checagem dentro de showLegalMoves()
. Creio que a mais complexa seja a Rainha, por motivos que você pode imaginar. Há também o efeito de cores aplicadas nas casas para onde é possível se mover (em azul) e peças para capturar (em vermelho).
Por fim, a função move()
tem a finalidade de identificar a peça, sua coordenada de origem e, caso o usuário selecione uma posição válida, a coordenada de destino. Se um destino inválido for selecionado, a checkState()
-- que estava mostrando os destinos permitidos -- desliga o atributo moving e deixa de mostrar os destinos permitidos, com ajuda da clearLegalMoves()
.
Isso foi tudo que consegui construir até então. Note que a minha lógica foi implementada pensando nas peças brancas. As peças pretas foram pensadas como papel de decisão da CPU, que será o nosso adversário.
Bora aprender a integrar o Stockfish!