diff --git a/scala/build-web.sh b/scala/build-web.sh new file mode 100755 index 0000000..e251135 --- /dev/null +++ b/scala/build-web.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +# Usage: ./build-web.sh +# Compiles src/ + web/ to JavaScript with Scala.js: web/dist/snake.js (index.html) and web/dist/pong.js (pong.html). +# Then serve the repo root (node scripts/serve.mjs) and open http://localhost:8000/scala/index.html. +set -euo pipefail +cd "$(dirname "$0")" +SCALA_CLI="${SCALA_CLI:-$(command -v scala-cli || echo "$HOME/.local/bin/scala-cli")}" +mkdir -p web/dist +build() { #
+ "$SCALA_CLI" --power package --js --js-module-kind es src web project.scala --main-class "$1" -o "$2" -f +} +build snake.web.SnakeMain web/dist/snake.js +build snake.web.PongMain web/dist/pong.js diff --git a/scala/index.html b/scala/index.html new file mode 100644 index 0000000..b86a432 --- /dev/null +++ b/scala/index.html @@ -0,0 +1,22 @@ + + + + + + Pure Snake + + + + +

Pure Snake

+ +

Arrows / WASD to move. Space to pause, R to restart (after module 10).

+ + + diff --git a/scala/pong.html b/scala/pong.html new file mode 100644 index 0000000..1b60632 --- /dev/null +++ b/scala/pong.html @@ -0,0 +1,22 @@ + + + + + + Pure Pong + + + + +

Pure Pong

+ +

Up/Down arrows (or W/S) move your paddle. First to 5 wins. R restarts.

+ + + diff --git a/scala/web/BrowserPlatform.scala b/scala/web/BrowserPlatform.scala new file mode 100644 index 0000000..6370db2 --- /dev/null +++ b/scala/web/BrowserPlatform.scala @@ -0,0 +1,50 @@ +package snake +package web + +import scala.scalajs.js +import scala.scalajs.js.Dynamic.global +import scala.scalajs.js.typedarray.Uint32Array + +/** + * Browser glue: turns a canvas into a Platform. Impure by nature; not part of the exercises' logic. + * `js.Dynamic` lets us call browser APIs by name without type facades (and without extra libraries). + */ +final class BrowserPlatform(canvas: js.Dynamic) extends Platform: + private val ctx = canvas.getContext("2d") + private val window = global.window + + // Keys that scroll the page should only steer the game. + private val prevent = Set("ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", " ") + + private def listen(kind: String, cb: String => Unit): Unit = + val handler: js.Function1[js.Dynamic, Unit] = e => + val key = e.key.asInstanceOf[String] + if prevent(key) then e.preventDefault() + cb(key) + window.addEventListener(kind, handler) + + def fillRect(x: Int, y: Int, w: Int, h: Int, color: String): Unit = + ctx.fillStyle = color + ctx.fillRect(x, y, w, h) + + def fillText(x: Int, y: Int, text: String, color: String, size: Int, align: String): Unit = + ctx.fillStyle = color + ctx.font = s"${size}px monospace" + ctx.textAlign = align + ctx.fillText(text, x, y) + + def randomSeed(): Long = + val buf = new Uint32Array(1) + global.crypto.getRandomValues(buf) + buf(0).toLong + + // t is the number of milliseconds since the first animation frame. + def startClock(onTime: Long => Unit): Unit = + def frame(start: Option[Double]): js.Function1[Double, Unit] = now => + val t0 = start.getOrElse(now) + onTime((now - t0).toLong) + window.requestAnimationFrame(frame(Some(t0))) + window.requestAnimationFrame(frame(None)) + + def onKeyDown(cb: String => Unit): Unit = listen("keydown", cb) + def onKeyUp(cb: String => Unit): Unit = listen("keyup", cb) diff --git a/scala/web/Main.scala b/scala/web/Main.scala new file mode 100644 index 0000000..1555b8b --- /dev/null +++ b/scala/web/Main.scala @@ -0,0 +1,16 @@ +package snake +package web + +import scala.scalajs.js.Dynamic.global + +// Two programs, one runtime: build-web.sh picks the entry point with --main-class. + +private def canvas() = global.document.getElementById("game") + +/** scala/index.html: Snake. */ +object SnakeMain: + def main(args: Array[String]): Unit = Runtime.runApp(App.main, BrowserPlatform(canvas())) + +/** scala/pong.html: Pong (m11). */ +object PongMain: + def main(args: Array[String]): Unit = Runtime.runApp(pong.PongApp.pongMain, BrowserPlatform(canvas()))