Tesi Specialistica - Bruno Tagliapietra

  • 780 views
Uploaded on

 

More in: Business
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
    Be the first to like this
No Downloads

Views

Total Views
780
On Slideshare
0
From Embeds
0
Number of Embeds
0

Actions

Shares
Downloads
15
Comments
0
Likes
0

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide

Transcript

  • 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
  • 2. 2
  • 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
  • 40. Fig. 7 -Algoritmo semplificato di ricerca della mossa migliore 40
  • 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
  • 61. typedef struct { moveUni m; int capture; int fiftMoves; } history_t; protos.h protos.h contiene i prototipi delle funzioni. Si rimanda la spiegazione di ciascuna di essi alla parte che la tratta nel dettaglio. BOOLEANVAL in_check(int s); BOOLEANVAL attack(int sq, int side); void take_back(); void generate(); void generateCaptures(); void genDo(int from, int to, int bits); void generatePromotions(int from, int to, int bits); int getSimpleStrFromMove(moveBytes, char* result); void getStringSquare(int number, char* result); void initBoard(); BOOLEANVAL doMove(moveBytes m); void parseFEN(char* FEN_string); int parseMove(char *square); int evaluate(); int eval_BLACK_king(int sq); int eval_BLACK_pawn(int sq); int eval_WHITE_king(int sq); int eval_WHITE_pawn(int sq); void sort(int from); int search(int alpha, int beta, int depth); void sort_pv(); void think(BOOLEANVAL print); int quiesce(int alpha,int beta,int depth); void checkup(); int get_ms(); void print_square(int number); void print_move_str(moveBytes m); void print_result(); altri header RaumschachChessEngine.h, stdafx.h e targetver.h, i quali poco hanno a che fare con la realizzazione effettiva dell’algoritmo; servono infatti per il wrapping del Motore in una .dll. Per completezza, sono riportati in appendice. 61
  • 62. C Files data.c data.c contiene tutte le strutture dati necessarie allo scopo (e previste nella progettazione): • mailbox¸addresser, pieces, e colors per la rappresentazione della scacchiera. • offset e offsets per la movimentazione dei pezzi. • generData per contenere le mosse generate. • pv e pvLength per mantenere ogni variazione principale trovata in ciascuna iterazione dell’iterative deepening. • history per l’implementazione di history heuristics. E’ stato fatto largo uso di strutture dati sovrabbondanti e dati hard-coded per massimizzare la velocità di esecuzione a scapito della flessibilità ed occupazione di memoria, come richiesto dalla progettazione. Non si riportano qui le definizioni di mailbox e addresser, semplicemente il sistema descritto nella progettazione (vedi pag. 25) è stato adattato agli scacchi 3D: mailbox rappresenta un cubo 9x9x9, cioè il cubo 5x5x5 della scacchiera contenuto in un cubo sufficientemente più grande in modo da far sì che tutte le mosse possibili di un pezzo ricadano all’interno di esso. Se una mossa risulterà nello spostamento in una cella di mailbox contenente “-1” allora significa che essa è fuori dalla scacchiera. Altrimenti, si dovrà aggiornare il valore di [ [ ]] con l’intero corrispondente al tipo di pezzo e [ [ ]] con l’intero corrispondente al colore del pezzo. offsets contiene il numero di direzioni in cui ciascuno dei 7 pezzi può muovere: int off_sets[7] = { 0, 24, 8, 12, 6, 26, 26 }; La matrice offset, che non riportiamo per brevità, invece, per ciascuno dei 7 pezzi contiene il numero da sommare all’indice di mailbox corrispondente alla posizione attuale per muoversi in ciascuna delle direzioni in cui il pezzo può muoversi. Di seguito si riporta il codice rimanente con a lato i commenti esplicativi. int plyCurrent; /* the number of half-moves (plyCurrent) since the root of the search tree */ int histPlyCurrent; /* h for history; the number of plyCurrent since the beginning of the game */ 62
  • 63. /* generData is some memory for move lists that are created by the movegenerators. The move list for plyCurrent n starts at firstMove[n] and endsat firstMove[n + 1]. */ gener_t generData[GENE_STACK]; int firstMove[MAXIMUM_PLY]; /* a "triangular" PV array; */ moveUni pv[MAXIMUM_PLY][MAXIMUM_PLY]; int pvLength[MAXIMUM_PLY]; BOOLEANVAL followPV; /* the engine will search for maxTime milliseconds or until it finishes searching maxDepth plyCurrent. */ int maxTime; int maxDepth; /* the time when the engine starts searching, and when it should stop */ int startTime; int stopTime; int fiftMoves; /* the number of moves since a capture or pawn moveUni, used to handle the fiftMoves-moveUni-draw rule */ int nodes; /* the number of nodes we've searched */ /* the history heuristic array (used for move ordering) */ int history[125][125]; /* we need an array of history_t's so we can take back the moves we make */ history_t historyData[HISTORY_STACK]; 63
  • 64. board.c board.c contiene tutte le funzioni responsabili dello stato della scacchiera: attack determina se una certa casella passata come parametro è minacciata dal bianco o dal nero. E’ molto simile a generate, utilizza un algoritmo simile a quello di quest’ultima funzione, ma limitato alle mosse di cattura. generate genera tutte le mosse pseudo-legali nell’attuale posizione (non tiene conto dello scacco al re). Spazza la scacchiera in cerca di pezzi appartenenti al lato che ha il tratto e poi determina quali caselle essi attaccano. Quando trova una mossa possible chiama genPush che la inserisce nello stack. Di seguito riportiamo parti del codice di questa funzione indicando le parti omesse con “/* --- RIASSUNTO PARTE OMESSA --- */”. void generate() { /* ---Dichiarazioni iniziali ---*/ for (i = 0; i < 125; i++) { if (colors[i] == sideToMove) { if (pieces[i] == PAWN) { /* --- Le mosse dei pedoni non sono trattabili con il metodo degli offset perché variano a seconda della situazione delle caselle circostanti il pedone (mosse che può fare solo in cattura). Perciò sono testate al di fuori dal ciclo principale --- */ } else //se non è un pedone { for (j = 0; j < off_sets[pieces[i]]; ++j) { //per ogni offset di questo tipo di pezzo for (n = i;;) //ciclo infinito con inizializz. { //n rappresenta una ipotetica casella in cui //muovere n = mail_box[addresser[n] + off_set[pieces[i]][j]]; if (n == -1) break; //se contiene -1 siamo usciti dalla scacchiera if (colors[n] != EMPTY) { if (colors[n] == sideNotToMove) //è una mossa di cattura! genPush(i, n, 1); break; } 64
  • 65. genPush(i, n, 0); //è una mossa normale if (!slide[pieces[i]])break; } } } } } } doMove esegue una certa mossa passata come parametro e verifica se si è sotto scacco; se sì, richiama take_back. generateCaptures genera solo le mosse di cattura pseudo-legali nell’attuale posizione. E’ molto simile a generate e ad attack. genPush inserisce una mossa nello stack generData. Assegna inoltre alla mossa lo score per l’ordinamento dell’history heuristics. void genPush(int from, int to, int bits) { gener_t *g; //Se si è mosso un pedone si invoca genPushProms if (bits & 2) { if (sideToMove == WHITE) { if (to <= Ee5) { genPushProms(from, to, bits); return; } } else { if (to >= Aa1) { genPushProms(from, to, bits); return; } } } /* ---Inserimento dati della mossa g nello stack ---*/ if (colors[to] != EMPTY) //se si tratta di una mossa di cattura facciamo in modo che //sia valutata prima delle altre g->score = 1000000 + (pieces[to] * 10) - pieces[from]; else g->score = history[from][to]; } 65
  • 66. genPushProms inserisce nello stack una dietro l’altra le 5 possibili mosse in caso di promozione. Si fa in modo tale da osservare per prime anche le mosse di promozione, nello stesso modo che nella genPush per le mosse di cattura, perché anche a seguito di esse la situazione muta notevolmente. initBoard inizializza la scacchiera per l’inizio della partita. void initBoard() { //inizia il bianco sideToMove = WHITE; sideNotToMove = BLACK; //azzeramento contatore delle 50 mosse consecutive fiftMoves = 0; //azzeramento contatore delle mezze-mosse(ply) esaminate plyCurrent = 0; //azzeramento contatore delle mezze-mosse dall’inizio della //partita histPlyCurrent = 0; //la lista delle mosse alla mezza-mossa n-esima inizia a //firstMove[n] e termina a firstMove[n+1] (nell’array generData) firstMove[0] = 0; } in_check applica attack sulla casella in cui si trova il Re del colore passato come parametro s. BOOLEANVAL in_check(int s) { int j; for (j=0; j < 125; ++j) if (pieces[j] == KING && colors[j] == s) return attack(j, s ^ 1); } parseMove traduce una mossa dal formato testuale scelto nell’analisi, ovvero stringa da 6 o 7 caratteri (3 per la casella di partenza, 3 per la casella di arrivo, 1 per l’iniziale dell’eventuale pezzo in cui promuovere) al formato descritto dalle strutture dati interne del Motore. parseFEN traduce la posizione in formato che si è battezzato Forsyth–Edwards- Raumschach, descritto nell’analisi, nel formato interno al Motore e aggiorna la scacchiera alla posizione letta. 66
  • 67. take_back annulla l’ultima mossa eseguita. Analizzando il codice riportato in appendice si può notare che vi sono parti di codice chiaramente ridondanti e ripetute; tutto è fatto in modo da ottimizzare la velocità di esecuzione, anche a scapito del riutilizzo del codice. search.c contiene le funzioni che implementano l’algoritmo di ricerca così come progettato. checkup richiamato ad intervalli regolari durante il calcolo, controlla che il tempo non sia scaduto. sort esegue il sorting necessario per l’history heuristics. void sort(int from) { int i; int bs; /* best score */ int bi; /* best i */ gener_t g; bs = -1; bi = from; // cerca fra le mosse pseudo-legali della ply corrente for (i = from; i < firstMove[plyCurrent + 1]; ++i) // in modo da ottenere che la mossa con lo score // più alto vada all’indice from. In questo modo verrà // eseguita come prossima mossa // (si ricordi che score non ha nulla a che vedere con // la valutazione della “bontà” della mossa, ma è un indice // della variabilità della posizione che questa mossa // potrebbe potenzialmente implicare) if (generData[i].score > bs) { bs = generData[i].score; bi = i; } g = generData[from]; generData[from] = generData[bi]; generData[bi] = g; } 67
  • 68. sort_pv Viene richiamata quando l’algoritmo di ricerca sta seguendo il ramo della variazione principale(vedi pag.35) risultato dalla ricerca effettuata nel precedente ciclo di iterative deepening (funzione search). Se trova nello stack generData la mossa della variazione principale corrispondente alla ply corrente, fa in modo che venga valutata per prima. Questo perché se era considerata la migliore nel ciclo di iterative deepening precedente probabilmente lo sarà di nuovo. In questo modo si leniscono le inefficienze introdotte dall’iterative deepening, che, si ricordi, si è stati costretti ad utilizzare per poter interrompere l’elaborazione e fornire un risultato al termine del tempo limite impostato (vedi pagg. 17 e 37). void sort_pv() { int i; followPV = FALSE; for(i = firstMove[plyCurrent]; i < firstMove[plyCurrent + 1]; ++i) if (generData[i].m.u == pv[0][plyCurrent].u) { followPV = TRUE; //così si fa sì che la mossa sia analizzata per prima generData[i].score += 10000000; return; } } think, search e quiesce implementano l’algoritmo di ricerca vero e proprio (vedi fig. 7) think richiama iterativamente search, aumentando la profondità massima di ricerca ad ogni iterazione, fino alla massima impostata. Così viene implementato l’iterative deepening, che permette di ottenere una risposta dopo un certo tempo massimo (alla peggio valutando la posizione con profondità 1, cioè muovendo praticamente a caso). void think(BOOLEANVAL print) { int i, j, x; stop_search = FALSE; //si fa in modo da poter ritornare qui nel caso finisse il //tempo prima di raggiungere la profondità massima setjmp(env); if (stop_search) { //se è finito il tempo si fa tornare la scacchiera //com’era prima. while (plyCurrent) take_back(); return; 68
  • 69. } //--- INIZIALIZZAZIONI --- for (i = 1; i <= maxDepth; ++i) { followPV = TRUE; //ad ogni iterazione viene chiama search x = search(-10000, 10000, i); //--- STAMPE CONDIZIONALI (per debug) --- } if (x > 9000 || x < -9000) //in questo caso è stata trovata una mossa decisiva break; } } search implementa l’algoritm alpha-beta pruning come descritto nella progettazione. Essa richiama quiesce, dedicata alla ricerca di mosse quiescenti (nel caso nostro si è deciso di far cadere in questa categoria le mosse di cattura), quando raggiunge la profondità massima. int search(int alpha, int beta, int depth) { int i, j, x; BOOLEANVAL areWeInCheck, foundAnyMove; //Se si è raggiunta la massima profondità, si continua //a cercare ma solo fra le mosse quiescenti if (depth == 0) return quiesce(alpha,beta); //Si aggiorna il contatore di numero di nodi visitati ++nodes; //Si controlla che il tempo a disposizione non sia finito if ((nodes & 1023) == 0) checkup(); //Se si è raggiunta la massima profondità assoluta //si valuta la posizione e si smette. if (plyCurrent >= MAXIMUM_PLY - 1) return evaluate(); if (histPlyCurrent >= HISTORY_STACK - 1) return evaluate(); //Nel caso si sia sotto scacco è meglio continuare //a cercare anche oltre la profondità massima impostata areWeInCheck = in_check(sideToMove); if (areWeInCheck) ++depth; //Si generano le mosse possibili per la posizione generate(); 69
  • 70. //Vedi sortpv (pag. 66) if (followPV) sort_pv(); foundAnyMove = FALSE; //Per ogni mossa... for (i=firstMove[plyCurrent];i<firstMove[plyCurrent+1];++i) { //Vedi sort (pag. 66) sort(i); //Se la mossa non è permessa, passare alla prossima if (!doMove(generData[i].m.b)) continue; foundAnyMove = TRUE; //L’esplorazione ricorsiva dell’albero prosegue x = -search(-beta, -alpha, depth - 1); //Quando si “riemerge” dalla ricorsione, si ritorna //alla posizione precedente take_back(); //Se c’è un cutoff... if (x > alpha) { //...history heuristics: la casella corrispondente a //questa mossa viene incrementata, così si cercherà //prima nei rami contenenti questa mossa in futuro //(vedi analisi) history[(int)generData[i].m.b.from] [(int)generData[i].m.b.to] += depth; if (x >= beta) { //in questo caso non vi saranno risultati //migliori (pruning). return beta; } alpha = x; //si aggiorna la variazione principale pv[plyCurrent][plyCurrent] = generData[i].m; for(j=plyCurrent+1;j<pvLength[plyCurrent+1];++j) pv[plyCurrent][j] = pv[plyCurrent + 1][j]; pvLength[plyCurrent] = pvLength[plyCurrent + 1]; } } //se non si sono trovate mosse legali, o si ha perso o si è //in stallo,e quindi parità (nella posizione studiata). if (!foundAnyMove) { if (areWeInCheck) return -10000 + plyCurrent; else return 0; } 70
  • 71. //regola delle 50 mosse (100 plies) if (fiftMoves >= 100) return 0; return alpha; } quiesce è quasi identica a search. La differenza sta nel fatto che essa considera soltanto le mosse di cattura, e cerca fino a che non ve ne sono più. In questo modo ci si porterà in situazioni il più statiche possibile, dove la funzione di valutazione darà il massimo risultato. int quiesce(int alpha,int beta) { /* --- INIZIALIZZAZIONE E STESSI CONTROLLI DI SEARCH --- */ //Qui si valuta subito la posizione (più probabilità di //pruning trattandosi di mosse più drastiche). x = evaluate(); if (x >= beta) return beta; if (x > alpha) alpha = x; //Si generano le mosse di cattura generateCaptures(); if (followPV) sort_pv(); for (i=firstMove[plyCurrent];i<firstMove[plyCurrent+1];++i) { /*--- L’IMPLEMENTAZIONE DI ALPHA-BETA PRUNING E’ ANALOGA A QUELLA DELLA FUNZIONE search ---*/ } return alpha; } eval.c eval.c contiene le funzioni e le strutture dati che servono per valutare il punteggio di una posizione. /* contiene i valori dei pezzi, come definiti in architettura (vedi) */ int piece_value[7] = { 100, 220, 780, 510, 380, 1280, 0 }; gli array che terminano in –pcsq rappresentano mappe di valori che vanno sommati ai valori dei mezzi a seconda della loro posizione sulla scacchiera. Questi valori al momento sono fissati arbitrariamente, in base alla mobilità conseguita dalla posizione; in futuro potrebbe essere interessante tentare di trovare valori migliori tramite un approccio di tipo machine-learning. 71
  • 72. /* flip serve a calcolare il valore pezzo/casella per il nero. Se il valore di cavallo bianco è knight_pcsq[sq], l’equivalente per il nero è knight_pcsq[flip[sq]] */ int flip[125] = { 120, 121, 122, 123, 124, 115, 116, 117, 118, 119, 110, 111, 112, 113, 114, 105, 106, 107, 108, 109, 100, 101, 102, 103, 104, 95, 96, 97, 98, 99, 90, 91, 92, 93, 94, 85, 86, 87, 88, 89, 80, 81, 82, 83, 84, 75, 76, 77, 78, 79, 70, 71, 72, 73, 74, 65, 66, 67, 68, 69, 60, 61, 62, 63, 64, 55, 56, 57, 58, 59, 50, 51, 52, 53, 54, 45, 46, 47, 48, 49, 40, 41, 42, 43, 44, 35, 36, 37, 38, 39, 30, 31, 32, 33, 34, 25, 26, 27, 28, 29, 20, 21, 22, 23, 24, 15, 16, 17, 18, 19, 10, 11, 12, 13, 14, 5, 6, 7, 8, 9, 0, 1, 2, 3, 4, }; evaluate calcola il valore della posizione corrente. int evaluate() { /* --- INIZIALIZZAZIONI --- */ // si passa per tutta la scacchiera for (i = 0; i < 125; ++i) { if (colors[i] == EMPTY) continue; if (pieces[i] == PAWN) { pawn_mat[colors[i]] += piece_value[PAWN]; } else piece_mat[colors[i]] += piece_value[pieces[i]]; } /* alla fine del ciclo, pawn_mat conterrà il valore base di tutti i pedoni neri e di tutti i pedoni bianchi*/ 72
  • 73. /* piece_mat conterrà il valore base degli altri pezzi. Si mantengono separati i punteggi per lasciare spazio ad eventuali sviluppi futuri in grado di tener conto della struttura dei pedoni, come già si fa negli scacchi classici */ score[WHITE] = piece_mat[WHITE] + pawn_mat[WHITE]; score[BLACK] = piece_mat[BLACK] + pawn_mat[BLACK]; /* nel secondo ciclo vengono applicate le mappe di punteggio -pcsq */ for (i = 0; i < 125; ++i) { if (colors[i] == EMPTY) continue; if (colors[i] == WHITE) { switch (pieces[i]) { case PAWN: score[WHITE] += eval_WHITE_pawn(i); break; case KNIGHT: score[WHITE] += knight_pcsq[i]; break; case BISHOP: score[WHITE] += bishop_pcsq[i]; break; case ROOK: break; case KING: if (piece_mat[BLACK] <= 1200) score[WHITE] += king_endgame_pcsq[i]; //evaluate particolare per re in endgame. Il valore 1200 è del tutto empirico, per sviluppi futuri sarebbe opportuno determinare un modo migliore per definire l’endgame. else score[WHITE] += eval_WHITE_king(i); //evaluate normale per il re bianco break; } } else { /* --- CODICE SIMILE PER IL GIOCATORE NERO--- */ } } } /* si prepara il valore da passare all’alpha-beta pruning */ if (sideToMove == WHITE) return score[WHITE] - score[BLACK]; //else 73
  • 74. return score[BLACK] - score[WHITE]; } int eval_WHITE_pawn(int sq) { int r; r = 0; r += pawn_pcsq[sq]; return r; } int eval_WHITE_king(int sq) { int r; r = king_pcsq[sq]; r *= piece_mat[BLACK]; //altro valore empirico. Futuri sviluppi possono migliorarne //la scelta. r /= 3100; return r; } RaumschachChessEngine.cpp e altri Questo è il file dove sono dichiarate le funzioni che permettono all’interfaccia di interrogare il Motore e interagire con esso. E’ un file C++ perché nell’implementazione Windows del gioco è stato necessario un wrapping del motore in una DLL C++. A questi si aggiungono main.c, utilizzato per le operazioni di debugging. dllmain.cpp e stdafx.cpp legati al wrapping del Motore in una DLL. 74
  • 75. Realizzazione dei modelli 3D Come da progettazione sono stati realizzati con Blender otto modelli 3D, uno per ogni pezzo degli scacchi più un parallelepipedo che formerà la casella fondamentale per la realizzazione della scacchiera. Per esigenze di compatibilità tra Blender e XNA sono inclusi nel progetto in formato FBX, formato proprietario di Autodesk. Fig. 14 - Modello della regina in costruzione, in Blender 75
  • 76. Fig. 15 - Il modello della regina completo in Blender 76
  • 77. Realizzazione dell’Interfaccia Premessa: XNA Application Model La classe base su cui si basa l’application model fornito da XNA è Game. Essa fornisce un framework per far girare un “gioco”, o simulazione, basandosi su un intervallo di tempo fisso o variabile. Perciò, per creare un gioco si deriva dalla classe Game ereditando obbligatoriamente i metodi Update, Draw e Initialize. Update è demandato all’aggiornamento della logica del gioco, Draw al disegno di ciascun frame e Initialize all’inizializzazione del gioco. Game Loop Timing Un Game può essere o a intervalli di tempo fissi o variabili; per default, fissi. A seconda del tipo di intervallo varierà quanto spesso è richiamato Update. Game Loop ad intervallo fisso Un Game ad intervallo fisso chiama il suo Update ogni intervallo di tempo dalla durata specificat in TargetElapsedTime. Finito di girare Update, Game chiama Draw; una volta esaurito Draw, se ancora non è ora di chiamare di nuovo Update, aspetta. Se Update eccede il tempo concessogli, Game assegna il valore true a IsRunningSlowly e richima di nuovo Update senza Draw. Ovvero, “perde frames”, ma in questo modo la simulazione rimane aggiornata nello stato interno, anche se risulta bloccata dal punto di vista dell’utente. Game Loop ad intervallo variabile In questo caso Update e Draw vengono chiamati a ciclo continuo, migliorando le prestazioni ma lasciando al programmatore tutti gli oneri della temporizzazione. Nel caso preso in esame l’intervallo fisso è stato scelto come il più indicato, non essendovi nella nostra simulazione operazioni di grafica molto complesse. Game Component GameComponent e DrawableGameComponent sono rispettivamente le due classi dalle quali derivare per costruire moduli per il proprio Game. Aggiungendoli ad esso con Game.add(...) i loro metodi Draw, Update ed Initialize saranno chiamati automaticamente. 77
  • 78. Questa feature è stata utilizzata solo in parte, in quanto limitativa per la costruzione dei moduli. Content Pipeline XNA permette di modificare parti della pipeline che esso utilizza per il caricamento degli asset di gioco. Fig. 16 - Schema della Content Pipeline di XNA E’ composta da quattro componenti principali: 1. Importer Tra gli importer standard per modelli 3D vi è anche quello per i file Autodesk .fbx, l’unico formato compatibile tra Blender ed XNA, appunto. Il tipo di input e di output varia a seconda dell’importer che si usa. 2. Content Processor Anche qui, vi sono processori diversi per ciascun tipo di oggetto; durante la realizzazione di questo progetto si è deciso utilizzare un processore custom di terze parti, per poter implementare la selezione dei modelli tramite click del mouse (vedi) 3. Content Compiler Trasforma gli oggetti in formato binario. Li compila, appunto 4. Content Loader Carica i compilati nella memoria del gioco in modo che possano essere utilizzati. 78
  • 79. Librerie di terze parti TrianglePickingProcessor E’ un ContentProcessor custom, inserisce informazioni sul posizionamento dei vertici, che può essere utilizzato a runtime per implementare la selezione di un particolare triangolo su questo modello; è l’unico modo di ottenere la selezionabilità del modello tramite mouse. Questa classe eredita dal processore standard ModelProcessor; di esso estende il metodo Process, utilizzandolo per inserire dati custom nella proprietà Tag del modello. XWinForms E’ una library open source, ancora in beta, che fornisce un’interfaccia grafica simile a WinForms, anche se molto più rudimentale, utilizzabile all’interno di XNA. Su di essa si sono anche operate delle piccole modifiche e delle correzioni di bug. ScreenManager Per la realizzazione dell’interfaccia, è stato esteso il framework XNA con un componente denominato ScreenManager. L’obiettivo è impostare la programmabilità dell’Interfaccia a schermate. Lo ScreenManager gestisce lo stato del gioco, la visualizzazione dei menu, e la visualizzazione in generale. E’ un framework a sua volta perché permette di strutturare la simulazione che si vuol costruire per XNA in schermate, cosa che XNA da solo non permette. Ciò risulta molto comodo sopratutto per la costruzione dei menù. GameScreen Da questa classe si può derivare per costruire una schermata che viene visualizzata sullo schermo. Supporta effetti di transizione (transition-on e transition-off) e una modalità pop-up. MenuScreen Deriva da GameScreen. Visualizza una serie di MenuEntry tramite le quali è possibile navigare all’interno dei menu e fornisce la logica per gestirle. Derivando da essa si costruisce una schermata di menu. In figura 17 è rappresentata dai rettangoli azzurri. 79
  • 80. Fig. 17 - Screen Flow schema dell'Interfaccia LoadingScreen Deriva da MenuScreen. Il suo scopo è coordinare le transizioni tra il sistema di schermate di menù ed il gioco vero e proprio, perché per transizioni molto grandi ci potrebbe essere bisogno di tempo per caricare i dati. Perciò viene visualizzato un LoadingScreen nel frattempo; ciò avviene nel seguente modo: • Si fa in modo che tutte le schermate esistenti eseguano un transition- off. • Si attiva uno screen di loading, che nello stesso istante eseguirà un transition-on 80
  • 81. • Quando gli schermi che devono sparire completano il transition-off, il loading screen attiva lo schermo successivo (quello che potrebbe necessitare di un certo quantitativo di tempo per essere attivato) GameplayScreen E’ la schermata di gioco vera e propria ed è il risultato del lavoro svolto e descritto in questo documento. E’ trattata nella sua interezza nella sezione Gameplay. Gameplay Il “gameplay” è reso tramite la classe GameplayScreen, che rappresenta la schermata di gioco, tramite le videocamere virtuali che rendono possibile l’illusione del movimento nello spazio tridimensionale simulato, e tramite la classe PositionableModel e le derivate da essa. Videocamere Virtuali Le videocamere virtuali sono state organizzate in modo da essere accessibili da qualunque punto del codice. Sono organizzate nella seguente gerarchia: • BaseCamera: classe astratta, definisce le matrici view e projection, utilizzate per la proiezione dello spazio 3D sul campo visivo, e i metodi base. o Cubical Camera: classe che definisce la camera a movimento legato alla scacchiera (vedi progettazione) o Free Movement Camera: classe che definisce la camera a movimento libero (vedi progettazione) Di seguito il codice di queste tre classi: 81
  • 82. BaseCamera using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework; namespace Raumschach_Chess { public abstract class BaseCamera { protected static BaseCamera activeCamera = null; // View and projection protected Matrix projection = Matrix.Identity; protected Matrix view = Matrix.Identity; // protected const float angle = 0.5f; protected float turnSpeed; protected Game Game; public BaseCamera(Game game) { if (ActiveCamera == null) ActiveCamera = this; this.Game = game; } public BaseCamera(float turnspeed, Game game):this(game) { this.turnSpeed = turnspeed; } public BaseCamera(float turnspeed, Game game, Vector3 initialPosition) : this(turnspeed,game) { view *= Matrix.CreateTranslation(initialPosition); } public BaseCamera(float turnspeed, Game game, Vector3 initialPosition, Vector3 initialTarget) : this(turnspeed,game) { view *= Matrix.CreateLookAt(initialPosition, initialTarget, Vector3.UnitY); } public static BaseCamera ActiveCamera { get { return activeCamera; } set { activeCamera = value; } } public Matrix Projection { get { return projection; } } public Matrix View 82
  • 83. { get { return view; } } public virtual void Update(GameTime gameTime){} public virtual void LoadContent() { float ratio = (float)this.Game.GraphicsDevice.Viewport.Width / (float)this.Game.GraphicsDevice.Viewport.Height; projection = Matrix.CreatePerspectiveFieldOfView(MathHelper.P iOver4, ratio, 10, 10000); } } } 83
  • 84. CubicalCamera using System; using System.Collections.Generic; using System.Linq; using System.Text; using Microsoft.Xna.Framework.Input; using Microsoft.Xna.Framework; namespace Raumschach_Chess { public class CubicalCamera : BaseCamera { public int CubeEdges { get{ return cubeEdges;} } private int floor = 0; public int Floor { get { return floor; } set { int reference = CubeEdges; if (value <= -1) value = reference + value % reference; floor = value % reference; } } private int horizontalPosition = 0; public int HorizontalPosition { get { return horizontalPosition; } set { int reference = CubeEdges * 4; if (value <= -1) value = reference + value % reference; horizontalPosition = value % reference; } } public float step; private int cubeEdges; private Vector3 defaultPosition; private Vector3 finalTargetPosition; private Vector3 initialTargetPosition; private bool isMoving = false; private TimeSpan initialMovementTime; private float secondsToDoMovement = 1; public Vector3 finalPosition; private Vector3 initialPosition; public CubicalCamera(float turnspeed, Game game, int cubeEdges) :base(turnspeed, game) { this.cubeEdges = cubeEdges; 84
  • 85. } public override void Update(GameTime gameTime) { float delta = (float)gameTime.ElapsedGameTime.TotalSeconds; KeyboardState keyboard = Keyboard.GetState(); if (keyboard.IsKeyDown(Keys.Escape)) Game.Exit(); float deltaPitch = 0; float deltaYaw = 0; //se non ci si sta muovendo si inizia una fase di movimento if (!this.isMoving) { if (keyboard.IsKeyDown(Keys.W)) deltaPitch -= angle * turnSpeed * delta; if (keyboard.IsKeyDown(Keys.S)) deltaPitch += angle * turnSpeed * delta; if (keyboard.IsKeyDown(Keys.A)) deltaYaw -= angle * turnSpeed * delta; if (keyboard.IsKeyDown(Keys.D)) deltaYaw += angle * turnSpeed * delta; if (keyboard.IsKeyDown(Keys.Up)) { Floor++; StartMovement(); initialMovementTime = gameTime.TotalGameTime; } if (keyboard.IsKeyDown(Keys.Down)) { Floor--; StartMovement(); initialMovementTime = gameTime.TotalGameTime; } if (keyboard.IsKeyDown(Keys.Left)) { HorizontalPosition--; StartMovement(); initialMovementTime = gameTime.TotalGameTime; } if (keyboard.IsKeyDown(Keys.Right)) { HorizontalPosition++; StartMovement(); initialMovementTime = gameTime.TotalGameTime; } } else {//altrimenti si continua il movimento precedente, passo passo attorno al cubo float amount = (float)(gameTime.TotalGameTime.TotalSeconds - initialMovementTime.TotalSeconds) / secondsToDoMovement; if (amount >= 1) { view = Matrix.CreateLookAt(finalPosition, finalTargetPosition, Vector3.Up); this.isMoving = false; } else view = Matrix.CreateLookAt( 85
  • 86. initialPosition + (finalPosition – initialPosition) * amount, initialTargetPosition + (finalTargetPosition – initialTargetPosition) * amount, Vector3.Up); } //aggiungo il contributo della rotazione su sé stessa della camera Matrix Rotx = Matrix.CreateRotationX(MathHelper.ToRadians(deltaPitch)); Matrix Roty = Matrix.CreateRotationY(MathHelper.ToRadians(deltaYaw)); view *= Rotx * Roty; } public override void LoadContent() { step = ((Raumschach)Game).OptionsCurrent.SquareSize; defaultPosition = ((Raumschach)Game).OptionsCurrent.LeftBottom +new Vector3(- step, step / 2, -step); finalPosition = defaultPosition; view = Matrix.CreateLookAt(defaultPosition, new Vector3(- 2*defaultPosition.X,0,-2*defaultPosition.Z) , Vector3.Up); HorizontalPosition--; StartMovement(); initialMovementTime = TimeSpan.Zero; base.LoadContent(); } private void StartMovement() { float x, y, z; float xTarget, zTarget; y = floor; if (HorizontalPosition < cubeEdges) { z = zTarget = horizontalPosition + 1f; x = -2; xTarget = x + 100; } else if ((HorizontalPosition >= cubeEdges) && (HorizontalPosition < 2 * cubeEdges)) { z = cubeEdges + 3.5f; x = xTarget = horizontalPosition - 4f; zTarget = z - 100; } else if ((HorizontalPosition >= 2 * cubeEdges) && (HorizontalPosition < 3 * cubeEdges)) { z = zTarget = 3 * cubeEdges - horizontalPosition ; x = cubeEdges + 3.5f; xTarget = x - 100; } else { z = -2; 86
  • 87. x = xTarget = 4 * cubeEdges - horizontalPosition ; zTarget = z + 100; } initialPosition = finalPosition; finalPosition = new Vector3(step * x, step * y, step * z) + defaultPosition; initialTargetPosition = finalTargetPosition; finalTargetPosition = new Vector3(step * xTarget, step * y, step * zTarget) + defaultPosition; isMoving = true; } } } 87
  • 88. FreeMovementCamera using System; using System.Collections.Generic; using System.Text; using Microsoft.Xna.Framework; using Microsoft.Xna.Framework.Input; namespace Raumschach_Chess { public class FreeMovementCamera : BaseCamera { protected float speed; public FreeMovementCamera(Game game):base(game) { } public FreeMovementCamera(float speed, float turnspeed, Game game):base(turnspeed, game) { this.speed = speed; } public FreeMovementCamera(float speed, float turnspeed, Game game, Vector3 initialPosition) : base(turnspeed,game,initialPosition) { this.speed = speed; } public FreeMovementCamera(float speed, float turnspeed, Game game, Vector3 initialPosition, Vector3 initialTarget) : base(turnspeed, game, initialPosition,initialTarget) { this.speed = speed; } public override void Update(GameTime gameTime) { float delta = (float)gameTime.ElapsedGameTime.TotalSeconds; KeyboardState keyboard = Keyboard.GetState(); float deltaPitch = 0; float deltaYaw = 0; float deltaRoll = 0; float distance = 0; if (keyboard.IsKeyDown(Keys.Up)) deltaPitch -= angle * turnSpeed * delta; if (keyboard.IsKeyDown(Keys.Down)) deltaPitch += angle * turnSpeed * delta; if (keyboard.IsKeyDown(Keys.Left)) deltaYaw -= angle * turnSpeed * delta; if (keyboard.IsKeyDown(Keys.Right)) deltaYaw += angle * turnSpeed * delta; 88
  • 89. if (keyboard.IsKeyDown(Keys.Q)) deltaRoll -= angle * turnSpeed * delta; if (keyboard.IsKeyDown(Keys.W)) deltaRoll += angle * turnSpeed * delta; if (keyboard.IsKeyDown(Keys.A)) distance += speed * delta; if (keyboard.IsKeyDown(Keys.Z)) distance -= speed * delta; if (keyboard.IsKeyDown(Keys.Escape)) Game.Exit(); Matrix Rotx = Matrix.CreateRotationX(MathHelper.ToRadians(deltaPitch)); Matrix Roty = Matrix.CreateRotationY(MathHelper.ToRadians(deltaYaw)); Matrix Rotz = Matrix.CreateRotationZ(MathHelper.ToRadians(deltaRoll)); view *= Rotx * Roty * Rotz; Matrix matrisce = Matrix.CreateTranslation(new Vector3(2, 3, 4) * 1000); view *= Matrix.CreateTranslation(Vector3.UnitZ * distance); } } } 89
  • 90. Gestione dei modelli PositionableModel Seguendo l’Application Model di XNA è stato fatto sì che ogni modello caricato, sia esso pezzo degli scacchi o parte della scacchiera, contenta in sé tutta la logica che lo riguarda. E’ stata scritta perciò una classe base, PositionableModel, nella quale si è centralizzata tutta la logica base per il posizionamento del modello nello spazio tridimensionale e per la configurazione comune degli Effects predefiniti di XNA. public class PositionableModel { /* --- INIZIALIZZAZIONE E COSTRUTTORI --- */ public virtual void Draw(GameTime gameTime) { Matrix[] transforms = new Matrix[model.Bones.Count]; model.CopyAbsoluteBoneTransformsTo(transforms); foreach (ModelMesh mesh in Model.Meshes) { Matrix world = transforms[mesh.ParentBone.Index] * WorldTransform; foreach (BasicEffect eff in mesh.Effects) { eff.World = world; //I dati vengono raccolti dalla videocamera virtuale //al momento attiva eff.Projection = BaseCamera.ActiveCamera.Projection; eff.View = BaseCamera.ActiveCamera.View; SetBasicEffectSettings(eff); } mesh.Draw(); } } /* --- Configurazione degli effects standard --- */ } Square Questa classe rappresenta i mattoncini fondamentali componenti la scacchiera. Anch’essa deriva da PositionableModel. 90
  • 91. Piece Questa classe deriva da PositionableModel. Tutta la parte dedicata al disegno non cambia, infatti il metodo Draw rimane lo stesso. L’animazione del pezzo è ottenuta tramite il metodo Update. La posizione finale del pezzo è determinata dalla matrice FinalWorldTransform. Quando l’animazione inizia, viene settato il punto di destinazione da raggiungere, e, fissato come tempo per l’animazione 2 sec, si somma alla matrice di posizione corrente una frazione della matrice differenza fra posizione finale e posizione iniziale. Di seguito è riportato il codice: public override void Update(GameTime gameTime) { if (!animating) { animDiff = FinalWorldTransform - wTrans; if (animDiff != zeroMatrix) { animating = true; animStartTime = gameTime.TotalGameTime; wTransAtAnimStartTime = wTrans; } } else { double msecsElaps = gameTime.TotalGameTime.TotalMilliseconds – animStartTime.TotalMilliseconds; if (msecsElaps >= 2000) { animating = false; wTrans = FinalWorldTransform; } else { wTrans = wTransAtAnimStartTime + ((int)msecsElaps) * (animDiff / 2000); } } base.Update(gameTime); } 91
  • 92. Logiche di gioco ChessboardLogics Da essa l’interfaccia richiama le funzioni del Motore, implementato nel file RaumschachChessEngine.dll, tramite Platform Invoke. Di seguito un esempio di metodo wrapper: //nome della dll da importare [System.Runtime.InteropServices.DllImport("RaumschachChessEngine.dll")] //l’attributo MarshalAs indica, in questo caso, come il CLR deve //interpretare il dato che gli giunge dalla dll [return: System.Runtime.InteropServices.MarshalAs( System.Runtime.InteropServices.UnmanagedType.LPStr)] public static extern String MakeMoveAndGetNext( //in quest’altro caso invece il MarshalAs indica come inviare un //parametro alla dll [System.Runtime.InteropServices.MarshalAs( System.Runtime.InteropServices.UnmanagedType.LPStr)] String move, int sideToMove ); 92
  • 93. Conclusioni Raggiungimento degli obiettivi In ultima analisi, si può quindi affermare che gli obiettivi principali di questa tesi sono stati raggiunti. L’Interfaccia possiede le caratteristiche di usabilità richieste, risulta adattabile a diversi stili di visualizzazione da parte dell’utente. Il Motore risponde in modo coerente alle mosse dell’avversario. La regolabilità nel senso del tempo disponibile per una mossa e nella profondità massima di analisi è stata ottenuta e resa facilmente applicabile. Sviluppi futuri E’ chiaro che gli sviluppi possibili sono innumerevoli. Dal lato grafico: • Personalizzazione dei pezzi: costruzione di modelli 3D diversi, magari “a tema” • Texturizzazione di pezzi e scacchiera • Applicazione di shader più complessi: è stato utilizzato quello base offerto da XNA ma nulla vieta di elaborarne di più complessi. Dal lato tecnico: • Potenziamento dell’intelligenza artificiale, in particolar modo della funzione di valutazione della mossa, magari tramite Genetic Programming o altri modelli di apprendimento computerizzato • Maggior utilizzo della library xWinForms (ancora in fase di beta testing) per ottenere un’interfaccia ancora più user-friendly. 93
  • 94. Bibliografia Letteratura • “How Computers Play Chess” di David N. L. Levy e Monroe Newborn • “XNA 2.0 Game Programming Recipes: A Problem-Solution Approach” di Riemer Grootjans Web Wikipedia http://www.wikipedia.org Chess programming • Chess programming – getting started http://www.gamedev.net/reference/programming/features/chess1/ • Computer chess programming theory http://www.frayn.net/beowulf/theory.html • Computer chess – Top 5000 http://www.top-5000.nl/chess.htm • Chess programming wiki http://chessprogramming.wikispaces.com/ • Computer chess information and resources http://chessprogramming.wikispaces.com/ XNA programming • XNA Creators Club Online http://creators.xna.com/en-US/ • XNA Developer Center http://msdn.microsoft.com/en-us/aa937791.aspx • XNA Community Forums http://forums.xna.com/forums/ 94
  • 95. Third Part Libraries • XNA Framework GameEngine Development http://roecode.wordpress.com/2008/01/28/xna-framework- gameengine-development-part-7- screenmanagergamecomponent/ • GameProjects.com http://www.gameprojects.com/project/?id=e68f3464c4 3D modeling • Blender 3D – Noob To Pro http://en.wikibooks.org/wiki/Blender_3D:_Noob_to_Pro 95
  • 96. Ringraziamenti Porgo un sentito ringraziamento ai miei familiari, ai miei amici (musicisti, ingegneri e non), ed a tutti coloro che mi hanno supportato e sopportato durante questo cammino. Un grazie particolare all’intero corpo docente che si prodiga in questo delicato compito che è l’insegnamento di una materia delicata ed in continua evoluzione come l’ingegneria informatica. In particolare ai docenti Bartoli e Fermeglia, entrambi a mio modesto parere fra i migliori del corpo docente del mio corso. E grazie anche a te che stai leggendo. 96
  • 97. Appendice Fast, Minimum Storage Ray/Triangle Intersection 97
  • 98. 98
  • 99. 99
  • 100. 100
  • 101. 101
  • 102. 102