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

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

28 Aug 2025 07:57AM UTC coverage: 46.066% (+0.5%) from 45.534%
#415

push

github

srs-mate
refactor: fix rebase conflict

1 of 10 new or added lines in 2 files covered. (10.0%)

66 existing lines in 6 files now uncovered.

1206 of 2618 relevant lines covered (46.07%)

6.6 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.entity.dynamicentity.Robot
14
import io.github.srs.model.entity.dynamicentity.behavior.BehaviorContext
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

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

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

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

52
  end Controller
53

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

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

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

79
    object Controller:
80

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

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

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

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

×
137
          loop(s)
UNCOV
138

×
139
        end simulationLoop
140

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

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

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

×
178
            case PAUSED =>
UNCOV
179
              IO.sleep(50.millis).as(state)
×
UNCOV
180

×
181
            case STOPPED =>
UNCOV
182
              IO.pure(state)
×
UNCOV
183

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

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

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

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

×
271
      end ControllerImpl
272

273
    end Controller
274

275
  end Component
276

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