Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 22 additions & 52 deletions src/main/scala/floodsim/model/cell/Cell.scala
Original file line number Diff line number Diff line change
@@ -1,90 +1,60 @@
package floodsim.model.cell

import floodsim.model.*

enum CellType:
case HouseWithConcrete, HouseWithGrass, Square, Field
import floodsim.model.grid.Coordinate

sealed trait Cell:
def coordinate: Coordinate
def dimensions: Double
def waterVolume: Double
def obstacles: Seq[Obstacle]
def altitude: Double
def absorption: AbsorptionRate.AbsorptionRate
def cellType: CellType
def physics: CellPhysics

def waterHeight: Double =
CellPhysics.calculateWaterHeight(dimensions, waterVolume, obstacles)
physics.calculateWaterHeight(dimensions, waterVolume, obstacles)

def waterSpeed: Double =
CellPhysics.calculateWaterSpeed(dimensions, obstacles)
physics.calculateWaterSpeed(dimensions, obstacles)

def updateWater(newWaterVolume: Double): Cell =
val (absorbedWater, newAbsorption) = CellPhysics.calculateAbsorption(waterVolume, absorption)
val (absorbedWater, newAbsorption) =
physics.calculateAbsorption(waterVolume, absorption)

withUpdatedValues(
waterVolume = waterVolume - absorbedWater + newWaterVolume,
absorption = newAbsorption
)

def totalHeight: Double = altitude + waterHeight

def asSymbol: String = cellType.symbol

protected def withUpdatedValues(waterVolume: Double, absorption: AbsorptionRate.AbsorptionRate): Cell

