Skip to content

4. Detailed Design

Dallas edited this page Dec 21, 2022 · 18 revisions

Design di dettaglio

Il menù iniziale e la schermata di simulazione sono state realizzate in pair programming da Nicholas Ricci ed Emanuele Dall'Ara, di conseguenza molto del lavoro è sovrapposto e sviluppato da entrambe le parti; nello stesso modo la gestione delle logiche di attacco sono state sviluppate in pair programming da Alex Baiardi e Giulio Zaccaroni.

Il pair programming è una tecnica agile di sviluppo software nella quale due programmatori lavorano insieme (nel nostro caso in remoto). Uno dei due, indicato come "conducente" scrive il codice mentre l'altro, detto "osservatore", svolge un ruolo di supervisione e di revisione simultanea del codice.

Nicholas Ricci

Design view

Di seguito vengono approfonditi i temi e i componenti specifici realizzati:

  • La console, ovvero la parte di HUD nella simulazione che si occupa di informare l'utente sui parametri degli stati caricati e sugli stati che vengono sconfitti
  • La schermata di fine simulazione
  • Visualizzazione dei soldati nella simulazione

Partendo dalla console ho pensato di renderla "scrollable" in modo che l'utente non debba continuamente controllare se ci sono stati eventi nuovi e possa concentrarsi sulla simulazione in corso. Il pulsante di pausa è stato aggiunto per poter fermare la simulazione e controllare eventuali eventi o parametri(sia dalla console che sugli stati). La scelta di utilizzare un evidenziatore per colorare il nome dello stato con il colore dello stato stesso è legata al fatto che il componente utilizzato non supportava la colorazione del testo.

La schermata di fine simulazione è stata fatta il più semplice possibile per informare l'utente di quali stati non sono stati sconfitti e con quali parametri (risorse, esercito e cittadini). In alto vediamo la lista degli stati vincitori oppure, se interrompiamo la simulazione forzatamente col pulsante di stop, il titolo ci informa che erano presenti ancora delle guerre e che non ci sono dei vincitori. Esiste anche un titolo più raro nel caso in cui tutti gli stati perdano.

Infine i soldati sono rappresentati con due tipi di figure geometriche: un quadrato per le unità di precisione e un triangolo per le unità a distanza. La scelta di queste figure è legata al fatto che facendole abbastanza piccole si riescono comunque a riconoscere, mentre ad esempio il cerchio risultava deforme con così pochi pixel a disposizione.

Design Simulation Engine

Il simulation engine è la sezione che si occupa di gestire tutte le interazioni, le relazioni e i movimenti del programma. Ci sono diversi metodi che fanno parte del loop principale, di seguito vediamo quelli sviluppati singolarmente:

  • Il RelationsSimulationComponent
  • Il WarSimulationComponent
  • Il cambiamento di velocità dello simulazione

Il compito principale del RelationsSimulationComponent è quello di aggiornare le relazioni tra stati in base ai propri alleati e alle proprie guerre: come spiegato nell' Init c'è la possibilità che uno stato alleato entri in guerra a supportare l'amico.

Il compito principale del WarSimulationComponent è quello di verificare se uno stato viene sconfitto e di gestirne tutte le conseguenze. In particolare, per prima cosa si verifica se uno stato è in una delle tre condizioni di sconfitta, in caso positivo vengono presi gli elementi rimanenti (esercito, cittadini, risorse e il territorio) e vengono divisi tra tutti gli stati che hanno vinto. Infine aggiorniamo gli stati vincitori e rimuoviamo le relazioni dello stato sconfitto.

Il cambiamento di velocità viene effettuato tramite dei pulsanti che permettono di diminuire il tempo tra un ciclo e l'altro

Emanuele Dall'Ara

Design view

Per la realizzazione della View, ho creato dei mockup grafici utilizzando il tool open source Figma.
Tramite i mockup ho cercato di realizzare un prototipo di sistema completo di ogni sua funzionalità con componenti intuitivi e semplici in modo da non creare confusione ai vari utilizzatori dell'applicazione.
Il primo mockup realizzato è stato il pannello di avvio della simulazione.
Questa schermata molto semplice è composta da tre bottoni ognuno dei quali ha un certo scopo:

  • Start Game, per iniziare la simulazione
  • Help, tramite un'altra schermata, guida l'utente a capire come funziona il simulatore
  • Exit, chiude il simulatore

