| Paint glimmer with nothing under it f554a01 nandi 12d ago | 1 | (ns glimmer-gfx.tests |
| 2 | "Every check here runs headless: the backend measures, places, paints and |
| 3 | dispatches into a plain framebuffer, so none of it needs a window or a |
| 4 | display. `jolt test`." |
| 5 | (:require [glimmer.ratom :as ra] |
| 6 | [glimmer.core :as gui] |
| 7 | [glimmer.backend :as backend] |
| 8 | [glimmer-gfx.core :as gfx] |
| Play three games on the gfx backend e98a184 nandi 11d ago | 9 | [glimmer-gfx.tictactoe :as ttt] |
| 10 | [glimmer-gfx.maze :as maze] |
| 11 | [glimmer-gfx.asteroids :as ast] |
| Paint glimmer with nothing under it f554a01 nandi 12d ago | 12 | [glimmer-gfx.raster :as r] |
| 13 | [clojure.string :as str])) |
| 14 | |
| 15 | ;; --- the rasterizer ---------------------------------------------------------- |
| 16 | |
| 17 | (defn- check-raster! [] |
| 18 | (let [b (r/buf 200 120) |
| 19 | at (fn [x y] (aget ^ints (:px b) (+ (* y (:w b)) x)))] |
| 20 | (r/clear b 0) |
| 21 | (r/rect! b 10 10 5 5 0xff0000) |
| 22 | (assert (= 0xff0000 (at 12 12))) |
| 23 | (assert (= 0 (at 9 9)) "a rect must not bleed past its edge") |
| 24 | (r/rect! b -5 -5 3 3 0x00ff00) ; offscreen, must not throw |
| 25 | (r/line! b 0 0 199 119 0x0000ff) |
| 26 | (assert (= 0x0000ff (at 0 0))) |
| 27 | (r/clear b 0) |
| 28 | (r/text! b 0 0 "A" 0xffffff 1) |
| 29 | (assert (some #(= 0xffffff (at % 0)) (range 3)) "the glyph drew nothing"))) |
| 30 | |
| 31 | ;; --- a component, through the real reconciler -------------------------------- |
| 32 | |
| 33 | (def ^:private clicks (ra/atom 0)) |
| 34 | |
| 35 | (defn- counter [] |
| 36 | [:card {} |
| 37 | [:title {:label "Counter"}] |
| 38 | [:label {:label (str "Count: " (ra/deref clicks))}] |
| 39 | [:hbox {:spacing 8} |
| 40 | [:button {:label "-1" :on-click #(ra/swap! clicks dec)}] |
| 41 | [:button {:label "+1" :kind :primary :on-click #(ra/swap! clicks inc)}]]]) |
| 42 | |
| 43 | (defn- walk [n] (cons n (mapcat walk (:children @n)))) |
| 44 | (defn- tagged [root tag] (first (filter #(= tag (:tag @%)) (walk root)))) |
| 45 | (defn- buttons [root] (filter #(= :button (:tag @%)) (walk root))) |
| 46 | (defn- labelled? [root prefix] |
| 47 | (some #(and (= :label (:tag @%)) |
| 48 | (str/starts-with? (str (:label (:props @%))) prefix)) |
| 49 | (walk root))) |
| 50 | (defn- centre [n] |
| 51 | (let [[x y w h] (:rect @n)] [(+ x (quot w 2)) (+ y (quot h 2))])) |
| 52 | |
| 53 | (defn- check-tree! [root] |
| 54 | (assert (tagged root :card) "no card in the tree") |
| 55 | (assert (= 2 (count (buttons root))) "expected two buttons") |
| 56 | (assert (labelled? root "Count: 0") "the label did not render the ratom")) |
| 57 | |
| 58 | (defn- check-layout! [root] |
| 59 | (let [[cx cy] (:rect @(tagged root :card)) |
| 60 | [tx ty] (:rect @(tagged root :title)) |
| 61 | [_ by bw bh] (:rect @(first (buttons root)))] |
| 62 | (assert (= [0 0] [cx cy]) "the card should sit at the origin") |
| 63 | (assert (= [10 10] [tx ty]) "a card must inset its child by PAD") |
| 64 | (assert (and (pos? bw) (= 26 bh)) "button box is the wrong size") |
| 65 | (assert (> by ty) "the title must be placed above the buttons")) |
| 66 | ;; an hbox lays its children left to right, with the gap it was given |
| 67 | (let [[b1 b2] (map #(:rect @%) (buttons root))] |
| 68 | (assert (= (second b1) (second b2)) "hbox children must share a baseline") |
| 69 | (assert (= (+ (first b1) (nth b1 2) 8) (first b2)) "hbox gap not honoured"))) |
| 70 | |
| 71 | (defn- check-clicks! [root buf] |
| 72 | (let [plus (second (buttons root)) |
| 73 | p (centre plus) |
| 74 | state (atom {}) |
| 75 | before (ra/deref clicks)] |
| 76 | (gfx/render-once root buf 400 {:mouse p :down? true :released? false} state) |
| 77 | (assert (= before (ra/deref clicks)) "a press alone must not fire the handler") |
| 78 | (gfx/render-once root buf 400 {:mouse p :down? false :released? true} state) |
| 79 | (assert (= (inc before) (ra/deref clicks)) "the click did not fire") |
| 80 | (gfx/render-once root buf 400) |
| 81 | (assert (labelled? root (str "Count: " (inc before))) |
| 82 | "the reactive re-render never reached the tree") |
| 83 | |
| 84 | ;; press, then release somewhere else: not a click |
| 85 | (gfx/render-once root buf 400 {:mouse p :down? true :released? false} state) |
| 86 | (gfx/render-once root buf 400 {:mouse [399 239] :down? false :released? true} state) |
| 87 | (assert (= (inc before) (ra/deref clicks)) "releasing off the button must not fire"))) |
| 88 | |
| 89 | (defn- check-painted! [root buf] |
| 90 | (gfx/render-once root buf 400) |
| 91 | (let [at (fn [x y] (aget ^ints (:px buf) (+ (* y (:w buf)) x))) |
| 92 | [x y] (:rect @(tagged root :card)) |
| 93 | row (+ 4 (second (:rect @(second (buttons root)))))] |
| 94 | (assert (= (:card gfx/theme) (at (+ x 2) (+ y 2))) "card background not painted") |
| 95 | (assert (some #(= (:accent gfx/theme) (at % row)) (range 400)) |
| 96 | "the primary button is not painted in the accent colour"))) |
| 97 | |
| 98 | (defn- check-scheduled! [root buf] |
| 99 | ;; With a loop running, glimmer refuses to re-render inline and posts the work |
| 100 | ;; through :schedule instead. That is the path a real window takes, and a |
| 101 | ;; different one from every check above. |
| 102 | (reset! backend/loop-running? true) |
| 103 | (try |
| 104 | (let [before (ra/deref clicks) |
| 105 | p (centre (second (buttons root))) |
| 106 | state (atom {})] |
| 107 | (gfx/render-once root buf 400 {:mouse p :down? true :released? false} state) |
| 108 | (gfx/render-once root buf 400 {:mouse p :down? false :released? true} state) |
| 109 | (assert (= (inc before) (ra/deref clicks)) "the handler did not fire under a loop") |
| 110 | (gfx/render-once root buf 400) ; the next frame drains the queue |
| 111 | (assert (labelled? root (str "Count: " (inc before))) |
| 112 | "the scheduled re-render never reached the tree")) |
| 113 | (finally (reset! backend/loop-running? false)))) |
| 114 | |
| Play three games on the gfx backend e98a184 nandi 11d ago | 115 | ;; --- asteroids ---------------------------------------------------------------- |
| 116 | |
| 117 | (defn- check-asteroids! [] |
| 118 | (assert (= [5.0 5.0] (ast/wrap [645.0 485.0])) "the world must wrap") |
| 119 | (assert (ast/hit? [10 10] [12 12] 5)) |
| 120 | (assert (not (ast/hit? [10 10] [200 200] 5))) |
| 121 | (assert (ast/hit? [5 5] [635 475] 20) "a hit across the seam still counts") |
| 122 | (let [g (ast/new-game) |
| 123 | ;; thrust moves the ship; nothing else does |
| 124 | drifting (nth (iterate #(ast/step % #{0x77}) g) 5)] |
| 125 | (assert (not= (get-in g [:ship :pos]) (get-in drifting [:ship :pos])) |
| 126 | "thrust did not move the ship") |
| 127 | (assert (:thrusting (:ship drifting))) |
| 128 | ;; turning is a rotation, not a translation |
| 129 | (let [turned (ast/step g #{0xff53})] |
| 130 | (assert (pos? (get-in turned [:ship :angle])) "right did not turn")) |
| 131 | ;; holding space auto-fires, but paced -- not one bullet per frame |
| 132 | (let [a (ast/step g #{0x20}) |
| 133 | b (ast/step a #{0x20}) |
| 134 | held (nth (iterate #(ast/step % #{0x20}) g) 20)] |
| 135 | (assert (= 1 (count (:bullets a))) "space did not fire") |
| 136 | (assert (= 1 (count (:bullets b))) "the cooldown did not hold the trigger") |
| 137 | (assert (< 1 (count (:bullets held)) 20) "auto-fire is off, or unpaced")) |
| 138 | ;; a bullet placed on a rock splits it and scores |
| 139 | (let [rk (assoc (first (:rocks g)) :pos [100 100] :vel [0 0] :size 3) |
| 140 | st (ast/step (assoc g :rocks [rk] |
| 141 | :bullets [{:pos [100 100] :vel [0 0] :life 30}]) |
| 142 | #{})] |
| 143 | (assert (= 2 (count (:rocks st))) "the rock did not split in two") |
| 144 | (assert (every? #(= 2 (:size %)) (:rocks st)) "the pieces are the wrong size") |
| 145 | (assert (= 10 (:score st)) "no score for the hit") |
| 146 | (assert (empty? (:bullets st)) "the bullet survived its own hit")) |
| 147 | ;; the smallest rock leaves nothing behind, and an empty field respawns |
| 148 | (let [rk (assoc (first (:rocks g)) :pos [100 100] :vel [0 0] :size 1) |
| 149 | st (ast/step (assoc g :rocks [rk] |
| 150 | :bullets [{:pos [100 100] :vel [0 0] :life 30}]) |
| 151 | #{})] |
| 152 | (assert (seq (:rocks st)) "an empty field should respawn") |
| 153 | (assert (every? #(= 3 (:size %)) (:rocks st)) "respawned rocks are not full size")) |
| 154 | ;; a rock on top of the ship ends it |
| 155 | (let [rk (assoc (first (:rocks g)) :pos (get-in g [:ship :pos]) :vel [0 0])] |
| 156 | (assert (:lost? (ast/step (assoc g :rocks [rk]) #{})))) |
| 157 | ;; and it all paints: poly! into a real framebuffer, nothing thrown |
| 158 | (let [buf (r/buf 640 480)] |
| 159 | (@#'ast/draw! buf (nth (iterate #(ast/step % #{0x77 0x20}) g) 30)) |
| 160 | (assert (some #(not= (:bg @#'ast/colours) %) (seq ^ints (:px buf))) |
| 161 | "the frame painted nothing")) |
| 162 | ;; bullets expire |
| 163 | (let [st (ast/step (assoc g :bullets [{:pos [10 10] :vel [0 0] :life 1}]) #{})] |
| 164 | (assert (empty? (:bullets st)) "the bullet outlived its life")))) |
| 165 | |
| 166 | ;; --- the maze ----------------------------------------------------------------- |
| 167 | |
| 168 | (defn- check-maze! [] |
| 169 | (let [level ["####" "#..#" "#..#" "####"] |
| 170 | C 24] |
| 171 | (assert (maze/wall? level 0 0)) |
| 172 | (assert (not (maze/wall? level 1 1))) |
| 173 | (assert (maze/wall? level 9 9) "off the map must read as wall") |
| 174 | (assert (maze/free? level C C) "the open cell should be free") |
| 175 | (assert (not (maze/free? level (+ (* 2 C) 1) C)) |
| 176 | "a box straddling the far wall is not free") |
| 177 | ;; walking into a wall costs the blocked axis only: this slides down it |
| 178 | (let [p [C C]] |
| 179 | (assert (= p (maze/move level p [-1 0])) "moved through a wall") |
| 180 | (assert (= p (maze/move level p [0 0]))) |
| 181 | (let [[x y] (maze/move level [(* 2 C) C] [1 1])] |
| 182 | (assert (= x (* 2 C)) "should have been stopped by the wall on the right") |
| 183 | (assert (> y C) "but should still have slid downward"))) |
| 184 | ;; the dot under the player's centre is the one that goes |
| 185 | (assert (= #{[2 1]} (maze/eat #{[1 1] [2 1]} [C C]))))) |
| 186 | |
| 187 | ;; --- the game ---------------------------------------------------------------- |
| 188 | |
| 189 | (defn- check-tictactoe! [] |
| 190 | (let [empty (vec (repeat 9 nil))] |
| 191 | (assert (nil? (ttt/winner empty))) |
| 192 | (assert (= :x (ttt/winner [:x :x :x nil nil nil nil nil nil]))) |
| 193 | (assert (= :o (ttt/winner [:x :x nil nil nil nil :o :o :o]))) |
| 194 | (assert (nil? (ttt/winner [:x :x :o nil nil nil nil nil nil]))) |
| 195 | ;; X plays 4, O answers in the first free square |
| 196 | (let [b (ttt/play empty 4)] |
| 197 | (assert (= :x (b 4)) "X did not take the square it clicked") |
| 198 | (assert (= :o (b 0)) "O did not reply")) |
| 199 | ;; an occupied square, and a finished game, are both no-ops |
| 200 | (assert (= (ttt/play empty 4) (ttt/play (ttt/play empty 4) 4))) |
| 201 | (let [won [:x :x :x :o :o nil nil nil nil]] |
| 202 | (assert (= won (ttt/play won 5)) "the board moved after the game was over")) |
| 203 | ;; a winning move ends it: no reply gets appended |
| 204 | (let [b (ttt/play [:x :x nil :o :o nil nil nil nil] 2)] |
| 205 | (assert (= :x (ttt/winner b))) |
| 206 | (assert (nil? (b 5)) "O replied to a move that had already won")))) |
| 207 | |
| Paint glimmer with nothing under it f554a01 nandi 12d ago | 208 | (defn -main [& _] |
| 209 | (check-raster!) |
| Play three games on the gfx backend e98a184 nandi 11d ago | 210 | (check-tictactoe!) |
| 211 | (check-maze!) |
| 212 | (check-asteroids!) |
| Paint glimmer with nothing under it f554a01 nandi 12d ago | 213 | (let [root (gfx/root-node) |
| 214 | buf (r/buf 400 240)] |
| 215 | (gui/mount root :vbox [counter]) |
| 216 | (gfx/render-once root buf 400) |
| 217 | (check-tree! root) |
| 218 | (check-layout! root) |
| 219 | (check-clicks! root buf) |
| 220 | (check-painted! root buf) |
| 221 | (check-scheduled! root buf)) |
| 222 | (println "ok")) |