From af3fa652fc241fc292831064f9fc6c9abafdaafd Mon Sep 17 00:00:00 2001 From: Luca Patrignani Date: Fri, 25 Jul 2025 17:50:49 +0200 Subject: [PATCH 01/19] =?UTF-8?q?=E2=9C=A8=20Change=20favourite=20game=20w?= =?UTF-8?q?hen=20a=20player=20stand=20up?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/scala/model/entities/Player.scala | 4 ++++ .../model/entities/customers/Customer.scala | 16 ++++++++++++---- .../main/scala/model/entities/games/Game.scala | 3 +++ .../scala/model/managers/DecisionManager.scala | 16 +++++++++++++--- .../managers/movements/PlayerManagers.scala | 4 ++-- backend/src/test/scala/model/TestTicker.scala | 4 +++- .../model/managers/TestDecisionManager.scala | 1 + docs/report.md | 2 +- 8 files changed, 39 insertions(+), 11 deletions(-) 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/Customer.scala b/backend/src/main/scala/model/entities/customers/Customer.scala index 54fb65d..831cf6f 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 @@ -43,13 +43,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 +62,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 +105,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 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/managers/DecisionManager.scala b/backend/src/main/scala/model/managers/DecisionManager.scala index 453e5f5..4c40333 100644 --- a/backend/src/main/scala/model/managers/DecisionManager.scala +++ b/backend/src/main/scala/model/managers/DecisionManager.scala @@ -1,5 +1,8 @@ package model.managers +import scala.util.Random + +import model.entities.ChangingFavouriteGamePlayer import model.entities.Entity import model.entities.Player import model.entities.customers.Bankroll @@ -28,6 +31,7 @@ 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 @@ -173,7 +177,10 @@ 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] = @@ -188,10 +195,13 @@ object PostDecisionUpdater: oldState.isPlaying != newState.isPlaying } - val changePosition = hasStopPlaying.map { case (_, newP) => + val changePosition = hasStopPlaying.map { case (oldP, newP) => newP - .withPosition(newP.previousPosition.getOrElse(newP.position)) + .withPosition(oldP.previousPosition.get) .withDirection(-newP.direction) + .withFavouriteGame( + Random.shuffle(gameTypesPresent.filter(_ != newP.favouriteGame)).head + ) } val unchanged = unchangedState.map(_._2) changePosition ++ unchanged 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/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/managers/TestDecisionManager.scala b/backend/src/test/scala/model/managers/TestDecisionManager.scala index 88d30b4..ae99ab3 100644 --- a/backend/src/test/scala/model/managers/TestDecisionManager.scala +++ b/backend/src/test/scala/model/managers/TestDecisionManager.scala @@ -67,3 +67,4 @@ class TestDecisionManager extends AnyFunSuite with Matchers: PostDecisionUpdater.updatePosition(List(oldCustomer), List(newCustomer)) updatedCustomer.head.customerState shouldBe Idle updatedCustomer.head.position shouldBe oldCustomer.position + updatedCustomer.head.favouriteGame should not be oldCustomer.favouriteGame diff --git a/docs/report.md b/docs/report.md index 750e670..d519485 100644 --- a/docs/report.md +++ b/docs/report.md @@ -127,7 +127,7 @@ 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. From b755f479e9383a0f06b674f504b5066203c14e78 Mon Sep 17 00:00:00 2001 From: Luca Patrignani Date: Fri, 25 Jul 2025 18:22:02 +0200 Subject: [PATCH 02/19] =?UTF-8?q?=E2=9C=A8=20Color=20playing=20customer=20?= =?UTF-8?q?red?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/main/scala/view/CanvasManager.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/main/scala/view/CanvasManager.scala b/frontend/src/main/scala/view/CanvasManager.scala index 0f61f18..9182694 100644 --- a/frontend/src/main/scala/view/CanvasManager.scala +++ b/frontend/src/main/scala/view/CanvasManager.scala @@ -5,6 +5,7 @@ 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 org.scalajs.dom import org.scalajs.dom.MouseEvent import org.scalajs.dom.html @@ -146,7 +147,9 @@ 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.customerState match + case CustState.Playing(_) => "red" + case CustState.Idle => "blue" ctx.fill() ctx.stroke() } From 0bea8ca36051dc9b6813774b174256b2bbec6074 Mon Sep 17 00:00:00 2001 From: Marco Galeri Date: Sat, 26 Jul 2025 00:42:57 +0200 Subject: [PATCH 03/19] =?UTF-8?q?=F0=9F=9A=B8=20Improve=20despawn=20and=20?= =?UTF-8?q?ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/entities/spawner/Spawner.scala | 22 ++++++++++++------- .../model/managers/DecisionManager.scala | 4 ++-- backend/src/main/scala/utils/TriggerDSL.scala | 8 +++---- .../model/managers/TestDecisionManager.scala | 18 +++++++++++---- .../src/main/scala/view/CanvasManager.scala | 11 +++++++++- 5 files changed, 44 insertions(+), 19 deletions(-) diff --git a/backend/src/main/scala/model/entities/spawner/Spawner.scala b/backend/src/main/scala/model/entities/spawner/Spawner.scala index f944025..e737d44 100644 --- a/backend/src/main/scala/model/entities/spawner/Spawner.scala +++ b/backend/src/main/scala/model/entities/spawner/Spawner.scala @@ -69,13 +69,19 @@ case class Spawner( ) else state - 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 + private def defaultCustomerCreation(): Customer = + 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( @@ -88,7 +94,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) Customer() .withPosition(this.position.around(5.0)) diff --git a/backend/src/main/scala/model/managers/DecisionManager.scala b/backend/src/main/scala/model/managers/DecisionManager.scala index 7159a75..5477079 100644 --- a/backend/src/main/scala/model/managers/DecisionManager.scala +++ b/backend/src/main/scala/model/managers/DecisionManager.scala @@ -157,8 +157,8 @@ case class DecisionManager[ 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]( diff --git a/backend/src/main/scala/utils/TriggerDSL.scala b/backend/src/main/scala/utils/TriggerDSL.scala index 0dc228e..2083820 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.bankroll / c.startingBankroll) >= 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.bankroll / c.startingBankroll) <= r def Always[A]: Trigger[A] = new Trigger[A]: def eval(c: A): Boolean = true diff --git a/backend/src/test/scala/model/managers/TestDecisionManager.scala b/backend/src/test/scala/model/managers/TestDecisionManager.scala index 1196b81..c369c05 100644 --- a/backend/src/test/scala/model/managers/TestDecisionManager.scala +++ b/backend/src/test/scala/model/managers/TestDecisionManager.scala @@ -4,7 +4,7 @@ import model.Ticker import model.entities.* import model.entities.customers.* import model.entities.customers.CustState.{Idle, Playing} -import model.entities.customers.RiskProfile.VIP +import model.entities.customers.RiskProfile.{Impulsive, VIP} import model.entities.games.* import org.scalatest.funsuite.AnyFunSuite import org.scalatest.matchers.should.Matchers @@ -32,7 +32,7 @@ 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() @@ -48,9 +48,13 @@ class TestDecisionManager extends AnyFunSuite with Matchers: 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") : val ticked = @@ -96,4 +100,10 @@ class TestDecisionManager extends AnyFunSuite with Matchers: val newCustomer = oldCustomer.withPosition(Vector2D.zero).changeState(Idle) 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 oldCustomer.position + + 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 diff --git a/frontend/src/main/scala/view/CanvasManager.scala b/frontend/src/main/scala/view/CanvasManager.scala index 0f61f18..d47dd37 100644 --- a/frontend/src/main/scala/view/CanvasManager.scala +++ b/frontend/src/main/scala/view/CanvasManager.scala @@ -5,6 +5,10 @@ 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.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 @@ -146,7 +150,12 @@ 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 = ctx.fillStyle ctx.fill() ctx.stroke() } From 15d42c23b5ee1f297903e07a64439a4a557c7a09 Mon Sep 17 00:00:00 2001 From: Marco Galeri Date: Sat, 26 Jul 2025 03:13:27 +0200 Subject: [PATCH 04/19] =?UTF-8?q?=F0=9F=90=9B=20Fix=20games=20not=20unlock?= =?UTF-8?q?ing=20properly=20and=20=20tuning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/entities/customers/Bankroll.scala | 2 + .../model/managers/DecisionManager.scala | 83 +++++++++++++------ backend/src/main/scala/update/Update.scala | 9 +- backend/src/main/scala/utils/TriggerDSL.scala | 4 +- .../entities/customers/TestBankroll.scala | 4 + .../model/managers/TestDecisionManager.scala | 22 +++++ 6 files changed, 94 insertions(+), 30 deletions(-) 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/managers/DecisionManager.scala b/backend/src/main/scala/model/managers/DecisionManager.scala index 5477079..c087b7b 100644 --- a/backend/src/main/scala/model/managers/DecisionManager.scala +++ b/backend/src/main/scala/model/managers/DecisionManager.scala @@ -6,7 +6,6 @@ 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,6 +16,7 @@ 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 @@ -45,14 +45,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,15 +69,15 @@ 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), // 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), // Casual - + SwitchRule(Casual, SlotMachine, FlatBet, FrustAbove(50) || BrRatioBelow(0.7), FlatBet, 0.015), // Impulsive SwitchRule(Impulsive, Blackjack, Martingale, Losses(3), OscarGrind, 0.10), SwitchRule(Impulsive, Roulette, Martingale, Losses(3), FlatBet, 0.07), @@ -108,9 +108,9 @@ 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) @@ -118,12 +118,21 @@ case class DecisionManager[ case LeaveCasino() => None } - private def updateInGameBehaviours(c: A): A = + 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,7 +164,6 @@ 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).min(95.0) ) || FrustAbove((80 * mod.fMod).min(95.0)) @@ -174,12 +182,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), @@ -220,16 +227,8 @@ object PostDecisionUpdater: before: Seq[P], post: Seq[P] ): List[P] = - 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 (hasStopPlaying, unchangedState, remained) = + groupForChangeOfState[P](before, post) val changePosition = hasStopPlaying.map { case (_, newP) => newP @@ -238,3 +237,35 @@ object PostDecisionUpdater: } 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 + } + (hasStopPlaying, unchangedState, remained) diff --git a/backend/src/main/scala/update/Update.scala b/backend/src/main/scala/update/Update.scala index fbe81a1..55c8ab1 100644 --- a/backend/src/main/scala/update/Update.scala +++ b/backend/src/main/scala/update/Update.scala @@ -60,11 +60,16 @@ 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 + ) + state.copy(customers = pDUPosition, games = pDUGames) case AddCustomers(strategy) => state.setSpawner( diff --git a/backend/src/main/scala/utils/TriggerDSL.scala b/backend/src/main/scala/utils/TriggerDSL.scala index 2083820..8a07dbb 100644 --- a/backend/src/main/scala/utils/TriggerDSL.scala +++ b/backend/src/main/scala/utils/TriggerDSL.scala @@ -26,11 +26,11 @@ object TriggerDSL: 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/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/managers/TestDecisionManager.scala b/backend/src/test/scala/model/managers/TestDecisionManager.scala index c369c05..22d1056 100644 --- a/backend/src/test/scala/model/managers/TestDecisionManager.scala +++ b/backend/src/test/scala/model/managers/TestDecisionManager.scala @@ -107,3 +107,25 @@ class TestDecisionManager extends AnyFunSuite with Matchers: 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).getOrElse(null) + 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 From ab3a3aeeed996eb78182ad876838803c4f6a1d53 Mon Sep 17 00:00:00 2001 From: NickGhignatti Date: Sat, 26 Jul 2025 09:43:10 +0200 Subject: [PATCH 05/19] =?UTF-8?q?=F0=9F=90=9B=20Fix=20reset=20cleaning=20t?= =?UTF-8?q?he=20border=20walls=20from=20the=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/main/scala/view/ButtonBar.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/main/scala/view/ButtonBar.scala b/frontend/src/main/scala/view/ButtonBar.scala index 76f9c4b..3f17af8 100644 --- a/frontend/src/main/scala/view/ButtonBar.scala +++ b/frontend/src/main/scala/view/ButtonBar.scala @@ -62,8 +62,8 @@ class ButtonBar( ) ) case "Reset" => - canvasManager.reset() eventBus.writer.onNext(Event.ResetSimulation) + canvasManager.reset() if (timerId.now().isDefined) { dom.window.clearInterval(timerId.now().get) } From 5fb9ecb9f3fc73283634c9a8c48c8915405b6c58 Mon Sep 17 00:00:00 2001 From: Luca Patrignani Date: Sat, 26 Jul 2025 10:04:06 +0200 Subject: [PATCH 06/19] =?UTF-8?q?=E2=9C=A8=20Choose=20better=20parameters?= =?UTF-8?q?=20for=20movements?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/main/scala/view/ConfigForm.scala | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/frontend/src/main/scala/view/ConfigForm.scala b/frontend/src/main/scala/view/ConfigForm.scala index 7769ff1..8ba9b29 100644 --- a/frontend/src/main/scala/view/ConfigForm.scala +++ b/frontend/src/main/scala/view/ConfigForm.scala @@ -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,12 +46,12 @@ 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(1), (m, v) => m.copy(boredomIncrease = v) ) ) From b2058eafef9dcdb3c6d2e8c21dbc99dbb8f1b560 Mon Sep 17 00:00:00 2001 From: Luca Patrignani Date: Sat, 26 Jul 2025 10:25:54 +0200 Subject: [PATCH 07/19] =?UTF-8?q?=F0=9F=8E=A8=20Create=20single=20customer?= =?UTF-8?q?=20adapter?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/entities/customers/Customer.scala | 14 ++- backend/src/test/scala/model/TestTicker.scala | 4 +- .../model/managers/TestDecisionManager.scala | 106 +++++++++++------- .../src/test/scala/utils/TestTriggerDSL.scala | 37 +++--- 4 files changed, 105 insertions(+), 56 deletions(-) diff --git a/backend/src/main/scala/model/entities/customers/Customer.scala b/backend/src/main/scala/model/entities/customers/Customer.scala index 98f30b1..40ecd2b 100644 --- a/backend/src/main/scala/model/entities/customers/Customer.scala +++ b/backend/src/main/scala/model/entities/customers/Customer.scala @@ -131,7 +131,7 @@ case class DefaultMovementManager( ) ) | 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 +216,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/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/managers/TestDecisionManager.scala b/backend/src/test/scala/model/managers/TestDecisionManager.scala index 22d1056..9227f38 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.customers.RiskProfile.{Impulsive, VIP} -import model.entities.games.* +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.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), @@ -34,15 +38,23 @@ class TestDecisionManager extends AnyFunSuite with Matchers: 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), @@ -53,17 +65,16 @@ class TestDecisionManager extends AnyFunSuite with Matchers: val doubled = manager.update(List(customer)) if newGame.head.getLastRoundResult.head.getMoneyGain > 0 then doubled.head.placeBet().amount shouldBe 20.0 - else - doubled.head.placeBet().amount shouldBe 10.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), @@ -74,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), @@ -92,40 +103,55 @@ 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 test("Leave the casino when condition triggered"): - val customer = Customer().withBoredom(99).withFrustration(99).withCustomerState(Idle).withProfile(Impulsive) + 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() + (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).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,secondGame), + List(mockGamePlayed, secondGame), ticked ) val manager = DecisionManager[Customer](newGame) val idle = manager.update(List(customer)) - val updatedGames = PostDecisionUpdater.updateGames(List(customer),idle,newGame) - + 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/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 From 1f9641b95b35577b0f8eeee7d9bd0ae843fb7ae0 Mon Sep 17 00:00:00 2001 From: NickGhignatti Date: Sat, 26 Jul 2025 10:44:06 +0200 Subject: [PATCH 08/19] =?UTF-8?q?=E2=9C=A8=20Time=20recursion=20on=20day?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/entities/spawner/SpawningStrategy.scala | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) 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 From 2d127d5ceb053fe08dfab611b297c1ab00db574c Mon Sep 17 00:00:00 2001 From: Luca Patrignani Date: Sat, 26 Jul 2025 10:45:15 +0200 Subject: [PATCH 09/19] =?UTF-8?q?=E2=9C=A8=20Implement=20random=20movement?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../movements/RandomMovementManager.scala | 16 +++++++++++++++ .../movements/TestRandomMovementTest.scala | 20 +++++++++++++++++++ 2 files changed, 36 insertions(+) create mode 100644 backend/src/main/scala/model/managers/movements/RandomMovementManager.scala create mode 100644 backend/src/test/scala/model/managers/movements/TestRandomMovementTest.scala 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/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) From 4aa781e3758014c0dc30f694d12095e320e40aa6 Mon Sep 17 00:00:00 2001 From: NickGhignatti Date: Sat, 26 Jul 2025 10:56:30 +0200 Subject: [PATCH 10/19] =?UTF-8?q?=E2=9C=A8=20Introduce=20framerate=20conce?= =?UTF-8?q?pt=20in=20the=20simulation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/scala/model/SimulationState.scala | 15 ++++++++++++++- .../test/scala/model/TestSimulationState.scala | 8 ++++++++ frontend/src/main/scala/view/ButtonBar.scala | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) 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/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/frontend/src/main/scala/view/ButtonBar.scala b/frontend/src/main/scala/view/ButtonBar.scala index 3f17af8..48689d5 100644 --- a/frontend/src/main/scala/view/ButtonBar.scala +++ b/frontend/src/main/scala/view/ButtonBar.scala @@ -57,7 +57,7 @@ class ButtonBar( Some( dom.window.setInterval( () => eventBus.writer.onNext(Event.SimulationTick), - 1000 / 60 + 1000 / model.now().frameRate ) ) ) From 8bcd713d19a58db3b7547caf12beeca9eb97a654 Mon Sep 17 00:00:00 2001 From: Luca Patrignani Date: Sat, 26 Jul 2025 10:59:45 +0200 Subject: [PATCH 11/19] =?UTF-8?q?=E2=9C=A8=20Add=20random=20movement=20to?= =?UTF-8?q?=20default=20customer=20and=20its=20weight=20to=20the=20view?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/scala/model/entities/customers/Customer.scala | 5 ++++- frontend/src/main/scala/view/ConfigForm.scala | 5 +++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/backend/src/main/scala/model/entities/customers/Customer.scala b/backend/src/main/scala/model/entities/customers/Customer.scala index 40ecd2b..34f936c 100644 --- a/backend/src/main/scala/model/entities/customers/Customer.scala +++ b/backend/src/main/scala/model/entities/customers/Customer.scala @@ -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 @@ -113,7 +114,8 @@ 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 = @@ -130,6 +132,7 @@ case class DefaultMovementManager( | VelocityLimiterManager(maxSpeed) ) ) + | SingleCustomerAdapter(randomMovementWeight * RandomMovementManager()) | WallAvoidingAdapter(AvoidObstaclesManager()) | SingleCustomerAdapter(MoverManager()) diff --git a/frontend/src/main/scala/view/ConfigForm.scala b/frontend/src/main/scala/view/ConfigForm.scala index 8ba9b29..655b600 100644 --- a/frontend/src/main/scala/view/ConfigForm.scala +++ b/frontend/src/main/scala/view/ConfigForm.scala @@ -53,6 +53,11 @@ case class ConfigForm(update: Var[Update], model: Var[SimulationState]): "Boredom increase", Var(1), (m, v) => m.copy(boredomIncrease = v) + ), + Parameter( + "Random movement weight", + Var(0.2), + (m, v) => m.copy(randomMovementWeight = v) ) ) // Spawning strategy selection From f2bfa6b8768e547843f279cdb196bf283f9befbd Mon Sep 17 00:00:00 2001 From: NickGhignatti Date: Sat, 26 Jul 2025 11:19:18 +0200 Subject: [PATCH 12/19] =?UTF-8?q?=F0=9F=92=84=20Update=20game=20colors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/main/scala/view/Component.scala | 12 ++++++------ frontend/src/main/scala/view/Sidebar.scala | 2 +- style.css | 4 ++++ 3 files changed, 11 insertions(+), 7 deletions(-) diff --git a/frontend/src/main/scala/view/Component.scala b/frontend/src/main/scala/view/Component.scala index 9b01d7f..67b29a8 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 = "#910909" 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 = "#aa16ba" 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 = "#24ba16" ctx.lineWidth = 2 ctx.strokeRect( modelComponent.position.x, 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; From e7f13dca2bb90afbd83525a6b0e260e9e7730e0b Mon Sep 17 00:00:00 2001 From: NickGhignatti Date: Sat, 26 Jul 2025 11:31:37 +0200 Subject: [PATCH 13/19] =?UTF-8?q?=E2=9C=85=20Fix=20a=20test=20for=20gaussi?= =?UTF-8?q?an=20strategy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test changed due to day wrap around --- backend/src/test/scala/model/spawner/TestSpawningStrategy.scala | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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"): From a32fdcb12d0b7ff7820ef0262c47db1db118637e Mon Sep 17 00:00:00 2001 From: Luca Patrignani Date: Sat, 26 Jul 2025 12:20:53 +0200 Subject: [PATCH 14/19] =?UTF-8?q?=F0=9F=93=9D=20Document=20the=20random=20?= =?UTF-8?q?movement=20behaviour?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/report.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/report.md b/docs/report.md index d519485..d99d8df 100644 --- a/docs/report.md +++ b/docs/report.md @@ -131,6 +131,8 @@ function calculate_cohesion(boid, nearby_boids){ - **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. From 004c291d5a6f74c793e1c7cb7ed8b400e2b4d430 Mon Sep 17 00:00:00 2001 From: Luca Patrignani Date: Sat, 26 Jul 2025 13:02:02 +0200 Subject: [PATCH 15/19] =?UTF-8?q?=F0=9F=92=84=20Stroke=20with=20black=20th?= =?UTF-8?q?e=20playing=20customers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/main/scala/view/CanvasManager.scala | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/main/scala/view/CanvasManager.scala b/frontend/src/main/scala/view/CanvasManager.scala index 1ef87c4..efdadd6 100644 --- a/frontend/src/main/scala/view/CanvasManager.scala +++ b/frontend/src/main/scala/view/CanvasManager.scala @@ -5,6 +5,7 @@ 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 @@ -161,7 +162,9 @@ class CanvasManager( case Regular => "green" case VIP => "red" case Impulsive => "magenta" - ctx.strokeStyle = ctx.fillStyle + ctx.strokeStyle = customer.customerState match + case CustState.Playing(_) => "black" + case _ => ctx.fillStyle ctx.fill() ctx.stroke() } From 63bb07aed868d143b526b2145d4d04ba06cd1c0c Mon Sep 17 00:00:00 2001 From: Luca Patrignani Date: Sat, 26 Jul 2025 13:02:58 +0200 Subject: [PATCH 16/19] =?UTF-8?q?=F0=9F=90=9B=20Do=20not=20make=20the=20cu?= =?UTF-8?q?stomer=20move=20when=20they=20are=20playing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/entities/customers/Customer.scala | 22 ++++++++++--------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/backend/src/main/scala/model/entities/customers/Customer.scala b/backend/src/main/scala/model/entities/customers/Customer.scala index 985de30..49c527b 100644 --- a/backend/src/main/scala/model/entities/customers/Customer.scala +++ b/backend/src/main/scala/model/entities/customers/Customer.scala @@ -128,19 +128,21 @@ case class DefaultMovementManager( 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) ) ) - | SingleCustomerAdapter(randomMovementWeight * RandomMovementManager()) | WallAvoidingAdapter(AvoidObstaclesManager()) | SingleCustomerAdapter(MoverManager()) From ebcec9bcc346c7f56b4ca437b33be5cee4f2db72 Mon Sep 17 00:00:00 2001 From: Luca Patrignani Date: Sat, 26 Jul 2025 13:04:09 +0200 Subject: [PATCH 17/19] =?UTF-8?q?=E2=9C=A8=20Tune=20some=20simulation=20pa?= =?UTF-8?q?rameters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/main/scala/view/ConfigForm.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/src/main/scala/view/ConfigForm.scala b/frontend/src/main/scala/view/ConfigForm.scala index 655b600..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), @@ -51,7 +51,7 @@ case class ConfigForm(update: Var[Update], model: Var[SimulationState]): ), Parameter( "Boredom increase", - Var(1), + Var(0.2), (m, v) => m.copy(boredomIncrease = v) ), Parameter( From 2b1477d29e587e7d39cb9344e0709eae3608a0c8 Mon Sep 17 00:00:00 2001 From: Marco Galeri Date: Sat, 26 Jul 2025 13:13:18 +0200 Subject: [PATCH 18/19] =?UTF-8?q?=F0=9F=90=9B=20When=20changing=20game=20b?= =?UTF-8?q?et=20get=20select=20correctly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/entities/spawner/Spawner.scala | 2 +- .../model/managers/DecisionManager.scala | 29 +++++++++++++++++-- backend/src/main/scala/update/Update.scala | 18 +++++++----- 3 files changed, 39 insertions(+), 10 deletions(-) diff --git a/backend/src/main/scala/model/entities/spawner/Spawner.scala b/backend/src/main/scala/model/entities/spawner/Spawner.scala index 760e5e9..274e64f 100644 --- a/backend/src/main/scala/model/entities/spawner/Spawner.scala +++ b/backend/src/main/scala/model/entities/spawner/Spawner.scala @@ -89,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.05) + case SlotMachine => FlatBetting[Customer](br * 0.05, defaultRedBet) Customer() .withPosition(this.position.around(5.0)) diff --git a/backend/src/main/scala/model/managers/DecisionManager.scala b/backend/src/main/scala/model/managers/DecisionManager.scala index e63dc27..b8679ed 100644 --- a/backend/src/main/scala/model/managers/DecisionManager.scala +++ b/backend/src/main/scala/model/managers/DecisionManager.scala @@ -24,6 +24,7 @@ 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 @@ -35,6 +36,7 @@ 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 @@ -86,7 +88,17 @@ case class DecisionManager[ 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(VIP,SlotMachine,Martingale, Always ,FlatBet,0.03), + SwitchRule(Impulsive,SlotMachine,Martingale, Always ,FlatBet,0.03), + SwitchRule(Casual,SlotMachine,Martingale, Always ,FlatBet,0.03), + SwitchRule(Regular,SlotMachine,Martingale, Always ,FlatBet,0.03), + SwitchRule(VIP,SlotMachine,OscarGrind, Always ,FlatBet,0.03), + SwitchRule(Impulsive,SlotMachine,OscarGrind, Always ,FlatBet,0.03), + SwitchRule(Casual,SlotMachine,OscarGrind, Always ,FlatBet,0.03), + SwitchRule(Regular,SlotMachine,OscarGrind, Always ,FlatBet,0.03), + ) //format: on object ConfigLoader: @@ -117,11 +129,24 @@ case class DecisionManager[ 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 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 diff --git a/backend/src/main/scala/update/Update.scala b/backend/src/main/scala/update/Update.scala index 21ac09c..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 = @@ -122,7 +117,16 @@ case class Update(customerManager: DefaultMovementManager): updatedCustomerState, state.games ) - state.copy(customers = pDUPosition, games = pDUGames) + + 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( From b3fe060aef30d4bcdb68a94718e8944ead40e6c1 Mon Sep 17 00:00:00 2001 From: Marco Galeri Date: Sat, 26 Jul 2025 13:33:29 +0200 Subject: [PATCH 19/19] =?UTF-8?q?=F0=9F=92=84=20Improve=20decision=20and?= =?UTF-8?q?=20ui?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../model/managers/DecisionManager.scala | 21 ++++++++++++------- frontend/src/main/scala/view/Component.scala | 6 +++--- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/backend/src/main/scala/model/managers/DecisionManager.scala b/backend/src/main/scala/model/managers/DecisionManager.scala index b8679ed..5e7ade0 100644 --- a/backend/src/main/scala/model/managers/DecisionManager.scala +++ b/backend/src/main/scala/model/managers/DecisionManager.scala @@ -76,28 +76,35 @@ case class DecisionManager[ SwitchRule(VIP, Blackjack, Martingale, Losses(3), OscarGrind, 0.05), SwitchRule(VIP, Roulette, Martingale, Losses(4), OscarGrind, 0.05), 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) || 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(VIP,SlotMachine,Martingale, Always ,FlatBet,0.03), SwitchRule(Impulsive,SlotMachine,Martingale, Always ,FlatBet,0.03), - SwitchRule(Casual,SlotMachine,Martingale, Always ,FlatBet,0.03), - SwitchRule(Regular,SlotMachine,Martingale, Always ,FlatBet,0.03), - SwitchRule(VIP,SlotMachine,OscarGrind, Always ,FlatBet,0.03), + SwitchRule(Impulsive,Blackjack,FlatBet, Always ,Martingale,0.02), SwitchRule(Impulsive,SlotMachine,OscarGrind, Always ,FlatBet,0.03), - SwitchRule(Casual,SlotMachine,OscarGrind, Always ,FlatBet,0.03), - SwitchRule(Regular,SlotMachine,OscarGrind, Always ,FlatBet,0.03), + SwitchRule(Impulsive,Roulette,FlatBet, Always ,Martingale,0.03), ) //format: on diff --git a/frontend/src/main/scala/view/Component.scala b/frontend/src/main/scala/view/Component.scala index 67b29a8..6207794 100644 --- a/frontend/src/main/scala/view/Component.scala +++ b/frontend/src/main/scala/view/Component.scala @@ -57,7 +57,7 @@ class SlotComponent(initialModel: SlotMachineGame) ) // Draw border - ctx.strokeStyle = "#910909" + ctx.strokeStyle = "#470505" ctx.lineWidth = 2 ctx.strokeRect( modelComponent.position.x, @@ -84,7 +84,7 @@ class RouletteComponent(initialModel: RouletteGame) ) // Draw border - ctx.strokeStyle = "#aa16ba" + ctx.strokeStyle = "#590c61" ctx.lineWidth = 2 ctx.strokeRect( modelComponent.position.x, @@ -111,7 +111,7 @@ class BlackJackComponent(initialModel: BlackJackGame) ) // Draw border - ctx.strokeStyle = "#24ba16" + ctx.strokeStyle = "#125c0b" ctx.lineWidth = 2 ctx.strokeRect( modelComponent.position.x,