Successivamente, ho pensato alla realizzazione della schermata di simulazione.
In questa ho dovuto pensare un po' di più a come visualizzare tutte le informazioni necessarie al funzionamento del simulatore nel modo più semplice possibile.
Per farlo ho scomposto la finestra di visualizzazione in due "sottofinestre" con un rapporto di 3/4 tra esse. La "sottofinestra" più ampia verrà denominata GamePanel, la restante finestra invece Hud.


Il GamePanel è la finestra in cui si deve osservare graficamente l'andamento della simulazione. Questa finestra dovrà mostrare per ciascuno stato:

  • I suoi confini, delimitati da un bordo particolare e riconoscibile
  • Le sue truppe di precisione e quelle ad area, entrambe identificate da una forma geometrica o immagine differente (per le truppe ad area questa avrà una dimensione più ampia)
  • Aggiornare le posizioni delle truppe in base al loro movimento


Hud è il pannello responsabile degli eventi innescati dall'utente. Questo pannello, tramite una logica di bottoni, deve dare la possibilità di:

  • Caricare un file di configurazione
  • Impostare la velocità di simulazione a run-time
  • Fermare/riprendere/interrompere la simulazione
  • Visualizzare a video gli eventi più importanti della simulazione

Infine ho realizzato la schermata di aiuto in cui un utente può effettivamente capire come funziona il simulatore e come costruire un file di configurazione, indispensabile per avviare il simulatore.

Schermata di avvio simulazione Schermata di simulazione Schermata di aiuto
home game help

Per la realizzazione dell'applicazione, si è deciso di utilizzare Java Swing cercando di applicare il più possibile il pattern di programmazione MVC. Si è preferito l'utilizzo di questa libreria per via dell’esperienza nell’utilizzo da parte del team di sviluppo e grazie alla possibilità di Scala di garantire l’utilizzo di codice Java all’interno del programma.

Con l'utilizzo di questo pattern di programmazione, si è suddivisa tutta la programmazione della GUI in Java Swing in questo modo:

  • Controller: si mette in ascolto su tutte le azioni dell'interfaccia utente ed esegue gli aggiornamenti del modello corrispondente. Può ascoltare azioni da viste diverse.

  • Model: Rappresenta lo stato e la logica del dominio, i metodi per modificare lo stato. Notifica i vari listener del modello. Ogni Model è indipendente.

  • View: responsabile della rappresentazione grafica dell'interfaccia utente, del layout degli elementi dell'interfaccia utente, ascolta anche gli aggiornamenti del modello e aggiorna l'interfaccia grafica se necessario.

Questa suddivisione ha portato alla creazione del GameStateController che, attraverso all'utilizzo del SimulationEngine e dei suoi metodi, esegue gli update grafici della simulazione.

Screenshot 2022-12-21 alle 17 07 46

Risultato finale della schermata simulazione

Giulio Zaccaroni

Design Listenable

Data l'importanza del pattern Listener in una simulazione ho deciso di andare a inserire due entità che vadano a rappresentare la possibilità di ascoltare e di essere ascoltati:

  • Listenable: il suo compito è quello di permettere ai Listener di mettersi in ascolto per un determinato evento se interessati e di smettere quando non lo sono più
  • Listener: espongono un'interfaccia per essere informati quando ci sono degli eventi per cui hanno mostrato interesse

Design Simulation Engine

Il simulation engine è il motore della simulazione: una volta avviato (start o resume) restituisce a intervalli regolari (determinati dalla velocità) l'environment aggiornato. I suoi stati possono essere:

  • Paused: l'engine è in pausa: da questo stato può essere avviato con resume o terminato con terminate
  • Running: l'engine sta funzionando e producendo nuovi environment: da questo stato può essere messo in pausa con pause o terminato con terminate
  • Terminated: l'engine è stato bloccato dall'utente o ha completato l'esecuzione Il suo comportamento è descrivibile come quello di un automa a stati finiti con il seguente diagramma degli stati:
Screenshot 2022-12-20 at 11 52 08

