Metodologie di Programmazione: Progetto

Riccardo Silvestri

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.

Albero delle Mosse

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).

Scelta di una mossa

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:

  1. $$$S$$$ seleziona un'azione $$$A$$$ (di tipo 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.
  2. $$$S$$$ quasi-seleziona un'azione $$$A$$$ se $$$S$$$ è un sotto-insieme proprio dell'insieme di posizioni di $$$A$$$.
  3. $$$S$$$ seleziona un nodo (dell'albero delle mosse) se $$$S$$$ seleziona la prima azione della sotto-mossa del nodo.
  4. $$$S$$$ quasi-seleziona un nodo se $$$S$$$ quasi-seleziona la prima azione della sotto-mossa del nodo.

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:

  1. Se $$$A_i$$$ è ADD, con posizione $$$p$$$ e pezzo $$$pm$$$, allora $$$S_i$$$ è la singola azione REMOVE con posizione $$$p$$$.
  2. Se $$$A_i$$$ è 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)$$$}.
  3. Se $$$A_i$$$ è 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$$$.
  4. Se $$$A_i$$$ è JUMP, con posizioni $$$(p_1, p_2)$$$, allora $$$S_i$$$ è la singola azione JUMP con posizioni $$$(p_2, p_1)$$$.
  5. Se $$$A_i$$$ è 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.

Classi e interfacce modificate o aggiunte rispetto all'homework 2

Le aggiunte e le modifiche sono in progetto.jar. Queste possono essere tranquillamente integrate nell'homework 2 senza problemi.

Funzionalità dell'applicazione

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à.

  1. L'utente può scegliere un gioco tra quelli disponibili (quelli attualmente disponibili nel framework ma anche altri che potrebbero essere aggiunti), ottenibili tramite GameFactories. Può impostare gli eventuali parametri.
  2. Poi l'utente può scegliere i giocatori tra quelli disponibili (quelli attualmente disponibili nel framework ma anche altri che potrebbero essere aggiunti), ottenibili tramite PlayerFactories. Può impostarne gli eventuali parametri.
  3. L'utente può scegliere se far giocare la partita da giocatori che sono programmi o può giocare contro un programma o può giocare contro altri giocatori umani. I giocatori umani devono essere rappresentati gestiti tramite gli oggetti della classe PlayerGUI.
  4. Durante lo svolgimento di una partita, che deve essere gestita tramite un oggetto 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.
  5. L'utente può scegliere un tempo minimo di pausa tra una mossa e la successiva (vedi parametro 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.
  6. In ogni momento l'utente più interrompere la partita in corso. L'interfaccia utente deve essere sempre reattiva, qualunque siano le operazioni in esecuzione.
  7. Se si aggiunge una 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.

Valutazione

La valutazione del progetto dipenderà dai seguenti aspetti.

  1. Rispetto delle specifiche, affidabilità ed efficienza valutate anche tramite testing.
  2. Effettivo rispetto dell'architettura del framework e quindi possibilità di aggiungere giochi e giocatori rendendoli disponibili ed usabili dagli utenti dell'applicazione senza alcun ulteriore aggiustamento.
  3. Qualità e usabilità della GUI.
  4. Organizzazione e struttura del codice.
  5. Leggibilità e documentazione.

Consegna

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
14 giugno 17 giugno
5 luglio 8 luglio
11 settembre 15 settembre

Testing

In projectTester.jar ci sono i sorgenti per il test del framework gapp.ulg del progetto.

16 Set 2016


  1. Con la notazione X·Y è intesa la concatenazione delle sequenza X con la sequenza Y.

  2. Si noti che non ci possono essere due prefissi decisionali di minima lunghezza.

  3. Però ci sono giochi in cui un nodo finale può avere nodi figli come ad esempio la Dama Cinese.

  4. 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.

  5. 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.

  6. 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.