Java Tutorial - Parte 2 0.1
Un tutorial per esempi
Caricamento in corso...
Ricerca in corso...
Nessun risultato
Versione 0.3: il pannello del giocatore

Introduzione

Nel capitolo precedente abbiamo usato diversi componenti grafici della libreria Java Swing: etichette, bottoni, pannelli, layout managers, etc. Ci sono moltissimi altri componenti in Swing ma andremo con ordine: quando ci serviranno lo useremo. I più curiosi e smaniosi possono leggere la documentazione ufficiale in lingua inglese a questo link: Using Swing Components che da una panoramica generale su tutti i componenti della libreria.

In questo capitolo implementeremo il pannello del giocatore che si compone di due sottopannelli:

  • SequencePanel: il pannello delle sequenze che abbiamo già scritto e testato nel capitolo precedente ma che modificheremo in questo capitolo
  • CommandPanel: il pannello dei bottoni di comando

Quello che vogliamo ottenere è un pannello di questo tipo:

Il sottopannello in alto è il pannello delle sequenze che contiene la solution e tutte le guess con i risultati del confronto con la solution. Questo pannello viene posizionato nel BorderLayout.CENTER Il sottopannello in basso è il pannello dei comandi che avevamo scritto in Il sotto-pannello dei bottoni e faceva parte della classe principale Main02. Questo pannello viene posizionato nel BorderLayout.PAGE_END.
Schematicamente, i due sottopannelli saranno posizionati come segue:

  --title-----------
 |     CENTER       |
 |                  |
 |  SequencePanel   |
 |                  |
 |                  |
 |                  |
  ------------------
 |   PAGE_END       |
 |                  |
 |  CommandPanel    |
 |                  |
  ------------------

L'intero pannello del giocatore viene racchiuso in un bordo di colore bianco con un titolo anch'esso di colore bianco: il titolo sarà il nome del giocatore.

I nuovi files sorgente

In questo capitolo analizzeremo dei nuovi files sorgente che fanno parte del package mastermind.gui e la nuova classe principale Main03 che contiene l'entry-point per testare il codice:

nome file package descrizione
SequencePanel03.java mastermind.gui il nuovo pannello delle sequenze
CommandPanel.java mastermind.gui il pannello dei bottoni di comandi
PlayerPanel.java mastermind.gui il pannello del giocatore
InputListener.java mastermind.gui la interfaccia di input delle sequenze
Main03.java mastermind.app la classe principale della versione 0.3

Caratteristiche del pannello giocatore

Il pannello del giocatore viene usato per gestire le sequenze, sia la solution che le guess: i bottoni del sottopannello dei comandi (il CommandPanel) vengono usati per aggiungere, togliere e cancellare uno o più simboli delle sequenze. Ma quale delle dieci sequenze deve essere aggiornata?
Ebbene, dipende dalla fase di gioco: come descritto in Le regole del gioco nel MasterMind vi sono due fasi:

  • nella prima fase i due giocatori scelgono una sequenza segreta
  • nella seconda fase i due giocatori tentano, a turno, di indovinare la sequenza segreta dell'avversario; il giocatore ha a disposizione vari tentativi fino ad un massimo stabilito dai parametri di gioco

Definiremo pertanto una variabile intera di nome guessIndex che, per convenzione, può assumere i seguenti valori:

  • -1: siamo nella prima fase del gioco, i bottoni di comando aggiornano la solution
  • da ZERO alla lunghezza della array delle guess : siamo nella seconda fase del gioco e il valore della variabile rappresenta l'indice della tabella delle guess; i bottoni di comando aggiornano la guess alla riga indicata da guessIndex
  • 99: la partita è terminata, i bottoni di comando sono disabilitati

Se guessIndex è uguale a 99 e la partita è terminata. Il pannello dei comandi dovrebbe essere abilitato soltanto se il giocatore a cui si riferisce è un giocatore umano e solo se egli è in turno di gioco. Ma non è tutto. Per dare maggiore feedback all'utente, anche i singoli bottoni del pannello dei comandi dovrebbero essere abilitati o meno:

  • se il parametro di gioco n è minore di dieci, per esempio otto, significa che i codici "8" e "9" non sono disponibili per le sequenze; i relativi bottoni dovrebbero essere disabilitati
  • se il parametro di gioco repeat (=consenti la ripetizione dei codici) è uguale a false, quando l'user inserisce un simbolo in sequenza il bottone di comando relativo a quel simbolo dovrebbe essere disabilitato

