Aggiornamento: mi è piaciuto così tanto questo argomento che ho scritto Puzzle di programmazione, Posizioni degli scacchi e Codifica di Huffman . Se leggi questo articolo, ho determinato che l' unico modo per memorizzare uno stato di gioco completo è memorizzare un elenco completo di mosse. Continua a leggere per scoprire il motivo. Quindi uso una versione leggermente semplificata del problema per il layout dei pezzi.
Il problema
Questa immagine illustra la posizione iniziale degli scacchi. Gli scacchi si svolgono su una scacchiera 8x8 con ogni giocatore che inizia con un set identico di 16 pezzi composto da 8 pedoni, 2 torri, 2 cavalieri, 2 alfieri, 1 regina e 1 re come illustrato qui:
Le posizioni sono generalmente registrate come una lettera per la colonna seguita dal numero per la riga, quindi la regina del bianco è in d1. Le mosse sono spesso memorizzate in notazione algebrica , che non è ambigua e generalmente specifica solo le informazioni minime necessarie. Considera questa apertura:
- e4 e5
- Nf3 Nc6
- ...
che si traduce in:
- Il bianco muove il pedone del re da e2 a e4 (è l'unico pezzo che può arrivare a e4 da qui "e4");
- Il nero muove il pedone del re da e7 a e5;
- Il bianco sposta il cavallo (N) in f3;
- Il nero sposta il cavaliere in c6.
- ...
La scheda ha questo aspetto:
Una capacità importante per qualsiasi programmatore è quella di essere in grado di specificare correttamente e in modo univoco il problema .
Quindi cosa manca o cosa è ambiguo? A quanto pare.
Board State vs Game State
La prima cosa che devi determinare è se stai memorizzando lo stato di una partita o la posizione dei pezzi sul tabellone. Codificare semplicemente le posizioni dei pezzi è una cosa, ma il problema dice "tutte le successive mosse legali". Il problema inoltre non dice nulla sulla conoscenza delle mosse fino a questo punto. Questo in realtà è un problema come spiegherò.
Arrocco
Il gioco è proceduto come segue:
- e4 e5
- Nf3 Nc6
- Si b5 a6
- Ba4 Bc5
Il tabellone appare come segue:
Il bianco ha la possibilità di arroccare . Parte dei requisiti per questo è che il re e la torre in questione non possono mai essersi mossi, quindi se il re o una delle due torri di ciascuna parte ha mosso dovrà essere immagazzinato. Ovviamente se non sono nelle posizioni di partenza si sono spostati altrimenti è necessario specificarlo.
Esistono diverse strategie che possono essere utilizzate per affrontare questo problema.
In primo luogo, potremmo memorizzare 6 bit di informazioni extra (1 per ogni torre e re) per indicare se quel pezzo si è mosso. Potremmo semplificarlo memorizzando solo un po 'per uno di questi sei quadrati se il pezzo giusto si trova in esso. In alternativa potremmo trattare ogni pezzo non mosso come un altro tipo di pezzo, quindi invece di 6 tipi di pezzo su ciascun lato (pedone, torre, cavaliere, alfiere, regina e re) ce ne sono 8 (aggiungendo torre immobile e re immobile).
En Passant
Un'altra regola peculiare e spesso trascurata negli scacchi è En Passant .
Il gioco è andato avanti.
- e4 e5
- Nf3 Nc6
- Si b5 a6
- Ba4 Bc5
- OO b5
- Si b3 b4
- c4
Il pedone nero in b4 ora ha la possibilità di muovere il suo pedone in b4 in c3 prendendo il pedone bianco in c4. Questo accade solo alla prima opportunità, il che significa che se il Nero passa l'opzione ora non può eseguire la mossa successiva. Quindi dobbiamo archiviarlo.
Se conosciamo la mossa precedente possiamo sicuramente rispondere se En Passant è possibile. In alternativa possiamo memorizzare se ogni pedone sul suo 4 ° rango si è appena spostato lì con una doppia mossa in avanti. Oppure possiamo guardare ogni possibile posizione di En Passant sul tabellone e avere una bandiera per indicare se è possibile o meno.
Promozione
È la mossa di White. Se il Bianco muove il suo pedone da h7 a h8, può essere promosso a qualsiasi altro pezzo (ma non al re). Il 99% delle volte viene promosso a Regina ma a volte non lo è, in genere perché ciò potrebbe forzare uno stallo quando altrimenti vinceresti. Questo è scritto come:
- h8 = Q
Questo è importante nel nostro problema perché significa che non possiamo contare sul fatto che ci sia un numero fisso di pezzi su ciascun lato. È del tutto possibile (ma incredibilmente improbabile) che una parte finisca con 9 regine, 10 torri, 10 alfieri o 10 cavalieri se tutte le 8 pedine vengono promosse.
Stallo
Quando ti trovi in una posizione dalla quale non puoi vincere, la tua migliore tattica è tentare lo stallo . La variante più probabile è quella in cui non puoi fare una mossa legale (di solito perché qualsiasi mossa quando metti il tuo re sotto controllo). In questo caso puoi richiedere un pareggio. Questo è facile da soddisfare.
La seconda variante consiste nella triplice ripetizione . Se la stessa posizione sul tabellone si verifica tre volte in una partita (o si verificherà una terza volta nella mossa successiva), è possibile richiedere la patta. Le posizioni non devono necessariamente verificarsi in un ordine particolare (il che significa che non deve essere ripetuta tre volte la stessa sequenza di mosse). Questo complica enormemente il problema perché devi ricordare ogni precedente posizione in board. Se questo è un requisito del problema, l'unica possibile soluzione al problema è memorizzare ogni mossa precedente.
Infine, c'è la regola delle cinquanta mosse . Un giocatore può richiedere la patta se nessun pedone si è mosso e nessun pezzo è stato preso nelle cinquanta mosse consecutive precedenti, quindi dovremmo memorizzare quante mosse da quando un pedone è stato spostato o un pezzo preso (l'ultima delle due. Ciò richiede 6 bit (0-63).
Di chi è il turno?
Ovviamente dobbiamo anche sapere di chi è il turno e questa è una singola informazione.
Due problemi
A causa del caso di stallo, l'unico modo fattibile o sensato per memorizzare lo stato del gioco è memorizzare tutte le mosse che hanno portato a questa posizione. Affronterò quell'unico problema. Il problema dello stato della scacchiera sarà semplificato in questo modo: memorizzare la posizione attuale di tutti i pezzi sulla scacchiera ignorando le condizioni di arrocco, en passant, stallo e di chi è il turno .
La disposizione dei pezzi può essere gestita a grandi linee in due modi: memorizzando il contenuto di ogni quadrato o memorizzando la posizione di ogni pezzo.
Contenuti semplici
Ci sono sei tipi di pezzi (pedone, torre, cavaliere, alfiere, regina e re). Ogni pezzo può essere bianco o nero, quindi un quadrato può contenere uno dei 12 pezzi possibili o può essere vuoto quindi ci sono 13 possibilità. 13 può essere memorizzato in 4 bit (0-15) Quindi la soluzione più semplice è memorizzare 4 bit per ogni quadrato per 64 quadrati o 256 bit di informazione.
Il vantaggio di questo metodo è che la manipolazione è incredibilmente facile e veloce. Questo potrebbe anche essere esteso aggiungendo altre 3 possibilità senza aumentare i requisiti di stoccaggio: un pedone che si è mosso di 2 spazi nell'ultimo turno, un re che non si è mosso e una torre che non si è mossa, il che soddisferà molto dei problemi menzionati in precedenza.
Ma possiamo fare di meglio.
Codifica base 13
Spesso è utile pensare alla posizione nel consiglio di amministrazione come a un numero molto elevato. Questo è spesso fatto in informatica. Ad esempio, il problema dell'arresto tratta un programma per computer (giustamente) come un numero elevato.
La prima soluzione tratta la posizione come un numero a 64 cifre in base 16 ma, come dimostrato, c'è ridondanza in questa informazione (essendo le 3 possibilità inutilizzate per "cifra") quindi possiamo ridurre lo spazio numerico a 64 cifre in base 13. Ovviamente questo non può essere fatto nel modo più efficiente possibile con la base 16, ma risparmierà sui requisiti di archiviazione (e il nostro obiettivo è ridurre al minimo lo spazio di archiviazione).
In base 10 il numero 234 è equivalente a 2 x 10 2 + 3 x 10 1 + 4 x 10 0 .
In base 16 il numero 0xA50 è equivalente a 10 x 16 2 + 5 x 16 1 + 0 x 16 0 = 2640 (decimale).
Quindi possiamo codificare la nostra posizione come p 0 x 13 63 + p 1 x 13 62 + ... + p 63 x 13 0 dove p i rappresenta il contenuto del quadrato i .
2 256 è uguale a circa 1,16e77. 13 64 è uguale a circa 1,96e71, che richiede 237 bit di spazio di archiviazione. Quel risparmio di appena il 7,5% ha un costo di manipolazione significativamente maggiore.
Codifica a base variabile
Nei tabelloni legali alcuni pezzi non possono apparire in determinate caselle. Ad esempio, le pedine non possono comparire al primo o all'ottavo grado, riducendo le possibilità per quelle caselle a 11. Ciò riduce le possibili scacchiere a 11 16 x 13 48 = 1,35e70 (approssimativamente), richiedendo 233 bit di spazio di archiviazione.
In realtà la codifica e la decodifica di tali valori in e da decimale (o binario) è un po 'più complicata ma può essere eseguita in modo affidabile ed è lasciata come esercizio al lettore.
Alfabeti a larghezza variabile
I due metodi precedenti possono essere descritti entrambi come codifica alfabetica a larghezza fissa . Ciascuno degli 11, 13 o 16 membri dell'alfabeto viene sostituito con un altro valore. Ogni "carattere" ha la stessa larghezza ma l'efficienza può essere migliorata se si considera che ogni carattere non è ugualmente probabile.
Considera il codice Morse (nella foto sopra). I caratteri in un messaggio sono codificati come una sequenza di trattini e punti. Quei trattini e punti vengono trasferiti via radio (in genere) con una pausa tra di loro per delimitarli.
Notare come la lettera E ( la lettera più comune in inglese ) sia un singolo punto, la sequenza più breve possibile, mentre Z (la meno frequente) è composta da due trattini e due segnali acustici.
Un tale schema può ridurre significativamente la dimensione di un messaggio previsto , ma ha il costo di aumentare la dimensione di una sequenza di caratteri casuale.
Va notato che il codice Morse ha un'altra caratteristica incorporata: i trattini sono lunghi come tre punti, quindi il codice sopra è stato creato con questo in mente per ridurre al minimo l'uso dei trattini. Poiché 1 e 0 (i nostri elementi costitutivi) non hanno questo problema, non è una caratteristica che dobbiamo replicare.
Infine, ci sono due tipi di pause nel codice Morse. Una pausa breve (la lunghezza di un punto) viene utilizzata per distinguere tra punti e trattini. Uno spazio più lungo (la lunghezza di un trattino) viene utilizzato per delimitare i caratteri.
Quindi come si applica al nostro problema?
Codifica Huffman
Esiste un algoritmo per gestire i codici di lunghezza variabile chiamato codifica Huffman . La codifica di Huffman crea una sostituzione del codice di lunghezza variabile, in genere utilizza la frequenza prevista dei simboli per assegnare valori più brevi ai simboli più comuni.
Nell'albero sopra, la lettera E è codificata come 000 (o sinistra-sinistra-sinistra) e S è 1011. Dovrebbe essere chiaro che questo schema di codifica non è ambiguo .
Questa è una distinzione importante dal codice Morse. Il codice Morse ha il separatore di caratteri, quindi può eseguire sostituzioni ambigue (ad esempio, 4 punti possono essere H o 2 Is) ma abbiamo solo 1 e 0 quindi scegliamo invece una sostituzione non ambigua.
Di seguito è una semplice implementazione:
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; }
}
con dati statici:
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);
}
}
}
Un possibile output è:
White Black
Empty 0
Pawn 110 100
Rook 11111 11110
Knight 10110 10101
Bishop 10100 11100
Queen 111010 111011
King 101110 101111
Per una posizione iniziale, ciò equivale a 32 x 1 + 16 x 3 + 12 x 5 + 4 x 6 = 164 bit.
Differenza di stato
Un altro possibile approccio è combinare il primo approccio con la codifica di Huffman. Questo si basa sul presupposto che le scacchiere più attese (piuttosto che quelle generate casualmente) hanno maggiori probabilità che non assomiglino, almeno in parte, a una posizione di partenza.
Quindi quello che fai è XOR la posizione corrente della scheda a 256 bit con una posizione iniziale di 256 bit e quindi codificarla (usando la codifica Huffman o, diciamo, qualche metodo di codifica della lunghezza di esecuzione ). Ovviamente questo sarà molto efficiente all'inizio (64 0 probabilmente corrispondono a 64 bit) ma aumenterà lo spazio di archiviazione richiesto man mano che il gioco procede.
Posizione del pezzo
Come accennato, un altro modo per affrontare questo problema è memorizzare invece la posizione di ogni pezzo che un giocatore ha. Questo funziona particolarmente bene con le posizioni di fine partita in cui la maggior parte dei quadrati sarà vuota (ma nell'approccio di codifica di Huffman, i quadrati vuoti usano comunque solo 1 bit).
Ogni lato avrà un re e 0-15 altri pezzi. A causa della promozione, la composizione esatta di quei pezzi può variare abbastanza da non poter presumere che i numeri basati sulle posizioni di partenza siano massimi.
Il modo logico per dividerlo è memorizzare una posizione composta da due lati (bianco e nero). Ogni lato ha:
- Un re: 6 bit per la posizione;
- Ha pedine: 1 (sì), 0 (no);
- Se sì, numero di pedoni: 3 bit (0-7 + 1 = 1-8);
- Se sì, la posizione di ogni pedone è codificata: 45 bit (vedi sotto);
- Numero di non pedoni: 4 bit (0-15);
- Per ogni pezzo: tipo (2 bit per regina, torre, cavaliere, alfiere) e posizione (6 bit)
Per quanto riguarda la posizione dei pedoni, i pedoni possono essere solo su 48 caselle possibili (non 64 come le altre). In quanto tale, è meglio non sprecare i 16 valori in più che utilizzare 6 bit per pedone. Quindi se hai 8 pedoni ci sono 48 8 possibilità, pari a 28.179.280.429.056. Hai bisogno di 45 bit per codificare tanti valori.
Sono 105 bit per lato o 210 bit in totale. Tuttavia, la posizione di partenza è il caso peggiore per questo metodo e migliorerà notevolmente man mano che rimuovi i pezzi.
Va sottolineato che ci sono meno di 48 8 possibilità perché i pedoni non possono essere tutti nella stessa casella. Il primo ha 48 possibilità, il secondo 47 e così via. 48 x 47 x… x 41 = 1.52e13 = 44 bit di archiviazione.
Puoi migliorare ulteriormente questo eliminando le caselle che sono occupate da altri pezzi (compreso l'altro lato) in modo da poter piazzare prima i pedoni bianchi poi i non pedoni neri, poi i pedoni bianchi e infine i pedoni neri. In una posizione iniziale, i requisiti di archiviazione vengono ridotti a 44 bit per il bianco e 42 bit per il nero.
Approcci combinati
Un'altra possibile ottimizzazione è che ciascuno di questi approcci ha i suoi punti di forza e di debolezza. Potresti, ad esempio, scegliere i migliori 4 e quindi codificare un selettore di schema nei primi due bit e quindi la memoria specifica dello schema dopo.
Con l'overhead così piccolo, questo sarà di gran lunga l'approccio migliore.
Stato del gioco
Torno al problema di memorizzare una partita piuttosto che una posizione . A causa della triplice ripetizione dobbiamo memorizzare l'elenco delle mosse che si sono verificate fino a questo punto.
Annotazioni
Una cosa che devi determinare è che stai semplicemente memorizzando un elenco di mosse o stai annotando il gioco? Le partite di scacchi sono spesso annotate, ad esempio:
- Bb5 !! Nc4?
La mossa del bianco è contrassegnata da due punti esclamativi come brillante mentre quella del nero è vista come un errore. Vedi punteggiatura degli scacchi .
Inoltre potresti anche dover memorizzare del testo libero man mano che vengono descritte le mosse.
Presumo che le mosse siano sufficienti quindi non ci saranno annotazioni.
Notazione algebrica
Potremmo semplicemente memorizzare il testo della mossa qui ("e4", "Bxb5", ecc.). Includendo un byte di terminazione che stai guardando a circa 6 byte (48 bit) per mossa (caso peggiore). Non è particolarmente efficiente.
La seconda cosa da provare è memorizzare la posizione iniziale (6 bit) e la posizione finale (6 bit) in modo da 12 bit per movimento. Questo è decisamente meglio.
In alternativa possiamo determinare tutte le mosse legali dalla posizione corrente in modo prevedibile e deterministico e in uno stato che abbiamo scelto. Questo poi risale alla codifica di base variabile menzionata sopra. Il Bianco e il Nero hanno 20 mosse possibili ciascuno per la prima mossa, di più nella seconda e così via.
Conclusione
Non esiste una risposta assolutamente giusta a questa domanda. Ci sono molti possibili approcci di cui sopra sono solo alcuni.
Quello che mi piace di questo e di problemi simili è che richiede abilità importanti per qualsiasi programmatore come considerare il modello di utilizzo, determinare accuratamente i requisiti e pensare ai casi d'angolo.
Posizioni di scacchi prese come screenshot da Chess Position Trainer .