Essendo un componente con molta logica e complessità ho deciso di scomporlo in dei sotto-componenti: i SimulationComponent. Questa scelta ha permesso di scomporre il problema in sottoparti (divide et impera) e rendere testabili le singole unità. Ogni SimulationComponent ha due compiti:

  • Emettere zero o più eventi quando avviene qualcosa di particolare (ad esempio quando uno stato vince la guerra)
  • Restituire l'Environment aggiornato alla fine della sua esecuzione Per realizzarlo ho individuato due possibili strade:
  • Nella prima ogni SimulationComponent espone un flusso di eventi (pattern Observer) tra cui il termine della sua esecuzione
  • Nella seconda ogni SimulationComponent espone un flusso di eventi (pattern Observer) e al termine restituisce l'ambiente aggiornato (pattern Future) Ho deciso di propendere per la seconda strada per dividere maggiormente la logica e assicurarmi che arrivi sempre per ultimo l'ambiente aggiornato. È importante che tutti i componenti funzionino in maniera asincrona per evitare di bloccare l'interfaccia grafica durante il suo funzionamento. Quindi, per riassumere, ogni componente prende in ingresso l'Environment della simulazione e, al completamento dell'elaborazione, ne restituisce un altro. Quello restituito verrà passato al componente successivo e così via.

Design Prolog Engine e Prolog Predicates

Prolog Engine è il componente che si occupa di interfacciarsi con il codice scritto in Prolog. Il suo obiettivo è quello di astrarre il più possibile dai meccanismi della libreria sottostante e offrire un'interfaccia di facile utilizzo per l'utente finale. Per farlo, offre due metodi, il primo per accedere ai termini, il secondo per accedere alle variabili di risultato di una computazione in modo lazy. Per aumentare ancora di più il livello di astrazione rispetto a Prolog e nascondere totalmente i dettagli implementativi è stata introdotto un ulteriore oggetto chiamato PrologPredicates che contiene dei metodi per tutti i predicati Prolog senza esporre alcun dettaglio implementativo.

Design Movimento

Movement è il componente che si occupa di gestire tutto ciò che riguarda la localizzazione delle unità (Locatable) e il loro spostamento (Movable). Una istanza Movable si sposta attraverso una determinata strategia (TargetFinderStrategy) restituendo una nuova istanza di sè stessa nella nuova posizione sfruttando il F-Bounded Type Polymorphism rispettando il paradigma funzionale. La decisione di estrapolare la strategia di movimento in un componente a sè stante chiamato TargetFinderStrategy si è rivelata necessaria per rendere possibile il movimento delle unità anche con logiche diverse. Questo si occupa, dato lo stato di appartenenza, di trovare le posizioni che per l'unità ha senso raggiungere. Successivamente, l'unità utilizza le sue logiche interne per muoversi verso uno di essi. L'esternalizzazione della logica di individuazione dei potenziali obiettivi mette le basi per una futura estensione dove, dei generali, possono coordinare il movimento di gruppi di unità con logiche più raffinate.

Design Validation

Un aspetto importante quando si accettano dati da fonti esterne è la validazione, quindi ho deciso di introdurre la possibilità di realizzare entità validabili (Validatable) che possano essere validate riguardo alla correttezza dei dati e, nel caso vi siano errori, alla loro possibile segnalazione. Per fare questo ho individuato due possibili approcci:

  • La generazione di una lista di errori per ogni tipo (con il rischio che un errore iniziale ne possa generare altri)
  • La generazione di un singolo errore con il conseguente blocco della validazione Ho preferito la seconda strada (la stessa usata nei compilatori), in modo da dare all'utente finale da subito un elenco di tutti gli errori trovati nel file anche se, in certi casi, alcuni possono essere non totalmente pertinenti.

Design Repository

La repository si occupa di andare a leggere la configurazione della simulazione da una sorgente e la restituisce come modello del livello di dominio. È molto importante ribadire in questa situazione il fatto che la rappresentazione dei dati nel supporto da cui vengono letti deve essere indipendente da quella del dominio. Il rischio, altrimenti, è quello di avere un forte accoppiamento tra queste con il risultato di dover aggiornare entrambe le rappresentazioni dei dati ogni qualvolta una delle due cambi. Per fare questo è stata aggiunta un'ulteriore astrazione: i DTO (Data Transfer Object), la differenza tra questi e gli oggetti a livello di dominio è che questi non hanno alcun comportamento se non quello di archiviare e recuperare i loro dati. La repository quindi:

  1. va a leggere un file attraverso una data source che le restituisce i DTO e manda un errore se qualcosa non è nel formato aspettato
  2. effettua il mapping tra il DTO (livello data) e il dominio (livello domain)
  3. valida i dati letti e segnala un errore se qualcosa non è corretto

