Upversioning whole system to 0.2.0, for flow feature

This commit is contained in:
Simon Brooke 2023-07-10 13:46:42 +01:00
parent 8b3639edd5
commit 4f35557b38
8 changed files with 104 additions and 152 deletions

View file

@ -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"]

View file

@ -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)))

View file

@ -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]

View file

@ -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.

View file

@ -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`.

View file

@ -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)))

View file

@ -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?`.

View file

@ -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)