domenica 8 maggio 2016

Svolgimento Esercizio HumansVsZombies

In questo post verranno mostrate le soluzioni relative all'esercizio HumansVsZombie; come base delle soluzioni ho preso in considerazione i vari progetti che mi sono stai inviati.

Funzionalità 1 - Aggiunta classe HuberZombie

Questa funzionalità ha lo scopo di far riflettere sulla progettazione delle classi del progetto. Uno dei progetti che mi sono stati inviati conteneva la seguente soluzione al problema:

public class HuberZombie extends Zombie {

void findHumansToInfect(Field field, Location location, Field updatedField) {
 Iterator adjacentLocations = field.adjacentLocations(location);
 while (adjacentLocations.hasNext()) {
  Location where = (Location) adjacentLocations.next();
  Object actor = field.getObjectAt(where);
  if (actor instanceof Human) {
   Human human = (Human) actor;
   human.infectedHuman(updatedField);
   }
  }
 }
}


public class Game {
...
/**
 * Gioca un turno
 */
public void playOneTurn() {
 turn++;

 // Esegui le azioni di tutti i giocatori
 for (Human human : humans) {
  human.act(currentField, updatedField);
 }
 for (Zombie zombie : zombies) {
  zombie.act(currentField, updatedField);
 }
 for (HuberZombie huberZombie : huberZombies) {
  huberZombie.act(currentField, updatedField);
 }
 
 ...
 }
}

Questa soluzione, anche se corretta, presenta alcune problematiche dal punto di vista concettuale. Supponiamo di volere aggiungere un ulteriore classe di attori: i "cani zombie". Seguendo l'implementazione precedente la cosa più naturale da fare sarebbe la creazione di un ulteriore classe che estende la classe degli zombie, seguita dalla creazione di un ciclo for per la gestione di questi attori.  E se volessimo aggiungere altre tipologie di zombie o di umani? Questo porterebbe ad una prolificazione di codice duplicato e ad una sbagliata concettualizzazione degli oggetti del sistema.

Mettiamo un attimo da parte l'implementazione degli HuberZombie ed esaminiamo il metodo base playOneTurn():

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
public void playOneTurn() {
 turn++;

 // Esegui le azioni di tutti i giocatori
 for (Human human : humans) {
  human.act(currentField, updatedField);
 }
 for (Zombie zombie : zombies) {
  zombie.act(currentField, updatedField);
 } 
 // Aggiorna il campo da gioco per il prossimo turno
 Field temp = currentField;
 currentField = updatedField;
 updatedField = temp;
 updatedField.clear();

 // Aggiorna la visualizzazione del campo da gioco
 view.showStatus(turn, currentField);
}

Le righe 5-10 dovrebbero essere un campanello di allarme in quanto siamo in presenza di codice duplicato: per ogni attore che vogliamo aggiungere al gioco dobbiamo creare il corrispettivo ciclo for per far agire gli attori in questione. Per risolvere questo problema è necessario pensare in questo modo:
Il nostro scopo è quello di forzare tutti gli attori del sistema ad avere un metodo "act" che rappresenta l'azione che deve essere eseguita da quell'attore.
Per implementare questo pensiero è possibile inserire un'interfaccia:

/**
 * Actor e' la classe astratta che rappresenta tutti i tipi di
 * partecipanti al gioco
 *
 */
public interface Actor
{
/**
 * Ogni attore compie un azione durante il turno
 *
 * @param currentField Il campo da gioco corrente
 * @param updatedField Il campo da gioco per il prossimo turno.
 */
void act(Field currentField,
                         Field updatedField);
                         
/**
 * Location dell'attore
 * @return Location dell'attore.
 */
Location getLocation();
                      
}

Se ad ogni attore del sistema facciamo implementare questa interfaccia, il metodo playOneTurn() si trasforma magicamente in: 


