1.2 Hello World!
2.2.1 Obiettivi
1. Si vuole avere una classe PokerHand che permetta di gestire un gruppo di car-te (generalmencar-te 5, parametro del costruttore) provenienti da un mazzo specifico (indicato in fase di costruzione degli oggetti PokerHand). Gli oggetti PokerHand forniscono un metodo getRank
()per determinare il punteggio di una mano (in caso di dubbio, le regole sono riassunte qui:
https://en.wikipedia.org/wiki/List_of_poker_hands). I possibili punteggi sono:
public enum HandRank { HIGH_CARD,
ONE_PAIR, TWO_PAIR, THREE_OF_A_KIND, STRAIGHT, FLUSH, FULL_HOUSE, FOUR_OF_A_KIND, STRAIGHT_FLUSH }
2. Definire una nuova classe principale (nel package it
.unimi.di.prog2.poker) con la quale sperimentare l’uso degli oggetti PokerHand
3. Gli oggetti PokerHand espongono il gruppo di carte di cui sono composti solo
tra-mite un iteratore (Utilizzare il pattern Iterator, descritto nel paragrafo 3.6 del libro
di testo, sfruttando l’interfaccia Iterable della libreria standard)
4. L’implementazione del metodo getRank() rischia di cadere nell’anti-pattern “Switch Statement” (vedi pag. 47 del libro di testo). Per evitarlo, è possibile organizzare la valutazione del punteggio secondo un pattern “Chain-of-responsibility”: si definisce un’interfaccia ChainedHandEvaluator che espone un metodo che, dato un oggetto PokerHand ne calcola il punteggio (HandRank) corrispondente; per ogni tipologia di punteggio che si vuole valutare, occorrerà implementare un sotto-tipo appropriato di ChainedHandEvaluator. Ciascun sotto-tipo di ChainedHandEvaluator conosce anche il “prossimo” valutatore (comunicato col proprio costruttore): ciò permette di costruire una catena di valutatori. Si può perciò iniziare dal valutatore del punteggio più alto (STRAIGHT_FLUSH) che avrà come prossimo valutatore quello di FOUR_OF_A_KIND, ecc. La logica di valutazione sarà: se il valutatore riconosce lo schema del “proprio” punteggio, restituisce il valore opportuno (p.es. se il valutatore del tris trova 3 carte dello stesso valore nella PokerHand restituisce THREE_OF_A
⌋_KIND, altrimenti richiama il prossimo valutatore, probabilmente il valutatore di doppie coppie). Non è necessario realizzare tutti i valutatori: tre a scelta sono sufficienti.
5. Gli oggetti PokerHand devono implementare l’interfaccia Comparable, ordinando le PokerHand secondo il valore restituito da getRank
(). Non c’è bisogno di definire l’ordinamento fra mani con lo stesso punteggio.
2.3 Soluzioni
L’esercizio prevede l’aggiunta di alcune classi base per la gestione del gioco del poker sfruttando le classi che sono già presenti nel package ca
.mcgill.cs.stg.solitaire.c⌋ards
.
Tra queste riconosciamo:
Suit fornisce il range di valori possibili per i semi di una carta Rank fornisce il range di valori possibili per i valori di una carta
Card fornisce l’astrazione di una carta. Essendo il costruttore privato, per ottenere la istanza di una carta vengono forniti alcuni metodi statici tra cui ad esempio il metodo get(Rank, Suit) (si veda il capitolo 4.8 del libro per una spiegazione delle motivazioni di questa scelta)
Deck fornisce l’astrazione di un mazzo di carte.
CardStack fornisce una struttura dati di servizio usata internamente da Deck (non verrà usata in questo esercizio)
Visto che viene richiesto che le classi prodotte dovranno far parte del package it
.un⌋imi.di.prog2.poker
.
Si crea il package (vd. Fig.
2.1):• si selezione nella area progetto a sinistra la voce src/main/java
2.3 Soluzioni
Figura 2.1: Menu per creare un package
• menu contestuale (tasto destro)
•
New Package1. Scomponiamo nei vari punti:
a) Si vuole avere una classePokerHand
Ci si posiziona sul package precedentemente creato e (menu contestuale o ctrl+ N si crea la nuova classe.
b) che permetta di gestire un gruppo di carte
Dentro alla classe PokerHand ci dovrà quindi essere un attributo capace di contenere diverse carte, ad esempio: private
List<Card> cards;
c) (generalmente 5, parametro del costruttore) provenienti da un mazzo specifico (indi-cato in fase di costruzione degli oggettiPokerHand)
Il costruttore di
PokerHand
deve avere due parametri: un numero di carte (int) e il mazzo da cui prenderle (Deck
). Il suo compito è quindi di istanziare unArrayL
⌋ist
di carte (non è necessario specificare che sono 5 visto cheList
è una struttura dinamica), e per il numero di carte voluto prendere (draw
) una carta dal mazzo e aggiungerla alla lista interna.public PokerHand(int numCards, Deck deck) { cards = new ArrayList<>();
for (int i = 0; i < numCards; i++) { cards.add(deck.draw());
} }
Guardando meglio il metodo
draw
della classeDeck
, ci accorgiamo però che il suo contratto prevede che possa essere chiamato solo se il mazzo non è vuoto. Modifichia-mo quindi la condizione di permanenza delfor nel seguente modo:i
<numCards
&& !deck.isEmpty()
d) Gli oggettiPokerHandforniscono un metodogetRank()per determinare il punteg-gio di una mano. I possibili punteggi sono: [. . . ]
Prima di tutto, definisco il nuovo tipo enum come suggerito. Si procede come per la creazione di una classe, ma poi si indica la scelta opportuna.
Poi aggiungiamo alla classe
PokerHand
il metodo richiesto (in questo momento con implementazione fittizia): publicHandRank
getRank() { return null; } 2. Definire una nuova classe principale (nel package it.unimi.di.prog2.poker) con laquale sperimentare l’uso degli oggetti PokerHand
Aggiungiamo una classe
Main
che al suo interno definisce il metodomain
per poter provare a istanziare la classePokerHand
finora creata.public class Main {
public static void main(String[] args) { Deck deck = new Deck();
PokerHand pokerHand = new PokerHand(5, deck);
System.out.println(pokerHand);
System.out.println((pokerHand.getRank()));
} }
Al fine di avere una stampa significativa, ridefiniamo dentro alla classe
PokerHand
anche il metodotoString()
. Per fare questo, è possibile (al posto di scrivere tutto a mano) usare lo shortcut ctrl+ O che fa comparire una finestra in cui sono presenti i possibili metodi che possiamo ridefinire (fare @Override).Il codice da inserire può essere ad esempio:
@Override
public String toString() {
StringBuilder result = new StringBuilder("Le mie carte sono:\n");
for (Card card : cards) {
result.append(card.toString());
result.append('\n');
}return result.toString();
}
Eseguendo a questo punto il
main
della classeMain
, otteniamo un output simile (le carte effettive dipenderanno dalla mescolanza) a:> Task :Main.main() Le mie carte sono:
NINE of CLUBS EIGHT of SPADES NINE of DIAMONDS JACK of HEARTS ACE of CLUBS
Una soluzione alternativa poteva essere quella di rimappare il problema di dare una rappresentazione testuale sul metodo
toString
definito daList
:2.3 Soluzioni
@Override
public String toString() {
StringBuilder result = new StringBuilder("Le mie carte sono:");
result.append(cards.toString());
return result.toString();
}
Ottenendo un output simile a:
> Task :Main.main()
Le mie carte sono:[ACE of CLUBS, FIVE of DIAMONDS, QUEEN of CLUBS, KING of HEARTS, KING of SPADES]
⤶
⤷
3. Gli oggetti PokerHand espongono il gruppo di carte di cui sono composti solo tramite un iteratore (Utilizzare il pattern Iterator, descritto nel paragrafo 3.6 del libro di testo, sfruttando l’interfacciaIterabledella libreria standard)
Lo scopo di questa richiesta è fornire un accesso “protetto” alla lista delle carte della mano. Per prima cosa aggiungiamo la parte di codice che (come richiesto da Java) dice che la classe
PokerHand
aderisce alla interfacciaIterable
. Trasformiamo quindi la riga di intestazione della classe in: public class PokerHand implementsIterable<Card>
{...}
Questo fa sì che la classe
PokerHand
debba implementare (infatti ci verrà segnalato con una bisciolina rossa questa attuale mancanza) il metodoiterator()
. Il metodo deve restituire uniteratore
sulle carte della mano. Possiamo rimappare questo compito direttamente sul metodoiterator()
del nostro attributocards
:@Override
public Iterator<Card> iterator() { return cards.iterator(); }
4. L’implementazione del metodogetRank()rischia di cadere nell’anti-pattern “Switch State-ment” (vedi pag. 47 del libro di testo). Per evitarlo, è possibile organizzare la valutazione del punteggio secondo un pattern Chain of Responsibility
Riportiamo uno schema classico del pattern in Fig.2.2).
a) si definisce un’interfacciaChainedHandEvaluatorche espone un metodo che, dato un oggettoPokerHand ne calcola il punteggio (HandRank) corrispondente;
Creiamo quindi (sempre nel solito package) la interfaccia richiesta con il seguente codice (il nome del metodo è lasciato alla vostra fantasia, qui abbiamo scelto
eval
⌋uate
):public interface ChainedHandEvaluator { HandRank evaluate(PokerHand pokerHand);
}
b) per ogni tipologia di punteggio che si vuole valutare, occorrerà implementare un sotto-tipo appropriato di ChainedHandEvaluator. Ciascun sotto-tipo di Chain⌋ edHandEvaluator conosce anche il “prossimo” valutatore (comunicato col proprio costruttore): ciò permette di costruire una catena di valutatori.
c) Si può perciò iniziare dal valutatore del punteggio più alto (STRAIGHT_FLUSH) che avrà come prossimo valutatore quello diFOUR_OF_A_KIND, ecc. La logica di valuta-zione sarà: se il valutatore riconosce lo schema del “proprio” punteggio, restituisce il
Client + request()
<<interface>>
Handler + handleRequest()
ConcreteHandler +
+
ConcreteHandler(Handler) handleRequest()
handler.handleRequest()
if (can_handle) { do_it } else { next->handleRequest(); }
0..1
1 0..1
handler next
Figura 2.2: Chain of Responsability pattern
valore opportuno (p.es. se il valutatore del tris trova 3 carte dello stesso valore nella PokerHand restituisce THREE_OF_A_KIND, altrimenti richiama il prossimo valuta-tore, probabilmente il valutatore di doppie coppie). Non è necessario realizzare tutti i valutatori: tre a scelta sono sufficienti.
Scegliamo di implementare le classi per riconoscere le seguenti configurazioni
FLUSH
,HIGH_CARD
eONE_PAIR
.Trattiamo per primo il caso
HIGH_CARD
che essendo il caso terminale che non può fallire può essere in realtà semplificato rispetto al modello classico. In particolare, sfruttando la conoscenza che è l’ultimo nodo della catena, si può evitare di aggiungere l’attributonext
:public class HighCardEvaluator implements ChainedHandEvaluator {
@Override
public HandRank evaluate(PokerHand pokerHand) { return HandRank.HIGH_CARD;
} }
Più significativo è invece il caso del riconoscitore di
FLUSH
. Qui possiamo trovare l’attributonext
settato nel costruttore che indica a chi dovremo passare la mano qualora non fosse unFLUSH
. La logica del riconoscimento della configurazioneFLUSH
(che prevede che tutte le carte della mano abbiano lo stesso seme) è quello di iterare sulle carte della mano memorizzando il seme della prima carta nella variabile localesuit
, e interrompendo il riconoscimento alla prima carta successiva di seme diverso.Se usciamo dal ciclo avendo esaminato tutte le carte vuol dire che abbiamo un
FLU
⌋SH
, altrimenti restituiremo come valore quello che ci darà l’evaluate
del valutatore successivo.public class FlushEvaluator implements ChainedHandEvaluator { private final ChainedHandEvaluator next;
public FlushEvaluator(ChainedHandEvaluator nextEvaluator) {
2.3 Soluzioni
PokerHand + getRank() : HandRank
<<interface>>
ChainedHandEvaluator + evaluate(PokerHand) : HandRank
FlushEvaluator
+ evaluate(PokerHand) : HandRank HighCardEvaluator
+ evaluate(PokerHand) : HandRank
1 1
0..1
Figura 2.3: getRank() using Chain of Responsability
assert nextEvaluator != null;
next = nextEvaluator;
}
@Override
public HandRank evaluate(PokerHand pokerHand) { assert pokerHand != null;
Suit suit = null;
for (Card card : pokerHand) { if (suit == null) {
suit = card.getSuit();
} else if (suit != card.getSuit()) { return next.evaluate(pokerHand);
}
}return HandRank.FLUSH;
} }
Il diagramma delle classi corrispondente alla situazione del codice sopra esposto diventa perciò quello in Fig.2.3.
Per completare la consegna data che chiedeva di implementare tre dei riconoscitori, e in maniera simile alla classe precedente, possiamo implementare il riconoscitore di
ONE_PAIR
.Anche in questo caso troviamo l’attributo
next
con significato analogo. La logica del riconoscimento si basa su un array di boolean aggiuntivo di cardinalità uguale a quello dei possibili valori di ranking. Per ogni posizione verrà memorizzato se è già stata trovata una carta con quel ranking (uno specifico valore di rank corrisponde nell’array alla posizione corrispondente alla sua posizione nell’elenco di valori dell’enumerativo che si può ottenere con la funzioneordinal
). Iterando sulle carte della mano, ogni volta si controlla se è già stata trovata una carta con lo stessorank
. In caso positivo si restituisce che ha trovato una coppia, altrimenti tiene nota dentro all’array di booleanipresent
che ha trovato una carta con quel rank, altrimenti si aggiorna l’arraypresent
con la nuova scoperta. Se al termine della scansione della mano non si è trovata nessuna coppia, restituiremo come valore quello che ci darà l’evaluate
del valutatore successivo.public class OnePairEvaluator implements ChainedHandEvaluator { private final ChainedHandEvaluator next;
public OnePairEvaluator(ChainedHandEvaluator nextEvaluator) { assert nextEvaluator != null;
this.next = next;
}
@Override
public HandRank evaluate(PokerHand hand) {
EnumMap<Rank, Boolean> present = new EnumMap<>(Rank.class);
for (Card card : hand) {
if (present.getOrDefault(card.getRank(), false)) return HandRank.ONE_PAIR;
elsepresent.put(card.getRank(), true);
}return next.evaluate(hand);
} }
Una volta creati gli anelli della catena dei valutatori dobbiamo istanziarli e collegarli opportunamente e richiamare la valutazione da dentro il metodo
getRank()
. Essendo la catena dei valutatori stateless possiamo istanziarla una sola volta alla creazione della classePokerHand
e condividerla tra tutte le sue istanze mediante un attributostatic private final.static private final ChainedHandEvaluator EVALUATOR = new Flush(
new OnePairEvaluator(
new HighCardRanking()));
...
public HandRank getRank() { return EVALUATOR.evaluate(this);
}
5. Gli oggettiPokerHanddevono implementare l’interfaccia Comparable, ordinando lePo⌋
kerHand secondo il valore restituito da getRank(). Non c’è bisogno di definire l’ordina-mento fra mani con lo stesso punteggio.
Aggiungiamo alle interfacce implementate dalla classe
PokerHand
ancheComparable
di oggettiPokerHand
: public class PokerHand implementsIterable<Card>, Co
⌋mparable<PokerHand> {...}
Questo aggiunge ai doveri della classePokerHand
quello di implementare il metodopublic int compareTo(PokerHand o){...}Visto che in questo punto ci accontentiamo di fare l’ordinamento semplicemente tramite il valore restituito da
getRank
che è un enumerativo e quindi esplicitamente ordinato secondo l’ordine di presentazione dei valori (dal più piccolo al più grande), è sufficiente:public int compareTo(PokerHand o) { assert o != null;
return getRank().compareTo(o.getRank());
}
3 Lab03: Pokerhand (cont.)
3.1 Repository
Si prosegue da dove si era arrivati nel laboratorio 2 (2).
Vecchie e nuove indicazioni possono essere trovate in:
https://gitlab.com/programmazione2/lab02
Scritto da Martin P. Robillard e adattato per il corso di "Programmazione II" @ Unimi.
3.2 Esercizi
Il codice fornito è una versione (adattata secondo le convenzioni Gradle) del programma Deck, scritto da Martin P. Robillard.
In questo esercizio si richiede di utilizzare le classi del package ca.mcgill.cs.stg.s
⌋olitaire.cards
senza modificarle, nello spirito dell’“Open/Closed Principle”. Le classi prodotte dovranno far parte del package it.unimi.di.prog2.poker.
3.2.1 Obiettivi
In aggiunta a quanto già svolto nel capitolo
2:1. Definire un costruttore di PokerHand a partire da una stringa tipo
"7H JC 0H AC 4⌋C"
→ (SEVEN of HEARTS, JACK of CLUBS, TEN of HEARTS, ACE of CLUBS,
FOUR of CLUBS) . Si suggerisce di usare la classe Scanner (che implementa il pattern Iterator ) e di definire due metodi Rank
parseRank(String)e Suit
pars⌋eSuit(String)
;
2. Definire una classe PokerTable con la responsabilità di gestire un numero variabile di giocatori, ognuno dei quali riceve una PokerHand dal medesimo Deck;
3. La classe PokerTable deve fornire un metodo PokerHand
getHand(inti) che restituisce una copia della mano del giocatore i-esimo;
4. Aggiungere un costruttore a PokerHand che parte da una lista di carte e ne fa una copia interna;
5. Aggiungere a PokerTable un metodo PokerHand
change(intplayer, List<Ca
⌋rd> toChange) che permetta di cambiare le carte della mano del giocare nume-ro player, con il vincolo che almeno una deve restare invariata; si definiscano opportunamente:
• l’asserzione della precondizione che le carte da cambiare devono far parte della
mano del giocatore e che almeno una carta non venga cambiata
• l’asserzione della postcondizione che il risultato non contiene le carte che si volevano cambiare
6. La classe PokerTable deve fornire un metodo che restituisce un iteratore su Int
⌋eger che permetta di scorrere gli identificatori dei player ordinati dal punteggio più alto al più basso
7. Se rimane tempo completare il confronto di PokerHand in modo che risolva anche i casi di parità di ranking (vedi
https://en.wikipedia.org/wiki/List_of_poker_hands
)
3.3 Soluzioni
1. Definire un costruttore di PokerHand a partire da una stringa tipo"7H JC 0H AC 4C"
→ (SEVEN of HEARTS, JACK of CLUBS, TEN of HEARTS, ACE of CLUBS, FOUR of CLUBS). Si suggerisce di usare la classeScanner(che implementa il pattern Iterator) e di definire due metodi Rank parseRank(String) eSuit parseSuit(String) Viene richiesto di implementare un costruttore di
PokerHand
a partire da una descrizione testuale. E si suggerisce di usare la classe di libreria Scanner per fare il parsing della stringa.public PokerHand(String desc) { Scanner input = new Scanner(desc);
while (input.hasNext()) { String card = input.next();
...
} }
Come si può vedere da questo codice, la classe
Scanner
può essere usata come tutti gli altriIterator
che abbiamo visto tramite le due funzionihasNext
enext
. In questo caso ciò di cui viene controllata l’esistenza e eventualmente restituito è “la prossima parola” o per essere precisi la prossima sequenza di caratteri delimitata da spazi o simili (tabulazioni, newline, etc). Quindi, qualora venga passata ad esempio la stringa "7H JC 0H AC 4C", le prime 5 chiamate alla funzionenext
restituiranno nell’ordine le stringhe "7H", "JC","0H","AC","4C".
Suddivisa la stringa nelle sue cinque parti (ognuna rappresentate una carta) ora bisogna convertire queste stringhe di due lettere nel
Rank
eSuit
della carta da cercare. Il testo suggerisce di definire a questo scopo due funzioni per incapsulare meglio l’algoritmo e rendere più facilmente modificabili gli stessi in possibili futuri momenti di refactoring.Se assumiamo le due funzioni come esistenti il nostro costruttore può essere completato mettendo al posto dei puntini la seguente linea:
cards.add(Card.get(parseRank(card), parseSuit(card)));
Rimane però da risolvere il problema di definire le due funzioni. Usare un comodo switch anche se magari ci annotiamo a mente che potrebbe essere un punto da rifattorizzare in seguito.
private Suit parseSuit(String card) { switch (card.charAt(1)) {
3.3 Soluzioni
case 'C': return Suit.CLUBS;
case 'D': return Suit.DIAMONDS;
case 'H': return Suit.HEARTS;
case 'S': return Suit.SPADES;
default: throw new IllegalArgumentException("Error in parsing Suit letter");
} }
private Rank parseRank(String card) { switch (card.charAt(0)) {
case 'A':
case '1': return Rank.ACE;
case '2': return Rank.TWO;
case '3': return Rank.THREE;
case '4': return Rank.FOUR;
case '5': return Rank.FIVE;
case '6': return Rank.SIX;
case '7': return Rank.SEVEN;
case '8': return Rank.EIGHT;
case '9': return Rank.NINE;
case '0': return Rank.TEN;
case 'J': return Rank.JACK;
case 'Q': return Rank.QUEEN;
case 'K': return Rank.KING;
default: throw new IllegalArgumentException("Error in parsing Rank letter");
} }
Questa soluzione non è elegantissima, ma è molto semplice da immaginare e veloce da implementare.
A questo punto possiamo usare il costruttore per creare mani ad hoc e codificare più facilmente nel main del programma delle prove significative delle nostre funzioni (diventa molto più facile ad esempio scrivere un programma che faccia un po’ di prove per controllare il funzionamente della funzione
getRank
).2. Definire una classe PokerTable con la responsabilità di gestire un numero variabile di giocatori, ognuno dei quali riceve unaPokerHand dal medesimoDeck
Da questa indicazione si capiscono due cose:
PokerTable
deve essere capace di memo-rizzare al suo interno un numero variabile di giocatori e che i giocatori sono caratterizzati semplicemente dalle carte che hanno in mano (la loroPokerHand
). Viene quindi naturale definire un attributoplayers
di tipoList<PokerHand>
.Il costruttore di
PokerHand
riceverà in input il numero di giocatori da gestire, e inizializ-zerà l’attributoplayers
con mani di 5 carte ognuna (stiamo considerando un poker a 5 carte). Ogni tavolo da gioco è caratterizzato anche dal mazzo da cui vengono pescate le carte.public class PokerTable {
private final List<PokerHand> players;
private final Deck deck;
public PokerTable(int num) { players = new ArrayList<>();
deck = new Deck();
for (int i = 0; i < num; i++) { players.add(new PokerHand(5, deck));
} } }
3. La classe PokerTable deve fornire un metodoPokerHand getHand(int i) che resti-tuisce una copia della mano del giocatore i-esimo
Qui viene aggiunta l’informazione che i giocatori (rappresentati da una
PokerHand
) devono essere citabili attraverso un numero. L’aver usato come struttura dati interna unArray
⌋List
ci aiuta a implementare la funzione richiesta. Se non ci fosse richiesto esplicitamente di ritornare una copia della mano invece di un riferimento alla mano stessa, avremmo infatti potuto usare il seguente semplice codice:public PokerHand getHand(int i) { assert i>=0 && i < players.size();
return players.get(i);
}
La asserzione iniziale ci esplicita che accettiamo numeri solo rappresentanti indici validi di giocatori (in realtà lo stesso check verrebbe già effettuato da Java e se fallisce restituireb-be una eccezione di tipo
ArrayIndexOutOfBoundsException
) L’istruzione successiva restituisce al chiamante un riferimento alla i-esimaPokerHand
. Questa operazione è in generale pericolosa perché (come abbiamo visto sia a lezione che in precedenti laboratori) darebbe accesso proprio a parte delle informazioni nascoste. In questo caso la richiesta di farne una copia è in realtà probabilmente eccessivamente conservativa in quanto (almeno fino a questo momento) la classePokerHand
è definita in maniera che risulta possedere la caratteristica di immutabilità e quindi potrebbe essere passato il suo riferimento senza rischi.Dobbiamo però aderire alla richiesta di farne una copia. Al momento l’unico modo di creare una
PokerHand
specificando le carte è tramite il costruttore da stringa da pocoDobbiamo però aderire alla richiesta di farne una copia. Al momento l’unico modo di creare una