From a7a4cb5b2adfb02d21004b8ef5b35b739255fb3d Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 29 Oct 2026 01:04:20 +0100 Subject: [PATCH] feat(fsharp): Fable browser platform, snake and pong pages, build-web.sh Co-Authored-By: Claude Opus 5.5 --- fsharp/.config/dotnet-tools.json | 13 +++++++ fsharp/build-web.sh | 11 ++++++ fsharp/index.html | 22 +++++++++++ fsharp/pong.html | 22 +++++++++++ fsharp/web/BrowserPlatform.fs | 64 ++++++++++++++++++++++++++++++++ fsharp/web/PongMain.fs | 6 +++ fsharp/web/SnakeMain.fs | 6 +++ fsharp/web/Web.fsproj | 33 ++++++++++++++++ 8 files changed, 177 insertions(+) create mode 100644 fsharp/.config/dotnet-tools.json create mode 100755 fsharp/build-web.sh create mode 100644 fsharp/index.html create mode 100644 fsharp/pong.html create mode 100644 fsharp/web/BrowserPlatform.fs create mode 100644 fsharp/web/PongMain.fs create mode 100644 fsharp/web/SnakeMain.fs create mode 100644 fsharp/web/Web.fsproj diff --git a/fsharp/.config/dotnet-tools.json b/fsharp/.config/dotnet-tools.json new file mode 100644 index 0000000..a7460ac --- /dev/null +++ b/fsharp/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "fable": { + "version": "5.24.0", + "commands": [ + "fable" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/fsharp/build-web.sh b/fsharp/build-web.sh new file mode 100755 index 0000000..9088e1f --- /dev/null +++ b/fsharp/build-web.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash +# Usage: ./build-web.sh +# Compiles src/ + web/ to JavaScript with Fable (a local dotnet tool, see .config/dotnet-tools.json) into web/dist/. +# index.html loads the Snake entry module, pong.html the Pong one. +# Then serve the repo root (node scripts/serve.mjs) and open http://localhost:8000/fsharp/index.html. +set -euo pipefail +cd "$(dirname "$0")" +export PATH="$HOME/.dotnet:$PATH" +export DOTNET_NOLOGO=1 DOTNET_CLI_TELEMETRY_OPTOUT=1 +dotnet tool restore +dotnet fable web -o web/dist --noCache diff --git a/fsharp/index.html b/fsharp/index.html new file mode 100644 index 0000000..34aaefe --- /dev/null +++ b/fsharp/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/fsharp/pong.html b/fsharp/pong.html new file mode 100644 index 0000000..3279f94 --- /dev/null +++ b/fsharp/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/fsharp/web/BrowserPlatform.fs b/fsharp/web/BrowserPlatform.fs new file mode 100644 index 0000000..92554b4 --- /dev/null +++ b/fsharp/web/BrowserPlatform.fs @@ -0,0 +1,64 @@ +/// Browser glue: turns a canvas into a Platform. Impure by nature; not part of the exercises' logic. +/// `?` (Fable.Core.JsInterop dynamic access) calls browser APIs by name, without type bindings or extra packages. +module Snake.Web.BrowserPlatform + +open Fable.Core +open Fable.Core.JsInterop +open Snake.Runtime + +[] +let private window: obj = jsNative + +[] +let private crypto: obj = jsNative + +// Keys that scroll the page should only steer the game. +let private prevent = [ "ArrowUp"; "ArrowDown"; "ArrowLeft"; "ArrowRight"; " " ] + +let private listen (kind: string) (cb: string -> unit) : unit = + window?addEventListener (kind, + fun (e: obj) -> + let key: string = e?key + + if List.contains key prevent then + e?preventDefault () + + cb key) + +// t is the number of milliseconds since the first animation frame. +let private startClock (onTime: int64 -> unit) : unit = + let rec frame (start: float option) : float -> unit = + fun now -> + let t0 = defaultArg start now + onTime (int64 (now - t0)) + window?requestAnimationFrame (frame (Some t0)) + + window?requestAnimationFrame (frame None) + +// Fable compiles a uint32 array to a Uint32Array, which is what crypto.getRandomValues fills. +let private randomSeed () : uint32 = + let buf: uint32[] = Array.zeroCreate 1 + crypto?getRandomValues (buf) + buf[0] + +/// A Platform drawing on `canvas` (an HTML canvas element) and listening to the window's keys and frames. +let create (canvas: obj) : Platform = + let ctx = canvas?getContext ("2d") + + { FillRect = + fun x y w h color -> + ctx?fillStyle <- color + ctx?fillRect (x, y, w, h) + FillText = + fun x y text color size align -> + ctx?fillStyle <- color + ctx?font <- $"{size}px monospace" + ctx?textAlign <- align + ctx?fillText (text, x, y) + RandomSeed = randomSeed + StartClock = startClock + OnKeyDown = listen "keydown" + OnKeyUp = listen "keyup" } + +/// The page's ``. +let gameCanvas () : obj = window?document?getElementById ("game") diff --git a/fsharp/web/PongMain.fs b/fsharp/web/PongMain.fs new file mode 100644 index 0000000..6a51ba7 --- /dev/null +++ b/fsharp/web/PongMain.fs @@ -0,0 +1,6 @@ +/// fsharp/pong.html loads this module (web/dist/PongMain.js): one call runs Pong (m11). +module Snake.Web.PongMain + +open Snake + +Runtime.runApp (BrowserPlatform.create (BrowserPlatform.gameCanvas ())) (Pong.App.pongMain ()) diff --git a/fsharp/web/SnakeMain.fs b/fsharp/web/SnakeMain.fs new file mode 100644 index 0000000..23f06aa --- /dev/null +++ b/fsharp/web/SnakeMain.fs @@ -0,0 +1,6 @@ +/// fsharp/index.html loads this module (web/dist/SnakeMain.js): one call runs the whole Snake program. +module Snake.Web.SnakeMain + +open Snake + +Runtime.runApp (BrowserPlatform.create (BrowserPlatform.gameCanvas ())) (App.main ()) diff --git a/fsharp/web/Web.fsproj b/fsharp/web/Web.fsproj new file mode 100644 index 0000000..631d6eb --- /dev/null +++ b/fsharp/web/Web.fsproj @@ -0,0 +1,33 @@ + + + + net10.0 + + $(NoWarn);FS0046 + + + + + + + + + + + + + + + + + + + + + + + + + +