public void playOneTurn() {
 turn++;

 // Esegui le azioni di tutti i giocatori
 for (Actor actor : actors) {
  actors.act(currentField, updatedField);
 }
  
 // Aggiorna il campo da gioco per il prossimo turno
 Field temp = currentField;
 currentField = updatedField;
 updatedField = temp;
 updatedField.clear();

 // Aggiorna la visualizzazione del campo da gioco
 view.showStatus(turn, currentField);
}



Nel caso in cui volessimo aggiungere ulteriori cento attori, il metodo playOneTurn() rimarrebbe invariato. Questa modifica comporta ulteriori benefici per la classe Game: il metodo reset() si trasforma in 



1
2
3
4
5
6
7
8
public void reset() {
 turn = 0;
 actors.clear();
 updatedField.clear();
 actorGenerator.populate(currentField, actors);
 // Aggiorna la visualizzazione del campo da gioco
 view.showStatus(turn, currentField);
}

e sarà solamente la classe ActorGenerator che si occuperà di inserire all'interno della lista degli attori le implementazione vere e proprie di quest'ultimi.


Funzionalità 2, 3- Modifica uccisione zombie 

Una delle possibili soluzioni a questi problemi è la seguente (la soluzione è stata presa da un progetto che mi è stato inviato):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
public class Munition {

 private boolean unpicked = true;
 private Location location;

 public void act(Field currentField, Field updatedField) {

  if (unpicked) {
   updatedField.place(this);
  }
 }

 ...
}

Anche se può sembrare strano, l'oggetto Munition è anch'esso un attore del sistema in quanto svolge una parte attiva all'interno del gioco e quindi è corretto considerarlo come un attore.
A questo punto gli umani, durante il loro turno, possono andare alla ricerca di munizioni per l'uccisione degli zombie.
Sempre prendendo spunto dalle soluzioni che mi sono state inviate, la soluzione per la gestione della raccolta delle munizioni e dell'uccisione degli zombie tramite pistola è la seguente:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class Human{
....
public void act(Field currentField, Field updatedField) {

 // Se l'umano è infettato non può fare nulla
 if (!infected) {

  if (munitions < 5) {
   // raccogli le munizioni
   findMunitionsToPick(currentField, getLocation());
  }

  if (munitions > 0) {
   // uccidi gli zombie adiacenti alla posizione dell'umano
   findZombieToKill(currentField, getLocation());
  }

  // Sposta l'umano alla ricerca degli zombie o di munizioni
  Location newLocation = updatedField.freeAdjacentLocation(getLocation());

  if (newLocation != null) {
   setLocation(newLocation);
   updatedField.place(this);
  }

 }

}

/**
 * Metodo per la ricerca delle munizioni
 * 
 * @param field
 *            Campo da gioco
 * @param location
 *            Location dell'umano
 */

public void findMunitionsToPick(Field field, Location location) {
 Iterator adjacentLocations = field.adjacentLocations(location);
 while (adjacentLocations.hasNext()) {
  Location where = (Location) adjacentLocations.next();
  Object actor = field.getObjectAt(where);
  if (actor instanceof Munition) {
   Munition munition = (Munition) actor;
   munition.setPicked();
   munitions++;
   break;
  }
 }
}

private void findZombieToKill(Field field, Location location) {

 Iterator adjacentLocations = field.adjacentLocations(location);
 while (adjacentLocations.hasNext()) {
  Location where = (Location) adjacentLocations.next();
  Object actor = field.getObjectAt(where);
  if (actor instanceof Zombie) {
   Zombie zombie = (Zombie) actor;
   zombie.setDead();
   munitions--;
   break;
  } else if (actor instanceof HuberZombie) {
   HuberZombie huberZombie = (HuberZombie) actor;
   huberZombie.setDead();
   munitions--;
   break;

  }
 }

}...
}

Il metodo act è corretto: se l'umano ha meno di 5 munizioni allora vai alla ricerca di queste munizioni per uccidere gli zombie. Il metodo findMunitionsToPick presenta un errore: se l'umano ha una sola munizione e vuole raccogliere tutte le munizioni intorno a lui, il break alla linea 48 glielo impedisce e di conseguenza va tolto: 