Queste features devono essere gestite dal pannello del giocatore interagendo tra il sottopannello delle sequenze e quello dei bottoni di comando: a questo proposito, oltre a mantenere il membro dati guessIndex il pannello del giocatore deve avere accesso anche ai parametri di gioco.

Il sottopannello delle sequenze

Ricorderete che quando avevamo scritto il pannello delle sequenze con i bottoni Done e Check (vedi Il pannello delle sequenze) la resa estetica di detti bottoni di comando era insoddisfacente.
In effetti, i bottoni di comando quadrati con il testo Done e Check non si abbinano con le icone circolari delle sequenze e anche il loro colore di fondo (grigio) è il classico pugno nell'occhio. Non solo, anche i bottoni nel pannello comandi della app di test (vedi La classe derivata Main02) erano brutti e poco intuitivi. Che significa la "b" e la "c" sul bottone? In questa versione della app disegneremo bottoni più eleganti e più intuitivi; nelle due immagini seguenti potete vederne il confronto:

PRIMA DOPO

Anche la classe JButton, al pari di JLabel, può essere contenere, oltre al testo da visualizzare anche una icona; abbiamo usato questo espediente per disegnare i simboli delle sequenze e useremo lo stesso sistema per disegnare i bottoni di comando esteticamente più belli. Cerchiamo in Internet le immagini per i bottoni Check/Done, backspace e cancel.
Nella tabella seguente potete vedere quelli che userò io. Le immagini sono in formato PNG e si trovano nella cartella mastermind/gui/images.

I bottoni di comando
backspace cancel done/check

Per creare un bottone circolare con le immagini di cui sopra si usa uno dei costruttori del componente JButton e precisamente il seguente:

JButton​(String text, Icon icon)

Il primo argomento è il testo che deve apparire nel bottone esattamente come già visto per i bottoni di comando nella prima versione. Nel caso dei bottoni di comando di cui sopra, il testo è nullo.
Il secondo argomento al costruttore è un oggetto di tipo Icon che viene creata da un metodo specifico leggendo la immagine da disco.

Nella classe CommandPanel è stato scritto un metodo specifico per creare la icona di un bottone scalando una immagine su disco; Il metodo è il readButtonIcon ed è statico.

// File: CommandPanel.java
public static Icon readButtonIcon( Object obj, String filename, int btnSize ) throws MMException
{
... omissis ...
return icon;
}

Il metodo accetta tre argomenti: un oggetto dal quale ottenere la risorsa immagine, il nome del file che contiene la immagine del bottone in formato PNG e le dimensioni in pixels della icona da restituire. Coloro che hanno letto la prima parte del mio tutorial su Java già conoscono l'argomento; per gli altri o per coloro che vogliono rinfrescare la memoria clikkare sul link seguente: Java Tutorial Parte Prima - I formati grafici.
Per ulteriori informazioni consiglio di cercare informazioni nella Grande Rete ed in special modo sulla preziossima Wikipedia

Il file PNG viene letto come risorsa URL: una caratteristica di Java che consente di astrarre la effettiva locazione fisica di una qualsiasi risorsa: è sufficiente specificarne il URL: nel nostro caso un path relativo su disco.
Una volta determinato il URL, la immagine viene letta in un oggetto di classe BufferedImage e scalata con il metodo getScaledInstance che restituisce la stessa immagine ma di dimensioni ridotte a btnSize che è l'argomento fornito al metodo stesso.
La icona necessaria a costruire il bottone personalizzato viene creata come istanza della classe ImageIcon; infine la istanza viene restituita al chiamante, che in questa versione, è il costruttore di SequencePanel.

Avrete anche notato che il metodo può sollevare una eccezione checked: questo nel caso la risorsa sia indisponibile oppure sono intervenuti errori nella lettura del file immagine. La eccezione viene intercettata nel metodo createPanel dei due pannelli delle sequenze e dei bottoni tuttavia la app non termina: se le immagini non sono disponibili i bottoni vengono comunque costruiti con il testo ed avremo i brutti bottoni che avevamo visto nella versione precedente.

Il sottopannello dei bottoni

