package snake import scala.collection.mutable.ListBuffer /** A fake platform: records every call and lets the test fire events by hand. */ class FakePlatform(seed: Long = 42) extends Platform: val calls: ListBuffer[List[Any]] = ListBuffer.empty private var onTime: Long => Unit = _ => () private var onDown: String => Unit = _ => () private var onUp: String => Unit = _ => () def fillRect(x: Int, y: Int, w: Int, h: Int, color: String): Unit = calls += List("fillRect", x, y, w, h, color) def fillText(x: Int, y: Int, text: String, color: String, size: Int, align: String): Unit = calls += List("fillText", x, y, text, color, size, align) def randomSeed(): Long = calls += List("randomSeed") seed def startClock(cb: Long => Unit): Unit = onTime = cb def onKeyDown(cb: String => Unit): Unit = onDown = cb def onKeyUp(cb: String => Unit): Unit = onUp = cb def fireTime(t: Long): Unit = onTime(t) def fireKey(key: String): Unit = onDown(key) def fireKeyUp(key: String): Unit = onUp(key) class M09RuntimeSuite extends munit.FunSuite: // The m09 program (classicMain) ticks every 150 ms; the shipped `main` uses speedActions after m10. private def main = M09Helpers.classicMain // What the platform should have been asked to draw for a scene. private def expectedCalls(scene: Scene): List[List[Any]] = scene.map { case Shape.Rect(x, y, w, h, color) => List("fillRect", x, y, w, h, color) case Shape.Text(x, y, text, color, size, align) => List("fillText", x, y, text, color, size, align) } private val rect = Shape.Rect(1, 2, 3, 4, "#abcdef") private val text = Shape.Text(5, 6, "hi", "#fff", 16, "center") test("runIO of a pure value returns it without touching the platform") { val p = FakePlatform() assertEquals(Runtime.runIO(IO.pure(7), p), 7) assertEquals(p.calls.toList, Nil) } test("runIO maps Draw Rect to fillRect(x, y, w, h, color)") { val p = FakePlatform() Runtime.runIO(IO.draw(rect), p) assertEquals(p.calls.toList, List(List("fillRect", 1, 2, 3, 4, "#abcdef"))) } test("runIO maps Draw Text to fillText(x, y, text, color, size, align)") { val p = FakePlatform() Runtime.runIO(IO.draw(text), p) assertEquals(p.calls.toList, List(List("fillText", 5, 6, "hi", "#fff", 16, "center"))) } test("runIO answers RandomSeed with platform.randomSeed() and continues with it") { val p = FakePlatform(99) assertEquals(Runtime.runIO(IO.randomSeed.map(_ + 1), p), 100L) assertEquals(p.calls.toList, List(List("randomSeed"))) } test("runIO performs effects in order") { val p = FakePlatform() Runtime.runIO(IO.sequence(List(IO.draw(rect), IO.draw(text), IO.draw(rect))), p) assertEquals(p.calls.toList.map(_.head), List("fillRect", "fillText", "fillRect")) } test("runIO handles 100000 effects without overflowing the stack") { val p = FakePlatform() val n = 100000 Runtime.runIO(IO.sequence(List.fill(n)(IO.draw(rect))), p) assertEquals(p.calls.length, n) } test("runApp asks the platform for a seed once, at start, and draws nothing before the first event") { val p = FakePlatform() Runtime.runApp(main, p) assertEquals(p.calls.toList, List(List("randomSeed"))) } test("runApp draws the initial scene on the first clock tick, then the ticked scene after 150 ms") { val p = FakePlatform(42) Runtime.runApp(main, p) p.calls.clear() p.fireTime(150) assertEquals( p.calls.toList, expectedCalls(View.view(Spawn.newGame(42))) ++ expectedCalls(View.view(Spawn.tick(Spawn.newGame(42)))) ) } test("runApp feeds key presses to the game") { val p = FakePlatform(42) Runtime.runApp(main, p) p.fireTime(0) p.calls.clear() p.fireKey("ArrowUp") p.fireTime(150) val turned = Spawn.newGame(42).copy(nextDir = Direction.Up) assertEquals(p.calls.toList, expectedCalls(View.view(turned)) ++ expectedCalls(View.view(Spawn.tick(turned)))) } test("runApp ignores key releases and unmapped keys (no redraw)") { val p = FakePlatform(42) Runtime.runApp(main, p) p.fireTime(0) p.calls.clear() p.fireKeyUp("ArrowUp") p.fireKey("x") assertEquals(p.calls.toList, Nil) }