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

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

14 Oct 2025 09:07AM UTC coverage: 78.136% (+0.005%) from 78.131%
#643

push

github

GiuliaNardicchia
chore: fix logging issues

1283 of 1642 relevant lines covered (78.14%)

8.9 hits per line

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

79.17
/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 io.github.srs.controller.message.RobotProposal
9
import io.github.srs.controller.protocol.Event
10
import io.github.srs.model.*
11
import io.github.srs.model.SimulationConfig.SimulationStatus.*
12
import io.github.srs.model.entity.dynamicentity.Robot
13
import io.github.srs.model.entity.dynamicentity.behavior.BehaviorContext
14
import io.github.srs.model.entity.dynamicentity.sensor.Sensor.senseAll
15
import io.github.srs.model.logic.*
16
import io.github.srs.utils.EqualityGivenInstances.given
17
import cats.implicits.*
18
import io.github.srs.utils.random.RNG
19
import com.typesafe.scalalogging.Logger
20

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

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

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

53
  end Controller
54

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

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

71
  /**
72
   * Component trait that defines the interface for creating a controller.
73
   *
74
   * @tparam S
75
   *   the type of the simulation state, which must extend [[io.github.srs.model.ModelModule.State]].
76
   */
77
  trait Component[S <: ModelModule.State]:
78
    context: Requirements[S] =>
1✔
79

80
    object Controller:
81

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

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

8✔
95
        private val logger = Logger(getClass.getName)
96

8✔
97
        /**
98
         * Starts the controller with the initial state.
99
         * @param initialState
100
         *   the initial state of the simulation.
101
         * @return
102
         *   an [[IO]] task that completes when the controller is started.
103
         */
104
        override def start(initialState: S): IO[S] =
105
          for
106
            queueSim <- Queue.unbounded[IO, Event]
107
            _ <- context.view.init(queueSim)
10✔
108
            _ <- runBehavior(queueSim, initialState)
109
            result <- simulationLoop(initialState, queueSim)
110
          yield result
111

112
        /**
113
         * Runs the simulation loop, processing events from the queue and updating the state.
114
         * @param s
115
         *   the current state of the simulation.
116
         * @param queue
117
         *   a concurrent queue that holds events to be processed.
118
         * @return
119
         *   an [[IO]] task that completes when the simulation loop ends.
120
         */
121
        override def simulationLoop(s: S, queue: Queue[IO, Event]): IO[S] =
122
          def loop(state: S): IO[S] =
123
            for
124
              startTime <- Clock[IO].realTime.map(_.toMillis)
125
              _ <- runBehavior(queue, state).whenA(state.simulationStatus == RUNNING)
15✔
126
              events <- queue.tryTakeN(None)
5✔
127
              newState <- handleEvents(state, events)
128
              _ <- context.view.render(newState)
129
              result <- handleStopCondition(newState) match
130
                case Some(io) => io
131
                case None =>
132
                  for
133
                    nextState <- nextStep(newState, startTime)
134
                    endTime <- Clock[IO].realTime.map(_.toMillis)
135
                    _ <- IO.blocking(logger.debug(s"Simulation loop took ${endTime - startTime} ms"))
136
                    res <- loop(nextState)
8✔
137
                  yield res
138
            yield result
139

140
          loop(s)
141

5✔
142
        end simulationLoop
143

144
        private def handleStopCondition(state: S): Option[IO[S]] =
145
          state.simulationStatus match
146
            case STOPPED =>
3✔
147
              Some(context.view.close() *> IO.pure(state))
8✔
148
            case ELAPSED_TIME =>
×
149
              Some(context.view.timeElapsed(state) *> IO.pure(state))
8✔
150
            case _ =>
14✔
151
              None
152

2✔
153
        /**
154
         * Processes the next step in the simulation based on the current state and start time.
155
         * @param state
156
         *   the current state of the simulation.
157
         * @param startTime
158
         *   the start time of the current simulation step in milliseconds.
159
         * @return
160
         *   the next state of the simulation wrapped in an [[IO]] task.
161
         */
162
        private def nextStep(state: S, startTime: Long): IO[S] =
163
          state.simulationStatus match
164
            case RUNNING =>
3✔
165
              tickEvents(startTime, state.simulationSpeed.tickSpeed, state)
8✔
166

8✔
167
            case PAUSED =>
168
              IO.sleep(50.millis).as(state)
×
169

×
170
            case _ =>
171
              IO.pure(state)
172

×
173
        /**
174
         * Runs the behavior of all robots in the environment and collects their action proposals.
175
         * @param queue
176
         *   the queue to which the proposals will be offered through the [[Event.RobotActionProposals]] event.
177
         * @param state
178
         *   the current state of the simulation.
179
         * @return
180
         *   an [[IO]] task that completes when the behavior has been run.
181
         */
