Metodologie di Programmazione: Progetto
Il progetto richiede di realizzare un'applicazione con un'interfaccia utente grafica (GUI) che permette di giocare ai giochi del framework (quello degli homework) sia con i giocatori del framework che con giocatori umani.
La funzionalità principale che una GUI per i giochi del framework deve offrire è permettere ad un utente di scegliere ed eseguire le mosse di un gioco. Ovviamente la scelta di una mossa deve essere gestita rispettando le regole del gioco e tramite una UI facile ed intuitiva. Inoltre, nel caso del nostro framework la programmazione della GUI può e deve essere indipendente dalle specificità dei vari giochi. In altre parole la gestione della scelta delle mosse, e di ogni altro aspetto di un gioco, non deve richiedere la conoscenza delle caratteristiche specifiche del gioco ma deve basarsi solamente sulle informazioni fornite dal framework. Uno dei vantaggi di questo approccio, ma non è il solo, sta nella facilità con cui si può introdurre un nuovo gioco perché è sufficiente definire la relativa GameFactory
e si avrà automaticamente anche la GUI per giocare con il nuovo gioco.
Quindi per gestire la scelta della mossa da parte dell'utente la programmazione della GUI deve basarsi essenzialmente sull'insieme delle mosse valide, ritornato dal metodo validMoves()
di GameRuler
, e ovviamente sulle informazioni relative alla board, fornite da getBoard()
. Per assistere l'utente durante la scelta della mossa, sopratutto quando tra le mosse valide ci sono mosse costituite da sequenze complesse di azioni, la GUI deve poter distinguere tra quelle azioni che possono essere decise dall'utente e quelle che seguono obbligatoriamente secondo le regole del gioco. Ad esempio, in Othello l'utente giocatore può scegliere la posizione (purché sia valida) dove mettere la propria pedina, azione ADD
, ma poi il conseguente rovesciamento delle pedine dell'avversario, azione SWAP
, è obbligatoria e non può essere decisa dall'utente. Nella Dama possono presentarsi situazioni molto più complesse in cui alcune sequenze di catture di pedine, azioni JUMP
seguite da REMOVE
, sono obbligatorie e altre possono essere scelte tra diverse alternative. Come già detto la programmazione della GUI non può fare questa distinzione in base alla "conoscenza" dello specifico gioco perché non "conosce" le regole di Othello o della Dama. Fortunatamente (per modo di dire, la fortuna qui non c'entra nulla) l'insieme stesso delle mosse valide, nel suo complesso, contiene le informazioni sufficienti per fare tale distinzione. Infatti, come spiegato di seguito, basta organizzare l'insieme delle mosse valide in una opportuna struttura ad albero.
L'albero delle mosse dà una struttura ad albero all'insieme delle mosse valide di tipo ACTION
(quindi non contempla la mossa RESIGN
e l'eventuale mossa PASS
). L'albero è rappresentato e gestito dagli oggetti che implementano l'interfaccia MoveChooser
della classe PlayerGUI
nel package gapp.ulg.game.util
. Prima di descriverlo dobbiamo introdurre alcune nozioni. Per prefisso di una sequenza di azioni, e in particolare di una mossa, intendiamo una qualsiasi sotto-sequenza di azioni consecutive della sequenza che iniziano con la prima azione della sequenza. Prefissi particolari sono
quello vuoto, che è comune a tutte le sequenze, e quello totale che comprende l'intera sequenza o mossa. Diciamo che un prefisso $$$P$$$ è decisionale (rispetto all'insieme di mosse valide) se è o totale o esistono almeno due mosse valide $$$M_1$$$ e $$$M_2$$$ tali che1
$$M_1 = P\cdot A_1\cdot Q_1 \quad \mbox{e} \quad M_2 = P\cdot A_2 \cdot Q_2$$
dove $$$A_1$$$ e $$$A_2$$$ sono azioni differenti e $$$Q_1$$$ ed $$$Q_2$$$ sono sequenze di azioni qualsiasi possibilmente vuote. Ad esempio, in Othello il prefisso vuoto è decisionale solo se ci sono almeno due mosse valide, mentre un prefisso che consiste in una azione ADD
non è decisionale perché non è totale e è incluso in al più una sola mossa valida. Nella Dama se una pedina può catturare, il prefisso corrispondente all'azione JUMP
del catturare non è decisionale perché è seguito da un unica azione REMOVE
(che rimuove la pedina catturata). In sostanza un prefisso non totale $$$P$$$ è decisionale solo se avendo eseguito le azioni di una mossa fino a $$$P$$$ ci sono almeno due azioni successive diverse che possono continuare la mossa in modo valido.
I nodi dell'albero delle mosse corrispondono ai prefissi decisionali delle mosse valide. La radice dell'albero è il prefisso decisionale più corto2 tra tutti i prefissi decisionali delle
mosse valide. Un nodo relativo a un prefisso totale è detto finale (metodo isFinal
di MoveChooser
). Tipicamente un nodo finale non ha figli3, quindi è una foglia dell'albero, e ogni nodo foglia è sempre un nodo finale. I figli di un nodo di prefisso $$$P$$$, sono tutti i prefissi decisionali $$$P\cdot Q$$$ tali che ogni prefisso $$$P\cdot R$$$, con $$$R$$$ prefisso proprio non vuoto di $$$Q$$$, non è decisionale.
Quindi i nodi dell'albero delle mosse corrispondono esattamente ai punti in cui un giocatore (umano) deve prendere una decisione. Se il prefisso è totale, deve decidere se fare o meno la mossa e se non è totale, deve decidere tra almeno due alternative di continuazione della mossa che sta considerando.
Per sotto-mossa di un nodo intendiamo la differenza tra il prefisso del nodo e quello del suo nodo genitore, cioè se $$$P$$$ è il prefisso del nodo e $$$Q$$$ quello del genitore, la sotto-mossa del nodo è la sotto-sequenza di azioni $$$D$$$ tale che $$$P = Q \cdot D$$$. La sotto-mossa della radice è il suo prefisso, si vedano i metodi subMove
e childrenSubMoves
di MoveChooser
. La figura sottostante mostra un esempio di situazione di gioco (assai improbabile ma possibile della Dama) con l'insieme delle mosse valide e il relativo albero delle mosse.
Per ogni nodo è mostrato il relativo prefisso e le sotto-mosse dei nodi sono evidenziate in rosso (eccetto quella della radice).
Vediamo ora in che modo l'albero delle mosse aiuta a scegliere una mossa valida (di tipo ACTION
) tramite una GUI. In generale per scegliere una mossa bisogna prendere una o più decisioni in sequenza. Ad esempio, in Othello bisogna decidere in quale posizione (tra quelle valide) mettere la pedina, nella Dama si potrebbe dover prendere più decisioni se ci sono più sequenze di catture valide (ad es. la figura sopra), in Connect6 si deve decidere in quali due posizioni mettere (tra quelle non occupate) due pedine. Queste decisioni sono rappresentate proprio dai prefissi decisionali delle mosse valide e quindi dai nodi dell'albero delle mosse. La decisione relativa a un nodo foglia, quindi un nodo finale, è di eseguire la mossa corrispondente o tornare sui propri passi e sceglierne un'altra. La decisione relativa a un nodo non finale consiste nel decidere uno dei suoi nodi figli. Siccome le prime azioni delle sotto-mosse dei nodi figli sono tutte diverse4, la decisione può basarsi sulla scelta della prima azione. Basta quindi considerare il modo di scegliere un'azione. In una GUI si può permettere all'utente di selezionare una o più posizioni della board (ad es. tramite click del mouse) e questo insieme di posizioni a sua volta seleziona una o più azioni (le prime azioni di sotto-mosse) che sono compatibili con tale insieme. Più precisamente, sia $$$S$$$ un qualsiasi insieme non vuoto di posizioni, introduciamo le seguenti definizioni:
Action
) se $$$S$$$ è uguale all'insieme delle posizioni di $$$A$$$, cioè all'insieme delle posizioni nel suo campo pos
(quindi indipendentemente dall'ordine), eccetto se $$$A$$$ è di tipo JUMP
, nel qual caso $$$S$$$ deve essere uguale all'insieme che consiste solamente nella prima posizione di pos
.La quasi-selezione è utile per guidare l'utente quando occorrono più posizioni per selezionare un'azione (ad es. un movimento di pedine nel gioco Abalone. La selezione dei nodi è gestita dai metodi select
, quasiSelected
e clearSelection
di MoveChooser
.
La selezione delle azioni e dei corrispondenti nodi, può non essere sufficiente a scegliere una singola mossa o continuazione di mossa. In Othello è sempre sufficiente perché seleziona l'unica mossa che inizia con l'azione ADD
della posizione selezionata. Ma in molti altri giochi non è sufficiente. Ad esempio nella Dama, negli Scacchi, in Abalone e in tanti altri giochi uno o più pezzi possono muoversi, azioni MOVE
e JUMP
, in più direzioni e ci possono essere quindi più azioni che sono selezionate dallo stesso insieme di posizioni. In casi come questi per completare la scelta rendendola univoca occorre che il giocatore scelga anche un'altra posizione (quella d'arrivo di una JUMP
) o una linea di movimento (azioni MOVE
). Queste scelte sono gestite tramite i metodi jumpSelection
e moveSelection
di MoveChooser
. In altri casi come nella variante Chameleon del gioco Hex un giocatore può aggiungere in una posizione (azione ADD
) pezzi diversi o negli Scacchi quando un pedone arriva alla riga opposta è promosso, cioè sostituito (azione SWAP
), con un pezzo a scelta del giocatore. In questi casi il giocatore deve scegliere il pezzo da aggiungere o da sostituire e questa scelta è gestita dai metodi selectionPieces
e doSelection
di MoveChooser
che gestiscono anche il caso di un'azione REMOVE
.
La selezione dei nodi è effettuata con il metodo select
relativamente ai nodi figli del nodo corrente dell'albero delle mosse. Il MoveChooser
mantiene un nodo corrente che inizialmente è la radice dell'albero delle mosse. Tramite i metodi doSelection
, jumpSelection
e moveSelection
, e la selezione corrente, è possibile navigare l'albero spostando il nodo corrente verso le foglie ed eventualmente riportandolo indietro con il metodo back
. Il metodo back
ritorna la sotto-mossa inversa della sotto-mossa del precedente nodo corrente ed è definita come segue. Sia $$$A_1\cdot A_2\cdots A_k$$$ una sequenza di $$$k$$$ azioni, la sequenza inversa è la sequenza $$$S_k\cdot S_{k-1}\cdots S_1$$$ tale che ogni $$$S_i$$$ è la sequenza di azioni che inverte l'azione $$$A_i$$$ ed è così definita:
ADD
, con posizione $$$p$$$ e pezzo $$$pm$$$, allora $$$S_i$$$ è la singola azione REMOVE
con posizione $$$p$$$.REMOVE
, con posizioni $$$(p_1,p_2,\ldots,p_n)$$$ e corrispondenti pezzi rimossi $$$(pm_1,pm_2,\ldots,pm_n)$$$, allora $$$S_i$$$ è la sequenza di azioni {ADD
, $$$(p_1,pm_1)$$$}$$$\cdots$$${ADD
, $$$(p_2,pm_2)$$$}$$$\cdots$$${ADD
, $$$(p_n,pm_n)$$$}.MOVE
, con linea di movimento $$$(d, ns)$$$ e posizioni $$$(p_1,p_2,\ldots,p_n)$$$, allora $$$S_i$$$ è la singola azione MOVE
con linea di movimento $$$(\overline{d}, ns)$$$ e posizioni $$$(q_1,q_2,\ldots,q_n)$$$, dove $$$\overline{d}$$$ è la direzione opposta a $$$d$$$ e ogni $$$q_j$$$ è la posizione d'arrivo di $$$p_j$$$.JUMP
, con posizioni $$$(p_1, p_2)$$$, allora $$$S_i$$$ è la singola azione JUMP
con posizioni $$$(p_2, p_1)$$$.SWAP
, con posizioni $$$(p_1,p_2,\ldots,p_n)$$$ e corrispondenti pezzi originali (cioè quelli che c'erano primo dello SWAP
) $$$(pm_1,pm_2,\ldots,pm_n)$$$, allora $$$S_i$$$ è la sequenza di azioni {SWAP
, $$$(p_1,pm_1)$$$}$$$\cdots$$${SWAP
, $$$(p_2,pm_2)$$$}$$$\cdots$$${SWAP
, $$$(p_n,pm_n)$$$}.Chiaramente la sequenza inversa dipende, in generale, dalla board sulla quale è eseguita la sequenza di azioni da invertire.
Ogniqualvolta con la navigazione dell'albero delle mosse si arriva a un nodo corrente finale si può scegliere la mossa corrispondente, cioè il prefisso totale del nodo corrente, tramite il metodo move
di MoveChooser
. In ogni momento si può scegliere la resa con il metodo resign
e se tra le mosse valide c'è PASS
, e questo si può sapere con il metodo mayPass
, si può sceglierla con il metodo pass
. Dopo che una mossa è stata scelta l'oggetto MoveChooser
diventa inutilizzabile, cioè tutti i suoi metodi lanciano IllegalStateException
, perché ormai ha assolto alla sua funzione.
Action
in gapp.ulg.game.board
modificati i (javadoc dei) metodi equals
e hashCode
per azioni di tipo JUMP
e precisato il javadoc del costruttore per azioni JUMP
.Player
in gapp.ulg.game.board
modificato il contratto (cioè il javadoc) del metodo getMove
per gestire l'interruzione del thread corrente e aggiunto metodo di default threads
.GameFactories
in gapp.ulg.games
per ottenere tutte le GameFactory
(per board game) disponibili.PlayerFactories
in gapp.ulg.play
per ottenere tutte le PlayerFactory
(per board game) disponibili.PlayerGUI
in gapp.ulg.game.util
per rappresentare e gestire giocatori che scelgono le mosse tramite una GUI.PlayGUI
in gapp.ulg.game.util
per gestire partite controllate da una GUI.Le aggiunte e le modifiche sono in progetto.jar
. Queste possono essere tranquillamente integrate nell'homework 2 senza problemi.
Il progetto richiede quindi di realizzare un'applicazione con un'interfaccia utente grafica (GUI) che permette di giocare ai giochi del framework sia con i giocatori del framework che con giocatori umani. Più specificatamente deve realizzare le seguenti funzionalità.
GameFactories
. Può impostare gli eventuali parametri.PlayerFactories
. Può impostarne gli eventuali parametri.PlayerGUI
.PlayGUI
, viene mostrata la board e dopo ogni mossa viene mostrata l'esecuzione possibilmente passo passo e non semplicemente il cambio della board. Se il gioco ha dei punteggi, questi sono mostrati e aggiornati durante la partita. Se è stato imposto un tempo per le mosse, un indicatore del tempo che passa è mostrato ad ogni mossa.minTime
del metodo play
di PlayGUI
). Se almeno uno dei giocatori è un programma, l'utente può scegliere, prima dell'inizio della partita, le varie impostazioni circa i limiti sull'uso dei thread. Inoltre, l'utente può impostare il maxBlockTime
, vedi l'omonimo parametro del costruttore di PlayGUI
, e i valori dei parametri tol
e timeout
del metodo play
di PlayGUI
.GameFactory
e viene registrata in GameFactories
e/o viene aggiunta una PlayerFactory
e viene registrata in PlayerFactories
, l'applicazione deve permettere di giocare al nuovo gioco e/o di usare i nuovi giocatori senza nessun ulteriore aggiustamento.Per l'implementazione si può usare solamente la libreria standard di Java. La GUI deve essere implementata tramite JavaFX. Ovviamente l'implementazione deve basarsi sull'architettura del framework estesa con le classi e interfacce descritte sopra. Seguire scrupolosamente i javadoc delle classi e delle interfacce. L'implementazione dell'intera applicazione deve essere contenuta nel package gapp
di progetto.jar
. Quindi tutti gli eventuali altri package devono essere sotto-package di gapp
e tutte le definizioni di classi e interfacce devono appartenere al package gapp
o a un sotto-package. Inoltre deve esserci un package gapp.gui
che deve contenere direttamente una classe di nome Main
che definisce il metodo main
dell'applicazione. In altri termini eseguendo il main
della classe gapp.gui.Main
si esegue l'applicazione.
La valutazione del progetto dipenderà dai seguenti aspetti.
Il progetto deve essere consegnato in un unico file JAR di nome progetto.jar
di dimensione inferiore a 500KB e il cui contenuto deve rispettare la seguente struttura:
progetto
|--- src
|-- gapp
|-- sub-package1
|-- sub-package2
. . .
|--- resources
|--- eventuali sub-directory
Più precisamente il JAR deve contenere una sola directory e questa deve chiamarsi progetto
. Tale directory deve contenere una directory src
ed eventualmente un'altra directory resources
se ci sono risorse che servono alla GUI come immagini e CSS. Quindi la directory progetto
non deve direttamente contenere altre directory o file oltre a src
ed eventualmente resources
. Ovviamente, sotto gapp
ci deve essere l'intero framework dell'homework 2 incluse le integrazioni in progetto.jar
debitamente implementate.
Il JAR non deve contenere file che non servono al progetto. In particolare la directory src
deve contenere esclusivamente file sorgenti .java
e la directory resources
deve contenere solamente risorse che sono necessarie alla GUI. Per costruire il JAR si deve usare il comando
jar cvMf progetto.jar progetto
Dopo aver creato il JAR, per controllare che il contenuto sia giusto, si deve fare il seguente controllo. Prima di tutto estrarre tutti i file con il comando
jar -xf progetto.jar
Poi compilare il progetto con il comando (i percorsi sono nella versione Linux/Mac, per windows usare \
al posto di /
)5:
javac -classpath path/progetto/src progetto/src/gapp/gui/Main.java
dove path
è il percorso assoluto della directory che contiene la directory progetto
. Infine eseguire il progetto con il comando6
java -cp path/progetto/src:path/progetto/resources gapp.gui.Main
Il carattere :
che separa i due percorsi va bene per Linux/Mac mentre per Windows bisogna usare ;
. Controllare quindi che l'esecuzione dell'applicazione avvenga senza errori. ATTENZIONE: se il JAR consegnato non supera questo controllo non sarà preso in considerazione.
Prima di consegnare il progetto bisogna registrarlo tramite la pagina Registrazione Progetto. Poi può essere consegnato tramite la pagina Consegna Progetto.
Se si vuole discutere il progetto in un appello, il progetto va consegnato qualche giorno prima dell'appello secondo la seguente tabella:
Data consegna | Appello |
---|---|
|
|
|
|
15 settembre |
In projectTester.jar
ci sono i sorgenti per il test del framework gapp.ulg
del progetto.
16 Set 2016
Con la notazione X·Y è intesa la concatenazione delle sequenza X con la sequenza Y.↩
Si noti che non ci possono essere due prefissi decisionali di minima lunghezza. ↩
Però ci sono giochi in cui un nodo finale può avere nodi figli come ad esempio la Dama Cinese.↩
Se le sotto-mosse relative a due figli di un nodo di prefisso P avessero la stessa prima azione i loro prefissi avrebbero la forma P·A·Q e P·A·R dove A è la prima azione in comune, ma allora esisterebbe una sotto-sequenza S che è un prefisso sia di Q che di R tale che P·A·S è un prefisso decisionale, ma questo sarebbe in contraddizione con l'ipotesi che i due nodi sono figli del nodo di prefisso P.↩
L'opzione -classpath
imposta uno o più percorsi assoluti (separati da :
per Linux/Mac e ;
per Windows) in cui il compilatore può ricercare file sorgenti. ↩
L'opzione -cp
(abbreviazione di -classpath
) imposta uno o più percorsi assoluti (separati da :
per Linux/Mac e ;
per Windows) in cui la JVM può ricercare file .class
e risorse.↩