Il sottopannello dei bottoni viene gestito dalla classe specializzata CommandPanel ed esso occupa la posizione BorderLayout.PAGE_END nel pannello del giocatore. Schematicamente, è costruito come una griglia di 4x3 (o 6x2) = 12 celle disposte, ovviamente, in un GridLayout:

  -----------------
 | (0) (1) (2) (3) |
 | (4) (5) (6) (7) |
 | (8) (9) (b) (c) | 
  -----------------
 |     RESIGN      |
  -----------------

I 12 bottoni della griglia sono i codici da "0" a "9" che rappresentano i simboli della sequenza più il bottone backspace (codice 10) ed il bottone cancel (codice 11).
Al di fuori della griglia inseriremo il bottone Resign; quest'ultimo sarà un bottone normale, con il testo "Resign" (=abbandona, mi arrendo) dal momento che non ho trovato una icona abbastanza intuitiva per rappresentarlo. Siete ovviamente invitati ad ovviare a questo mio limite: il sorgente di questo piccolo progetto è rilaciato sotto licenza GPL (la licenza open source per eccellenza) e quindi potete modificare questi sorgenti come vi pare e piace purchè rispettiate i termini imposti dalla GPL, primo tra tutti il dovere di rilasciare sotto GPL anche le vostre modifiche.

I componenti grafici del pannello dei bottoni di comando vengono creati nel metodo createPanel della classe CommandPanel.

Creare i bottoni di comando

Il metodo che crea il componente JButton da visualizzare nella GUI è il createButton:

// File CommandPanel.java
public JButton createButton( char ac, ActionListener listener, Icon icon, String text )
{
logger.finer( "createButton() action-command="+ac + ", text="+text );
JButton btn = new JButton( text, icon );
btn.setVerticalTextPosition( SwingConstants.CENTER );
btn.setHorizontalTextPosition( SwingConstants.CENTER );
btn.setForeground( buttonForeground );
btn.setBackground( buttonBackground );
btn.setFont( font );
btn.setActionCommand( Character.toString( ac ));
btn.setBorderPainted( false );
btn.setContentAreaFilled( false );
if ( listener != null ) {
btn.addActionListener( listener );
}
return btn;
}

Il metodo è ampiamente parametrizzato in modo da poter creare qualsiasi bottone. La lista degli argomenti al metodo è piuttosto corposa:

  • ac: questo rappresenta la action-command dell'evento button-clikked: essa è personalizzata poichè non è possibile lasciarla al mero testo visualizzato sul bottone di comando in quanto il testo è variabile in base al tipo di simbolo
  • listener il gestore degli eventi ActionEvent è il pannello del giocatore: poichè quest'ultimo è il contenitore di entrambi i sotto-pannelli ( il pannello dei comandi ed il pannello delle sequenze) è più che logico impostare il pannello contenitore quale gestore degli eventi
  • icon la icona del bottone che può essere lo sfondo grigio chiaro o scuro per il tipo di simbolo NUMBERS e LETTERS oppure il cerchio del colore specifico per il tipo di simboli COLORS
  • text il testo da visualizzare nel bottone che sarà numerico per il tipo di simbolo NUMBERS, alfabetico per il tipo di simbolo LETTERS è nullo per il tipo di simbolo COLORS

Nel metoco createButton vengono impostate altre caratteristiche del componente JButton quali l'allineamento orizzontale e verticale, il font da usare ed il colore di sfondo e primo piano per il testo.

Un altro metodo degno di nota è il createSymbolButton che crea i 10 bottoni di comandi dei simboli da "0" a "9". Questo metodo viene richiamato 10 volte nel metodo createPanel iterando tra i 10 codici dei simboli a disposizione. A questo proposito il lettore avrà notato che se il parametro di gioco n è inferiore a 10 il bottone corrispondente al codice non disponibile non viene creato ma, al suo posto, viene inserito nella griglia un bottone nullo creato dal metodo createNullButton

Il nuovo pannello delle sequenze

Questo componente è già stato implementato nella classe SequencePanel nel capitolo precedente ma in questa nuova versione vogliamo cambiare la resa grafica dei bottoni di comando "Done" e "Check" sostituendo il testo (brutto) con delle icone più intuitive. E' ovvio che dobbiamo modificare il sorgente della classe; operazione banale disponendo di un editor di testi ma ... non vogliamo perdere del tutto il vecchio codice.