L'idea alla base della realizzazione dei DTO dovrà essere quella di:

  • Minimizzare i dati inseriti dall'utente
  • Minimizzare i possibili errori nei dati inseriti dall'utente

Alex Baiardi

Design Domain Model

La progettazione del modello del dominio cerca di rappresentare le varie entità cercando di aderire ai principi della programmazione funzionale come ad esempio l'immutabilità e la dichiaratività. Inoltre ci si è ispirati anche ai principi SOLID della programmazione ad oggetti. Per ottenere l'immutabilità sono state progettate delle funzioni che, per cambiare lo stato di un oggetto, restituiscono un nuovo oggetto che rappresenta l'oggetto iniziale modificato cercando di renderle il più dichiarative possibile. Ad esempio il trait che definisce Environment prevede un Factory Method copiedWith() che genera una nuova istanza aggiornata con i parametri forniti. La ricerca di dichiaratività ha permesso di avere una struttura facilmente comprensibile e allo stesso tempo astratta che non dipende dalle implementazioni che vincolano l'estendibilità e le modifiche future di cui il progetto potrebbe avere bisogno. Nello specifico per ottenere ciò è stato particolarmente utile il type aliasing che fornisce Scala. Infatti questo meccanismo permette di astrarre dall'implementazione e aiuta la comprensione del codice. Ad esempio la gestione delle risorse e dei civili che in questa fase del progetto è molto semplice, in sviluppi futuri potrà essere cambiata modificando le implementazioni senza toccare le interfacce. Un altra accortezza è l'utilizzo dei tipi di dato generici.

Country

La classe Country rappresenta uno stato all'interno della simulazione con tutti gli attributi che lo caratterizzano disponibili in sola lettura, per la modifica vengono esposte alcune funzioni dichiarate nel trait UpdatableAsset, il quale definisce alcune factory per generare copie aggiornate. Un'altra caratteristica degli stati è di produrre risorse attraverso la popolazione, questa è stata incapsulata dal trait ResourcesProducer che definisce la funzione dailyProduction in cui sarà effettuato il calcolo sulle risorse prodotte giornalmente. Questo nuovo concetto che viene dichiarato è altamente riusabile, infatti si può pensare che in futuro la popolazione sia modellata come singoli cittadini ognuno dei quali è aderisce allo stesso concetto.

Country UML

ArmyUnits

Il trait ArmyUnit definisce il concetto di unità dell'esercito ed è definito come mixin dei trait Attacker, Movable e ResurcesConsumer. Questo componente lascia la libertà di essere esteso in vari modi, in questo caso al momento sono PrecisionArmyUnit e AreaArmyUnit, tuttavia implementa alcuni comportamenti comuni come il movimento e la logica di attacco. In particolare per l'attacco si utilizzano i pattern Strategy e Template Method lasciando ai clienti dell'interfaccia la libertà di fornire la strategia per individuare i target e a chi implementa il trait la libertà di definire l'AttackAction risultato dei colpi andati a buon fine. Army UML

Relations

La gestione delle relazioni è affidata all'implementazione del trait InterCountryReletions, il quale prevede di memorizzare le relazioni tra i vari stati offrendo la possibilità di eseguire varie operazioni:

  • Aggiunta e rimozione di relazioni
  • Elenco degli stati alleati o nemici
  • Presenza di guerre
  • Recupero della relazione tra due stati

Ogni relazione è modellata come una coppia di stati e il tipo di relazione tra di essi è definito nell'enum RelationStatus.

Design dell'attacco

La fase di attacco all'interno della simulazione avviene in modo atomico, ovvero si offre a tutte le unità la possibilità di eseguire l'attacco sferrando i colpi a disposizione. La logica di attacco delegata all'AttackSimulationComponent prevede di rilevare tutti gli eventi di attacco generati dalle unità degli eserciti dei vari stati. Successivamente per ciascun evento si valutano le conseguenze in base al tipo di evento considerando caratteristiche particolari come ad esempio l'area di impatto per gli attacchi ad area.

Clone this wiki locally