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);
                }
            }
        }
    }





Nessun commento:

Posta un commento