Atualização: Eu gostei tanto deste tópico que escrevi Puzzles de Programação, Posições de Xadrez e Codificação de Huffman . Se você ler isso, concluí que a única maneira de armazenar um estado de jogo completo é armazenando uma lista completa de movimentos. Continue lendo para saber o porquê. Portanto, uso uma versão ligeiramente simplificada do problema para o layout das peças.
O problema
Esta imagem ilustra a posição inicial do xadrez. O xadrez ocorre em um tabuleiro de 8x8 com cada jogador começando com um conjunto idêntico de 16 peças consistindo em 8 peões, 2 torres, 2 cavalos, 2 bispos, 1 rainha e 1 rei conforme ilustrado aqui:
As posições são geralmente registradas como uma letra para a coluna seguida pelo número para a linha, então a rainha das brancas está em d1. Os movimentos são mais frequentemente armazenados em notação algébrica , que não é ambígua e geralmente especifica apenas as informações mínimas necessárias. Considere esta abertura:
- e4 e5
- Nf3 Nc6
- …
que se traduz em:
- O branco move o peão do rei de e2 para e4 (é a única peça que pode chegar a e4, portanto, “e4”);
- As pretas movem o peão do rei de e7 para e5;
- O branco move o cavalo (N) para f3;
- As pretas movem o cavalo para c6.
- …
O quadro é parecido com este:
Uma habilidade importante para qualquer programador é ser capaz de especificar o problema de forma correta e inequívoca .
Então, o que está faltando ou é ambíguo? Muito ao que parece.
Estado do tabuleiro vs estado do jogo
A primeira coisa que você precisa determinar é se você está armazenando o estado de um jogo ou a posição das peças no tabuleiro. Codificar simplesmente as posições das peças é uma coisa, mas o problema diz “todos os movimentos legais subsequentes”. O problema também não diz nada sobre saber os movimentos até este ponto. Na verdade, esse é um problema, como vou explicar.
Castling
O jogo decorreu da seguinte forma:
- e4 e5
- Nf3 Nc6
- Bb5 a6
- Ba4 Bc5
O quadro é o seguinte:
O branco tem a opção de roque . Parte dos requisitos para isso é que o rei e a torre relevante nunca podem ter se movido, portanto, se o rei ou qualquer uma das torres de cada lado se moveu, precisará ser armazenado. Obviamente, se eles não estiverem em suas posições iniciais, eles se moveram, caso contrário, isso precisa ser especificado.
Existem várias estratégias que podem ser usadas para lidar com esse problema.
Em primeiro lugar, poderíamos armazenar 6 bits extras de informação (1 para cada torre e rei) para indicar se aquela peça havia se movido. Poderíamos simplificar isso armazenando apenas um pouco para um desses seis quadrados se a peça certa estiver nele. Alternativamente, poderíamos tratar cada peça imóvel como outro tipo de peça, portanto, em vez de 6 tipos de peça em cada lado (peão, torre, cavalo, bispo, rainha e rei), haja 8 (adicionando torre imóvel e rei imóvel).
En Passant
Outra regra peculiar e frequentemente negligenciada no xadrez é En Passant .
O jogo progrediu.
- e4 e5
- Nf3 Nc6
- Bb5 a6
- Ba4 Bc5
- OO b5
- Bb3 b4
- c4
O peão preto em b4 agora tem a opção de mover seu peão em b4 para c3 pegando o peão branco em c4. Isso só acontece na primeira oportunidade, o que significa que se as pretas passarem a opção agora, ele não poderá fazer o próximo movimento. Portanto, precisamos armazenar isso.
Se conhecermos o movimento anterior, podemos definitivamente responder se En Passant é possível. Alternativamente, podemos armazenar se cada peão em sua 4ª fileira acabou de se mover para lá com um movimento duplo para frente. Ou podemos olhar para cada posição possível do En Passant no tabuleiro e ter uma bandeira para indicar se é possível ou não.
Promoção
É a jogada das brancas. Se as brancas moverem seu peão em h7 para h8, ele pode ser promovido a qualquer outra peça (mas não ao rei). 99% das vezes é promovida a Rainha, mas às vezes não é, normalmente porque isso pode forçar um impasse quando, de outra forma, você ganharia. Isso é escrito como:
- h8 = Q
Isso é importante em nosso problema porque significa que não podemos contar com um número fixo de peças em cada lado. É totalmente possível (mas incrivelmente improvável) que um lado termine com 9 rainhas, 10 torres, 10 bispos ou 10 cavalos se todos os 8 peões forem promovidos.
Impasse
Quando está em uma posição da qual você não pode vencer, sua melhor tática é tentar um impasse . A variante mais provável é quando você não pode fazer uma jogada legal (geralmente porque qualquer jogada coloca seu rei em xeque). Neste caso, você pode reivindicar um empate. Este é fácil de atender.
A segunda variante é por repetição tripla . Se a mesma posição do tabuleiro ocorrer três vezes em um jogo (ou ocorrerá uma terceira vez no próximo lance), um empate pode ser reivindicado. As posições não precisam ocorrer em nenhuma ordem particular (o que significa que não precisa da mesma sequência de movimentos repetidos três vezes). Este complica muito o problema porque você tem que se lembrar de todas as posições anteriores do tabuleiro. Se este for um requisito do problema, a única solução possível para o problema é armazenar todos os movimentos anteriores.
Por último, existe a regra dos cinquenta movimentos . Um jogador pode reivindicar um empate se nenhum peão se moveu e nenhuma peça foi tirada nos cinquenta movimentos consecutivos anteriores, então precisaríamos armazenar quantos movimentos desde que um peão foi movido ou uma peça retirada (o mais recente dos dois. Isso requer 6 bits (0-63).
Quem é a vez?
Claro que também precisamos saber de quem é a vez e esta é uma única informação.
Dois problemas
Por causa do caso de impasse, a única maneira viável ou sensata de armazenar o estado do jogo é armazenar todos os movimentos que levaram a essa posição. Vou resolver esse problema. O problema do estado do tabuleiro será simplificado para isto: armazene a posição atual de todas as peças no tabuleiro ignorando as condições de roque, en passant, impasse e de quem é a vez .
O layout das peças pode ser amplamente tratado de duas maneiras: armazenando o conteúdo de cada quadrado ou armazenando a posição de cada peça.
Conteúdo Simples
Existem seis tipos de peças (peão, torre, cavalo, bispo, rainha e rei). Cada peça pode ser branca ou preta, portanto, um quadrado pode conter uma das 12 peças possíveis ou pode estar vazio, com 13 possibilidades. 13 pode ser armazenado em 4 bits (0-15). Portanto, a solução mais simples é armazenar 4 bits para cada quadrado vezes 64 quadrados ou 256 bits de informação.
A vantagem desse método é que a manipulação é incrivelmente fácil e rápida. Isso poderia até ser estendido adicionando mais 3 possibilidades sem aumentar os requisitos de armazenamento: um peão que se moveu 2 casas no último turno, um rei que não se moveu e uma torre que não se moveu, que cuidará de muito das questões mencionadas anteriormente.
Mas nós podemos fazer melhor.
Codificação Base 13
Muitas vezes é útil pensar na posição do conselho como um número muito grande. Isso geralmente é feito na ciência da computação. Por exemplo, o problema da parada trata um programa de computador (com razão) como um grande número.
A primeira solução trata a posição como um número de base 16 de 64 dígitos, mas como demonstrado, há redundância nesta informação (sendo as 3 possibilidades não utilizadas por “dígito”), portanto, podemos reduzir o espaço de número para 64 dígitos de base 13. É claro que isso não pode ser feito de forma tão eficiente quanto a base 16, mas economizará nos requisitos de armazenamento (e minimizar o espaço de armazenamento é nosso objetivo).
Na base 10, o número 234 é equivalente a 2 x 10 2 + 3 x 10 1 + 4 x 10 0 .
Na base 16, o número 0xA50 é equivalente a 10 x 16 2 + 5 x 16 1 + 0 x 16 0 = 2640 (decimal).
Portanto, podemos codificar nossa posição como p 0 x 13 63 + p 1 x 13 62 + ... + p 63 x 13 0 onde p i representa o conteúdo do quadrado i .
2 256 é igual a aproximadamente 1,16e77. 13 64 é igual a aproximadamente 1,96e71, o que requer 237 bits de espaço de armazenamento. Essa economia de apenas 7,5% tem um custo de custos de manipulação significativamente maiores.
Codificação de base variável
Em tabuleiros legais, certas peças não podem aparecer em certas casas. Por exemplo, os peões não podem ocorrer na primeira ou na oitava fileiras, reduzindo as possibilidades dessas casas para 11. Isso reduz as cartas possíveis para 11 16 x 13 48 = 1,35e70 (aproximadamente), exigindo 233 bits de espaço de armazenamento.
Na verdade, codificar e decodificar esses valores de e para decimal (ou binário) é um pouco mais complicado, mas pode ser feito de forma confiável e é deixado como um exercício para o leitor.
Alfabetos de largura variável
Os dois métodos anteriores podem ser descritos como codificação alfabética de largura fixa . Cada um dos 11, 13 ou 16 membros do alfabeto é substituído por outro valor. Cada “caractere” tem a mesma largura, mas a eficiência pode ser melhorada quando você considera que cada caractere não é igualmente provável.
Considere o código Morse (foto acima). Os caracteres em uma mensagem são codificados como uma sequência de traços e pontos. Esses traços e pontos são transferidos por rádio (normalmente) com uma pausa entre eles para delimitá-los.
Observe como a letra E ( a letra mais comum em inglês ) é um único ponto, a sequência mais curta possível, enquanto Z (a menos frequente) tem dois travessões e dois bipes.
Esse esquema pode reduzir significativamente o tamanho de uma mensagem esperada, mas tem o custo de aumentar o tamanho de uma sequência de caracteres aleatória.
Deve-se notar que o código Morse tem outro recurso embutido: os traços são tão longos quanto três pontos, então o código acima é criado com isso em mente para minimizar o uso de traços. Como os 1s e 0s (nossos blocos de construção) não têm esse problema, não é um recurso que precisamos replicar.
Por último, existem dois tipos de pausas no código Morse. Um pequeno descanso (o comprimento de um ponto) é usado para distinguir entre pontos e traços. Um intervalo mais longo (o comprimento de um traço) é usado para delimitar os caracteres.
Então, como isso se aplica ao nosso problema?
Codificação Huffman
Existe um algoritmo para lidar com códigos de comprimento variável denominado codificação de Huffman . A codificação de Huffman cria uma substituição de código de comprimento variável, normalmente usa a frequência esperada dos símbolos para atribuir valores mais curtos aos símbolos mais comuns.
Na árvore acima, a letra E é codificada como 000 (ou esquerda-esquerda-esquerda) e S é 1011. Deve ficar claro que esse esquema de codificação não é ambíguo .
Esta é uma distinção importante do código Morse. O código Morse tem o separador de caracteres para que ele possa fazer substituições ambíguas (por exemplo, 4 pontos podem ser H ou 2 Is), mas temos apenas 1s e 0s, então escolhemos uma substituição inequívoca.
Abaixo está uma implementação simples:
private static class Node {
private final Node left;
private final Node right;
private final String label;
private final int weight;
private Node(String label, int weight) {
this.left = null;
this.right = null;
this.label = label;
this.weight = weight;
}
public Node(Node left, Node right) {
this.left = left;
this.right = right;
label = "";
weight = left.weight + right.weight;
}
public boolean isLeaf() { return left == null && right == null; }
public Node getLeft() { return left; }
public Node getRight() { return right; }
public String getLabel() { return label; }
public int getWeight() { return weight; }
}
com dados estáticos:
private final static List<string> COLOURS;
private final static Map<string, integer> WEIGHTS;
static {
List<string> list = new ArrayList<string>();
list.add("White");
list.add("Black");
COLOURS = Collections.unmodifiableList(list);
Map<string, integer> map = new HashMap<string, integer>();
for (String colour : COLOURS) {
map.put(colour + " " + "King", 1);
map.put(colour + " " + "Queen";, 1);
map.put(colour + " " + "Rook", 2);
map.put(colour + " " + "Knight", 2);
map.put(colour + " " + "Bishop";, 2);
map.put(colour + " " + "Pawn", 8);
}
map.put("Empty", 32);
WEIGHTS = Collections.unmodifiableMap(map);
}
e:
private static class WeightComparator implements Comparator<node> {
@Override
public int compare(Node o1, Node o2) {
if (o1.getWeight() == o2.getWeight()) {
return 0;
} else {
return o1.getWeight() < o2.getWeight() ? -1 : 1;
}
}
}
private static class PathComparator implements Comparator<string> {
@Override
public int compare(String o1, String o2) {
if (o1 == null) {
return o2 == null ? 0 : -1;
} else if (o2 == null) {
return 1;
} else {
int length1 = o1.length();
int length2 = o2.length();
if (length1 == length2) {
return o1.compareTo(o2);
} else {
return length1 < length2 ? -1 : 1;
}
}
}
}
public static void main(String args[]) {
PriorityQueue<node> queue = new PriorityQueue<node>(WEIGHTS.size(),
new WeightComparator());
for (Map.Entry<string, integer> entry : WEIGHTS.entrySet()) {
queue.add(new Node(entry.getKey(), entry.getValue()));
}
while (queue.size() > 1) {
Node first = queue.poll();
Node second = queue.poll();
queue.add(new Node(first, second));
}
Map<string, node> nodes = new TreeMap<string, node>(new PathComparator());
addLeaves(nodes, queue.peek(), "");
for (Map.Entry<string, node> entry : nodes.entrySet()) {
System.out.printf("%s %s%n", entry.getKey(), entry.getValue().getLabel());
}
}
public static void addLeaves(Map<string, node> nodes, Node node, String prefix) {
if (node != null) {
addLeaves(nodes, node.getLeft(), prefix + "0");
addLeaves(nodes, node.getRight(), prefix + "1");
if (node.isLeaf()) {
nodes.put(prefix, node);
}
}
}
Uma saída possível é:
White Black
Empty 0
Pawn 110 100
Rook 11111 11110
Knight 10110 10101
Bishop 10100 11100
Queen 111010 111011
King 101110 101111
Para uma posição inicial, isso equivale a 32 x 1 + 16 x 3 + 12 x 5 + 4 x 6 = 164 bits.
Diferença de estado
Outra abordagem possível é combinar a primeira abordagem com a codificação de Huffman. Isso é baseado na suposição de que a maioria dos tabuleiros de xadrez esperados (em vez dos gerados aleatoriamente) têm mais probabilidade de, pelo menos em parte, se parecer com uma posição inicial.
Portanto, o que você faz é aplicar um XOR à posição atual da placa de 256 bits com uma posição inicial de 256 bits e, em seguida, codificá-la (usando a codificação Huffman ou, digamos, algum método de codificação de comprimento de execução ). Obviamente, isso será muito eficiente para começar (64 0s provavelmente correspondendo a 64 bits), mas aumenta o armazenamento necessário à medida que o jogo avança.
Posição da peça
Como mencionado, outra forma de atacar esse problema é, em vez disso, armazenar a posição de cada peça que o jogador possui. Isso funciona particularmente bem com posições finais em que a maioria dos quadrados estará vazia (mas na abordagem de codificação de Huffman, quadrados vazios usam apenas 1 bit de qualquer maneira).
Cada lado terá um rei e 0-15 outras peças. Por causa da promoção, a composição exata dessas peças pode variar o suficiente para que você não possa assumir que os números baseados nas posições iniciais são máximos.
A maneira lógica de dividir isso é armazenar uma posição que consiste em dois lados (branco e preto). Cada lado tem:
- Um rei: 6 bits pela localização;
- Tem peões: 1 (sim), 0 (não);
- Se sim, número de peões: 3 bits (0-7 + 1 = 1-8);
- Se sim, a localização de cada peão é codificada: 45 bits (veja abaixo);
- Número de não peões: 4 bits (0-15);
- Para cada peça: tipo (2 bits para rainha, torre, cavalo, bispo) e localização (6 bits)
Quanto à localização do peão, os peões só podem estar em 48 casas possíveis (não em 64 como as outras). Como tal, é melhor não desperdiçar os 16 valores extras que usaria com 6 bits por peão. Portanto, se você tiver 8 peões, existem 48 8 possibilidades, igualando 28.179.280.429.056. Você precisa de 45 bits para codificar tantos valores.
Isso é 105 bits por lado ou 210 bits no total. A posição inicial é o pior caso para este método e ficará substancialmente melhor à medida que você remove peças.
Deve-se ressaltar que existem menos de 48 8 possibilidades porque os peões não podem estar todos na mesma casa. O primeiro tem 48 possibilidades, o segundo 47 e assim por diante. 48 x 47 x… x 41 = 1,52e13 = armazenamento de 44 bits.
Você pode melhorar ainda mais eliminando as casas que estão ocupadas por outras peças (incluindo o outro lado) para que você possa colocar primeiro os não peões brancos, depois os não peões pretos, depois os peões brancos e por último os peões pretos. Em uma posição inicial, isso reduz os requisitos de armazenamento para 44 bits para branco e 42 bits para preto.
Abordagens Combinadas
Outra otimização possível é que cada uma dessas abordagens tem seus pontos fortes e fracos. Você poderia, digamos, escolher os 4 melhores e codificar um seletor de esquema nos primeiros dois bits e, em seguida, o armazenamento específico do esquema.
Com uma sobrecarga tão pequena, essa será de longe a melhor abordagem.
Estado do jogo
Volto ao problema de armazenar um jogo em vez de uma posição . Por causa da repetição tripla, temos que armazenar a lista de movimentos que ocorreram até este ponto.
Anotações
Uma coisa que você precisa determinar é se você está simplesmente armazenando uma lista de movimentos ou está anotando o jogo? Os jogos de xadrez são frequentemente anotados, por exemplo:
- Bb5 !! Nc4?
O movimento das brancas é marcado por dois pontos de exclamação como brilhantes, ao passo que o das pretas é visto como um erro. Veja a pontuação do xadrez .
Além disso, você também pode precisar armazenar texto livre à medida que os movimentos são descritos.
Estou assumindo que os movimentos são suficientes, portanto, não haverá anotações.
Notação Algébrica
Poderíamos simplesmente armazenar o texto do movimento aqui (“e4”, “Bxb5”, etc). Incluindo um byte de terminação, você está vendo cerca de 6 bytes (48 bits) por movimento (pior caso). Isso não é particularmente eficiente.
A segunda coisa a tentar é armazenar a localização inicial (6 bits) e a localização final (6 bits), portanto, 12 bits por movimento. Isso é significativamente melhor.
Alternativamente, podemos determinar todos os movimentos legais da posição atual de uma forma previsível e determinística e o estado que escolhemos. Isso então volta para a codificação de base variável mencionada acima. As brancas e as pretas têm 20 movimentos possíveis, cada um em seu primeiro movimento, mais no segundo e assim por diante.
Conclusão
Não há uma resposta absolutamente certa para essa pergunta. Existem muitas abordagens possíveis, das quais as acima são apenas algumas.
O que eu gosto sobre este e problemas semelhantes é que ele exige habilidades importantes para qualquer programador, como considerar o padrão de uso, determinar os requisitos com precisão e pensar em casos extremos.
Posições de xadrez tiradas como capturas de tela do Treinador de posição de xadrez .