Metodologie di Programmazione: Lezione 9

Riccardo Silvestri

Strutturare Codice

In questa lezione è presentato e discusso il primo homework. È anche un'occasione per iniziare a discutere i principi di base per strutturare e organizzare il codice.

Qualsiasi sistema software non banale sarà modificato ed esteso nel futuro. A volte l'estensibilità è parte integrante delle specifiche, come ad esempio nel caso di un prototipo. Anche se non si conoscono in anticipo tutte le possibili estensioni, è bene progettare il sistema software in vista di future estensioni. Procedendo in tal modo la struttura del codice ci guadagnerà in razionalità, leggibilità e possibilità di riuso. Non a caso una migliore riusabilità del codice è favorita proprio dal progettare pensando alle possibili estensioni future. Progettare per l'estensibilità favorisce la riusabilità ed entrambe sono strettamente legate al information hiding. Infatti, la riusabilità di una componente software (ad esempio una classe o un metodo) è facilitata se questa ha poche e ben definite dipendenze con il resto del software. E ciò è strettamente connesso con il fatto che la componente possa avere un'implementazione libera da inutili dipendenze. A sua volta proprio la decomposizione del sistema in componenti largamente indipendenti facilita l'estensibilità rendendo più agevole aggiungere componenti o modificare componenti esistenti. Quindi i tre principi, estensibilità, riusabilità e information hiding, se ben usati si aiutano a vicenda.

Il primo homework riguarda l'implementazione di un framework per realizzare giochi da tavolo (board games) che facilita l'introduzione, cioè la programmazione, di nuovi giochi o di varianti di giochi già realizzati. Il framework è strutturato in modo tale da separare le funzionalità, cioè la meccanica, dei giochi dalla gestione delle eventuali UI ed è orientato verso l'individuazione di componenti che possono essere riusate in più giochi. Permettendo inoltre di sviluppare componenti generali capaci di funzionare con vaste famiglie di giochi, come ad esempio una UI usata per l'interazione con l'utente che gestisce in modo automatico la visualizzazione di giochi.

Board games

Prima di iniziare ad esaminare la struttura del framework dell'homework facciamo una breve panoramica dei tipi di giochi che vorremmo gestire. Dallo sconfinato universo dei giochi da tavolo ci restringiamo alla sotto-famiglia dei giochi astratti di strategia non aleatori escludendo così giochi come il Backgammon (che è astratto ma usa i dadi), Risiko e Monopoli (che non sono astratti e sono aleatori). Della ancora molto ampia varietà di giochi che rimangono, qui sotto è mostrata una piccola selezione. Per ognuno sono riportate sinteticamente alcune informazioni sulla meccanica del gioco.

Scacchi (Chess) Due giocatori. Board 8x8. 6 tipi di pezzi, Pedone (Pawn), Torre (Rook), Cavallo (Knight), Alfiere (Bishop), Regina (Queen) e Re (King). I pezzi si possono muovere (ogni tipo di pezzo ha i propri movimenti), mangiare (cioè rimuovere dalla board) e sostituire (cioè promuovere).
Shogi (Scacchi giapponesi) Due giocatori. Board 9x9. 8 tipi di pezzi, King, Rook, Bishop, Gold General, Silver General, Knight, Lance, Pawn, più 6 tipi di pezzi promossi, Promoted Rook, Promoted Bishop, Promoted Silver, Promoted Knight, Promoted Lance, Promoted Pawn. I pezzi si possono muovere (ogni tipo di pezzo ha i propri movimenti), catturare (cioè rimuovere dalla board), sostituire (promuovere) e aggiungere (cioè rimettere sulla board dopo che sono stati catturati).
Camelot Due giocatori. Board 12x14 ma con alcune posizione rimosse. 2 tipi di pezzi, Man e Knight. I pezzi si possono muovere e mangiare.
Breakthrough Due giocatori. Board 8x8 ma può essere giocato su board di qualsiasi dimensione. Un solo tipo di pezzo, Pawn. I pezzi si possono muovere e mangiare.
Dama (Checkers) Due giocatori. Molte varianti con diverse board 8x8, 10x10, 12x12 e regole leggermente differenti. Un solo tipo di pezzo, Pedina (Man) e la sua promozione, Dama (King). I pezzi si possono muovere, mangiare e sostituire.
Othello (Reversi) Due giocatori. Board 8x8. Un solo tipo di pezzo, Disco. I pezzi si possono solamente aggiungere (cioè mettere sulla board) e sostituire (cioè rovesciare, da bianchi a neri o viceversa).
Go Due giocatori. Board 19x19, ma può essere giocato su 9x9, 13x13 o 17x17. Un solo tipo di pezzo, Disco. I pezzi si possono solamente aggiungere e catturare.
Connect6 Due giocatori. Board 19x19 ma può essere giocato su board di qualsiasi dimensione. Un solo tipo di pezzo, Disco. I pezzi si possono solamente aggiungere.
Connect Four (Forza 4) Due giocatori. Board 7x6 ma ci sono varianti con board 8x7, 9x7, 10x7, 8x8. Un solo tipo di pezzo. I pezzi si possono solamente aggiungere, ma ci sono variazioni in cui i pezzi si possono anche eliminare dalla riga di fondo facendo scendere i pezzi superiori.
Mulino (Filetto o Tris) Due giocatori. Board speciale le cui posizioni sono un sottoinsieme di quelle di una board 8x8; per le adiacenze ci sono diverse varianti. Un solo tipo di pezzo. I pezzi si possono aggiungere, muovere e eliminare.
Hex Due giocatori. Board romboidale con caselle esagonali, dimensioni variabili 10x10, 11x11, 14x14. Un solo tipo di pezzo, Pedina. I pezzi si possono solamente aggiungere.
Havannah Due giocatori. Board esagonale di lato 10 con caselle esagonali. Un solo tipo di pezzo. I pezzi si possono solamente aggiungere.
Abalone Due giocatori. Board esagonale di lato 5 con caselle esagonali. Un solo tipo di pezzo, Marble. I pezzi si possono solamente muovere (anche due o tre alla volta) e rimuovere.
Chinese checkers (Dama cinese) Giocatori 2, 3, 4 o 6. Board a forma di stella con caselle esagonali. Un solo tipo di pezzo. I pezzi si possono solamente muovere.
Three-player chess Tre giocatori. Board esagonale speciale con caselle quadrate. Gli stessi tipi di pezzi degli Scacchi e le stesse mosse.
Four-player chess Quattro giocatori. Board derivata da una 14x14 rimuovendo alcune posizioni. Gli stessi tipi di pezzi degli Scacchi. I pezzi si possono muovere come negli Scacchi.

