diff --git a/project.clj b/project.clj index 21b692e..e21d537 100644 --- a/project.clj +++ b/project.clj @@ -1,4 +1,4 @@ -(defproject mw-engine "0.1.6-SNAPSHOT" +(defproject mw-engine "0.2.0-SNAPSHOT" :dependencies [[org.clojure/clojure "1.11.1"] [org.clojure/clojurescript "1.11.60" :scope "provided"] [org.clojure/math.combinatorics "0.2.0"] diff --git a/src/cljc/mw_engine/core.clj b/src/cljc/mw_engine/core.clj index 1483c57..e598f60 100644 --- a/src/cljc/mw_engine/core.clj +++ b/src/cljc/mw_engine/core.clj @@ -1,6 +1,27 @@ -(ns ^{:doc "Functions to transform a world and run rules." +(ns ^{:doc "Functions to transform a world and run rules. + + Every rule is a function of two arguments, a cell and a world. If the rule + fires, it returns a new cell, which should have the same values for `:x` and + `:y` as the old cell. Anything else can be modified. + + While any function of two arguments can be used as a rule, a special high + level rule language is provided by the `mw-parser` package, which compiles + rules expressed in a subset of English rules into suitable functions. + + A cell is a map containing at least values for the keys :x, :y, and :state; + a transformation should not alter the values of :x or :y, and should not + return a cell without a keyword as the value of :state. Anything else is + legal. + + A world is a two dimensional matrix (sequence of sequences) of cells, such + that every cell's `:x` and `:y` properties reflect its place in the matrix. + See `world.clj`. + + Each time the world is transformed (see `transform-world`, for each cell, + rules are applied in turn until one matches. Once one rule has matched no + further rules can be applied to that cell." :author "Simon Brooke"} - mw-engine.core + mw-engine.core (:require [mw-engine.utils :refer [get-int-or-zero map-world]] [taoensso.timbre :as l])) @@ -26,29 +47,6 @@ ;;;; Copyright (C) 2014 Simon Brooke ;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;; -;;;; Every rule is a function of two arguments, a cell and a world. If the rule -;;;; fires, it returns a new cell, which should have the same values for :x and -;;;; :y as the old cell. Anything else can be modified. -;;;; -;;;; While any function of two arguments can be used as a rule, a special high -;;;; level rule language is provided by the `mw-parser` package, which compiles -;;;; rules expressed in a subset of English rules into suitable functions. -;;;; -;;;; A cell is a map containing at least values for the keys :x, :y, and :state; -;;;; a transformation should not alter the values of :x or :y, and should not -;;;; return a cell without a keyword as the value of :state. Anything else is -;;;; legal. -;;;; -;;;; A world is a two dimensional matrix (sequence of sequences) of cells, such -;;;; that every cell's :x and :y properties reflect its place in the matrix. -;;;; See `world.clj`. -;;;; -;;;; Each time the world is transformed (see `transform-world`, for each cell, -;;;; rules are applied in turn until one matches. Once one rule has matched no -;;;; further rules can be applied. -;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn apply-rule "Apply a single `rule` to a `cell`. What this is about is that I want to be able, @@ -62,19 +60,18 @@ (ifn? rule) (apply-rule world cell rule nil) (seq? rule) (let [[afn src] rule] (apply-rule world cell afn src)))) ([world cell rule source] - (let [result (apply rule (list cell world))] - (cond - (and result source) (merge result {:rule source}) - :else result)))) + (let [result (apply rule (list cell world))] + (cond + (and result source) (merge result {:rule source}) + :else result)))) (defn- apply-rules "Derive a cell from this `cell` of this `world` by applying these `rules`." [world cell rules] (cond (empty? rules) cell - :else (let [result (apply-rule world cell (first rules))] - (cond result result - :else (apply-rules world cell (rest rules)))))) - + :else (let [result (apply-rule world cell (first rules))] + (cond result result + :else (apply-rules world cell (rest rules)))))) (defn- transform-cell "Derive a cell from this `cell` of this `world` by applying these `rules`. If an @@ -82,8 +79,8 @@ [world cell rules] (try (merge - (apply-rules world cell rules) - {:generation (+ (get-int-or-zero cell :generation) 1)}) + (apply-rules world cell rules) + {:generation (+ (get-int-or-zero cell :generation) 1)}) (catch Exception e (merge cell {:error (format "%s at generation %d when in state %s" @@ -96,7 +93,7 @@ (defn transform-world "Return a world derived from this `world` by applying these `rules` to each cell." ([world rules] - (map-world world transform-cell (list rules)))) + (map-world world transform-cell (list rules)))) (defn run-world "Run this world with these rules for this number of generations. @@ -111,8 +108,8 @@ (reduce (fn [world iteration] (l/info "Running iteration " iteration) (transform-world world rules)) - (transform-world world init-rules) - (range generations))) + (transform-world world init-rules) + (range generations))) diff --git a/src/cljc/mw_engine/display.clj b/src/cljc/mw_engine/display.clj index d0e48a3..4a52445 100644 --- a/src/cljc/mw_engine/display.clj +++ b/src/cljc/mw_engine/display.clj @@ -35,18 +35,15 @@ [state] (subs (str state) 1)) - (defn format-image-path "Render this `state`, assumed to be a keyword indicating a state in the world, into a path which should recover the corresponding image file." [state] (format "%s/%s.png" *image-base* (format-css-class state))) - (defn format-mouseover [cell] (str cell)) - (defn render-cell "Render this world cell as a Hiccup table cell." [cell] @@ -55,13 +52,11 @@ [:a {:href (format "inspect?x=%d&y=%d" (:x cell) (:y cell))} [:img {:alt (:state cell) :width 32 :height 32 :src (format-image-path state)}]]])) - (defn render-world-row "Render this world `row` as a Hiccup table row." [row] (apply vector (cons :tr (map render-cell row)))) - (defn render-world-table "Render this `world` as a Hiccup table." [world] diff --git a/src/cljc/mw_engine/flow.clj b/src/cljc/mw_engine/flow.clj index 778d8a7..78bff33 100644 --- a/src/cljc/mw_engine/flow.clj +++ b/src/cljc/mw_engine/flow.clj @@ -1,6 +1,26 @@ (ns mw-engine.flow - "Allow flows of values between cells in the world." - (:require [mw-engine.utils :refer [get-cell get-num merge-cell]] + "Allow flows of values between cells in the world. + + The design here is: a flow object is a map with the following properties: + 1. :source, whose value is a location; + 2. :destination, whose value is a location; + 3. :property, whose value is a keyword; + 4. :quantity, whose value is a positive real number. + + A location object is a map with the following properties: + 1. :x, whose value is a natural number not greater than the extent of the world; + 2. :y, whose value is a natural number not greater than the extent of the world. + + To execute a flow is transfer the quantity specified of the property specified + from the cell at the source specified to the cell at the destination specified; + if the source doesn't have sufficient of the property, then all it has should + be transferred, but no more: properties to be flowed cannot be pulled negative. + + Flowing values through the world is consequently a two stage process: firstly + 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 + executed. This namespace deals with mainly with execution." + (:require [mw-engine.utils :refer [get-cell get-num in-bounds? merge-cell]] [taoensso.timbre :refer [info warn]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -25,32 +45,6 @@ ;;;; Copyright (C) 2014 Simon Brooke ;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;; -;;;; Functions to create and to print two dimensional cellular automata. -;;;; Nothing in this namespace should determine what states are possible within -;;;; the automaton, except for the initial state, :new. -;;;; -;;;; A cell is a map containing at least values for the keys :x, :y, and :state. -;;;; -;;;; A world is a two dimensional matrix (sequence of sequences) of cells, such -;;;; that every cell's :x and :y properties reflect its place in the matrix. -;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -;; OK, the design here is: a flow object is a map with the following properties: -;; 1. :source, whose value is a location; -;; 2. :destination, whose value is a location; -;; 3. :property, whose value is a keyword; -;; 4. :quantity, whose value is a positive real number. -;; -;; A location object is a map with the following properties: -;; 1. :x, whose value is a natural number not greater than the extent of the world; -;; 2. :y, whose value is a natural number not greater than the extent of the world. -;; -;; to execute a flow is transfer the quantity specified of the property specified -;; from the cell at the source specified to the cell at the destination specified; -;; if the source doesn't have sufficient of the property, then all it has should -;; be transferred, but no more. (defn coordinate? "Return `true` if this object `o` is a valid coordinate with respect to @@ -65,12 +59,13 @@ (defn location? "Return `true` if this object `o` is a location as defined above with respect to - this `world`, else `false`. Assumes square worlds." + this `world`, else `false`." [o world] (try (and (map? o) - (coordinate? (:x o) world) - (coordinate? (:y o) world)) + (integer? (:x o)) + (integer? (:y o)) + (in-bounds? world (:x o) (:y o))) (catch Exception e (warn (format "Not a valid location: %s; %s" o (.getMessage e))) false))) @@ -98,16 +93,16 @@ (let [sx (-> flow :source :x) sy (-> flow :source :y) source (get-cell world sx sy) - dx (-> flow :destination :x) + dx (-> flow :destination :x) dy (-> flow :destination :y) dest (get-cell world dx dy) - p (:property flow) - q (min (:quantity flow) (get-num source p)) - s' (assoc source p (- (source p) q)) - d' (assoc dest p (+ (get-num dest p) q))] + p (:property flow) + q (min (:quantity flow) (get-num source p)) + s' (assoc source p (- (source p) q)) + d' (assoc dest p (+ (get-num dest p) q))] (info (format "Moving %f units of %s from %d,%d to %d,%d" (float q) (name p) sx sy dx dy)) - (merge-cell (merge-cell world s') d')) + (merge-cell (merge-cell world s') d')) (catch Exception e (warn (format "Failed to execute flow %s: %s" flow (.getMessage e))) ;; return the world unmodified. diff --git a/src/cljc/mw_engine/heightmap.clj b/src/cljc/mw_engine/heightmap.clj index e282dcd..24c250d 100644 --- a/src/cljc/mw_engine/heightmap.clj +++ b/src/cljc/mw_engine/heightmap.clj @@ -1,4 +1,7 @@ -(ns ^{:doc "Functions to apply a heightmap to a world." +(ns ^{:doc "Functions to apply a heightmap to a world. + + Heightmaps are considered only as greyscale images, so colour is redundent + (will be ignored). Darker shades are higher." :author "Simon Brooke"} mw-engine.heightmap (:require [mikera.image.core :refer [load-image filter-image]] @@ -28,11 +31,6 @@ ;;;; Copyright (C) 2014 Simon Brooke ;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;; -;;;; Heightmaps are considered only as greyscale images, so colour is redundent -;;;; (will be ignored). Darker shades are higher. -;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; (defn tag-property "Set the value of this `property` of this cell from the corresponding pixel of this `heightmap`. diff --git a/src/cljc/mw_engine/natural_rules.clj b/src/cljc/mw_engine/natural_rules.clj index b655704..f695409 100644 --- a/src/cljc/mw_engine/natural_rules.clj +++ b/src/cljc/mw_engine/natural_rules.clj @@ -1,5 +1,10 @@ -(ns ^{:doc "A set of MicroWorld rules describing a simplified natural ecosystem." - :author "Simon Brooke"} +(ns ^{:doc "A set of MicroWorld rules describing a simplified natural ecosystem. + + Since the completion of the rule language this is more or less obsolete - + there are still a few things that you can do with rules written in Clojure + that you can't do in the rule language, but not many and I doubt they're + important. " + :author " Simon Brooke "} mw-engine.natural-rules (:require [mw-engine.utils :refer [get-int get-neighbours get-neighbours-with-state member?]])) @@ -25,13 +30,6 @@ ;;;; Copyright (C) 2014 Simon Brooke ;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;; -;;;; Since the completion of the rule language this is more or less obsolete - -;;;; there are still a few things that you can do with rules written in Clojure -;;;; that you can't do in the rule language, but not many and I doubt they're -;;;; important. -;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; treeline at arbitrary altitude. (def treeline 150) @@ -183,4 +181,4 @@ (list vegetation-rules herbivore-rules - predator-rules))) + predator-rules))) \ No newline at end of file diff --git a/src/cljc/mw_engine/utils.clj b/src/cljc/mw_engine/utils.clj index 2254f77..1be5a61 100644 --- a/src/cljc/mw_engine/utils.clj +++ b/src/cljc/mw_engine/utils.clj @@ -47,7 +47,6 @@ [_ cell] (merge cell {:generation (get-int-or-zero cell :generation)})) - (defn in-bounds "True if x, y are in bounds for this world (i.e., there is a cell at x, y) else false. *DEPRECATED*: it's a predicate, prefer `in-bounds?`. diff --git a/src/cljc/mw_engine/world.clj b/src/cljc/mw_engine/world.clj index 34d1e1b..b5fcd17 100644 --- a/src/cljc/mw_engine/world.clj +++ b/src/cljc/mw_engine/world.clj @@ -1,8 +1,16 @@ -(ns ^{:doc "Functions to create and to print two dimensional cellular automata." - :author "Simon Brooke"} - mw-engine.world - (:require [clojure.string :as string] - [mw-engine.utils :refer [population]])) +(ns ^{:doc "Functions to create and to print two dimensional cellular automata. + + Nothing in this namespace should determine what states are possible within + the automaton, except for the initial state, :new. + + A cell is a map containing at least values for the keys `:x`, `:y`, and `:state`. + + A world is a two dimensional matrix (sequence of sequences) of cells, such + that every cell's `:x` and `:y` properties reflect its place in the matrix." + :author "Simon Brooke"} + mw-engine.world + (:require [clojure.string :as string] + [mw-engine.utils :refer [population]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;;;; @@ -26,51 +34,14 @@ ;;;; Copyright (C) 2014 Simon Brooke ;;;; ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;;;; -;;;; Functions to create and to print two dimensional cellular automata. -;;;; Nothing in this namespace should determine what states are possible within -;;;; the automaton, except for the initial state, :new. -;;;; -;;;; A cell is a map containing at least values for the keys :x, :y, and :state. -;;;; -;;;; A world is a two dimensional matrix (sequence of sequences) of cells, such -;;;; that every cell's :x and :y properties reflect its place in the matrix. -;;;; -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(defn- make-cell +(defmacro make-cell "Create a minimal default cell at x, y * `x` the x coordinate at which this cell is created; * `y` the y coordinate at which this cell is created." [x y] - {:x x :y y :state :new}) - - -(defn- make-world-row - "Make the (remaining) cells in a row at this height in a world of this width. - - * `index` x coordinate of the next cell to be created; - * `width` total width of the matrix, in cells; - * `height` y coordinate of the next cell to be created." - [index width height] - (cond (= index width) nil - :else (cons (make-cell index height) - (make-world-row (inc index) width height)))) - - -(defn- make-world-rows - "Make the (remaining) rows in a world of this width and height, from this - index. - - * `index` y coordinate of the next row to be created; - * `width` total width of the matrix, in cells; - * `height` total height of the matrix, in cells." - [index width height] - (cond (= index height) nil - :else (cons (apply vector (make-world-row 0 width index)) - (make-world-rows (inc index) width height)))) + `{:x ~x :y ~y :state :new}) (defn make-world "Make a world width cells from east to west, and height cells from north to @@ -79,16 +50,17 @@ * `width` a natural number representing the width of the matrix to be created; * `height` a natural number representing the height of the matrix to be created." [width height] - (apply vector (make-world-rows 0 width height))) - + (apply vector + (map (fn [h] + (apply vector (map #(make-cell % h) (range width)))) + (range height)))) (defn truncate-state "Truncate the print name of the state of this cell to at most limit characters." [cell limit] (let [s (:state cell)] (cond (> (count (str s)) limit) (subs s 0 limit) - :else s))) - + :else s))) (defn format-cell "Return a formatted string summarising the current state of this cell." @@ -98,13 +70,11 @@ (population cell :deer) (population cell :wolves))) - (defn- format-world-row "Format one row in the state of a world for printing." [row] (string/join (map format-cell row))) - (defn print-world "Print the current state of this world, and return nil. @@ -112,8 +82,8 @@ [world] (println) (dorun - (map - #(println - (format-world-row %)) - world)) + (map + #(println + (format-world-row %)) + world)) nil)