sand.flan now loads a 16x8 sheet of two 8x8 brush frames and draws it four ways: the frame under the cursor through draw-texture-rec, and three badges in the corner through draw-texture, draw-texture-v and draw-texture-ex. That is not decoration — it is one call site per binding that the acceptance table cannot reach, and without it draw-texture-v and draw-texture-ex would be code nobody had ever executed. A failed load says so by name. LoadTexture on a missing file returns an id of 0, and every draw with that texture silently does nothing, so the program would look like it had a drawing bug rather than a missing file. texture-valid? is asked once at load and the answer is both printed and remembered, so the sand still runs with the cursor off. brush.png is generated rather than drawn — two circles, one ring and one filled, 102 bytes — so the repository gains an asset nobody has to keep. What this was checked by: xvfb-run, a screenshot of the running window, and the badges counted in it. Also with brush.png moved away, which is how the refusal path above is known to fire rather than merely to compile.
125 lines
6.0 KiB
Plaintext
125 lines
6.0 KiB
Plaintext
;;;; 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)))
|