Skip to content

5. Implementation

Piccio98 edited this page Dec 21, 2022 · 27 revisions

Implementazione

Meccanismi avanzati di scala

Dato il contesto di sviluppo si è cercato il più possibile di utilizzare il paradigma funzionale. Di seguito sono elencati alcuni aspetti rilevanti utilizzati durante lo sviluppo:

For comprehension

Questo costrutto è risultato molto utile in alcune situazioni per scrivere del codice in maniera concisa e dichiarativa, di seguito vediamo una porzione estratta da ArmyUnit in cui è utilizzato:

for
  target <- availableTargets
  probabilityOfSuccess <- Seq.fill(availableHits)(Random.nextInt(100))
  if probabilityOfSuccess < chanceOfHit
yield hitTarget(target)

Pattern matching

Questo meccanismo permette di eseguire un match fra un valore e un dato pattern, eseguendo automaticamente i casting e le instanceOf necessarie. Ci è risultato molto utile per migliorare la qualità del codice evitando indentazioni multiple di if-else e rendendolo molto più leggibile. Inoltre il match permette di decomporre un oggetto nelle sue componenti e questo è molto comodo, ad esempio, se abbinato con gli Option perchè possiamo definire i due casi, quello in cui è vuoto e quello in cui è presente, permettendo così di lavorarci direttamente.
Di seguito vediamo un esempio particolare dell'utilizzo di questo costrutto:

events match
  case (event: AttackAction.AreaAttackAction) :: tail =>
  ...
  case (event: AttackAction.PrecisionAttackAction) :: tail =>
  ...
  case Seq() => environment

Option

Il tipo Option è utilizzato per descrivere la possibile assenza di un valore, evitando di utilizzare il null. Ad esempio vediamo come il simulationEngine non è ancora pronto quando viene creato il controller delle UI:

private var simulationEngine: Option[SimulationEngine] = None

Either

Questo costrutto è utilizzato per gestire valori che potrebbero essere di due tipi diversi. Nel progetto è stato utilizzato per rendere più idiomatica la gestione degli errori e delle eccezioni. Di seguito vediamo un estratto da Validation:

def validate(): 
  Either[List[ValidationError], Unit]

Given instances

Le Given instances sono dei meccanismi che permettono di passare informazioni contestuali al compilatore in maniera automatica, in questo modo il compilatore apprende il valore da usare senza doverlo specificare ogni volta. Nel progetto è risultato particolarmente utile per definire il valore dell'Environment usabile da tutti i componenti. Una qualsiasi funzione che necessita del valore given può utilizzare la parola chiave using per recuperarlo e averlo diponibile.

Extension method

Permettono di aggiungere uno o più metodi a un classe al di fuori della sua dichiarazione. Questo è particolarmente utile per estendere i tipi implementati da terzi, ad esempio nel nostro progetto sono stati utilizzati per aggiungere il metodo toColor alle String.

extension (field: String)
  private[presentation] def toColor: Color

Programmazione asincrona

La programmazione asincrona è stata necessaria in quanto il core della simulazione (ovvero il loop del SimulationEngine) deve essere separato dalla grafica, questo per evitare le durante il tempo di elaborazione la gui non resti bloccata. Per ottenere questo obiettivo è stata scelta la libreria Monix, della quale abbiamo utilizzato i task.

Implementazioni specifiche

Listenable

Listenable è stato sviluppato come trait base che contiene i metodi per aggiungere e rimuovere un Listener, in fase di aggiunta viene restituita una istanza di Disposable che facilita la rimozione del Listener. Per rendere più idiomatica la fase di registrazione è stato creato un metodo onReceiveEvent che memorizza la classe dell'evento (Event) restituendo una istanza di una classe "helper" OnEventPart:

    def onReceiveEvent[Event]: OnEventPart[Event] =
        OnEventPart()

A sua volta la classe OnEventPart espone un metodo from che raccoglie informazioni su che Listenable dobbiamo ascoltare restituendo una istanza di OnEventWithListener:

    def from(listenable: Listenable[Event]): OnEventWithListener[Event] =
      OnEventWithListener(listenable)

Questa infine dopo aver raccolto le informazioni sulla closure da invocare quando c'è un evento si registra sul Listenable

    def run(closure: ListenClosure[Event]): Disposable =
      listenable.addListener(closure)

