• Non ci sono risultati.

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 Package

1. 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 un

ArrayL

ist

di carte (non è necessario specificare che sono 5 visto che

List

è 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 classe

Deck

, 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): public

HandRank

getRank() { return null; } 2. Definire una nuova classe principale (nel package it.unimi.di.prog2.poker) con la

quale sperimentare l’uso degli oggetti PokerHand

Aggiungiamo una classe

Main

che al suo interno definisce il metodo

main

per poter provare a istanziare la classe

PokerHand

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 metodo

toString()

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

Main

, 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 da

List

:

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 interfaccia

Iterable

. Trasformiamo quindi la riga di intestazione della classe in: public class PokerHand implements

Iterable<Card>

{...}

Questo fa sì che la classe

PokerHand

debba implementare (infatti ci verrà segnalato con una bisciolina rossa questa attuale mancanza) il metodo

iterator()

. Il metodo deve restituire un

iteratore

sulle carte della mano. Possiamo rimappare questo compito direttamente sul metodo

iterator()

del nostro attributo

cards

:

@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

e

ONE_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’attributo

next

:

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’attributo

next

settato nel costruttore che indica a chi dovremo passare la mano qualora non fosse un

FLUSH

. La logica del riconoscimento della configurazione

FLUSH

(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 locale

suit

, 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 funzione

ordinal

). Iterando sulle carte della mano, ogni volta si controlla se è già stata trovata una carta con lo stesso

rank

. In caso positivo si restituisce che ha trovato una coppia, altrimenti tiene nota dentro all’array di booleani

present

che ha trovato una carta con quel rank, altrimenti si aggiorna l’array

present

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 classe

PokerHand

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

anche

Comparable

di oggetti

PokerHand

: public class PokerHand implements

Iterable<Card>, Co

mparable<PokerHand> {...}

Questo aggiunge ai doveri della classe

PokerHand

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(int

i) 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(int

player, 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 altri

Iterator

che abbiamo visto tramite le due funzioni

hasNext

e

next

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

next

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

e

Suit

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 loro

PokerHand

). Viene quindi naturale definire un attributo

players

di tipo

List<PokerHand>

.

Il costruttore di

PokerHand

riceverà in input il numero di giocatori da gestire, e inizializ-zerà l’attributo

players

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 un

Array

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-esima

PokerHand

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

PokerHand

è 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 poco

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 poco

Documenti correlati