public void findMunitionsToPick(Field field, Location location) {
 Iterator adjacentLocations = field.adjacentLocations(location);
 while (adjacentLocations.hasNext()) {
  Location where = (Location) adjacentLocations.next();
  Object actor = field.getObjectAt(where);
  if (actor instanceof Munition && munitions < 5 ) {
   Munition munition = (Munition) actor;
   munition.setPicked();
   munitions++;
  }
 }
}

Un errore simile si presenta nel metodo findZombieToKill(). Supponiamo che l'umano abbia una sola munizione a disposizione e che durante la ricerca delle munizioni non sia riuscito a trovarne altre; così come è stato implementato il metodo, l'umano uccide sempre tutti gli zombie che sono intorno a lui indipendentemente dal numero di proiettili (solamente se ha zero proiettili non può uccidere nessuno). In questo caso il codice va modificato nella seguente modo: 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
private void findZombieToKill(Field field, Location location) {

Iterator adjacentLocations = field.adjacentLocations(location);
while (adjacentLocations.hasNext()) {
 Location where = (Location) adjacentLocations.next();
 Object actor = field.getObjectAt(where);
 if (actor instanceof Zombie && munitions > 0 ) {
  Zombie zombie = (Zombie) actor;
  zombie.setDead();
  munitions--;
  break;
 } else if (actor instanceof HuberZombie && munitions > 0) {
  HuberZombie huberZombie = (HuberZombie) actor;
  huberZombie.setDead();
  munitions--;
  break;

 }
}

}

Aggiungendo questo controllo, però, esce di nuovo il problema del codice duplicato. I due if sono praticamente uguali e questo è sinonimo di una cattiva progettazione del classi: per ogni tipo di zombie ci dovrebbe essere un if che controlla la tipologia di zombie. Una soluzione sarebbe quella di creare una classe astratta Zombie e farla estendere alla classe RealZombie che rappresenta lo zombie normale e alla classe HuberZombie.

public abstract Zombie {
 
private boolean alive = true;

...

public void setDead() {
    alive = false;
}

}

Il metodo findZombieToKill() diventa quindi 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private void findZombieToKill(Field field, Location location) {

Iterator adjacentLocations = field.adjacentLocations(location);
while (adjacentLocations.hasNext()) {
 Location where = (Location) adjacentLocations.next();
 Object actor = field.getObjectAt(where);
 if (actor instanceof Zombie && munitions > 0 ) {
  Zombie zombie = (Zombie) actor;
  zombie.setDead();
  munitions--;
  break;
 }
}

}

In questo modo abbiamo eliminato il problema del codice duplicato.

Funzionalità 4 

Una soluzione che mi è stata inviata è la seguente: 

public class GameView extends JFrame {
...
 public GameView(int height, int width) {
..
Container contents = getContentPane();
 getContentPane().setLayout(new BoxLayout(getContentPane(), BoxLayout.PAGE_AXIS));
 contents.add(stepLabel);
 contents.add(fieldView, BorderLayout.CENTER);
 contents.add(population);

 txtInsertNumberTurns = new JTextField();
 txtInsertNumberTurns.setMaximumSize(new Dimension(300, 26));
 txtInsertNumberTurns.setText("Insert number turns");
 getContentPane().add(txtInsertNumberTurns);
 txtInsertNumberTurns.setColumns(10);

 JButton btnStart = new JButton("Start");

 btnStart.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent e) {
   try {
    int turns = Integer.parseInt(txtInsertNumberTurns.getText());
    game.playGame(turns);
   } catch (NumberFormatException e1) {
    lblNewLabel.setVisible(true);
   }
  }
 });
 lblNewLabel = new JLabel("You did not insert a number!");
 lblNewLabel.setVisible(false);
 getContentPane().add(lblNewLabel);
 getContentPane().add(btnStart);

 btnNewButton = new JButton("Reset");
 btnNewButton.addActionListener(new ActionListener() {
  public void actionPerformed(ActionEvent e) {
   game.reset();
  }
 });
 getContentPane().add(btnNewButton);
 pack();
 setVisible(true);

