Skip to content

Implementazione - Monti Michele

Panoramica

Il mio contributo in questo progetto è stato concentrato principalmente nello sviluppo delle parti seguenti, seguendo i principi della programmazione funzionale e dell'immutabilità.

  • MapGenerator: implementazione della generazione procedurale di mappe terreno e meteo utilizzando distribuzioni gaussiane per pattern naturali.
  • DamageCalculator: implementazione di calcolatori di danno e statistiche di allagamento.
  • Timer: sviluppo di un timer immutabile per il tracciamento del tempo di simulazione.
  • GridDSL: sviluppo di un Domain Specific Language per la costruzione di griglie di simulazione alluvione tramite una sintassi dichiarativa.
  • ReportGenerator: sviluppo del generatore di report di simulazione con esportazione su file.

Nello sviluppo si è mantenuta una forte enfasi sull'immutabilità e sulla trasparenza referenziale, utilizzando tipi algebrici e strutture dati persistenti.

MapGenerator

Architettura Modulare con Mixin Trait

Il MapGenerator è progettato seguendo un'architettura modulare basata su mixin trait che permette una composizione flessibile e l'estensione per nuovi tipi di mappe.

Il trait base fornisce le funzionalità comuni a tutti i generatori di mappe:

scala
sealed trait BaseMapGenerator

Caratteristiche principali:

  • Centro randomizzato: evita pattern prevedibili generando il centro in una zona marginata
  • Distanza normalizzata: standardizza le distanze per lavorare con valori tra 0 e 1
  • Distribuzione gaussiana: base matematica per pattern naturali

In particolare nel metodo generateDistributedGrid viene implementata la logica di creazione della mappa prescindendo dal tipo di cella ce dovrà popolarla (per il momento Cell o Weather) grazie al tipo generico T.

scala
protected def generateDistributedGrid[T](size: Int, selector: (Double, Double) => T): Map[Coordinate, T] =
val m      = margin(size, 0.1)
val center = randomCenter(size, m)
            (for
              x <- 0 until size
              y <- 0 until size
            yield
              val distance           = sqrt(pow(x - center.x, 2) + pow(y - center.y, 2))
              val normalizedDistance = distance / (size / 2.0)
              val randomValue        = rand.nextDouble()
              Coordinate(x, y) -> selector(normalizedDistance, randomValue)
          ).toMap

Il trait BaseMapGenerator viene esteso da due trait specifici per i generatori di terreno e meteo. Questi trait implementano le logiche specifiche per i rispettivi tipi di mappe aderendo all'interfaccia comune.

scala
sealed trait GroundMapGenerator extends BaseMapGenerator

sealed trait SkyMapGenerator extends BaseMapGenerator

Infine esponiamo un MapGenerator che combina entrambi i generatori, fornendo un punto di accesso unico per la generazione di mappe.

scala
object MapGenerator extends GroundMapGenerator with SkyMapGenerator

Questo approccio ci garantisce l'estensibilità del codice, permettendo di aggiungere facilmente nuovi tipi di mappe in futuro semplicemente creando nuovi trait che estendano BaseMapGenerator.

DamageCalculator

Il DamageCalculator fornisce metriche per l'analisi della situazione di allagamento, implementando pattern funzionali per l'elaborazione delle griglie.

Metriche Principali

scala
object DamageCalculator extends DamageCalculatorLike:
    def averageWaterHeight(grid: Grid[Cell]): Double =
        val cells = extractCells(grid)
        cells.map(_.waterHeight).sum / cells.size
    
    def floodedMapPercentage(grid: Grid[Cell], floodThreshold: Double): Double =
        val cells = extractCells(grid)
        percentageOf(cells, allCells, cell => floodedCellPercentage(cell) >= floodThreshold)
    
    def floodedHousesPercentage(grid: Grid[Cell], floodThreshold: Double): Double =
        val cells = extractCells(grid)
        percentageOf(cells, isHouse, cell => floodedCellPercentage(cell) >= floodThreshold)

Diverse funzioni hanno in comune il calcolo di una percentuale che però varia in base a filtri e condizioni. Il metodo privato percentageOf risolve questa esigenza con una funzione higher-order che richiede una funzione per filtrare i tipi di Cell che vogliamo analizzare e una che funga da condizione da soddisfare.

