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

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

23 Aug 2025 10:18AM UTC coverage: 58.131% (+0.4%) from 57.73%
#358

Pull #55

github

sceredi
chore: remove print
Pull Request #55: fix: move illumination caching to simulation state

10 of 27 new or added lines in 11 files covered. (37.04%)

13 existing lines in 6 files now uncovered.

1126 of 1937 relevant lines covered (58.13%)

8.19 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.controller.message.RobotProposal
10
import io.github.srs.controller.protocol.Event
11
import io.github.srs.model.*
12
import io.github.srs.model.SimulationConfig.SimulationStatus.{ PAUSED, RUNNING, STOPPED }
13
import io.github.srs.model.UpdateLogic.*
14
import io.github.srs.model.entity.dynamicentity.Robot
15
import io.github.srs.model.entity.dynamicentity.sensor.Sensor.senseAll
16
import io.github.srs.model.logic.*
17
import io.github.srs.utils.EqualityGivenInstances.given_CanEqual_Event_Event
18
import io.github.srs.utils.SimulationDefaults.debugMode
19
import io.github.srs.model.SimulationConfig.SimulationStatus
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 [[ModelModule.State]].
31
   */
32
  trait Controller[S <: ModelModule.State]:
33
    /**
×
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[Unit]
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 [[IO]] task that completes when the simulation loop ends.
50
     */
51
    def simulationLoop(s: S, queue: Queue[IO, Event]): IO[Unit]
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 [[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 [[ModelModule.State]].
76
   */
77
  trait Component[S <: ModelModule.State]:
78
    context: Requirements[S] =>
×
79

80
    object Controller:
81

×
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

×
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

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

×
110
        /**
111
         * Runs the simulation loop, processing events from the queue and updating the state.
112
         * @param s
113
         *   the current state of the simulation.
114
         * @param queue
115
         *   a concurrent queue that holds events to be processed.
116
         * @return
117
         *   an [[IO]] task that completes when the simulation loop ends.
118
         */
119
        override def simulationLoop(s: S, queue: Queue[IO, Event]): IO[Unit] =
120
          def loop(state: S): IO[Unit] =
×
121
            for
×
122
              startTime <- Clock[IO].realTime.map(_.toMillis)
×
NEW
123
              _ <- updateField(queue, state)
×
124
              _ <- runBehavior(queue, state).whenA(state.simulationStatus == RUNNING)
125
              events <- queue.tryTakeN(Some(50))
×
126
              newState <- handleEvents(state, events)
127
              _ <- context.view.render(newState)
128
              nextState <- nextStep(newState, startTime)
129
              endTime <- Clock[IO].realTime.map(_.toMillis)
130
              _ <- if debugMode then IO.println(s"Simulation loop took ${endTime - startTime} ms") else IO.unit
131
              _ <- if stopCondition(nextState) then IO.unit else loop(nextState)
132
            yield ()
133

×
134
          loop(s)
135

×
136
        /**
137
         * Checks if the simulation should stop based on the current state.
138
         * @param state
139
         *   the current state of the simulation.
140
         * @return
141
         *   a boolean indicating whether the simulation should stop.
142
         */
143
        private def stopCondition(state: S): Boolean =
144
          state.simulationStatus == STOPPED ||
×
145
            elapsedTimeReached(state.simulationTime, state.elapsedTime)
×
146

×
147
        /**
148
         * Checks if the elapsed time has reached the maximum simulation time.
149
         * @param simulationTime
150
         *   the maximum simulation time, if defined.
151
         * @param elapsedTime
152
         *   the elapsed time since the simulation started.
153
         * @return
154
         *   a boolean indicating whether the elapsed time has reached the maximum simulation time.
155
         */
156
        private def elapsedTimeReached(simulationTime: Option[FiniteDuration], elapsedTime: FiniteDuration): Boolean =
157
          simulationTime.exists(max => elapsedTime >= max)
×
158

×
159
        /**
160
         * Processes the next step in the simulation based on the current state and start time.
161
         * @param state
162
         *   the current state of the simulation.
163
         * @param startTime
164
         *   the start time of the current simulation step in milliseconds.
165
         * @return
166
         *   the next state of the simulation wrapped in an [[IO]] task.
167
         */
168
        private def nextStep(state: S, startTime: Long): IO[S] =
169
          state.simulationStatus match
×
170
            case RUNNING =>
×
171
              tickEvents(startTime, state.simulationSpeed.tickSpeed, state)
×
172

×
173
            case PAUSED =>
174
              IO.sleep(50.millis).as(state)
×
175

×
176
            case STOPPED =>
177
              IO.pure(state)
×
178

×
179
        private def updateField(queue: Queue[IO, Event], state: S): IO[Unit] =
NEW
180
          for _ <-
×
NEW
181
              if state.simulationStatus == SimulationStatus.RUNNING then queue.offer(Event.UpdateLightField)
×
NEW
182
              else IO.unit
×
NEW
183
          yield ()
×
NEW
184

×
185
        /**
186
         * Runs the behavior of all robots in the environment and collects their action proposals.
187
         * @param queue
188
         *   the queue to which the proposals will be offered through the [[Event.RobotActionProposals]] event.
189
         * @param state
190
         *   the current state of the simulation.
191
         * @return
192
         *   an [[IO]] task that completes when the behavior has been run.
193
         */
194
        private def runBehavior(queue: Queue[IO, Event], state: S): IO[Unit] =
195
          for
×
196
            proposals <- state.environment.entities.collect { case robot: Robot => robot }.toList.parTraverse { robot =>
×
197
              for
×
198
                sensorReadings <- robot.senseAll[IO](state)
199
                action = robot.behavior.run(sensorReadings)
200
              yield RobotProposal(robot, action)
201
            }
202
            _ <- queue.offer(Event.RobotActionProposals(proposals))
×
203
          yield ()
204

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

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

×
244
        /**
245
         * Handles a single event and updates the state accordingly.
246
         * @param state
247
         *   the current state of the simulation.
248
         * @param event
249
         *   the event to be processed.
250
         * @return
251
         *   the updated state of the simulation after processing the event, wrapped in an [[IO]] task.
252
         */
253
        private def handleEvent(state: S, event: Event): IO[S] =
254
          event match
×
255
            case Event.Tick(deltaTime) => context.model.tick(state, deltaTime)
×
256
            case Event.TickSpeed(speed) => context.model.tickSpeed(state, speed)
×
257
            case Event.Random(rng) => context.model.random(state, rng)
×
258
            case Event.Pause => context.model.pause(state)
×
259
            case Event.Resume => context.model.resume(state)
×
260
            case Event.Stop => context.model.stop(state)
×
261
            case Event.RobotActionProposals(proposals) => context.model.handleRobotActionsProposals(state, proposals)
×
NEW
262
            case Event.UpdateLightField => context.model.updateLightField(state)
×
UNCOV
263

×
264
      end ControllerImpl
265

266
    end Controller
267

268
  end Component
269

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