Nella sezione Gestire le versioni della app avevo accennato al fatto che tenere traccia delle versioni di un progetto non è una operazione banale e che sono stati creati appositi strumenti, anche molto complessi, per ottenere questo risultato. L'uso di tali strumenti è assolutamente off-topic (=fuori tema) per questo tutorial però vi posso insegnare un trucchetto che unisce la facilità d'uso ad una discreta efficacia.
Uno strumento di gestione del versionamento deve soffisfare almeno i seguenti punti:

  • non deve essere necessario avere più di una copia dello stesso file sorgente; questo è necessario per non dover apportare modifiche dovute alla risoluzione di bugs in due o più files
  • si deve poter cogliere "al volo" il fatto che un metodo di una classe è stato modificato nelle varie versioni
  • si deve poter cogliere "al volo" il fatto che un metodo di una classe è stato aggiunto nelle varie versioni
  • non si devono avere metodi duplicati (che fanno le stesse identiche operazioni) nelle varie versioni
  • deve essere possibile eseguire la applicazione in una qualsiasi versione

L'approccio usato in questo progetto è quello delle versioni incrementali; si tratta di una tecnica piuttosto semplice ed adatta a piccoli progetti scritti da un solo sviluppatore. Siete ovviamente liberi di usare qualsiasi altro approccio desideriate tenendo conto che qualunque tecnica intendiate usare deve soddisfare almeno i criteri elencati poc'anzi.

Scriveremo pertanto una nuova classe per il pannello delle sequenze che chiameremo SequencePanel03. Già il nome ci indica che questa nuova classe è stata introdotta nella versione 0.3 del progetto. Poichè non vogliamo riscrivere i metodi della classe originale (che funzionano perchè li avevamo testati in main02) deriviamo la nuova classe da SequencePanel: in questo modo, la nuova classe eredita tutti i metodi della vecchia versione, non dobbiamo duplicare nemmeno un metodo, nè un membro dati.
Se la nuova classe ha bisogno di nuovi membri dati, li definiamo: questo ci consente di cogliere "al volo" le modifiche apportate ai membri dati del componente.

Per quanto riguarda i metodi, se abbiamo bisogno di un nuovo metodo basta definirlo; anche in questo caso possiamo notare subito che il metodo è nuovo della classe in nuova versione dal momento che è stato definito.
Ma la classe SequencePanel03 non deve aggiungere alcun metodo: deve invece modificare il metodo createPanel in modo da usare le icone per i bottoni speciali anzichè il testo. Ma come distinguiamo un metodo aggiunto da uno modificato nella nuova classe? Facile, con la annotazione @Override!

// File: SequencePanel03.java
@Override
public JPanel createPanel( SequenceGUI.SymbolType symbolType, int iconSize )
{
... omissis ...
}

Quindi le regole di questo approccio al versionamento possono essere riepilogate così:

  • la classe che deve essere modificata deriva dalla vecchia versione; il suo nome è uguale alla classe modificata con un suffisso che indica il numero di versione
  • i metodi che devono essere modificati avranno la annotazione @Override
  • i metodi nuovi non avranno la annotazione

Il pannello del giocatore

Il costruttore

Gli argomenti al costruttore di ogni pannello giocatore sono:

  • id l'identificativo del pannello: convenzionalmente, si imposta come ID=0 il pannello di sinistra e come ID=1 il pannello di destra.
  • title: il titolo del pannello che viene visualizzato nel bordo bianco che incornicia l'intero pannello del giocatore (vedi Introduzione)
  • l: il listener (=gestore) degli eventi di input; questo argomento sarà approfondito in Il listener di input.
  • params i parametri di gioco poichè la visualizzazione dei componenti dipende da essi

Una piccola annotazione merita l'argomento params: benchè i parametri di gioco siano assolutamente necessari solo alla logica della applicazione, il pannello del giocatore ne ha bisogno perchè nella strategia della applicazione abbiamo deciso di dare un forte feedback all'utente già nella grafica. In particolare:

  • il parametro n (=numero di codici a disposizione) viene usato per abilitare solo quei bottoni di comando minori di n
  • il parametro k (= lunghezza della sequenza) viene usato per stabilire quante icone dei simboli visualizzare nella classe SequenceGUI
  • il parametro repeat viene usato per disabilitare il bottone corrispondente ad un simbolo inserito in sequenza se repeat è FALSE
  • il parametro maxTries viene usato per stabilire quante righe di guess saranno visualizzate nel pannello delle sequenze