Questa soluzione porta al seguente risultato:



Il codice proposto è giusto ma può essere migliorato. Il codice presenta alcune complicazioni nel caso in cui volessimo spostare i componenti dal basso verso l'alto o aggiungere delle proprietà (margini, allineamento, ecc..) ai vari componenti. Attuare delle modifiche del genere significare cambiare molte linee di codice e generare parecchia confusione all'interno della classe.

Una possibile soluzione a questo problema è  quella di creare un pannello che contenga i componenti necessari all'avvio di una nuova partita: abbiamo bisogno di un pulsante "play", di un textfield per immettere un numero di turni ed infine un pulsante di reset. Poiché questi componenti svolgono delle operazioni diverse dalla visualizzazione del campo, è buona norma inserirle all'interno di un altro layout: in questo modo è più facile gestirne la sua posizione, la sua gestione e il suo riuso.
Per utilizzare un layout in java swing è necessario associarlo ad un componente grafico: in questo caso il componente che fa al caso nostro è un JPanel che mette a disposizione un panello di componenti. Il layout che ho scelto è il GridLayout il quale permette di inserire i vari componenti grafici all'interno di una griglia costituita da righe e colonne. Il codice è il seguente: 


public class GameView extends JFrame {
...
 public GameView(int height, int width) {
..
       //Creo il layout formato da 4 righe e 2 colonne
        GridLayout gridLayout = new GridLayout(4,2);
        //Setto lo gap verticale e orizzontale del layout
        gridLayout.setHgap(5);
        gridLayout.setVgap(5);

        //Creo un nuovo JPanel e gli associo il layout appena creato
        JPanel jPanel = new JPanel();
        jPanel.setLayout(gridLayout);

        //Aggiungo i componenti del pannello
        jPanel.add(new JTextField("Numero turni"));
        jPanel.add(new JButton("Start game"));
        jPanel.add(new JButton("End game"));
        jPanel.add(stepLabel);

        Container contents = getContentPane();
        //Aggiungo il pannello al container della view
        contents.add(jPanel, BorderLayout.NORTH);
        contents.add(fieldView, BorderLayout.CENTER);
        .....
    }
}

Il risultato ottenuto è:




Funzionalità 5

Alcuni "cattivi pattern" di programmazione sono già stati corretti nei precedenti punti. Di seguito vengono riportati quelli che non sono stati corretti.

Nella classe ActorGenerator vengono utilizzati dei numeri "secchi" per il controllo della generazione delle pedine all'interno del campo da gioco:


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void populate(Field field, List humans, List zombies)
    {
        Random rand = new Random();
        field.clear();
        for(int row = 0; row < field.getDepth(); row++) {
            for(int col = 0; col < field.getWidth(); col++) {
                if(rand.nextDouble() <= 0.2) {
                    Human human = new Human();
                    human.setLocation(row, col);
                    humans.add(human);
                    field.place(human);
                }
                else if(rand.nextDouble() <= 0.5) {
                    Zombie zombie = new Zombie();
                    zombie.setLocation(row, col);
                    zombies.add(zombie);
                    field.place(zombie);
                }
            }
        }
    }

E' importante che questi numeri vengano inseriti all'interno di constanti invece di essere utilizzati direttamente all'interno del codice: in questo modo e possibile modificarli senza andare a cercare, ogni volta, dove sono stati posizionati all'interno del codice. Nel nostro caso è necessario aggiungere due semplici costanti:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// La probabilta' con cui un umano può essere creato all'interno della griglia
private static final double HUMAN_CREATION_PROBABILITY = 0.2;
    // La probabilta' con cui uno zombie può essere creato all'interno della griglia
private static final double ZOMBIE_CREATION_PROBABILITY = 0.5;

