Beginners Guide to TikTok for Search - Rachel Pearson - We are Tilt __ Bright...
Tesi Specialistica - Bruno Tagliapietra
1. Università degli Studi di Trieste
Facoltà di Ingegneria
Tesi di Laurea Specialistica
in
Ingegneria Informatica
PROGETTAZIONE E
REALIZZAZIONE DI UNA
APPLICAZIONE PER IL
GIOCO DEGLI
"SCACCHI 3D"
LAUREANDO: RELATORE:
Chiar.mo Prof.
Bruno TAGLIAPIETRA Maurizio FERMEGLIA
Anno Accademico 2008/09
200
3. Indice
Introduzione 5
Motivazione 5
Analisi 7
Descrizione del gioco preso in esame 7
Raumschach 7
Pezzi 9
Scopo del gioco e relativo regolamento 16
Notazione delle mosse 16
Requisiti funzionali 17
Requisiti non funzionali 18
Progettazione 19
Considerazioni preliminari 19
Architettura del sistema 20
Interfaccia – Motore VS Model – View - Controller 20
Progettazione del Motore 21
Rappresentazione della scacchiera e generazione mosse 23
Tecniche di ricerca 32
Valutazione di una posizione 41
Progettazione dell’Interfaccia 50
Scacchiera Tridimensionale 50
Comunicazione Interfaccia-Motore 56
Realizzazione 57
Tecnologie utilizzate 57
Realizzazione del Motore 60
Header files 60
C Files 62
Realizzazione dei modelli 3D 75
Realizzazione dell’Interfaccia 77
Premessa: XNA Application Model 77
Librerie di terze parti 79
Gameplay 81
3
4. Videocamere Virtuali 81
Gestione dei modelli 90
Logiche di gioco 92
Conclusioni 93
Raggiungimento degli obiettivi 93
Sviluppi futuri 93
Bibliografia 94
Letteratura 94
Web 94
Ringraziamenti 96
Appendice 97
Fast, Minimum Storage Ray/Triangle Intersection 97
4
5. Introduzione
Motivazione
Il gioco degli scacchi è considerato un problema molto interessante dal
punto di vista della sua risoluzione e gioco da parte di un elaboratore: nel
corso degli anni, fin dagli albori dell’informatica, gli appassionati e i
professionisti di scacchi e di computer hanno perfezionato algoritmi sempre
più sofisticati, fino a riuscire recentemente a far vincere il computer contro i
più grandi maestri.
Vale la pena ricordare che gli scacchi, a differenza ad esempio della dama,
sono ancora un problema aperto: cioè, mentre nella dama ormai si conosce
la mossa “migliore” in ogni situazione, dall’inizio della partita fino alla fine,
Vi sono competizioni mondiali che consistono in un gran numero di partite
fra i cosiddetti “motori” per il gioco degli scacchi, ovvero software molto
perfezionati e specifici il cui unico obiettivo è vincere una partita di scacchi.
Alcuni fra i motori migliori sono stati poi inseriti nei programmi di scacchi
commerciali, il più famoso di tutti è sicuramente “Chessmaster 10”: presenta
innumerevoli funzionalità quali interfaccia grafica user friendly, possibilità
di selezionare le abilità dell’avversario computerizzato (fino ad arrivare ad
un’abilità stimata con punteggio FIDE 2966), un breve corso di scacchi
integrato...
Sarebbe stato quindi presuntuoso e difficilmente fattibile per un singolo
scrivere qualcosa che sia competitivo sia dal punto di vista del motore, cioè
dell’intelligenza del programma, sia dal punto di vista delle funzionalità
offerte all’utente, rimanendo nel campo degli scacchi classici.
Un’alternativa sarebbe stata inventare un gioco del tutto nuovo. Tuttavia
spesso i giochi complessi difficilmente risultano equilibrati, interessanti e
umanamente giocabili alla prima versione.
Per questi motivi si è deciso di “riesumare” e computerizzare una fra le
versioni più interessanti di questo gioco, che sono indubbiamente quelle
che si svolgono su una scacchiera tridimensionale. Le tre principali
alternative erano: Raumschach (in tedesco “scacchi nello spazio”), inventata
nei primi del ‘900, Asimov Chess (versione ideata da Isaac Asimov in uno
dei suoi racconti, ma mai sufficientemente giocata), Star Trek 3D Chess
(versione giocata nell’omonima saga).
5
6. Si sono scelti i Raumschach fra i tre perché essi sono stati indubbiamente i
più giocati e i più sperimentati; inoltre non esiste alcuna implementazione
sotto forma di videogame che sia fedele all’originale di questo gioco, ideato
e giocato in alcuni club tedeschi nel periodo tra la prima e la seconda guerra
mondiale.
6
7. Analisi
Descrizione del gioco preso in esame
La versione scelta è, fra le versioni tridimensionali esistenti del gioco degli
scacchi, la più documentata e la più giocata in assoluto.
Le versioni tridimensionali del gioco degli scacchi, abbreviabili in 3D chess,
fanno parte delle innumerevoli varianti di questo famosissimo gioco. Le più
antiche risalgono alla fine del 1800.
Una delle più famose versioni tridimensionali degli scacchi è appunto quella
analizzata in questo documento. Essa è detta Raumschach, ovvero “scacchi
spaziali” in lingua tedesca.
Inventata nel 1907 da Ferdinand Maack, è stata giocata prevalentemente in
Germania. Nel 1919 Maack fondò anche un club di Raumschach ad
Amburgo, rimasto attivo fino all’inizio del secondo conflitto mondiale.
Inizialmente giocata in un cubo 8x8x8, la versione del 1907 vide la luce su
una scacchiera cubica con cinque caselle per ognuna delle tre dimensioni.
Raumschach
Come negli scacchi classici l’obiettivo è catturare il re
avversario. Questo può avvenire solo tramite lo
“scacco matto”, ossia una posizione in cui non vi è
mossa in grado di impedire la cattura del re nella
mossa immediatamente successiva.
I contendenti muovono uno dei propri pezzi alla
volta, a turno, alternandosi. I movimenti, descritti più
avanti in dettaglio, sono simili a quelli degli scacchi
classici.
Ognuna delle 125 caselle è identificata da tre
coordinate: “livello” di appartenenza, “riga” e
“colonna”, in quest’ordine.
• Livelli: identificati dalle lettere Fig. 1 Schema della scacchiera
maiuscole A,B,C,D,E
• Righe: identificate dalle cifre 1,2,3,4,5
• Colonne: identificate dalle lettere minuscole a,b,c,d,e
Seguendo la disposizione di fig.2 si inizia, per convenzione, la numerazione
delle caselle da quella posta sul livello più basso, sulla colonna più a sinistra
e sulla riga più vicina al giocatore con i pezzi di colore bianco. Perciò, nella
casella Aa 1 vi sarà all’inizio partita una torre “bianca”, e in Ba1 nel
medesimo istante un alfiere “bianco”, con il re posto nel piano più basso.
Sempre da fig.2 è facile ed importante notare che la disposizione dei pezzi
7
8. neri risulta speculare a quella dei pezzi bianchi, quindi con il re posto sul
posto
livello più alto. Quindi in Ee5 vi sarà una torre “nera” e in Ed5 un unicorno
“nero”.
Fig. 2 - La disposizione iniziale della scacchiera, dal piano più alto (E) al piano più basso (A)
8
9. Pezzi
I pezzi sono, come negli scacchi classici, Re, Regina, Torri, Alfieri, Cavalli e
scacchi
Pedoni, con l’aggiunta degli Unicorni. Ogni giocatore ha a disposizione 10
pedoni, 2 torri, 2 cavalli, 2 alfieri, 2 unicorni, 1 regina e naturalmente 1 re.
Come negli scacchi classici su ciascuna casella può stazionare un solo pezzo
può
e l’unico in grado di “saltare”, cioè di non subire l’ostruzione degli altri pezzi
presenti sulla scacchiera, è il cavallo. Qualsiasi pezzo di un certo colore può
occupare una casella contenente un pezzo avversario, catturando in questoi
modo quest’ultimo, il quale verrà rimosso dalla scacchiera.
Pedone: come negli scacchi classici il suoi movimenti cambiano a seconda
che esso catturi o meno un pezzo. Senza cattura esso deve muovere di un
passo di torre verso il campo avversario; se “bianco”, quindi, può muovere
verso l’alto o verso avanti rispetto al giocatore che lo controlla. In caso di
cattura a questo movimento dovrà essere aggiunto un cambio di colonna
verso destra o verso sinistra, risultante in un passo d’alfiere sullo ste stesso
piano o sul piano immediatamente successivo nel viaggio verso il campo
avversario. A differenza degli scacchi classici non c’è differenza di possibilità
di movimento fra la prima mossa e le successive. Non c’è mossa en-passant.
Lettera identificativa: P
9
10. Torre:
può muovere in linea retta di quante caselle si vuole in un solo turno. Può
muoversi in sei versi; è utile immaginare che essa per muoversi esca da una
delle sei facce del cubo di partenza.
Es: una torre posta al centro di una scacchiera vuota, quindi in Cc3, si può
: vuota,
muovere in
o Cc1, Cc2,Cc4, Cc5 (Varia la riga)
o Ca3, Cb3, Cd3, Ce3 (Varia la colonna)
o Ac3, Bc3, Dc3, Ec3 (Varia il livello)
Lettera identificativa: R (dall’inglese “rook”)
10
11. Alfiere: anch’esso muove in linea retta per quante caselle si vuole in un solo
caselle
turno. A differenza della torre però esso esce dagli spigoli del cubo, perciò
può muoversi in otto versi.
Es: Un alfiere posizionato nella casella Dc4 in una scacchiera vuota può
:
muoversi in
• Da2, Db3, Dd5, De2, Dd3, Db5
(nello ste
stesso livello, come negli scacchi classici)
• Eb4, Ed4, Cb4, cd4, Ba4, Be4
(nel piano posto frontalmente al giocatore)
• Ec3, Ec5, Cc3, Cc5, Bc2, Ac1
(nel piano posto lateralmente al giocatore)
Lettera identificativa: B (dall’ingelse “bishop”)
11
12. Unicorno: anch’esso in linea retta per quante caselle si vuole in un solo
nch’esso
turno, ma uscendo dai vertici del cubo. Perciò si muove in quattro versi.
Es: un unicorno posizionato al centro di una scacchiera vuota (Cc3) può
:
essere posto in:
Dd4, Ee5, Bb2, Aa1, Db2, Ea1, Bd4, Ae5, Ee1, Dd2, Bb4, Aa5, Ea5, Db4, Bd2,
,
Ae1
Lettera identificativa: U
12
13. Regina: essa può muovere sia come una torre, sia come un alfiere, sia come
un unicorno.
Lettera identificativa: Q (dall’inglese “queen”)
13
14. Re: esso si sposta come la regina però di un solo passo per turno. Ovvero, la
regina
casella di arrivo deve essere adiacente alla casella di partenza cioè avere in
comune con essa una faccia, uno spigolo o alla peggio un solo vertice. In
altre parole un re può muovere in ciascuna delle otto caselle più vicine alla
caselle
casella di partenza. A differenza degli scacchi classici non vi sono manovre
di arrocco.
Lettera identificativa: K (dall’inglese “king”)
14
15. Cavallo: E’ l’unico pezzo in grado di saltare oltre gli altri pezzi. Si deve
muovere di un solo passo di torre più un solo passo di alfiere. In altre
olo
parole, date le coordinate della casella di partenza, una di esse deve restare
invariata, una deve variare di 1 e la terza deve variare di 2.
Es: in questi esempi indichiamo il variare delle coordinate racchiudendo tra
: coordinate
parentesi l’entità della variazione di livello, riga e colonna, in quest’ordine.
Perciò ad esempio la tripla (0,1,2) indicherà un insieme di caselle poste
contemporaneamente nello stesso livello di quella di partenza, su righe
distanti 1 e colonne distanti 2.
i
Un cavallo posto al centro di una scacchiera, nella casella Cc3, può essere
posto in ciascuna delle 24 caselle sottostanti, purché libere da pezzi dello
stesso colore:
Cb1, Cb5, Cd1, Cd5 (0,1,2) --------- Ca2, Ca4, Ce2, Ce4 (0,2,1)
Bc1, Bc5, Dc1, Dc5 (1,0,2) --------- Ba3, Be3, Da3, De3 (1,2,0)
Ab3, Eb3, Ad3, Ed3 (2,1,0) --------- Ac2, Ec4, Ac4, Ac2 (2,0,1)
Lettera identificativa: N (dall’inglese “knight”)
15
16. Scopo del gioco e relativo regolamento
Come negli scacchi classici, lo scopo del gioco consiste nel dare "scacco
matto" (dal persiano Shah Màt = il re è morto) al re avversario; si ha "scacco
matto" quando il Re, trovandosi sotto la minaccia diretta dei pezzi avversari,
non ha la possibilità di sottrarsi ad essa (cioè sarebbe sicuramente catturato
alla mossa successiva, se non si trattasse del Re).
Lo "scacco" invece è l'attacco (evitabile) che un pezzo avversario porta al Re.
L'avversario non può eseguire alcuna mossa che metta o lasci il proprio re
sotto "scacco". La partita può terminare anche per abbandono da parte di
un contendente, ovviamente con la vittoria dell'altro.
Il gioco termina obbligatoriamente in parità (patta) nei seguenti casi:
1. se restano sulla scacchiera soltanto i due re;
2. se il giocatore che ha il tratto non può muovere alcun pezzo,
ma il suo re non è sotto scacco (stallo).
3. se per cinquanta mosse consecutive (cinquanta mosse per
ciascun giocatore) non viene catturato alcun pezzo e non viene
mosso alcun pedone.
Notazione delle mosse
Durante il corso della storia sono state ideate innumerevoli notazioni per gli
scacchi classici. Senza dubbio la più utilizzata, per le sue caratteristiche di
intuitività e brevità, è la cosiddetta “notazione algebrica”, descritta nel
regolamento internazionale degli scacchi. Risulterebbe però problematico, e
con tutta probabilità controproducente: infatti in essa si preferisce, per
identificare una mossa, annotare la lettera iniziale del nome del pezzo che
la esegue seguita dalle coordinate della casella di arrivo.
Es: Cf3 (cavallo mosso in f3, non si sa da quale punto di partenza).
E’ evidente che tale notazione necessita di complesse regole per evitare
equivoci nel caso in cui, ad esempio, i cavalli che possono muovere in f3
siano più di uno.
Per questo motivo, la notazione adottata nel nostro caso consisterà in:
casella di partenza – casella di arrivo – iniziale di eventuale pezzo “di
promozione”
es: Aa1Ca1 (il pezzo presente in Aa1 va in Ca1),
Ec4Ec5Q (il pezzo, pedone, presente in Ec4 va Ec5 e viene
promosso in regina);
16
17. Requisiti funzionali
Di seguito sono riportati i requisiti funzionali raggruppati in
macrofunzionalità. Ciascuna macrofunzionalità è raggiunta nel momento in
cui vengono soddisfatti tutti i requisiti in esso contenuti.
Requisiti giocabilità 2 giocatori su stessa postazione
• Il software deve presentare una scacchiera cubica 5x5x5, conforme al
regolamento indicato precedentemente. Detta scacchiera deve essere
inizialmente configurata come da Fig.2 e contenere tutti e soli i pezzi
indicati in Fig.2.
• Si vuole inoltre che i movimenti consentiti sui pezzi siano conformi
al regolamento precedentemente indicato.
• Essendo la visualizzazione di una scacchiera 3D ostica all’essere
umano, si vuole che vi siano più modalità di visualizzazione della
stessa (lontano-vicino).
• Per selezionare il pezzo da muovere e conseguentemente la casella in
cui muoverlo si vuole poter usare il mouse: un primo click sul pezzo
lo deve evidenziare, un secondo click fuori dal pezzo deve:
o Spostare il pezzo se il click è stato effettuato su una casella
dove il pezzo stesso possa muovere.
o Selezionare un pezzo diverso se il click è stato effettuato su un
altro pezzo selezionabile.
o Deselezionare il pezzo selezionato altrimenti.
17
18. Requisiti giocabilità giocatore umano vs giocatore
computerizzato
• Si vuole che vi sia la possibilità di giocare contro un avversario
computerizzato.
• Si vuole che l’abilità di detto avversario sia regolabile. Per la
precisione:
o Deve essere possibile regolare il tempo massimo concesso per
eseguire una mossa.
o Deve essere possibile fissare la profondità di analisi massima
del software.
Requisiti giocabilità 2 giocatori umani su postazioni diverse
• Si vuole che vi sia possibilità di giocare via internet con un avversario
umano.
Requisiti non funzionali
• Portabilità: si vuole che il gioco sia cross platform il più possibile.
18
19. Progettazione
Considerazioni preliminari
Dallo studio dei requisiti emerge come prima peculiarità il fatto che si tratti
di arrivare alla realizzazione di un videogame. Già questa semplice
particolarità presenta non poche implicazioni progettuali:
Restringendo il campo e riassumendo, dal punto di vista informatico si
tratta di un gioco da tavolo (board videogame) dotato di “intelligenza
artificiale” (AI), che deve quindi essere in grado di tener testa ad un
giocatore umano.
Si delina quindi una prima suddivisione in “motore”, ossia la parte pensante
del gioco, e “interfaccia utente”, cioè tutto ciò che è percepito
sensorialmente dal giocatore umano.
Dovendo essere intelligente, il motore deve avere come caratteristiche
peculiari rapidità di calcolo, efficienza, ed un certo grado di “regolabilità
della difficoltà”.
L’interfaccia, per contro, dovrà soddisfare i requisiti richiesti; inoltre, non
avendo particolari requisiti di efficienza, sarà facile progettarla in modo che
sia il più manutenibile possibile.
Altro punto fondamentale, si tratta di un software che fa uso di grafica 3D.
Sarà quindi necessaria la scelta di tecnologie adatte a coprire le necessità
emerse da queste prime considerazioni iniziali. In particolare si avrà
bisogno di:
• Un game engine con supporto 3D (OpenGL, Direct3D o entrambi).
E’ un software che serve a sviluppare video game. Le funzionalità
principali fornite da un game engine comprendono un renderer per
grafica 2D e/o 3D, animazioni, networking, gestione della memoria,
threading.
In più quello da noi scelto dovrà facilitare la portabilità, come da
requisito.
• Un software di 3D modeling per la realizzazione dei modelli (i pezzi,
la scacchiera).
19
20. Architettura del sistema
Nella primissima fase della definizione architetturale è opportuno
mantenersi ad alti livelli di astrazione, scendendo nei dettagli il meno
possibile. Via via che l’architettura prende forma, la definizione delle
singole parti sarà sempre più dettagliata.
Interfaccia – Motore VS Model – View - Controller
In accordo alle considerazioni preliminari, in prima approssimazione viene
molto naturale suddividere il progetto in due macroparti, come segue:
• Interfaccia Utente
Si occupa di raccogliere input dall’utente (le mosse che egli intende
dall’utente
fare) e di visualizzare a video la scacchiera.
• Motore
In esso sono implementate le regole del gioco, lo stato della partita e
gioco,
la logica di gioco computerizzato.
In questo modo, ad ogni input dell’utente l’Interfaccia Utent dovrà:
Utente
1. Compiere un’operazione di traduzione dell’input.
2. Passare al Motore la suddetta traduzione.
3. Attendere risposta del Motore.
4. Aggiornare la visualizzazione basandosi sullo stato del Motore.
Ad ogni input ricevuto, il Motore dovrà:
1. Porre a confronto l’input ricevuto con le regole.
2. Se l’input ricevuto corrisponde ad una mossa permessa, modificare lo
stato e generare un messaggio di conferma.
Altrimenti, generare un messaggio di errore.
3. Rispondere con il messaggio generato all’Intefaccia Utente
20 Fig. 3 Model - View - Controller
21. L’architettura così descritta richiama il pattern archietturale Model View
Controller. Il comportamento del sistema viene normalmente così illustrato:
1. L’utente interagisce in qualche modo con l’interfaccia (View)
2. Il Controller reagisce all’input, ad esempio tramite una callback.
3. Il Controller propaga sul Modello il risultato dell’azione dell’utente;
solitamente modifica lo stato del Modello.
4. Una View utilizza il modello indirettamente per generare
un’interfaccia utente appropriata, o per modificare quella già
esistente. Il Controller e il Modello non “sanno” che la View esiste.
Nel nostro caso Model e Controller farebbero entrambi parte del Motore.
Questa è infatti l’architettura più conveniente; tutto lo stato è contenuto nel
Motore, che viene interrogato dall’Interfaccia ogniqualvolta ve ne sia
bisogno.
Progettazione del Motore
Per essere in grado di assolvere alla sua funzione in base ai requisiti
descritti, il Motore di 3D Chess deve innanzitutto possedere un sistema di
strutture dati atte a descrivere lo stato della partita in corso.
Lo stato sarà formato da:
• Stato della scacchiera, ovvero stato di ciascuna delle 125 caselle.
Ciascuna di esse può essere vuota oppure contenere uno dei 7 tipi di
pezzi disponibili, che possono essere a loro volta “bianchi” o “neri.
Perciò ogni casella ha 7 ∗ 2 + 1 = 15 stati possibili.
• Numero di mosse consecutive dall’ultima cattura di pezzo o mossa di
pedone (essenziale per soddisfare la regola di partita patta in caso di
50 mosse consecutive senza catture o mosse di pedoni).
• Giocatore che ha il tratto (cioè: tocca muovere al bianco o al nero?)
• Inoltre, volendo dare la possibilità di “takeback”, ovvero di poter
annullare un certo numero di mosse, sarebbe opportuno mantenere
uno stack delle mosse fatte fino a quel momento. In alternativa, per
semplificare il calcolo, si potrebbero memorizzare direttamente le
posizioni della scacchiera.
21
22. A questo punto, basandosi sullo stato così descritto, il Motore deve essere in
grado di:
• Esprimere una valutazione quantitativa sulla posizione
• Per un certo numero di “livelli”, si esso n:
o Generare tutte le mosse possibili
o Valutare tutte le posizioni
o Per ciascuna posizione, generare tutte le mosse possibili
o Fermarsi quando ha raggiunto il tempo limite
In questo modo si sarà in grado di fissare il “livello” di difficoltà sia
basandosi sulla massima profondità di esplorazione dell’albero delle mosse,
sia fissando un tempo limite massimo oltre il quale il Motore dovrà
selezionare la mossa ritenuta fino a quel momento la migliore.
Naturalmente, tutto questo deve essere fatto nel modo più efficiente
possibile, in quanto questo è il requisito fondamentale che deve avere il
nostro Motore.
Da queste considerazioni iniziali si può facilmente concludere che tre sono i
problemi fondamentali da risolvere:
• Rappresentazione della scacchiera:
quali strutture dati utilizzare per rappresentare una posizione?
• Tecniche di ricerca:
come identificare le mosse possibili e selezionare quelle che
sembrano essere più “promettenti” in modo che vengano esplorate
per prime?
• Valutazione di una posizione:
come valutare quantitativamente una posizione?
Descriviamo ora i punti presi in considerazione nella progettazione della
soluzione a questi tre principali problemi.
22
23. Rappresentazione della scacchiera e generazione mosse
Nei motori per il gioco degli scacchi classici vengono utilizzati da molti anni
delle strutture dati ormai considerate standard per la rappresentazione di
una scacchiera.
E’ nostro intento adottare la più consona ai nostri scopi, fornendo dapprima
una descrizione delle possibili alternative.
Alternative progettuali
1. Lista di pezzi
Era utilizzata dai primi programmi di scacchi, i quali avevano a
disposizione pochissima memoria. Perciò anziché riferirsi prima alla
cella e da essa ottenere il pezzo, mantenevano una lista dei pezzi in
gioco e relative coordinate.
E’ utilizzata ancora oggi in congiunzione ad altre tecniche nel caso
serva scoprire rapidamente la posizione di un certo pezzo.
2. Array based
Una fra le alternative più intuitive, ovvero creare un array di tante
caselle quante sono quelle della scacchiera che trattiamo. Ciascun
elemento dell’array identificherà il contenuto della casella: ad
esempio assegnando un valore convenzionale a ciascuna delle 15
possibili combinazioni (vuota, uno dei 7 tipi di pezzi bianchi o uno
dei 7 tipi di pezzi neri).
Diventa lento durante la generazione delle mosse in quanto per ogni
mossa generata bisogna controllare se essa è dentro o fuori dalla
scacchiera, rallentando in questo modo il processo di generazione.
3. Codifica Huffman
Ispirato alla nota codifica di Huffman, le posizioni vengono descritte
con configurazioni di bit di lunghezza inversamente proporzionale
alla probabilità che esse compaiono.
Ecco un esempio che farebbe al caso nostro:
Casella vuota = 0
Pedone = 10c
Unicorno = 1100c
Alfiere = 1101c
Cavallo = 1110c
Torre = 1111c
Regina = 11110c
Re = 11111c
dove ‘c’ rappresenta il colore (ad esempio 1 per bianco e 0 per nero).
23
24. Servono ancora 1 bit per identificare chi ha il tratto e 7 bit per la
regola delle 50 mosse.
Questa strategia risulta molto onerosa dal punto di vista della
complessità di calcolo, mentre è conveniente per le ridotte
dimensioni in memoria.
4. Metodo 0x88:
Questo metodo funziona negli scacchi classici perché le 64 caselle
sono in numero una potenza di 2, come pure le due dimensioni 8x8.
Non può essere utilizzato nel nostro caso (la scacchiera è composta
da 125 caselle, con tre dimensioni 5x5x5, e non vi sono potenze di 2
fra questi numeri), ma lo citiamo, senza spiegarlo, per completezza.
5. Bitboard:
Anche questo metodo non funziona per noi. Consiste nello sfruttare
la parallelizzabilità del calcolo in caso di processori a 64 bit quando
le caselle sono 64, come negli scacchi normali. Anche questo è citato
solo per completezza
6. Notazione Forsyth–Edwards (FEN)
Basata su un sistema ideato dal giornalista scozzese David Forsyth, è
stata estesa da Steven J. Edwards per utilizzi informatici.
Un “record” FEN definisce una particolare posizione di gioco; si
tratta di una stringa di caratteri ASCII tutti sulla stessa riga,
contenente sei campi, separati da uno spazio.
I campi sono:
(NB: la sguente descrizione si riferisce al regolamento degli scacchi
classici. E’ qui riportata per confronto con la variante FEN elaborata
per il caso nostro e descritta in seguito)
a. Posizionamento dei pezzi (dalla prospettiva del bianco)
Ciascuna delle 8 righe è descritta come segue, ed è separata
dalle altre utilizzando il carattere “/”.
Seguendo la notazione algebrica standard (stessa iniziale dei
pezzi descritta in questo documento nella parte di analisi), i
pezzi bianchi sono designati usando lettere maiuscole
(“PNBRQK”) mentre quelli neri con lettere minuscole
(“pnbrqk”). Le caselle vuote consecutive vengono raggruppate
e indicate con cifre da 1 a 8.
b. Colore di chi ha il tratto
“w” per bianco (white), “b” per nero (black).
24
25. c. Arrocco
Se non si può più arroccare si segnerà “-“. Altrimenti il
vocabolario è il seguente: “K” (il bianco può arroccare dal lato
di re), “Q” (il bianco può arroccare dal lato di regina), “k” (il
nero può arroccare dal lato di re), “q” (il nero può arroccare
dal lato di regina).
d. En Passant
Se un pedone può essere mangiato “en passant” si segneranno
le coordinate della casella sulla quale finirà un eventuale
pedone avversario che effettuerà la mangiata.
Altrimenti si segna “-“.
e. Halfmove clock
Così si definisce il numero di mezze mosse passate da quando
è stato mosso un pedone o mangiato un pezzo. Se raggiunge
100 la partita è pari.
f. Fullmove number:
Il numero di mosse complete eseguite fino al momento della
scrittura della posizione. Comincia da 1 e viene incrementato
ogniqualvolta muove il nero.
Esempio di record FEN per scacchi classici (appena descritto) nella
posizione iniziale:
rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1
Ispirandoci a questa notazione possiamo derivarne con poco sforzo
una rappresentazione appropriata ed intuitiva per il caso preso in
esame da questa tesi.
Notazione Forsyth–Edwards-Raumschach (FENR)
Non contemplando il regolamento del gioco da noi preso in esame la
presa “en passant” né tantomeno alcun tipo di arrocco, i campi che
compongono un record FENR saranno soltanto 4, separati da spazi.
Gli ultimi 3, ovvero “Colore di chi ha il tratto”, “Halfmove clock” e
“Fullmove number”, rimarranno fedeli all’originale.
Il primo, riguardante la descrizione del posizionamento dei pezzi,
sarà annotato come segue.
Come nell’originale la scacchiera verrà rappresentata dal punto di
vista del bianco. I piani saranno scritti dal più alto al più basso. La
rappresentazione delle caselle vuote e le lettere rappresentanti i
pezzi seguiranno le regole dell’originale, con l’aggiunta della “u” e
“U” per gli unicorni rispettivamente neri e bianchi.
25
26. Come nell’originale le righe saranno separate da “/”. I piani, invece,
saranno separati da “-“.
Esempio di record FENR per 3D Chess nella posizione iniziale:
rnknr/ppppp/5/5/5-buqbu/ppppp/5/5/5-5/5/5/5/5-
5/5/5/PPPPP/BUQBU-5/5/5/PPPPP/RNKNR w 0 0
(cfr. con fig. 2).
7. Mailbox Array
E’ una variante della struttura dati Array Based (descritta al punto 2),
mantiene i suoi vantaggi ed abbatte la difficoltà di determinare,
durante la generazione delle mosse, se la mossa generata finisca
dentro o fuori dalla scacchiera.
Descriviamo dapprima il metodo utilizzato negli scacchi classici, più
semplice in quanto riferito ad una scacchiera bidimensionale.
Per rappresentare la scacchiera si utilizzano 2 array
monodimensionali di interi.
Uno formato da 8 ∗ 8 = 64 caselle, denominato “addresser”.
L’altro formato da 12 ∗ 12 = 144 caselle, denominato “mailbox”.
Mailbox sarà così composto:
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 0 1 2 3 4 5 6 7 -1 -1
-1 -1 8 9 10 11 12 13 14 15 -1 -1
-1 -1 16 17 18 19 20 21 22 23 -1 -1
-1 -1 24 25 26 27 28 29 30 31 -1 -1
-1 -1 32 33 34 35 36 37 38 39 -1 -1
-1 -1 40 41 42 43 44 45 46 47 -1 -1
-1 -1 48 49 50 51 52 53 54 55 -1 -1
-1 -1 56 57 58 59 60 61 62 63 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
-1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1 -1
26
27. Addresser sarà invece composto come segue:
26 27 28 29 30 31 32 33
38 39 40 41 42 43 44 45
50 51 52 53 54 55 56 57
62 63 64 65 66 67 68 69
74 75 76 77 78 79 80 81
86 87 88 89 90 91 92 93
98 99 100 101 102 103 104 105
110 111 112 113 114 115 116 117
Ovvero addresser conterrà tutti e soli, nell’ordine, gli indici delle
caselle di Mailbox che non contengono −1.
A questi due array si aggiunge la scacchiera Array Based, sia esso
ℎ , costituita da un array monodimensionale di interi
composto da 64 caselle. I pezzi vengono rappresentati in esso con
valori convenzionali.
Esempio:
0: casella vuota
1: pedone bianco , -1 pedone nero
2: unicorno bianco , -2 pedone nero
3: cavallo bianco , -3 cavallo nero
4: alfiere bianco , -4 alfiere nero
5: torre bianca , -5 torre nera
6: regina bianca , -6 regina nera
7: re bianco , -7 re nero
Ora, per ogni verso in cui ciascun pezzo può muoversi si definisce un
offset. Aggiungendo questo offset all’indice della casella in cui esso si
trova si otterrà la successiva casella nella direzione di movimento di
quell’offset.
Ad esempio, per l’alfiere gli offset saranno 4, uno per ogni direzione,
e saranno −13, −11, 11, 13.
Infatti, un ipotetico alfiere in posizione 115, ovvero la posizione di
partenza dell’alfiere di re bianco, potrebbe muoversi ad esempio
nelle caselle 115 − 13 = 102 e 115 − 11 = 104 (prendendo come
riferimento l’array Addresser). E infatti:
mailbox[102] contiene 52
e
mailbox[104] contiene 54
Infatti 52 e 54 sono le nuove coordinate dell’alfiere nella scacchiera
Array Based.
27
28. Si vede subito che tentando di far muovere l’alfiere in questione fuori
dalla scacchiera si ottiene −1.
Infatti 115 + 11 = 126 e 115 + 13 = 128. E
mailbox[126] contiene -1
e
mailbox[128] contiene -1
Vediamo l’algoritmo in linguaggio naturale: con questo sistema, per
ogni casella 0 ≤ ≤ 63 sarà necessario e sufficiente:
• Se la casella non è vuota, fare riferimento agli offset del pezzo
in essa contenuto.
• Per ogni offset:
1. Sommare il primo offset ad [ ]. Sia
l’intero così ottenuto.
2. Se [ ] contiene −1 fermarsi e passare
all’offset successivo. Se sono finiti gli offset, passare alla
casella successiva.
3. Altrimenti significa che il pezzo può muoversi in
ℎ [ [ ]] , a meno che questa
casella non sia occupata da un altro pezzo dello stesso
colore o che a seguito di questa mossa il re non venga
messo sotto scacco.
4. Se il pezzo può muoversi di più caselle in una direzione
(alfiere, torre o regina) ripetere il punto 1. Altrimenti
passare all’offset successivo. Se sono finiti gli offset,
passare alla casella successiva.
NOTA: Le colonne e le righe contenenti −1 sono due per ogni lato
dell’array. Ne basterebbero soltanto una per lato se non fosse per il
cavallo, il quale nel caso in cui si trovasse sul bordo di uno dei lati
potrebbe essere spinto appunto di due colonne, o righe, fuori dalla
scacchiera da uno dei suoi offset.
A questo punto si può immaginare un’implementazione di questo
algoritmo in C. Avendo già descritto l’algoritmo in linguaggio
naturale sembra superfluo utilizzare lo pseudocodice, e più
appropriato il C, anche per sottolineare la semplicità di calcolo
ottenuta con la struttura dati “Mailbox Array” Siano:
28
29. • “mailbox” l’array da 144 caselle.
• “addresser” l’array da 64 caselle.
• “chessboard” la scacchiera Array Based (supponiamo che il
valore convenzionale per “casella vuota” sia zero), quindi da
64 caselle.
• “color” un array da 64 caselle contenente −1, 1 o 0 a seconda
che una casella sia occupata da un pezzo nero, bianco o vuota,
rispettivamente.
• “offset” un array bidimensionale contenente in ogni colonna
tutti gli offset per muovere ciascun pezzo (zero per i pezzi che
hanno meno direzioni).
• “slide” un array di boolean che contiene “vero” nelle celle
destinate ad alfiere, torre e regina, “falso” nelle altre.
L’algoritmo necessario a determinare tutte le caselle in cui il pezzo
contenuto nella casella “i” (da zero a 64) può essere mosso è il
seguente:
// *** ALGORITMO DI GENERAZIONE MOSSE
BOOL is_moveable = FALSE;
int n;
for (j = 0; j < offsets[piece[i]]; ++j)
//il tipo di pezzo contenuto in piece[i]
//ha offsets[piece[i]] offset diversi
for (n = i;;) {
//inizializzazione e ciclo infinito
//”n” è la casella da esaminare
n = mailbox[addresser[n] + offset[piece[i]][j]];
//utilizzando le schematizzazioni di mailbox e
//addresser sopra indicate è facile capire che
//con questa operazione “n” diventa -1 nel caso
//in cui il movimento usando l’offset
//individuato nel modo descritto porti fuori
//dalla scacchiera
if (n == -1)
break;
//se siamo usciti dalla scacchiera ci fermiamo
//e passiamo all’offset successivo
if (color[n] != 0) {
//se la casella non è vuota...
if (color[n] == not_my_side)
is_moveable = TRUE;
//se è occupata da un pezzo avversario allora
29
30. //è possibile occuparla (catturando il pezzo)
break;
}
is_moveable = TRUE;
if (!slide[piece[i]])
break;
//se si tratta di alfiere, torre o regina
//esaminiamo un ulteriore spostamento nella
//direzione dell’offset preso in esame
}
E’ chiaro che questo algoritmo risolve il problema della generazione
delle mosse
Passando tutte le 64 caselle in questo modo si ha rapidamente la
situazione della scacchiera in una determinata posizione.
A questo punto si tratta di portare questo metodo alla nostra
situazione di scacchiera cubica. A tal proposito, sarà sufficiente:
1. Modificare l’array “Mailbox” in un array con 9x9x9 = 729
caselle, in modo da avere in ogni direzione sufficiente
spazio per eventuali pezzi “caduti” fuori dalla scacchiera.
Per ogni dimensione, infatti, avremo le 5 caselle utili più 2
caselle per lato (necessarie a mantenere i movimenti dei
cavalli)
2. Modificare l’array “addresser” trasformandolo in un array
di 125 caselle. Anch’esse, come nell’esempio degli scacchi
classici, dovranno contenere gli indirizzi delle caselle di
Mailbox che non contengono “-1”.
3. Costruire opportunamente l’array degli offset basandosi
sulle dimensioni di “mailbox”.
4. Modificare gli array “chessboard” e “color” in array da 125
caselle.
5. Modificare l’array “slide” aggiungendo ad esso l’unicorno.
L’algoritmo resterà invariato e il metodo sarà applicabile al caso nostro.
30
31. Soluzione adottata
La soluzione più adatta al caso nostro è sicuramente “Mailbox Array”,
unendo essa la praticità della scacchiera “Array Based” senza però gli
handicap di inefficienza di cui quest’ultima soffre.
Anche la facilità di adattabilità della metodologia al nostro tipo di problema
la rende la miglior candidata fra le proposte elencate.
Esso però è difficilmente applicabile al caso del pedone: il movimento del
pedone è condizionato in modo particolare dai pezzi avversari che lo
circondano; esso infatti può muovere in diagonale soltanto se vi sono
presenti pezzi avversari nelle caselle di destinazione (per un’accurata
descrizione del movimento del pedone v. Analisi).
Basterà utilizzare l’algoritmo di generazione precedentemente individuato
solo per i pezzi che non siano pedoni, anteponendo ad esso un controllo
condizionale.
Per i pedoni sarà sufficiente controllare lo stato delle 6 caselle interessate
dal movimento di ciascuno di loro, a seconda che sia nero o sia bianco, in
modo da massimizzare l’efficienza pur scrivendo codice ridondante.
In pseudocodice:
foreach (casella c della scacchiera)
{
if (color[c] == colore di turno)
{
if (piece[c] == pedone)
{
if(color[c] == bianco)
{
//controllo dello stato delle 6 caselle
//corrispondenti al movimento pedone bianco
}
else
{
//controllo dello stato delle 6 caselle
//corrispondenti al movimento pedone nero
}
}
else
{
//esegui algoritmo di generazione mosse
}
}
}
I dettagli sulla effettiva implementazione sono riportati nella sezione
dedicata alla realizzazione.
31
32. Tecniche di ricerca
La ricerca di una mossa in un gioco a turni si risolve, in generale, sotto
forma di ricerca in un albero.
La prima idea che viene in mente è impostare una massima profondità,
generare tutto l’albero fino a quella profondità e valutare le “foglie”, ovvero
le posizioni definite terminali.
E’ ovvio che in ogni caso la “visibilità” che l’algoritmo consente è limitata
dalla profondità impostata.
Questo fenomeno è chiamato “horizon effect”: il numero dei possibili stati, o
posizioni, è molto grande e I computer possono cercare soltanto fra una
piccola parte di essi. Perciò potrebbe capitare ad esempio che la mossa
ritenuta migliore guardando soltanto 5 mosse più avanti risulti disastrosa, al
punto di essere determinante per la perdita della partita (pensiamo ai
famosi “sacrifici”: ne è un esempio lampante la famosa partita a scacchi
detta “L’immortale”, nella quale il bianco vince pur essendo in schiacciante
inferiorità numerica).
Per questo motivo negli anni si è tentato di ottimizzare la ricerca della
mossa migliore facendo in modo di selezionare i “rami” nei quali cercare
con profondità maggiore o nei quali cercare prima.
Perciò il problema della ricerca viene a sua volta scomposto in tre problemi:
1. Algoritmo di ricerca da utilizzare
2. Come ottenere una differenza efficace di profondità nella ricerca
3. In quale ordine effettuare la ricerca nei vari livelli dell’albero
Algoritmo di ricerca
Di seguito si elencano brevemente alcune fra le procedure di ricerca più
utilizzate:
• Minimax
Così cita il teorema del minimax (dalla teoria dei giochi):
per ogni gioco a somma zero con strategie finite che interessa due
giocatori, esiste un valore V ed una strategia mista per ciascun
giocatore tale che:
a. Data la strategia del giocatore 2, il miglior risultato per
il giocatore 1 è V.
b. Data la strategia del giocatore 1, il miglior risultato per il
giocatore 2 è –V.
32
33. Senza entrare ne merito della teoria dei giochi, il cui
nel
approfondimento va al di là dello scopo di questa tesi, possiamo
applicare questo teorema agli scacchi, ed anche ai nostri scacchi 3D.
Infatti, dall’applicazione del teorema del minimax scaturisce
l’applicazione
l’algoritmo minimax. Esso è un algoritmo ricorsivo che serve
minimax.
generalmente a cercare la miglior mossa successiva in un gioco di n
giocatori. Viene associate un valore a ciascuna posizione o stato del
sta
gioco; questo valore indica quanto buono è per un giocatore
raggiungere q quella posizione. Il giocatore poi farà la mossa che
massimizza il minimo valore della posizione risultante dalle possibili
mosse successive dell’avversario.
Min
Fig. 4 Esempio di albero minimax con valori calcolati
Il valore dei nodi viene valutato con una funzione euristica discussa e
descritta più avanti.
a
Di seguito un’implementazione in pseudocodice dell’algortimo
maximin.
function minimax(node, depth, color)
if ((node è un nodo terminale) or (depth == 0))
node terminale depth 0
return color * (valore euristico del nodo
valore nodo)
else
α := -
-∞
foreach child of node
α := max(α,
max(α,-minimax(child, depth-1), -color)
color)
return α
Alla prima chiamata si setta il nodo di partenza, il colore che deve
setta
muovere e la profondità massima da raggiungere.
33
34. • Alfa-Beta pruning
È un algortimo di ricerca il cui obiettivo è ridurre il numero di nodi
da valutare rispetto all’algoritmo minimax: esso infatti smette di
valutare una mossa quando è stata trovata almeno una possibilità in
grado di provare che la mossa in questione è peggiore di una
precedentemente esaminata. Questo farà sì che tale mossa non
necessiti di ulteriore valutazione.
E’ un’ottimizzazione che non cambia il risultato dell’algoritmo,
quindi sicuramente da adottare.
I benefici dell’alfa-beta pruning consistono nel fatto che
utilizzandolo si riescono ad eliminare rami dell’albero di ricerca: in
questo modo il tempo di ricerca può essere limitato ai rami “più
promettenti”, permettendo così una più profonda ricerca.
L’ottimizzazione riduce la profondità effettiva a poco più della metà
rispetto al minimax se i nodi sono valutati in ordine ottimale o quasi
ottimale. La dimostrazione di tale affermazione esula dagli obiettivi
di questa tesi.
L’algoritmo funziona mantenendo due valori, alfa e beta, che
rappresentano il minimo punteggio ottenibile dal giocatore
massimizzante e il massimo punteggio ottenibile dal giocatore
minimizzante, rispettivamente. Alfa è inizialmente infinitamente
negativo, beta infinitamente positivo. La loro distanza decresce al
progredire della ricorsione.
Nel momento in cui beta diventa minore di alfa significa che la
posizione corrente non può essere il risultato del gioco ottimale di
entrambi i giocatori, perciò diventa inutile esplorarla ulteriormente:
è infatti inutile esplorare eventualità di gioco non ottimali nella
speranza che esse si verifichino; questo, negli scacchi ma non solo, è
uno dei più gravi e comuni errori dei giocatori principianti: sperare
in un grossolano errore dell’avversario per assicurarsi la vittoria.
34
35. Fig. 5 - Una illustrazione di alfa-beta pruning. Esplorare i sottoalberi in grigio non
beta
migliora la soluzione (nell'esplorazione da sinistra a destra). "min" e "max"
rappresentano i turni del giocatore e dell'avversario rispettivamente.
Pseudocodice dell’algori
dell’algoritmo:
function alfa
alfabeta(node, depth, α, β))
//β rappresenta la miglior scelta della mossa
preecedente (e quindi dell’altro giocatore)
if ((node è un nodo terminale) or (depth = 0))
node depth ==
return (valore euristico del nodo)
foreach child of node
α := ma
max(α, -alfabeta(child, depth-1,
1,-β,-α))
// uso della simmetria, -β diventa α
if β≤α
break // Beta cut-off
return α
//chiamata iniziale
alfabeta(origin, depth, -infinity, +infinity)
beta(origin, infinity,
NOTA:
Dalla fig. 5 si nota che se i sottorami di terzo livello, figli del nodo di
i
secondo livello marcato con valore 3, fossero scambiati di posto, uno
di loro non verrebbe nemmeno analizzato perché verrebbe tagliato
fuori dal pruning.
Infatti la tecnica di pruning può venire ulteriormente migliorata
p
utilizzando metodi di ordinamento euristici per individuare parti di
alberi che probabilmente potrebbero causare tagli se analizzate per
prime. Idealmente, sarebbe necessario che fossero esaminate per
prime le mos “migliori”.
mosse
Una delle tecniche più diffuse in questo senso, per la sua efficacia e
basso costo di calcolo, è la cosiddetta killer heuristic.
35
36. • Killer Heuristic
Si tratta di una tecnica di ordinamento di alberi in supporto
all’algoritmo di alfa-beta pruning.
Si tenta di produrre un cutoff sperando che una mossa che ha
prodotto un cutoff in un altro ramo dell’albero alla stessa profondità
produca un cutoff nella posizione in cui ci si trova al momento.
Particolarmente efficace negli scacchi, dove spesso una mossa buona
si rivela tale anche in posizioni di poco diverse. Perciò, esplorando la
mossa killer prima di altre mosse spesso si riesce a creare un taglio, il
cui beneficio sarà tanto maggiore quanto minore sarà la profondità in
cui lo si esegue.
History Heuristic
Ulteriore evoluzione, di comprovata efficacia negli scacchi classici, è
la cosiddetta history heuristic.
Si costruisce una tabella di mosse, indicizzandole in qualche modo,
ad esempio tramite il pezzo che muove e la casella di destinazione. Al
verificarsi di un taglio l’elemento appropriato della tabella viene
incrementato, ad esempio aggiungendo ad esso la profondità
rimanente (quindi mosse meno profonde risulteranno avere
punteggio più alto).
In questo modo si tenderà a privilegiare, fra le mosse che hanno
causato tagli, quelle avvenute in rami meno profondi dell’albero,
quindi in grado di produrre tagli più cospicui.
Qualunque sia l’algoritmo di ordinamento utilizzato, questo dovrà
avvenire prima della ricerca sull’albero.
• Quiescenza
Questo metodo di ricerca si è provato funzionale sperimentalmente,
e deriva da un’imitazione di comportamento umano: un giocatore
infatti tende istintivamente ad analizzare più in profondità situazioni
“agitate”, ovvero mosse attraverso le quali la posizione viene
profondamente alterata; tipicamente, mosse di cattura.
Il metodo consiste semplicemente nel dedicare maggiore profondità
all’analisi di mosse di cattura.
• Principal Variation + Iterative Deepening
Il termine variazione si riferisce ad una specifica sequenza di mosse
in un gioco a turni generico; spesso è utilizzato per identificare un
ipotetico stato futuro del gioco.
36
37. La variazione principale (principal variation) è quella particolare
variazione che è la più vantaggiosa per il giocatore corrente,
assumendo che il giocatore avversario risponda con le mosse per lui
di volta in volta migliori. In altre parole essa è la “migliore” o
“corretta” linea di gioco.
Nel contesto qui preso in esame si indica con variazione principale la
sequenza che il giocatore computerizzato crede essere la migliore;
questo assunto non è garantito a causa dell’euristicità dell’algoritmo
di valutazione e dell’horizon effect.
Fig. 6 In blu la variazione principale di un ipotetico albero minimax
Estendendo ancora il significato del termine, si può pensare di avere
una variazione principale per ciascun grado di profondità dell’albero:
ciascuna di esse sarebbe lunga quanto la profondità esaminata.
Utilizzare un array bidimensionale quadrato (avente come “lato” la
massima profondità che si decide di analizzare), unito ad uno
schema iniziale di iterative deepening, ovvero un ciclo che richiama
l’algoritmo di alfa-beta pruning con limite di profondità inizialmente
pari a 1 e ad ogni iterazione incrementato fino ad un certo massimo,
permette di mantenere la “storia” delle soluzioni trovate e
l’evoluzione della soluzione migliore trovata verso l’ottimalità
attraverso i vari passaggi all’interno dell’albero.
37
38. Soluzione adottata
Per la sua comprovata capacità di migliorare l’efficienza rispetto al
Minimax, l’algoritmo che si utilizzerà sarà l’alfa-beta pruning, corredato da
ricerca di quiescenza a profondità maggiore.
Inoltre, si utilizzerà una tabella per l’implementazione dell’algoritmo
“History Heuristics”.
In questo modo si potrà efficientemente compiere ricerche in profondità
nell’albero, potendo così supplire alle carenze (inevitabili) della funzione di
valutazione della posizione.
Si è desciso di utilizzare una ricerca a tre livelli. Riferendosi alla figura 7:
una prima funzione, think, richiama l’algoritmo alfa-beta pruning, chiamato
search, dando ad esso profondità limite inizialmente pari ad 1 e ad ogni
iterazione incrementata di 1.
Questo è necessario per soddisfare il requisito della limitazione temporale
nella nostra ricerca: alfa-beta pruning è un algoritmo di tipo depth-first;
questo significa che non si ha soluzione fino a che non è stato esplorato
tutto l’albero alla massima profondità desiderata.
Invece, impostando la profondità massima di volta in volta maggiore, ed
utilizzando un array per contenere tutte le variazioni principali ottenute
nelle varie iterazioni, l’algoritmo di ricerca potrà essere interrotto anche
dopo brevissimo tempo: in caso di interruzione verrà utilizzata la miglior
soluzione trovata fino a quel momento, indipendentemente da quanto in
profondità si sia riusciti ad andare.
Ad ogni iterazione viene chiamata la search, che implementa alfa-beta
pruning. Essa genera tutte le mosse possibili nella posizione corrente e,
iterando sull’elenco che le contiene, ne esegue la i-esima, richiama sé stessa
(invertendo alfa e beta perché il punteggio del giocatore nero è opposto a
quello del giocatore bianco) riducendo la profondità rimanente (depth),
annulla la mossa i-esima. Poi controlla il valore restituito dalla chiamata
ricorsiva, e, se è avvenuto un cut-off, aggiorna la tabella per history
heuristics, aggiorna l’array delle variazioni principali, e aggiorna il valore di
alfa.
Proseguendo con la ricorsione, la profondità rimanente (depth) descresce
fino ad arrivare a zero. A quel punto viene richiamata un’altra funzione che
implementa l’alfa-beta pruning, chiamata quiesce. Essa, a differenza di
search, genera soltanto mosse di cattura; così facendo si è implementata la
ricerca estesa per situazioni dove il punteggio della posizione può variare di
molto (per l’appunto nel caso di catture di pezzi).
In essa avviene anche la chiamata alla funzione di valutazione della
posizione.
38
39. Con il sistema così progettato si può ottenere grande profondità di calcolo
nell’esplorazione della posizione nel caso il tempo concesso al computer sia
elevato e contemporaneamente una risposta in breve tempo nel caso si
volesse dare al software soltanto pochi secondi per decidere.
39
41. Valutazione di una posizione
Questo, dei tre problemi è sicuramente il più arduo: come valutare la bontà
di una posizione? Molte sono le strategie di calcolo utilizzate negli scacchi
normali. In ogni caso, la valutazione avviene con un algoritmo molto
specifico, che funziona soltanto per lo speciale gioco degli scacchi classici.
Tuttavia, alcuni fattori di cui si tiene conto negli scacchi possono essere
utilizzati anche nel nostro caso.
Sicuramente saranno sufficienti a giungere ad una soluzione iniziale
soddisfacente, in modo che l’utente percepisca un certo grado di
intelligenza nella scelta delle mosse da parte del computer, in quanto sono
le più influenti anche nelle scacchi classici.
In ogni caso si tratta sempre di algoritmi euristici, per i quali non riusciamo
nemmeno a stimare il grado di ottimalità della soluzione trovata.
Vediamo gli accorgimenti applicabili.
Alternative progettuali
• Bilanciamento degli schieramenti
Il modo più intuitivo per assegnare un punteggio ad una posizione è
contare quanto “materiale”, nel senso di pezzi, è ancora disponibile al
bianco e quanto al nero.
Inizialmente, entrambi dispongono di dieci pedoni, due unicorni,
due cavalli, due alfieri, due torri, una regina, e naturalmente un re.
La “potenza” di un pezzo è determinata unicamente dal suo grado di
mobilità.
La mobilità dipende sia da come esso può intrinsecamente muoversi
(per regolamento) sia dalla sua posizione sulla scacchiera.
41
42. Vediamo in una tabella le “potenzialità” di ciascun pezzo:
Direzioni Caselle
Pezzo Al Sui Al Sui Slide Note
centro vertici centro vertici
Può muoversi in un ristretto numero di caselle
Unicorno 8 1 8 4 Sì
rispetto le 125 totali (vedi fig.8)
La sua mobilità è ostacolata soltanto da pezzi
Cavallo 24 8 24 8 No presenti sulle caselle di destinazione e non da
quelli sul tragitto (“salta” oltre i pezzi).
Può muoversi su metà delle caselle totali
Alfiere 12 3 32 12 Sì
(quelle del colore della sua casa di partenza)
Può muoversi sempre al massimo in 12 caselle,
Torre 6 3 12 12 Sì
a meno che non vi siano ostacoli sul tragitto.
Ad ogni mossa possiede le mobilità addizionate
Regina 26 7 52 28 Sì
di un alfiere, di una torre e di un unicorno.
Come tutti i pezzi è molto mobile al centro
della scacchiera. Tuttavia è opportuno tenerlo
Re 26 7 26 7 No
“in salvo” in quanto la sua perdita comporta la
sconfitta. Il suo valore sarà quindi infinito.
Il pedone non è qui trattato, essendo un pezzo “speciale” sotto molti
punti di vista.
Alcuni chiarimenti sulla tabella: con “slide” stiamo ad indicare se un
pezzo può “scivolare” di più caselle in una direzione, nel modo in cui
lo fanno appunto l’alfiere, la torre, la regina e l’unicorno. Nella
tabella si è tentato di quantificare intuitivamente in prima
approssimazione la mobilità dei pezzi, confrontando il numero di
direzioni e il numero di caselle in cui ciascun pezzo può muoversi nel
caso esso si trovi su una scacchiera vuota in un vertice oppure al
centro.
Risulta immediatamente chiaro che alcuni pezzi sono sempre più
mobili di altri, e tutti traggono beneficio quando sono spostati verso
il centro della scacchiera.
Ad esempio, è chiaro che l’unicorno è sicuramente il pezzo di minor
valore fra quelli segnati in tabella: non solo non è molto mobile, ma
non può nemmeno viaggiare su tutta la scacchiera, bensì solo su una
minima parte di essa (circa ¼ delle caselle).
Per contro, la regina è sicuramente il pezzo che vale di più
(ricordiamo che il re ha valore idealmente infinito, quindi non
conta), in quanto essa è il pezzo più mobile di tutti.
42
43. Fig. 8 - I 4 “tipi” di unicorno diversi e il dettaglio delle loro mobilità.
La cosa più difficile nella valutazione del materiale a disposizione è
quantificare il valore dei singoli pezzi; negli scacchi classici si sono
fissati, nei secoli di esperienza, alcuni valori di riferimento:
o Pedone: 1
o Cavallo: 3
o Alfiere: 3
o Torre: 5
o Regina: 9
In realtà vi sono opinioni discordanti, spesso all’alfiere viene
attribuito un valore maggiore che al cavallo. Poi verso il finale,
ovvero quando ad un certo punto i pezzi in gioco cominciano ad
essere “pochi”, i valori cambiano.
Inoltre, il valore dei pedoni negli scacchi classici varia fortemente a
seconda della posizione del pedone: se infatti esso può facilmente
essere portato in zona di promozione varrà la pena difenderlo anche
a costo di perdere ad esempio una torre ed un alfiere, per guadagnare
una regina.
43
44. Indipendentemente da tutto ciò, analizzando la nostra variante 3D, ci
si accorge che i valori non vi si adattano: la torre, ad esempio, non è
così “potente” come negli scacchi, perde infatti nella terza
dimensione la sua funzione di “muro”; inoltre non è più così efficace
nel sostegno dei pedoni.
Lungi dagli obiettivi di questa tesi eseguire un approfondito studio
sul punteggio base da assegnare ai pezzi, si è immaginata una sorta di
“traduzione” dagli scacchi classici agli scacchi 3D, successivamente
spiegata.
• Sviluppo
Il valore di un pezzo è tanto maggiore quanto maggiore è la sua
influenza sulla scacchiera. Ad esempio una torre nella sua posizione
originale non “controlla” nessuna casella, ovvero non può muoversi
perché bloccata da altri pezzi del suo stesso schieramento.
I pezzi hanno massima influenza quando sono mossi verso il centro
della scacchiera.
Per questi motivi, negli scacchi classici si costruiscono array di interi
di dimensioni della scacchiera, i cui valori corrispondono a piccoli
incrementi o decrementi di punteggio da applicare ai pezzi a seconda
della loro posizione.
In questo modo una scacchiera con pezzi sviluppati varrà di più
rispetto ad una con i pezzi bloccati nelle posizioni originali.
Nel caso nostro sarà semplice adattare una simile tecnica, non
essendo obiettivo di questa tesi individuare i numeri ottimali per
ottenere la valutazione più precisa: sarà sufficiente un array con
somma zero e valori tanto cospicui da fare la differenza rispetto ad
un metodo di valutazione privo di questo accorgimento.
• Struttura dei pedoni
Negli scacchi classici, in due dimensioni, risulta abbastanza semplice
e intuitivo modificare il valore dei pedoni in funzione delle loro
posizioni reciproche.
Ad esempio, se in una colonna vi sono due o più pedoni dello stesso
colore è naturale pensare che essi si ostacoleranno. Al comparire
della terza dimensione tuttavia questo fenomeno tende praticamente
a scomparire, muovendosi essi non solo lungo le colonne ma anche
attraverso i piani.
44
45. Omettiamo quindi di descrivere le varie strategie utilizzate per il
calcolo del valore della struttura dei pedoni negli scacchi classici,
perché esse non sarebbero adottabili per nostri scacchi 3D.
• Posizione del re
Negli scacchi classici, durante l’apertura (inizio della partita) e il
mediogioco (parte del gioco difficilmente definibile, in essa i pezzi
sono sviluppati e sono ancora “tanti”), il re è bene che stia dove si
trova all’inizio, o ancora meglio “arroccato” ad uno dei due lati.
Negli scacchi 3D non vi è manovra di arrocco, quindi è consigliabile
che all’inizio esso rimanga nei pressi della casella iniziale, anche
perché sarebbe inutile sprecare preziose mosse di sviluppo per
spostare il lento re e portarlo alla mercé dell’esercito avversario.
Nel finale della partita, quando i pezzi sono “pochi”, è essenziale,
negli scacchi classici, che il re partecipi attivamente al
combattimento.
Infatti, in tutti i finali fondamentali (re e regina contro re, re e torre
contro re, re e pedone contro re, ...) esso gioca un ruolo
fondamentale.
Di uguale importanza, se non maggiore, sarà negli scacchi 3D, nei
quali è ancora più arduo, a causa della tridimensionalità, dare lo
scacco matto.
E’ quindi utile individuare un momento della partita oltre il quale il
re “vale” di più se portato verso il centro dela scacchiera.
La metodologia più semplice da utilizzare è valutare il materiale a
disposizione dell’avversario: nel momento in cui i pezzi sono troppo
pochi per nuocere seriamente, allora il re potrà essere portato allo
scoperto.
45
46. Soluzione adottata
Bilanciamento degli schieramenti
Anzitutto è opportuno fissare dei valori di base a ciascun tipo di
pezzo. Ciò può essere fatto nel seguente modo: si possono definire i
suddetti valori rapportando i gradi di mobilità dei pezzi negli scacchi
classici con quelli degli stessi nel 3D, e variare il loro valore
proporzionalmente.
Nella tabella seguente si compara la mobilità di cavallo, alfiere, torre
e regina negli scacchi 3D e negli scacchi classici.
3D Chess Scacchi Classici
Pezzo Direzioni Caselle visitabili Direzioni Caselle visitabili
Al centro Sui vertici Al centro Sui vertici Al centro Sui vertici Al centro Sui vertici
Cavallo 24 8 24 8 8 2 8 2
Alfiere 12 3 32 12 4 1 13 7
Torre 6 3 12 12 4 2 14 14
Regina 26 7 52 28 8 3 27 21
Nella prossima tabella si riportano soltanto le caselle visitabili,
rapportate però al numero di caselle totali della scacchiera (125 nel
3D, 64 negli scacchi classici).
Caselle visitabili / Caselle totali
Pezzo 3D Chess Scacchi Classici
Al centro Sui vertici Al centro Sui vertici
Cavallo 0.192 0.064 0.125 0.031
Alfiere 0.256 0.096 0.203 0.109
Torre 0.096 0.096 0.219 0.219
Regina 0.416 0.224 0.422 0.328
Si nota subito che il cavallo 3D risulta molto più mobile di quello
classico. Al contrario, la torre è sensibilmente meno mobile, come ci
si aspettava.
46
47. Si osservi ora nella tabella seguente l’aumento di mobilità in
prossimità dei vertici e in posizione centrale di ciascun pezzo dagli
scacchi classici agli scacchi 3D:
Fattore di incremento di mobilità nel 3D
Pezzo Direzioni Caselle visitabili Caselle visitabili/Caselle totali
Al centro Sui vertici Al centro Sui vertici Al centro Sui vertici
Cavallo 3 4 3 4 1.536 2.064
Alfiere 3 3 2.286 1.714 1.261 0.881
Torre 1.5 1.5 0.857 0.857 0.438 0.438
Regina 3.25 2.333 1.857 1.333 0.986 0.683
A questo punto, applicando a questi fattori di incremento e ai valori
negli scacchi classici un’opportuna funzione, sarà possibile ottenere
dei valori da sperimentare nel motore per questi pezzi.
Sicuramente il fattore di peso maggiore sarà il rapporto tra caselle
visitabili e caselle totali, esso esprime la differenza di mobilità meglio
rispetto agli altri fattori, essendo relativo.
Un buon esempio potrebbe essere dare 10% di peso agli incrementi di
direzioni, 14% di peso agli incrementi assoluti di caselle visitabili e
26% a quelli relativi.
Perciò, definiti con
e gli incrementi assoluti di direzione rispettivamente al centro e ai vertici
e gli incrementi assoluti di caselle rispettivamente al centro e ai vertici
e gli incrementi relativi di caselle rispettivamente al centro e ai vertici
il fattore selezionato è:
= [10 ∗ ( + ) + 14 ∗ ( + ) + 28 ∗ ( + ) ]/100
Esso andrà poi moltiplicato per il valore convenzionale del pezzo
negli scacchi classici.
Sostituendo i valori numerici si ottiene:
o Cavallo: = 2.616 segue punteggio: 2.616 ∗ 3 = 7.848
o Alfiere: = 1.717 segue punteggio: 1.717 ∗ 3 = 5.151
o Torre: = 0.768 segue punteggio: 0.768 ∗ 5 = 3.839
o Regina: = 1.439 segue punteggio: 1.439 ∗ 9 = 12.95
47
48. I valori così ottenuti saranno utilizzati come punteggio base dei
pezzi. Mancano ancora tuttavia Unicorno e Pedone.
Per quanto riguarda l’Unicorno, esso ha una mobilità che oscilla tra
circa (sui vertici) e circa (al centro) rispetto a quello della regina
(in base alle tabelle stilate). Inoltre, può muoversi al massimo su
circa ¼ della scacchiera; questo fa di esso un pezzo estremamente
debole e poco prezioso.
Fissiamo il valore dell’unicorno a rispetto a quello della regina, cioè
2.16.
Per i pedoni non si hanno parametri di valutazione da cui partire, se
non il fatto che negli scacchi classici essi valgono 1 punto. Fissiamo il
loro valore a 1 anche negli scacchi 3D.
Pezzo valore
Si riassume nella tabella a fianco i valori di base che Pedone 1
si è deciso di adottare, arrotondati ad una cifra Unicorno 2.2
Cavallo 7.8
decimale: sarebbe inutile essere molto precisi in
Alfiere 5.1
quanto si tratta di valori praticamente arbitrari. Torre 3.8
Regina 12.9
Sviluppo
I pezzi aumentano di valore quando vengono sviluppati, cioè
sostanzialmente portati verso il centro della scacchiera. Questo vale
quasi per tutti, a parte per i pedoni.
Essi infatti valgono tanto quanto più si avvicinano alla zona di
promozione.
Il modo più semplice ed efficiente per ottenere questo effetto è
utilizzare un array di interi grande quanto la scacchiera, inserendo
un numero negativo nelle caselle “cattive” (vertici e bordi) e un
numero positivo nelle caselle “buone” (centrali).
Per ogni pezzo mosso si sommerà al suo valore il numero
corrispondente alla casella in cui lo si vuole muovere.
Questi accorgimenti si rivelano utili specialmente nella fase iniziale
del gioco.
Posizione del re
Al re assegneremo in fase di realizzazione un valore
incommensurabilmente alto rispetto a quello degli altri pezzi,
naturalmente, perché perderlo equivale a perdere la partita.
48
49. Il re necessita anche di due array di interi grandi come la scacchiera:
uno per l’inizio della partita, per fare in modo che esso tenda a
rimanere nella posizione iniziale, e uno per il finale.
Si entrerà nel “finale” quando il valore dei pezzi avversari sarà uguale
o inferiore a 1300, re escluso.
Riepilogo funzionamento globale
Fig. 9 - comportamento macroscopico del motore
In figura 9 è illustrato il comportamento del motore dal punto di vista
macroscopico: la UI comunica al motore la posizione della scacchiera,
intesa come stato della partita. Se valida, si generano le mosse possibili. Se
ne esistono, si ricerca la mossa migliore, e la si comunica alla UI.
49
50. Progettazione dell’Interfaccia
Scacchiera Tridimensionale
Essa è la componente principale dell’interfaccia utente.
Vale la pena stendere una breve introduzione sull’argomento grafica
tridimensionale.
La grafica tridimensionale
La computer grafica 3D è un ramo della computer grafica che si basa
sull'elaborazione di modelli virtuali in 3D da parte di un computer. Essa
viene utilizzata insieme alla computer animation nella realizzazione di
immagini visuali per cinema o televisione, videogiochi, ingegneria, usi
commerciali o scientifici. Il termine può anche essere riferito ad immagini
prodotte con lo stesso metodo.
La grafica computerizzata tridimensionale è basilarmente la scienza, lo
studio e il metodo di proiezione della rappresentazione matematica di
oggetti tridimensionali tramite un'immagine bidimensionale attraverso
l'uso di tecniche come la prospettiva e l'ombreggiatura (shading) per
simulare la percezione di questi oggetti da parte dell'occhio umano. Ogni
sistema 3D deve fornire due elementi: un metodo di descrizione del sistema
3D stesso ("scena"), composto di rappresentazioni matematiche di oggetti
tridimensionali, detti "modelli", e un meccanismo di produzione di
un'immagine 2D dalla scena, detto "renderer".
Modelli 3D
Oggetti tridimensionali semplici possono essere rappresentati con
equazioni operanti su un sistema di riferimento cartesiano tridimensionale:
per esempio, l'equazione x²+y²+z²=r² è perfetta per una sfera di raggio r.
Anche se equazioni così semplici possono sembrare limitative, l'insieme
degli oggetti realizzabili viene ampliato con una tecnica chiamata geometria
solida costruttiva (CSG, constructive solid geometry), la quale combina
oggetti solidi (come cubi, sfere, cilindri, ecc.) per formare oggetti più
complessi attraverso le operazioni booleane (unione, sottrazione e
intersezione): un tubo può ad esempio essere rappresentato come la
differenza tra due cilindri aventi diametro differente.
L'impiego di equazioni matematiche pure come queste richiede l'utilizzo di
una gran quantità di potenza di calcolo, e non sono quindi pratiche per le
applicazioni in tempo reale come videogiochi e simulazioni. Una tecnica più
efficiente, ma che permette un minore livello di dettaglio, per modellare
oggetti consiste nel rilevare solo alcuni punti dell'oggetto, senza
informazioni sulla curva compresa tra di essi. Il risultato è chiamato
50
51. modello poligonale. Questo presenta "faccette" piuttosto che curve, ma
sono state sviluppate tecniche d rendering per ovviare a questa perdita di
di
dati.
Delle superfici poligonali di un modello senza informazioni sulla curvatura
possono essere comunque raffinate per via algoritmica in superfici
perfettamente curve: questa tecnica è chiamata "superfici di su suddivisione"
(subsurfing), perché la superficie viene suddivisa con un processo iterativo
,
in più superfici, sempre più piccole, fedeli alla curva interpolata e che vanno
a comporre un'unica superficie sempre più liscia.
Creazione della scena
Una scena si compone di "primitive" (modelli tridimensionali che non
ompone
possono essere ulteriormente scomposti).
Le primitive sono generalmente descritte all'interno del proprio sistema di
riferimento locale, e vengono posizionate sulla scena attraverso opportune
trasformazioni. Le trasformazioni affini più impiegate, come omotetia,
zioni.
rotazione e traslazione, possono essere descritte in uno spazio proiettivo
con una matrice 4x4: esse si applicano moltiplicando la matrice per il
vettore a quattro componenti che rappresenta ogni punto di controllo delle
ogni
curva. La quarta dimensione è denominata coordinata omogenea.
Rendering
Il rendering è il processo di produzione dell'immagine finale a partire dal
modello matematico del soggetto (scena). Esistono molti algoritmi di
rendering, ma tutti implicano la proiezione dei modelli 3D su una superficie
2D.
Gli oggetti tridimensionali devono essere proiettati idealmente sulla
superficie bidimensionale del monitor.
Il tipo di proiezione p usato è la prospettica, anche se ad esempio nei
più ,
sistemi CAD e CAM si utilizza la proiezione ortogonale.
stemi
La differenza fra le due è che nella prospettica gli oggetti più lontani sono
visualizzati di dimensioni minori rispetto a quelli più vicini all’ “occhio”. I
minori
software rendono la prospettiva moltiplicando una costante di dilatazione k
moltiplicando
elevata all’opposto della distanza dall’osservatore. Se k = 1 non vi è
prospettiva. All’aumentare di k aumenta la distorsione prospettica.
Fig. 10
Un punto di fuga - Due punti di fuga - Tre punti di fuga
Una trattazione dettagliata della matematica riguardande la proiezione
prospettica esula dagli scopi di questo testo.
51
52. Per una spiegazione intuitiva è sufficiente immaginare una videocamera che
si muove all’interno di uno spazio tridimensionale. In questo spazio sono
contenuti i modelli tridimensionali, nel nostro caso si tratta della scacchiera
e dei pezzi.
L’utente sarà in grado di controllare il “movimento” di questa videocamera
ideale all’interno dello spazio 3D in modo tale da poter visualizzare
comodamente.
Fig. 11 - Field of view (campo visivo) della videocamera
In figura 11 è descritto il campo visivo che viene sottoposto all’utente: è di
forma tronco-conica (frustum), tutto ciò che non è contenuto in esso non
viene proiettato (sulla base minore, che rappresenta la superficie dello
schermo) e quindi non viene visualizzato.
Progettazione dei modelli
Vi sono principalmente quattro modi per rappresentare un modello 3D:
• Modelli poligonali – Un insieme di punti nello spazio
tridimensionale, detti vertici, vengono connessi da segmenti in modo
da formare una maglia (mesh). La maggior parte dei modelli 3D sono
costruiti in questo modo per la loro flessibilità e velocità di
rendering. Tuttavia, i poligoni sono piani, quindi con essi le
curvature possono soltanto essere approssimate utilizzando un gran
numero di poligoni.
52
53. • NURBS – Le superfici NURBS sono definite da curve spline, cioè
parametrizzate da punti di controllo pesati. La curva segue (senza
necessariamente interpolarli) i punti. Aumentando il peso di un
punto si farà in modo che la curva passi più vicino ad esso. In questo
modo si riescono ad ottenere superfici estremamente lisce.
• Splines & Patches – Come le NURBS, splines and patches dipendono
da line curve per definire la superficie visibile. Sono una via di mezzo
fra NURBS e poligoni in termini di flessibilità e facilità di utilizzo.
• Primitives modeling – Consiste nell’utilizzare primitive geometriche
come sfere, cilindri, coni o cubi, come “mattoni” per modelli più
complessi. Si ottengono così costruzioni veloci, e le forme sono
matematicamente precise. Richiede però di definire
geometricamente il modello da costruire; ciò non è sempre possible
o facile da ottenere.
Nel nostro caso, trattandosi di pezzi degli scacchi, quindi di forme
geometriche non troppo complesse, i metodi migliori sarebbero modelli
poligonali o primitives modeling. Quest’ultimo sembrerebbe il più indicato,
tuttavia, sebbene sia controintuitivo, i modelli ottenuti sarebbero
certamente più grezzi: modellare tutto tramite primitive, infatti,
implicherebbe poter utilizzare soltanto curve e forme definibili
geometricamente. Invece, tramite poligoni, si avrà più rapidità al momento
della costruzione dei modelli (si potranno comporre facilmente tramite una
serie di punti); l’arrotondamento delle superfici si potrà ottenere per
suddivisioni successive, mantenendo inoltre la massima compatibilità (i
formati poligonali sono i più comuni al momento).
Pezzi
Consisteranno in sette modelli poligonali, ciascuno per un tipo di pezzo. Il
colore e il posizionamento sulla scacchiera verranno dati loro dal
programma di gioco.
Scacchiera
Per costruirla si utilizzerà un mattoncino fondamentale (un parallelepipedo
molto esteso in larghezza rispetto alla sua altezza, una sorta di
“mattonella”), ripetuto 125 volte a colorazione alterna.
Si visualizzeranno cioè cinque scacchiere piane sovrapposte, come in figura
1.
53
54. Interazione con l’utente
Mouse – Movimentazione dei pezzi
Per muovere i pezzi si deve poter, tramite mouse, selezionare il pezzo da
muovere e successivamente selezionare la casella in cui muoverlo. In figura
12 è illustrato l’algoritmo ad alto livello per la selezione del pezzo.
L’utente fa click sulla
schermata dell’interfaccia
3D
No
Ha fatto click su un
Sì
Attesa di pezzo?
interazione utente
No
No Ha fatto click su una
Il pezzo è suo?
casella?
No Sì Sì
Al momento è
Il pezzo può
evidenziato un
muovere?
pezzo? Sì
Sì
Togliere l’evidenziazione
Evidenziare il pezzo e le
al pezzo precedentemente
La casella è caselle in cui esso può
selezionato ed alle caselle No
evidenziata? muovere
in cui esso può muovere
(Selezione Pezzo)
(Deselezione Pezzo)
Sì
Muovere il pezzo nella
casella (Spostamento
Pezzo)
Fig. 12 - Flow chart raffigurante il loop di input da mouse per l'utente
54
55. Il problema fondamentale però è che l’utente fa click su un punto di una
superficie piana; a partire dalle coordinate piane del click bisogna risalire a
quale punto dello spazio esso corrisponde.
I modelli che posizioniamo nello spazio sono costituiti da triangoli. Si
tratterà quindi di determinare, ciclando su tutti i modelli contenuti nello
spazio, quale di essi è il più vicino alla camera fra quelli intersecati dalla
semiretta avente origine nel punto in cui l’utente ha fatto click.
I modelli sono composti da triangoli. Bisognerà quindi procedere in questo
modo:
1. Per ogni modello controllare se la sfera di raggio minimo che lo
contiene tutto interseca la semiretta in questione.
2. Se la sfera è intersecata bisognerà testare, triangolo per triangolo, se
vi sono triangoli componenti il modello che intersecano la
semiretta.
3. Se ve ne sono, salvare quello di distanza minima dalla camera. Se
minore di quelli precedentemente trovati, memorizzare il modello a
cui appartiene.
In questo modo si troverà il modello più vicino alla camera intersecato dalla
semiretta.
Il problema più complesso è individuare quali triangoli di un modello,
ammesso che ve ne siano, siano intersecati dalla semiretta.
Un buon sistema per ottenere questo risultato è utilizzare un algoritmo il
cui uso è abbastanza diffuso per casi come questo, ideato da Tomas Moller e
Ben Trumbore nel 2003 chiamato “Fast, Minimum Storage Ray-Triangle
Intersection".
Questo algoritmo permette, senza perdita di velocità rispetto agli algoritmi
precedenti, di risparmiare memoria nel suo utilizzo.
In appendice si allega il documento originale che sarà utilizzato per
implementare l’algoritmo nella parte di realizzazione.
Tastiera – Camera
Per far in modo che il gioco sia il più controllabile possibile, si progettano
due modalità di visualizzazione (camera).
Una il prima persona, che permette cioè di spostare liberamente la camera
per mezzo della tastiera nello spazio contenente la scacchiera.
55
56. Un’altra invece vincolata alle righe e alle colonne della scacchiera: ovvero,
essendovi, 5 righe e 5 colonne per ciascuno dei 5 piani, con (5 + 5) ∗ 5 = 50
posizioni; ad ogni spostamento di posizione la si farà puntare
parallelamente alla direzione della riga o colonna, ma sarà possibile ruotarla
sul posto per facilitare la visualizzazione.
L’utente potrà in qualsiasi momento cambiare camera passando da totale
libertà di movimento a controllo più vincolato.
Comunicazione Interfaccia-Motore
Differenza sostanziale tra le realizzazioni di Interfaccia e Motore sarà la
tecnologia d’implementazione; per la prima è necessaria una piattaforma in
grado di supportare la grafica 3D e che renda il più possibile agevole lo
sviluppo di videogame, mentre per il secondo è importante l’efficienza di
calcolo.
Perciò, per l’Interfaccia sarà certamente opportuno utilizzare una API per il
game-programming. Ve ne sono tante disponibili, ma non molte soddisfano
il requisito della portabilità che è stato richiesto (vedi Analisi, pag. 17).
Per quanto riguarda il Motore sarà più utile una realizzazione che non giri
su macchina virtuale; in questo modo si avrà il massimo dell’efficienza.
Quindi un linguaggio compilato.
Per soddisfare la portabilità basterà usare un linguaggio molto diffuso, in
modo che esistano compilatori per esso almeno per le piattaforme più
utilizzate; certo, ci sarà bisogno di ricompilare per passare da un ambiente
ad un altro, ma il requisito di efficienza è certamente più importante per
ottenere un Motore “intelligente” in grado di tener testa al giocatore umano.
La comunicazione fra i due è sufficiente che sia in una sola direzione, cioè
dall’Interfaccia al Motore, o vice versa; non ponendo vincoli in questo
frangente si lascia maggior libertà in fase di realizzazione.
56
57. Realizzazione
Tecnologie utilizzate
Microsoft XNA (Xbox New Architecture)
Si tratta di un inseme di tool inseriti in un ambiente di runtime “managed”
il cui scopo è facilitare lo sviluppo di videogame sollevando lo sviluppatore
dall’onere di dover scrivere codice ripetitivo. Comprende:
• XNA Framework
Si basa sull’implementazione del .NET Compact Framework 2.0 per
Xbox 360 e su .NET Framework 2.0 per Windows. E’ disponibile per
Windows XP, Windows Vista e Xbox 360; permette di scrivere i
videogame per il runtime di XNA: questo fa sì che essi possano girare
su qualsiasi piattaforma a patto che questa supporti il runtime.
L’utilizzo di questa tecnologia quindi ci permetterebbe di portare senza
troppe difficoltà il gioco su piattaforme diverse, e ci solleva inoltre
dall’onere di reinventare ciò che nel gergo del game development si
definisce “game loop” (e tutto ciò che esso implica).
• XNA Build
E’ un insieme di tool per la gestione degli “asset” di gioco (icone, file
audio, modelli 3D, ...). Aiuta a definire la cosiddetta game asset
pipeline. Quest’ultima descrive il processo attraverso il quale i
contenuti vengono trasformati in forme utilizzabili dall’engine del
gioco. XNA Build permette di personalizzare a vari livelli ogni stadio
della suddetta pipeline.
• Mono.XNA
E’ una implementazione cross platform di XNA gaming framework.
Prende il nome da Mono, ormai nota implementazione cross
platform di .NET framework
Gira su Windows, MacOS e Linux e usa OpenGL per la grafica 3D.
Al momento è ancora in fase di realizzazione.
Questo aspetto di XNA ci assicura la portabilità richiesta nei requisiti non
funzionali.
XNA sarà quindi utilizzato per la realizzazione dell’Interfaccia.
57
58. Blender
E’ un’applicazione per grafica 3D rilasciata sotto forma di software libero
sotto GNU General Public License. Può essere usato per modeling e molto
altro ancora, ed è disponibile per Linux, Mac Os X e Windows.
Questo lo rende più che sufficiente per lo scopo di questa tesi.
Con Blender saranno costruiti i modelli 3D di pezzi e scacchiera.
ANSI C
E’ lo standard emesso dall’American National Standards Institute (ANSI)
per il linguaggio di programmazione C.
ANSI C è supportato praticamente da tutti i compilatori più utilizzati;
perciò, ogni programma scritto in C standard garantisce la compilazione su
qualsiasi piattaforma che abbia una conforme implementazione del C.
Il motore, essendo nulla più che una collezione di algoritmi e strutture dati,
potrà quindi essere costruito in ANSI C per massimizzarne la portabilità pur
mantenendo alta l’efficienza, trattandosi di un linguaggio compilato.
Managed/Unmanaged Interoperability
Realizzando l’Interfaccia in XNA, si utilizzerà C#, un linguaggio Microsoft
facente parte del CLR di .NET. Si tratterà quindi di codice che gira in
macchina virtuale, ovvero managed.
Per contro, il Motore sarà scritto in ANSI C. Per la realizzazione in
Microsoft Windows verrà compilato come dll C++ ed esporrà delle funzioni
che potranno essere richiamate dall’Interfaccia tramite il sistema di
interoperabilità tra codice managed e unmanaged.
Si tratterà quindi di invocare codice unmanaged dal codice managed.
Vi sono alcune alternative per compiere questa operazione:
• Platform Invoke (detta anche P/Invoke) permette di richiamare
funzioni scritte in qualsiasi linguaggio unmanaged a patto che la
signature sia ridichiarata in codice managed.
• COM interop permette di richiamare component COM da qualsiasi
linguaggio managed.
• C++ interop (anche detta It Just Works, IJW) è una caratteristica
specifica del C++ che permette di utilizzare direttamente API COM e
non solo. E’ più potente di COM interop ma richiede più cura.
58
59. Su MSDN si raccomanda di scegliere seguendo il flow-chart riportato in
figura 13.
Fig. 13 - Flow chart per la scelta della tecnologia per interoperabilità managed/unmanaged
La dll del motore non sarà certo una COM API, essendo scritta in C.
Con complex API si intende un’API che ha le signature dei metodi scritte in
modo difficile da dichiarare in codice managed. Essendo l’API ancora da
scrivere, faremo in modo che ciò non avvenga.
L’API una volta scritta rimarrà tale; o comunque il numero di funzioni sarà
sufficientemente piccolo (dell’ordine della decina) che anche se varierà non
sarà difficile propagare la variazione anche al codice managed.
Quindi la tecnologia per la comunicazione tra Interfaccia e Motore sarà
Platform Invoke.
59
60. Realizzazione del Motore
Il motore è stato realizzato in ANSI C, non essendovi necessità o particolari
benefici nel ricorrere all’OOP in un caso come questo.
Esso consiste di 5 file header “.h” e 8 file “.c”.
Header files
defs.h
defs.h, contenente la definizione delle costanti, dei tipi non primitivi
utilizzati e delle variabili globali. Qui sotto si riporta uno spezzone di codice
contente le strutture dati utilizzate per la memorizzazione delle mosse
generate.
moveBytes contiene l’indice della casella di arrivo e partenza di una mossa
(from e to), il pezzo a cui viene promosso nel caso di mossa di promozione
(promote), una serie di bit indicante il tipo di mossa (bits: i primi 3 bit di
questo char vengono usati per indicare se si tratta di una mossa di cattura,
e/o di un pedone, e/o di promozione).
typedef struct {
int from;
int to;
int promote;
char bits;
} moveBytes;
In union con un int in modo che si riesca a compararle rapidamente.
typedef union {
moveBytes b;
int u;
} moveUni;
gener_t associa la mossa ad un punteggio (score) utilizzato per il sorting
nell’implementazione dell’history heuristics.
typedef struct {
moveUni m;
int score;
} gener_t;
history_t è invece un elemento dello stack usato per mantenere la storia
della partita, in modo da poter ritornare indietro di una mossa.
60