Il metodo createPanel

Il componente GUI del pannello del giocatore PlayerPanel viene creato col metodo createPanel al quale vanno passati due argomenti: il tipo di simboli e la dimensione delle icone per le sequenze ed i bottoni di comando. Il codice è di facile lettura; unico appunto è che il metodo non crea due o più copie del componente JPanel. Anche se dovesse essere richiamato due o più volte, restituisce sempre e solo il componente creato la prima volta:

// File: PlayerPanel.java
public JPanel createPanel( int iconSize, SequenceGUI.SymbolType symbolType )
{
if ( guiPanel != null ) {
return guiPanel;
}
guiPanel = new JPanel();
... omissis ...
guiPanel.setName( Integer.toString( panelID ));
}

L'ultima istruzione che vedete nel codice sopra esposto merita un commento: il metodo setName è implementato in java.awt.Component da cui la classe JPanel deriva indirettamente. Questo metodo può essere usato per impostare una stringa a piacere in ogni componente GUI; la stringa può essere recuperata dal metodo getName in ogni momento ma, in particolar modo, quando si verifica un evento.
Qualsiasi evento inserito nella coda (vedi Il gestore degli eventi) deriva da EventObject il cui unico compito è quello di memorizzare quale oggetto ha causato l'evento. Il suo unico metodo interessante è il getSource che ritorna, appunto, l'oggetto sorgente cioè l'oggetto che ha accodato (o generato) l'evento. Per mezzo di questo metodo e del getName del componente è possibile quindi conoscere quale tra tutti i componenti dello stesso tipo ha causato l'evento:

public void actionPerformed( ActionEvent evt )
{
Object source = evt.getSource();
assert source instanceof Component : "ERROR in actionPerformed: source is not a Component class";
String componentName = ((Component) source).getName();
...

Il listener degli eventi

Il pannello del giocatore deve rispondere agli eventi button-clikked generati dai bottoni di comando e per questo motivo il pannello deve implementare l'interfaccia ActionListener:

// File: PlayerPanel.java
public class PlayerPanel implements ActionListener
{
... omisis ...
public void actionPerformed( ActionEvent evt )
{
String ac = evt.getActionCommand();
logger.finer( "actionPerfoned() panelID: " + getID() + ", action: " + ac);
switch( ac.charAt(0)) {
... cases ...
}
}
}

Il metodo actionPerformed ottiene la commandAction specifica per ogni bottone e per ogni case richiama un metodo privato specifico. Il blocco switch analizza solo il primo carattere della commandAction dal momento che per ogni bottone il primo carattere della action-command è unico:

  • case D (=Done): richiama il metodo privato done
  • case C (=Check): richiama il metodo privato check
  • case R (=Resign): richiama il metodo privato resign
  • case 0 .. 9 (i codici dei 10 simboli): richiama il metodo privato addSymbol
  • case b (=backspace): richiama il metodo privato backspace
  • case c (=cancel): richiama il metodo privato cancel

Il metodo addSymbol

Il metodo addSymbol viene richiamato quando l'user clikka uno dei bottoni dei simboli nel pannello comandi; i bottoni dei simboli dipendono dal tipo di simbolo desiderato dallo user (possono essere i numeri da "0" a "9", le lettere da "A" a "J" oppure uno dei dieci colori previsti dalla applicazione, vedi La classe Params) ma la loro actionCommand è sempre la stessa: un codice da "0" a "9".
Il simbolo deve ovviamente essere aggiunto alla sequenza ma ... quale? La solution oppure la guess? La risposta a questa domanda sta nella fase del gioco: se siamo nella prima fase cioè quella della scelta della solution, il simbolo và aggiunto alla solution mentre se siamo nella seconda fase và aggiunto alla guess.
La fase di gioco viene determinata dal membro dati guessIndex che stabilisce anche la riga delle guess che deve essere aggiornata:

// File: PlayerPanel.java
private void addSymbol( char ch )
{
boolean r = false;
if ( guessIndex < 0 ) {
r = addSolutionSymbol( ch );
}
else {
r = addGuessSymbol( ch );
}
... omissis ...
}

I metodi addSolutionSymbol e addGuessSymbol ritornano un booleano che vale true se la sequenza è completa. Nel caso la sequenza non fosse completa, il metodo addSymbol del pannello giocatore disabilita il bottone relativo al simbolo appena aggiunto alla sequenza ma solo se nei parametri di gioco il flag di ripetizione è repeat=false.
Questo perchè se la ripetizione dei simboli non è ammessa, è opportuno disabilitare il bottone del simbolo appena aggiunto. Viceversa, se la sequenza è completa, oltre alla abilitazione del bottone Done/Check è necessario riabilitare tutti i bottoni dei simboli dal momento che l'user può scegliere una nuova sequenza semplicemente aggiungendo altri simboli.

Il metodo backspace

Il metodo privato backspace funziona più o meno allo stesso modo ma a parti invertite: il membro dati guessIndex stabilisce ancora la fase di gioco: se negativo siamo nella prima fase, quella della solution; in caso contrario siamo nella seconda fase, quella delle ipotesi di soluzione.
La differenza rispetto alla addSymbol in cui il bottone relativo al simbolo aggiunto alla sequenza veniva disabilitato, in questo metodo dobbiamo abilitare il bottone in argomento dal momento che il simbolo è stato rimosso dalla sequenza e può essere aggiunto nuovamente.
Infine, la rimozione di un simbolo dalla sequenza presuppone sempre che la sequenza non sia completa, semmai lo fosse stata, e quindi è necessario disabilitare il bottone Done o Check a seconda della fase del gioco:

// File: PlayerPanel.java
private void backspace()
{
logger.fine( "backspace() panelID: " + getID());
int ch = -1;
if ( guessIndex < 0 ) {
ch = getSequencePanel().popSolutionChar();
setDoneEnabled( false );
}
else {
ch = getSequencePanel().popGuessChar( guessIndex );
setCheckEnabled( guessIndex, false );
}
// riabilita il bottone 'ch' se disabilitato
if ( ch >= '0' && ch <= '9' ) {
int idx = ch - '0';
enableButton( idx, true );
}
}

Il metodo cancel

Il metodo cancel cancella l'intera sequenza della solution se guessIndex è negativo, oppure della guess nel caso il membro dati anzidetto sia positivo o pari a zero.
Si deve anche disabilitare il bottone Done o Check a seconda della fase di gioco e, infine, abilitare tutti i bottoni dei simboli nel sottopannello dei comandi dal momento che la sequenza non contiene più alcun simbolo:

I metodi done, check e resign

I metodi visti finora gestivano le sequenze nel sottopannello delle sequenze ma non avevano un significato particolare: tutte le azioni che riguardano la scelta di una sequenza possono essere gestite dal pannello del giocatore.
Ma la pressione dei bottoni Done, Check e Resign hanno un significato particolare: indicano che il giocatore ha scelto una sequenza e questa deve essere considerata come un vero e proprio input che deve essere gestito dalla logica del gioco; il pannello del giocatore è insufficiente a gestire l'input della sequenza. A questo proposito và ricordato che anche il resign può essere considerata una sequenza di guess: si tratta di una sequenza di resa.

Ma noi non abbiamo ancora implementato la logica del gioco e, quindi, dove và a finire l'input? La risposta più immediata è: ma nel Main, è ovvio! E' nel programma principale che c'è la logica del gioco, no? Ebbene, la risposta è NO, non sarà nel programma principale che manterremo la logica del gioco ma in una classe specializzata, scritta proprio per gestire la logica del gioco e solo quella.

Allora, adesso dove lo mandiamo visto che la classe deputata a gestirlo non è stata ancora scritta? Questa è la classica situazione nella quale ci serve una interfaccia: non è necessario sapere adesso a quale classe invieremo l'input del giocatore, è sufficiente sapere che qualsiasi classe và bene, oggi è la classe Main, domani sarà la classe MasterMind e dopodomani sarà una altra ancora. L'importante è che qualsiasi classe venga utilizzata per gestire l'input dello user definisca alcuni fondamentali metodi che vengono richiamati dal pannello giocatore quando l'input è disponible.
Ma questa è proprio la caratteristica delle interfaccie!

Definiamo pertanto la interfaccia InputListener la cui implementazione dovrà essere passata al costruttore del pannello giocatori in modo che i metodi privati del pannello possano passare l'input ad una classe specifica che definiremo in seguito:

// File: PlayerPanel.java
private void done()
{
String solution = sequencePanel.getSolution();
if ( inputListener != null ) {
inputListener.haveSolution( getID(), solution, true );
}
}
private void check()
{
String guess = sequencePanel.getGuess();
if ( inputListener != null ) {
inputListener.haveGuess( getID(), guess, true );
}
}
private void resign()
{
if ( inputListener != null ) {
inputListener.resign( getID(), true );
}
}

Avrete sicuramente notato che ai metodi della classe che implementa la interfaccia InputListener abbiamo passato la ID del pannello del giocatore come argomento. Il perchè lo scopriremo nella prossima sezione.

Il listener di input

La interfaccia che gestisce l'input dell'utente è la seguente:

public interface InputListener
{
public void haveSolution( int id, Sequence solution, boolean gui );
public void haveGuess( int id, Sequence guess, boolean gui );
public void resign( int id, boolean gui );

Gli argomenti sono i seguenti:

  • id l'identificativo del giocatore o del pannello GUI che invia l'input
  • solution la sequenza segreta
  • guess la ipotesi di soluzione
  • gui TRUE se l'input proviene dalla GUI cioè da un pannello giocatore: in questo caso id si riferisce alla ID del pannello FALSE se l'input proviene da un oggetto giocatore, cioè derivato da Player: in questo caso id si riferisce al turno del giocatore

A che cosa serve l'argomento gui? In fondo, ogni giocatore ha il suo pannello giocatore con la propria ID: ZERO per il pannello di sinitra e UNO per quello di destra. Ebbene, non è così e l'argomento booleano gui è assolutamente indispensabile. Facciamo un esempio concreto:

Supponiamo che un umano il cui nome giocatore è "Lukas" giochi una partita contro il computer il cui nome di default è "PlayerAI1". Questa è la situazione nella prima fase, quando i giocatori scelgono la solution:

Il giocatore umano usa il pannello comandi del proprio pannello-giocatore per scegliere la solution e, subito dopo questa operazione, ha inizio la seconda fase del gioco nella quale i giocatori tentano di indovinare la solution dell'avversario. E' piuttosto intuitivo che le guess del computer dovrebbero essere inserite nel pannello del giocatore umano, subito sotto alla solution del giocatore umano in modo che sia visivamente chiaro come il computer sta procedendo.
Allo stesso modo, le guess del giocatore umano dovrebbero essere inserite nel pannello del giocatore I.A. e che, quindi, i bottoni abilitati per l'umano siano quelli del pannello con ID=1 (il pannello del computer). Per intenderci, la situazione è la seguente:

Appare quindi piuttosto chiaro che le guess del giocatore umano vengono registrate nella actionPerformed del pannello destro, quello con ID=1, quello associato al computer. Per questo motivo è necessario passare al listener di input, oltre alla ID del pannello/giocatore anche un flag che indica se l'input avviene dalla GUI oppure no.
Il turno del giocatore che ha inviato l'input deve essere determinato dal server di gioco in base alla seguente elaborazione:

  • se gui è FALSE, l'argomento id è il turno di gioco
  • se gui è TRUE:
    • se siamo nella prima fase del gioco, il turno del giocatore equivale a id
    • se siamo mella seconda fase del gioco, il turno del giocatore è l'inverso di id

La classe principale

Ora scriviamo la classe principale Main03 per valutare la interfaccia del pannello giocatore. Creiamo tre pannelli giocatore con diversi parametri di gioco e usiamo il pannello comandi per editare la solution e le guess. Ogni pannello giocatore gestisce le sequenze in modo autonomo: quando si arriva alla fine delle possibili guess, il pannello si resetta e si può ricominciare daccapo. I parametri usati nei tre pannelli sono i seguenti:

Parametro Panel_0 Panel_1 Panel_2
codex NUMBERS LETTERS COLORS
n 8 10 10
k 3 3 4
repeat false true false

Questo è lo screenshot di ciò che vogliamo ottenere e verificare:

Dal test eseguito possiamo verificare che:

  • nel Panel_0 i bottoni di comando "8" e "9" sono correttamente disabiltiati poichè il parametro di gioco n è uguale a 8: i codici disponibili vanno da 0 a 7
  • nel Panel_1 i bottoni "F" e "G" sono abilitati anche se questi due simboli sono già presenti nella sequenza parziale; poichè il parametro di gioco repeat è impostato a TRUE, i bottoni devono essere abilitati
  • di contro, il parametro di gioco repeat è impostato a FALSE nel Panel_2, e quindi i bottoni dei colori arancio, rosso e verde sono correttamente disabilitati poichè già inseriti nella sequenza parziale

Quindi tutto ok? Non proprio: quando implementeremo la logica del gioco ci accorgeremo che c'è un bug (letteralmente significa "insetto", "bacherozzo" ma in informatica indica un errore logico) nella classe PlayerPanel che consente al giocatore umano di inserire simboli ripetuti anche in presenza del parametro di gioco repeat=false. Sfrutteremo questo bug (ovviamente per scopi disonesti) nella sezione Le backdoors

Il codice della classe principale è di facile lettura; solo un paio di appunti. Il primo appunto riguarda il layout manager usato per posizionare i tre pannelli: invece del BerderLayout usato nel capitolo precedente, è stato scelto il BoxLayout che è uno dei più versatili manager di posizionamento dei componenti. Per i più curiosi, sul portale ufficiale Oracle™è disponibile una guida in lingua inglese che ne riassume le caratteristiche: How to Use BoxLayout.
Useremo alcune delle caratteristiche peculiari del layout manager BoxLayout quando scriveremo la barra di stato della applicazione (vedi La barra di stato).

Il secondo commento da fare è che la app principale implementa la interfaccia InputListener in modo che il pannello dei giocatori richiami il metodo preposto alla pressione dei bottoni di comando Done, Check e Resign

public class Main03 extends Main implements InputListener
{
... omissis ...

Degno di nota è anche lo spezzone di codice nel metodo haveGuess che avanza automaticamente alla riga successiva ogni volta che una guess viene confrontata con la solution (bottone Check). Quando non ci sono più righe di guess disponibili, il pannello del giocatore si resetta e si può ricominciare daccapo.
Questo si ottiene ricreando il content-pane principale istanziano dei nuovi pannelli giocatore ed impostando il nuovo content-pane nel frame utilizzando il metodo JFrame.setContentPane.

// File Main03.java
public void haveGuess( int id, String guess, boolean gui )
{
... omissis ...
// arrivati alla fine delle guess, visualizza una dialog-box
// e istanzia un nuovo pannello giocatore vuoto per ricominciare daccapo
if ( nextRow >= playerPanels[id].getMaxGuess()) {
JOptionPane.showMessageDialog​( null, // parentComponent
"raggiunta la fine delle ipotesi", // message
"INFORMATION", // title
JOptionPane.INFORMATION_MESSAGE ); // messageType
try {
JPanel main = createContentPane();
frame.setContentPane( main );
frame.pack();
}
catch( ... omissis ...
}

Infine, come ultimo commento al codice della classe principale possiamo notare che in questa app di test l'avanzamento alla riga di guess successiva avviene richiamando il metodo PlayerPanel.nextGuessRow. Ma questo metodo non è quello canonico da usare nella logica del gioco in cui, invece, l'indice della riga delle guess dovrebbe essere impostata attraverso il metodo setGuessRow che viene pilotato dal server di gioco.
Il compilatore non può evitare di farci usare il metodo non-canonico ma può comunque aiutarci a prevenirne l'uso nelle versioni future inserendo la annotazione @Deprecated subito prima di definire il metodo:

@Deprecated
public int nextGuessRow()
{
... omissis ...
}

Con la annotazione @Deprecated il compilatore emette un warning ogni volta che in qualche sorgente usiamo il metodo "deprecato" (=sconsigliato):

mastermind>javac mastermind\game\Main03.java
Note: mastermind\game\Main03.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.

Il ridimensionamento del frame

Avete provato a giocare un pò con questa versione della applicazione? Se lo avete fatto avrete notato che il layout manager BoxLayout gestisce lo spazio destinato ai tre pannelli delle sequenze in un modo diverso dal BorderLayout della versione precedente (vedi Il ridimensionamento del frame): mentre il BorderLayout ridimensiona solo il pannello centrale il layout usato in questa versione (il BoxLayout) ridimeniona i tre pannelli in modo uniforme aumentando e diminuendo le dimensioni di tutti i tre pannelli.

Ulteriore documentazione

La documentazione completa del package descritto in questo capitolo può essere visualizzata clikkando il seguente link che riporta alla documentazione Javadoc della versione 0.3 del progetto: The MasterMind Project Version 0.3

Argomento precedente - Argomento successivo - Indice Generale