Questa cascata di classi permette di scrivere il seguente codice:

    onReceiveEvent[SimulationEvent] from component run handleEvent

Per semplificare l'utilizzo di Listenable è stata anche fornito una istanza astratta SimpleListenable che fornisce già una implementazione semplice.

Simulation Engine

Per implementare il SimulationEngine è stato fatto uso della libreria Monix, questa contiene diversi tipi di entità (Observable, Task...) per semplificare la scrittura di programmi asincroni basati sugli eventi in Scala. L'idea alla base è che ogni SimulationComponent dato un Environment restituisce un Task che quando viene eseguito restituisce un nuovo environment aggiornato. Quindi, la combinazione dei singoli SimulationComponent è una iterazione della simulazione.

    simulationComponents
        .foldLeft(Task(previous)) { (task, simulationComponent) =>
        task.flatMap(simulationComponent.run)
        }

A differenza di un Future i Task non si avviano automaticamente quando vengono creati, quindi sarà necessario invocarli come vedremo a breve. Quando la simulazione viene avviata, ad intervalli regolari (determinati dalla velocità della simulazione) viene avviata una iterazione completa che produce un environment. La simulazione si blocca quando le condizioni di termine della simulazione sono soddisfatte (takeWhileInclusive) o viene cancellata dall'esterno (ogni Task quando viene avviato restituisce un Cancelable che può essere cancellato).

    Observable
        .intervalAtFixedRate(simulationDayDuration / speed)
        .scanEval(Task(currentEnvironment)) { (previous, _) =>
        simulationComponents
            .foldLeft(Task(previous)) { (task, simulationComponent) =>
            task.flatMap(simulationComponent.run)
            }
        }
        .takeWhileInclusive(_.interCountryRelations.hasOngoingWars)

Il metodo scanEval ci serve per avere l'environment restituito alla fine dell'iterazione precedente da passare in ingresso all'iterazione successiva.

Prolog

Le due funzioni che sono state sviluppate in Prolog sono:

  • calcolo della distanza tra due punti N-dimensionali;
  • ricerca di obiettivi entro un certo range, ordinamento per distanza e filtro dei primi N risultati.

Calcolo della distanza

Per quanto riguarda il calcolo della distanza il problema è stato scomposto in due parti:

  • sum_diff_squared(P1, P2, Z): a una lista di coordinate di due punti P1 e P2 fa corrispondere in Z la somma dei delta delle loro coordinate;
  • distance(P1, P2, R): a una lista di coordinate di due punti P1 e P2 fa corrispondere in R la radice quadrata di sum_diff_squared ovvero la distanza dei punti.

Calcolo dei target

Per quanto riguarda il calcolo dei target il problema è stato scomposto in 6 parti:

  • can_reach(P1, P2, Range): dice se un punto P2 è all'interno di un certo range da P1, per farlo si avvale di distance.
  • reachable_targets(P, Range, AllT, ReachableT): a un punto P fa corrispondere nella lista ReachableT tutti i punti presenti nella lista dei targets (AllT) presenti entro un certo raggio Range tramite can_reach
  • sort_targets_per_distance(P, AllT, OrderedT): dato un punto P ad esso fa corrispondere in OrderedT tutti i punti in AllT ordinati per distanza da lui usando la quick sort e sort_targets_partition come predicato di supporto.
  • list_limit(L, N, R): dato una lista L e un numero di elementi N ad essi fa corrispondere una lista R che contiene al più i primi N elementi di L.
  • reachable_targets_limit(P, Range, AllT, Limit, LT): riunisce tutti i predicati sopra in un unico predicato per rendere più semplice la chiamata da Scala

Validation

Nello sviluppo di Validation è stato sviluppato un trait Validatable che contiene le informazioni riguardo alla entità che sta venendo validata ValidatableEntity, per usare questo trait è sufficiente implementare un metodo validationErrors a cui far restituire gli eventuali errori di validazione. Questi ultimi possono essere generati con un semplice DSL creato ad-hoc attraverso l'uso della meta-programmazione, in pratica Validatable contiene un'estensione (di ogni tipo) che aggiunge un metodo must il quale legge il nome della variabile attraverso una macro e lo salva all'interno di ValidationPart assieme a informazioni sulla classe stessa.

  extension [Value](field: Value)
    transparent inline def must(validator: Validator[Value])(using
      entity: ValidatableEntity
    ): List[ValidationError] =
      val part =
        ValidationPart[Value](unCamelCaseString(nameOf(field)), field, entity)
      validator.validate(part)

