feat(scala): Scala.js browser platform, snake and pong pages, build-web.sh
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f4b671678e
commit
183821737e
Executable
+13
@@ -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() { # <main class> <output file>
|
||||
"$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
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pure Snake</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; gap: 12px; background: #1b1b1b; color: #ddd; font: 14px system-ui, sans-serif; }
|
||||
h1 { margin: 0; font-size: 20px; }
|
||||
canvas { background: #111111; border: 2px solid #333; }
|
||||
p { margin: 0; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Pure Snake</h1>
|
||||
<canvas id="game" width="400" height="400"></canvas>
|
||||
<p>Arrows / WASD to move. Space to pause, R to restart (after module 10).</p>
|
||||
<script type="module" src="web/dist/snake.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Pure Pong</title>
|
||||
<link rel="icon" href="data:,">
|
||||
<style>
|
||||
body { margin: 0; min-height: 100vh; display: flex; flex-direction: column; align-items: center;
|
||||
justify-content: center; gap: 12px; background: #1b1b1b; color: #ddd; font: 14px system-ui, sans-serif; }
|
||||
h1 { margin: 0; font-size: 20px; }
|
||||
canvas { background: #111111; border: 2px solid #333; }
|
||||
p { margin: 0; color: #999; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Pure Pong</h1>
|
||||
<canvas id="game" width="640" height="400"></canvas>
|
||||
<p>Up/Down arrows (or W/S) move your paddle. First to 5 wins. R restarts.</p>
|
||||
<script type="module" src="web/dist/pong.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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)
|
||||
@@ -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()))
|
||||
Reference in New Issue
Block a user