182
        private def runBehavior(queue: Queue[IO, Event], state: S): IO[Unit] =
183
          val robots = state.environment.entities.collect { case r: Robot => r }.sortBy(_.id.toString)
184

35✔
185
          def process(remaining: List[Robot], rng: RNG, acc: List[RobotProposal]): IO[(List[RobotProposal], RNG)] =
186
            remaining match
187
              case Nil => IO.pure((acc.reverse, rng))
2✔
188
              case robot :: tail =>
17✔
189
                for
15✔
190
                  sensorReadings <- robot.senseAll[IO](state.environment)
191
                  ctx = BehaviorContext(sensorReadings, rng)
24✔
192
                  (action, newRng) = robot.behavior.run[IO](ctx)
193
                  _ <- queue.offer(Event.Random(newRng))
194
                  next <- process(tail, newRng, RobotProposal(robot, action) :: acc)
195
                yield next
196

×
197
          for
198
            (proposals, _) <- process(robots, state.simulationRNG, Nil)
199
            _ <- queue.offer(Event.RobotActionProposals(proposals))
12✔
200
          yield ()
201

202
        /**
203
         * Processes tick events, adjusting the tick speed based on the elapsed time since the last tick.
204
         * @param start
205
         *   the start time of the current tick in milliseconds.
206
         * @param tickSpeed
207
         *   the speed of the tick in [[FiniteDuration]].
208
         * @param state
209
         *   the current state of the simulation.
210
         * @return
211
         *   the next state of the simulation wrapped in an [[IO]] task.
212
         */
213
        private def tickEvents(start: Long, tickSpeed: FiniteDuration, state: S): IO[S] =
214
          for
215
            now <- Clock[IO].realTime.map(_.toMillis)
216
            timeToNextTick = tickSpeed.toMillis - (now - start)
18✔
217
            adjustedTickSpeed = if timeToNextTick > 0 then timeToNextTick else 0L
218
            sleepTime = FiniteDuration(adjustedTickSpeed, MILLISECONDS)
219
            _ <- IO.sleep(sleepTime)
220
            tick <- handleEvent(state, Event.Tick(state.dt))
221
          yield tick
222

223
        /**
224
         * Handles a sequence of events, processing them in the order they were received.
225
         * @param state
226
         *   the current state of the simulation.
227
         * @param events
228
         *   the sequence of events to be processed.
229
         * @return
230
         *   the final state of the simulation after processing all events, wrapped in an [[IO]] task.
231
         */
232
        private def handleEvents(state: S, events: Seq[Event]): IO[S] =
233
          for finalState <- events.foldLeft(IO.pure(state)) { (taskState, event) =>
234
              for
11✔
235
                currentState <- taskState
236
                newState <- handleEvent(currentState, event)
237
              yield newState
238
            }
239
          yield finalState
240

241
        /**
242
         * Handles a single event and updates the state accordingly.
243
         * @param state
244
         *   the current state of the simulation.
245
         * @param event
246
         *   the event to be processed.
247
         * @return
248
         *   the updated state of the simulation after processing the event, wrapped in an [[IO]] task.
249
         */
250
        private def handleEvent(state: S, event: Event): IO[S] =
251
          event match
252
            case Event.Tick(deltaTime) =>
2✔
253
              context.model.update(state)(using s => bundle.tickLogic.tick(s, deltaTime))
15✔
254
            case Event.TickSpeed(speed) =>
11✔
255
              context.model.update(state)(using s => bundle.tickLogic.tickSpeed(s, speed))
3✔
256
            case Event.Random(rng) =>
×
257
              context.model.update(state)(using s => bundle.randomLogic.random(s, rng))
15✔
258
            case Event.Pause =>
11✔
259
              context.model.update(state)(using s => bundle.pauseLogic.pause(s))
8✔
260
            case Event.Resume =>
×
261
              context.model.update(state)(using s => bundle.resumeLogic.resume(s))
8✔
262
            case Event.Stop =>
×
263
              context.model.update(state)(using s => bundle.stopLogic.stop(s))
8✔
264
            case Event.RobotActionProposals(proposals) =>
×
265
              context.model.update(state)(using s => bundle.robotActionsLogic.handleRobotActionsProposals(s, proposals))
15✔
266

13✔
267
      end ControllerImpl
268

269
    end Controller
270

271
  end Component
272

273
  /**
274
   * Interface trait that combines the provider and component traits for the controller module.
275
   *
276
   * @tparam S
277
   *   the type of the simulation state, which must extend [[io.github.srs.model.ModelModule.State]].
278
   */
279
  trait Interface[S <: ModelModule.State] extends Provider[S] with Component[S]:
280
    self: Requirements[S] =>
281
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