Mise à jour: J'ai tellement aimé ce sujet que j'ai écrit des puzzles de programmation, des positions d'échecs et du codage Huffman . Si vous lisez ceci, j'ai déterminé que le seul façon de stocker un état de jeu complet est de stocker une liste complète des coups. Lisez la suite pour savoir pourquoi. J'utilise donc une version légèrement simplifiée du problème pour la disposition des pièces.
Le problème
Cette image illustre la position de départ des échecs. Les échecs se produisent sur un plateau 8x8 avec chaque joueur commençant par un ensemble identique de 16 pièces composé de 8 pions, 2 tours, 2 chevaliers, 2 évêques, 1 reine et 1 roi comme illustré ici:
Les positions sont généralement enregistrées sous la forme d'une lettre pour la colonne suivie du numéro de la ligne, de sorte que la reine des blancs est à d1. Les déplacements sont le plus souvent stockés en notation algébrique , qui est sans ambiguïté et ne spécifie généralement que les informations minimales nécessaires. Considérez cette ouverture:
- e4 e5
- Nf3 Nc6
- …
ce qui se traduit par:
- Les blancs déplacent le pion du roi de e2 à e4 (c'est la seule pièce qui peut atteindre e4 d'où «e4»);
- Le noir déplace le pion du roi de e7 à e5;
- Blanc déplace le chevalier (N) vers f3;
- Le noir déplace le chevalier vers c6.
- …
Le tableau ressemble à ceci:
Une capacité importante pour tout programmeur est de pouvoir spécifier correctement et sans ambiguïté le problème .
Alors, qu'est-ce qui manque ou est ambigu? Beaucoup comme il se trouve.
État du plateau vs état du jeu
La première chose que vous devez déterminer est de savoir si vous stockez l'état d'un jeu ou la position des pièces sur le plateau. Encoder simplement les positions des pièces est une chose mais le problème dit «tous les coups légaux ultérieurs». Le problème ne dit rien non plus sur la connaissance des mouvements jusqu'à ce point. C'est en fait un problème comme je vais l'expliquer.
Roque
Le jeu s'est déroulé comme suit:
- e4 e5
- Nf3 Nc6
- Bb5 a6
- Ba4 Bc5
Le tableau ressemble à ceci:
Le blanc a la possibilité de roquer . Une partie des exigences pour cela est que le roi et la tour concernée ne peuvent jamais avoir bougé, donc si le roi ou l'une des tours de chaque camp a bougé, il faudra stocker. Évidemment, s'ils ne sont pas sur leurs positions de départ, ils ont déménagé sinon il faut le préciser.
Il existe plusieurs stratégies qui peuvent être utilisées pour résoudre ce problème.
Premièrement, nous pourrions stocker 6 bits d'informations supplémentaires (1 pour chaque tour et roi) pour indiquer si cette pièce avait bougé. Nous pourrions rationaliser cela en ne stockant qu'un peu pour l'un de ces six carrés si la bonne pièce s'y trouve. Alternativement, nous pourrions traiter chaque pièce non déplacée comme un autre type de pièce, donc au lieu de 6 types de pièces de chaque côté (pion, tour, chevalier, évêque, reine et roi), il y en a 8 (en ajoutant la tour non déplacée et le roi immobile).
En passant
Une autre règle particulière et souvent négligée dans les échecs est En Passant .
Le jeu a progressé.
- e4 e5
- Nf3 Nc6
- Bb5 a6
- Ba4 Bc5
- OO b5
- Bb3 b4
- c4
Le pion noir sur b4 a maintenant la possibilité de déplacer son pion sur b4 vers c3 en prenant le pion blanc sur c4. Cela ne se produit qu'à la première opportunité, ce qui signifie que si le noir passe l'option maintenant, il ne peut pas la prendre au prochain coup. Nous devons donc stocker cela.
Si nous connaissons le mouvement précédent, nous pouvons certainement répondre si En Passant est possible. Alternativement, nous pouvons stocker si chaque pion de son 4ème rang vient de s'y déplacer avec un double mouvement en avant. Ou nous pouvons regarder chaque position possible de En Passant sur le plateau et avoir un drapeau pour indiquer si c'est possible ou non.
Promotion
C'est le geste de White. Si Blanc déplace son pion de h7 à h8, il peut être promu à n'importe quelle autre pièce (mais pas le roi). 99% du temps, elle est promue reine, mais parfois ce n'est pas le cas, généralement parce que cela peut forcer une impasse alors que vous gagneriez autrement. Ceci s'écrit:
- h8 = Q
C'est important dans notre problème car cela signifie que nous ne pouvons pas compter sur un nombre fixe de pièces de chaque côté. Il est tout à fait possible (mais incroyablement improbable) qu'un camp se retrouve avec 9 reines, 10 tours, 10 évêques ou 10 chevaliers si les 8 pions sont promus.
Impasse
Lorsque vous êtes dans une position à partir de laquelle vous ne pouvez pas gagner, votre meilleure tactique est d'essayer une impasse . La variante la plus probable est celle où vous ne pouvez pas effectuer de mouvement légal (généralement parce que tout mouvement met votre roi en échec). Dans ce cas, vous pouvez réclamer un tirage au sort. Celui-ci est facile à gérer.
La deuxième variante est par triple répétition . Si la même position sur le plateau se produit trois fois dans une partie (ou se produira une troisième fois au coup suivant), un match nul peut être réclamé. Les positions n'ont pas besoin de se produire dans un ordre particulier (ce qui signifie qu'il n'est pas nécessaire de suivre la même séquence de mouvements répétés trois fois). Celui-ci complique grandement le problème car vous devez vous souvenir de chaque position précédente au conseil d'administration. Si cela est une exigence du problème, la seule solution possible au problème est de stocker chaque mouvement précédent.
Enfin, il y a la règle des cinquante coups . Un joueur peut réclamer un tirage si aucun pion n'a bougé et qu'aucune pièce n'a été prise dans les cinquante mouvements consécutifs précédents, nous aurions donc besoin de stocker le nombre de mouvements depuis qu'un pion a été déplacé ou une pièce prise (le dernier des deux. Cela nécessite 6 bits (0 à 63).
A qui le tour?
Bien sûr, nous avons également besoin de savoir à qui appartient le tour et ceci est une simple information.
Deux problèmes
En raison du cas d'impasse, la seule façon faisable ou raisonnable de stocker l'état du jeu est de stocker tous les mouvements qui ont conduit à cette position. Je vais m'attaquer à ce problème. Le problème de l'état du plateau sera simplifié à ceci: stocker la position actuelle de toutes les pièces sur le plateau en ignorant les conditions de roque, en passant, d'impasse et à qui appartient le tour .
La disposition des pièces peut être gérée globalement de deux manières: en stockant le contenu de chaque carré ou en stockant la position de chaque pièce.
Contenu simple
Il existe six types de pièces (pion, tour, chevalier, évêque, reine et roi). Chaque pièce peut être blanche ou noire donc un carré peut contenir l'une des 12 pièces possibles ou il peut être vide donc il y a 13 possibilités. 13 peut être stocké en 4 bits (0-15) La solution la plus simple est donc de stocker 4 bits pour chaque carré multiplié par 64 carrés ou 256 bits d'information.
L'avantage de cette méthode est que la manipulation est incroyablement simple et rapide. Cela pourrait même être prolongé en ajoutant 3 possibilités supplémentaires sans augmenter les exigences de stockage: un pion qui a bougé de 2 cases au dernier tour, un roi qui n'a pas bougé et une tour qui n'a pas bougé, ce qui en fera beaucoup. des problèmes mentionnés précédemment.
Mais nous pouvons faire mieux.
Encodage Base 13
Il est souvent utile de considérer la position du conseil comme un très grand nombre. Cela se fait souvent en informatique. Par exemple, le problème d'arrêt traite (à juste titre) un programme informatique comme un grand nombre.
La première solution traite la position comme un nombre à 64 chiffres en base 16 mais comme démontré il y a une redondance dans cette information (étant les 3 possibilités inutilisées par «chiffre») afin que nous puissions réduire l'espace numérique à 64 base 13 chiffres. Bien sûr, cela ne peut pas être fait aussi efficacement que la base 16, mais cela permettra d'économiser sur les besoins de stockage (et minimiser l'espace de stockage est notre objectif).
En base 10, le nombre 234 équivaut à 2 x 10 2 + 3 x 10 1 + 4 x 10 0 .
En base 16, le nombre 0xA50 équivaut à 10 x 16 2 + 5 x 16 1 + 0 x 16 0 = 2640 (décimal).
Nous pouvons donc encoder notre position comme p 0 x 13 63 + p 1 x 13 62 + ... + p 63 x 13 0 où p i représente le contenu du carré i .
2 256 équivaut à environ 1,16e77. 13 64 équivaut à environ 1,96e71, ce qui nécessite 237 bits d'espace de stockage. Cette économie de seulement 7,5% entraîne une augmentation significative des coûts de manipulation.
Encodage de base variable
Dans les planches légales, certaines pièces ne peuvent pas apparaître dans certaines cases. Par exemple, les pions ne peuvent pas apparaître au premier ou au huitième rang, réduisant les possibilités pour ces carrés à 11. Cela réduit les planches possibles à 11 16 x 13 48 = 1,35e70 (environ), ce qui nécessite 233 bits d'espace de stockage.
En fait, le codage et le décodage de telles valeurs vers et à partir de décimal (ou binaire) est un peu plus compliqué, mais cela peut être fait de manière fiable et est laissé comme un exercice au lecteur.
Alphabets à largeur variable
Les deux méthodes précédentes peuvent toutes deux être décrites comme un codage alphabétique à largeur fixe . Chacun des 11, 13 ou 16 membres de l'alphabet est remplacé par une autre valeur. Chaque «caractère» a la même largeur, mais l'efficacité peut être améliorée si l'on considère que chaque caractère n'est pas également probable.
Considérez le code Morse (photo ci-dessus). Les caractères d'un message sont codés sous la forme d'une séquence de tirets et de points. Ces tirets et points sont transférés par radio (généralement) avec une pause entre eux pour les délimiter.
Remarquez que la lettre E (la lettre la plus courante en anglais ) est un seul point, la séquence la plus courte possible, tandis que Z (la moins fréquente) est composée de deux tirets et de deux bips.
Un tel schéma peut réduire considérablement la taille d'un message attendu mais se fait au prix de l'augmentation de la taille d'une séquence de caractères aléatoires.
Il convient de noter que le code Morse a une autre fonctionnalité intégrée: les tirets sont aussi longs que trois points, donc le code ci-dessus est créé dans cet esprit pour minimiser l'utilisation des tirets. Puisque les 1 et les 0 (nos blocs de construction) n'ont pas ce problème, ce n'est pas une fonctionnalité que nous devons répliquer.
Enfin, il existe deux types de repos en code Morse. Un court repos (la longueur d'un point) est utilisé pour distinguer les points et les tirets. Un espace plus long (la longueur d'un tiret) est utilisé pour délimiter les caractères.
Alors, comment cela s'applique-t-il à notre problème?
Codage Huffman
Il existe un algorithme pour traiter les codes de longueur variable appelé codage Huffman . Le codage Huffman crée une substitution de code de longueur variable, utilise généralement la fréquence attendue des symboles pour attribuer des valeurs plus courtes aux symboles les plus courants.
Dans l'arborescence ci-dessus, la lettre E est codée comme 000 (ou gauche-gauche-gauche) et S est 1011. Il doit être clair que ce schéma de codage est sans ambiguïté .
C'est une distinction importante avec le code Morse. Le code Morse a le séparateur de caractères, donc il peut faire une substitution ambiguë (par exemple, 4 points peuvent être H ou 2 Is) mais nous n'avons que des 1 et des 0 donc nous choisissons une substitution non ambiguë à la place.
Voici une implémentation simple:
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; }
}
avec des données statiques:
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);
}
et:
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);
}
}
}
Une sortie possible est:
White Black
Empty 0
Pawn 110 100
Rook 11111 11110
Knight 10110 10101
Bishop 10100 11100
Queen 111010 111011
King 101110 101111
Pour une position de départ, cela équivaut à 32 x 1 + 16 x 3 + 12 x 5 + 4 x 6 = 164 bits.
Différence d'état
Une autre approche possible consiste à combiner la toute première approche avec le codage Huffman. Ceci est basé sur l'hypothèse que la plupart des échiquiers attendus (plutôt que ceux générés aléatoirement) sont plus susceptibles qu'improbables, au moins en partie, de ressembler à une position de départ.
Donc, ce que vous faites est XOR la position actuelle de la carte de 256 bits avec une position de départ de 256 bits, puis encodez cela (en utilisant le codage Huffman ou, par exemple, une méthode de codage de longueur d'exécution ). Evidemment cela sera très efficace au départ (64 0s correspondant probablement à 64 bits) mais augmentera le stockage requis au fur et à mesure que le jeu progresse.
Position de la pièce
Comme mentionné, une autre manière d'attaquer ce problème est de stocker à la place la position de chaque pièce d'un joueur. Cela fonctionne particulièrement bien avec les positions de fin de partie où la plupart des carrés seront vides (mais dans l'approche de codage de Huffman, les carrés vides n'utilisent de toute façon que 1 bit).
Chaque camp aura un roi et 0 à 15 autres pièces. En raison de la promotion, la composition exacte de ces pièces peut varier suffisamment pour que vous ne puissiez pas supposer que les chiffres basés sur les positions de départ sont des maxima.
La manière logique de diviser ceci est de stocker une position composée de deux côtés (blanc et noir). Chaque côté a:
- Un roi: 6 bits pour l'emplacement;
- A des pions: 1 (oui), 0 (non);
- Si oui, nombre de pions: 3 bits (0-7 + 1 = 1-8);
- Si oui, l'emplacement de chaque pion est codé: 45 bits (voir ci-dessous);
- Nombre de non-pions: 4 bits (0-15);
- Pour chaque pièce: type (2 bits pour reine, tour, chevalier, évêque) et emplacement (6 bits)
Quant à l'emplacement des pions, les pions ne peuvent être que sur 48 cases possibles (pas 64 comme les autres). En tant que tel, il vaut mieux ne pas gaspiller les 16 valeurs supplémentaires que l'utilisation de 6 bits par pion utiliserait. Donc, si vous avez 8 pions, il y a 48 8 possibilités, soit 28 179 280 429 056. Vous avez besoin de 45 bits pour encoder autant de valeurs.
Cela représente 105 bits par côté ou 210 bits au total. La position de départ est cependant le pire des cas pour cette méthode et elle s'améliorera considérablement à mesure que vous retirerez des pièces.
Il faut préciser qu'il y a moins de 48 8 possibilités car les pions ne peuvent pas tous être dans la même case Le premier a 48 possibilités, le second 47 et ainsi de suite. 48 x 47 x… x 41 = 1,52e13 = stockage de 44 bits.
Vous pouvez encore améliorer cela en éliminant les cases qui sont occupées par d'autres pièces (y compris l'autre côté) afin que vous puissiez d'abord placer les non-pions blancs puis les non-pions noirs, puis les pions blancs et enfin les pions noirs. En position de départ, cela réduit les besoins de stockage à 44 bits pour le blanc et à 42 bits pour le noir.
Approches combinées
Une autre optimisation possible est que chacune de ces approches a ses forces et ses faiblesses. Vous pouvez, par exemple, choisir les 4 meilleurs, puis encoder un sélecteur de schéma dans les deux premiers bits, puis le stockage spécifique au schéma après cela.
Avec des frais généraux aussi faibles, ce sera de loin la meilleure approche.
État du jeu
Je reviens sur le problème du stockage d'un jeu plutôt que d'une position . En raison de la triple répétition, nous devons stocker la liste des mouvements qui se sont produits jusqu'à présent.
Annotations
Une chose que vous devez déterminer est de stocker simplement une liste de coups ou d'annoter le jeu? Les parties d'échecs sont souvent annotées, par exemple:
- Bb5 !! Nc4?
Le coup de White est marqué par deux points d'exclamation comme brillant alors que celui de Black est considéré comme une erreur. Voir Ponctuation des échecs .
De plus, vous pourriez également avoir besoin de stocker du texte libre au fur et à mesure que les mouvements sont décrits.
Je suppose que les mouvements sont suffisants, il n'y aura donc pas d'annotations.
Notation algébrique
Nous pourrions simplement stocker le texte du mouvement ici («e4», «Bxb5», etc.). Y compris un octet de fin, vous regardez environ 6 octets (48 bits) par déplacement (pire des cas). Ce n'est pas particulièrement efficace.
La deuxième chose à essayer est de stocker l'emplacement de départ (6 bits) et l'emplacement de fin (6 bits) donc 12 bits par déplacement. C'est nettement mieux.
Alternativement, nous pouvons déterminer tous les mouvements légaux à partir de la position actuelle d'une manière prévisible et déterministe et l'état que nous avons choisi. Cela revient ensuite au codage de base variable mentionné ci-dessus. Les Blancs et les Noirs ont 20 coups possibles chacun sur leur premier coup, plus sur le second et ainsi de suite.
Conclusion
Il n'y a pas de réponse absolument correcte à cette question. Il existe de nombreuses approches possibles dont celles ci-dessus ne sont que quelques-unes.
Ce que j'aime dans ce problème et dans des problèmes similaires, c'est qu'il exige des capacités importantes pour tout programmeur, telles que la prise en compte du modèle d'utilisation, la détermination précise des exigences et la réflexion sur les cas secondaires.
Positions d'échecs prises comme captures d'écran de Chess Position Trainer .