package snake /** The outside world, as the runtime sees it. */ trait Platform: def fillRect(x: Int, y: Int, w: Int, h: Int, color: String): Unit def fillText(x: Int, y: Int, text: String, color: String, size: Int, align: String): Unit def randomSeed(): Long def startClock(onTime: Long => Unit): Unit def onKeyDown(cb: String => Unit): Unit def onKeyUp(cb: String => Unit): Unit /** * m09 runtime: the single impure place in the course (with the web/ glue); everything else only builds values. * Here we interpret IO descriptions against a Platform and hold the one mutable variable, the current stream. */ object Runtime: // Perform one op on the platform and return its answer. private def perform(op: Op, p: Platform): OpResult = op match case Op.RandomSeed => OpResult.Seed(p.randomSeed()) case Op.Draw(Shape.Rect(x, y, w, h, color)) => p.fillRect(x, y, w, h, color) OpResult.Done case Op.Draw(Shape.Text(x, y, text, color, size, align)) => p.fillText(x, y, text, color, size, align) OpResult.Done // Tail recursive, so it compiles to a loop: a scene is hundreds of ops and the stack must not grow with them. @annotation.tailrec def runIO[A](io: IO[A], p: Platform): A = io match case IO.Pure(a) => a case IO.Effect(op, k) => runIO(k(perform(op, p)), p) def runApp(main: IO[Stream[IO[Unit]]], p: Platform): Unit = var stream = runIO(main, p) def feed(event: Event): Unit = val (ios, next) = stream.step(event) stream = next ios.foreach(runIO(_, p)) p.onKeyDown(key => feed(Event.KeyDown(key))) p.onKeyUp(key => feed(Event.KeyUp(key))) p.startClock(t => feed(Event.Time(t)))