diff --git a/backend/src/main/scala/model/SimulationState.scala b/backend/src/main/scala/model/SimulationState.scala index 4f4eb10..4fcda02 100644 --- a/backend/src/main/scala/model/SimulationState.scala +++ b/backend/src/main/scala/model/SimulationState.scala @@ -35,7 +35,8 @@ case class SimulationState( games: List[Game], spawner: Option[Spawner], walls: List[Wall], - ticker: Ticker = Ticker(60.0) + ticker: Ticker = Ticker(60.0), + frameRate: Double = 60.0 ) /** Factory and utility object for creating SimulationState instances. @@ -275,3 +276,15 @@ extension (state: SimulationState) */ def addWall(wall: Wall): SimulationState = state.copy(walls = wall :: state.walls) + + /** Creates a new SimulationState with an updated framerate. + * + * Ticker is updated according to framerate + * + * @param frameRate + * the new framerate + * @return + * new SimulationState with the new framerate + */ + def updateFrameRate(frameRate: Double): SimulationState = + state.copy(frameRate = frameRate, ticker = Ticker(frameRate)) diff --git a/backend/src/main/scala/model/entities/Player.scala b/backend/src/main/scala/model/entities/Player.scala index ee534c7..71ae38c 100644 --- a/backend/src/main/scala/model/entities/Player.scala +++ b/backend/src/main/scala/model/entities/Player.scala @@ -9,3 +9,7 @@ trait Player[T <: Player[T]] extends Movable[T] with Entity: def isPlaying: Boolean def play(game: Game): T def stopPlaying: T + +trait ChangingFavouriteGamePlayer[T <: ChangingFavouriteGamePlayer[T]] + extends Player[T]: + def withFavouriteGame(gameType: GameType): T diff --git a/backend/src/main/scala/model/entities/customers/Bankroll.scala b/backend/src/main/scala/model/entities/customers/Bankroll.scala index d19dcc8..054ba11 100644 --- a/backend/src/main/scala/model/entities/customers/Bankroll.scala +++ b/backend/src/main/scala/model/entities/customers/Bankroll.scala @@ -16,4 +16,6 @@ trait Bankroll[T <: Bankroll[T]]: ) withBankroll(newBankroll, true) + def bankrollRatio: Double = bankroll / startingBankroll + def withBankroll(newBankroll: Double, update: Boolean = false): T diff --git a/backend/src/main/scala/model/entities/customers/Customer.scala b/backend/src/main/scala/model/entities/customers/Customer.scala index 98f30b1..49c527b 100644 --- a/backend/src/main/scala/model/entities/customers/Customer.scala +++ b/backend/src/main/scala/model/entities/customers/Customer.scala @@ -3,8 +3,8 @@ package model.entities.customers import scala.util.Random import model.SimulationState +import model.entities.ChangingFavouriteGamePlayer import model.entities.Entity -import model.entities.Player import model.entities.customers.CustState.Idle import model.entities.customers.CustState.Playing import model.entities.customers.RiskProfile.Regular @@ -18,6 +18,7 @@ import model.managers.movements.Boids._ import model.managers.movements.PlayerManagers import model.managers.movements.PlayerManagers.GamesAttractivenessManager import model.managers.movements.PlayerManagers.PlayerSitterManager +import model.managers.movements.RandomMovementManager import model.managers.| import utils.Vector2D @@ -43,13 +44,13 @@ case class Customer( StatusProfile, CustomerState[Customer], HasBetStrategy[Customer], - Player[Customer]: + ChangingFavouriteGamePlayer[Customer]: def withId(newId: String): Customer = this.copy(id = newId) def withPosition(newPosition: Vector2D): Customer = - this.copy(position = newPosition, previousPosition = Some(position)) + this.copy(position = newPosition) def withBankroll(newRoll: Double, update: Boolean = false): Customer = if update then this.copy(bankroll = newRoll) @@ -62,7 +63,12 @@ case class Customer( this.copy(frustration = newFrustration) def withCustomerState(newState: CustState): Customer = - this.copy(customerState = newState) + this.copy( + customerState = newState, + previousPosition = newState match + case Playing(_) => Some(position) + case _ => None + ) def withDirection(newDirection: Vector2D): Customer = this.copy(direction = newDirection) @@ -100,6 +106,9 @@ case class Customer( case Playing(_) => true case _ => false + override def withFavouriteGame(gameType: GameType): Customer = + copy(favouriteGame = gameType) + /** This manager implements the default behaviour for the customer. It combines * the boid-like behaviours, the games' attractiveness and avoids the * collisions of customers with walls and games @@ -113,25 +122,29 @@ case class DefaultMovementManager( separationWeight: Double = 1.0, gamesAttractivenessWeight: Double = 1.0, sittingRadius: Double = 100, - boredomIncrease: Double = 0.1 + boredomIncrease: Double = 0.1, + randomMovementWeight: Double = 0 ) extends BaseManager[SimulationState]: override def update(slice: SimulationState): SimulationState = slice | FilterManager( - GamesAttractivenessAdapter( - gamesAttractivenessWeight * GamesAttractivenessManager(boredomIncrease) - | PlayerSitterManager(sittingRadius) + BoidsAdapter( + PerceptionLimiterManager(perceptionRadius) + | alignmentWeight * AlignmentManager() + | cohesionWeight * CohesionManager() + | separationWeight * SeparationManager(avoidRadius) + | VelocityLimiterManager(maxSpeed) ) - | BoidsAdapter( - PerceptionLimiterManager(perceptionRadius) - | alignmentWeight * AlignmentManager() - | cohesionWeight * CohesionManager() - | separationWeight * SeparationManager(avoidRadius) - | VelocityLimiterManager(maxSpeed) + | SingleCustomerAdapter(randomMovementWeight * RandomMovementManager()) + | GamesAttractivenessAdapter( + gamesAttractivenessWeight * GamesAttractivenessManager( + boredomIncrease + ) + | PlayerSitterManager(sittingRadius) ) ) | WallAvoidingAdapter(AvoidObstaclesManager()) - | BoidsAdapter(MoverManager()) + | SingleCustomerAdapter(MoverManager()) /** This manager adapts the `manager` which updates players contexts to one * which manipulates `SimulationState`. The contexts are updated one-by-one, @@ -216,3 +229,15 @@ private case class FilterManager(manager: BaseManager[SimulationState]) games = slice.games.map(g => updatedState.games.find(_.id == g.id).getOrElse(g)) ) + +/** This manager adapts a base manager that handles a single customer to accept + * a simulation state + * @param manager + * the adapted manager + */ +private case class SingleCustomerAdapter(manager: BaseManager[Customer]) + extends BaseManager[SimulationState]: + override def update(slice: SimulationState): SimulationState = + slice.copy( + customers = slice.customers.map(_ | manager) + ) diff --git a/backend/src/main/scala/model/entities/games/Game.scala b/backend/src/main/scala/model/entities/games/Game.scala index 9ce6a54..5f407e9 100644 --- a/backend/src/main/scala/model/entities/games/Game.scala +++ b/backend/src/main/scala/model/entities/games/Game.scala @@ -25,6 +25,9 @@ object Roulette extends GameType /** Game type representing blackjack games */ object Blackjack extends GameType +/** Game's types present in the current implementation */ +val gameTypesPresent: Seq[GameType] = Seq(SlotMachine, Roulette, Blackjack) + /** Abstract base trait for all casino games in the simulation. * * Represents a game entity that can be positioned in 2D space, maintains diff --git a/backend/src/main/scala/model/entities/spawner/Spawner.scala b/backend/src/main/scala/model/entities/spawner/Spawner.scala index 9e2a23e..274e64f 100644 --- a/backend/src/main/scala/model/entities/spawner/Spawner.scala +++ b/backend/src/main/scala/model/entities/spawner/Spawner.scala @@ -66,12 +66,17 @@ case class Spawner( else state private def defaultCustomerCreation(): Customer = - val br = Random.between(50, 10000) - val p = br match - case b if b < 100 => Casual - case b if b < 1500 => Regular - case b if b < 5000 => Impulsive - case b if b < 10000 => VIP + val pNumber = Random.between(1, 5) + val p = pNumber match + case 1 => Casual + case 2 => Regular + case 3 => Impulsive + case 4 => VIP + val br = p match + case Casual => Random.between(50, 151) + case Regular => Random.between(200, 1501) + case VIP => Random.between(3000, 8001) + case Impulsive => Random.between(1500, 5001) val fg = Random .shuffle( Seq( @@ -84,7 +89,7 @@ case class Spawner( val bs = fg match case Roulette => MartingaleStrat[Customer](br * 0.02, defaultRedBet) case Blackjack => MartingaleStrat[Customer](br * 0.02, defaultRedBet) - case SlotMachine => FlatBetting[Customer](br * 0.04) + case SlotMachine => FlatBetting[Customer](br * 0.05, defaultRedBet) Customer() .withPosition(this.position.around(5.0)) diff --git a/backend/src/main/scala/model/entities/spawner/SpawningStrategy.scala b/backend/src/main/scala/model/entities/spawner/SpawningStrategy.scala index a3013e2..1779b40 100644 --- a/backend/src/main/scala/model/entities/spawner/SpawningStrategy.scala +++ b/backend/src/main/scala/model/entities/spawner/SpawningStrategy.scala @@ -8,6 +8,8 @@ package model.entities.spawner * step functions. */ trait SpawningStrategy: + protected def hourPerDay: Double = 24.0 + /** Calculates the number of customers to spawn at the given time. * * @param time @@ -51,10 +53,11 @@ case class GaussianStrategy( base: Int = 0 ) extends SpawningStrategy: override def customersAt(time: Double): Int = + val dayTime = time % hourPerDay if (stdDev <= 0) { - if (math.abs(time - mean) < 1e-9) (base + peak).toInt else base + if (math.abs(dayTime - mean) < 1e-9) (base + peak).toInt else base } else { - val exponent = -0.5 * math.pow((time - mean) / stdDev, 2) + val exponent = -0.5 * math.pow((dayTime - mean) / stdDev, 2) val value = base + peak * math.exp(exponent) math.round(value).toInt.max(0) } @@ -83,9 +86,10 @@ case class StepStrategy( endTime: Double ) extends SpawningStrategy: override def customersAt(time: Double): Int = - if (startTime > endTime) then - if (time <= endTime || time >= startTime) then highRate else lowRate - else if (time >= startTime && time <= endTime) then highRate + val dayTime = time % hourPerDay + if startTime > endTime then + if dayTime <= endTime || dayTime >= startTime then highRate else lowRate + else if dayTime >= startTime && dayTime <= endTime then highRate else lowRate /** Builder class for constructing and composing spawning strategies using a diff --git a/backend/src/main/scala/model/managers/DecisionManager.scala b/backend/src/main/scala/model/managers/DecisionManager.scala index 7159a75..5e7ade0 100644 --- a/backend/src/main/scala/model/managers/DecisionManager.scala +++ b/backend/src/main/scala/model/managers/DecisionManager.scala @@ -1,12 +1,14 @@ package model.managers +import scala.util.Random + +import model.entities.ChangingFavouriteGamePlayer import model.entities.Entity import model.entities.Player import model.entities.customers.Bankroll import model.entities.customers.BetStratType import model.entities.customers.BettingStrategy import model.entities.customers.BoredomFrustration -import model.entities.customers.CustState.Idle import model.entities.customers.CustomerState import model.entities.customers.FlatBet import model.entities.customers.FlatBetting @@ -17,20 +19,24 @@ import model.entities.customers.MovableWithPrevious import model.entities.customers.OscarGrind import model.entities.customers.OscarGrindStrat import model.entities.customers.RiskProfile +import model.entities.customers.RiskProfile.Casual import model.entities.customers.RiskProfile.Impulsive import model.entities.customers.RiskProfile.Regular import model.entities.customers.RiskProfile.VIP import model.entities.customers.StatusProfile +import model.entities.customers.defaultRedBet import model.entities.games.Blackjack import model.entities.games.Gain import model.entities.games.Game import model.entities.games.GameType import model.entities.games.Roulette import model.entities.games.SlotMachine +import model.entities.games.gameTypesPresent import utils.DecisionNode import utils.DecisionTree import utils.Leaf import utils.MultiNode +import utils.TriggerDSL.Always import utils.TriggerDSL.BoredomAbove import utils.TriggerDSL.BrRatioAbove import utils.TriggerDSL.BrRatioBelow @@ -45,14 +51,14 @@ case class DecisionManager[ extends BaseManager[Seq[A]]: private val gameList = games.map(_.gameType).distinct // Configuration + private case class Limits(tp: Double, sl: Double) + private case class Modifiers(limits: Limits, bMod: Double, fMod: Double) private object ProfileModifiers: - case class Limits(tp: Double, sl: Double) - case class Modifiers(limits: Limits, bMod: Double, fMod: Double) val modifiers: Map[RiskProfile, Modifiers] = Map( RiskProfile.VIP -> Modifiers(Limits(tp = 3.0, sl = 0.3), 1.30, 0.80), RiskProfile.Regular -> Modifiers(Limits(2.5, 0.3), 1.0, 1.0), RiskProfile.Casual -> Modifiers(Limits(1.5, 0.5), 1.40, 1.30), - RiskProfile.Impulsive -> Modifiers(Limits(5.0, 0.0), 0.70, 1.5) + RiskProfile.Impulsive -> Modifiers(Limits(5.0, 0.1), 0.70, 1.5) ) // Rule & Future External Config case class SwitchRule( @@ -69,20 +75,37 @@ case class DecisionManager[ // VIP SwitchRule(VIP, Blackjack, Martingale, Losses(3), OscarGrind, 0.05), SwitchRule(VIP, Roulette, Martingale, Losses(4), OscarGrind, 0.05), - SwitchRule(VIP, SlotMachine, FlatBet, FrustAbove(50), FlatBet, 0.015), + SwitchRule(VIP, SlotMachine, FlatBet, FrustAbove(50) || BrRatioBelow(0.5), FlatBet, 0.015), + SwitchRule(VIP,SlotMachine,Martingale, Always ,FlatBet,0.03), + SwitchRule(VIP,SlotMachine,OscarGrind, Always ,FlatBet,0.03), + SwitchRule(VIP,Blackjack,FlatBet, Always ,Martingale,0.03), + SwitchRule(VIP,Roulette,FlatBet, Always ,Martingale,0.03), // Regular SwitchRule(Regular, Blackjack, OscarGrind, BrRatioAbove(1.3), Martingale, 0.015), SwitchRule(Regular, Blackjack, Martingale, Losses(3), OscarGrind, 0.02), SwitchRule(Regular, Roulette, OscarGrind, BrRatioAbove(1.3), Martingale, 0.015), SwitchRule(Regular, Roulette, Martingale, Losses(3), OscarGrind, 0.02), - SwitchRule(Regular, SlotMachine, FlatBet, FrustAbove(60), FlatBet, 0.01), + SwitchRule(Regular, SlotMachine, FlatBet, FrustAbove(60) || BrRatioBelow(0.5), FlatBet, 0.01), + SwitchRule(Regular,SlotMachine,Martingale, Always ,FlatBet,0.03), + SwitchRule(Regular,SlotMachine,OscarGrind, Always ,FlatBet,0.03), + SwitchRule(Regular,Blackjack,FlatBet, Always ,OscarGrind,0.02), + SwitchRule(Regular,Roulette,FlatBet, Always ,OscarGrind,0.02), // Casual - + SwitchRule(Casual, SlotMachine, FlatBet, FrustAbove(50) || BrRatioBelow(0.7), FlatBet, 0.015), + SwitchRule(Casual,SlotMachine,Martingale, Always ,FlatBet,0.03), + SwitchRule(Casual,SlotMachine,OscarGrind, Always ,FlatBet,0.03), + SwitchRule(Casual,Blackjack,FlatBet, Always ,OscarGrind,0.03), + SwitchRule(Casual,Roulette,FlatBet, Always ,FlatBet,0.03), // Impulsive SwitchRule(Impulsive, Blackjack, Martingale, Losses(3), OscarGrind, 0.10), SwitchRule(Impulsive, Roulette, Martingale, Losses(3), FlatBet, 0.07), SwitchRule(Impulsive, Roulette, FlatBet, BrRatioAbove(1), Martingale, 0.03), - SwitchRule(Impulsive, SlotMachine, FlatBet, FrustAbove(50), FlatBet, 0.02) + SwitchRule(Impulsive, SlotMachine, FlatBet, FrustAbove(50), FlatBet, 0.02), + SwitchRule(Impulsive,SlotMachine,Martingale, Always ,FlatBet,0.03), + SwitchRule(Impulsive,Blackjack,FlatBet, Always ,Martingale,0.02), + SwitchRule(Impulsive,SlotMachine,OscarGrind, Always ,FlatBet,0.03), + SwitchRule(Impulsive,Roulette,FlatBet, Always ,Martingale,0.03), + ) //format: on object ConfigLoader: @@ -108,22 +131,44 @@ case class DecisionManager[ decision match case ContinuePlaying() => - Some(updateInGameBehaviours(c).updateBoredom(3.0 * mod.bMod)) + Some(updateInGameBehaviours(c, mod).updateBoredom(3.0 * mod.bMod)) case StopPlaying() => - Some(c.changeState(Idle).updateFrustration(-15.0 * (2 - mod.fMod))) + Some(c.stopPlaying.updateFrustration(-15.0 * (2 - mod.fMod))) case ChangeStrategy(s) => Some(c.changeBetStrategy(s).updateBoredom(-15.0 * (2 - mod.bMod))) - case WaitForGame() => Some(c) + case WaitForGame() => Some(getNewGameBet(c)) case Stay() => Some(c) case LeaveCasino() => None } - private def updateInGameBehaviours(c: A): A = + private def getNewGameBet(c: A): A = + val rules = rulesByProfile(c.riskProfile) + rules + .collectFirst { + case rule + if rule.game == c.getGameOrElse.get.gameType && rule.strategy == c.betStrategy.betType && rule.trigger + .eval(c) => + c.changeBetStrategy(betDefiner(rule, c)) + } + .getOrElse( + c.changeBetStrategy(FlatBetting(c.bankroll * 0.01, defaultRedBet)) + ) + + private def updateInGameBehaviours(c: A, mod: Modifiers): A = val updatedGame = games.find(_.id == c.getGameOrElse.get.id).get val lastRound = updatedGame.getLastRoundResult lastRound.find(_.getCustomerWhichPlayed == c.id) match - case Some(g) => c.updateAfter(-g.getMoneyGain) - case _ => c + case Some(g) => + if g.getMoneyGain > 0 then + c.updateFrustration( + (5 / c.bankrollRatio.max(0.5).min(2.0)) * mod.fMod + ).updateAfter(-g.getMoneyGain) + else + c.updateFrustration( + (-5 / c.bankrollRatio.max(0.5).min(2.0)) * (2 - mod.fMod) + ).updateAfter(-g.getMoneyGain) + + case _ => c // === Tree Builders === private def buildDecisionTree: DecisionTree[A, CustomerDecision] = @@ -155,10 +200,9 @@ case class DecisionManager[ private def leaveStayNode: DecisionTree[A, CustomerDecision] = def leaveRequirements(c: A): Boolean = val mod = ProfileModifiers.modifiers(c.riskProfile) - val r = c.bankroll / c.startingBankroll val trigger: Trigger[A] = BoredomAbove( - (80 * mod.bMod).max(100.0) - ) || FrustAbove((80 * mod.fMod).max(100.0)) + (80 * mod.bMod).min(95.0) + ) || FrustAbove((80 * mod.fMod).min(95.0)) || BrRatioAbove(mod.limits.tp) || BrRatioBelow(mod.limits.sl) trigger.eval(c) DecisionNode[A, CustomerDecision]( @@ -174,12 +218,11 @@ case class DecisionManager[ ): DecisionTree[A, CustomerDecision] = def stopPlayingRequirements(c: A): Boolean = val mod = ProfileModifiers.modifiers(profile) - val r = c.bankroll / c.startingBankroll + val betAmount = updateInGameBehaviours(c, mod).betStrategy.betAmount val trigger: Trigger[A] = BoredomAbove(bThreshold * mod.bMod) || FrustAbove(fThreshold * mod.fMod) || BrRatioAbove(mod.limits.tp) || BrRatioBelow(mod.limits.sl) - updateInGameBehaviours(c).betStrategy.betAmount > c.bankroll || trigger - .eval(c) + betAmount > c.bankroll - 1 || trigger.eval(c) DecisionNode[A, CustomerDecision]( predicate = c => stopPlayingRequirements(c), @@ -216,25 +259,55 @@ case class DecisionManager[ ) object PostDecisionUpdater: - def updatePosition[P <: MovableWithPrevious[P] & CustomerState[P] & Entity]( + def updatePosition[ + P <: MovableWithPrevious[P] & CustomerState[P] & + ChangingFavouriteGamePlayer[P] & Entity + ]( before: Seq[P], post: Seq[P] ): List[P] = + val (hasStopPlaying, unchangedState, remained) = + groupForChangeOfState[P](before, post) + + val changePosition = hasStopPlaying.map { case (oldP, newP) => + newP + .withPosition(oldP.previousPosition.get) + .withDirection(-newP.direction) + .withFavouriteGame( + Random.shuffle(gameTypesPresent.filter(_ != newP.favouriteGame)).head + ) + } + val unchanged = unchangedState.map(_._2) + changePosition ++ unchanged + + def updateGames[P <: CustomerState[P] & Entity]( + before: Seq[P], + post: Seq[P], + games: List[Game] + ): List[Game] = + val (hasStopPlaying, unchangedState, remained) = + groupForChangeOfState(before, post) + val gameCustomerMap = + hasStopPlaying.map((_._1)).map(c => c.getGameOrElse.get.id -> c).toMap + val gameMap = games.map(g => g.id -> g).toMap + val gameLeft = gameMap.keySet.intersect(gameCustomerMap.keySet) + val (gameToUnlock, gameUnchanged) = gameMap.keySet.toList + .map(id => (gameMap(id), gameCustomerMap.get(id))) + .partition { case (game, cust) => cust.isDefined } + val updatedGame = + gameToUnlock.map((g, c) => g.unlock(c.get.id)).map(r => r.option().get) + updatedGame ++ gameUnchanged.map((g, _) => g) + + private def groupForChangeOfState[P <: CustomerState[P] & Entity]( + before: Seq[P], + post: Seq[P] + ): (List[(P, P)], List[(P, P)], Set[String]) = val beforeMap = before.map(p => p.id -> p).toMap val postMap = post.map(p => p.id -> p).toMap - val remained = beforeMap.keySet.intersect(postMap.keySet) - val (hasStopPlaying, unchangedState) = remained.toList .map(id => (beforeMap(id), postMap(id))) .partition { case (oldState, newState) => oldState.isPlaying != newState.isPlaying } - - val changePosition = hasStopPlaying.map { case (_, newP) => - newP - .withPosition(newP.previousPosition.getOrElse(newP.position)) - .withDirection(-newP.direction) - } - val unchanged = unchangedState.map(_._2) - changePosition ++ unchanged + (hasStopPlaying, unchangedState, remained) diff --git a/backend/src/main/scala/model/managers/movements/PlayerManagers.scala b/backend/src/main/scala/model/managers/movements/PlayerManagers.scala index 8f33454..d683dfb 100644 --- a/backend/src/main/scala/model/managers/movements/PlayerManagers.scala +++ b/backend/src/main/scala/model/managers/movements/PlayerManagers.scala @@ -66,9 +66,9 @@ object PlayerManagers: if distance(slice.player.position, game.position) < sittingRadius => slice.copy( player = slice.player + .play(game) .withPosition(game.center) - .withDirection(Vector2D.zero) - .play(game), + .withDirection(Vector2D.zero), games = slice.games .map(g => if g.id == game.id then game else g) ) diff --git a/backend/src/main/scala/model/managers/movements/RandomMovementManager.scala b/backend/src/main/scala/model/managers/movements/RandomMovementManager.scala new file mode 100644 index 0000000..2f6d8cf --- /dev/null +++ b/backend/src/main/scala/model/managers/movements/RandomMovementManager.scala @@ -0,0 +1,16 @@ +package model.managers.movements + +import model.entities.customers.Movable +import model.managers.WeightedManager +import utils.Vector2D + +case class RandomMovementManager[M <: Movable[M]](weight: Double = 1) + extends WeightedManager[M]: + override def updatedWeight(weight: Double): WeightedManager[M] = + copy(weight = weight) + + override def update(slice: M): M = slice.addedDirection( + Vector2D(random(), random()) * weight + ) + + private def random(): Double = (2 * Math.random()) - 1 diff --git a/backend/src/main/scala/update/Update.scala b/backend/src/main/scala/update/Update.scala index 64d724d..70977b0 100644 --- a/backend/src/main/scala/update/Update.scala +++ b/backend/src/main/scala/update/Update.scala @@ -98,12 +98,7 @@ case class Update(customerManager: DefaultMovementManager): ) case UpdateCustomersPosition => - update(state | customerManager, UpdateGames) - - case UpdateGames => - val updatedGames = - GameResolver.update(state.customers.toList, state.games, state.ticker) - update(state.copy(games = updatedGames), UpdateSimulationBankrolls) + update(state | customerManager, UpdateSimulationBankrolls) case UpdateSimulationBankrolls => val updatedBankroll = @@ -113,11 +108,25 @@ case class Update(customerManager: DefaultMovementManager): case UpdateCustomersState => val updatedCustomerState = DecisionManager[Customer](state.games).update(state.customers) - val postDecisionUpdate = PostDecisionUpdater.updatePosition( + val pDUPosition = PostDecisionUpdater.updatePosition( state.customers, updatedCustomerState ) - state.copy(customers = postDecisionUpdate) + val pDUGames = PostDecisionUpdater.updateGames( + state.customers, + updatedCustomerState, + state.games + ) + + update( + state.copy(customers = pDUPosition, games = pDUGames), + UpdateGames + ) + + case UpdateGames => + val updatedGames = + GameResolver.update(state.customers.toList, state.games, state.ticker) + state.copy(games = updatedGames) case AddCustomers(strategy) => state.setSpawner( diff --git a/backend/src/main/scala/utils/TriggerDSL.scala b/backend/src/main/scala/utils/TriggerDSL.scala index 0dc228e..8a07dbb 100644 --- a/backend/src/main/scala/utils/TriggerDSL.scala +++ b/backend/src/main/scala/utils/TriggerDSL.scala @@ -18,19 +18,19 @@ object TriggerDSL: def FrustAbove[A <: BoredomFrustration[A]](p: Double): Trigger[A] = new Trigger[A]: - def eval(c: A): Boolean = c.frustration > p + def eval(c: A): Boolean = c.frustration >= p def BoredomAbove[A <: BoredomFrustration[A]](p: Double): Trigger[A] = new Trigger[A]: - def eval(c: A): Boolean = c.boredom > p + def eval(c: A): Boolean = c.boredom >= p def BrRatioAbove[A <: Bankroll[A]](r: Double): Trigger[A] = new Trigger[A]: - def eval(c: A): Boolean = (c.bankroll / c.startingBankroll) > r + def eval(c: A): Boolean = c.bankrollRatio >= r def BrRatioBelow[A <: Bankroll[A]](r: Double): Trigger[A] = new Trigger[A]: - def eval(c: A): Boolean = (c.bankroll / c.startingBankroll) < r + def eval(c: A): Boolean = c.bankrollRatio <= r def Always[A]: Trigger[A] = new Trigger[A]: def eval(c: A): Boolean = true diff --git a/backend/src/test/scala/model/TestSimulationState.scala b/backend/src/test/scala/model/TestSimulationState.scala index b1a7a5a..3381ebb 100644 --- a/backend/src/test/scala/model/TestSimulationState.scala +++ b/backend/src/test/scala/model/TestSimulationState.scala @@ -128,3 +128,11 @@ class TestSimulationState extends AnyFunSuite: .setSpawner(spawner) assert(state.spawner.contains(spawner)) + + test("update framerate should update both framerate and ticker"): + val initialState = SimulationState.empty() + val updatedState = initialState.updateFrameRate(120.0) + + assert(initialState.frameRate != updatedState.frameRate) + assert(updatedState.frameRate == 120) + assert(initialState.ticker != updatedState.ticker) diff --git a/backend/src/test/scala/model/TestTicker.scala b/backend/src/test/scala/model/TestTicker.scala index eee1dfe..a828e73 100644 --- a/backend/src/test/scala/model/TestTicker.scala +++ b/backend/src/test/scala/model/TestTicker.scala @@ -1,6 +1,8 @@ package model -import model.entities.games.{Blackjack, Roulette, SlotMachine} +import model.entities.games.Blackjack +import model.entities.games.Roulette +import model.entities.games.SlotMachine import org.scalatest.funsuite.AnyFunSuite class TestTicker extends AnyFunSuite: diff --git a/backend/src/test/scala/model/entities/customers/TestBankroll.scala b/backend/src/test/scala/model/entities/customers/TestBankroll.scala index 9e486b1..0092f41 100644 --- a/backend/src/test/scala/model/entities/customers/TestBankroll.scala +++ b/backend/src/test/scala/model/entities/customers/TestBankroll.scala @@ -39,3 +39,7 @@ class TestBankroll extends AnyFunSuite: assert( ex.getMessage === s"requirement failed: Bankroll amount must be positive, instead is ${startValue + netLoss}" ) + + test("bankroll ratio should work as expected"): + val c = Customer().withBankroll(1000.0).updateBankroll(500.0) + assert(c.bankrollRatio == 1.5) diff --git a/backend/src/test/scala/model/entities/customers/TestPreviousPosition.scala b/backend/src/test/scala/model/entities/customers/TestPreviousPosition.scala index 674adb2..d74a99c 100644 --- a/backend/src/test/scala/model/entities/customers/TestPreviousPosition.scala +++ b/backend/src/test/scala/model/entities/customers/TestPreviousPosition.scala @@ -1,15 +1,17 @@ package model.entities.customers +import model.entities.games.GameBuilder import org.scalatest.funsuite.AnyFunSuite import utils.Vector2D class TestPreviousPosition extends AnyFunSuite: private val customer = Customer() + private val game = GameBuilder.slot(Vector2D(10, 10)) test("The customer previous position is initially none"): assert(customer.previousPosition.isEmpty) test( - "After being moved a customer previous position contains its previous position" - ) - val movedCustomer = customer.withPosition(Vector2D(1, 1)) - assert(movedCustomer.previousPosition.get == customer.position) + "When a customer sits to a game its previous position contains its previous position" + ): + val playingCustomer = customer.play(game) + assert(playingCustomer.previousPosition.get == customer.position) diff --git a/backend/src/test/scala/model/managers/TestDecisionManager.scala b/backend/src/test/scala/model/managers/TestDecisionManager.scala index 1196b81..5a362dc 100644 --- a/backend/src/test/scala/model/managers/TestDecisionManager.scala +++ b/backend/src/test/scala/model/managers/TestDecisionManager.scala @@ -1,27 +1,31 @@ package model.managers import model.Ticker -import model.entities.* -import model.entities.customers.* -import model.entities.customers.CustState.{Idle, Playing} +import model.entities._ +import model.entities.customers.CustState.Idle +import model.entities.customers.CustState.Playing +import model.entities.customers.RiskProfile.Impulsive import model.entities.customers.RiskProfile.VIP -import model.entities.games.* +import model.entities.customers._ +import model.entities.games._ import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers -import utils.* -import utils.TriggerDSL.* +import utils._ class TestDecisionManager extends AnyFunSuite with Matchers: val normalTicker: Ticker = Ticker(60) // 12, 60, 42 - test("StopPlaying when boredom/frustration exceed thresholds") : + test("StopPlaying when boredom/frustration exceed thresholds"): val ticked = - (0 until normalTicker.blackjackTick.toInt).foldLeft(normalTicker)((t, _) => - t.update() + (0 until normalTicker.blackjackTick.toInt).foldLeft(normalTicker)( + (t, _) => t.update() ) val mockGame = GameBuilder.blackjack(Vector2D.zero) - val customer = Customer().withBoredom(99).withFrustration(99).withCustomerState(Playing(mockGame)) - val mockGamePlayed = mockGame.lock(customer.id).getOrElse(null) + val customer = Customer() + .withBoredom(99) + .withFrustration(99) + .withCustomerState(Playing(mockGame)) + val mockGamePlayed = mockGame.lock(customer.id).option().get val newGame = GameResolver.update( List(customer), List(mockGamePlayed), @@ -32,34 +36,45 @@ class TestDecisionManager extends AnyFunSuite with Matchers: val idle = manager.update(List(customer)) idle.head.customerState shouldBe Idle - /*test("Step Strategy should update correctly"): + test("Step Strategy should update correctly"): val ticked = - (0 until normalTicker.blackjackTick.toInt).foldLeft(normalTicker)((t, _) => - t.update() + (0 until normalTicker.blackjackTick.toInt).foldLeft(normalTicker)( + (t, _) => t.update() ) val mockGame = GameBuilder.blackjack(Vector2D.zero) - val customer = Customer().withCustomerState(Playing(mockGame)).withBetStrategy(MartingaleStrat(10.0,defaultRedBet)) - val losingGame = Gain(customer.id,customer.betStrategy.betAmount) - val mockGamePlayed = mockGame.copy(gameHistory = GameHistory(List(losingGame,losingGame,losingGame,losingGame))).lock(customer.id).getOrElse(null) + val customer = Customer() + .withCustomerState(Playing(mockGame)) + .withBetStrategy(MartingaleStrat(10.0, defaultRedBet)) + val losingGame = Gain(customer.id, customer.betStrategy.betAmount) + val mockGamePlayed = mockGame + .copy(gameHistory = + GameHistory(List(losingGame, losingGame, losingGame, losingGame)) + ) + .lock(customer.id) + .option() + .get val newGame = GameResolver.update( List(customer), List(mockGamePlayed), ticked ) + val manager = DecisionManager[Customer](newGame) val doubled = manager.update(List(customer)) - doubled.head.placeBet().amount shouldBe 20.0*/ + if newGame.head.getLastRoundResult.head.getMoneyGain > 0 then + doubled.head.placeBet().amount shouldBe 20.0 + else doubled.head.placeBet().amount shouldBe 10.0 - test("ContinuePlaying when thresholds not exceeded") : + test("ContinuePlaying when thresholds not exceeded"): val ticked = - (0 until normalTicker.blackjackTick.toInt).foldLeft(normalTicker)((t, _) => - t.update() + (0 until normalTicker.blackjackTick.toInt).foldLeft(normalTicker)( + (t, _) => t.update() ) val mockGame = GameBuilder.blackjack(Vector2D.zero) val customer = Customer().withCustomerState(Playing(mockGame)) - val mockGamePlayed = mockGame.lock(customer.id).getOrElse(null) + val mockGamePlayed = mockGame.lock(customer.id).option().get val newGame = GameResolver.update( List(customer), List(mockGamePlayed), @@ -70,17 +85,17 @@ class TestDecisionManager extends AnyFunSuite with Matchers: val playing = manager.update(List(customer)) playing.head.customerState shouldBe Playing(mockGame) - test("ChangeStrategy if rule trigger matches") : + test("ChangeStrategy if rule trigger matches"): val ticked = - (0 until normalTicker.blackjackTick.toInt).foldLeft(normalTicker)((t, _) => - t.update() + (0 until normalTicker.blackjackTick.toInt).foldLeft(normalTicker)( + (t, _) => t.update() ) val mockGame = GameBuilder.blackjack(Vector2D.zero) val customer = Customer() .withProfile(VIP) .withCustomerState(Playing(mockGame)) - .withBetStrategy(MartingaleStrat(10,defaultRedBet).copy(lossStreak = 4)) - val mockGamePlayed = mockGame.lock(customer.id).getOrElse(null) + .withBetStrategy(MartingaleStrat(10, defaultRedBet).copy(lossStreak = 4)) + val mockGamePlayed = mockGame.lock(customer.id).option().get val newGame = GameResolver.update( List(customer), List(mockGamePlayed), @@ -88,12 +103,56 @@ class TestDecisionManager extends AnyFunSuite with Matchers: ) val manager = DecisionManager[Customer](newGame) val changeStrat = manager.update(List(customer)) - changeStrat.head.betStrategy shouldBe OscarGrindStrat(customer.bankroll*0.05,customer.bankroll,defaultRedBet) + changeStrat.head.betStrategy shouldBe OscarGrindStrat( + customer.bankroll * 0.05, + customer.bankroll, + defaultRedBet + ) test("updatePosition should change position if state was changed"): - val spawnCustomer = Customer().withPosition(Vector2D(10.0,10.0)).withCustomerState(Playing(GameBuilder.slot(Vector2D.zero))) - val oldCustomer = spawnCustomer.withPosition(Vector2D(20.0,20.0)) + val spawnCustomer = Customer() + .withPosition(Vector2D(10.0, 10.0)) + .withCustomerState(Playing(GameBuilder.slot(Vector2D.zero))) + val oldCustomer = spawnCustomer.withPosition(Vector2D(20.0, 20.0)) val newCustomer = oldCustomer.withPosition(Vector2D.zero).changeState(Idle) - val updatedCustomer = PostDecisionUpdater.updatePosition(List(oldCustomer),List(newCustomer)) + val updatedCustomer = + PostDecisionUpdater.updatePosition(List(oldCustomer), List(newCustomer)) updatedCustomer.head.customerState shouldBe Idle - updatedCustomer.head.position shouldBe oldCustomer.position \ No newline at end of file + updatedCustomer.head.position shouldBe spawnCustomer.position + updatedCustomer.head.favouriteGame should not be oldCustomer.favouriteGame + + test("Leave the casino when condition triggered"): + val customer = Customer() + .withBoredom(99) + .withFrustration(99) + .withCustomerState(Idle) + .withProfile(Impulsive) + val manager = DecisionManager[Customer](Nil) + val nobody = manager.update(List(customer)) + nobody.isEmpty shouldBe true + + test("Unlock the game when stop playing"): + val ticked = + (0 until normalTicker.blackjackTick.toInt).foldLeft(normalTicker)( + (t, _) => t.update() + ) + val mockGame = GameBuilder.blackjack(Vector2D.zero) + val secondGame = GameBuilder.slot(Vector2D.zero) + val customer = Customer() + .withBoredom(99) + .withFrustration(99) + .withCustomerState(Playing(mockGame)) + val mockGamePlayed = mockGame.lock(customer.id).option().get + val newGame = GameResolver.update( + List(customer), + List(mockGamePlayed, secondGame), + ticked + ) + val manager = DecisionManager[Customer](newGame) + val idle = manager.update(List(customer)) + val updatedGames = + PostDecisionUpdater.updateGames(List(customer), idle, newGame) + + updatedGames.size shouldBe 2 + newGame.head.gameState.currentPlayers shouldBe 1 + updatedGames.head.gameState.currentPlayers shouldBe 0 diff --git a/backend/src/test/scala/model/managers/movements/TestRandomMovementTest.scala b/backend/src/test/scala/model/managers/movements/TestRandomMovementTest.scala new file mode 100644 index 0000000..6f58ba8 --- /dev/null +++ b/backend/src/test/scala/model/managers/movements/TestRandomMovementTest.scala @@ -0,0 +1,20 @@ +package model.managers.movements + +import model.entities.customers.Movable +import model.managers.| +import org.scalatest.funsuite.AnyFunSuite +import utils.Vector2D + +class TestRandomMovementTest extends AnyFunSuite: + private case class MovableImpl(position: Vector2D, direction: Vector2D) + extends Movable[MovableImpl]: + override def withPosition(newPosition: Vector2D): MovableImpl = + copy(position = newPosition) + + override def withDirection(newDirection: Vector2D): MovableImpl = + copy(direction = newDirection) + + test("A movable moved by a brownian motion should move"): + val movable = MovableImpl(Vector2D.zero, Vector2D.zero) + val updated = movable | RandomMovementManager() + assert(updated.direction.magnitude > 0) diff --git a/backend/src/test/scala/model/spawner/TestSpawningStrategy.scala b/backend/src/test/scala/model/spawner/TestSpawningStrategy.scala index d34c89a..12e1b3c 100644 --- a/backend/src/test/scala/model/spawner/TestSpawningStrategy.scala +++ b/backend/src/test/scala/model/spawner/TestSpawningStrategy.scala @@ -43,7 +43,7 @@ class TestSpawningStrategy extends AnyFunSuite: test("GaussianStrategy should respect base offset"): val strategy = GaussianStrategy(100, 10.0, 2.0, 10) - assert(strategy.customersAt(100.0) == 10) + assert(strategy.customersAt(99.0) == 10) assert(strategy.customersAt(10.0) == 110) test("StepStrategy should return low rate before start time"): diff --git a/backend/src/test/scala/utils/TestTriggerDSL.scala b/backend/src/test/scala/utils/TestTriggerDSL.scala index 09d713e..b257f26 100644 --- a/backend/src/test/scala/utils/TestTriggerDSL.scala +++ b/backend/src/test/scala/utils/TestTriggerDSL.scala @@ -1,15 +1,23 @@ package utils -import model.entities.customers.{Customer, MartingaleStrat, defaultRedBet} +import model.entities.customers.Customer +import model.entities.customers.MartingaleStrat +import model.entities.customers.defaultRedBet import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers -import utils.TriggerDSL.{Always, BoredomAbove, BrRatioAbove, BrRatioBelow, FrustAbove, Losses, Trigger} +import utils.TriggerDSL.Always +import utils.TriggerDSL.BoredomAbove +import utils.TriggerDSL.BrRatioAbove +import utils.TriggerDSL.BrRatioBelow +import utils.TriggerDSL.FrustAbove +import utils.TriggerDSL.Losses +import utils.TriggerDSL.Trigger class TestTriggerDSL extends AnyFunSuite with Matchers: - test("FrustrationAbove trigger returns true when exceeded") : + test("FrustrationAbove trigger returns true when exceeded"): val c = Customer().withFrustration(80) FrustAbove(60).eval(c) shouldBe true - - test("BankrollRatioAbove trigger works correctly") : + + test("BankrollRatioAbove trigger works correctly"): val c = Customer().withBankroll(1000.0).updateBankroll(500.0) BrRatioAbove(1.4).eval(c) shouldBe true BrRatioAbove(2.0).eval(c) shouldBe false @@ -19,21 +27,22 @@ class TestTriggerDSL extends AnyFunSuite with Matchers: BrRatioBelow(2.0).eval(c) shouldBe true BrRatioBelow(1.4).eval(c) shouldBe false - - test("And trigger only passes if both do") : + test("And trigger only passes if both do"): val c = Customer().withFrustration(80).withBoredom(90) val trigger: Trigger[Customer] = FrustAbove(60) && BoredomAbove(80) trigger.eval(c) shouldBe true - - test("Or trigger passes if at least one condition is met") : + + test("Or trigger passes if at least one condition is met"): val c = Customer().withFrustration(90).withBoredom(20) val trigger: Trigger[Customer] = FrustAbove(50) || BoredomAbove(80) trigger.eval(c) shouldBe true - - test("Always trigger always passes") : + + test("Always trigger always passes"): val c = Customer() Always.eval(c) shouldBe true - - test("Losses trigger for Martingale strategy works") : - val c = Customer().withBetStrategy(MartingaleStrat(10,defaultRedBet).copy(lossStreak = 4)) + + test("Losses trigger for Martingale strategy works"): + val c = Customer().withBetStrategy( + MartingaleStrat(10, defaultRedBet).copy(lossStreak = 4) + ) Losses(3).eval(c) shouldBe true diff --git a/docs/report.md b/docs/report.md index 750e670..d99d8df 100644 --- a/docs/report.md +++ b/docs/report.md @@ -127,10 +127,12 @@ function calculate_cohesion(boid, nearby_boids){ } } ``` -- **Games attraction**: Customers are attracted by their favourite game, in particular the customer looks around for the nearest game of its favourite type and moves towards it. If no game of its liking is found, this behaviour won't affect its movements and the frustration index of the customer will increase of `INCREASE_FRUSTRATION`. +- **Games attraction**: Customers are attracted by their favourite game, in particular the customer looks around for the nearest game of its favourite type and moves towards it. If no game of its liking is found, this behaviour won't affect its movements and the frustration index of the customer will increase of `INCREASE_FRUSTRATION`. When a customer stands up from a game, it picks randomly a new favourite game, excluding its previous favourite game. - **Collisions with walls and games**: Customers can't collide with obstacles, which are games and walls. When the resulting velocity would make the customer collide with obstacles, its velocity is set to zero and the movement canceled. +- **Random movement**: Customers change their directions randomly, as they are touring the casino. + Each customer is affected only by the boids within a certain distance, defined by the `PERCEPTION_RADIUS`. If a boid is outside this radius, it is not considered in the calculations. Each customer updates its position and velocity according to the following algorithm: ``` @@ -146,10 +148,11 @@ b.velocity += SEPARATION_WEIGHT * separation b.velocity += ALIGNMENT_WEIGHT * alignment b.velocity += COHESION_WEIGHT * cohesion b.velocity += GAME_ATTRACTION_WEIGHT * game_attraction +b.velocity += RANDOM_DIRECTION_WEIGHT * random_direction() /* Limit speed to MAX_SPEED */ if magnitude(b.velocity) > MAX_SPEED -b.velocity = normalize(b.velocity) * MAX_SPEED +b.velocity = normalize(b.velocity) * MAX_SPEED if b.canSee(b.position + b.velocity) { /* Update position */ @@ -166,7 +169,8 @@ Other parameters that influence the boids behavior are: - `SEPARATION_WEIGHT`: weight for separation force - `ALIGNMENT_WEIGHT`: weight for alignment force - `COHESION_WEIGHT`: weight for cohesion force -- `GAME_ATTRACTION_WEIGHT`: weight for game attraction force. +- `GAME_ATTRACTION_WEIGHT`: weight for game attraction force +- `RANDOM_DIRECTION_WEIGHT`: weight for the random movements. All of these parameters can be configured by the user in order to simulate different scenarios. When a customer is close to a game of its liking, that is the distance between the customer's and game's position is less than `SITTING_RADIUS`, the player sits and plays the game. While a customer is playing it does not move. diff --git a/frontend/src/main/scala/view/ButtonBar.scala b/frontend/src/main/scala/view/ButtonBar.scala index 76f9c4b..48689d5 100644 --- a/frontend/src/main/scala/view/ButtonBar.scala +++ b/frontend/src/main/scala/view/ButtonBar.scala @@ -57,13 +57,13 @@ class ButtonBar( Some( dom.window.setInterval( () => eventBus.writer.onNext(Event.SimulationTick), - 1000 / 60 + 1000 / model.now().frameRate ) ) ) case "Reset" => - canvasManager.reset() eventBus.writer.onNext(Event.ResetSimulation) + canvasManager.reset() if (timerId.now().isDefined) { dom.window.clearInterval(timerId.now().get) } diff --git a/frontend/src/main/scala/view/CanvasManager.scala b/frontend/src/main/scala/view/CanvasManager.scala index fc04d2d..efdadd6 100644 --- a/frontend/src/main/scala/view/CanvasManager.scala +++ b/frontend/src/main/scala/view/CanvasManager.scala @@ -5,6 +5,11 @@ import com.raquo.laminar.api.L.Var import com.raquo.laminar.api.L.unsafeWindowOwner import model.SimulationState import model.entities.Entity +import model.entities.customers.CustState +import model.entities.customers.RiskProfile.Casual +import model.entities.customers.RiskProfile.Impulsive +import model.entities.customers.RiskProfile.Regular +import model.entities.customers.RiskProfile.VIP import org.scalajs.dom import org.scalajs.dom.MouseEvent import org.scalajs.dom.html @@ -152,7 +157,14 @@ class CanvasManager( state.customers.foreach { customer => ctx.beginPath() ctx.arc(customer.position.x, customer.position.y, 3, 0, Math.PI * 2) - ctx.fillStyle = "green" + ctx.fillStyle = customer.riskProfile match + case Casual => "blue" + case Regular => "green" + case VIP => "red" + case Impulsive => "magenta" + ctx.strokeStyle = customer.customerState match + case CustState.Playing(_) => "black" + case _ => ctx.fillStyle ctx.fill() ctx.stroke() } diff --git a/frontend/src/main/scala/view/Component.scala b/frontend/src/main/scala/view/Component.scala index 9b01d7f..6207794 100644 --- a/frontend/src/main/scala/view/Component.scala +++ b/frontend/src/main/scala/view/Component.scala @@ -47,7 +47,7 @@ class SlotComponent(initialModel: SlotMachineGame) override val model: Var[SlotMachineGame] = Var(initialModel) def render(ctx: dom.CanvasRenderingContext2D): Unit = - ctx.fillStyle = "#3498db" + ctx.fillStyle = "#910909" val modelComponent = model.now() ctx.fillRect( modelComponent.position.x, @@ -57,7 +57,7 @@ class SlotComponent(initialModel: SlotMachineGame) ) // Draw border - ctx.strokeStyle = "#2980b9" + ctx.strokeStyle = "#470505" ctx.lineWidth = 2 ctx.strokeRect( modelComponent.position.x, @@ -74,7 +74,7 @@ class RouletteComponent(initialModel: RouletteGame) override val model: Var[RouletteGame] = Var(initialModel) def render(ctx: dom.CanvasRenderingContext2D): Unit = - ctx.fillStyle = "#3498db" + ctx.fillStyle = "#aa16ba" val modelComponent = model.now() ctx.fillRect( modelComponent.position.x, @@ -84,7 +84,7 @@ class RouletteComponent(initialModel: RouletteGame) ) // Draw border - ctx.strokeStyle = "#2980b9" + ctx.strokeStyle = "#590c61" ctx.lineWidth = 2 ctx.strokeRect( modelComponent.position.x, @@ -101,7 +101,7 @@ class BlackJackComponent(initialModel: BlackJackGame) override val model: Var[BlackJackGame] = Var(initialModel) def render(ctx: dom.CanvasRenderingContext2D): Unit = - ctx.fillStyle = "#3498db" + ctx.fillStyle = "#24ba16" val modelComponent = model.now() ctx.fillRect( modelComponent.position.x, @@ -111,7 +111,7 @@ class BlackJackComponent(initialModel: BlackJackGame) ) // Draw border - ctx.strokeStyle = "#2980b9" + ctx.strokeStyle = "#125c0b" ctx.lineWidth = 2 ctx.strokeRect( modelComponent.position.x, diff --git a/frontend/src/main/scala/view/ConfigForm.scala b/frontend/src/main/scala/view/ConfigForm.scala index 7769ff1..1dd1538 100644 --- a/frontend/src/main/scala/view/ConfigForm.scala +++ b/frontend/src/main/scala/view/ConfigForm.scala @@ -17,7 +17,7 @@ case class ConfigForm(update: Var[Update], model: Var[SimulationState]): updater: (DefaultMovementManager, Double) => DefaultMovementManager ) private val parameters = List( - Parameter("Max Speed", Var(20.0), (m, v) => m.copy(maxSpeed = v)), + Parameter("Max Speed", Var(2.0), (m, v) => m.copy(maxSpeed = v)), Parameter( "Perception Radius", Var(100.0), @@ -26,17 +26,17 @@ case class ConfigForm(update: Var[Update], model: Var[SimulationState]): Parameter("Avoid Radius", Var(50.0), (m, v) => m.copy(avoidRadius = v)), Parameter( "Alignment Weight", - Var(1.0), + Var(0.1), (m, v) => m.copy(alignmentWeight = v) ), Parameter( "Cohesion Weight", - Var(1.0), + Var(0.1), (m, v) => m.copy(cohesionWeight = v) ), Parameter( "Separation Weight", - Var(1.0), + Var(0.1), (m, v) => m.copy(separationWeight = v) ), Parameter( @@ -46,13 +46,18 @@ case class ConfigForm(update: Var[Update], model: Var[SimulationState]): ), Parameter( "Sitting Radius", - Var(100.0), + Var(30.0), (m, v) => m.copy(sittingRadius = v) ), Parameter( "Boredom increase", - Var(0.1), + Var(0.2), (m, v) => m.copy(boredomIncrease = v) + ), + Parameter( + "Random movement weight", + Var(0.2), + (m, v) => m.copy(randomMovementWeight = v) ) ) // Spawning strategy selection diff --git a/frontend/src/main/scala/view/Sidebar.scala b/frontend/src/main/scala/view/Sidebar.scala index e9cd6c9..ded6e17 100644 --- a/frontend/src/main/scala/view/Sidebar.scala +++ b/frontend/src/main/scala/view/Sidebar.scala @@ -10,7 +10,7 @@ class Sidebar: def init(canvasManager: CanvasManager): Unit = components.foreach { comp => val element = dom.document.createElement("div").asInstanceOf[html.Div] - element.className = "draggable-component" + element.className = s"draggable-component component-${comp.toLowerCase}" element.textContent = comp element.dataset.update("type", comp) sidebar.appendChild(element) diff --git a/style.css b/style.css index 97b3a49..af30fbb 100644 --- a/style.css +++ b/style.css @@ -57,6 +57,10 @@ body { font-weight: bold; } +.component-slot { background-color: #910909; } +.component-blackjack { background-color: #24ba16; } +.component-roulette { background-color: #aa16ba; } + #button-bar { grid-column: 1; grid-row: 2;