• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

Scala-Robotics-Simulator / PPS-22-srs / #336

22 Aug 2025 08:10AM UTC coverage: 57.135% (+0.4%) from 56.785%
#336

Pull #51

github

sceredi
chore: remove tostring from warts

It is the only way to serialize behaviors
Pull Request #51: feat: serialization/parsing of behavior

17 of 20 new or added lines in 9 files covered. (85.0%)

1 existing line in 1 file now uncovered.

1105 of 1934 relevant lines covered (57.14%)

7.94 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/src/main/scala/io/github/srs/controller/ControllerModule.scala
1
package io.github.srs.controller
2

3
import scala.concurrent.duration.{ DurationInt, FiniteDuration, MILLISECONDS }
4
import scala.language.postfixOps
5

6
import cats.effect.std.Queue
7
import cats.effect.{ Clock, IO }
8
import cats.syntax.all.*
9
import io.github.srs.model.*
10
import io.github.srs.model.SimulationConfig.SimulationStatus
11
import io.github.srs.model.UpdateLogic.*
12
import io.github.srs.model.entity.dynamicentity.Robot
13
import io.github.srs.model.entity.dynamicentity.sensor.Sensor.senseAll
14
import io.github.srs.model.logic.*
15
import io.github.srs.utils.SimulationDefaults.debugMode
16
import io.github.srs.utils.random.RNG
17
import io.github.srs.utils.random.RandomDSL.{ generate, shuffle }
18

19
/**
20
 * Module that defines the controller logic for the Scala Robotics Simulator.
21
 */
22
object ControllerModule:
23

×
24
  /**
25
   * Controller trait that defines the interface for the controller.
26
   *
27
   * @tparam S
28
   *   the type of the state, which must extend [[ModelModule.State]].
29
   */
30
  trait Controller[S <: ModelModule.State]:
31
    /**
×
32
     * Starts the controller with the initial state.
33
     *
34
     * @param initialState
35
     *   the initial state of the simulation.
36
     */
37
    def start(initialState: S): IO[Unit]
38

39
    /**
40
     * Runs the simulation loop, processing events from the queue and updating the state.
41
     *
42
     * @param s
43
     *   the current state of the simulation.
44
     * @param queue
45
     *   a concurrent queue that holds events to be processed.
46
     * @return
47
     *   an [[IO]] task that completes when the simulation loop ends.
48
     */
49
    def simulationLoop(s: S, queue: Queue[IO, Event]): IO[Unit]
50

51
  end Controller
52

53
  /**
54
   * Provider trait that defines the interface for providing a controller.
55
   *
56
   * @tparam S
57
   *   the type of the state, which must extend [[ModelModule.State]].
58
   */
59
  trait Provider[S <: ModelModule.State]:
60
    val controller: Controller[S]
61

62
  /**
63
   * Defines the dependencies required by the controller module. In particular, it requires a
64
   * [[io.github.srs.view.ViewModule.Provider]] and a [[io.github.srs.model.ModelModule.Provider]].
65
   */
66
  type Requirements[S <: ModelModule.State] =
67
    io.github.srs.view.ViewModule.Provider[S] & io.github.srs.model.ModelModule.Provider[S]
68

69
  /**
70
   * Component trait that defines the interface for creating a controller.
71
   *
72
   * @tparam S
73
   *   the type of the simulation state, which must extend [[ModelModule.State]].
74
   */
75
  trait Component[S <: ModelModule.State]:
76
    context: Requirements[S] =>
×
77

78
    object Controller:
79

×
80
      /**
81
       * Creates a controller instance.
82
       *
83
       * @return
84
       *   a [[Controller]] instance.
85
       */
86
      def apply()(using bundle: LogicsBundle[S]): Controller[S] = new ControllerImpl
87

×
88
      /**
89
       * Private controller implementation that delegates the simulation loop to the provided model and view.
90
       */
91
      private class ControllerImpl(using bundle: LogicsBundle[S]) extends Controller[S]:
92

×
93
        override def start(initialState: S): IO[Unit] =
94
          for
×
95
            queueSim <- Queue.unbounded[IO, Event]
×
96
            _ <- context.view.init(queueSim)
×
97
            _ <- runBehavior(queueSim, initialState)
98
            _ <- simulationLoop(initialState, queueSim)
99
          yield ()
100

×
101
        override def simulationLoop(s: S, queue: Queue[IO, Event]): IO[Unit] =
102
          def loop(state: S): IO[Unit] =
×
103
            for
×
104
              startTime <- Clock[IO].realTime.map(_.toMillis)
×
105
              _ <- runBehavior(queue, state).whenA(state.simulationStatus == SimulationStatus.RUNNING)
×
106

×
107
              events <- queue.tryTakeN(Some(50))
