Update: Dieses Thema hat mir so gut gefallen, dass ich Programmierpuzzles, Schachpositionen und Huffman-Codierung geschrieben habe . Wenn Sie dies durchlesen, habe ich festgestellt, dass die einzige Möglichkeit, einen vollständigen Spielstatus zu speichern, darin besteht, eine vollständige Liste der Züge zu speichern. Lesen Sie weiter, warum. Daher verwende ich eine leicht vereinfachte Version des Problems für das Stücklayout.
Das Problem
Dieses Bild zeigt die Startposition Schach. Schach findet auf einem 8x8-Brett statt, wobei jeder Spieler mit einem identischen Satz von 16 Figuren beginnt, die aus 8 Bauern, 2 Türmen, 2 Rittern, 2 Bischöfen, 1 Königin und 1 König bestehen, wie hier dargestellt:
![Start Schachposition](https://i.stack.imgur.com/EthuN.png)
Positionen werden im Allgemeinen als Buchstabe für die Spalte aufgezeichnet, gefolgt von der Nummer für die Zeile, sodass die Königin von Weiß bei d1 liegt. Bewegungen werden meistens in algebraischer Notation gespeichert , die eindeutig ist und im Allgemeinen nur die erforderlichen minimalen Informationen angibt. Betrachten Sie diese Öffnung:
- e4 e5
- Sf3 Sc6
- …
was übersetzt bedeutet:
- Weiß bewegt den Bauern des Königs von e2 nach e4 (es ist das einzige Stück, das zu e4 gelangen kann, daher „e4“);
- Schwarz bewegt den Bauern des Königs von e7 auf e5;
- Weiß bewegt den Ritter (N) auf f3;
- Schwarz bewegt den Ritter auf c6.
- …
Das Board sieht so aus:
![frühe Eröffnung](https://i.stack.imgur.com/dod6p.png)
Eine wichtige Fähigkeit für jeden Programmierer besteht darin , das Problem korrekt und eindeutig spezifizieren zu können .
Was fehlt oder ist nicht eindeutig? Viel, wie sich herausstellt.
Board State vs Game State
Als erstes müssen Sie feststellen, ob Sie den Status eines Spiels oder die Position der Spielsteine auf dem Brett speichern. Es ist eine Sache, einfach die Positionen der Teile zu kodieren, aber das Problem lautet „alle nachfolgenden rechtlichen Schritte“. Das Problem sagt auch nichts darüber aus, die Bewegungen bis zu diesem Punkt zu kennen. Das ist eigentlich ein Problem, wie ich erklären werde.
Rochade
Das Spiel ist wie folgt verlaufen:
- e4 e5
- Sf3 Sc6
- Lb5 a6
- Ba4 Lc5
Das Board sieht wie folgt aus:
![später öffnen](https://i.stack.imgur.com/XxBaT.png)
Weiß hat die Möglichkeit zu schlössern . Ein Teil der Anforderungen hierfür ist, dass sich der König und der betreffende Turm niemals bewegt haben können. Ob sich also der König oder einer der Türme jeder Seite bewegt hat, muss gespeichert werden. Wenn sie nicht auf ihren Startpositionen sind, sind sie offensichtlich umgezogen, andernfalls muss dies angegeben werden.
Es gibt verschiedene Strategien, um dieses Problem zu lösen.
Erstens könnten wir zusätzliche 6 Informationsbits speichern (1 für jeden Turm und König), um anzuzeigen, ob sich dieses Stück bewegt hat. Wir könnten dies rationalisieren, indem wir nur ein bisschen für eines dieser sechs Quadrate speichern, wenn das richtige Stück darin ist. Alternativ könnten wir jedes unbewegte Stück als einen anderen Stücktyp behandeln. Anstelle von 6 Stücktypen auf jeder Seite (Bauer, Turm, Ritter, Bischof, Königin und König) gibt es 8 (Hinzufügen von unbewegtem Turm und unbewegtem König).
En Passant
Eine andere eigenartige und oft vernachlässigte Regel im Schach ist En Passant .
![en passant](https://i.stack.imgur.com/feWtq.png)
Das Spiel ist fortgeschritten.
- e4 e5
- Sf3 Sc6
- Lb5 a6
- Ba4 Lc5
- OO b5
- Bb3 b4
- c4
Schwarzs Bauer auf b4 hat jetzt die Möglichkeit, seinen Bauern auf b4 nach c3 zu verschieben und den weißen Bauern auf c4 zu nehmen. Dies geschieht nur bei der ersten Gelegenheit, dh wenn Schwarz die Option jetzt weitergibt, kann er sie beim nächsten Zug nicht mehr ausführen. Also müssen wir das speichern.
Wenn wir den vorherigen Schritt kennen, können wir definitiv antworten, ob En Passant möglich ist. Alternativ können wir speichern, ob jeder Bauer auf seinem 4. Rang gerade mit einem doppelten Vorwärtszug dorthin gezogen ist. Oder wir können uns jede mögliche En Passant-Position auf dem Brett ansehen und eine Flagge haben, um anzuzeigen, ob dies möglich ist oder nicht.
Beförderung
![Bauernförderung](https://i.stack.imgur.com/dnxKa.png)
Es ist der Schritt von Weiß. Wenn Weiß seinen Bauern auf h7 nach h8 bewegt, kann er zu jeder anderen Figur befördert werden (aber nicht zum König). 99% der Zeit wird es zu einer Königin befördert, aber manchmal nicht, normalerweise, weil dies eine Pattsituation erzwingen kann, wenn Sie sonst gewinnen würden. Dies ist geschrieben als:
- h8 = Q.
Dies ist bei unserem Problem wichtig, da wir uns nicht darauf verlassen können, dass auf jeder Seite eine feste Anzahl von Teilen vorhanden ist. Es ist durchaus möglich (aber unglaublich unwahrscheinlich), dass eine Seite 9 Königinnen, 10 Türme, 10 Bischöfe oder 10 Ritter hat, wenn alle 8 Bauern befördert werden.
Patt
Wenn Sie sich in einer Position befinden, aus der Sie nicht gewinnen können, versuchen Sie am besten, eine Pattsituation zu erreichen . Die wahrscheinlichste Variante ist, wenn Sie keinen legalen Zug machen können (normalerweise, weil jeder Zug, wenn Sie Ihren König in Schach halten). In diesem Fall können Sie eine Auslosung beantragen. Dieser ist leicht zu bedienen.
Die zweite Variante besteht aus dreifacher Wiederholung . Wenn dieselbe Brettposition in einem Spiel dreimal vorkommt (oder im nächsten Zug ein drittes Mal vorkommt), kann ein Unentschieden beansprucht werden. Die Positionen müssen nicht in einer bestimmten Reihenfolge auftreten (was bedeutet, dass nicht dieselbe Abfolge von Zügen dreimal wiederholt werden muss). Dies erschwert das Problem erheblich, da Sie sich an jede vorherige Boardposition erinnern müssen. Wenn dies eine Anforderung des Problems ist, besteht die einzig mögliche Lösung des Problems darin, jeden vorherigen Zug zu speichern.
Schließlich gibt es die Fünfzig-Zug-Regel . Ein Spieler kann ein Unentschieden beanspruchen, wenn sich in den letzten fünfzig aufeinanderfolgenden Zügen kein Bauer bewegt hat und keine Figur genommen wurde. Daher müssten wir speichern, wie viele Züge seit dem Bewegen eines Bauern oder einer genommenen Figur (die letzte der beiden) erforderlich sind 6 Bits (0-63).
Wer ist dran?
Natürlich müssen wir auch wissen, wer an der Reihe ist, und dies ist eine einzelne Information.
Zwei Probleme
Aufgrund des Pattfalls besteht die einzig mögliche oder sinnvolle Möglichkeit, den Spielstatus zu speichern, darin, alle Bewegungen zu speichern, die zu dieser Position geführt haben. Ich werde dieses eine Problem angehen. Das Problem mit dem Board-Status wird dadurch vereinfacht: Speichern Sie die aktuelle Position aller Teile auf dem Board, ohne Rücksicht auf Rochade, en passant, Patt-Bedingungen und wer an der Reihe ist .
Das Stücklayout kann auf zwei Arten behandelt werden: durch Speichern des Inhalts jedes Quadrats oder durch Speichern der Position jedes Stücks.
Einfacher Inhalt
Es gibt sechs Stückarten (Bauer, Turm, Ritter, Bischof, Königin und König). Jedes Stück kann weiß oder schwarz sein, so dass ein Quadrat eines von 12 möglichen Stücken enthalten kann, oder es kann leer sein, so dass es 13 Möglichkeiten gibt. 13 kann in 4 Bits (0-15) gespeichert werden. Die einfachste Lösung besteht also darin, 4 Bits für jedes Quadrat mal 64 Quadrate oder 256 Bits an Informationen zu speichern.
Der Vorteil dieser Methode ist, dass die Manipulation unglaublich ist einfach und schnell ist. Dies könnte sogar erweitert werden, indem 3 weitere Möglichkeiten hinzugefügt werden, ohne die Speicheranforderungen zu erhöhen: ein Bauer, der in der letzten Runde 2 Felder bewegt hat, ein König, der sich nicht bewegt hat, und ein Turm, der sich nicht bewegt hat, was viel zu bieten hat von zuvor erwähnten Problemen.
Aber wir können es besser machen.
Base 13-Codierung
Es ist oft hilfreich, sich die Vorstandsposition als eine sehr große Zahl vorzustellen. Dies geschieht häufig in der Informatik. Zum Beispiel behandelt das Problem des Anhaltens ein Computerprogramm (zu Recht) als eine große Anzahl.
Bei der ersten Lösung wird die Position als 64-stellige Basis-16-Zahl behandelt. Wie gezeigt, sind diese Informationen jedoch redundant (dies sind die 3 nicht verwendeten Möglichkeiten pro „Ziffer“), sodass wir den Zahlenraum auf 64-Basis-13-Stellen reduzieren können. Natürlich kann dies nicht so effizient durchgeführt werden wie Base 16, aber es spart Speicherbedarf (und die Minimierung des Speicherplatzes ist unser Ziel).
In Basis 10 entspricht die Zahl 234 2 x 10 2 + 3 x 10 1 + 4 x 10 0 .
In Basis 16 entspricht die Zahl 0xA50 10 x 16 2 + 5 x 16 1 + 0 x 16 0 = 2640 (dezimal).
Wir können also unsere Position als p 0 x 13 63 + p 1 x 13 62 + ... + p 63 x 13 0 codieren, wobei p i den Inhalt des Quadrats i darstellt .
2 256 entspricht ungefähr 1,16e77. 13 64 entspricht ungefähr 1,96e71, was 237 Bit Speicherplatz erfordert. Diese Einsparung von nur 7,5% führt zu deutlich erhöhten Manipulationskosten.
Variable Basiskodierung
In juristischen Gremien können bestimmte Teile nicht auf bestimmten Feldern erscheinen. Zum Beispiel können Bauern nicht in der ersten oder achten Reihe auftreten, was die Möglichkeiten für diese Quadrate auf 11 reduziert. Dadurch werden die möglichen Bretter auf 11 16 x 13 48 = 1,35e70 (ungefähr) reduziert , was 233 Bit Speicherplatz erfordert.
Das eigentliche Codieren und Decodieren solcher Werte zu und von Dezimal (oder Binär) ist etwas komplizierter, kann jedoch zuverlässig durchgeführt werden und wird dem Leser als Übung überlassen.
Alphabete mit variabler Breite
Die beiden vorhergehenden Methoden können beide als alphabetische Codierung mit fester Breite beschrieben werden . Jedes der 11, 13 oder 16 Mitglieder des Alphabets wird durch einen anderen Wert ersetzt. Jedes „Zeichen“ hat die gleiche Breite, aber die Effizienz kann verbessert werden, wenn Sie bedenken, dass nicht jedes Zeichen gleich wahrscheinlich ist.
![Morse-Code](https://i.stack.imgur.com/xjZLN.gif)
Betrachten Sie den Morsecode (siehe Abbildung oben). Zeichen in einer Nachricht werden als Folge von Strichen und Punkten codiert. Diese Striche und Punkte werden (normalerweise) über Funk mit einer Pause zwischen ihnen übertragen, um sie abzugrenzen.
Beachten Sie, dass der Buchstabe E ( der häufigste Buchstabe im Englischen ) ein einzelner Punkt ist, die kürzestmögliche Folge, während Z (der am wenigsten häufige) zwei Striche und zwei Pieptöne enthält.
Ein solches Schema kann die Größe einer erwarteten Nachricht erheblich verringern , geht jedoch zu Lasten der Vergrößerung einer zufälligen Zeichenfolge.
Es sollte beachtet werden, dass Morsecode eine weitere integrierte Funktion hat: Bindestriche sind so lang wie drei Punkte, daher wird der obige Code in diesem Sinne erstellt, um die Verwendung von Bindestrichen zu minimieren. Da 1s und 0s (unsere Bausteine) dieses Problem nicht haben, müssen wir diese Funktion nicht replizieren.
Schließlich gibt es zwei Arten von Pausen im Morsecode. Eine kurze Pause (die Länge eines Punktes) wird verwendet, um zwischen Punkten und Strichen zu unterscheiden. Eine längere Lücke (die Länge eines Strichs) wird verwendet, um Zeichen abzugrenzen.
Wie trifft dies auf unser Problem zu?
Huffman-Codierung
Es gibt einen Algorithmus für den Umgang mit Codes variabler Länge, der als Huffman-Codierung bezeichnet wird . Die Huffman-Codierung erzeugt eine Codesubstitution variabler Länge und verwendet normalerweise die erwartete Häufigkeit der Symbole, um den häufigeren Symbolen kürzere Werte zuzuweisen.
![Huffman-Codebaum](https://upload.wikimedia.org/wikipedia/commons/thumb/8/82/Huffman_tree_2.svg/500px-Huffman_tree_2.svg.png)
In dem obigen Baum ist der Buchstabe E als 000 (oder links-links-links) und S als 1011 codiert. Es sollte klar sein, dass dieses Codierungsschema eindeutig ist .
Dies ist ein wichtiger Unterschied zum Morsecode. Morsecode hat das Zeichentrennzeichen, so dass es ansonsten eine mehrdeutige Ersetzung durchführen kann (z. B. 4 Punkte können H oder 2 Is sein), aber wir haben nur 1s und 0s, also wählen wir stattdessen eine eindeutige Ersetzung.
Unten ist eine einfache Implementierung:
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; }
}
mit statischen Daten:
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);
}
und:
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);
}
}
}
Eine mögliche Ausgabe ist:
White Black
Empty 0
Pawn 110 100
Rook 11111 11110
Knight 10110 10101
Bishop 10100 11100
Queen 111010 111011
King 101110 101111
Für eine Startposition entspricht dies 32 x 1 + 16 x 3 + 12 x 5 + 4 x 6 = 164 Bit.
Zustandsunterschied
Ein anderer möglicher Ansatz besteht darin, den allerersten Ansatz mit der Huffman-Codierung zu kombinieren. Dies basiert auf der Annahme, dass die meisten erwarteten Schachbretter (und nicht zufällig generierte) zumindest teilweise eher einer Startposition ähneln.
Was Sie also tun, ist XOR die aktuelle 256-Bit-Kartenposition mit einer 256-Bit-Startposition und codieren diese dann (unter Verwendung der Huffman-Codierung oder beispielsweise einer Methode zur Lauflängencodierung ). Offensichtlich ist dies zunächst sehr effizient (64 0s entsprechen wahrscheinlich 64 Bit), erhöht jedoch den Speicherbedarf im Verlauf des Spiels.
Stückposition
Wie bereits erwähnt, besteht eine andere Möglichkeit, dieses Problem anzugreifen, darin, die Position jedes Stücks eines Spielers zu speichern. Dies funktioniert besonders gut bei Endspielpositionen, an denen die meisten Quadrate leer sind (beim Huffman-Codierungsansatz verwenden leere Quadrate ohnehin nur 1 Bit).
Jede Seite hat einen König und 0-15 andere Teile. Aufgrund der Beförderung kann die genaue Zusammensetzung dieser Teile so unterschiedlich sein, dass Sie nicht davon ausgehen können, dass die auf den Startpositionen basierenden Zahlen Maxima sind.
Der logische Weg, dies aufzuteilen, besteht darin, eine Position zu speichern, die aus zwei Seiten besteht (Weiß und Schwarz). Jede Seite hat:
- Ein König: 6 Bits für den Ort;
- Hat Bauern: 1 (ja), 0 (nein);
- Wenn ja, Anzahl der Bauern: 3 Bits (0-7 + 1 = 1-8);
- Wenn ja, wird die Position jedes Bauern codiert: 45 Bit (siehe unten);
- Anzahl der Nicht-Bauern: 4 Bits (0-15);
- Für jedes Stück: Typ (2 Bits für Königin, Turm, Ritter, Bischof) und Ort (6 Bits)
Was den Bauernstandort betrifft, können sich die Bauern nur auf 48 möglichen Feldern befinden (nicht auf 64 wie die anderen). Daher ist es besser, nicht die zusätzlichen 16 Werte zu verschwenden, die bei Verwendung von 6 Bits pro Bauer verwendet würden. Wenn Sie also 8 Bauern haben, gibt es 48 8 Möglichkeiten, was 28.179.280.429.056 entspricht. Sie benötigen 45 Bit, um so viele Werte zu codieren.
Das sind 105 Bit pro Seite oder insgesamt 210 Bit. Die Ausgangsposition ist jedoch der schlechteste Fall für diese Methode und wird beim Entfernen von Teilen wesentlich besser.
Es sollte darauf hingewiesen werden, dass es weniger als 48 8 Möglichkeiten gibt, da sich die Bauern nicht alle auf demselben Feld befinden können. Die erste hat 48 Möglichkeiten, die zweite 47 und so weiter. 48 x 47 x… x 41 = 1,52e13 = 44-Bit-Speicher.
Sie können dies weiter verbessern, indem Sie die Felder entfernen, die von anderen Teilen (einschließlich der anderen Seite) belegt werden, sodass Sie zuerst die weißen Nichtbauern, dann die schwarzen Nichtbauern, dann die weißen Bauern und zuletzt die schwarzen Bauern platzieren können. In einer Startposition reduziert dies den Speicherbedarf auf 44 Bit für Weiß und 42 Bit für Schwarz.
Kombinierte Ansätze
Eine weitere mögliche Optimierung besteht darin, dass jeder dieser Ansätze seine Stärken und Schwächen hat. Sie könnten beispielsweise die besten 4 auswählen und dann einen Schemaselektor in den ersten beiden Bits und anschließend den schemaspezifischen Speicher codieren.
Mit dem so geringen Overhead wird dies bei weitem der beste Ansatz sein.
Spielstatus
Ich komme auf das Problem zurück, ein Spiel statt einer Position zu speichern . Aufgrund der dreifachen Wiederholung müssen wir die Liste der Bewegungen speichern, die bis zu diesem Punkt aufgetreten sind.
Anmerkungen
Eine Sache, die Sie feststellen müssen, ist, dass Sie einfach eine Liste von Zügen speichern oder das Spiel mit Anmerkungen versehen. Schachspiele werden oft kommentiert, zum Beispiel:
- Lb5 !! Sc4?
Der Zug von Weiß wird durch zwei Ausrufezeichen als brillant markiert, während der von Schwarz als Fehler angesehen wird. Siehe Schachpunktion .
Außerdem müssen Sie möglicherweise freien Text speichern, wenn die Bewegungen beschrieben werden.
Ich gehe davon aus, dass die Bewegungen ausreichend sind, damit es keine Anmerkungen gibt.
Algebraische Notation
Wir könnten einfach den Text des Umzugs hier speichern ("e4", "Bxb5" usw.). Einschließlich eines Abschlussbytes sehen Sie ungefähr 6 Bytes (48 Bit) pro Bewegung (schlimmster Fall). Das ist nicht besonders effizient.
Der zweite Versuch besteht darin, den Startort (6 Bit) und den Endort (6 Bit) so zu speichern, dass 12 Bit pro Zug vorhanden sind. Das ist deutlich besser.
Alternativ können wir alle rechtlichen Schritte von der aktuellen Position auf vorhersehbare und deterministische Weise und in dem von uns gewählten Zustand bestimmen. Dies geht dann auf die oben erwähnte variable Basiscodierung zurück. Weiß und Schwarz haben jeweils 20 mögliche Züge beim ersten Zug, mehr beim zweiten und so weiter.
Fazit
Es gibt keine absolut richtige Antwort auf diese Frage. Es gibt viele mögliche Ansätze, von denen die oben genannten nur einige sind.
Was ich an diesem und ähnlichen Problemen mag, ist, dass es Fähigkeiten erfordert, die für jeden Programmierer wichtig sind, wie das Verwendungsmuster zu berücksichtigen, Anforderungen genau zu bestimmen und über Eckfälle nachzudenken.
Schachpositionen als Screenshots vom Schachpositionstrainer .