Adesso abbiamo un'idea delle entità che costituiscono questo tipo di giochi. C'è una board di varie forme che determina le posizioni in cui è possibile disporre i pezzi. I giocatori sono almeno due ma possono essere di più. Poi ovviamente ogni gioco è determinato dalle sue regole che stabiliscono i turni di gioco, le mosse ammissibili e i criteri di terminazione. Già questo primo sguardo a ciò di cui il framework deve occuparsi ci può dare delle indicazioni di massima su una sua possibile struttura. Chiaramente ci sono entità semplici, come le posizioni, i pezzi, la geometria della board e altre più complesse come le mosse e le regole del gioco. Alcune di queste entità sono comuni a più giochi, come ad esempio i pezzi e le board. Queste dovrebbero essere rappresentate nel framework in modo tale che sia facile usarle dovunque siano utili, così da evitare replicazione di codice e favorire codice più snello e leggibile. Propenderemo quindi per introdurre classi e/o interfacce per rappresentare tali entità di modo che risultino essere componenti di uso il più generale possibile.

Per le entità più complesse come il gioco, la gestione di una partita e i giocatori, le idee saranno più chiare dopo aver esaminato nel dettaglio le entità più semplici. Però possiamo comunque iniziare a riflettere sulle entità più complesse cercando di ottenere delle linee guida che potrebbero esserci utili anche per determinare la struttura di quelle più semplici1. Sicuramente ci dovrà essere una qualche parte di codice che si occupa di gestire una partita di un particolare gioco. Sarebbe bene che questo sia usabile in una varietà di situazioni, ad esempio da un gestore di UI per permettere agli utenti di giocare una partita ma anche da parte di altri programmi per giocare partite tra giocatori che sono programmi. Probabilmente ci sarà quindi un qualche tipo di oggetto che fungerà da gestore di partite di un certo gioco. Volendo permettere che i giocatori possano essere qualsiasi, sia umani che programmi, sia direttamente connessi all'applicazione, magari tramite una UI, che collegati in remoto tramite una rete di computer come Internet, ci sarà anche un qualche tipo di oggetto che rappresenterà un giocatore. Le definizioni dei gestori di partite o giochi e degli oggetti che rappresentano i giocatori, dovrebbero essere sufficientemente astratte da permettere a programmi che realizzano applicazioni sui giochi di poter trattare le entità coinvolte, giochi e giocatori, nel modo più generale e uniforme possibile. In altri termini non si vuole che tali applicazioni debbano trattare con codice ad hoc i diversi giochi o le diverse tipologie di giocatori, a meno che non sia strettamente necessario. Quindi, cerchiamo di evitare replicazione di codice e di favorire codice snello e uniforme negli utilizzatori del framework cercando di prevedere i possibili usi del framework stesso. Di solito questo approccio ha l'effetto di migliorare la struttura del framework rendendola più razionale e versatile.

Esaminiamo ora in dettaglio le entità una alla volta, iniziando da quelle più semplici, decidendo come rappresentarle e definirle nel framework.

Board e posizioni

