More work on flow rules. All rules must now have metadata
Better error capturing!
This commit is contained in:
parent
866c00bea0
commit
68298cf9c1
|
@ -22,7 +22,9 @@
|
||||||
further rules can be applied to that cell."
|
further rules can be applied to that cell."
|
||||||
:author "Simon Brooke"}
|
:author "Simon Brooke"}
|
||||||
mw-engine.core
|
mw-engine.core
|
||||||
(:require [mw-engine.utils :refer [get-int-or-zero map-world]]
|
(:require [clojure.string :refer [starts-with?]]
|
||||||
|
[mw-engine.flow :refer [flow-world]]
|
||||||
|
[mw-engine.utils :refer [get-int-or-zero map-world rule-type]]
|
||||||
[taoensso.timbre :as l]))
|
[taoensso.timbre :as l]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -55,23 +57,21 @@
|
||||||
an ifn, or a list (ifn source-text). This function deals with despatching
|
an ifn, or a list (ifn source-text). This function deals with despatching
|
||||||
on those two possibilities. `world` is also passed in in order to be able
|
on those two possibilities. `world` is also passed in in order to be able
|
||||||
to access neighbours."
|
to access neighbours."
|
||||||
([world cell rule]
|
[world cell rule]
|
||||||
(cond
|
(cond
|
||||||
(ifn? rule) (apply-rule world cell rule nil)
|
(ifn? rule) (apply rule (list cell world))
|
||||||
(seq? rule) (let [[afn src] rule] (apply-rule world cell afn src))))
|
(seq? rule) (let [[afn src lisp] rule
|
||||||
([world cell rule source]
|
result (apply-rule world cell afn)]
|
||||||
(let [result (apply rule (list cell world))]
|
(when result
|
||||||
(cond
|
(merge result {:rule src
|
||||||
(and result source) (merge result {:rule source})
|
:lisp lisp})))))
|
||||||
:else result))))
|
|
||||||
|
|
||||||
(defn- apply-rules
|
(defn- apply-rules
|
||||||
"Derive a cell from this `cell` of this `world` by applying these `rules`."
|
"Derive a cell from this `cell` of this `world` by applying these `rules`."
|
||||||
[world cell rules]
|
[world cell rules]
|
||||||
(cond (empty? rules) cell
|
(or
|
||||||
:else (let [result (apply-rule world cell (first rules))]
|
(first (remove nil? (map #(apply-rule world cell %) rules)))
|
||||||
(cond result result
|
cell))
|
||||||
:else (apply-rules world cell (rest rules))))))
|
|
||||||
|
|
||||||
(defn- transform-cell
|
(defn- transform-cell
|
||||||
"Derive a cell from this `cell` of this `world` by applying these `rules`. If an
|
"Derive a cell from this `cell` of this `world` by applying these `rules`. If an
|
||||||
|
@ -83,33 +83,45 @@
|
||||||
{:generation (+ (get-int-or-zero cell :generation) 1)})
|
{:generation (+ (get-int-or-zero cell :generation) 1)})
|
||||||
(catch Exception e
|
(catch Exception e
|
||||||
(merge cell {:error
|
(merge cell {:error
|
||||||
(format "%s at generation %d when in state %s"
|
(format "%s with message `%s` at generation %d when in state %s"
|
||||||
|
(-> e .getClass .getName)
|
||||||
(.getMessage e)
|
(.getMessage e)
|
||||||
(:generation cell)
|
(:generation cell)
|
||||||
(:state cell))
|
(:state cell))
|
||||||
:stacktrace (map #(.toString %) (.getStackTrace e))
|
:stacktrace ;; (remove #(starts-with? % "clojure.")
|
||||||
|
(map #(.toString %) (.getStackTrace e))
|
||||||
|
;;)
|
||||||
:state :error}))))
|
:state :error}))))
|
||||||
|
|
||||||
(defn transform-world
|
(defn transform-world
|
||||||
"Return a world derived from this `world` by applying these `rules` to each cell."
|
"Return a world derived from this `world` by applying the production rules
|
||||||
([world rules]
|
found among these `rules` to each cell."
|
||||||
(map-world world transform-cell (list rules))))
|
[world rules]
|
||||||
|
(map-world world transform-cell
|
||||||
|
;; Yes, that `list` is there for a reason!
|
||||||
|
(list
|
||||||
|
(filter
|
||||||
|
#(= :production (rule-type %))
|
||||||
|
rules))))
|
||||||
|
|
||||||
(defn run-world
|
(defn run-world
|
||||||
"Run this world with these rules for this number of generations.
|
"Run this world with these rules for this number of generations.
|
||||||
|
|
||||||
* `world` a world as discussed above;
|
* `world` a world as discussed above;
|
||||||
* `init-rules` a sequence of rules as defined above, to be run once to initialise the world;
|
* `init-rules` a sequence of rules as defined above, to be run once to initialise the world;
|
||||||
* `rules` a sequence of rules as defined above, to be run iteratively for each generation;
|
* `rules` a sequence of rules as defined above, to be run iteratively for each generation;
|
||||||
* `generations` an (integer) number of generations.
|
* `generations` an (integer) number of generations.
|
||||||
|
|
||||||
Return the final generation of the world."
|
|
||||||
[world init-rules rules generations]
|
|
||||||
(reduce (fn [world iteration]
|
|
||||||
(l/info "Running iteration " iteration)
|
|
||||||
(transform-world world rules))
|
|
||||||
(transform-world world init-rules)
|
|
||||||
(range generations)))
|
|
||||||
|
|
||||||
|
|
||||||
|
**NOTE THAT** all rules **must** be tagged with `rule-type` metadata, or thet **will not**
|
||||||
|
be executed.
|
||||||
|
|
||||||
|
Return the final generation of the world."
|
||||||
|
([world rules generations]
|
||||||
|
(run-world world rules rules (dec generations)))
|
||||||
|
([world init-rules rules generations]
|
||||||
|
(reduce (fn [world iteration]
|
||||||
|
(l/info "Running iteration " iteration)
|
||||||
|
(let [w' (transform-world world rules)]
|
||||||
|
(flow-world w' rules)))
|
||||||
|
(transform-world world init-rules)
|
||||||
|
(range generations))))
|
||||||
|
|
|
@ -22,7 +22,8 @@
|
||||||
there's a planning stage, in which all the flows to be executed are computed
|
there's a planning stage, in which all the flows to be executed are computed
|
||||||
without changing the world, and then an execution stage, where they're all
|
without changing the world, and then an execution stage, where they're all
|
||||||
executed. This namespace deals with mainly with execution."
|
executed. This namespace deals with mainly with execution."
|
||||||
(:require [mw-engine.utils :refer [get-cell get-num in-bounds? merge-cell]]
|
(:require [mw-engine.utils :refer [get-cell get-num in-bounds? merge-cell
|
||||||
|
rule-type]]
|
||||||
[taoensso.timbre :refer [info warn]]))
|
[taoensso.timbre :refer [info warn]]))
|
||||||
|
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
@ -104,7 +105,7 @@
|
||||||
d' (assoc dest p (+ (get-num dest p) q))]
|
d' (assoc dest p (+ (get-num dest p) q))]
|
||||||
(if (= q (:quantity flow))
|
(if (= q (:quantity flow))
|
||||||
(info (format "Moving %f units of %s from %d,%d to %d,%d"
|
(info (format "Moving %f units of %s from %d,%d to %d,%d"
|
||||||
(float q) (name p) sx sy dx dy))
|
(float q) (name p) sx sy dx dy))
|
||||||
(warn (format "Moving %s from %d,%d to %d,%d; %f units ordered but only %f available"
|
(warn (format "Moving %s from %d,%d to %d,%d; %f units ordered but only %f available"
|
||||||
(name p) sx sy dx dy (float (:quantity flow)) (float q))))
|
(name p) sx sy dx dy (float (:quantity flow)) (float q))))
|
||||||
(merge-cell (merge-cell world s') d'))
|
(merge-cell (merge-cell world s') d'))
|
||||||
|
@ -118,6 +119,28 @@
|
||||||
[world flows]
|
[world flows]
|
||||||
(reduce execute world (filter #(flow? % world) flows)))
|
(reduce execute world (filter #(flow? % world) flows)))
|
||||||
|
|
||||||
|
(defn plan-flows
|
||||||
|
"Plan, but do not execute, all the flows in this `world` implied by these
|
||||||
|
`flow-rules` (which are expected to be pre-compiled). Return the list of
|
||||||
|
plans, as flow objects."
|
||||||
|
[world flow-rules]
|
||||||
|
(assert (every? #(= :flow (rule-type %)) flow-rules))
|
||||||
|
(remove nil?
|
||||||
|
(flatten
|
||||||
|
(map ;; across all the cells
|
||||||
|
(fn [cell]
|
||||||
|
(map ;; across all the rules
|
||||||
|
(fn [rule] (apply rule (list cell world)))
|
||||||
|
flow-rules))
|
||||||
|
(flatten world)))))
|
||||||
|
|
||||||
|
(defn flow-world
|
||||||
|
"Return a world derived from this `world` by applying the flow rules
|
||||||
|
found among these `rules` to each cell, and executing all the flows
|
||||||
|
planned."
|
||||||
|
[world rules]
|
||||||
|
(execute-flows world (plan-flows world (filter #(= :flow (rule-type %)) rules))))
|
||||||
|
|
||||||
;; building blocks for compiled flow rules
|
;; building blocks for compiled flow rules
|
||||||
|
|
||||||
(defmacro create-location
|
(defmacro create-location
|
||||||
|
|
|
@ -32,6 +32,12 @@
|
||||||
;;;;
|
;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn abs
|
||||||
|
"Prior to Clojure 1.11, there is no native `abs` function. Afterwards, there
|
||||||
|
is."
|
||||||
|
[n]
|
||||||
|
(Math/abs n))
|
||||||
|
|
||||||
(defn tag-property
|
(defn tag-property
|
||||||
"Set the value of this `property` of this cell from the corresponding pixel of this `heightmap`.
|
"Set the value of this `property` of this cell from the corresponding pixel of this `heightmap`.
|
||||||
If the heightmap you supply is smaller than the world, this will break.
|
If the heightmap you supply is smaller than the world, this will break.
|
||||||
|
|
|
@ -299,3 +299,8 @@
|
||||||
(merge %2 cell)
|
(merge %2 cell)
|
||||||
%2))
|
%2))
|
||||||
world))
|
world))
|
||||||
|
|
||||||
|
(defn rule-type
|
||||||
|
"Return the rule-type of this compiled `rule`."
|
||||||
|
[rule]
|
||||||
|
(:rule-type (meta rule)))
|
||||||
|
|
|
@ -35,6 +35,24 @@
|
||||||
;;;;
|
;;;;
|
||||||
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
||||||
|
|
||||||
|
(defn cell?
|
||||||
|
"Return `true` if `obj` is a cell, as understood by MicroWorld, else `false`."
|
||||||
|
[obj]
|
||||||
|
(and (map? obj) ;; it's a map...
|
||||||
|
;; TODO: it's worth checking (and this does not) that cells have the
|
||||||
|
;; right co-ordinates!
|
||||||
|
(pos-int? (:x obj)) ;; with an x co-ordinate...
|
||||||
|
(pos-int? (:y obj)) ;; and a y co-ordinate...
|
||||||
|
(keyword? (:state obj)))) ;; and a state which is a keyword.
|
||||||
|
|
||||||
|
(defn world?
|
||||||
|
"Return `true` if `obj` is a world, as understood by MicroWorld, else `false`."
|
||||||
|
[obj]
|
||||||
|
(and (coll? obj) ;; it's a collection...
|
||||||
|
(every? coll? obj) ;; of collections...
|
||||||
|
(= 1 (count (set (map count obj)))) ;; all of which are the same length...
|
||||||
|
(every? cell? (flatten obj)))) ;; and every element of each of those is a cell.
|
||||||
|
|
||||||
(defmacro make-cell
|
(defmacro make-cell
|
||||||
"Create a minimal default cell at x, y
|
"Create a minimal default cell at x, y
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,43 @@
|
||||||
(ns mw-engine.core-test
|
(ns mw-engine.core-test
|
||||||
(:require [clojure.test :refer [deftest is testing]]
|
(:require [clojure.test :refer [deftest is testing]]
|
||||||
[mw-engine.core :refer [apply-rule]]))
|
[mw-engine.core :refer [apply-rule transform-world]]
|
||||||
|
[mw-engine.world :refer [make-world]]))
|
||||||
|
|
||||||
(deftest apply-rule-test
|
(deftest apply-rule-test
|
||||||
(testing "Application of a single rule"
|
(testing "Application of a single rule"
|
||||||
(let [afn (eval
|
(let [afn (vary-meta
|
||||||
(fn [cell _world]
|
(eval
|
||||||
(cond
|
(fn [cell _world]
|
||||||
(= (:state cell) :new)
|
(cond
|
||||||
(merge cell {:state :grassland}))))
|
(= (:state cell) :new)
|
||||||
pair (list afn "Test source")]
|
(merge cell {:state :grassland}))))
|
||||||
(is (nil? (apply-rule nil {:state :water} afn))
|
merge {:rule-type :production})
|
||||||
"Rule shouldn't fire when state is wrong")
|
pair (list afn "Test source")]
|
||||||
(is (nil? (apply-rule nil {:state :water} pair))
|
(is (nil? (apply-rule nil {:state :water} afn))
|
||||||
"Rule shouldn't fire when state is wrong")
|
"Rule shouldn't fire when state is wrong")
|
||||||
(is (= (:state (apply-rule nil {:state :new} afn)) :grassland)
|
(is (nil? (apply-rule nil {:state :water} pair))
|
||||||
"Rule should fire when state is correct")
|
"Rule shouldn't fire when state is wrong")
|
||||||
(is (= (:state (apply-rule nil {:state :new} pair)) :grassland)
|
(is (= (:state (apply-rule nil {:state :new} afn)) :grassland)
|
||||||
"Rule should fire when state is correct")
|
"Rule should fire when state is correct")
|
||||||
(is (nil? (:rule (apply-rule nil {:state :new} afn)))
|
(is (= (:state (apply-rule nil {:state :new} pair)) :grassland)
|
||||||
"No rule text if not provided")
|
"Rule should fire when state is correct")
|
||||||
(is (= (:rule (apply-rule nil {:state :new} pair)) "Test source")
|
(is (nil? (:rule (apply-rule nil {:state :new} afn)))
|
||||||
"Rule text cached on cell if provided"))))
|
"No rule text if not provided")
|
||||||
|
(is (= (:rule (apply-rule nil {:state :new} pair)) "Test source")
|
||||||
|
"Rule text cached on cell if provided"))))
|
||||||
|
|
||||||
|
(deftest transform-world-tests
|
||||||
|
(testing "Application of a single rule"
|
||||||
|
(let [afn (vary-meta
|
||||||
|
(eval
|
||||||
|
(fn [cell _world]
|
||||||
|
(cond
|
||||||
|
(= (:state cell) :new)
|
||||||
|
(merge cell {:state :grassland}))))
|
||||||
|
merge {:rule-type :production})
|
||||||
|
world (make-world 3 3)
|
||||||
|
expected [[{:y 0, :state :grassland, :x 0, :generation 1} {:y 0, :state :grassland, :x 1, :generation 1} {:y 0, :state :grassland, :x 2, :generation 1}]
|
||||||
|
[{:y 1, :state :grassland, :x 0, :generation 1} {:y 1, :state :grassland, :x 1, :generation 1} {:y 1, :state :grassland, :x 2, :generation 1}]
|
||||||
|
[{:y 2, :state :grassland, :x 0, :generation 1} {:y 2, :state :grassland, :x 1, :generation 1} {:y 2, :state :grassland, :x 2, :generation 1}]]
|
||||||
|
actual (transform-world world (list afn))]
|
||||||
|
(is (= actual expected)))))
|
Loading…
Reference in a new issue