public void populate(Field field, List humans, List zombies)
    {
        Random rand = new Random();
        field.clear();
        for(int row = 0; row < field.getDepth(); row++) {
            for(int col = 0; col < field.getWidth(); col++) {
                if(rand.nextDouble() <= HUMAN_CREATION_PROBABILITY) {
                    Human human = new Human();
                    human.setLocation(row, col);
                    humans.add(human);
                    field.place(human);
                }
                else if(rand.nextDouble() <= ZOMBIE_CREATION_PROBABILITY) {
                    Zombie zombie = new Zombie();
                    zombie.setLocation(row, col);
                    zombies.add(zombie);
                    field.place(zombie);
                }
            }
        }
    }





lunedì 18 aprile 2016

Progetto Java: Humans vs Zombies








In questo post spiegherò le specifiche dell'esercizio sul gioco "HumasVsZombie".

Lo scopo dell'esercizio è in linea con quelli già svolti durante il corso e consiste nell'aggiungere alcune funzionalità ad un progetto Java che implementa il gioco "Humans vs Zombies".

I sorgenti del progetto possono essere scaricati dal seguente link SORGENTI PROGETTO

Humans vs Zombies

HumansVsZombie è un gioco a turni in cui due schieramenti, umani e zombie, si combattono tra di loro al fine di affermare la propria sopravvivenza. Il gioco è costituito da un numero arbitrario di turni e termina quando tutti i turni sono stati completati o quando rimane in vita un solo schieramento (zombie o umani).
All'inizio del gioco gli attori di ogni schieramento vengono creati e posizionati in maniera casuale all'interno di un campo formato da una matrice (n x m). Il numero degli attori dipenderà dalla probabilità di creazione associata ad ogni schieramento. Ogni casella della matrice rappresenta una posizione del campo e in ogni posizione può esserci un solo attore: naturalmente alcune posizioni possono rimanere vuote.
Ad ogni turno, gli attori del sistema (umani e zombie) compiono una determinata azione:
  • gli umani possono spostarsi alla ricerca di zombie e ucciderli
  • gli zombie possono spostarsi alla ricerca di umani da infettare
Gli umani sono sempre i primi ad iniziare il turno e una volta che hanno concluso le proprie azioni è possibile eseguire quelle degli zombie. Se un umano uccide uno zombie prima dell'inizio dell'azione di quest'ultimo allora lo zombie verrà rimosso dal campo da gioco. Viceversa, se uno zombie infetta un umano, quell'umano verrà rimosso dal gioco solamente al turno successivo e prima che possa compiere qualsiasi azione. Quando tutti gli umani e tutti gli zombie avranno compiuto la propria azione il turno finisce ed è possibile passare a quello successivo.
La posizione dell'attore di un determinato schieramento stabilisce l'ordine con cui gli attori possono eseguire la propria azione: il campo da gioco viene perlustrato a partire dalla prima casella in alto a sinistra e si prosegue con le caselle appartenenti alla stessa riga, una volta perlustrata la prima riga si passa alla successiva fino ad arrivare all'ultima. Quando in una casella è presente un attore e quell'attore appartiene allo schieramento che può agire in quel frangente di turno allora l'attore potrà eseguire la sua azione.

Struttura del gioco


1. Campo da gioco

Il gioco si svolge all'interno di un campo rappresentato da un matrice (n x m) dove ogni posizione è una casella del campo. Ogni casella è identificata attraverso il numero di riga e di colonna della matrice.
Il campo da gioco è costituito dalla classe Field che contiene tutti gli attori presenti nel gioco. Una casella del campo può essere vuota o può essere occupata, in maniera esclusiva, da uno zombie o da un umano. Ad ogni attore verrà associato un oggetto, chiamato Location, nel quale sono contenute le coordinate dell'attore all'interno del Field (numero di riga, numero di colonna).
Il posizionamento degli attori del gioco viene effettuata contestualmente alla loro creazione . Per ogni casella del gioco viene estratto un numero e se questo numero è minore di una certa probabilità (ogni attore ha associata una probabilità di creazione) viene creato un attore e posizionato in quella casella.

La visualizzazione del campo da gioco avviene tramite l'uso delle librerie di Java Swing. La creazione del campo e la sua gestione avvengono tramite l'utilizzo della classe GameView.





