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

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

14 Oct 2025 11:02AM UTC coverage: 77.925% (-2.7%) from 80.592%
#666

push

github

GiuliaNardicchia
refactor: remove useless speed update method from Wheel class

1292 of 1658 relevant lines covered (77.93%)

8.8 hits per line

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

80.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 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
import io.github.srs.protos.ping.PongerFs2Grpc
21
import io.github.srs.controller.protobuf.ping.PongerService
22
import io.grpc.netty.shaded.io.grpc.netty.NettyServerBuilder
23

24
/**
25
 * Module that defines the controller logic for the Scala Robotics Simulator.
26
 */
27
object ControllerModule:
28

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

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

56
  end Controller
57

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

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

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

83
    object Controller:
84

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

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

8✔
98
        private val logger = Logger(getClass.getName)
99

8✔
100
        /**
101
         * Starts the controller with the initial state.
102
         * @param initialState
103
         *   the initial state of the simulation.
104
         * @return
105
         *   an [[IO]] task that completes when the controller is started.
106
         */
107
        override def start(initialState: S): IO[S] =
108
          for
109
            queueSim <- Queue.unbounded[IO, Event]
110
            service <- PongerFs2Grpc.bindServiceResource(new PongerService).allocated
10✔
111
            (svc, release) = service
112
            server = NettyServerBuilder.forPort(50051).addService(svc).build()
113
            _ <- IO.println("Starting PongerService") *> IO(server.start())
114
            _ <- context.view.init(queueSim)
3✔
115
            _ <- runBehavior(queueSim, initialState)
116
            result <- simulationLoop(initialState, queueSim)
117
            _ <- IO(server.shutdownNow().awaitTermination()) *> release
118
          yield result
4✔
119

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

148
          loop(s)
149

5✔
150
        end simulationLoop
151

152
        private def handleStopCondition(state: S): Option[IO[S]] =
153
          state.simulationStatus match
154
            case STOPPED =>
3✔
155
              Some(context.view.close() *> IO.pure(state))
8✔
156
            case ELAPSED_TIME =>
×
157
              Some(context.view.timeElapsed(state) *> IO.pure(state))
8✔
158
            case _ =>
14✔
159
              None
160

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

8✔
175
            case PAUSED =>
176
              IO.sleep(50.millis).as(state)
×
177

×
178
            case _ =>
179
              IO.pure(state)
180

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

35✔
193
          def process(remaining: List[Robot], rng: RNG, acc: List[RobotProposal]): IO[(List[RobotProposal], RNG)] =
194
            remaining match
195
              case Nil => IO.pure((acc.reverse, rng))
2✔
196
              case robot :: tail =>
17✔
197
                for
15✔
198
                  sensorReadings <- robot.senseAll[IO](state.environment)
199
                  ctx = BehaviorContext(sensorReadings, rng)
24✔
200
                  (action, newRng) = robot.behavior.run[IO](ctx)
201
                  _ <- queue.offer(Event.Random(newRng))
202
                  next <- process(tail, newRng, RobotProposal(robot, action) :: acc)
203
                yield next
204

×
205
          for
206
            (proposals, _) <- process(robots, state.simulationRNG, Nil)
207
            _ <- queue.offer(Event.RobotActionProposals(proposals))
12✔
208
          yield ()
209

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

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

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

13✔
275
      end ControllerImpl
276

277
    end Controller
278

279
  end Component
280

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