108
              shuffledEvents <- shuffleEvents(queue, state, events)
109
              newState <- handleEvents(state, shuffledEvents)
110

111
              _ <- context.view.render(newState)
112

113
              nextState <- nextStep(newState, startTime)
114

115
              stop = newState.simulationStatus == SimulationStatus.STOPPED ||
116
                newState.simulationTime.exists(max => newState.elapsedTime >= max)
117

118
              endTime <- Clock[IO].realTime.map(_.toMillis)
119
              _ <- if debugMode then IO.println(s"Simulation loop took ${endTime - startTime} ms") else IO.unit
120

121
              _ <- if stop then IO.unit else loop(nextState)
122
            yield ()
123

×
124
          loop(s)
125

×
126
        end simulationLoop
127

128
        private def nextStep(state: S, startTime: Long): IO[S] =
129
          state.simulationStatus match
×
130
            case SimulationStatus.RUNNING =>
×
131
              tickEvents(startTime, state.simulationSpeed.tickSpeed, state)
×
132

×
133
            case SimulationStatus.PAUSED =>
134
              IO.sleep(50.millis).as(state)
×
135

×
136
            case SimulationStatus.STOPPED =>
137
              IO.pure(state)
×
138

×
139
        private def runBehavior(queue: Queue[IO, Event], state: S): IO[Unit] =
140
          state.environment.entities.collect { case robot: Robot =>
×
141
            for
×
142
              sensorReadings <- robot.senseAll[IO](state.environment)
×
NEW
143
              action = robot.behavior.run(sensorReadings)
×
UNCOV
144
              _ <- queue.offer(Event.RobotAction(queue, robot, action))
×
145
            yield ()
146
          }.toList.parSequence.void
×
147

×
148
        private def tickEvents(start: Long, tickSpeed: FiniteDuration, state: S): IO[S] =
149
          for
×
150
            now <- Clock[IO].realTime.map(_.toMillis)
×
151
            timeToNextTick = tickSpeed.toMillis - (now - start)
×
152
            adjustedTickSpeed = if timeToNextTick > 0 then timeToNextTick else 0L
153
            sleepTime = FiniteDuration(adjustedTickSpeed, MILLISECONDS)
154
            _ <- IO.sleep(sleepTime)
×
155
            tick <- handleEvent(state, Event.Tick(tickSpeed))
156
          yield tick
157

×
158
        private def shuffleEvents(queue: Queue[IO, Event], state: S, events: Seq[Event]): IO[Seq[Event]] =
159
          val (controllerEvents, otherEvents) = events.partition:
×
160
            case _: Event.RobotAction => false
×
161
            case _ => true
×
162
          val (shuffledEvents, nextRNG: RNG) = state.simulationRNG generate (otherEvents shuffle)
×
163
          queue.offer(Event.Random(nextRNG)).as(controllerEvents ++ shuffledEvents)
×
164

×
165
        private def handleEvents(state: S, shuffledEvents: Seq[Event]): IO[S] =
166
          for finalState <- shuffledEvents.foldLeft(IO.pure(state)) { (taskState, event) =>
×
167
              for
×
168
                currentState <- taskState
169
                newState <- handleEvent(currentState, event)
170
              yield newState
171
            }
172
          yield finalState
×
173

×
174
        private def handleEvent(state: S, event: Event): IO[S] =
175
          event match
×
176
            case Event.Increment if state.simulationStatus == SimulationStatus.RUNNING =>
×
177
              context.model.increment(state)
×
178
            case Event.Tick(deltaTime) if state.simulationStatus == SimulationStatus.RUNNING =>
×
179
              context.model.tick(state, deltaTime)
×
180
            case Event.TickSpeed(speed) => context.model.tickSpeed(state, speed)
×
181
            case Event.Random(rng) => context.model.random(state, rng)
×
182
            case Event.Pause => context.model.pause(state)
×
183
            case Event.Resume => context.model.resume(state)
×
184
            case Event.Stop => context.model.stop(state)
×
185
            case Event.RobotAction(queue, robot, action) => context.model.handleRobotAction(state, queue, robot, action)
×
186
            case Event.CollisionDetected(queue, robot, updatedRobot) =>
×
187
              context.model.handleCollision(state, queue, robot, updatedRobot)
×
188
            case _ => IO.pure(state)
×
189

×
190
      end ControllerImpl
191

192
    end Controller
193

194
  end Component
195

196
  /**
197
   * Interface trait that combines the provider and component traits for the controller module.
198
   *
199
   * @tparam S
200
   *   the type of the simulation state, which must extend [[ModelModule.State]].
201
   */
202
  trait Interface[S <: ModelModule.State] extends Provider[S] with Component[S]:
203
    self: Requirements[S] =>
204
end ControllerModule
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc