|
Java Tutorial - Parte 2 0.1
Un tutorial per esempi
|
In questa versione implementiamo due giocatori: il giocatore umano, che interagisce con la GUI ed il giocatore di TEST. Sarà anche implementata la logica del gioco in modo da poter effettivamente giocare una partita anche se contro un giocatore stupido. Nella figura seguente potete vedere uno screenshot di cosa ci aspettiamo di ottenere:
Come anticipato, il giocatore di test è un giocatore stupido che genera una sequenza segreta (la solution) casuale e la trasmette al server. Nel proprio turno di gioco, il giocatore di test restituisce la propria guess generandola casualmente: una strategia di gioco pessima, ovviamente. Non ultimo, la GUI visualizza la soluzione del giocatore di test nel pannello delle sequenze dell'avversario. Quindi l'umano vede la soluzione del suo avversario: più facile di così ...
Per i più curiosi premetto che la applicazione che trovate in questi sorgenti non funziona: non vi allarmate, si corregge in pochi secondi (vedi Event Dispatching Thread) L'ho lasciata in questo stato per farvi comprendere meglio un aspetto importante della programmazione guidata dagli eventi. In questa versione introduciamo il nuovo package mastermin.player che riunisce le classi che descrivono i vari tipi di giocatore e diverse nuove classi del package mastermind.game che riunisce la classi che gestiscono la logica del gioco e le classi principali.
Nella seguente tabella trovate l'elenco dei nuovi files sorgente:
| nome file | package | descrizione |
|---|---|---|
| StatusBar.java | mastermind.gui | il pannello della barra di stato |
| Player.java | mastermind.player | la classe base astratta del giocatore |
| PlayerLocal.java | mastermind.player | la classe base del giocatore locale |
| PlayerHuman.java | mastermind.player | la classe del giocatore di tipo umano |
| PlayerTest.java | mastermind.player | la classe del giocatore di test |
| PlayerManager.java | mastermind.player | il manager dei giocatori |
| Application.java | mastermind.game | la interfaccia della applicazione |
| MasterMind.java | mastermind.game | la logica del gioco (classe base) |
| MasterMindServer.java | mastermind.game | il server di gioco |
| NotifyListener.java | mastermind.game | la interfaccia del gestore delle notifiche |
| Main04.java | mastermind.app | la classe principale della versione 0.4 |
In questa versione abbiamo il layout definitivo della applicazione: vi saranno due pannelli giocatore, il pannello sinistro, quello del player in turno ZERO, ed un pannello destro, quello del player in turno UNO. Sotto ai due pannelli dei giocatori ci sarà una statusbar (=barra di stato).
--HUMAN0---------- --TEST1----------- | | | | | | | | | | | | | PlayerPanel | | PlayerPanel | | | | | | BorderLayout. | | BorderLayout. | | LINE_START | | LINE_END | | | | | | | | | | | | | ------------------ ------------------ ---------------------------------------- | StatusBar: BorderLayout.PAGE_END | ----------------------------------------
Il pannello del giocatore è stato descritto in Versione 0.3: il pannello del giocatore mentre la statusbar sarà descritta in questo capitolo e precisamente nella sezione La barra di stato.
Le classi dei giocatori risiedono nel package mastermind.player; tutte le classi giocatore derivano dalla classe base astratta Player, in modo che sia possibile riferirsi a qualsiasi tipo di giocatore con una sola variabile di tipo Player. La gerarchia delle classi giocatore è la seguente:
Player
|
--------------------------
| |
PlayerRemote PlayerLocal
|
-----------------------------
| | |
PlayerHuman PlayerTest PlayerAILa classe base Player è una classe astratta; il giocatore deve fornire l'input al server di gioco e questo input consiste nella solution e nelle guess. Tipi di giocatore diversi forniscono l'input in modo diverso; l'umano interagisce con la GUI mentre il giocatore di test elabora le sequenze con il codice Java. Pertanto i due metodi astratti per eccellenza sono:
getSolution: che ritorna la soluzione getGuess: che ritorna la ipotesi di soluzione dell'avversarioLa classe base definisce praticamente tutti i membri dati di un giocatore: sono dichiarati protetti, in modo che le classi derivate possano accedervi:
InputListener listener: il listener degli eventi di input; essi sono solo due, la scelta di una solution e di una guess String nickname: il nome del giocatore che è opzionale, solo per il giocatore umano viene effettivamente usato un nome Params params: i parametri di gioco necessari ai giocatori A.I. e di test per generare una solution che rispetti le regole String solution: la sequenza segreta scelta dal giocatore, questo dato benchè esistente non è realmente usato poichè la solution viene inviata dal giocatore al server di gioco il quale le possiede entrambe int turn: il turno di gioco, 0 oppure 1.Nella classe base viene anche definito un enumerato che stabilisce un codice mnemonico per le costanti che definiscono i tipi di giocatore:
Il costruttore di un oggetto giocatore accetta un solo parametro: il turno di gioco di questo giocatore che deve essere ZERO oppure UNO:
Oltre ai due metodi che restituiscono la solution e la guess, gli altri metodi astratti sono:
getType(): che restituisce uno degli enumerati visti sopra isLocal(): che restituisce true se il giocatore è locale isRemote(): che restituisce true se il giocatore è remoto; in realtà questo metodo non è astratto; la sua implementazione può essere definita nella classe base poichè basta ritornare la negazione di isLocal isConnected(): che restituisce true se il giocatore è connesso al server di giocoOltre ai metodi astratti scritti più sopra, il player generico dichiara anche molti metodi astratti che riguardano le notifiche degli eventi del gioco. Il server invia una notifica per ogni evento scaturito dal gioco; le notifiche vengono inviate dal server alla GUI (la classe base MasterMind) e ad entrmbi i giocatori. I nomi di tutti questi metodi astratti cominciano per notify e sono dichiarati nella interfaccia NotifyListener:
notifyNewGame: la notifica viene inviata dal server quando una nuova partita sta per cominciare; l'argomento al metodo è l'oggetto Params che contiene i parametri di gioco notifyHaveSolution: un giocatore ha inviato la propria solution al server di gioco; gli argomenti al metodo sono il turno del giocatore e la sequenza della soluzione notifyStartGame: la prima fase di gioco si è conclusa e ora si passa alla seconda fase in cui i giocatori cercano di indovinare la sequenza segreta, l'argomento al metodo è il turno di gioco notifyHaveGuess: un giocatore ha inviato la propria guess al server di gioco; il metodo viene richiamato con due argomenti: il turno del giocatore e la sequenza di guess notifyHaveResults: il server notifica al giocatore i risultati del confronto tra la guess e la solution dell'avversario notifySwapTurn: dopo la elaborazione dei risultati di un confronto, il turno di gioco cambia; ora è il turno dell'avversario notifyEndGame: la partita è terminata, il metodo viene richiamato con due argomenti: il turno del vincitore ed un booleano che indica se si tratta di resign notifySolution: dopo che la partita si è conclusa, il server notifica ai due giocatori la solution dell'avversarioNon tutti i tipi di giocatore sono interessati alle notifiche provenienti dal server di gioco: per esempio la classe PlayerHuman non è interessata ad alcuna notifica dal momento che il giocatore umano vede l'avanzare del gioco attraverso la GUI. La classe PlayerTest, di contro, è interessata a due eventi
Anche il giocatote A.I. è interessato ai medesimi eventi a cui è interessato il giocatore di test, più un altro evento: il haveResults che notifica i risultati del confronto: poichè è su questi risultati che si basa la strategia di gioco della A.I., questo evento non può essere ignorato dal giocatore A.I.
Nel membro dati nickname della classe astratta Player è possibile inserire un nomignolo per il giocatore; tuttavia, questo feature è previsto solo per il giocatore umano e, comunque, è opzionale anche per esso.
Il nome del giocatore viene restituito dal metodo getNickname ma, se il membro dati è null, la stringa restituita dal metodo suddetto è composta in modo automatico. Essa è formata dalla stringa "Player" seguita dal tipo di giocatore e dal turno di gioco. Per esempio, il giocatore di test in turno UNO sarà mominato: PlayerTEST1.
La classe PlayerLocal è la classe base dei tre giocatori locali: il giocatore umano, il computer ed il giocatore di test. Si tratta ancora di una classe base astratta dal momento che i metodi specializzati che restituiscono la solution e la guess devono essere implementati nelle derivate. Tuttavia, la classe base dei giocatori locali implemanta alcuni metodi astratti comuni, tra i quali:
Un giocatore locale ritorna sempre true per il metodo isLocal e anche per il metodo isConnected dal momento che un giocatore locale è sempre connesso al server di gioco; il che è ovvio essendo locale.
Oltre ai due metodi suesposti, la classe base del giocatore locale implementa tutti i metodi astratti il cui nome comincia per notifyXxxx e che vengono richiamati dal game-server nel momento in cui viene rilevato un evento di gioco. La implementazione di questi metodi nella classe base del giocatore locale si limita al log dell'evento con livello CONFIG con due eccezioni:
notifyNewGame nel quale vengono passati come argomento i parametri di gioco, che vengono memorizzati nella classe Player dal momento che devono essere disponibili per generare le sequenze casuali. Ancora in questo metodo, la classe PlayerLocal richiama il metodo astratto getSolution che, se restituisce una sequenza, quest'ultima viene inviata al server di gioco notifySwapTurn nel quale la classe PlayerLocal richiama il metodo getGuess: anche in questo caso, se quest'ultimo metodo ritorna una sequenza essa sarà inviata al server di gioco come guess Perchè viene passato come argomento al metodo un oggetto di classe InputListener (vedi Il listener di input)?
E' ovvio che il listener dell'input del giocatore è sempre la classe MasterMindServer ma ci possono essere diverse istanze di questa classe; dal momento che i giocatori possono giocare anche più di una partita, la classe del server può essere istanziata più volte. E l'evento newGame è il posto giusto per passare tale riferimento alla classe Player.
La classe PlayerHuman, derivata da PlayerLocal, descrive un giocatore umano. Essa sovrascrive i due metodi astratti principali, quelli che differiscono sempre in base al tipo di giocatore: il metodo getSolution che restituisce la solution del giocatore ed il metodo getGuess che restituisce la sequenza della ipotesi di soluzione dell'avversario:
I due metodi restituiscono entrambi null dal momento che l'umano interagisce con la GUI per scegliere le sequenze e quindi sarà un evento ActionListener quello che fornirà l'input al game-server.
Anche il metodo astratto getType dovrà essere implementato:
Infine, il costruttore del giocatore umano accetta come argomento, oltre al turno di gioco, una stringa che rappresenta il nome del giocatore; questo è opzionale poichè il nome del giocatore può essere determinato dalla classe Player.
La implementazione del giocaore di TEST è davvero facile: si tratta di un giocatore stupido, già accennato in Il giocatore di test, che genera casualmente sia la solution che le guess: quindi i due metodi astratti da implementare getSolution e getGuess sono praticamente identici.
La classe che descrive il giocatore remoto è la PlayerRemote ma non farà parte del package mastermind.player bensì del package mastermind.net il quale implementerà le classi necessarie alla connessione remota. Su questo argomento si sono scatenate le più infiammate diatribe (cosidette flames) nei gruppi di discussione tra programmatori (quanto siamo sciocchi, vero?) La domanda è questa: in quale package dovrebbe essere collocata la classe PlayerRemote? Ci sono due candidati:
mastermind.player che raggruppa le classi giocatore mastermind.net che raggruppa le classi dedicate alla connessione remotaE' un bel dilemma, non c'è che dire, ed entrambi i packages sono candidati più che logici per la classe del giocatore remoto. In questi casi di dubbio, la linea generale su cui dovrebbe ricadere la scelta è quella della minor dipendenza dei packages (escluse le libreria Java, ovviamente):
common non ha alcuna dipendenza gui dipende solo da common player dipende solo da common game dipende da common, da gui e da player Se noi inserissimo PlayerRemote nel package mastermind.player allora introduciamo una ulteriore dipendenza al package player: la dipendenza dal package che implementa la connessione remota. Inoltre, la classe PlayerRemote userà in modo estensivo le classi nel package mastermind.net e quindi il suo posto ideale è proprio in quel package dal momento che non sarà necessario importare le classi di quel package.
Un ulteriore vantaggio nell'inserire PlayerRemote nel package mastermind.net stà nel fatto che la classe del giocatore remoto avrebbe accesso ai metodi e membri dati delle classi di quello stesso package senza bisogno di definire tali membri pubblici: come ricorderete, l'attributo di default dei membri di una classe è il package private che dà accesso ai membri di una classe a tutte le altre classi dello stesso package.
Il giocatore remoto verrà implementato nel capitolo Versione 0.8: il protocollo Mastermind.
I giocatori di Mastermind vengono gestiti da una classe diversa da quella che si occupa della logica del gioco. Una partita di Mastermind viene gestita dalla classe MasterMindServer: essa gestisce una sola partita. Tuttavia, gli stessi due giocatori possono giocare due o più partite e pertanto i giocatori non dovrebbero essere gestiti dalla classe che implementa la logica del gioco.
Quale classe dovrebbe gestire i giocatori? Ed inoltre, abbiamo bisogno di una classe specializzata per gestire i giocatori? Il server di gioco ha la necessità di accedere agli oggetti giocatore ma poichè non abbiamo ancora creato la classe specifica come possiamo scrivere la logica del gioco senza il manager dei giocatori?
Anche in questo caso, una interfaccia è la soluzione migliore.
Nel file sorgente PlayerManager.java viene definita la interfaccia che gestisce i giocatori: in questo modo, possiamo scrivere il server di gioco passando al suo costruttore la classe che implementa l'interfaccia ma senza preoccuparci di quale essa sia mentre scriviamo il server di gioco.
Per la cronoca, la classe che funge da manager dei giocatori è la applicazione principale, la classe Main.
I metodi che gestiscono i giocatori sono i seguenti:
getPlayer: ritorna il giocatore che ha il proprio turno fornito nell'argomento; il turno deve essere ZERO o UNO, l'oggetto ritornato non può essere null getOpponent: due metodi sovraccaricati che ritornano l'avversario del giocatore che ha il turno oppure dell'oggetto giocatore fornito nell'argomento getHumanPlayer: ritorna il giocatore di tipo umano e che può ritornare null se non vi sono giocatori umani; una condizione piuttosto strana ma possibile, come vedremo in seguito getHumanOpponent: ritorna l'avversario del giocatore di tipo umano; può ritornare null se non vi sono giocatori umani getRemotePlayer: ritorna il giocatore di tipo remoto; può ritornare null se non vi sono giocatori remoti getRemoteOpponent(): ritorna l'avversario del giocatore di tipo remoto; può ritornare null se non vi sono giocatori remoti areBothHumans: ritorna TRUE se entrambi i giocatori sono di tipo umano; questo particolare metodo potrebbe sembrare assurdo ma come vedremo nella sezione Umano contro umano risolverà una situazione particolareIl pannello principale contiene, oltre ai due pannelli dei giocatori, un pannello speciale che chiameremo barra di stato (statusbar, d'ora in avanti). Essa viene gestita dal file sorgente StatusBar.java
La barra di stato fornisce informazioni di vario tipo al giocatore umano attraverso la GUI. Partendo da sinistra, sulla statusbar vengono visualizzate:
Tutti questi componenti sono di classe JLabel che, come abbiamo già visto, è un componente GUI neutro: fornisce solo informazioni ma non è selezionabile nè "clikkabile". Per quanto riguarda il posizionamento dei componenti nella status bar, essi sono disposti in un layout manager di classe BoxLayout con orientamento orizzontale. Lo schema di posizionamento è il seguente:
--------------------------------------------------------------------------------------- |message-field (width=250 pixels) |<--horizontal-glue--> | win-count |RIGID| turn-flags | --------------------------------------------------------------------------------------
Il campo dei messaggi ha una larghezza fissa di 250 pixels mentre i due contatori delle vittorie ed i flags del turno hanno la larghezza decisa dalla GUI. Tra il campo dei messaggi ed i contatori delle vittorie è stato inserito il cosidetto glue orizzontale: si tratta di un componente fittizio che può restringersi od allargarsi a seconda delle esigenze.
Nel caso il frame venga ridimensionato dall'utente il componente fittizio glue si allarga quando il frame si allarga e si restringe quando il frame si rimpicciolisce. In questo modo le dimensioni degli altri componenti vengono mantenute costanti. Il glue orizzontale si crea con il metodo statico Box.createHorizontalGlue().
Tra i due componenti del conteggio delle vittorie è stata inserita una rigid area di 45 pixels: anche in questo caso si tratta di un componente fittizio largo 45 pixels usato esclusivamente per distanziare i componenti del conteggio delle vittorie dai componenti che indicano il flag del turno.
La rigid area si crea con il metodo statico Box.createRigidArea().
Il codice che crea la statusbar è contenuto nel suo costruttore dal momento che la classe StatusBar deriva da JPanel. Per quanto riguarda la implementazione dei metodi non mi sembra ci sia qualche commento da fare: direi che è facilmente comprensibile.
La statusbar viene creata nel metodo createGamePanel della classe MasterMind, che analizzeremo in dettaglio nella prossima sezione: la statusbar è un membro dati statico della classe MasterMind poichè, una volta creata la prima statusbar, essa rimane in essere fino al termine della applicazione e quindi viene riutilizzata ogni volta che si deve creare il pannello di una nuova partita. Il riutilizzo della statusbar è necessario per mantenere i contatori delle vittorie.
Per logica del gioco si intende l'insieme delle regole che fanno avanzare il gioco stesso e con esso la applicazione. Prima di iniziare ad analizzare il codice precisiamo che la logica del gioco viene mantenuta in una classe specializzata, sganciata dal main. Questo ci porta alcuni vantaggi tra i quali:
main avrebbe complicato il codice di questi componenti Un altro aspetto su cui il lettore deve concentrarsi è che questa applicazione è una applicazione GUI e che, come già accennato in Introduzione alla GUI il paradigma di programmazione è totalmente diverso da una applicazione CLI la quale procede come un flusso dall'inizio alla fine.
In una GUI il paradigma è il cosidetto event driven programming (=programmazione guidata dagli eventi) e questo aspetto deve applicarsi non solo alla GUI ed al sistema operativo ma anche alle applicazioni.
Prendiamo come esempio l'inizio di una nuova partita che sarà creata dal Main in questo modo:
il gioco entra nella prima fase che convenzionalmente indicheremo con il turno uguale a -1; in questa fase il gioco accetta le solution dai giocatori. Ogni giocatore invia la propria soluzione al gioco richiamando il metodo haveSolution. Quando entrambe le soluzioni saranno disponibili, il server di gioco invierà una notifica, che chiameremo startGame:
Per esempio, la classe PlayerHuman non reagirà alla notifica startGame dal momento che l'umano vede la nuova situazione nella GUI ma l'oggetto di tipo PlayerRemote inoltrerà la notifica sulla connessione di rete in modo che anche la GUI dell'avversario, che stà su una macchina remota, possa essere informato della nuova situazione.
Quindi è nel metodo haveSolution che il server di gioco controlla se la partita vera e propria deve iniziare o meno. Gli altri metodi del server di gioco procedono allo stesso modo: quando viene rilevato un evento, per esempio, la soluzione da parte di un giocatore, il server di gioco invierà la notifica endGame alla GUI ed ai giocatori.
Sicuramente, gli oggetti interessati alle notifiche da parte del server sono la GUI, che deve aggiornare la finestra della applicazione, ed i giocatori ma può esserci anche qualcosa d'altro?
Non necessariamente almeno non in questo frangente specifico però in altre applicazioni potrebbe essere ma come possiamo prevederlo? Beh, non possiamo però possiamo fare qualcosa per non dover riscrivere codice inutilmente.
Dobbiamo prevedere una interfaccia comune per tutte le classi interessate alle notifiche, il NotifyListener. Qualunque classe implementi questa interfaccia può ricevere le notifiche dal server, anche in futuro, basta scrivere la nuova classe.
Poichè ho deciso di organizzare il gioco in un ambiente client/server dobbiamo avere due diverse istanze della classe MasterMind (vedi Il diagramma di flusso):
Poichè la visualizzazione degli eventi sulla GUI compete ad entrambe le classi, scriveremo una classe base comune dalla quale deriveremo le due classi specializzate. La classe base MasterMind è un classe base astratta e dichiara due metodi astratti:
run: questo metodo viene richiamato quando viene fatta partire una nuova partita isServer: questo metodo determina se la classe è il server di gioco o menoLa classe base MasterMind è quella che gestisce la visualizzazione degli eventi di gioco nella GUI ed contiene per lo più i metodi della interfaccia NotifyListener. Poichè la classe base gestisce la GUI, essa sarà responsabile anche della abilitazione / disabilitazione del pannello comandi del singolo giocatore: solo se il giocatore umano è in turno si abiliteranno i bottoni di comando nel suo pannello giocatore.
La classe MasterMind viene istanziata con un solo argomento: la classe che implementa la interfaccia Application per mezzo della quale la classe masterMind ha accesso a tutte le informazioni della applicazione principale. I metodi della interfaccia Application sono i seguenti:
getFrame(): ritorna il frame principale getGame(): ritorna la partita in corso getPlayerManager(): ritorna il player manager getProperties(): ritorna le proprietà della applicazione getVersion(): ritorna la versione della applicazione newGame(): crea una nuova partita terminate(): termina la applicazione getConnection(): ritorna l'oggetto connessione remota; questo metodo verrà introdotto nella interfaccia quando scriveremo le classi che gestiscono la connessione remotaLa classe che implementa questa interfaccia è la classe principale, cioè il Main; essa implementa anche il player manager, come abbiamo visto in Il manager dei giocatori.
Dopo la creazione, viene richiamato il metodo run il quale innesca la logica del gioco; dalle proprietà della applicazione questo metodo calcola i parametri di gioco e poi invia alla GUI la notifica newGame passando i parametri di gioco come argomento. Il metodo termina qui, non esegue niente altro: tutto il resto della applicazione avverrà in risposta agli eventi di gioco, non viene affatto pilotato dal metodo run.
La classe MasterMindServer definisce un metodo per ogni evento del gioco. Il corso degli eventi e dei metodi ad essi dedicati può essere sintetizzato come segue:
run il server legge i parametri di gioco, imposta il turno iniziale ed invia la notifica newGame alla GUI ed ai giocatori haveSolution riceve dai giocatori le solution; se entrambe le soluzioni sono disponibili la partita ha inizio richiamando il metodo startGame il quale innesca il primo scambio del turno swapTurn è quello che scambia il turno di gioco e che viene richiamato anche da startGame: si tratta di uno scambio particolare in cui il turno viene impostato a firstTurn haveGuess del server; haveGuess, il server confronta la sequenza inviata con la solution dell'avversario ed ottiene i risultati del confronto; questi risultati vengono elaborati nel metodo haveResults endGame swapTurn Appare quindi evidente che il gioco procede per mezzo dei due metodi di input delle sequenze haveSolution ed haveGuess e poichè solo la classe MasterMindServer implementa effettivamente questi metodi vi è un solo server in una sessione di gioco.
Il codice del server di gioco è di facile lettura e ampiamente commentato. Alcune scelte potrebbero sorprendervi e quindi meritano un breve commento:
Nei due metodi di input haveSolution ed haveGuess è stato inserito un controllo sulla validità delle sequenze inviate dai giocatori. Se la sequenza non è valida, il server notifica l'errore sia alla GUI che al giocatore che ha inviato la sequenza errata:
Vi sembrerà strano introdurre tale controllo dal momento che il PlayerPanel impedisce al giocatore umano di inserire simboli non consentiti in ragione ai parametri di gioco. D'altra parte, il giocatore di test e la A.I. non invierebbero mai sequenze non valide: sono delle macchine, non barano mai.
Ebbene, vi è un bug nel codice del PlayerPanel che consente al giocatore umano di "barare" inserendo nella sequenza simboli ripetuti anche se il parametro di gioco repeat=false. Sfrutteremo questo bug in futuro, quindi lo lasciamo.
Vi è una seconda possibilità di inviare sequenze invalide al server di gioco: il giocatore remoto. Non sappiamo chi o cosa si nasconde dietro la connesione: potrebbe essere un client fittizio scritto ad hoc proprio per mettere in crisi il server. Meglio controllare le sequenze prima di procedere: MAI e dico proprio MAI fidarsi ciecamente nè del codice che abbiamo scritto (potrebbe contenere un errore logico) nè di chi si connette alla nostra applicazione da remoto.
Vi sembra poco probabile che qualcuno possa perdere tempo per scrivere una app di questo genere? Ebbene, essa esiste e l'ho scritto io stesso proprio per mettere in crisi il server di gioco e quindi testare come esso reagisce: se scrivete una app che accetta connessioni remote il mio consiglio è quello di scrivere voi stessi una app malevola in modo da prevenire possibili attacchi (vedi Una applicazione ostile).
Nel metodo haveGuess è stato inserito un ulteriore controllo per evitare di mettere in crisi il server: se dovesse arrivare una guess fuori turno essa sarà semplicemente ignorata dal server:
I metodi del server sono tutti di facile lettura ma il metodo haveResults merita qualche analisi considerato che è il più complesso da leggere.
A prima vista, non dovrebbe essere un metodo particolarmente difficile, anzi. Si tratta di controllare se una guess è vincente oppure no dai risultati del confronto. Una implementazione piuttosto semplicistica e senza fronzoli, potrebbe apparire così:
In realtà, però, non è così semplice perchè si è deciso di non dare alcun vantaggio al giocatore in primo turno. Mi spiego meglio: se il giocatore in turno ZERO (il primo turno della prima partita è sempre lo ZERO) indovina la soluzione al quarto tentativo non è giusto assegnargli la vittoria perchè il suo avversario, il giocatore in turno UNO, ha fatto solo tre tentativi.
Per correttezza è necessario scambiare nuovamente il turno e dare la possibilità al giocatore UNO di eseguire ancora il suo ultimo tentativo: se dovesse indovinare la solution dell'avversario allora entrambi sono pervenuti alla soluzione nello stesso numero di tentativi: la partita è patta.
Pertanto, in caso di soluzione vincente da parte del giocatore in primo turno (che chiamiamo turno ZERO) il metodo haveResults lo imposta come vincitore potenziale, memorizzando il suo turno di gioco nel membro dati potentialWinner. Dopo l'ultimo scambio turno avremo le seguenti situazioni:
Il primo turno di gioco viene stabilito dal metodo swapFirstTurn che, come dice il nome, scambia il primo turno di gioco. Vi sembrerà strano dover scambiare il primo turno di gioco dal momento che esso potrebbe comunque essere sempre ZERO; questo non determina affatto un vantaggio per il giocatore ZERO come già descritto in Il metodo haveResults.
Ciononostante, ho previsto anche la possibilità di scambiare il primo turno di gioco nel caso di partite multiple con gli stessi giocatori: se la proprietà "swapturn" è TRUE, il primo turno di gioco viene scambiato (vedi La classe MMProperties).
Il primo turno di gioco di ogni partita è controllato dal membro dati statico firstTurn che è inizializzato a -1 (=nessun turno). Se il valore di questo membro dati è negativo, allora il primo turno è lo ZERO: siamo nella prima partita tra i due giocatori. In caso contrario, il primo turno di gioco viene scambiato con il costrutto:
La istruzione di cui sopra mette la variabile firstTurn in XOR (OR esclusivo) con la costante 1: questo provoca lo scambio del valore poichè l'operatore XOR scambia tutti i bits di una variabile: tutti gli "ZERO" diventano "UNO" e viceversa. Ovviamente, ci sono molti altri modi per ottenere lo stesso risultato. Per esempio, il seguente:
Il metodo swapTurn è quello che il server richiama dopo il metodo haveResults cioè dopo che la guess è stata confrontata con la solution dell'avversario e la stessa non è risultata vincente. Lo scambio del turno viene eseguito con il costrutto già visto in precedenza e cioè mettendo la variabile turn in XOR con 1:
Il server di gioco invia la notifica del cambio turno ai giocatori e alla GUI.
I metodi drawn ed endGame vengono richiamati quando una partita termina. Nel primo caso se la partita termina in parità e nel secondo caso se vi è un vincitore. Vi è da osservare, tuttavia, che il metodo draw richiama comunque il metodo endGame ma passando ad esso gli argomenti specifici del caso di partita patta.
Quando una partita termina, il server di gioco imposta la variabile turn al valore 2 (=partita terminata) in modo da ignorare qualsiasi eventuale input successivo che potrebbe arrivare: difficile che questo succeda in una partita locale dal momento che la applicazione è single-threaded (=con un singolo thread) ma in una partita remota questo è possibile.
Il compito del server di gioco quando una partita termina è quello di notificare i giocatori e la GUI di questo evento e notificare le solution sia ai giocatori che alla GUI. La notifica delle solution è un atto dovuto per chi perde: supponiamo di giocare contro la A.I. e quest'ultima vince; è ovvio che vogliamo vedere quale era la sua sequenza segreta considerato che non siamo riusciti ad indovinarla.
La classe MasterMindClient, derivata da MasterMind, è quella che viene istanziata nella macchina remota che funge da client. In particolare, il client implementa un metodo run che non esegue alcuna effettiva azione, non deve nemmeno creare la GUI: a questo ci pensa la classe base MasterMind alla ricezione della notifica newGame.
Il client sovrascrive i due metodi di input haveGuess e haveSolution: il compito del client è quello di inoltrare l'input del giocatore sulla connessione remota. Come descritto in Il giocatore remoto anche nel caso di questa classe abbiamo il dubbio di dove poterla collocare, cioè in quale package. Le scelte sono due:
mastermind.game dove sono raggruppate le classi del gioco mastermind.net dove sono raggruppate le classi che gestiscono le connessioni remoteAnche in questo caso, la scelta ricade sul secondo package, quello che raggruppa le classi che gestiscono la connessione di rete dal momento che la classe del client Mastermind ha senso solo in presenza di una connessione remota.
Ora che abbiamo almeno due giocatori e la logica del gioco, possiamo implementare la classe principale della applicazione. Come spiegato in Gestire le versioni della app la classe principale di questa versione, che chiameremo Main04, deriva da Main la quale già contiene il entry-point della applicazione.
La classe base contiene anche tutti i data members necessari alla applicazione, in questa classe specializzata definiremo solo i membri dati specifici e strettamente necessari al funzionamento di questa versione:
game: l'oggetto che contiene la partita in corso, derivato da MasterMind: poichè non abbiamo ancora una connessione remota, sarà di classe MasterMindServer players una array di due elementi di classe Player: un elemento per ognuno dei due giocatori license la array statica di stringhe che costituiscono lo splash-screen (vedi Lo splash-screen)Per quanto riguarda i metodi, notiamo dal sorgente che questa classe ne implementa molti: in effetti, è necessario implememntare tutti i metodi della interfaccia PlayerManager (vedi Il manager dei giocatori) e della interfaccia Application (vedi La interfaccia Application).
Oltre a questi, è necessario anche implementare il metodo createContentPane che ritorna il pannello da inserire nel frame principale: il metodo in argomento è astratto e quindi obbligatorio. Infine, vogliamo modificare il metodo overrideDefaultProperties impostando come proprietà "opponentType" (=tipo di avversario) la stringa "TEST" in modo da non doverla specificare sulla command-line.
A questo proposito vi suggerisco un ulteriore trucchetto per capire al volo se un metodo di una classe derivata è nuovo, definito per la prima volta nella classe derivata oppure se modifica il comportamento di un metodo definito in una classe base: il trucco sta nel usare la annotazione @Override:
Esempio:
Nello spezzone di codice suesposto si capisce al volo che il metodo overrideDefaultProperties è modificato in quanto esso già esiste nella classe base mentre il metodo createContentPane è nuovo, mai definito nella classe base Main (in effetti è un metodo astratto).
Ma cosa visualizziamo nel pannello principale, il cosidetto content panel? La risposta a questa domanda stà nel tipo di applicazione: prendiamo ad esempio un editor di testi (ma sarebbe la stessa cosa per un editor di immagini, suoni, video, etc). Allo startup, la applicazione si presenta con un pannello vuoto nel quale è possibile tramite i comandi di menù, creare un nuovo documento (o immagine, video, etc) o aprire un documento salvato in precedenza.
Ma la applicazione Mastermind è diversa; quando lo user la manda in esecuzione vuole giocare a mastermind. Non ci sono partite salvate in precedenza e non è necessario clikkare un bottone di comando per giocare una nuova partita: se l'user ha mandato in esecuzione la app è ovvio che intende giocare una nuova partita!
Potremmo in un certo senso far partire subito una partita ma con quali parametri di gioco? Abbiamo previsto una struttura appositamente creata per essi ed abbiamo anche previsto diverse proprietà della applicazione che l'user può cambiare.
Abbiamo sempre la possibilità di cambiare le proprietà sulla command-line, ricordate? (no, non ricordate? allora rileggete La classe MMProperties). Per esempio, se volessimo giocare una partita con sequenze lunghe 4 codici e con i simboli colorati possiamo farlo in questo modo:
>java -ea mastermind.game.Main k=4 codex=COLORS
ma non è molto bello e, sopratutto, non è molto professionale. Sarebbe molto meglio far partire la applicazione con un pannello che visualizza i parametri di gioco personalizzabili ed offra all'user la possibilità di modificarli. A questo porremo rimedio in Versione 0.7: Il pannello delle proprietà ma per il momento ci accontentiamo di quello che in gergo informatico viene chiamato uno splash screen (non credo esista una traduzione in lingua italiana).
Le applicazione medio/grandi hanno normalmente bisogno di un certo tempo per inizializzarsi poichè il sistema operativo deve caricare in memoria una grande quantità di codice e dati e questa operazione potrebbe durare diverse decine di secondi.
Per evitare che l'user abbia l'impressione che il sistema si sia congelato, le applicazioni complesse visualizzano sullo schermo una piccola finestra di inizializzazione che non ha bisogno di grandi risorse e quindi viene visualizzata immediatamente dal sistema operativo. Questa finestra viene chiamato splash-screen.
In realtà, la applicazione Mastermind è piccola ed il pannello principale contenente le componenti della partita verrebbe visualizzato immediatamente anche su macchine non particolarmente potenti. Cionondimeno, realizzeremo una sorta di splash-screen anche per Mastermind ma, al contrario di quelli delle app di grandi dimensioni, quello che scriveremo avrà bisogno del click di un bottone di comando per far partire la prima partita (altrimenti non lo vedremmo nemmeno, lo splash-screen). Inoltre, tramite lo splash-screen diamo la possibilità allo user di modificare i parametri di gioco.
Ecco uno screenshot di ciò che realizzeremo:
Il posizionamento dei componenti nel pannello dello splash-screen segue il layout BoxLayout con orientamente verticale. I componenti sono disposti in questo modo:
------------------ | | | immagine | | | ------------------ | array di | | stringhe | | | ------------------ | "play" "options" | ------------------
Le stringhe visualizzate nello splash-screen sono memorizzate in una array statica all'inizio del listato sorgente Main: si tratta di un messaggio standard che ogni applicazione a sorgenti aperti dovrebbe in qualche modo visualizzare anche se non necessariamente allo startup: il messaggio potrebbe essere inserito, per esempio, nella dialog-box About (=Informazioni su ...) della applicazione.
La pressione dei bottoni di comando "Play" e "Options" viene elaborata dal consueto metodo che implementa la interfaccia ActionListener.
Oltre al metodo pubblico statico main che rappresenta l'entry-point della app, la classe specializzata Main04 dovrà definire i seguenti metodi per la esecuzione della applicazione:
getVersion(): metodo astratto della interfaccia Application che non è stato intenzionalmente definito nella classe base poichè ogni classe derivata deve implementarlo a seconda della sua versione actionPerformed: che deve reagire al comando "Play" connettendo i giocatori al server di gioco ed istanziando una nuova partita connectPlayers(): che deve provvedere alla connessione dei giocatori; ovviamente per i giocatori locali non saranno necessarie operazioni di sorta in quanto locali createPlayer: che crea e restituisce un oggetto derivato da Player in base al tipo di giocatore scelto dal userLa pressione del bottone di comando "Play" richiama il metodo connectPlayers il quale ritorna un booleano che vale TRUE se entrambi i giocatori sono connessi. Al momento, essendoci solo giocatori locali è ovvio che i due giocatori sono sempre connessi e quindi viene creata una nuova partita col metodo newGame.
La pressione del bottone "Options" darà all'utente la possibilità di modificare le proprietà della applicazione tra le quali i parametri di gioco. Questa funzionalità sarà implementata nel capitolo Versione 0.7: Il pannello delle proprietà. Uno sguardo al metodo createPlayers è doveroso:
Il metodo crea una istanza della classe specialistica a seconda del tipo di giocatore ma, per i tipi "AI" e "REMOTE" non abbiamo ancora scritto la classe che li implementa; nel caso in esame, il metodo solleva una eccezione ma nelle versioni successive, quando le classi specializzate saranno implementate, sarà proprio il metodo createPlayer che verrà sovrascritto.
Ho già accennato al fatto che la programmazione GUI non ha un aspetto per così dire procedurale con un inizio ed una fine (vedi Introduzione alla GUI). Il flusso del programma in una GUI assomiglia a delle isole in cui il codice viene eseguito all'interno di un metodo in risposta ad un evento.
Dopo che è stata eseguita la azione propedeutica all'evento rilevato, il metodo che ha gestito l'evento rientra ed il controllo passa al dispatcher degli eventi: in un certo senso, il nostro codice non viene più eseguito.
Ci sono però dei casi in cui il metodo che ha gestito l'evento non può rientrare e cedere così il controllo al dispatcher se non dopo che l'user ha fornito una qualche sorta di input. Questo è il caso della fine della partita: quando una partita termina, il server di gioco invia la notifica alla GUI richiamando il metodo notifyEndGame il quale visualizza una finestra di dialogo in cui viene evidenziato il nome del vincitore, se esiste un vincitore.
Cosa dovrebbe fare il metodo MasterMind.notifyEndGame dopo che il messaggio è stato visualizzato? Il programma ha due opzioni:
Il codice non può prendere una decisione in autonomia e per questo motivo dà allo user la possibilità di scelta visualizzando due bottoni di comando nella finestra del messaggio. Le finestre che abbiamo usato finora sono sempre derivate da JPanel che è un componente GUI che non richiede per forza un input da parte dell'utente nemmeno se il pannello contenesse componenti come i JButton.
Qualunque metodo che crea il pannello ed i suoi componenti rientra subito dopo la creazione e la visualizzazione ad eccezione di un particolare tipo di pannello (o di finestra, che dir si voglia): le finestre di dialogo le quali hanno alcune caratteristica particolari:
Il linguaggio Java definisce la classe JOptionPane, contenuta nel package javax.swing, per gestire le finestre di dialogo modali. Benchè sia possibile creare istanze della classe JOptionPane, per il loro uso più semplice è preferibile usare uno dei tanti metodi statici che la classe mette a disposizione.
Nella sua forma più semplice, una finestra di dialogo modale può essere creata con questa semplice istruzione:
che visualizza una finestra di dialogo modale con il bordo, il titolo "Message" il bottone di chiusura finestra in alto a destra ed il bottone di comando "OK".
Essendo una finestra modale, le "altre istruzioni del metodo" NON vengono eseguite fino a che l'user non chiude la finestra modale clikkando "OK" oppure clikkando il bottone di chiusura finestra in alto a destra. Questo è il risultato:
Nella nostra applicazione, però, usiamo un costrutto molto più elaborato:
che visualizza una finestra di dialogo decisamente più accattivante e resttiuisce TRUE se l'user ha clikkato il bottone "Play again":
Il metodo statico showOptionDialog può accettare diversi argomenti che consentono di personalizzare l'aspetto della finestra di dialogo. Partiamo dal primo argomento e analizziamoli tutti:
parentComponent: definisce quale è il componente padre della finestra di dialogo. Normalmente si deve passare come argomento il frame della applicazione in modo che la finestra di dialogo sia centrata nel frame. Se si passa il valore null, la finestra di dialogo sarà centrata nello schermo message: il messaggio da visualizzare nella finestra di dialogo. Può essere un qualsiasi Object o una array di Object: il testo visualizzato viene estratto con il metodo toString di questo argomento title: il titolo della finestra che viene visualizzato nel bordo optionType: i bottoni di comando da visualizzare nella finestra di dialogo. Le costanti da passare al metodo sono: DEFAULT_OPTION, YES_NO_OPTION, YES_NO_CANCEL_OPTION e OK_CANCEL_OPTION. icon: tramite questo argomento è possibile passare al metodo una icona personalizzata che viene visualizzata al posto di quella predefinita in messageType. Per visualizzare la icona predefinita a seconda del tipo di messaggio basta passare il valore null per questo argomento options: una array di Object; normalmente viene passata una array di stringhe che rappresentano il testo dei bottoni di comando visualizzati nella finestra di dialogo. Nel nostro caso viene passata una array di due stringhe: "Play again" ed "Exit" che corrispondono ai due bottoni YES e NO del argomento optionType. defaultButton: il testo del bottone selezionato di default inizialmente. Nel caso della nostra finestra di dialogo questo argomento punta alla stringa "Play again" e pertanto viene selezionato quel bottone quando la finestra di dialogo viene mostrataIl metodo statico showOptionDialog ritorna un valore intero che deve essere confrontato con le costanti mnemoniche corrispondenti ai bottoni di comando visualizzati. Nel caso del Mastermind, la pressione del bottone "Play again" corrisponde al codice YES_OPTION. Altri possibili valori sono: NO_OPTION, CANCEL_OPTION, OK_OPTION e CLOSED_OPTION.
Come anticipato in I nuovi files sorgente la applicazione non funziona: se provate a giocare una partita contro il giocatore di test vi accorgerete che la GUI sembra non voler avanzare nei tentativi di soluzione inviati dal giocatore di test:
Eppure, dai messaggi del logger osserviamo che il contatore dei tentativi avanza regolarmente:
lug 08, 2025 5:15:12 PM mastermind.game.MasterMind notifySwapTurn INFO: notifySwapTurn - turn: 0, tryCounter=5
Il codice sembra a posto: la prima volta che il giocatore di test invia una guess non vincente (dubito che il giocatore di test possa mai inviare una guess vincente), viene richiamato il metodo swapTurn del server di gioco il quale:
haveGuess con una nuova guess non vincente swapTurn Sicuramente avrete capito che il metodo swapTurn viene richiamato ricorsivamente ed alla fine delle operazioni descritte siamo ancora nello swapTurn iniziale che non è mai rientrato.
Ma questo non va bene. E' necessario che la prima chiamata al metodo swapTurn rientri ed aggiorni le variabili interne. Questo è il "lato oscuro" della programmazione guidata dagli eventi: i metodi che rispondono agli eventi devono durare poco, eseguire i compiti assegnati e rientrare al più presto possibile.
Dobbiamo stare attenti a non richiamare ricorsivamente gli stessi metodi nella gestione di un evento; ma il flusso delle operazioni è corretto: il giocatore di test DEVE rispondere alla notifica swapTurn inviando la guess.
Abbiamo due soluzioni: la prima è quella di elaborare la guess in un thread separato, chiamiamolo secondario, in modo che il thread principale, quello che esegue la logica del gioco continua per conto suo ed il metodo swapTurn rientra sicuramente.
Lo svantaggio di questa soluzione è che i due threads separati eseguono in parallelo e non possiamo sapere se il thread separato elabora ed invia la guess del giocatore di test prima, dopo o durante la esecuzione del thread principale. Il rischio è che la guess dal giocatore di test arrivi al thread principale prima della conclusione del metodo swapTurn, il che ci riporta al problema iniziale.
Ho già accennato al fatto che una applicazione GUI Java viene eseguita in due threads diversi:
main Tutti gli eventi che scaturiscono dalla applicazione vengono "accodati" in una coda degli eventi e prelevati dal EDT uno alla volta ed elaborati: per ogni evento il dispatcher individua un metodo che si è registrato come gestore di quello specifico evento. Lo abbiamo già affrontato:
Il metodo addActionListener registra questo oggetto come gestore del evento button-clicked per il bottone di comando creato. Per ripassare l'argomento rileggete Il gestore degli eventi. Possiamo sfruttare la caratteristica del EDT per risolvere elegantemente ed in modo assolutamente sicuro il problema di cui sopra.
Aprite il file sorgente PlayerLocal.java ed osservate che il metodo notifySwapTurn ottiene una guess se il turno attuale è uguale al turno del giocatore locale. Se una guess viene restituita (questo accade sempre per i giocatori TEST ed AI) provvede ad inoltrarla al server di gioco richiamando il metodo sendGuess:
Per sistemare questo bug della applicazione è sufficiente togliere i caratteri di commento alle righe del sorgente del metodo sendGuess:
Quello che abbiamo fatto con quel codice è "accodare" l'invio della guess da parte del giocatore locale nella coda degli eventi: quando il EDT rientrerà dal metodo swapTurn del server di gioco, preleverà questo evento e lo gestirà richiamando il metodo run della classe anonima che implementa Runnable.
E' il metodo statico SwingUtilities.invokeLater che accoda un evento nel EDT: esso accetta un solo argomento, un oggetto che implementa la interfaccia Runnable. Quest'ultima possiede un solo metodo astratto; il metodo run che è quello da implementare per eseguire la azione che vogliamo accodare nella coda degli eventi.
Infine, un breve commento al metodo notifyHaveSolution della classe MasterMind, quella che implementa la GUI: il metodo viene richiamato dal server di gioco quando uno dei giocatori inoltra la propria solution: essendo una sequenza segreta, essa non dovrebbe essere elaborata in modi particolari e, soprattutto, non dovrebbe essere visualizzata nel pannello del giocatore ma ci sono alcune eccezioni:
La variabile displayedSolution rappresenta la stringa della solution visibile nel pannello del giocatore. Essa dipende dal tipo di giocatore:
Vi è però un caso particolare: se i due giocatori sono entrambi umani. Questa è una situazione che ben difficilmente si potrà verificare dal momento che due giocatori umani non dovrebbero esistere sulla stessa macchina: molto meglio giocare una partita remota in cui ogni giocatore ha a disposizione la propria personale istanza della applicazione.
Ciononostante, in linea teorica, è possibile eseguire la applicazione con due giocatori umani:
>java -ea mastermind.game.Main opponentType=HUMAN playerName=Pippo opponentName=Topolino
Il problema in questa circostanza è che la solution di entrambi i giocatori è visibile sullo schermo:
Ovviamente sarebbe assurdo giocare una partita in questo contesto per ovvie ragioni ma questa circostanza non è poi così assurda perchè in una famiglia potrebbe esserci a disposizione solo un computer condiviso tra due o più componenti del nucleo familiare.
E allora perchè non dare la possibilità di giocare sullo stesso computer? In fondo la soluzione a questo problema è davvero semplice: è sufficente mascherare le due solution nel caso entrambi i giocatori sono umani:
La documentazione completa del package descritto in questo capitolo può essere visualizzata clikkando il seguente link che riporta alla documentazione Javadoc della versione 0.4 del progetto: The MasterMind Project Version 0.4
Argomento precedente - Argomento successivo - Indice Generale