lunedì 27 luglio 2009

Move generator.

La generazione delle mosse è il motore fondamentale di ogni motore di scacchi. E' dato per scontato in ogni descrizione dell'algoritmo alfa-beta, ma ciò non significa che non sia importante, o che sia facile da implementare. La generazione delle mosse è fortemente dipendente da come è rappresentata la scacchiera.In questa versione dell'engine ho scelto di utilizzare la rappresentazione della scacchiera 0x88. Questa rappresentazione prevede un totale di 8 bit per rappresentere le coordinate di una casa, codificando gli 8 bit come segue: riga<<4|col . visto che le righe/colonne contano da 0 a 7, i bit più significativi delle due quartine non potranno mai essere veri per una cella valida: ne consegue che, durante la generazione delle celle valide per le mosse, basterà un AND bit a bit ( bitwise and: & ) con 0x88 per capire che si sono raggiunti i bordi, e che occorre fermarsi.
Tale schema di generazione risulta, nella implementazione c#, più veloce di un approccio concorrente, denominato BitBoard, che prevede la rappresentazione dei pezzi sulla scacchiera tramite maschere di 64 bit.
Come vantaggio aggiuntivo la differenza delle rappresentzione 0x88 di due celle ritorna un valore univoco. Questo può essere utilizzato, e di fatto felpoII utilizza questa strategia, per costruire degli array in cui ogni elemento indica se una cella può essera attaccata da un altra ove su questa ci sia un determinato pezzo.
In pratica, per avere un pointer utilizzabile dalla differenza di celle, occorre aggiungere la costante 0x77, onde evitare negativi. FelpoII ha delle tabelle di attacco per tutti i pezzi eccetto i pedoni, per il Cavallo ed il Re l'array contiene semplicemente un booleano che indica se la cella X è attaccata da un Re ( o Cavallo ) sulla cella Y. per i pezzi "scorrevoli",invece, è memorizzata anche la direzione in cui l'attacco avviene, questo renderà più facile scoprire runtime se ci sono pezzi che sbarrano la strada: difatto le tabelle precalcolate per i pezzi scorrevoli indicano solo se esiste la possibilità di un attacco potenziale tra le due case, è compito del programma verificare se, nella situazione corrente, l'attacco c'è oppure no.
Maggiori informazioni sullo schema 0x88 le potete trovare qui.
Un move generator deve essere veloce: anche se, con un buon ordinamento, la velocità della generazione mosse pesa solo un 10% sull'algoritmo di ricerca, non bisogna prendere sottogamba il problema, specie se si utilizza un linguaggio come C#.
Di seguito alcuni Tip per scrivere un generatore efficiente, competitivo con altri engine magari scritti in C:
  • Evitare di usare una classe da costruire con "new" per le mosse. meglio utilizzare value type, o meglio ancora, impacchettare la mossa in un intero a 32 bit, codificando opportunamanete i bit relativi alla casa di partenza, quella di destinazione, en passant etc. Magari sembrerà poco object oriented, ma la mia prima versione che utilizzava addirittura classi derivate per ogni tipo di mosse aveva performance ridicole.
  • Evitare le liste, anche List<int>. Se sono velocissime quando scrivete un gestionale, diventano di una lentezza agghiacciante in un engine di scacchi. preferite gli array ( nel mio caso array di interi ). Anche i pezzi: a discapito della locuzione Piece List, che si trova in letteratura, meglio tenere i pezzi in un bell int[]. Tenetene due separati per i bianchi ed i neri. HG Mueller, nella sua versione di perft, usa separare in array diversi anche i pezzi "scorrevoli", i pedoni ed i cavalli. Questo può avere alcuni vantaggi durante la ricerca delle situazioni di scacco.
  • Se usate un approccio a "solo mosse valide" (*) assicuratevi di testare lo stato di scacco prima di generare le mosse, se siete in scacco singolo generate solo le mosse del re, quelle che attaccano l'attaccante, quelle che bloccano ( se si tratta di uno scacco lontano ) l'attacco.Se lo scacco è doppio, le uniche mosse da generare sono quelle del Re.
  • Effettuate la ricerca dello scacco in questo modo: sostituite al Re il pezzo "universale", cioè il pezzo che si muove contemporaneamente come tutti gli altri. Fatelo "camminare" in tutte le direzioni, se incontrate un pezzo vostro, marcatelo come "potenzialmente congelato", se dopo questo incontrate un altro pezzo dello stesso colore, smettete la ricerca e passate all'altra direzione. Se invece il pezzo è nemico, controllate se è in grado di attaccare nella direzione che state seguendo: se sì, marcate il pezzo "potenzialmente congelato" come "congelato" nella direzione di attacco, se no passate alla successiva direzione. La lista di pezzi congelati limiterà la generazione delle loro mosse.
  • Se vi sembra complicato, tenete conto che questo si fa prima della generazione delle mosse, e vi farà risparmiare moltissimo tempo evitando di controllare se la mossa generata vi mette in scacco: in pratica si generano a priori solo mosse valide.
(*) alcuni engine, generano mosse pseudo valide, contando sul fatto che durante Alpha Beta gran parte delle mosse sono, si spera, scartate.

In ultimo, testare bene il generatore, ma di questo parleremo più avanti...