Ogni gioco usa una board che può essere di diverse forme e che determina un insieme di posizioni che possono essere occupate dai pezzi (pedine, dischi, ecc.). Osserviamo che in tutti i casi (o quasi tutti) le posizioni possono essere identificate da coppie di coordinate date da due assi uno orizzontale e uno verticale (rispetto ad una immagine della board). Nel caso delle board quadrate o comunque con caselle quadrate, questo è evidente. Un po' meno per quelle con caselle esagonali. In tali casi si possono usare due assi che in generale non saranno perpendicolari ma, ad esempio, paralleli a due lati dell'esagono. Per uniformità chiameremo asse di base l'asse orizzontale o quello più vicino a quello orizzontale e chiameremo l'altro asse trasversale. Inoltre, per fissare in modo univoco questi sistemi di coordinate, stabiliamo che l'asse di base è orientato da sinistra verso destra, cioè le coordinate su tale asse crescono in tale direzione e l'asse trasversale è orientato dal basso verso l'alto. Tutte le volte che è possibile immagineremo la board con la sua posizione più in basso a sinistra con coordinate (0,0). Non è però sempre possibile, come ad esempio per board a forma di stella come quella della Dama Cinese. In tutti i casi porremmo i due assi (di base e trasversale) in modo tale che l'intera board cada nel quadrante positivo, così che le coordinate di tutte le posizioni sono non negative. Inoltre avvicineremo gli assi il più possibile in modo che tocchino la board. Vale a dire, la posizione più in basso dovrebbe avere coordinata trasversale zero e quella più a sinistra (rispetto all'inclinazione dell'asse trasversale) coordinata di base zero.

Così facendo possiamo pensare di racchiudere la board nel più piccolo rettangolo o parallelogramma determinato dai due assi e da due linee parallele agli assi che toccano la board agli altri estremi. Chiameremo width della board la lunghezza del lato di base di tale parallelogramma e height la lunghezza dell'altro lato. Nel caso particolare di una board di forma quadrata o rettangolare (come quella degli Scacchi), width e height sono proprio le dimensioni della board. Questo è vero anche per board con caselle esagonali di forma romboidale come quella del gioco Hex. Non è invece vero per board di forma esagonale o di altre forme speciali. Le posizioni (ammissibili) per tali board si potranno sempre rappresentare come un sottoinsieme di quelle del parallelogramma, cioè sono quelle del parallelogramma eccetto alcune escluse. Questo vale per tutte le board con forme speciali, come quelle dei giochi Camelot e Four-player chess.

Non ci sono classi già pronte (nella libreria di Java) per rappresentare coppie di interi cioè, nel nostro caso, le coppie di coordinate delle posizioni. Allora conviene definire una tale classe perché così si potranno più facilmente usare liste, insiemi o mappe di posizioni. Se non ci sono forti ragioni in contrario, conviene che gli oggetti siano immutabili. Questo perché in tal modo si potranno usare negli insiemi e nelle mappe (come chiavi) senza temere malfunzionamenti dovuti alla mutabilità e, come vedremo nella seconda parte del corso, l'immutabilità rende più agevole la programmazione multithreading (cioè parallela e concorrente). Inoltre se un oggetto è immutabile è più facile da manipolare anche per il programmatore perché non deve preoccuparsi se il suo stato (o valore) è cambiato e in che modo. Queste considerazioni hanno portato alla classe Pos dell'homework. Si osservi che proprio per poter usare in modo corretto gli oggetti Pos in liste, insiemi e mappe è necessario ridefinire in modo appropriato i metodi equals e hashCode.

Adiacenze

Una board non determina solamente le posizioni in cui disporre i pezzi del gioco, ma anche le adiacenze. Spesso i movimenti permessi ai pezzi sono determinati dal tipo di adiacenza tra le posizioni. Ad esempio, negli Scacchi l'Alfiere si muove sulle diagonali, cioè lungo le adiacenze per spigolo delle caselle, e la Torre sulle linee, cioè lungo le adiacenze per lato delle caselle. In generale una board come quella degli Scacchi determina otto direzioni di adiacenza che possiamo indicare in base alle direzioni dei due assi (l'asse di base è orientato verso destra e quello trasversale verso l'alto):

UP, DOWN, LEFT, RIGHT, UP_L, UP_R, DOWN_L, DOWN_R

Un sistema che ha questo tipo di adiacenze (cioè basato su caselle quadrate o rettangolari) lo chiameremo OCTAGONAL. Mentre per board con caselle esagonali le adiacenze sono solamente quelle per lato, queste sono sei e le possiamo indicare con un appropriato sottoinsieme delle otto:

LEFT, RIGHT, UP_L, UP_R, DOWN_L, DOWN_R

Per la selezione di queste sei abbiamo preso come modello la board di Hex. Un sistema con questo tipo di adiacenze lo chiameremo EXAGONAL.

Tipi di pezzi

Ogni gioco ha i suoi tipi di pezzi anche se alcuni giochi usano tipi di pezzi che, almeno dal punto di vista fisico, sono gli stessi di quelli di altri giochi. L'apparenza fisica (cioè la forma e dimensione) di un tipo di pezzo non ci interessa quanto la sua meccanica all'interno di un gioco. Vale a dire come può essere usato, quali tipi di mosse può fare. Da questo punto di vista c'interessa identificarne solamente la funzione e questa di solito cambia da gioco a gioco (anche se il pezzo ha esattamente la stessa forma, dimensione e colore). Per fare ciò è sufficiente un nome, come ad esempio Pedina nera o Alfiere bianco. Quindi potremmo pensare di usare delle semplici stringhe per rappresentare i vari tipi di pezzi. Questo può avere dei vantaggi e degli svantaggi. Può essere vantaggioso perché non pone limiti su quanti pezzi si possono rappresentare e quanti se ne possono aggiungere. D'altra parte non permette quasi nessun controllo su errori relativi al digitare un nome errato (ad esempio una minuscola al posto di una maiuscola) o all'usare una stringa che significa qualcos'altro per il nome di un pezzo (dato che le stringhe possono essere usate per tantissimi scopi). C'è anche un altro aspetto, se usassimo le stringhe dovremmo necessariamente avere per ogni tipo di pezzo almeno due versioni una per un colore e una per l'altro (ammesso che due colori siano sufficienti), ad es. "Pedone nero" e "Pedone bianco".

