Model-View-Controller
In questa sezione ci si concentra sull'implementazione del Model, View e Controller della simulazione, seguendo il pattern architetturale Model-View-Controller (MVC) corredato dal Cake Pattern per la gestione delle dipendenze.
ModelModule
Il trait Model[S]
è parametrizzato sul tipo di stato S
, che deve estendere State
.
Esso definisce l’interfaccia per aggiornare lo stato della simulazione in modo funzionale e sicuro.
Il metodo di aggiornamento è così definito:
def update(s: S)(using f: S => IO[S]): IO[S]
Esso accetta:
- lo stato corrente
s
; - una funzione di aggiornamento
f
, che produce, in modo asincrono, un nuovo stato incapsulato inIO
.
Grazie al parametro di contesto using
la logica di aggiornamento viene passata implicitamente: il Model
non deve conoscere quali eventi o regole hanno causato l’aggiornamento; si limita ad applicare la funzione
ricevuta.
Nota: lo stato è immutabile: ogni aggiornamento produce una nuova istanza di
State
, mantenendo l’integrità e la coerenza dei dati.
Il modulo segue il Cake Pattern, articolandosi in:
Provider[S]
: espone un’istanza concreta diModel
agli altri moduli, permettendo l’iniezione delle dipendenze;Component[S]
: fornisce l’implementazione concreta delModel
;Interface[S]
: combinaProvider
eComponent
, come interfaccia unificata del modulo.
ModelImpl
implementa update
semplicemente delegando l’aggiornamento alla funzione passata tramite using
, rendendo l’applicazione della logica di trasformazione completamente modulare e riutilizzabile.
ControllerModule
Il trait Controller[S]
è parametrizzata sul tipo di stato S
, che estende ModelModule.State
.
Esso espone due metodi:
start
: avvia la simulazione;simulationLoop
: esegue il ciclo principale della simulazione.
def start(initialState: S): IO[S]
def simulationLoop(s: S, queue: Queue[IO, Event]): IO[S]
Il Controller
è responsabile dell’avvio della simulazione, della gestione del ciclo di esecuzione e
degli eventi oltre alla comunicazione tra il Model
e la View
.
L’implementazione segue un approccio modulare e funzionale, sfruttando cats.effect
per la gestione della concorrenza
ed effetti asincroni.
Il modulo segue il Cake Pattern, articolandosi in:
Provider[S]
: espone un’istanza concreta diController
permettendo l’iniezione del controller nei moduli che ne hanno bisogno;Component[S]
: fornisce l’implementazione concreta delController
(richiedeModelModule.Provider
eViewModule.Provider
);Interface[S]
: combinaProvider
eComponent
, operando da interfaccia unificata del modulo.
Avvio della simulazione
Il metodo start
inizializza la simulazione creando una coda di eventi e avviando il ciclo di simulazione:
- utilizza
Queue.unbounded[IO, Event]
per creare una coda di eventi asincrona e non bloccante; - avvia la View chiamando
context.view.init
, che prepara l’interfaccia utente; - esegue i comportamenti dei robot con
runBehavior
, che raccoglie in parallelo le proposte di azione dei robot; - infine, avvia il ciclo principale chiamando
simulationLoop
passando lo stato iniziale e la coda degli eventi.
Ciclo di simulazione
Il metodo simulationLoop
implementa una funzione ricorsiva che:
- esegue i comportamenti dei robot se lo stato è
RUNNING
; - recupera ed elabora gli eventi dalla coda (
handleEvents
); - aggiorna la vista con lo stato corrente (
context.view.render
); - verifica la condizione di stop tramite
handleStopCondition
, che gestisce lo stato di terminazione della simulazione:STOPPED
: chiusura della view;ELAPSED_TIME
: passa alla view lo stato finale.
- se la simulazione non termina, esegue
nextStep
in base allo stato:RUNNING
: eseguetickEvents
, calcolando il tempo trascorso e regolando il tick in modo preciso;PAUSED
: sospende il ciclo per un breve intervallo (50 ms
);- altri stati: restituisce lo stato corrente senza modifiche.
- ripete ricorsivamente il loop.
Gestione degli eventi
La gestione degli eventi è stata resa modulare e funzionale tramite due metodi:
handleEvents
: processa una sequenza di eventi in ordine, applicando ciascun evento allo stato corrente;handleEvent
: gestisce un singolo evento, aggiornando lo stato tramite le logiche definite nelLogicsBundle
.
Gli eventi (Event
) comprendono:
Tick
: avanzamento temporale della simulazione;TickSpeed
: modifica della velocità dei tick;Random
: aggiornamento del generatore casuale;Pause
/Resume
/Stop
: controllo dello stato della simulazione;RobotActionProposals
: gestione delle proposte di azione (RobotProposal
) dei robot a ogni tick.
Ogni evento nel Controller
viene trasformato in un aggiornamento dello stato tramite le logiche definite nel
LogicsBundle
.
Il LogicsBundle
viene passato implicitamente al controller come given
e utilizzato con la keyword using
quando il
Controller
chiama il metodo update
del model.
Tick
// ...
case Event.Tick(elapsed) =>
context.model.update(s)(using context.logics.tick(elapsed))
// ...
In questo modo:
- il
Controller
non modifica direttamente lo stato, ma delega tutte le trasformazioni alModel
, specificando quale logica applicare (tick
,pause
,stop
,resume
, ecc.); - il
Model
applica la logica appropriata e restituisce il nuovo stato aggiornato.
Questo consente al Controller
di continuare il ciclo della simulazione con lo stato corretto, senza occuparsi
direttamente delle regole di aggiornamento o dei dettagli della business logic.
LogicsBundle
Il LogicsBundle
raccoglie le funzioni che definiscono come lo stato della simulazione viene aggiornato in risposta a
diversi eventi.
Ogni funzione prende lo stato corrente e, se necessario, parametri aggiuntivi, restituendo un nuovo stato aggiornato.
Le funzioni incluse sono:
-
tick
: aggiorna lo stato della simulazione avanzando il tempo trascorso e, se necessario, modificando lo stato in base al tempo massimo raggiunto; -
tickSpeed
: modifica la velocità della simulazione; -
random
: aggiorna il generatore di numeri casuali nello stato; -
pause
: mette la simulazione in pausa aggiornando lo stato; -
resume
: riprende la simulazione aggiornando lo stato; -
stop
: ferma la simulazione aggiornando lo stato; -
robotActions
: gestisce le proposte di azione dei robot (RobotProposal
) e aggiorna lo stato della simulazione (SimulationState
) di conseguenza.Per ciascuna proposta:
- si applica l’azione del robot all’ambiente usando una ricerca binaria per calcolare la massima durata di movimento sicura ( evitando collisioni con altri oggetti o robot);
- i movimenti di tutti i robot vengono calcolati in parallelo usando
parTraverse
; - i robot aggiornati sostituiscono quelli originali nell’ambiente simulato, mantenendo la
validità dell’ambiente tramite la funzione di
validate
; - se la validazione fallisce, lo stato dell’ambiente non viene modificato.
Esecuzione dei comportamenti dei robot
Il metodo runBehavior
seleziona tutte le entità di tipo Robot
presenti nell’ambiente.
Per ciascun robot:
- legge i sensori (
senseAll
); - costruisce un
BehaviorContext
(letture sensori + RNG) e calcola l’azione conrobot.behavior.run
; - aggiorna il generatore casuale della simulazione (
Event.Random
) con quello restituito dal comportamento; - crea una proposta di azione (
RobotProposal
); - inserisce in coda un evento
RobotActionProposals
contenente tutte le proposte di azione raccolte.
Questo approccio permette di calcolare i comportamenti in parallelo, riducendo i tempi di elaborazione e mantenendo l’aggiornamento dello stato coerente.
ViewModule
Il trait View[S]
definisce l’interfaccia della view, parametrizzata sul tipo di stato S
che estende
ModelModule.State
. La view espone quattro operazioni principali:
init
: inizializza la view e collega la coda degli eventi del controller, in modo che la view possa ricevere e reagire agli eventi;render
: aggiorna la visualizzazione in base allo stato corrente della simulazione, mostrando i cambiamenti dell'ambiente e delle entità;close
: chiude la view e libera le risorse;timeElapsed
: gestisce le azioni da eseguire quando il tempo massimo della simulazione è raggiunto.
def init(queue: Queue[IO, Event]): IO[Unit]
def render(state: S): IO[Unit]
def close(): IO[Unit]
def timeElapsed(state: S): IO[Unit]
Queste operazioni sono tutte implementate come effetti IO
, consentendo di gestire in modo sicuro e non bloccante
l’aggiornamento della UI
e la sincronizzazione con il ciclo di simulazione.
Il modulo segue il Cake Pattern, articolandosi in:
Provider[S]
: espone un’istanza concreta dellaView
agli altri moduli, supportando l’iniezione delle dipendenze;Component[S]
: definisce l’implementazione concreta della view tramitemakeView()
, usato per costruire nuove istanze (richiedeControllerModule.Provider
);Interface[S]
: combinaProvider
eComponent
, offrendo un’unica interfaccia al resto della simulazione.
In questo modo, la View
resta indipendente e intercambiabile: è possibile fornire implementazioni diverse (ad esempio, CLI o GUI) senza modificare le logiche di simulazione.