private final case class CellImpl(
final private case class BaseCell(
coordinate: Coordinate,
dimensions: Double,
waterVolume: Double,
obstacles: Seq[Obstacle],
altitude: Double,
absorption: AbsorptionRate.AbsorptionRate,
cellType: CellType
cellType: CellType,
physics: CellPhysics
) extends Cell:
protected def withUpdatedValues(waterVolume: Double, absorption: AbsorptionRate.AbsorptionRate): Cell =
copy(waterVolume = math.max(0.0, waterVolume), absorption = absorption)

object Cell:
private val ConcreteAbsorption: AbsorptionRate.AbsorptionRate = AbsorptionRate(0.2)
private val GrassAbsorption: AbsorptionRate.AbsorptionRate = AbsorptionRate(0.5)
private val SquareAbsorption: AbsorptionRate.AbsorptionRate = AbsorptionRate(0.6)
private val FieldAbsorption: AbsorptionRate.AbsorptionRate = AbsorptionRate(0.9)

final class Builder private[Cell] (
val dimensions: Double,
val altitude: Double,
val obstacles: Seq[Obstacle],
val absorption: AbsorptionRate.AbsorptionRate,
val waterVolume: Double,
val cellType: CellType
):
def withObstacles(obstacles: Seq[Obstacle]): Builder =
new Builder(dimensions, altitude, obstacles, absorption, waterVolume, cellType)

def withWaterVolume(waterVolume: Double): Builder =
new Builder(dimensions, altitude, obstacles, absorption, waterVolume, cellType)

def withAbsorption(absorption: AbsorptionRate.AbsorptionRate): Builder =
new Builder(dimensions, altitude, obstacles, absorption, waterVolume, cellType)

def build: Cell = CellImpl(
dimensions = dimensions,
waterVolume = waterVolume,
obstacles = obstacles,
altitude = altitude,
absorption = absorption,
cellType = cellType
)

def apply(cellType: CellType, dimensions: Double, altitude: Double): Builder =
val absorption = cellType match
case CellType.HouseWithConcrete => ConcreteAbsorption
case CellType.HouseWithGrass => GrassAbsorption
case CellType.Square => SquareAbsorption
case CellType.Field => FieldAbsorption

new Builder(
def apply(cellType: CellType, dimensions: Double, altitude: Double, coordinate: Coordinate): Cell =
BaseCell(
coordinate = coordinate,
dimensions = dimensions,
altitude = altitude,
obstacles = Seq.empty,
absorption = absorption,
waterVolume = 0.0,
cellType = cellType
obstacles = Seq.empty,
altitude = altitude,
absorption = cellType.defaultAbsorption,
cellType = cellType,
physics = DefaultCellPhysics
)
16 changes: 12 additions & 4 deletions src/main/scala/floodsim/model/cell/CellPhysics.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,18 @@ package floodsim.model.cell

import scala.math.*

object CellPhysics:
val AbsorptionFactor: Double = 0.1
val AbsorptionDecayFactor: Double = 0.01
val MaxWaterSpeed: Double = 10.0
trait CellPhysics:
def calculateWaterHeight(dimensions: Double, waterVolume: Double, obstacles: Seq[Obstacle]): Double
def calculateWaterSpeed(dimensions: Double, obstacles: Seq[Obstacle]): Double
def calculateAbsorption(
waterVolume: Double,
absorption: AbsorptionRate.AbsorptionRate
): (Double, AbsorptionRate.AbsorptionRate)

object DefaultCellPhysics extends CellPhysics:
private val AbsorptionFactor: Double = 0.1
private val AbsorptionDecayFactor: Double = 0.01
private val MaxWaterSpeed: Double = 10.0

def calculateWaterHeight(dimensions: Double, waterVolume: Double, obstacles: Seq[Obstacle]): Double =
if waterVolume <= 0 then 0.0
Expand Down
7 changes: 7 additions & 0 deletions src/main/scala/floodsim/model/cell/CellType.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package floodsim.model.cell

enum CellType(val symbol: String, val defaultAbsorption: AbsorptionRate.AbsorptionRate):
case HouseWithConcrete extends CellType("C", AbsorptionRate(0.2))
case HouseWithGrass extends CellType("G", AbsorptionRate(0.5))
case Square extends CellType("S", AbsorptionRate(0.6))
case Field extends CellType("F", AbsorptionRate(0.9))
144 changes: 84 additions & 60 deletions src/test/scala/floodsim/model/cell/CellPhysicsTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,83 +5,107 @@ import org.scalatest.matchers.should.Matchers

class CellPhysicsTest extends AnyFlatSpec with Matchers:

val dimensions = 10.0
val smallObstacle: Obstacle = Obstacle(10.0, 3.0)
val largeObstacle: Obstacle = Obstacle(50.0, 5.0)

"CellPhysics" should "calculate water height correctly with no obstacles" in {
val waterVolume = 100.0
val obstacles = Seq.empty[Obstacle]
val height = CellPhysics.calculateWaterHeight(dimensions, waterVolume, obstacles)
val expectedHeight = waterVolume / (dimensions * dimensions)
height should be(expectedHeight)
"DefaultCellPhysics" should "calculate zero water height when water volume is zero or negative" in {
val physics = DefaultCellPhysics
val dimensions = 10.0
val obstacles = Seq.empty[Obstacle]
physics.calculateWaterHeight(dimensions, 0.0, obstacles) shouldBe 0.0
physics.calculateWaterHeight(dimensions, -1.0, obstacles) shouldBe 0.0
}

it should "calculate water height correctly with obstacles" in {
val waterVolume = 100.0
val obstacles = Seq(smallObstacle)
val height = CellPhysics.calculateWaterHeight(dimensions, waterVolume, obstacles)
val availableArea = dimensions * dimensions - smallObstacle.surface
val expectedHeight = waterVolume / availableArea
height should be(expectedHeight)
it should "calculate correct water height with no obstacles" in {
val physics = DefaultCellPhysics
val dimensions = 10.0
val waterVolume = 100.0
val obstacles = Seq.empty[Obstacle]
// Area = 10*10 = 100, height = volume/area = 100/100 = 1.0
physics.calculateWaterHeight(dimensions, waterVolume, obstacles) shouldBe 1.0
}

it should "return zero height when no water" in {
val waterVolume = 0.0
val obstacles = Seq(smallObstacle)
val height = CellPhysics.calculateWaterHeight(dimensions, waterVolume, obstacles)
height should be(0.0)
it should "calculate correct water height with obstacles" in {
val physics = DefaultCellPhysics
val dimensions = 10.0
val waterVolume = 100.0
val obstacles = Seq(
Obstacle(20.0, 5.0),
Obstacle(10.0, 3.0)
)
// Area = 10*10 - 20 - 10 = 70, height = volume/area = 100/70 = 1.428...
physics.calculateWaterHeight(dimensions, waterVolume, obstacles) shouldBe (100.0 / 70.0)
}

it should "handle edge case when all area is occupied by obstacles" in {
val waterVolume = 100.0
val fullObstacle = Obstacle(dimensions * dimensions, 10.0)
val height = CellPhysics.calculateWaterHeight(dimensions, waterVolume, Seq(fullObstacle))
height should be(0.0)
it should "return zero water height when obstacles cover entire area" in {
val physics = DefaultCellPhysics
val dimensions = 10.0
val waterVolume = 100.0
val obstacles = Seq(
Obstacle(100.0, 5.0) // Covers entire area
)
physics.calculateWaterHeight(dimensions, waterVolume, obstacles) shouldBe 0.0
}

it should "calculate max water speed with no obstacles" in {
val obstacles = Seq.empty[Obstacle]
val speed = CellPhysics.calculateWaterSpeed(dimensions, obstacles)
speed should be(CellPhysics.MaxWaterSpeed)
it should "calculate maximum water speed with no obstacles" in {
val physics = DefaultCellPhysics
val dimensions = 10.0
val obstacles = Seq.empty[Obstacle]
physics.calculateWaterSpeed(dimensions, obstacles) shouldBe 10.0
}

it should "calculate reduced water speed with obstacles" in {
val obstacles = Seq(smallObstacle)
val speed = CellPhysics.calculateWaterSpeed(dimensions, obstacles)
val totalObstacleSurface = smallObstacle.surface
val cellArea = dimensions * dimensions
val expectedSpeed = (1.0 - (totalObstacleSurface / cellArea)) * CellPhysics.MaxWaterSpeed
speed should be(expectedSpeed)
val physics = DefaultCellPhysics
val dimensions = 10.0
val obstacles = Seq(
Obstacle(25.0, 3.0) // 25% of area
)
// obstacleFactor = 1.0 - (25.0 / 100.0) = 0.75, speed = 0.75 * 10.0 = 7.5
physics.calculateWaterSpeed(dimensions, obstacles) shouldBe 7.5
}

it should "calculate zero water speed when obstacles cover entire area" in {
val physics = DefaultCellPhysics
val dimensions = 10.0
val obstacles = Seq(
Obstacle(100.0, 5.0), // Covers entire area
Obstacle(50.0, 3.0) // This doesn't matter as we cap at 1.0
)
physics.calculateWaterSpeed(dimensions, obstacles) shouldBe 0.0
}

it should "calculate zero water speed when all area is blocked" in {
val fullObstacle = Obstacle(dimensions * dimensions, 10.0)
val speed = CellPhysics.calculateWaterSpeed(dimensions, Seq(fullObstacle))
speed should be(0.0)
it should "not absorb water when absorption rate is zero" in {
val physics = DefaultCellPhysics
val waterVolume = 100.0
val absorption = AbsorptionRate(0.0)
val (absorbedWater, newAbsorption) = physics.calculateAbsorption(waterVolume, absorption)
absorbedWater shouldBe 0.0
newAbsorption.value shouldBe 0.0
}

it should "calculate absorption correctly" in {
val waterVolume = 100.0
val absorption = AbsorptionRate(0.5)
val (absorbed, newAbsorption) = CellPhysics.calculateAbsorption(waterVolume, absorption)
val expectedAbsorbed = absorption.value * CellPhysics.AbsorptionFactor
absorbed should be(expectedAbsorbed)
val expectedNewAbsorption = absorption.value - (expectedAbsorbed * CellPhysics.AbsorptionDecayFactor)
newAbsorption.value should be(expectedNewAbsorption)
it should "absorb water correctly and decrease absorption rate" in {
val physics = DefaultCellPhysics
val waterVolume = 100.0
val absorption = AbsorptionRate(0.5)
// maxAbsorption = 0.5 * 0.1 = 0.05
// absorbedWater = min(100.0, 0.05) = 0.05
// newAbsorption = 0.5 - (0.05 * 0.01) = 0.5 - 0.0005 = 0.4995
val (absorbedWater, newAbsorption) = physics.calculateAbsorption(waterVolume, absorption)
newAbsorption.value shouldBe 0.4995 +- 0.0001
}

it should "not absorb more water than available" in {
val waterVolume = 0.01
val absorption = AbsorptionRate(0.5)
val (absorbed, _) = CellPhysics.calculateAbsorption(waterVolume, absorption)
absorbed should be <= waterVolume
it should "absorb only available water if less than max absorption" in {
val physics = DefaultCellPhysics
val waterVolume = 0.02
val absorption = AbsorptionRate(0.5)
// maxAbsorption = 0.5 * 0.1 = 0.05
// absorbedWater = min(0.02, 0.05) = 0.02
// newAbsorption = 0.5 - (0.02 * 0.01) = 0.5 - 0.0002 = 0.4998
val (absorbedWater, newAbsorption) = physics.calculateAbsorption(waterVolume, absorption)
newAbsorption.value shouldBe 0.4998 +- 0.0001
}

it should "not absorb when absorption rate is zero" in {
val waterVolume = 100.0
val absorption = AbsorptionRate(0.0)
val (absorbed, newAbsorption) = CellPhysics.calculateAbsorption(waterVolume, absorption)
absorbed should be(0.0)
newAbsorption.value should be(0.0)
it should "not change absorption rate when no water is absorbed" in {
val physics = DefaultCellPhysics
val waterVolume = 0.0
val absorption = AbsorptionRate(0.5)
val (absorbedWater, newAbsorption) = physics.calculateAbsorption(waterVolume, absorption)
newAbsorption.value shouldBe 0.5
}
Loading
Loading