Il problema non sta tanto nella proliferazione dei nomi ma nel fatto che questo renderebbe difficile scrivere codice che non dipende (o dipende solo simmetricamente) dal colore dei pezzi. Infatti le funzionalità dei pezzi generalmente non dipendono dal colore specifico ma solamente se hanno colore uguale o diverso da quello di altri pezzi. Così le funzionalità di un Pedone bianco sono del tutto simmetriche a quelle di un Pedone nero. Mentre le funzionalità di un Alfiere bianco sono radicalmente differenti da quelle di un Pedone bianco. Perciò sarebbe preferibile rappresentare i tipi di pezzi tramite oggetti che permettono di accedere separatamente ad ognuna di queste due proprietà: la proprietà che potremmo chiamare la specie del pezzo, Pedone, Alfiere, Disco, ecc., e il colore. Per mantenere un controllo sul tipo che rappresenta la specie, che è la più importante delle due proprietà, invece di usare le stringhe conviene usare un tipo ad hoc. Per le stesse ragioni considerate per la classe Pos, conviene che tale tipo sia immutabile. Una soluzione potrebbe quindi essere una enum i cui valori rappresentano le diverse specie di pezzi. Però è praticamente impossibile elencare tutte le specie di pezzi, tenendo anche in conto che continuamente vengono inventati nuovi giochi. E vogliamo che il framework sia facilmente estendibile e quindi usabile anche per nuovi giochi che ancora non esistono.

Una possibile soluzione è quella presentata nell'homework. La classe PieceModel rappresenta i tipi dei pezzi. Per evitare confusione con il termine tipo usato in Java, il tipo di pezzo è chiamato modello di pezzo. Gli oggetti di tale classe mantengono le due proprietà specie e colore. La enum Species2 contiene solamente alcune delle specie più comuni di pezzi ed è definita internamente a PieceModel perché è strettamente legata a tale classe. Per favorire l'estensibilità, cioè la possibilità di aggiungere altre specie di pezzi senza modificare direttamente il framework, la classe PieceModel è generica con parametro di tipo che rappresenta proprio la specie. Per garantire che gli oggetti di PieceModel siano immutabili, il tipo parametrico dovrebbe essere a sua volta immutabile. Java non fornisce alcun meccanismo per garantire che un tipo sia immutabile (cioè che gli oggetti di quel tipo siano immutabili). Però permette di vincolare una variabile di tipo ad essere una enum e questa è la dichiarazione del tipo parametrico usata per PieceModel: <S extends Enum<S>>, significa che S può essere solamente un tipo enum. Si osservi che per le stesse ragioni della classe Pos è necessario ridefinire i metodi equals e hashCode.

Board e disposizioni di pezzi