Questo metodo per funzionare deve essere:

  • inline: questo ci assicura che il codice sia espanso e valutato solo in fase di compilazione;
  • transparent: permette al sistema di type-inference del compilatore di andare a vedere dentro questo metodo per ottimizzazioni. altrimenti nameOf(field) non restituirebbe più il valore corretto. A questo punto viene creato un insieme di Validator (reperibili in CommonValidators) che permettono di coprire gran parte dei tipici casi d'uso. L'utilizzo di questa strategia ci permette di arrivare al seguente codice finale:
    speed must BeGreaterThanOrEqualTo(0.0)

il quale, automaticamente, se speed non è maggiore di 0 restituisce l'errore speed must be greater or equal than 0.0 senza la necessità che io gli passi il nome della variabile speed.

Nicholas Ricci

Codice Prodotto

  • domain
    • engine
      • simulation
        • SimulationEngine
          • changeSpeed()
        • components
          • RelationsSimulationComponent
          • WarSimulationComponent
  • presentation
    • view
      • Hud
        • displayInitialSimulationConfig()
        • highlightText()
        • writeToConsole()
        • enableSpeed()
        • highlightCountryId()
      • EndPanel
      • MenuActions
        • upAction()
        • downAction()
        • enterAction()
      • GameMap
        • draw of army units
      • GamePanel
        • addToPanel()
    • inputs
      • MenuKeyAction
      • EnterAction
    • controllers
      • GameStateController
        • setPanel()
        • changeSpeed()

Emanuele Dall'Ara

Codice Prodotto

  • data
    • Serializers
    • data_sources
      • SimulationEnvironmentDataSource
    • repositories
      • SimulationEnvironmentRepositoryImpl
  • domain
    • engine
      • simulation
        • components
          • ResourcesSimulationComponent
    • model
      • Environment
      • repositories
        • SimulationConfigRepository
  • presentation
    • common
      • UIConstants
    • controllers
      • GameStateController
    • inputs
      • GameMouseMotion
      • MenuMouseAdapter
    • view
      • GameMap
      • GamePanel
        • ComponentPosition
      • Hud
        • addJComponents
        • uploadJson
      • MainFrame
        • setPanel
      • MenuActions
        • getPreferredSize
        • invalidate
        • paintComponent
        • updateMouseAdapter
      • MenuHelp
      • MenuItemPainter
      • SimpleMenuItemPainter
  • Launcher

Giulio Zaccaroni

Codice Prodotto

  • [root]/.github
    • workflows
      • release_please.yml
      • check_pr.yml
  • data
    • models
      • ArmyDtos
      • GeomtryDtos
      • SimulationEnvironmentDtos
      • WorldDtos
    • samples
      • EncodedSamples
  • domain
    • engine
      • prolog
        • PrologEngine
        • PrologPredicates
      • simulation
        • SimulationEngine
        • components
          • SimulationComponent
          • MovementSimulationComponent
    • model
      • common
        • validation
          • CommonValidators
          • Validation
        • Disposable
        • Geometry (no MultiPolygon)
        • Listen
        • Math
        • Movement
      • fight
        • SimulationEvent
        • TargetFinderStrategy (no TargetFinderStrategyImpl)
    • presentation
      • common
        • StringToColor
      • model
        • MenuItems

Alex Baiardi

Codice Prodotto

  • domain
    • engine
      • simulation
        • components
          • AttackSimulationComponent
    • model
      • common
        • Geometry
          • Multypolygon
        • Resources
      • fight
        • Army
        • Fight
        • TargetFinderStrategy
          • TargetFinderStrategyImpl
      • world
        • Relations
        • World

Code coverage

Mediante la libreria Jacoco abbiamo calcolato la copertura dei test del nostro codice. Le classi coperte sono il 57% perché purtroppo utilizzare Swing in modalità headless per eseguire test automatici non è banale e quindi abbiamo testato solo i livelli di domain e data.

Screenshot 2022-12-21 at 18 48 44

Screenshot 2022-12-21 at 18 49 30

Clone this wiki locally