scala
private def percentageOf(cells: Seq[Cell], cellFilter: Cell => Boolean, condition: Cell => Boolean): Double =
    cells.filter(cellFilter) match
        case Seq()         => 0.0
        case filteredCells => filteredCells.count(condition) * 100.0 / filteredCells.size

Timer

Il Timer è implementato come classe immutabile che traccia il tempo trascorso in secondi.

scala
final class Timer private (private val totalSeconds: Int):
    def incrementSeconds(amount: Int): Timer =
      new Timer(totalSeconds + amount)
    
    def reset(): Timer =
      new Timer(0)
    
    def formattedElapsed: String =
        val minutes = totalSeconds / 60
        val seconds = totalSeconds % 60
        "%02d:%02d".format(minutes, seconds)

Ogni volta che il tempo deve essere aggiornato, viene restituita una nuova istanza di Timer con il tempo aggiornato, mantenendo l'immutabilità.

GridDSL

La normale creazione di una griglia espone dei metodi per una veloce creazione di mappe omogenee. Tuttavia, per testare scenari specifici o composizioni particolari ci si trova costretti a costruire una sequenza di celle manualmente e poi richiamarle singolarmente per impostare un valore di acqua contenuta. Per facilitare questo compito, è stato sviluppato un Domain Specific Language che permette di creare griglie con una sintassi dichiarativa e compatta.

Il DSL è costruito attorno al concetto di composizione orizzontale (|) e verticale (||) di celle:

scala
 --- | "30" | "50" ||
"10" | "20" | ---

Questo crea una griglia 2x3 con valori di percentuale di allagamento.

La classe GridBuilder accumula i valori delle celle e costruisce la griglia finale:

scala
final case class GridBuilder(cells: Vector[Vector[CellValue]]):
    def build(): DistanceGrid[Cell] =
        val height = cells.size
        val width  = cells.headOption.map(_.size).getOrElse(0)
        
        val gridMap = cells.zipWithIndex.flatMap { case (row, y) =>
          row.zipWithIndex.map { case (cellValue, x) =>
            Coordinate(x, y) -> createCell(cellValue)
          }
        }.toMap
        
        MapBasedDistanceGrid(MapBasedGrid(width, height, gridMap))

Mentre gli extension methods definiscono gli operatori di composizione:

scala
extension (left: GridStarter | String | GridBuilder)
    infix def |(right: GridStarter | String): GridBuilder

    infix def ||(right: GridStarter | String): GridBuilder

Infine, una given conversion permette l'uso automatico del builder dove è richiesta una DistanceGrid:

scala
given Conversion[GridBuilder, DistanceGrid[Cell]] = _.build()

ReportGenerator

Il ReportGenerator crea report formattati dei dati di simulazione con esportazione su file.

Il report fornirà informazioni come:

  • Percentuale di case allagate
  • Percentuale di area allagata
  • Altezza media dell'acqua

E una rappresentazione tabellare della griglia con percentuali di allagamento per cella al momento del report.

scala
private def formatGrid(grid: Grid[Cell], delimiter: String): String =
    val rows = (0 until grid.height).map { y =>
        val rowValues = (0 until grid.width).map { x =>
            val coordinate = Coordinate(x, y)
            grid.getElementAt(coordinate)
                .map(cellElement => "%3d".format(DamageCalculator.floodedCellPercentage(cellElement).toInt))
                .getOrElse(" 0")
        }
        val formattedRow = rowValues.mkString(delimiter)
        formattedRow + delimiter
    }
    rows.mkString("\n")

Testing

Grazie al DSL, è stato possibile creare test più completi senza compromettere la leggibilità. I test del DamageCalculator in particolare, necessitando di griglie con configurazioni specifiche e talvolta complesse, ne hanno tratto grande beneficio.

scala
"The DamageCalculator" should "verify that 100% of the map is flooded if all cells are flooded" in:
    val grid: DistanceGrid[Cell] =
      "100" | "100" | "100" ||
       "60" |  "70" |  "80" ||
       "55" |  "90" | "100"

    damageCalculator.floodedMapPercentage(grid, floodThreshold = 50.0) shouldBe 100.0

Qui ad esempio è stato possibile creare una griglia 3x3 con celle di diversa percentuale di allagamento in modo compatto e leggibile, facilitando la comprensione del test.