Ora che sappiamo come rappresentare anche i pezzi, torniamo alle board. Una board particolare come ad esempio quella degli Scacchi può essere usata anche da altri giochi, ad esempio Dama e Breakthrough. Programmi che gestiscono giochi che usano lo stesso tipo di board avranno parti di codice uguali o molto simili per rappresentare e gestire le posizioni della board e le disposizioni dei pezzi. Per evitare replicazione di codice e per facilitare la programmazione di nuovi giochi (cioè l'estensibilità), conviene introdurre una o più classi per gestire almeno le board più comuni. Così gestori di giochi che usano la stessa board potranno riusare la classe già pronta che gestisce la board. Quindi sicuramente conviene introdurre una o più classi per rappresentare (e gestire) le board e le disposizioni di pezzi su di esse. Come si intuisce dalla grande varietà di board nella seppur piccola selezione riportata sopra, è praticamente impossibile introdurre classi capaci di gestire tutte le possibili board, considerando anche quelle di nuovi giochi ancora da inventare. Inoltre alcune operazioni relative ad una board non dipendono dal tipo particolare di board, come ad esempio la possibilità di conoscere in quali posizioni si trovano i pezzi e altre dipendono solamente da caratteristiche generali quali il sistema di coordinate usato. Queste operazioni potrebbero essere utili ad un gestore della UI per visualizzare la board e i pezzi su di essa.

Per poter scrivere codice (per gestire un gioco o una UI per giochi) senza preoccuparsi del tipo particolare di board ma che possa invece essere usabile con la più grande varietà possibile di board, evitando così la replicazione di codice e al tempo stesso rendendolo più semplice e leggibile, conviene introdurre un'interfaccia che rappresenta le operazioni comuni alle diverse board. Questa è l'interfaccia Board dell'homework. La Board non ha solo operazioni relative alla sua forma e alle sue posizioni ma anche alla disposizione dei pezzi. Però un oggetto Board non ha bisogno di "conoscere" il significato dei pezzi perché deve semplicemente registrarne le posizioni sulla board. Per questa ragione e per permettere la massima versatilità dell'interfaccia si è preferito definirla come generica Board<P> rispetto al tipo P che rappresenta i pezzi (ovvero il modello dei pezzi). Inoltre le informazioni relative al sistema di coordinate e alle direzioni delle adiacenze sono state rappresentate con le enum System e Dir3 annidate nell'interfaccia Board perché sono strettamente legate alle board e non hanno significato al di fuori di esse.

C'è ancora un altro aspetto da considerare, l'immutabilità degli oggetti Board. Siccome assumiamo che le posizioni di una board non possano essere modificate durante lo svolgimento di un gioco, la mutabilità è dovuta solamente alle disposizioni dei pezzi. Per ragioni analoghe a quelle discusse in relazione a Pos e PieceModel sarebbe vantaggioso che oggetti di tipo Board siano immutabili. In questo caso ci sono ulteriori ragioni a favore dell'immutabilità. Non si vuole che una Board usata da un gestore di UI sia modificabile direttamente (dovrebbe essere modificata tramite un gestore del gioco). Questo vale in generale per tutti gli usi che non siano quelli da parte di gestori di giochi che sono i soli che "conoscono" il modo giusto di disporre i pezzi. Ci sono diverse possibili soluzioni, ad esempio si potrebbero definire due interfacce una per le board immutabili, senza quindi i metodi che modificano la disposizione dei pezzi, e una che la estende aggiungendo i metodi che mutano la disposizione dei pezzi. Si è preferita una soluzione con un'unica interfaccia4 che ha tutti i metodi ma quelli che mutano hanno l'implementazione di default che li rende non usabili. Così una classe che implementa Board, di default, è immutabile a meno che non ridefinisca i metodi che mutano. Per riconoscere più agevolmente se un oggetto di tipo Board è immutabile o meno è stato aggiunto il metodo isModifiable.

Nel framework sono state introdotte due classi BoardOct e BoardHex5 per rappresentare, rispettivamente, board modificabili di tipo OCTAGONAL e HEXAGONAL. Siccome queste sono modificabili è stato anche introdotto il metodo statico di utilità Utils.UnmodifiableBoard che permette di ottenere una versione immodificabile da una Board modificabile.

Mosse

Dopo aver deciso come rappresentare e definire i pezzi e le board di un gioco possiamo passare ad esaminare le mosse che i giocatori possono compiere durante le partite. Le mosse non sono entità semplici come le posizioni o i pezzi, basta pensare a quelle di giochi come la Dama, Scacchi e Abalone. Ad esempio nella Dama un pedina può compiere uno o più salti (non c'è un limite sul numero di salti se non quello delle dimensioni della board e il numero dei pezzi in gioco) mangiando ad ogni salto una pedina avversaria. In alcune varianti della Dama le mosse possono essere ancora più varie, la pedina promossa a King (l'omologo del pezzo chiamato Dama nella versione italiana) può, come si dice, volare potendo fare salti di lunghezza qualsiasi purché atterri su una qualsiasi casella vuota successiva alla pedina mangiata. Nella maggior parte dei giochi un giocatore in una mossa può muovere o aggiungere un solo pezzo, ma in alcuni giochi come Connect6 e Abalone si possono aggiungere o muovere più pezzi alla volta.

C'è una notevole varietà di mosse possibili nei vari giochi. Però sono tutte formate da combinazioni di un piccolo gruppo di azioni più semplici. Infatti le mosse possono essere specificate come sequenze formate dai seguenti tipi di azione:

È facile vedere che effettivamente una qualsiasi mossa dei giochi sopra elencati e di molti altri può essere espressa in modo naturale6 tramite una sequenza di tali azioni. A questo punto viene da sé l'idea di introdurre una classe per rappresentare le azioni. E questa è proprio la classe generica Action<P> dell'homework. Il parametro di tipo P, per il tipo del modello dei pezzi, è stato introdotto per ragioni analoghe a quelle discusse per l'interfaccia Board. I possibili tipi di azione, sopra elencati, sono specificati tramite una enum annidata nella classe Action. Al pari delle classi Pos e PieceModel, anche gli oggetti di Action è preferibile che siano immutabili e quindi la definizione di Action segue il modello di tali classi. Per garantire l'immutabilità, il campo pos che contiene la lista delle posizioni deve essere implementato tramite una lista immodificabile7. Come Pos e PieceModel anche Action deve ridefinire i metodi equals e hashCode.

Potremmo quindi rappresentare una qualsiasi mossa come una lista di oggetti Action? No, perché alcuni tipi di mossa non sarebbero rappresentabili e ce ne sono almeno due: passare il turno e abbandonare/arrendersi8. Quindi conviene introdurre una classe per rappresentare anche tali mosse. Questa è la classe generica Move<P> dell'homework, come al solito il parametro P è necessario per il tipo del modello dei pezzi. Per distinguere i vari tipi di mossa è stata definita una enum annidata. Per la classe Move valgono considerazioni analoghe a quelle discusse per Action riguardo all'immutabilità, il campo actions e la ridefinizione dei metodi equals e hashCode.

Giochi, partite e giocatori

Adesso abbiamo tutti gli elementi per riflettere su come rappresentare e gestire le partite di un gioco. Prima di tutto rimarchiamo che il framework non si occupa direttamente della UI o comunque della gestione della sorgente delle mosse, se provengono da un essere umano che interagisce con una UI o da una connessione di rete o da un programma. Si occupa invece di rappresentare le partite di giochi e i giocatori ma solamente dal punto di vista della meccanica dei giochi, cioè la rappresentazione delle configurazioni o stati del gioco durante una partita, le mosse lecite, l'aggiornamento dello stato a seguito di una mossa, ecc.

Ci sono molte possibilità da vagliare. Ci deve essere un oggetto per ogni singola partita o un oggetto può gestire anche più partite? Deve gestire solamente i turni di gioco e le mosse o deve gestire direttamente anche i giocatori in modo integrato? O i giocatori devono essere gestiti da oggetti distinti? Dovremmo introdurre un'interfaccia/interfacce o solamente delle classi per ogni gioco?

Sicuramente è meglio separare la gestione delle partite e giochi da quella dei giocatori. E questo per varie ragioni. La prima ha una valenza molto più generale dello specifico contesto di cui ci stiamo occupando. Quando due o più entità sono suscettibili di essere rappresentate con componenti distinte, cioè non è strettamente necessario che siano rappresentate in modo integrato con una sola componente, conviene cercare di rappresentarle con componenti distinte. I vantaggi derivano dal fatto che qualcosa di potenzialmente complesso, una sola componente che integra due o più entità, è ridotta a qualcosa di più semplice formato da due o più componenti separate. Questo ha di solito conseguenze benefiche su molti aspetti, facilità d'implementazione, leggibilità del codice, possibilità di riuso delle componenti perché sono più semplici e specifiche, facilità di attività di manutenzione del codice così come delle attività di testing e debugging. Oltre a tutto ciò nel nostro caso specifico ci sono anche altre ragioni. Volendo permettere che un giocatore possa essere sia un programma che un essere umano e che possa essere determinato sia dall'interazione con una UI direttamente gestita da un'applicazione o gestita a distanza tramite una rete, la separazione della gestione dei giocatori da quella dei giochi diventa quasi obbligatoria.

Quindi una prima decisione è presa: partite e giocatori sono rappresentati da componenti distinte. Ora dovremmo decidere se la componente o oggetto che gestisce un gioco gestisce solamente una singola partita o può gestirne più d'una. Tra una partita e un'altra partita, in generale, non ci sono relazioni che riguardano specificatamente il gioco stesso, quindi propendiamo per una componente capace di gestire solamente una singola partita anche perché è un po' più semplice di quella per molte partite. Quindi ci potranno essere oggetti di vario tipo ognuno dei quali capace di gestire una partita ad un certo gioco. Avremo sicuramente un tipo diverso di oggetti per ogni tipo di gioco. Ma così potremmo avere grossi problemi, ad esempio, nel costruire applicazioni che permettano agli utenti di scegliere tra molti giochi a cui giocare. Considerazioni simili a quelle già incontrate per le board valgono anche per i gestori di partita e spingono verso l'introduzione di un'interfaccia generale per tali componenti. Con una tale interfaccia avremo ancora gestori di partita di tipo diverso per ogni gioco. Ma tutti dovranno implementare la stessa interfaccia e così saranno usabili da qualsiasi codice anche senza conoscere il loro tipo specifico e soprattutto in modo uniforme e non con codice ad hoc per ognuno di essi. L'interfaccia per i gestori di partita può essere quindi definita come l'interfaccia generica GameRuler<P> dell'homework, anche qui P è il tipo del modello dei pezzi. I suoi metodi permettono di avere informazioni circa la partita come il nome del gioco e i nomi dei giocatori, di conoscere la disposizione dei pezzi, il giocatore di turno, di eseguire una mossa (e di fare anche l'undo dell'ultima mossa), di sapere se la partita è terminata e con quale esito. Si osservi che per rendere disponibile la disposizione dei pezzi, in ogni momento della partita, il metodo getBoard ritorna una Board immodificabile.

Prima di vedere la relazione tra GameRuler e i giocatori c'è un aspetto minore ma non trascurabile da considerare. Supponiamo di voler scrivere un'applicazione che dia la possibilità all'utente, tramite una UI (ad es. una GUI), di scegliere un gioco tra un elenco di giochi e di fare poi una partita nel gioco scelto. Alcuni giochi come la Dama o Checkers hanno una grande varietà di varianti relative alla dimensione della board e alle regole. Molti altri giochi possono essere giocati, ad esempio, su parecchie board di diverse dimensioni. Un modo tipico di rendere facile e naturale queste scelte è di far sì che la UI presenti prima di tutto la scelta del tipo di gioco e poi delle eventuali varianti (dimensioni della board, regole, ecc.) del gioco scelto. Per favorire questo tipo di utilizzi del framework è conveniente avere una componente che può creare (o fabbricare) gestori di partita di un certo tipo di gioco in funzione di eventuali parametri che determinano una variante particolare del gioco. Potremmo vedere una simile componente come una fabbrica di gestori di partita di uno specifico gioco. Siccome ogni tipo di gioco dovrebbe avere una sua fabbrica di giochi definita come un oggetto di un tipo specifico, conviene introdurre un'interfaccia per permetterne un utilizzo uniforme. E questa è l'interfaccia generica GameFactory<G> dove il parametro G è il tipo del gestore di partita. Si è preferito definirla in modo parametrico invece che direttamente solo per GameRuler perché così è usabile anche per altre categorie di giochi, non solo per i tipi di giochi che sono gestibili con un GameRuler. I parametri che determinano la specifica variante di un gioco sono rappresentati tramite oggetti che implementano l'interfaccia Param<T>. Tale interfaccia può gestire parametri con valori di un tipo qualsiasi T (Integer, String, o altro) e allo stesso tempo permette di presentare i possibili valori del parametro in una UI per la scelta da parte dell'utente senza bisogno che il gestore della UI "conosca" il significato del parametro. Oltre a ciò una GameFactory richiede e gestisce anche i nomi dei giocatori della partita che sarà fabbricata.

Giocatori

Chiaramente ogni giocatore o tipo di giocatore sarà rappresentato e gestito da un oggetto di un tipo specifico. Per i giocatori umani ci potranno essere oggetti di un certo tipo che li gestiscono tramite una UI, per i giocatori connessi in rete ci saranno altri tipi di oggetti così come per giocatori realizzati tramite programmi. Affinché li si possa usare in modo uniforme senza cioè preoccuparsi della loro specifica natura, evitando così la replicazione di codice e favorendo la scrittura di codice più snello e leggibile, è necessario introdurre un'interfaccia che astragga le caratteristiche comuni e fondamentali dei giocatori, ovvero quelle che servono proprio per giocare una partita tramite un GameRuler. Avendo deciso di introdurre un'interfaccia per i giocatori, si tratta ora di chiarire in che modo un GameRuler, cioè un gestore di partita, interagisce con i giocatori, cioè la detta interfaccia, per effettuare una partita. È evidente che un giocatore deve conoscere in ogni momento lo stato della partita (la disposizione dei pezzi sulla board e altro), conoscere le mosse degli altri giocatori e poi fare la propria mossa. Se dessimo a un oggetto giocatore accesso diretto all'oggetto GameRuler che sta gestendo la partita in corso, questi potrebbe fare di tutto, fare mosse non sue, fare l'undo di mosse, ecc. Potremmo allora consentire l'accesso solamente a una versione immodificabile dell'oggetto GameRuler, più precisamente, ad una view immodificabile. Questa sarebbe una soluzione accettabile anche se potrebbe non essere la soluzione migliore per giocatori connessi in remoto. Tuttavia vorremmo che un GameRuler non solo permetta di giocare una partita ma offra anche qualche servizio in più relativo alla "conoscenza" delle regole del gioco che necessariamente un GameRuler deve possedere. Infatti, i metodi isValid(Move<P> m), validMoves() e validMoves(Pos p) permettono di fare delle analisi del gioco considerando le mosse possibili. Combinati con i metodi move e unMove permettono di fare analisi a più grande profondità provando a fare una mossa, esaminare la situazione che ne risulta, fare poi un'altra mossa e così via. E grazie al metodo unMove potremo sempre ritornare alla configurazione di partenza. Per dare questa possibilità ai giocatori dovremmo necessariamente consentire l'accesso all'oggetto GameRuler modificabile. Però non deve essere necessariamente l'oggetto che gestisce la partita ma solamente una copia di tale oggetto. In questo modo ogni giocatore avrà la sua copia del GameRuler della partita e potrà usarla a suo piacimento senza che questo modifichi in alcun modo l'oggetto originale. Ovviamente sarà responsabilità del giocatore mantenere la sua copia sincronizzata con l'oggetto originale9. Per realizzare ciò l'interfaccia GameRuler ha il metodo copy10 che ritorna una copia profonda (deep copy) dell'oggetto stesso. Con copia profonda si intende una copia che è totalmente indipendente dall'oggetto originale che è stato copiato. Questo significa che l'oggetto copiato e la copia non devono condividere nessun valore che sia modificabile, però possono condividere valori immutabili (come ad es. stringhe). Questo garantisce che la modifica di uno dei due oggetti non ha alcuna influenza sull'altro. Se invece la copia condividesse con l'oggetto originale un valore modificabile, tale valore potrebbe essere modificato nella copia e questo produrrebbe un cambiamento anche nell'oggetto originale che lo condivide.

L'interfaccia dell'homework Player<P>, dove P è sempre il tipo del modello dei pezzi, è definita in ossequio alle decisioni sopra discusse. In particolare il suo metodo setGame(GameRuler<P> g) comunica al giocatore l'inizio di una nuova partita e gli passa una copia g del relativo GameRuler. Inoltre, per permettere la sincronizzazione con l'oggetto della partita ha il metodo moved(int i, Move<P> m) che comunica al giocatore le mosse degli altri giocatori e anche del giocatore stesso. Infine ha il metodo Move<P> getMove() che permette di chiedere al giocatore la sua mossa.

Grazie alla struttura che abbiamo finora discusso è possibile definire giocatori che possono giocare a qualsiasi gioco. Un esempio è la classe RandPlayer<P> che implementa Player<P> e usando il metodo validMoves() del GameRuler può sempre scegliere una mossa valida, in modo random. Programmi che giocano anche se in modo random sono utili per effettuare test rapidi di nuovi giochi o per avere immediatamente a disposizione, per un qualsiasi gioco, un giocatore (che è facile da battere).

Giocare

Uno degli obiettivi principali che hanno guidato la progettazione del framework discusso è la possibilità di usare le componenti del framework per scrivere programmi o applicazioni per giocare partite in una varietà di giochi in modo da non doversi preoccupare dei dettagli dei diversi giochi ma in modo uniforme e generale. E questo porta gli importanti vantaggi che abbiamo più volte messo in evidenza. Forse non è inutile sottolineare che questi non sono solamente dei semplici vantaggi ma molto spesso rappresentano la differenza tra un sistema software affidabile con una struttura che garantisce le attività di manutenzione ed estensibilità che sono necessarie perché il sistema sia usabile per un lungo periodo di tempo e un sistema inaffidabile mal strutturato difficile da mantenere ed estendere e quindi destinato a una scarsa usabilità e una vita molto breve. Insomma la differenza tra un sistema software di successo e uno destinato al fallimento.

Un programma basilare che usa il framework nel modo sopraddetto è quello del metodo Utils.play dell'homework. Prendendo in input una qualsiasi GameFactory che fabbrica GameRuler e un numero adeguato di Player qualsiasi, gioca una partita nel gioco del GameRuler ottenuto dalla GameFactory con i giocatori forniti. Quindi lo stesso codice può eseguire partite in un qualsiasi gioco (purché gestito da un GameRuler) e con giocatori qualsiasi. Un esempio un po' più complesso ma più vicino a quello che potrebbe fare un'applicazione basata su una UI che permette ad utenti di giocare, è quello del metodo Utils.playTextUI.

Chiaramente per poter provare il framework e in particolare tali metodi bisogna implementare qualche gioco. Esempi di giochi sono forniti dalle classi Othello, OthelloFactory e HexFactory.

25 Mar 2016


  1. Il processo di progettazione software non procede quasi mai in una sola direzione dai livelli più bassi verso i livelli più alti (bottom-up) o nella direzione opposta (top-down). Piuttosto ha un andamento oscillatorio irregolare che a volte va in una direzione e a volte nell'altra.

  2. Si noti che il modificatore static è implicito perché le enum annidate possono essere solamente statiche.

  3. In questo caso si è optato per un compromesso tra la massima versatilità è il requisito di non rendere l'homework troppo complicato. Invero per garantire la massima versatilità dell'interfaccia Board i riferimenti ai tipi System e Dir nei vari metodi si sarebbero dovuti sostituire con due parametri generici S e D che si sarebbero dovuti aggiungere al parametro P nella dichiarazione di Board.

  4. Una delle ragioni che hanno spinto a preferire la soluzione con un'unica interfaccia rispetto alle due interfacce (quella mutabile che estende quella immutabile) è che con quest'ultima l'immutabilità è aggirabile con un semplice cast.

  5. L'homework non richiede l'implementazione della classe BoardHex. È stata comunque introdotta per mostrare come potrebbe essere esteso il framework.

  6. Se volessimo semplicemente specificare l'effetto di una mossa, cioè il cambiamento che opera nella disposizione dei pezzi sulla board, sarebbero sufficienti solamente due tipi di azione ADD e REMOVE. Però in questo modo la rappresentazione della mossa non sarebbe naturale, cioè aderente allo spirito e alla pratica del gioco. Ad esempio per specificare il movimento di una Torre dovremmo rappresentarla come un'azione REMOVE, che rimuove la Torre dalla sua attuale posizione, seguita da un'azione ADD che aggiunge la Torre nella posizione di destinazione. Inoltre la specifica naturale di una mossa facilita il compito di un visualizzatore che troverà in tale specifica informazioni sufficienti per visualizzare in modo accettabile l'esecuzione di una mossa.

  7. Si osservi che il campo pos non poteva essere implementato tramite un array perché non c'è modo in Java di garantire che le componenti di un array non siano modificabili. Si sarebbe invece potuto introdurre un metodo getPos e reso privato il campo pos. Però il metodo getPos o ritornava comunque una lista immodificabile o una copia della lista del campo privato pos.

  8. In realtà ci sono anche altri tipi di mossa, ad esempio negli Scacchi un giocatore può proporre una patta da concordare con l'altro giocatore.

  9. Questo significa che se un giocatore modifica la sua copia provando a fare delle mosse, poi dovrà riportare la sua copia (tramite una o più invocazione del metodo unMove) allo stato precedente alle modifiche fatte, prima di effettuare la sua mossa nella partita.

  10. Il linguaggio Java ha un meccanismo per copiare oggetti o meglio per clonarli tramite l'interfaccia Cloneable e il metodo clone di Object, però il suo uso corretto non è agevole. In ogni caso quello che il meccanismo offre direttamente è solamente una copia non profonda (shallow copy).