;;;; Falling sand — Flan port of the Odin/Janet/Lisp/jank versions in ~/Development/fnm. ;;;; ;;;; THE SECOND ACCEPTANCE PROGRAM — build sequence milestone 4. calc-me.flan ;;;; comes first: sand cannot run at all until raylib FFI, keyword->enum ;;;; coercion and a window exist, and none of those should be on the critical ;;;; path to "the language runs something". ;;;; ;;;; It is tested twice: headless (N frames, hash the grid — the version CI runs ;;;; on native and wasm32) and interactive at 120 fps. This file is the ;;;; interactive half; the simulation itself lives in sand-sim/ so the headless ;;;; half can have it without linking raylib. See sand-sim/sim.flan for why that ;;;; split exists, and test/programs/sand-headless.flan for the other driver. ;;;; ;;;; Note what it still deliberately does not use: no Vec, no Map, no generics, ;;;; no user-written macros, no conditions, no allocator other than the stack ;;;; and static storage. ;;;; ;;;; Notation reminders (see plan.org and spec-memory.md): ;;;; [n T] fixed array, length n, element T — a VALUE, copies ;;;; [T] slice, ptr+len, non-owning (Vec T) owning, move-only ;;;; (Ptr T) pointer (Handle T) generational handle ;;;; types are inline name/type pairs, as in `let` and `defstruct` ;;;; an omitted return type means Unit ;;;; lowercase in a TYPE position is a type variable; in a LENGTH position ;;;; it is an ordinary compile-time value, so [rows [cols u32]] is unambiguous (import rl "vendor:raylib") ; directory = package; declaration optional (import sim "sand-sim") ; no collection prefix: relative to this file (import agent "vendor:agent") ; the dev agent: redefinitions, installed below ;; The brush sprite: a 16x8 sheet of two 8x8 frames, the ring drawn while the ;; mouse is idle and the blob while it is painting. It is here because the ;; texture calls cannot be in the acceptance table at all — loading one needs a ;; GL context — so the only way they are exercised is by running this. (defvar brush rl/Texture2D) (defvar brush-ok bool) ;; A missing file is not a crash and not silence: LoadTexture hands back a ;; texture with an id of 0, every draw with it is a no-op, and the program ;; looks like it has a drawing bug. So it is asked and said once, here. (defn load-brush [] (set brush (rl/load-texture "brush.png")) (set brush-ok (rl/texture-valid? brush)) (unless brush-ok (print-line "sand: cannot load brush.png — drawing the cursor is off"))) ;; Four draws, one per shape the call comes in, because none of them can be in ;; the acceptance table. The cursor picks one frame out of the sheet and so ;; needs a source rectangle; the three badges in the corner are the whole ;; sheet, at integer coordinates, at a Vector2 tinted with the current sand ;; colour, and scaled up — draw-texture, draw-texture-v and draw-texture-ex. (defn draw-brush [] (when brush-ok (let [m (rl/get-mouse-position) frame (f32 (if (rl/mouse-button-down? :left) 8.0 0.0))] (rl/draw-texture-rec brush (rl/Rectangle {:x frame :y 0.0 :width 8.0 :height 8.0}) (rl/Vector2 {:x (- (.x m) 4.0) :y (- (.y m) 4.0)}) rl/white) (rl/draw-texture brush 20 50 rl/white) (rl/draw-texture-v brush (rl/Vector2 {:x 44.0 :y 50.0}) (rl/get-color (nth sim/colors sim/current-color))) (rl/draw-texture-ex brush (rl/Vector2 {:x 72.0 :y 46.0}) 0.0 2.0 rl/white)))) ;; Locals are assignable places (spec-memory.md); parameters are not. (defn paint [] (let [m (rl/get-mouse-position) row (/ (i32 (.y m)) sim/cell-size) col (/ (i32 (.x m)) sim/cell-size)] (sim/paint-at row col))) ;; Every cross-function call in a dev build routes through an indirection cell, ;; so redefining this from the REPL reaches the running loop on the next frame. ;; No `varfn` (Janet), no `let update = ref` (OCaml), no var-routing (jank). ;; Release builds compile the same source to direct calls. ;; ;; A cell holds an (Fn ...) — a plain function pointer, no captured environment; ;; this one is (Fn [] Unit), `settle`'s is (Fn [i32 i32] Unit). Redefining ;; `settle` while `game-update` is mid-frame is safe ;; because old code is never unloaded; changing its SIGNATURE is not, and the ;; reload rejects it. See plan.org "What redefinition cannot do". (defn game-update [] (when (rl/key-pressed? :r) (sim/clear-grid)) (when (rl/mouse-button-down? :left) (paint)) (when (rl/mouse-button-released? :left) (sim/next-color)) (sim/step)) (defn game-draw [] (rl/clear-background rl/black) (dotimes [row sim/rows] (dotimes [col sim/cols] (let [c (at sim/grid row col)] (unless (= 0 c) (rl/draw-rectangle (i32 (* col sim/cell-size)) (i32 (* row sim/cell-size)) sim/cell-size sim/cell-size (rl/get-color c)))))) (draw-brush) (rl/draw-fps 20 20)) (defn main [] (rl/set-trace-log-level :warning) (rl/init-window sim/screen-width sim/screen-height "SAND") (defer (rl/close-window)) (rl/set-target-fps 120) ;; After the window, never before: LoadTexture uploads to the GPU and there ;; is no GPU to upload to until InitWindow has made a context. (load-brush) (defer (rl/unload-texture brush)) ;; The dev agent listens on a socket for redefinitions and hands them over; ;; (agent/poll) below is where they are installed. Building without --dev is ;; fine — nothing has cells to install into, so a module is refused on the ;; listener thread and the loop never notices. (agent/start "/tmp/flan-sand.sock") ;; Bare (defn main []) — argv and the i32 status are both optional. ;; Nothing in this loop allocates, so context/temp is never even touched. (until (rl/window-should-close?) ;; The frame boundary, and the only place a redefinition becomes visible: ;; nothing that could be redefined is on the stack here. (agent/poll) (game-update) (rl/begin-drawing) (game-draw) (rl/end-drawing)))