La figura precedente mostra un esempio del campo di gioco. Ogni quadratino colorato rappresenta un attore del sistema all'interno dei fields.

2. Attori

Ogni attore del gioco è rappresentata da una classe: Humans per gli umani e Zombie per gli zombie. Ognuna di queste classe ha associato un attributo Location. La location rappresenta la posizione di un singolo attore del gioco all'interno del campo da gioco Field. Ogni attore ha a disposizione, per ogni turno di gioco, una serie di azioni che può intraprendere.

Gli esseri umani hanno a disposizione due azioni:
  • Uccidi uno zombie che si trova nelle posizioni adiacenti a quelle dell'umano.
  • Una volta ucciso lo zombie, l'umano può spostarsi in una posizione libera adiacente a quella in cui si trova in cerca di altri zombie da uccidere.
Gli zombie hanno a disposizione due azioni:
  • Cerca degli umani da infettare. Durante ogni turno di gioco  uno zombie può infettare un solo umano adiacente alla sua posizione. Una volta che l'umano è stato infettato, al successivo turno si trasformerà in uno zombie.
  • Dopo avere infettato un umano, lo zombie può spostarsi in una posizione adiacente libera.

    3. Turni

    Il gioco si sviluppa tramite l'esecuzione di un numero di turni arbitrario. Ad ogni step vengono eseguite l'azioni associate ad ogni attore del gioco. La gestione dei turni e dell'esecuzione delle azioni è delegata alla classe Game.

    Obiettivi del progetto

    Gli obiettivi del progetto consistono nell'aggiunta di alcune funzionalità all'insieme di quelle preesistenti. In particolare devono essere implementate le seguenti funzionalità:
    1. Aggiungere una nuova classe di personaggi chiamati HuberZombie: questi zombie possono infettare tutti gli umani che si trovano nelle posizioni adiacenti a quella in cui si trova lo zombie.
    2. Modificare la modalità di uccisione degli zombie da parte degli umani: gli umani hanno a disposizione una pistola con un numero limitato di proiettili (pari a 5 proiettili durante tutto il gioco).
    3. Aggiungere la possibilità di raccogliere degli oggetti: in una casella, oltre agli zombie o agli umani, possono esserci delle munizioni che possono essere raccolte.
    4. (Opzionale) Modificare l'interfaccia grafica in maniera tale da aggiungere un piccolo pannello che permette di specificare il numero dei turni del gioco, di far partire il gioco e di fermarlo.
    5. All'interno del codice sono state inseriti, in maniera volontaria, alcuni "cattivi" pattern di programmazione, alcuni dei quali sono stati mostrati nei precedenti post di questo blog.
    Suggerimenti.... 

    Per lo sviluppo dei primi tre punti è utile pensare all'utilizzo di classi astratte e/o di interfacce (ponete particolare attenzione al codice duplicato il quale è indice di qualche problema di progettazione).

    Il punto quattro richiede qualche sforzo in più per essere implementato in quanto è necessario avere delle conoscenze minime delle librerie di Java Swing. Delle slide che possono aiutare per lo sviluppo dell'interfaccia grafica possono essere reperite dal seguente link Slide Java Swing. Su queste slide è utile studiare la parte relativa agli ActionListener e alla progettazione dei JPanel.
    La parte di codice da cui dovete partire si trova all'interno del costruttore della classe GameView:


    Container contents = getContentPane();
    //al posto di stepLabel inserire un JPanel
    contents.add(stepLabel, BorderLayout.NORTH);
    contents.add(fieldView, BorderLayout.CENTER);
    contents.add(population, BorderLayout.SOUTH);
    pack();
    setVisible(true);
    

    dove bisogna inserire un JPanel che al suo interno contiene i vari pulsanti o textfield che servono per gestire l'esecuzione del gioco.

    Per eventuali chiarimenti potete lasciare un commento sotto il post: cercherò di rispondervi il prima possibile.

    Altre informazioni utili
    Il progetto deve essere compilato con Maven per potere essere eseguito. La classe principale che contiene il metodo main() è App.java .