diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..08cf6ba
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,19 @@
+.calva/
+target/
+
+pom.xml
+
+.lein-repl-history
+
+.lein-failures
+
+eastwood.txt
+
+.clj-kondo/
+.lsp/
+.project
+.settings/
+.nrepl-port
+.classpath
+
+test.html
\ No newline at end of file
diff --git a/README.md b/README.md
index 01786d2..d188729 100644
--- a/README.md
+++ b/README.md
@@ -13,6 +13,13 @@ You can see MicroWorld in action [here](http://www.journeyman.cc/microworld/) -
but please don't be mean to my poor little server. If you want to run big maps
or complex rule-sets, please run it on your own machines.
+### Version compatibility
+
+There are substantial changes in how rule functions are evaluated between 0.1.x
+versions of MicroWorld libraries and 0.3.x versions. In particular, in 0.3.x
+metadata is held on rule functions which is essential to the functioning of the
+engine. Consequently, you cannot mix 0.1.x and 0.3.x libraries: it will not work.
+
## Usage
Primary entry points are make-world and run-world, both in mw-engine.core. See
diff --git a/buildall.sh b/buildall.sh
index 0eabd8e..6152aed 100755
--- a/buildall.sh
+++ b/buildall.sh
@@ -72,8 +72,9 @@ if [ $# -lt 1 ]
then
cat <<-EOF 1>&2
Usage:
- -archive Create a tar archive of the current state of the source.
- -build Build all components and commit to master.
+ -archive Create a tar archive of the current state of the source.
+ -build Build all components, commit and push to origin.
+ -docker Build and push a Docker image.
-email [ADDRESS] Your email address, to be recorded in the build signature.
-fullname [NAME] Your full name, to be recorded in the build signature.
-pull Pull from remote git repository
@@ -87,12 +88,14 @@ fi
while (( "$#" ))
do
case $1 in
- -a|-archive)
- archive="TRUE";;
+ -a|-archive)
+ archive="TRUE";;
-b|-build)
# 'build' is the expected normal case.
trial="FALSE";
;;
+ -d|-docker)
+ docker="TRUE";;
-e|-email)
shift;
email=$1;;
@@ -126,7 +129,7 @@ do
shift
done
-echo "Trial: ${trial}; email: ${email}; fullname ${fullname}; release: ${release}; webapps: $webappsdir"
+echo "Trial: ${trial}; docker: ${docker}; email: ${email}; fullname ${fullname}; release: ${release}; webapps: $webappsdir"
ls mw-* > /dev/null 2>&1
if [ $? -ne 0 ]
@@ -137,132 +140,141 @@ fi
for dir in mw-*
do
- pushd ${dir}
+ if [ "${dir}" != "mw-explore" ]
+ then
+ pushd ${dir}
- # Make a temporary directory to keep the work-in-progress files.
- if [ ! -d "${tmp}" ]
- then
- rm -f "${tmp}"
- mkdir "${tmp}"
- fi
+ # Make a temporary directory to keep the work-in-progress files.
+ if [ ! -d "${tmp}" ]
+ then
+ rm -f "${tmp}"
+ mkdir "${tmp}"
+ fi
- cat project.clj > ${tmp}/project.bak.1
- old=`cat project.clj | grep 'defproject mw' | sed 's/.*defproject mw-[a-z]* "\([A-Za-z0-9_.-]*\)".*/\1/'`
+ cat project.clj > ${tmp}/project.bak.1
+ old=`cat project.clj | grep 'defproject mw' | sed 's/.*defproject mw-[a-z]* "\([A-Za-z0-9_.-]*\)".*/\1/'`
- if [ "${release}" != "" ]
- then
- message="Preparing ${old} for release"
+ if [ "${release}" != "" ]
+ then
+ message="Preparing ${old} for release"
- # Does the 'old' version tag end with the token "-SNAPSHOT"? it probably does!
- echo "${old}" | grep 'SNAPSHOT'
- if [ $? -eq 0 ]
- then
- # It does...
- interim=`echo ${old} | sed 's/\([A-Za-z0-9_.-]*\)-SNAPSHOT.*/\1/'`
- if [ "${interim}" = "" ]
- then
- echo "Failed to compute interim version tag from '${old}'" 1>&2
- exit 1;
- fi
- setup-build-sig "${old}" "${interim}" "${fullname}" "${email}"
- message="Upversioned from ${old} to ${interim} for release"
- old=${interim}
- else
- setup-build-sig "unset" "${old}" "${fullname}" "${email}"
- fi
- else
- setup-build-sig "unset" "${old}" "${fullname}" "${email}"
- fi
+ # Does the 'old' version tag end with the token "-SNAPSHOT"? it probably does!
+ echo "${old}" | grep 'SNAPSHOT'
+ if [ $? -eq 0 ]
+ then
+ # It does...
+ interim=`echo ${old} | sed 's/\([A-Za-z0-9_.-]*\)-SNAPSHOT.*/\1/'`
+ if [ "${interim}" = "" ]
+ then
+ echo "Failed to compute interim version tag from '${old}'" 1>&2
+ exit 1;
+ fi
+ setup-build-sig "${old}" "${interim}" "${fullname}" "${email}"
+ message="Upversioned from ${old} to ${interim} for release"
+ old=${interim}
+ else
+ setup-build-sig "unset" "${old}" "${fullname}" "${email}"
+ fi
+ else
+ setup-build-sig "unset" "${old}" "${fullname}" "${email}"
+ fi
- sed -f ${tmp}/manifest.sed ${tmp}/project.bak.1 > project.clj
+ sed -f ${tmp}/manifest.sed ${tmp}/project.bak.1 > project.clj
- echo $message
+ echo $message
- lein clean
- lein compile
- if [ $? -ne 0 ]
- then
- echo "Sub-project ${dir} failed in compile" 1>&2
- exit 1
- fi
+ lein clean
+ lein compile
+ if [ $? -ne 0 ]
+ then
+ echo "Sub-project ${dir} failed in compile" 1>&2
+ exit 1
+ fi
- lein test
- if [ $? -ne 0 ]
- then
- echo "Sub-project ${dir} failed in test" 1>&2
- exit 1
- fi
+ lein test
+ if [ $? -ne 0 ]
+ then
+ echo "Sub-project ${dir} failed in test" 1>&2
+ exit 1
+ fi
- lein marg
- lein install
+ lein marg
+ lein install
- # If we're in the UI project, build the uberwar - and should
- # probably deploy it to local Tomcat for test
- if [ "${dir}" = "mw-ui" -a "${webappsdir}" != "" ]
- then
- lein ring uberwar
- sudo cp target/microworld.war "${webappsdir}"
- echo "Deployed new WAR file to local Tomcat at ${webappsdir}"
- fi
+ # If we're in the UI project, build the uberwar - and should
+ # probably deploy it to local Tomcat for test
+ if [ "${dir}" = "mw-ui" -a "${webappsdir}" != "" ]
+ then
+ lein ring uberwar
+ sudo cp target/microworld.war "${webappsdir}"
+ echo "Deployed new WAR file to local Tomcat at ${webappsdir}"
+ fi
- # Then unset manifest properties prior to committing.
- cat project.clj > ${tmp}/project.bak.2
- setup-build-sig
- sed -f ${tmp}/manifest.sed ${tmp}/project.bak.2 > project.clj
+ if [ "${dir}" = "mw-ui" -a "${docker}" = "TRUE" ]
+ then
+ lein docker build
+ lein docker push
+ fi
- if [ "${trial}" = "FALSE" ]
- then
- if [ "${message}" = "" ]
- then
- git commit -a
- else
- git commit -a -m "$message"
- fi
- git push origin master
- fi
+ # Then unset manifest properties prior to committing.
+ cat project.clj > ${tmp}/project.bak.2
+ setup-build-sig
+ sed -f ${tmp}/manifest.sed ${tmp}/project.bak.2 > project.clj
- if [ "${release}" != "" ]
- then
- branch="${old}_MAINTENANCE"
- if [ "${trial}" = "FALSE" ]
- then
- git branch "${branch}"
- git push origin "${branch}"
- fi
+ if [ "${trial}" = "FALSE" ]
+ then
+ if [ "${message}" = "" ]
+ then
+ git commit -a
+ else
+ git commit -a -m "$message"
+ fi
+ git push origin master
+ fi
- cat project.clj > ${tmp}/project.bak.3
- setup-build-sig "${old}" "${release}-SNAPSHOT" "${fullname}" "${email}"
- sed -f ${tmp}/manifest.sed ${tmp}/project.bak.3 > project.clj
- message="Upversioned from ${interim} to ${release}-SNAPSHOT"
+ if [ "${release}" != "" ]
+ then
+ branch="${old}_MAINTENANCE"
+ if [ "${trial}" = "FALSE" ]
+ then
+ git branch "${branch}"
+ git push origin "${branch}"
+ fi
- echo $message
+ cat project.clj > ${tmp}/project.bak.3
+ setup-build-sig "${old}" "${release}-SNAPSHOT" "${fullname}" "${email}"
+ sed -f ${tmp}/manifest.sed ${tmp}/project.bak.3 > project.clj
+ message="Upversioned from ${interim} to ${release}-SNAPSHOT"
- lein clean
- lein compile
- if [ $? -ne 0 ]
- then
- echo "Sub-project ${dir} failed in compile after branch to ${release}!" 1>&2
- exit 1
- fi
- lein marg
- lein install
+ echo $message
- # Then unset manifest properties prior to committing.
- cat project.clj > ${tmp}/project.bak.4
- setup-build-sig
- sed -f ${tmp}/manifest.sed ${tmp}/project.bak.4 > project.clj
+ lein clean
+ lein compile
+ if [ $? -ne 0 ]
+ then
+ echo "Sub-project ${dir} failed in compile after branch to ${release}!" 1>&2
+ exit 1
+ fi
+ lein marg
+ lein install
- if [ "${trial}" = "FALSE" ]
- then
- git commit -a -m "${message}"
- echo ${message}
- git push origin master
- fi
- fi
+ # Then unset manifest properties prior to committing.
+ cat project.clj > ${tmp}/project.bak.4
+ setup-build-sig
+ sed -f ${tmp}/manifest.sed ${tmp}/project.bak.4 > project.clj
- # if nothing broke so far, clean up...
- rm -rf "${tmp}"
- popd
+ if [ "${trial}" = "FALSE" ]
+ then
+ git commit -a -m "${message}"
+ echo ${message}
+ git push origin master
+ fi
+ fi
+
+ # if nothing broke so far, clean up...
+ rm -rf "${tmp}"
+ popd
+ fi
done
diff --git a/docs/cloverage/coverage.css b/docs/cloverage/coverage.css
new file mode 100644
index 0000000..2be4e57
--- /dev/null
+++ b/docs/cloverage/coverage.css
@@ -0,0 +1,40 @@
+.covered {
+ font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
+ background-color: #558B55;
+}
+
+.not-covered {
+ font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
+ background-color: red;
+}
+
+.partial {
+ font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
+ background-color: orange;
+}
+
+.not-tracked {
+ font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
+}
+
+.blank {
+ font-family: 'Bitstream Vera Sans Mono', 'Courier', monospace;
+}
+
+td {
+ padding-right: 10px;
+}
+
+td.with-bar {
+ width: 250px;
+ text-align: center;
+}
+
+td.with-number {
+ text-align: right;
+}
+
+td.ns-name {
+ min-width: 150px;
+ padding-right: 25px;
+}
diff --git a/docs/cloverage/index.html b/docs/cloverage/index.html
new file mode 100644
index 0000000..c18113a
--- /dev/null
+++ b/docs/cloverage/index.html
@@ -0,0 +1,172 @@
+
+
\ No newline at end of file
diff --git a/docs/codox/intro.html b/docs/codox/intro.html
new file mode 100644
index 0000000..9baafa0
--- /dev/null
+++ b/docs/codox/intro.html
@@ -0,0 +1,5 @@
+
+Introduction to mw-engine
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.
+
*with-history*
dynamic
I suspect that caching history on the cells is greatly worsening the memory problems. Make it optional, but by default false.
Apply a single rule to a cell. What this is about is that I want to be able, for debugging purposes, to tag a cell with the rule text of the rule which fired (and especially so when an exception is thrown).
\ No newline at end of file
diff --git a/docs/codox/mw-engine.display.html b/docs/codox/mw-engine.display.html
new file mode 100644
index 0000000..1d58efa
--- /dev/null
+++ b/docs/codox/mw-engine.display.html
@@ -0,0 +1,11 @@
+
+mw-engine.display documentation
\ No newline at end of file
diff --git a/docs/codox/mw-engine.drainage.html b/docs/codox/mw-engine.drainage.html
new file mode 100644
index 0000000..fa2d299
--- /dev/null
+++ b/docs/codox/mw-engine.drainage.html
@@ -0,0 +1,22 @@
+
+mw-engine.drainage documentation
Compute the total flow upstream of this cell in this world, and return a cell identical to this one but having a value of its flow property set from that computation. The function is memoised because the consequence of mapping a recursive function across an array is that many cells will be revisited - potentially many times.
+
Flow comes from a higher cell to a lower only if the lower is the lowest neighbour of the higher.
Return a list of the cells in this world which are higher than this cell and for which this cell is the lowest neighbour, or which are at the same altitude and have greater flow
Experimental non recursive flow algorithm, needs to be run on a world as many times as there are distinct altitude values. This algorithm works only if applied sequentially from the highest altitude to the lowest, see flow-world-nr.
Detects point hollows - that is, individual cells all of whose neighbours are higher. Return true if this cell has an altitude lower than any of its neighbours in this world
Return a row like this row, across which rainfall has been distributed; if rain-probability is specified, it is the probable rainfall on a cell with no gradient.
\ No newline at end of file
diff --git a/docs/codox/mw-engine.flow.html b/docs/codox/mw-engine.flow.html
new file mode 100644
index 0000000..5548d21
--- /dev/null
+++ b/docs/codox/mw-engine.flow.html
@@ -0,0 +1,29 @@
+
+mw-engine.flow documentation
The design here is: a flow object is a map with the following properties:
+
+
:source, whose value is a location;
+
:destination, whose value is a location;
+
:property, whose value is a keyword;
+
:quantity, whose value is a positive real number.
+
+
A location object is a map with the following properties:
+
+
:x, whose value is a natural number not greater than the extent of the world;
+
: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.
+
coordinate?
(coordinate? o world)
Return true if this object o is a valid coordinate with respect to this world, else false. Assumes square worlds.
Return a world like this world, except with the quantity of the property described in this flow object transferred from the source of that flow to its destination.
Plan, but do not execute, all the flows in this world implied by those of these rules (which are expected to be pre-compiled) which are flow rules. Return the list of plans, as flow objects.
\ No newline at end of file
diff --git a/docs/codox/mw-engine.heightmap.html b/docs/codox/mw-engine.heightmap.html
new file mode 100644
index 0000000..f5d987f
--- /dev/null
+++ b/docs/codox/mw-engine.heightmap.html
@@ -0,0 +1,31 @@
+
+mw-engine.heightmap documentation
Heightmaps are considered only as greyscale images, so colour is redundent (will be ignored). Darker shades are higher.
+
apply-heightmap
(apply-heightmap world imagepath)(apply-heightmap imagepath)
Apply the image file loaded from this path to this world, and return a world whose altitudes are modified (added to) by the altitudes in the heightmap. It is assumed that the heightmap is at least as large in x and y dimensions as the world. Note that, in addition to setting the :altitude of each cell, this function also sets the :gradient.
+
+
world a world, as defined in world.clj, q.v.; if world is not supplied, a world the size of the heightmap will be created;
+
imagepath a file path or URL which indicates an (ideally greyscale) image file.
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.
+
+
world not actually used, but present to enable this function to be passed as an argument to mw-engine.utils/map-world, q.v.
+
cell a cell, as discussed in world.clj, q.v. Alternatively, a map;
+
property the property (normally a keyword) whose value will be set on the cell.
+
heightmap an (ideally) greyscale image, whose x and y dimensions should exceed those of the world of which the cell forms part.
\ No newline at end of file
diff --git a/docs/codox/mw-engine.natural-rules.html b/docs/codox/mw-engine.natural-rules.html
new file mode 100644
index 0000000..e37c174
--- /dev/null
+++ b/docs/codox/mw-engine.natural-rules.html
@@ -0,0 +1,14 @@
+
+mw-engine.natural-rules documentation
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.
\ No newline at end of file
diff --git a/docs/codox/mw-engine.render.html b/docs/codox/mw-engine.render.html
new file mode 100644
index 0000000..d929a27
--- /dev/null
+++ b/docs/codox/mw-engine.render.html
@@ -0,0 +1,14 @@
+
+mw-engine.render documentation
\ No newline at end of file
diff --git a/docs/codox/mw-engine.utils.html b/docs/codox/mw-engine.utils.html
new file mode 100644
index 0000000..74001d9
--- /dev/null
+++ b/docs/codox/mw-engine.utils.html
@@ -0,0 +1,82 @@
+
+mw-engine.utils documentation
Utility functions needed by MicroWorld and, specifically, in the interpretation of MicroWorld rule.
+
add-history-event
(add-history-event cell rule)(add-history-event result rule detail)
If cell is non-nil, expect it to be a map representing a cell; add to its history an an event recording the firing of this rule. If detail is passed, treat it as a map of additional data to be added to the event.
x an integer representing an x coordinate in that world;
+
y an integer representing an y coordinate in that world;
+
depth an integer representing the distance from x,y that should be searched Gets the neighbours within the specified distance of the cell at coordinates x,y in this world.
(get-neighbours-with-property-value world x y depth property value op)(get-neighbours-with-property-value world x y depth property value)(get-neighbours-with-property-value world cell depth property value)(get-neighbours-with-property-value world cell property value)
Get the neighbours to distance depth of the cell at x, y in this world which have this value for this property.
Return a cell like this cell, but having a value for :generation, zero if the cell passed had no integer value for generation, otherwise the value taken from the cell passed. The world argument is present only for consistency with the rule engine and is ignored.
(map-world world function)(map-world world function additional-args)
Apply this function to each cell in this world to produce a new world. the arguments to the function will be the world, the cell, and any additional-args supplied. Note that we parallel map over rows but just map over cells within a row. That’s because it isn’t worth starting a new thread for each cell, but there may be efficiency gains in running rows in parallel.
Memoised get neighbours is more efficient when running deeply recursive algorithms on the same world. But it’s less efficient when running the engine in its normal iterative style, because then we will rarely call get naighbours on the same cell of the same world twice.
Return the population of this species in this cell. Currently a synonym for get-int, but may not always be (depending whether species are later implemented as actors)
+
+
cell a map;
+
species a keyword representing a species which may populate that cell.
\ No newline at end of file
diff --git a/docs/codox/mw-engine.world.html b/docs/codox/mw-engine.world.html
new file mode 100644
index 0000000..fea1dba
--- /dev/null
+++ b/docs/codox/mw-engine.world.html
@@ -0,0 +1,25 @@
+
+mw-engine.world documentation
\ No newline at end of file
diff --git a/docs/uberdoc.html b/docs/uberdoc.html
new file mode 100644
index 0000000..b7df272
--- /dev/null
+++ b/docs/uberdoc.html
@@ -0,0 +1,4092 @@
+
+mw-engine -- Marginalia
mw-engine: the state/transition engine of MicroWorld.
+
+
This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+
This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+
You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+USA.
+
+
Copyright (C) 2014 Simon Brooke
+
Base url (i.e., url of directory) from which to load tile images.
+
(def ^:dynamic *image-base*
+ "img/tiles")
Format this state, assumed to be a keyword indicating a state in the
+ world, into a CSS class
mw-engine: the state/transition engine of MicroWorld.
+
+
This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+
This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+
You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+USA.
+
+
Copyright (C) 2014 Simon Brooke
+
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.
+
+
+
world not actually used, but present to enable this function to be
+ passed as an argument to mw-engine.utils/map-world, q.v.
+
cell a cell, as discussed in world.clj, q.v. Alternatively, a map;
+
property the property (normally a keyword) whose value will be set on the cell.
+
heightmap an (ideally) greyscale image, whose x and y dimensions should
+ exceed those of the world of which the cell forms part.
Set the gradient property of each cell in this world to the difference in
+ altitude between its highest and lowest neghbours.
+
(defn tag-gradients
+ [world]
+ (map-world world tag-gradient))
Set the altitude of this cell from the corresponding pixel of this heightmap.
+ If the heightmap you supply is smaller than the world, this will break.
+
+
+
world not actually used, but present to enable this function to be
+ passed as an argument to mw-engine.utils/map-world, q.v.;
+
cell a cell, as discussed in world.clj, q.v. Alternatively, a map;
+
heightmap an (ideally) greyscale image, whose x and y dimensions should
+ exceed those of the world of which the cell forms part.
Apply the image file loaded from this path to this world, and return a world whose
+ altitudes are modified (added to) by the altitudes in the heightmap. It is assumed that
+ the heightmap is at least as large in x and y dimensions as the world. Note that, in
+ addition to setting the :altitude of each cell, this function also sets the :gradient.
+
+
+
world a world, as defined in world.clj, q.v.; if world is not supplied,
+a world the size of the heightmap will be created;
+
imagepath a file path or URL which indicates an (ideally greyscale) image file.
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.
+
mw-engine: the state/transition engine of MicroWorld.
+
+
This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+
This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+
You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+USA.
mw-engine: the state/transition engine of MicroWorld.
+
+
This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+
This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+
You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+USA.
+
+
Copyright (C) 2014 Simon Brooke
+
Return 'true' if elt is a member of col, else 'false'.
Return a cell like this cell, but having a value for :generation, zero if
+ the cell passed had no integer value for generation, otherwise the value
+ taken from the cell passed. The world argument is present only for
+ consistency with the rule engine and is ignored.
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?.
+
+
+
world a world as defined above;
+
x a number which may or may not be a valid x coordinate within that world;
+
y a number which may or may not be a valid y coordinate within that world.
+
+
(defn in-bounds
+ {:deprecated "1.1.7"}
+ [world x y]
+ (and (>= x 0) (>= y 0) (< y (count world)) (< x (count (first world)))))
True if x, y are in bounds for this world (i.e., there is a cell at x, y)
+ else false.
+
+
+
world a world as defined above;
+
x a number which may or may not be a valid x coordinate within that world;
+
y a number which may or may not be a valid y coordinate within that world.
+
+
(defn in-bounds?
+ {:added "1.1.7"}
+ [world x y]
+ (and (>= x 0) (>= y 0) (< y (count world)) (< x (count (first world)))))
Wholly non-parallel map world implementation; see documentation for map-world.
+
(defn map-world-n-n
+ ([world function]
+ (map-world-n-n world function nil))
+ ([world function additional-args]
+ (into []
+ (map (fn [row]
+ (into [] (map
+ #(apply function
+ (cons world (cons % additional-args)))
+ row)))
+ world))))
Wholly parallel map-world implementation; see documentation for map-world.
+
(defn map-world-p-p
+ ([world function]
+ (map-world-p-p world function nil))
+ ([world function additional-args]
+ (into []
+ (pmap (fn [row]
+ (into [] (pmap
+ #(apply function
+ (cons world (cons % additional-args)))
+ row)))
+ world))))
Apply this function to each cell in this world to produce a new world.
+ the arguments to the function will be the world, the cell, and any
+ additional-args supplied. Note that we parallel map over rows but
+ just map over cells within a row. That's because it isn't worth starting
+ a new thread for each cell, but there may be efficiency gains in
+ running rows in parallel.
+
(defn map-world
+ ([world function]
+ (map-world world function nil))
+ ([world function additional-args]
+ (into []
+ (pmap (fn [row]
+ (into [] (map
+ #(apply function
+ (cons world (cons % additional-args)))
+ row)))
+ world))))
Return the cell a x, y in this world, if any.
+
+
+
world a world as defined above;
+
x a number which may or may not be a valid x coordinate within that world;
+
y a number which may or may not be a valid y coordinate within that world.
+
+
(defn get-cell
+ [world x y]
+ (when (in-bounds? world x y)
+ (nth (nth world y) x)))
Get the value of a property expected to be an integer from a map; if not
+ present (or not an integer) return 0.
+
+
+
map a map;
+
key a symbol or keyword, presumed to be a key into the map.
+
+
(defn get-int
+ [map key]
+ (if (map? map)
+ (let [v (map key)]
+ (cond (and v (integer? v)) v
+ :else 0))
+ (throw (Exception. "No map passed?"))))
Get the value of a property expected to be a number from a map; if not
+ present (or not a number) return 0.
+
+
+
map a map;
+
key a symbol or keyword, presumed to be a key into the map.
+
+
(defn get-num
+ [map key]
+ (if (map? map)
+ (let [v (map key)]
+ (cond (and v (number? v)) v
+ :else 0))
+ (throw (Exception. "No map passed?"))))
Return the population of this species in this cell. Currently a synonym for
+ get-int, but may not always be (depending whether species are later
+ implemented as actors)
+
+
+
cell a map;
+
species a keyword representing a species which may populate that cell.
+
+
(defn population
+ [cell species]
+ (get-int cell species))
Memoised get neighbours is more efficient when running deeply recursive
+ algorithms on the same world. But it's less efficient when running the
+ engine in its normal iterative style, because then we will rarely call
+ get naighbours on the same cell of the same world twice.
+
(def memo-get-neighbours
+ (memoize
+ (fn [world x y depth]
+ (remove nil?
+ (map #(get-cell world (first %) (first (rest %)))
+ (remove #(= % (list x y))
+ (combo/cartesian-product
+ (range (- x depth) (+ x depth 1))
+ (range (- y depth) (+ y depth 1)))))))))
Get the neighbours to distance depth of a cell in this world.
+
+
Several overloads:
+* `world` a world, as described in world.clj;
+* `cell` a cell within that world
+Gets immediate neighbours of the specified cell.
+
+* `world` a world, as described in world.clj;
+* `cell` a cell within that world
+* `depth` an integer representing the depth to search from the
+ `cell`
+Gets neighbours within the specified distance of the cell.
+
+* `world` a world, as described in world.clj;
+* `x` an integer representing an x coordinate in that world;
+* `y` an integer representing an y coordinate in that world;
+* `depth` an integer representing the distance from [x,y] that
+ should be searched
+Gets the neighbours within the specified distance of the cell at
+coordinates [x,y] in this world.
+
+
(defn get-neighbours
+ ([world x y depth]
+ (memo-get-neighbours world x y depth))
+ ([world cell depth]
+ (memo-get-neighbours world (:x cell) (:y cell) depth))
+ ([world cell]
+ (memo-get-neighbours world (:x cell) (:y cell) 1)))
Get the neighbours to distance depth of the cell at x, y in this world which
+ have this value for this property.
+
+
* `world` a world, as described in `world.clj`;
+* `cell` a cell within that world;
+* `depth` an integer representing the distance from [x,y] that
+ should be searched (optional);
+* `property` a keyword representing a property of the neighbours;
+* `value` a value of that property (or, possibly, the name of another);
+* `op` a comparator function to use in place of `=` (optional).
+
+
+
It gets messy.
+
(defn get-neighbours-with-property-value
+ ([world x y depth property value op]
+ (filter
+ #(eval
+ (list op
+ (or (get % property) (get-int % property))
+ value))
+ (get-neighbours world x y depth)))
+ ([world x y depth property value]
+ (get-neighbours-with-property-value world x y depth property value =))
+ ([world cell depth property value]
+ (get-neighbours-with-property-value world (:x cell) (:y cell) depth
+ property value))
+ ([world cell property value]
+ (get-neighbours-with-property-value world cell 1
+ property value)))
Get the neighbours to distance depth of the cell at x, y in this world which
+ have this state.
+
+
* `world` a world, as described in `world.clj`;
+* `cell` a cell within that world;
+* `depth` an integer representing the distance from [x,y] that
+ should be searched;
+* `state` a keyword representing a state in the world.
+
+
(defn get-neighbours-with-state
+ ([world x y depth state]
+ (filter #(= (:state %) state) (get-neighbours world x y depth)))
+ ([world cell depth state]
+ (get-neighbours-with-state world (:x cell) (:y cell) depth state))
+ ([world cell state]
+ (get-neighbours-with-state world cell 1 state)))
Return the cell from among these cells which has the lowest numeric value
+ for this property.
If this cells x and y properties are equal to these x and y values,
+ return a cell like this cell but with the value of this property set to
+ this value. Otherwise, just return this cell.
+
(defn- set-cell-property
+ [cell x y property value]
+ (cond
+ (and (= x (:x cell)) (= y (:y cell)))
+ (merge cell {property value :rule "Set by user"})
+ :else cell))
Return a world like this world but with the value of exactly one property
+ of one cell changed to this value
+
(defn set-property
+ ([world cell property value]
+ (set-property world (:x cell) (:y cell) property value))
+ ([world x y property value]
+ (apply
+ vector ;; we want a vector of vectors, not a list of lists, for efficiency
+ (map
+ (fn [row]
+ (apply
+ vector
+ (map #(set-cell-property % x y property value)
+ row)))
+ world))))
Return a world like this world, but merge the values from this cell with
+ those from the cell in the world with the same co-ordinates
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.
mw-engine: the state/transition engine of MicroWorld.
+
+
This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+
This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+
You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+USA.
+
+
Copyright (C) 2014 Simon Brooke
+
Return true if this object o is a valid coordinate with respect to
+ this world, else false. Assumes square worlds.
+
(defn coordinate?
+ [o world]
+ (try
+ (and (or (zero? o) (pos-int? o))
+ (< o (count world)))
+ (catch Exception e
+ (warn (format "Not a valid coordinate: %s; %s" o (.getMessage e)))
+ false)))
Return true if this object o is a location as defined above with respect to
+ this world, else false.
Return a world like this world, except with the quantity of the property
+ described in this flow object transferred from the source of that flow
+ to its destination.
+
(defn execute
+ [world flow]
+ (try
+ (let [sx (-> flow :source :x)
+ sy (-> flow :source :y)
+ source (get-cell world sx sy)
+ 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))]
+ (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'))
+ (catch Exception e
+ (warn (format "Failed to execute flow %s: %s" flow (.getMessage e)))
+ ;; return the world unmodified.
+ world)))
Return a world like this world, but with each of these flows executed.
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.
+
mw-engine: the state/transition engine of MicroWorld.
+
+
This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+
This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+
You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+USA.
+
+
Copyright (C) 2014 Simon Brooke
+
Apply a single rule to a cell. What this is about is that I want to be able,
+ for debugging purposes, to tag a cell with the rule text of the rule which
+ fired (and especially so when an exception is thrown. So a rule may be either
+ 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
+ to access neighbours.
Derive a cell from this cell of this world by applying these rules.
+
(defn- apply-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))))))
Derive a cell from this cell of this world by applying these rules. If an
+ exception is thrown, cache its message on the cell and set it's state to error
+
(defn- transform-cell
+ [world cell rules]
+ (try
+ (merge
+ (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"
+ (.getMessage e)
+ (:generation cell)
+ (:state cell))
+ :stacktrace (map #(.toString %) (.getStackTrace e))
+ :state :error}))))
Return a world derived from this world by applying these rules to each cell.
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.
+
mw-engine: the state/transition engine of MicroWorld.
+
+
This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+
This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+
You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+USA.
Experimental, probably of no interest to anyone else; attempt to
+ compute drainage on a world, assumed to have altitudes already set
+ from a heightmap.
mw-engine: the state/transition engine of MicroWorld.
+
+
This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+
This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+
You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+USA.
+
+
Copyright (C) 2014 Simon Brooke
+
+
(def ^:dynamic *sealevel* 10)
forward declaration of flow, to allow for a wee bit of mutual recursion.
+
(declare flow)
Compute rainfall for a cell with this gradient west-east, given
+ remaining drops to distribute, and this overall map width.
+
(defn rainfall
+ [gradient remaining map-width]
+ (cond
+ ;; if there's no rain left in the cloud, it can't fall;
+ (zero? remaining)
+ 0
+ (pos? gradient)
+ ;; rain, on prevailing westerly wind, falls preferentially on rising ground;
+ (int (rand gradient))
+ ;; rain falls randomly across the width of the map...
+ (zero? (int (rand map-width))) 1
+ :else
+ 0))
Return a row like this row, across which rainfall has been distributed;
+ if rain-probability is specified, it is the probable rainfall on a cell
+ with no gradient.
Return a list of the cells in this world which are higher than this
+ cell and for which this cell is the lowest neighbour, or which are at the
+ same altitude and have greater flow
Detects point hollows - that is, individual cells all of whose neighbours
+ are higher. Return true if this cell has an altitude lower than any of
+ its neighbours in this world
+
(defn is-hollow
+ [world cell]
+ ;; quicker to count the elements of the list and compare equality of numbers
+ ;; than recursive equality check on members, I think. But worth benchmarking.
+ (let [neighbours (get-neighbours world cell)
+ altitude (get-int-or-zero cell :altitude)]
+ (= (count neighbours)
+ (count (get-neighbours-with-property-value
+ world (:x cell) (:y cell) 1 :altitude altitude >)))))
Raise the altitude of a copy of this cell of this world to the altitude
+ of the lowest of its neighbours.
Experimental non recursive flow algorithm, needs to be run on a world as
+ many times as there are distinct altitude values. This algorithm works only
+ if applied sequentially from the highest altitude to the lowest, see
+ flow-world-nr.
Compute the total flow upstream of this cell in this world, and return a cell identical
+ to this one but having a value of its flow property set from that computation. The function is
+ memoised because the consequence of mapping a recursive function across an array is that many
+ cells will be revisited - potentially many times.
+
+
Flow comes from a higher cell to a lower only if the lower is the lowest neighbour of the higher.
mw-engine: the state/transition engine of MicroWorld.
+
+
This program is free software; you can redistribute it and/or
+modify it under the terms of the GNU General Public License
+as published by the Free Software Foundation; either version 2
+of the License, or (at your option) any later version.
+
+
This program is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+GNU General Public License for more details.
+
+
You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+USA.
+
+
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.
+
Apply a single rule to a cell. What this is about is that I want to be able,
+ for debugging purposes, to tag a cell with the rule text of the rule which
+ fired (and especially so when an exception is thrown. So a rule may be either
+ 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
+ to access neighbours.
Derive a cell from this cell of this world by applying these rules.
+
(defn- apply-rules
+ [world cell rules]
+ (cond (empty? rules) cell
+ true (let [result (apply-rule world cell (first rules))]
+ (cond result result
+ true (apply-rules world cell (rest rules))))))
Derive a cell from this cell of this world by applying these rules. If an
+ exception is thrown, cache its message on the cell and set it's state to error
+
(defn- transform-cell
+ [world cell rules]
+ (try
+ (merge
+ (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"
+ (.getMessage e)
+ (:generation cell)
+ (:state cell))
+ :stacktrace (map #(.toString %) (.getStackTrace e))
+ :state :error}))))
Return a world derived from this world by applying these rules to each cell.
Consider this single argument as a map of :world and :rules; apply the rules
+ to transform the world, and return a map of the new, transformed :world and
+ these :rules. As a side effect, print the world.
\ No newline at end of file
diff --git a/project.clj b/project.clj
index a2739ff..a11a4d0 100644
--- a/project.clj
+++ b/project.clj
@@ -1,21 +1,32 @@
-(defproject mw-engine "0.1.5-SNAPSHOT"
+(defproject mw-engine "0.3.0-SNAPSHOT"
+ :cloverage {:output "docs/cloverage"}
+ :codox {:metadata {:doc "**TODO**: write docs"
+ :doc/format :markdown}
+ :output-path "docs/codox"
+ :source-uri "https://github.com/simon-brooke/mw-engine/blob/master/{filepath}#L{line}"}
+ :dependencies [[com.github.pmonks/embroidery "0.1.20"] ;; better pmap?
+ [distributions "0.1.2"] ;; mainly for investigating drainage
+ [org.clojure/clojure "1.11.1"]
+ [org.clojure/clojurescript "1.11.60" :scope "provided"]
+ [org.clojure/math.combinatorics "0.2.0"]
+ [org.clojure/tools.trace "0.7.11"]
+ [org.clojure/tools.namespace "1.4.4"]
+ [com.taoensso/timbre "6.2.1"]
+ [fivetonine/collage "0.3.0"]
+ ;; [hiccup "1.0.5"]
+ [hiccup "2.0.0-RC3"]
+ [net.mikera/imagez "0.12.0"]]
:description "Cellular automaton world builder."
- :url "http://www.journeyman.cc/microworld/"
- :manifest {
- "build-signature-version" "unset"
- "build-signature-user" "unset"
- "build-signature-email" "unset"
- "build-signature-timestamp" "unset"
- "Implementation-Version" "unset"
- }
:jvm-opts ["-Xmx4g"]
:license {:name "GNU General Public License v2"
:url "http://www.gnu.org/licenses/gpl-2.0.html"}
- :plugins [[lein-marginalia "0.7.1"]]
- :dependencies [[org.clojure/clojure "1.6.0"]
- [org.clojure/math.combinatorics "0.0.7"]
- [org.clojure/tools.trace "0.7.8"]
- [org.clojure/tools.namespace "0.2.4"]
- [hiccup "1.0.5"]
- [net.mikera/imagez "0.3.1"]
- [fivetonine/collage "0.2.0"]])
+
+ :min-lein-version "2.0.0"
+ :plugins [[lein-cljsbuild "1.1.7"]
+ [lein-cloverage "1.2.2"]
+ [lein-codox "0.10.8"]
+ [lein-kibit "0.1.2"]
+ [lein-marginalia "0.7.1"]]
+ :resource-paths ["resources" "target/cljsbuild"]
+ :source-paths ["src/clj" "src/cljs" "src/cljc"]
+ :url "http://www.journeyman.cc/microworld/")
diff --git a/resources/heightmaps/20x20/crucible.png b/resources/heightmaps/20x20/crucible.png
new file mode 100644
index 0000000..a1f21a6
Binary files /dev/null and b/resources/heightmaps/20x20/crucible.png differ
diff --git a/resources/heightmaps/20x20/crucible.xcf b/resources/heightmaps/20x20/crucible.xcf
new file mode 100644
index 0000000..d3a4163
Binary files /dev/null and b/resources/heightmaps/20x20/crucible.xcf differ
diff --git a/resources/test.edn b/resources/test.edn
new file mode 100644
index 0000000..a7a8e05
--- /dev/null
+++ b/resources/test.edn
@@ -0,0 +1 @@
+{:hello "goodbye"}
\ No newline at end of file
diff --git a/src/cljc/mw_engine/core.clj b/src/cljc/mw_engine/core.clj
new file mode 100644
index 0000000..588aa25
--- /dev/null
+++ b/src/cljc/mw_engine/core.clj
@@ -0,0 +1,160 @@
+(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
+ (:require [clojure.set :refer [difference]]
+ [mw-engine.flow :refer [flow-world]]
+ [mw-engine.utils :refer [add-history-event get-int-or-zero map-world rule-type]]
+ [taoensso.timbre :as l]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; mw-engine: the state/transition engine of MicroWorld.
+;;;;
+;;;; This program is free software; you can redistribute it and/or
+;;;; modify it under the terms of the GNU General Public License
+;;;; as published by the Free Software Foundation; either version 2
+;;;; of the License, or (at your option) any later version.
+;;;;
+;;;; This program is distributed in the hope that it will be useful,
+;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;;;; GNU General Public License for more details.
+;;;;
+;;;; You should have received a copy of the GNU General Public License
+;;;; along with this program; if not, write to the Free Software
+;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+;;;; USA.
+;;;;
+;;;; Copyright (C) 2014 Simon Brooke
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(def ^:dynamic *with-history*
+ "I suspect that caching history on the cells is greatly worsening the
+ memory problems. Make it optional, but by default false."
+ false)
+
+(def known-rule-types
+ "Types of rules we know about."
+ #{:ad-hoc :flow :production})
+
+(defn apply-rule
+ "Apply a single `rule` to a `cell`. What this is about is that I want to be able,
+ for debugging purposes, to tag a cell with the rule text of the rule which
+ fired (and especially so when an exception is thrown). "
+ ;; as of version 0-3-0, metadata for rules is now passed around on the metadata
+ ;; of the rule function itself. Yes, I know, this is obvious; but I'll confess
+ ;; I didn't think of it before.
+ [world cell rule]
+ (let [result (try
+ (apply rule (list cell world))
+ (catch Exception e
+ (l/warn e
+ (format
+ "Error in `apply-rule`: `%s` (%s) while executing rule `%s` on cell `%s`"
+ e
+ (.getMessage e)
+ (-> rule meta :lisp)
+ cell))))]
+ (if *with-history*
+ (add-history-event result rule)
+ result)))
+
+(defn- apply-rules
+ "Derive a cell from this `cell` of this `world` by applying these `rules`."
+ [world cell rules]
+ (or
+ (first
+ (remove
+ nil?
+ (try
+ (map #(apply-rule world cell %) rules)
+ (catch Exception e
+ (l/warn e
+ (format
+ "Error in `apply-rules`: `%s` (%s) while executing rules on cell `%s`"
+ (-> e .getClass .getName)
+ (.getMessage e)
+ cell))))))
+ cell))
+
+(defn- transform-cell
+ "Derive a cell from this `cell` of this `world` by applying these `rules`. If an
+ exception is thrown, cache its message on the cell and set it's state to error"
+ [world cell rules]
+ (try
+ (merge
+ (apply-rules world cell rules)
+ {:generation (+ (get-int-or-zero cell :generation) 1)})
+ (catch Exception e
+ (let [narrative (format "Error in `transform-cell`: `%s` (%s) at generation %d when in state %s;"
+ (-> e .getClass .getName)
+ (.getMessage e)
+ (:generation cell)
+ (:state cell))]
+ (l/warn e narrative)
+ cell))))
+
+(defn transform-world
+ "Return a world derived from this `world` by applying the production rules
+ found among these `rules` to each cell."
+ [world rules]
+ (map-world world transform-cell
+ ;; Yes, that `list` is there for a reason!
+ (list
+ (filter
+ #(#{:ad-hoc :production} (rule-type %))
+ rules))))
+
+(defn run-world
+ "Run this `world` with these `rules` for this number of `generations`.
+
+ * `world` a world as discussed above;
+ * `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;
+ * `generations` an (integer) number of generations.
+
+ **NOTE THAT** all rules **must** be tagged with `rule-type` metadata, or they **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]
+
+ (let [found-types (map rule-type (concat init-rules rules))]
+ (if (every? known-rule-types found-types)
+ (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))
+ (let [unexpected (difference (set found-types) known-rule-types)]
+ (throw
+ (ex-info (format
+ "Unexpected rule type(s) %s found. Expected types are %s"
+ unexpected
+ known-rule-types)
+ {:types unexpected})))))))
diff --git a/src/cljc/mw_engine/display.clj b/src/cljc/mw_engine/display.clj
new file mode 100644
index 0000000..4a52445
--- /dev/null
+++ b/src/cljc/mw_engine/display.clj
@@ -0,0 +1,65 @@
+(ns ^{:doc "Simple functions to allow a world to be visualised."
+ :author "Simon Brooke"}
+ mw-engine.display)
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; mw-engine: the state/transition engine of MicroWorld.
+;;;;
+;;;; This program is free software; you can redistribute it and/or
+;;;; modify it under the terms of the GNU General Public License
+;;;; as published by the Free Software Foundation; either version 2
+;;;; of the License, or (at your option) any later version.
+;;;;
+;;;; This program is distributed in the hope that it will be useful,
+;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;;;; GNU General Public License for more details.
+;;;;
+;;;; You should have received a copy of the GNU General Public License
+;;;; along with this program; if not, write to the Free Software
+;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+;;;; USA.
+;;;;
+;;;; Copyright (C) 2014 Simon Brooke
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(def ^:dynamic *image-base*
+ "Base url (i.e., url of directory) from which to load tile images."
+ "img/tiles")
+
+(defn format-css-class
+ "Format this `state`, assumed to be a keyword indicating a state in the
+ world, into a CSS class"
+ [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]
+ (let [state (:state cell)]
+ [:td {:class (format-css-class state) :title (format-mouseover cell)}
+ [: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]
+ (apply vector
+ (cons :table
+ (map render-world-row world))))
diff --git a/src/cljc/mw_engine/drainage.clj b/src/cljc/mw_engine/drainage.clj
new file mode 100644
index 0000000..54d4be9
--- /dev/null
+++ b/src/cljc/mw_engine/drainage.clj
@@ -0,0 +1,288 @@
+(ns ^{:doc "Experimental, probably of no interest to anyone else; attempt to
+ compute drainage on a world, assumed to have altitudes already set
+ from a heightmap."
+ :author "Simon Brooke"}
+ mw-engine.drainage
+ (:require [clojure.string :refer [replace]]
+ [hiccup2.core :refer [html]]
+ [mw-engine.core :refer [run-world]]
+ [mw-engine.heightmap :refer [apply-heightmap]]
+ [mw-engine.utils :refer [get-int-or-zero get-least-cell get-neighbours
+ get-neighbours-with-property-value
+ map-world]]
+ [taoensso.timbre :refer [info]]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; mw-engine: the state/transition engine of MicroWorld.
+;;;;
+;;;; This program is free software; you can redistribute it and/or
+;;;; modify it under the terms of the GNU General Public License
+;;;; as published by the Free Software Foundation; either version 2
+;;;; of the License, or (at your option) any later version.
+;;;;
+;;;; This program is distributed in the hope that it will be useful,
+;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;;;; GNU General Public License for more details.
+;;;;
+;;;; You should have received a copy of the GNU General Public License
+;;;; along with this program; if not, write to the Free Software
+;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+;;;; USA.
+;;;;
+;;;; Copyright (C) 2014 Simon Brooke
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+
+(def ^:dynamic *sealevel* 10)
+
+;; forward declaration of flow, to allow for a wee bit of mutual recursion.
+(declare flow)
+
+(defn rainfall
+ "Compute rainfall for a cell with this `gradient` west-east, given
+ `remaining` drops to distribute, and this overall map width."
+ [gradient remaining map-width]
+ (cond
+ ;; if there's no rain left in the cloud, it can't fall;
+ (zero? remaining)
+ 0
+ (pos? gradient)
+ ;; rain, on prevailing westerly wind, falls preferentially on rising ground;
+ (int (rand gradient))
+ ;; rain falls randomly across the width of the map...
+ (zero? (int (rand map-width))) 1
+ :else
+ 0))
+
+(defn rain-row
+ "Return a row like this `row`, across which rainfall has been distributed;
+ if `rain-probability` is specified, it is the probable rainfall on a cell
+ with no gradient."
+ ([row]
+ (rain-row row 1))
+ ([row rain-probability]
+ (rain-row row (count row) 0 (int (* (count row) rain-probability))))
+ ([row map-width previous-altitude drops-in-cloud]
+ (cond
+ (empty? row) nil
+ (pos? drops-in-cloud)
+ (let [cell (first row)
+ alt (or (:altitude cell) 0)
+ rising (- alt previous-altitude)
+ fall (rainfall rising drops-in-cloud map-width)]
+ (cons
+ (assoc cell :rainfall fall)
+ (rain-row (rest row) map-width alt (- drops-in-cloud fall))))
+ :else
+ (map
+ #(assoc % :rainfall 0)
+ row))))
+
+
+(defn rain-world
+ "Simulate rainfall on this `world`. TODO: Doesn't really work just now - should
+ rain more on west-facing slopes, and less to the east of high ground"
+ [world]
+ (info "rain-world started.")
+ (let [w' (into [] (map #(apply vector (rain-row %)) world))]
+ (info "rain-world completed")
+ w'))
+
+
+(defn flow-contributors
+ "Return a list of the cells in this `world` which are higher than this
+ `cell` and for which this cell is the lowest neighbour, or which are at the
+ same altitude and have greater flow"
+ [cell world]
+ (filter #(map? %)
+ (map
+ (fn [n]
+ (cond
+ (= cell (get-least-cell (get-neighbours world n) :altitude)) n
+ (and (= (:altitude cell) (:altitude n))
+ (> (or (:flow n) 0) (or (:flow cell) 0))) n))
+ (get-neighbours-with-property-value
+ world (:x cell) (:y cell) 1 :altitude
+ (or (:altitude cell) 0) >=))))
+
+
+(defn is-hollow
+ "Detects point hollows - that is, individual cells all of whose neighbours
+ are higher. Return true if this `cell` has an altitude lower than any of
+ its neighbours in this `world`"
+ [world cell]
+ ;; quicker to count the elements of the list and compare equality of numbers
+ ;; than recursive equality check on members, I think. But worth benchmarking.
+ (let [neighbours (get-neighbours world cell)
+ altitude (get-int-or-zero cell :altitude)]
+ (= (count neighbours)
+ (count (get-neighbours-with-property-value
+ world (:x cell) (:y cell) 1 :altitude altitude >)))))
+
+
+(defn flood-hollow
+ "Raise the altitude of a copy of this `cell` of this `world` to the altitude
+ of the lowest of its `neighbours`."
+ ([_world cell neighbours]
+ (let [lowest (get-least-cell neighbours :altitude)]
+ (merge cell {:state :water :altitude (:altitude lowest)})))
+ ([world cell]
+ (flood-hollow world cell (get-neighbours world cell))))
+
+
+(defn flood-hollows
+ "Flood all local hollows in this `world`. At this stage only floods single
+ cell hollows."
+ [world]
+ (info "flood-hollows started.")
+ (let [w' (map-world world
+ #(if (is-hollow %1 %2) (flood-hollow %1 %2) %2))]
+ (info "flood-hollows completed")
+ w'))
+
+(def max-altitude 255)
+
+(defn flow-nr
+ "Experimental non recursive flow algorithm, needs to be run on a world as
+ many times as there are distinct altitude values. This algorithm works only
+ if applied sequentially from the highest altitude to the lowest, see
+ `flow-world-nr`."
+ [cell world]
+ (when (= (- max-altitude (get-int-or-zero cell :generation))
+ (get-int-or-zero cell :altitude))
+ (let [contributors (flow-contributors cell world)]
+ (when contributors
+ (merge cell
+ {:flow (reduce +
+ (map
+ #(+ (get-int-or-zero % :rainfall)
+ (get-int-or-zero % :flow))
+ contributors))})))))
+
+(def flow
+ "Compute the total flow upstream of this `cell` in this `world`, and return a cell identical
+ to this one but having a value of its flow property set from that computation. The function is
+ memoised because the consequence of mapping a recursive function across an array is that many
+ cells will be revisited - potentially many times.
+
+ Flow comes from a higher cell to a lower only if the lower is the lowest neighbour of the higher."
+ (memoize
+ (fn [cell world]
+ (cond
+ (not (nil? (:flow cell))) cell
+ (<= (or (:altitude cell) 0) *sealevel*) cell
+ :else
+ (merge cell
+ {:flow (+ (:rainfall cell)
+ (apply +
+ (map (fn [neighbour] (:flow (flow neighbour world)))
+ (flow-contributors cell world))))})))))
+
+(defn flow-world-nr
+ "Experimental non-recursive flow-world algorithm"
+ [world]
+ (info "Non recursive flow-world started.")
+ (let [w' (run-world world (list (vary-meta flow-nr assoc :rule-type :ad-hoc)) max-altitude)]
+ (info "Non recursive flow-world completed")
+ w'))
+
+(defn flow-world
+ "Return a world like this `world`, but with cells tagged with the amount of
+ water flowing through them."
+ [world]
+ (info "Recursive flow-world started.")
+ (let [w' (map-world (rain-world world) flow)]
+ (info "Recursive flow-world completed")
+ w'))
+
+(defn explore-lake
+ "Return a sequence of cells starting with this `cell` in this `world` which
+ form a contiguous lake"
+ [_world _cell])
+
+(defn is-lake?
+ "If this `cell` in this `world` is not part of a lake, return nil. If it is,
+ return a cell like this `cell` tagged as part of a lake."
+ [world cell]
+ (cond
+ ;; if it's already tagged as a lake, it's a lake
+ (:lake cell) cell
+ (#{:lake :sea :water} (:state cell)) cell ;; if it's already tagged as
+ ;; wet, no need for change
+ (and (integer? (:x cell)) (integer? (:y cell)))
+ (let
+ [outflow (apply min (map :altitude (get-neighbours world cell)))]
+ (when-not
+ (> (:altitude cell) outflow)
+ (do
+ (info (format "tagging cell at %d, %d as lake" (:x cell) (:y cell)))
+ (assoc cell :lake true :state :lake))))
+ :else (throw (ex-info "Invalid cell?"
+ {:cell cell}))))
+
+(defn identify-lake
+ [world cell]
+ (or (is-lake? world cell) cell))
+
+(defn find-lakes
+ "Identify cells in this `world` which are lakes."
+ [world]
+ (info "find-lakes started.")
+ (let [w' (map-world world identify-lake)]
+ (info "find-lakes completed.")
+ w'))
+
+(defn run-drainage
+ "Create a world from the heightmap `hmap`, rain on it, and then compute river
+ flows."
+ [hmap]
+ (find-lakes (flow-world-nr (rain-world (flood-hollows (apply-heightmap hmap))))))
+
+(defn visualise-drainage
+ [world html-file]
+
+ (let [mxf (apply max (map :flow (flatten world)))
+ scf (/ 128 mxf)
+ mxa (apply max (map :altitude (flatten world)))
+ sca (/ 128 mxa)]
+ (spit
+ html-file
+ (replace
+ (str
+ (html [:html
+ [:head
+ [:title "Drainage visualisation"]
+ [:style "table, table tr td {
+ padding: 0.5em;
+ margin: 0.2em;
+ width: 2em;
+ height: 2em;
+ border-collapse: collapse;
+ border: none;}"]]
+ [:body
+ (into [:table]
+ (map
+ #(into [:tr]
+ (map
+ (fn [c]
+ (let [g (- 255 (int (* sca (:altitude c))))]
+ [:td {:style (if (> (:altitude c) 1)
+ (let [blue (int (* scf (or (:flow c) 0)))
+ o (- g blue)]
+ (format "background-color: rgb(%d, %d, %d)"
+ o
+ o
+ (+ g blue)))
+ "background-color: cornflower-blue")
+ :title (format "state %s, x %d, y %d, rainfall %d, flow %d"
+ (:state c) (:x c) (:y c) (:rainfall c) (:flow c))}
+ (or (:rainfall c) " ")]))
+ %))
+ world))]]))
+ "&"
+ "&"))))
+
+;; (visualise-drainage (run-drainage "resources/heightmaps/20x20/crucible.png") "test.html")
\ No newline at end of file
diff --git a/src/cljc/mw_engine/flow.clj b/src/cljc/mw_engine/flow.clj
new file mode 100644
index 0000000..8b0f0d5
--- /dev/null
+++ b/src/cljc/mw_engine/flow.clj
@@ -0,0 +1,179 @@
+(ns mw-engine.flow
+ "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 [add-history-event get-cell get-num
+ in-bounds? map-world merge-cell rule-type]]
+ [taoensso.timbre :refer [info warn]]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; mw-engine: the state/transition engine of MicroWorld.
+;;;;
+;;;; This program is free software; you can redistribute it and/or
+;;;; modify it under the terms of the GNU General Public License
+;;;; as published by the Free Software Foundation; either version 2
+;;;; of the License, or (at your option) any later version.
+;;;;
+;;;; This program is distributed in the hope that it will be useful,
+;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;;;; GNU General Public License for more details.
+;;;;
+;;;; You should have received a copy of the GNU General Public License
+;;;; along with this program; if not, write to the Free Software
+;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+;;;; USA.
+;;;;
+;;;; Copyright (C) 2014 Simon Brooke
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn coordinate?
+ "Return `true` if this object `o` is a valid coordinate with respect to
+ this `world`, else `false`. Assumes square worlds."
+ [o world]
+ (try
+ (and (or (zero? o) (pos-int? o))
+ (< o (count world)))
+ (catch Exception e
+ (warn (format "Not a valid coordinate: %s; %s" o (.getMessage e)))
+ false)))
+
+(defn location?
+ "Return `true` if this object `o` is a location as defined above with respect to
+ this `world`, else `false`."
+ [o world]
+ (try
+ (and (map? o)
+ (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)))
+
+(defn flow?
+ "Return `true` if this object `o` is a flow as defined above with respect to
+ this `world`, else `false`. Assumes square worlds."
+ [o world]
+ (try
+ (and (map? o)
+ (location? (:source o) world)
+ (location? (:destination o) world)
+ (keyword? (:property o))
+ (pos? (:quantity o)))
+ (catch Exception e
+ (warn (format "Not a valid flow: %s; %s" o (.getMessage e)))
+ false)))
+
+(defn execute
+ "Return a world like this `world`, except with the quantity of the property
+ described in this `flow` object transferred from the source of that flow
+ to its destination."
+ [world flow]
+ (try
+ (let [sx (-> flow :source :x)
+ sy (-> flow :source :y)
+ source (get-cell world sx sy)
+ 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' (add-history-event
+ (assoc source p (- (source p) q))
+ (:rule flow)
+ {:direction :sent :other {:x dx :y dy} :property p :quantity q})
+ d' (add-history-event
+ (assoc dest p (+ (get-num dest p) q))
+ (:rule flow)
+ {:direction :received :other {:x sx :y sy} :property p :quantity q})]
+ (if (= q (:quantity flow))
+ (info (format "Moving %f units of %s from %d,%d to %d,%d"
+ (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"
+ (name p) sx sy dx dy (float (:quantity flow)) (float q))))
+ (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.
+ world)))
+
+(defn execute-flows
+ "Return a world like this `world`, but with each of these flows executed."
+ [world flows]
+ (reduce execute world (filter #(flow? % world) flows)))
+
+(defn- plan-cell-flows
+ [world cell rules]
+ (map ;; across all the rules
+ (fn [rule] (let [r (try
+ (apply rule (list cell world))
+ (catch Exception any
+ (throw (ex-info "Planning of flows failed"
+ (merge (meta rule) {:cell cell})
+ any))))]
+ (when r (map #(assoc % :rule rule) r))))
+ rules))
+
+(defn plan-flows
+ "Plan, but do not execute, all the flows in this `world` implied by those of
+ these `rules` (which are expected to be pre-compiled) which are
+ flow rules. Return the list of plans, as flow objects."
+ [world rules]
+ (remove nil?
+ (flatten
+ (map-world
+ world
+ plan-cell-flows
+ (list (filter #(= :flow (rule-type %)) rules))))))
+
+(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 rules)))
+
+;; building blocks for compiled flow rules
+
+(defmacro create-location
+ [cell]
+ `(select-keys ~cell [:x :y]))
+
+(defmacro create-flow-quantity
+ [source dest prop quantity]
+ `{:source (create-location ~source)
+ :destination (create-location ~dest)
+ :prop ~prop
+ :quantity ~quantity})
+
+(defmacro create-flow-fraction
+ [source dest prop fraction]
+ `(create-flow-quantity ~source ~dest ~prop
+ (* ~fraction (get-num ~source ~prop))))
+
+(defmacro create-flow-percent
+ [source dest prop percent]
+ `(create-flow-fraction ~source ~dest ~prop (/ ~percent 100)))
\ No newline at end of file
diff --git a/src/mw_engine/heightmap.clj b/src/cljc/mw_engine/heightmap.clj
similarity index 64%
rename from src/mw_engine/heightmap.clj
rename to src/cljc/mw_engine/heightmap.clj
index 403cad0..24c250d 100644
--- a/src/mw_engine/heightmap.clj
+++ b/src/cljc/mw_engine/heightmap.clj
@@ -1,16 +1,36 @@
-;; 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.
-
-(ns mw-engine.heightmap
- (:import [java.awt.image BufferedImage])
- (:use mw-engine.utils
- mw-engine.world)
- (:require [fivetonine.collage.util :as collage :only [load-image]]
- [mikera.image.core :as imagez :only [filter-image get-pixels]]
- [mikera.image.filters :as filters]))
+(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]]
+ [mikera.image.filters :as filters]
+ [mw-engine.utils :refer [get-int get-neighbours map-world]]
+ [mw-engine.world :refer [make-world]]))
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; mw-engine: the state/transition engine of MicroWorld.
+;;;;
+;;;; This program is free software; you can redistribute it and/or
+;;;; modify it under the terms of the GNU General Public License
+;;;; as published by the Free Software Foundation; either version 2
+;;;; of the License, or (at your option) any later version.
+;;;;
+;;;; This program is distributed in the hope that it will be useful,
+;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;;;; GNU General Public License for more details.
+;;;;
+;;;; You should have received a copy of the GNU General Public License
+;;;; along with this program; if not, write to the Free Software
+;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+;;;; USA.
+;;;;
+;;;; Copyright (C) 2014 Simon Brooke
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn tag-property
"Set the value of this `property` of this cell from the corresponding pixel of this `heightmap`.
@@ -22,7 +42,7 @@
* `property` the property (normally a keyword) whose value will be set on the cell.
* `heightmap` an (ideally) greyscale image, whose x and y dimensions should
exceed those of the world of which the `cell` forms part."
- ([world cell property heightmap]
+ ([_ cell property heightmap]
(tag-property cell property heightmap))
([cell property heightmap]
(merge cell
@@ -41,9 +61,9 @@
[world cell]
(let [heights (remove nil? (map :altitude (get-neighbours world cell)))
highest (cond (empty? heights) 0 ;; shouldn't happen
- true (apply max heights))
+ :else (apply max heights))
lowest (cond (empty? heights) 0 ;; shouldn't
- true (apply min heights))
+ :else (apply min heights))
gradient (- highest lowest)]
(merge cell {:gradient gradient})))
@@ -62,7 +82,7 @@
* `cell` a cell, as discussed in world.clj, q.v. Alternatively, a map;
* `heightmap` an (ideally) greyscale image, whose x and y dimensions should
exceed those of the world of which the `cell` forms part."
- ([world cell heightmap]
+ ([_ cell heightmap]
(tag-property cell :altitude heightmap))
([cell heightmap]
(tag-property cell :altitude heightmap)))
@@ -77,16 +97,16 @@
a world the size of the heightmap will be created;
* `imagepath` a file path or URL which indicates an (ideally greyscale) image file."
([world imagepath]
- (let [heightmap (imagez/filter-image
- (filters/grayscale)
- (collage/load-image imagepath))]
+ (let [heightmap (filter-image
+ (load-image imagepath)
+ (filters/grayscale))]
(map-world
(map-world world tag-altitude (list heightmap))
tag-gradient)))
([imagepath]
- (let [heightmap (imagez/filter-image
- (filters/grayscale)
- (collage/load-image imagepath))
+ (let [heightmap (filter-image
+ (load-image imagepath)
+ (filters/grayscale))
world (make-world (.getWidth heightmap) (.getHeight heightmap))]
(map-world
(map-world world tag-altitude (list heightmap))
@@ -101,7 +121,7 @@
* `property` the property of each cell whose value should be added to from the
intensity of the corresponding cell of the image."
[world imagepath property]
- (let [heightmap (imagez/filter-image
- (filters/grayscale)
- (collage/load-image imagepath))]
+ (let [heightmap (filter-image
+ (load-image imagepath)
+ (filters/grayscale))]
(map-world world tag-property (list property heightmap))))
diff --git a/src/mw_engine/natural_rules.clj b/src/cljc/mw_engine/natural_rules.clj
similarity index 69%
rename from src/mw_engine/natural_rules.clj
rename to src/cljc/mw_engine/natural_rules.clj
index 6032ca3..f695409 100644
--- a/src/mw_engine/natural_rules.clj
+++ b/src/cljc/mw_engine/natural_rules.clj
@@ -1,13 +1,35 @@
-;; 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.
+(ns ^{:doc "A set of MicroWorld rules describing a simplified natural ecosystem.
-(ns mw-engine.natural-rules
- (:use mw-engine.utils
- mw-engine.world))
+ 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?]]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; mw-engine: the state/transition engine of MicroWorld.
+;;;;
+;;;; This program is free software; you can redistribute it and/or
+;;;; modify it under the terms of the GNU General Public License
+;;;; as published by the Free Software Foundation; either version 2
+;;;; of the License, or (at your option) any later version.
+;;;;
+;;;; This program is distributed in the hope that it will be useful,
+;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;;;; GNU General Public License for more details.
+;;;;
+;;;; You should have received a copy of the GNU General Public License
+;;;; along with this program; if not, write to the Free Software
+;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+;;;; USA.
+;;;;
+;;;; Copyright (C) 2014 Simon Brooke
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; treeline at arbitrary altitude.
(def treeline 150)
@@ -25,25 +47,25 @@
(def vegetation-rules
(list
;; Randomly, birds plant tree seeds into grassland.
- (fn [cell world] (cond (and (= (:state cell) :grassland)(< (rand 10) 1))(merge cell {:state :heath})))
+ (fn [cell _] (cond (and (= (:state cell) :grassland)(< (rand 10) 1))(merge cell {:state :heath})))
;; heath below the treeline grows gradually into forest, providing browsing pressure is not to high
- (fn [cell world]
+ (fn [cell _]
(cond (and
(= (:state cell) :heath)
;; browsing limit really ought to vary with soil fertility, but...
(< (+ (get-int cell :deer)(get-int cell :sheep)) 6)
(< (get-int cell :altitude) treeline))
(merge cell {:state :scrub})))
- (fn [cell world] (cond (= (:state cell) :scrub) (merge cell {:state :forest})))
+ (fn [cell _] (cond (= (:state cell) :scrub) (merge cell {:state :forest})))
;; Forest on fertile land grows to climax
- (fn [cell world]
+ (fn [cell _]
(cond
(and
(= (:state cell) :forest)
(> (get-int cell :fertility) 10))
(merge cell {:state :climax})))
;; Climax forest occasionally catches fire (e.g. lightning strikes)
- (fn [cell world] (cond (and (= (:state cell) :climax)(< (rand lightning-probability) 1)) (merge cell {:state :fire})))
+ (fn [cell _] (cond (and (= (:state cell) :climax)(< (rand lightning-probability) 1)) (merge cell {:state :fire})))
;; Climax forest neighbouring fires is likely to catch fire
(fn [cell world]
(cond
@@ -52,7 +74,7 @@
(not (empty? (get-neighbours-with-state world (:x cell) (:y cell) 1 :fire))))
(merge cell {:state :fire})))
;; After fire we get waste
- (fn [cell world] (cond (= (:state cell) :fire) (merge cell {:state :waste})))
+ (fn [cell _] (cond (= (:state cell) :fire) (merge cell {:state :waste})))
;; And after waste we get pioneer species; if there's a woodland seed
;; source, it's going to be heath, otherwise grassland.
(fn [cell world]
@@ -66,21 +88,21 @@
(get-neighbours-with-state world (:x cell) (:y cell) 1 :forest)
(get-neighbours-with-state world (:x cell) (:y cell) 1 :climax))))))
(merge cell {:state :heath})))
- (fn [cell world]
+ (fn [cell _]
(cond (= (:state cell) :waste)
(merge cell {:state :grassland})))
;; Forest increases soil fertility
- (fn [cell world]
+ (fn [cell _]
(cond (member? (:state cell) '(:forest :climax))
- (merge cell {:fertility (+ (get-int cell :fertility) 1)})))
- ))
+ (merge cell {:fertility (+ (get-int cell :fertility) 1)})))))
+
;; rules describing herbivore behaviour
(def herbivore-rules
(list
;; if there are too many deer for the fertility of the area to sustain,
;; some die or move on.
- (fn [cell world]
+ (fn [cell _]
(cond (> (get-int cell :deer) (get-int cell :fertility))
(merge cell {:deer (get-int cell :fertility)})))
;; deer arrive occasionally at the edge of the map.
@@ -99,7 +121,7 @@
(>= n 2))
(merge cell {:deer (int (/ n 2))}))))
;; deer breed.
- (fn [cell world]
+ (fn [cell _]
(cond
(>= (get-int cell :deer) 2)
(merge cell {:deer (int (* (:deer cell) 2))})))))
@@ -108,7 +130,7 @@
(def predator-rules
(list
;; wolves eat deer
- (fn [cell world]
+ (fn [cell _]
(cond
(>= (get-int cell :wolves) 1)
(merge cell {:deer (max 0 (- (get-int cell :deer) (get-int cell :wolves)))})))
@@ -117,7 +139,7 @@
;; (cond (> (get-int cell :wolves) 8) (merge cell {:wolves 8})))
;; if there are not enough deer to sustain the get-int of wolves,
;; some wolves die or move on. (doesn't seem to be working?)
- (fn [cell world]
+ (fn [cell _]
(cond (> (get-int cell :wolves) (get-int cell :deer))
(merge cell {:wolves 0})))
;; wolves arrive occasionally at the edge of the map.
@@ -136,28 +158,27 @@
(>= n 2))
(merge cell {:wolves 2}))))
;; wolves breed.
- (fn [cell world]
+ (fn [cell _]
(cond
(>= (get-int cell :wolves) 2)
- (merge cell {:wolves (int (* (:wolves cell) 2))})))
- ))
+ (merge cell {:wolves (int (* (:wolves cell) 2))})))))
+
;; rules which initialise the world
(def init-rules
(list
;; below the waterline, we have water.
- (fn [cell world]
+ (fn [cell _]
(cond (and (= (:state cell) :new) (< (get-int cell :altitude) waterline)) (merge cell {:state :water})))
;; above the snowline, we have snow.
- (fn [cell world]
+ (fn [cell _]
(cond (and (= (:state cell) :new) (> (get-int cell :altitude) snowline)) (merge cell {:state :snow})))
;; in between, we have a wasteland.
- (fn [cell world] (cond (= (:state cell) :new) (merge cell {:state :grassland}))
- )))
+ (fn [cell _] (cond (= (:state cell) :new) (merge cell {:state :grassland})))))
+
(def natural-rules (flatten
(list
vegetation-rules
herbivore-rules
- ;; predator-rules
- )))
+ predator-rules)))
\ No newline at end of file
diff --git a/src/cljc/mw_engine/render.clj b/src/cljc/mw_engine/render.clj
new file mode 100644
index 0000000..245059f
--- /dev/null
+++ b/src/cljc/mw_engine/render.clj
@@ -0,0 +1,96 @@
+(ns mw-engine.render
+ "Render a world as HTML.
+
+ Adapted (simplified) from mw-ui.render-world; this is for visualisation, not
+ interaction."
+ ;; TODO: but possibly it would be better if there is to be a newer version of
+ ;; mw-ui, to base it on this.
+ (:require [hiccup2.core :refer [html]])
+ )
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; This program is free software; you can redistribute it and/or
+;;;; modify it under the terms of the GNU General Public License
+;;;; as published by the Free Software Foundation; either version 2
+;;;; of the License, or (at your option) any later version.
+;;;;
+;;;; This program is distributed in the hope that it will be useful,
+;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;;;; GNU General Public License for more details.
+;;;;
+;;;; You should have received a copy of the GNU General Public License
+;;;; along with this program; if not, write to the Free Software
+;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+;;;; USA.
+;;;;
+;;;; Copyright (C) 2024 Simon Brooke
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(def ^:dynamic *state-images-relative-path* "img/tiles/")
+
+(defn format-css-class
+ "Format this statekey, assumed to be a keyword indicating a state in the
+ world, into a CSS class"
+ [statekey]
+ (when statekey (name statekey)))
+
+(defn format-image-path
+ "Render this statekey, assumed to be a keyword indicating a state in the
+ world, into a path which should recover the corresponding image file."
+ [statekey]
+ (format "%s%s.png" *state-images-relative-path* (format-css-class statekey)))
+
+(defn format-mouseover [cell]
+ (str cell))
+
+(defn render-cell
+ "Render this world cell as a Hiccup table cell."
+ [cell]
+ (let [state (:state cell)]
+ [:td {:class (format-css-class state) :title (format-mouseover cell)}
+ [:img {:alt (:state cell) :src (format-image-path state)}]]))
+
+
+(defn render-world-row
+ "Render this world row as a Hiccup table row."
+ [row]
+ (into [:tr] (map render-cell row)))
+
+(defn render-world-table
+ "Render this `world` as a complete HTML table in a DIV. If
+ `state-images-relative-path` is passed, use that to override the default path."
+ ([world]
+ [:div {:class "world"}
+ (into [:table] (map render-world-row world))
+ [:p
+ (str "Generation " (:generation (first (flatten world))))]])
+ ([world state-images-relative-path]
+ (binding [*state-images-relative-path* state-images-relative-path]
+ (render-world-table world))))
+
+(defn render-world-page
+ ([world]
+ [:html
+ [:head
+ [:title "Rendered world"]
+ [:style "div.world table, div.world table tr td {
+ padding: 0;
+ margin: 0;
+ border-collapse: collapse;
+ border: none;}"]]
+ [:body
+ (render-world-table world)]])
+ ([world state-images-relative-path]
+ (binding [*state-images-relative-path* state-images-relative-path]
+ (render-world-page world))))
+
+(defn world->html-file
+ ([world output-path]
+ (spit output-path (str (html (render-world-page world)))))
+ ([world output-path state-images-relative-path]
+ (binding [*state-images-relative-path* state-images-relative-path]
+ (world->html-file world output-path))))
+
diff --git a/src/cljc/mw_engine/utils.clj b/src/cljc/mw_engine/utils.clj
new file mode 100644
index 0000000..635e0d7
--- /dev/null
+++ b/src/cljc/mw_engine/utils.clj
@@ -0,0 +1,383 @@
+(ns ^{:doc " Utility functions needed by MicroWorld and, specifically, in the
+ interpretation of MicroWorld rule."
+ :author "Simon Brooke"}
+ mw-engine.utils
+ (:require [clojure.math.combinatorics :as combo]
+ [clojure.string :refer [join]]
+ [embroidery.api :refer [pmap*]]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; mw-engine: the state/transition engine of MicroWorld.
+;;;;
+;;;; This program is free software; you can redistribute it and/or
+;;;; modify it under the terms of the GNU General Public License
+;;;; as published by the Free Software Foundation; either version 2
+;;;; of the License, or (at your option) any later version.
+;;;;
+;;;; This program is distributed in the hope that it will be useful,
+;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;;;; GNU General Public License for more details.
+;;;;
+;;;; You should have received a copy of the GNU General Public License
+;;;; along with this program; if not, write to the Free Software
+;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+;;;; USA.
+;;;;
+;;;; Copyright (C) 2014 Simon Brooke
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn member?
+ "Return 'true' if elt is a member of col, else 'false'."
+ [elt col]
+ (contains? (set col) elt))
+
+(defn get-int-or-zero
+ "Return the value of this `property` from this `map` if it is a integer;
+ otherwise return zero."
+ [map property]
+ (let [value (map property)]
+ (if (integer? value) value 0)))
+
+(defn init-generation
+ "Return a cell like this `cell`, but having a value for :generation, zero if
+ the cell passed had no integer value for generation, otherwise the value
+ taken from the cell passed. The `world` argument is present only for
+ consistency with the rule engine and is ignored."
+ [_ 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?`.
+
+ * `world` a world as defined in [world.clj](mw-engine.world.html);
+ * `x` a number which may or may not be a valid x coordinate within that world;
+ * `y` a number which may or may not be a valid y coordinate within that world."
+ {:deprecated "1.1.7"}
+ [world x y]
+ (and (>= x 0) (>= y 0) (< y (count world)) (< x (count (first world)))))
+
+(defn in-bounds?
+ "True if x, y are in bounds for this world (i.e., there is a cell at x, y)
+ else false.
+
+ * `world` a world as defined in [world.clj](mw-engine.world.html);
+ * `x` a number which may or may not be a valid x coordinate within that world;
+ * `y` a number which may or may not be a valid y coordinate within that world."
+ {:added "1.1.7"}
+ [world x y]
+ (and (>= x 0) (>= y 0) (< y (count world)) (< x (count (first world)))))
+
+(defn map-world-n-n
+ "Wholly non-parallel map world implementation; see documentation for `map-world`."
+ ([world function]
+ (map-world-n-n world function nil))
+ ([world function additional-args]
+ (into []
+ (map (fn [row]
+ (into [] (map
+ #(apply function
+ (cons world (cons % additional-args)))
+ row)))
+ world))))
+
+
+(defn map-world-p-p
+ "Wholly parallel map-world implementation; see documentation for `map-world`."
+ ([world function]
+ (map-world-p-p world function nil))
+ ([world function additional-args]
+ (into []
+ (pmap (fn [row]
+ (into [] (pmap
+ #(apply function
+ (cons world (cons % additional-args)))
+ row)))
+ world))))
+
+(defn map-world
+ "Apply this `function` to each cell in this `world` to produce a new world.
+ the arguments to the function will be the world, the cell, and any
+ `additional-args` supplied. Note that we parallel map over rows but
+ just map over cells within a row. That's because it isn't worth starting
+ a new thread for each cell, but there may be efficiency gains in
+ running rows in parallel."
+ ([world function]
+ (map-world world function nil))
+ ([world function additional-args]
+ (into []
+ (pmap* (fn [row]
+ (into [] (map
+ #(apply function
+ (cons world (cons % additional-args)))
+ row)))
+ world))))
+
+(defn get-cell
+ "Return the cell a x, y in this world, if any.
+
+ * `world` a world as defined in [world.clj](mw-engine.world.html);
+ * `x` a number which may or may not be a valid x coordinate within that world;
+ * `y` a number which may or may not be a valid y coordinate within that world."
+ [world x y]
+ (when (in-bounds? world x y)
+ (nth (nth world y) x)))
+
+(defn get-int
+ "Get the value of a property expected to be an integer from a map; if not
+ present (or not an integer) return 0.
+
+ * `map` a map;
+ * `key` a symbol or keyword, presumed to be a key into the `map`."
+ [map key]
+ (if (map? map)
+ (let [v (map key)]
+ (cond (and v (integer? v)) v
+ :else 0))
+ (throw (Exception. "No map passed?"))))
+
+(defmacro get-num
+ "Get the value of a property expected to be a number from a map; if not
+ present (or not a number) return 0.
+
+ * `map` a map;
+ * `key` a symbol or keyword, presumed to be a key into the `map`."
+ [map key]
+ `(if (map? ~map)
+ (let [~'v (~map ~key)]
+ (cond (and ~'v (number? ~'v)) ~'v
+ :else 0))
+ (throw (Exception. "No map passed?"))))
+
+(defn population
+ "Return the population of this species in this cell. Currently a synonym for
+ `get-int`, but may not always be (depending whether species are later
+ implemented as actors)
+
+ * `cell` a map;
+ * `species` a keyword representing a species which may populate that cell."
+ [cell species]
+ (get-int cell species))
+
+(def memo-get-neighbours
+ "Memoised get neighbours is more efficient when running deeply recursive
+ algorithms on the same world. But it's less efficient when running the
+ engine in its normal iterative style, because then we will rarely call
+ get naighbours on the same cell of the same world twice."
+ (memoize
+ (fn [world x y depth]
+ (remove nil?
+ (map #(get-cell world (first %) (first (rest %)))
+ (remove #(= % (list x y))
+ (combo/cartesian-product
+ (range (- x depth) (+ x depth 1))
+ (range (- y depth) (+ y depth 1)))))))))
+
+(defn get-neighbours
+ "Get the neighbours to distance depth of a cell in this world.
+
+ Several overloads:
+ * `world` a world, as described in [world.clj](mw-engine.world.html);
+ * `cell` a cell within that world
+ Gets immediate neighbours of the specified cell.
+
+ * `world` a world, as described in[world.clj](mw-engine.world.html);
+ * `cell` a cell within that world
+ * `depth` an integer representing the depth to search from the
+ `cell`
+ Gets neighbours within the specified distance of the cell.
+
+ * `world` a world, as described in[world.clj](mw-engine.world.html);
+ * `x` an integer representing an x coordinate in that world;
+ * `y` an integer representing an y coordinate in that world;
+ * `depth` an integer representing the distance from [x,y] that
+ should be searched
+ Gets the neighbours within the specified distance of the cell at
+ coordinates [x,y] in this world."
+ ([world x y depth]
+ (if (and (integer? x) (integer? y) (integer? depth))
+ (memo-get-neighbours world x y depth)
+ (throw (ex-info "get-neighbours: integer arguments expected."
+ {:x x :y y :depth depth}))))
+ ([world cell depth]
+ (memo-get-neighbours world (:x cell) (:y cell) depth))
+ ([world cell]
+ (memo-get-neighbours world (:x cell) (:y cell) 1)))
+
+(defn get-neighbours-with-property-value
+ "Get the neighbours to distance depth of the cell at x, y in this world which
+ have this value for this property.
+
+ * `world` a world, as described in [world.clj](mw-engine.world.html);
+ * `cell` a cell within that world;
+ * `depth` an integer representing the distance from [x,y] that
+ should be searched (optional);
+ * `property` a keyword representing a property of the neighbours;
+ * `value` a value of that property (or, possibly, the name of another);
+ * `op` a comparator function to use in place of `=` (optional).
+
+ It gets messy."
+ ([world x y depth property value op]
+ (filter
+ #(eval
+ (list op
+ (or (get % property) (get-int % property))
+ value))
+ (get-neighbours world x y depth)))
+ ([world x y depth property value]
+ (get-neighbours-with-property-value world x y depth property value =))
+ ([world cell depth property value]
+ (get-neighbours-with-property-value world (:x cell) (:y cell) depth
+ property value))
+ ([world cell property value]
+ (get-neighbours-with-property-value world cell 1
+ property value)))
+
+(defn get-neighbours-with-state
+ "Get the neighbours to distance depth of the cell at x, y in this world which
+ have this state.
+
+ * `world` a world, as described in [world.clj](mw-engine.world.html);
+ * `cell` a cell within that world;
+ * `depth` an integer representing the distance from [x,y] that
+ should be searched;
+ * `state` a keyword representing a state in the world."
+ ([world x y depth state]
+ (filter #(= (:state %) state) (get-neighbours world x y depth)))
+ ([world cell depth state]
+ (get-neighbours-with-state world (:x cell) (:y cell) depth state))
+ ([world cell state]
+ (get-neighbours-with-state world cell 1 state)))
+
+(defn get-least-cell
+ "Return the cell from among these `cells` which has the lowest numeric value
+ for this `property`."
+ [cells property]
+ (first (sort-by property (filter #(number? (property %)) cells))))
+
+(defn get-most-cell
+ "Return the cell from among these `cells` which has the highest numeric value
+ for this `property`."
+ [cells property]
+ (last (sort-by property (filter #(number? (property %)) cells))))
+
+(defn- set-cell-property
+ "If this `cell`s x and y properties are equal to these `x` and `y` values,
+ return a cell like this cell but with the value of this `property` set to
+ this `value`. Otherwise, just return this `cell`."
+ [cell x y property value]
+ (cond
+ (and (= x (:x cell)) (= y (:y cell)))
+ (merge cell {property value :rule "Set by user"})
+ :else cell))
+
+(defn set-property
+ "Return a world like this `world` but with the value of exactly one `property`
+ of one `cell` changed to this `value`"
+ ([world cell property value]
+ (set-property world (:x cell) (:y cell) property value))
+ ([world x y property value]
+ (apply
+ vector ;; we want a vector of vectors, not a list of lists, for efficiency
+ (map
+ (fn [row]
+ (apply
+ vector
+ (map #(set-cell-property % x y property value)
+ row)))
+ world))))
+
+(defn merge-cell
+ "Return a world like this `world`, but merge the values from this `cell` with
+ those from the cell in the world with the same co-ordinates"
+ [world cell]
+ (if (in-bounds? world (:x cell) (:y cell))
+ (map-world world
+ #(if
+ (and
+ (= (:x cell) (:x %2))
+ (= (:y cell) (:y %2)))
+ (merge %2 cell)
+ %2))
+ world))
+
+(defn rule-type
+ "Return the rule-type of this compiled `rule`."
+ [rule]
+ (:rule-type (meta rule)))
+
+(defn add-history-event
+ "If `cell` is non-nil, expect it to be a map representing a cell; add
+ to its history an an event recording the firing of this rule. If
+ `detail` is passed, treat it as a map of additional data to be
+ added to the event."
+ ([cell rule]
+ (when cell (add-history-event cell rule {})))
+ ([result rule detail]
+ (when result
+ (let [rule-meta (meta rule)
+ event {:rule (:source rule-meta)
+ :rule-type (:rule-type rule-meta)
+ :generation (get-int-or-zero
+ result
+ :generation)}
+ event' (if detail (merge event detail) event)]
+ (merge result
+ {:history (concat
+ (:history result)
+ (list event'))})))))
+
+(defn- event-narrative [event]
+ (case (:rule-type event)
+ :production (:rule event)
+ :flow (format "%s %f units of %s %s %d,%d:\n %s"
+ (name (:direction event))
+ (:quantity event)
+ (:property event)
+ (if (= :sent (:direction event)) "to" "from")
+ (:x (:other event))
+ (:y (:other event))
+ (:rule event))))
+
+(defn history-string
+ "Return the history of this `cell` as a string for presentation to the user."
+ [cell]
+ (join "\n"
+ (map #(format "%6d: %s" (:generation %) (event-narrative %))
+ (:history cell))))
+
+(defn- extend-summary [summary rs rl event]
+ (str summary
+ (if rs (format "%d-%d (%d occurances): %s\n" rs
+ (:generation event)
+ rl
+ (event-narrative event))
+ (format "%d: %s\n" (:generation event)
+ (event-narrative event)))))
+
+(defn summarise-history
+ "Return, as a string, a shorter summary of the history of this cell"
+ [cell]
+ (loop [history (rest (:history cell))
+ event (first (:history cell))
+ prev nil
+ rs nil
+ rl 0
+ summary ""]
+ (cond (nil? event) (extend-summary summary rs rl prev)
+ (= (:rule event) (:rule prev)) (recur
+ (rest history)
+ (first history)
+ event
+ (or rs (:generation event))
+ (inc rl)
+ summary)
+ :else (recur (rest history)
+ (first history)
+ event
+ nil
+ 0
+ (extend-summary summary rs (inc rl) event)))))
diff --git a/src/cljc/mw_engine/world.clj b/src/cljc/mw_engine/world.clj
new file mode 100644
index 0000000..e21a316
--- /dev/null
+++ b/src/cljc/mw_engine/world.clj
@@ -0,0 +1,112 @@
+(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]]))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;;;
+;;;; mw-engine: the state/transition engine of MicroWorld.
+;;;;
+;;;; This program is free software; you can redistribute it and/or
+;;;; modify it under the terms of the GNU General Public License
+;;;; as published by the Free Software Foundation; either version 2
+;;;; of the License, or (at your option) any later version.
+;;;;
+;;;; This program is distributed in the hope that it will be useful,
+;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+;;;; GNU General Public License for more details.
+;;;;
+;;;; You should have received a copy of the GNU General Public License
+;;;; along with this program; if not, write to the Free Software
+;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
+;;;; USA.
+;;;;
+;;;; Copyright (C) 2014 Simon Brooke
+;;;;
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+
+(defn cell?
+ "Return `true` if `obj` is a cell, as understood by MicroWorld, else `false`."
+ [obj]
+ (let [x (:x obj)
+ y (:y obj)]
+ (and (map? obj) ;; it's a map...
+ ;; TODO: it's worth checking (and this does not) that cells have the
+ ;; right co-ordinates!
+ (or (zero? x)(pos-int? x)) ;; with an x co-ordinate...
+ (or (zero? y)(pos-int? y)) ;; 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...
+ (every? cell? (flatten obj)))) ;; and every element of each of those is a 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
+ "Make a world width cells from east to west, and height cells from north to
+ south.
+
+ * `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
+ (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)]
+ (try
+ (cond (> (count (str s)) limit) (subs (name s) 0 limit)
+ :else s)
+ (catch Exception any
+ (throw (ex-info (.getMessage any)
+ {:cell cell
+ :limit limit
+ :exception-class (.getClass any)}))))))
+
+(defn format-cell
+ "Return a formatted string summarising the current state of this cell."
+ [cell]
+ (format "%10s"
+ (truncate-state cell 10)))
+
+(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.
+
+ * `world` a world as defined above."
+ [world]
+ (println)
+ (dorun
+ (map
+ #(println
+ (format-world-row %))
+ world))
+ nil)
diff --git a/src/mw_engine/core.clj b/src/mw_engine/core.clj
deleted file mode 100644
index 9b95b55..0000000
--- a/src/mw_engine/core.clj
+++ /dev/null
@@ -1,102 +0,0 @@
-;; Functions to transform a world and run rules.
-
-(ns mw-engine.core
- (:use mw-engine.utils)
- (:require [clojure.core.reducers :as r]
- [mw-engine.world :as world])
- (:gen-class))
-
-;; 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,
- for debugging purposes, to tag a cell with the rule text of the rule which
- fired (and especially so when an exception is thrown. So a rule may be either
- 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
- to access neighbours."
- ([world cell rule]
- (cond
- (ifn? rule) (apply-rule cell world rule nil)
- (seq? rule) (let [[afn src] rule] (apply-rule cell world afn src))))
- ([cell world rule source]
- (let [result (apply rule (list cell world))]
- (cond
- (and result source) (merge result {:rule source})
- true result))))
-
-(defn- apply-rules
- "Derive a cell from this `cell` of this `world` by applying these `rules`."
- [world cell rules]
- (cond (empty? rules) cell
- true (let [result (apply-rule world cell (first rules))]
- (cond result result
- true (apply-rules world cell (rest rules))))))
-
-(defn- transform-cell
- "Derive a cell from this `cell` of this `world` by applying these `rules`. If an
- exception is thrown, cache its message on the cell and set it's state to error"
- [world cell rules]
- (try
- (merge
- (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"
- (.getMessage e)
- (:generation cell)
- (:state cell))
- :state :error}))))
-
-(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)))
-
-(defn- transform-world-state
- "Consider this single argument as a map of `:world` and `:rules`; apply the rules
- to transform the world, and return a map of the new, transformed `:world` and
- these `:rules`. As a side effect, print the world."
- [state]
- (let [world (transform-world (:world state) (:rules state))]
- ;;(world/print-world world)
- {:world world :rules (:rules state)}))
-
-
-(defn run-world
- "Run this world with these rules for this number of generations.
-
- * `world` a world as discussed above;
- * `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;
- * `generations` an (integer) number of generations.
-
- Return the final generation of the world."
- [world init-rules rules generations]
- (reduce (fn [world _iteration]
- (transform-world world rules))
- (transform-world world init-rules)
- (range generations)))
-
-
diff --git a/src/mw_engine/display.clj b/src/mw_engine/display.clj
deleted file mode 100644
index 0ad65e6..0000000
--- a/src/mw_engine/display.clj
+++ /dev/null
@@ -1,38 +0,0 @@
-(ns mw-engine.display
- (:use mw-engine.utils
- mw-engine.world)
- (:require [hiccup.core :refer [html]]))
-
-(defn format-css-class [state]
- "Format this `state`, assumed to be a keyword indicating a state in the
- world, into a CSS class"
- (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 "img/tiles/%s.png" (format-css-class state)))
-
-(defn format-mouseover [cell]
- (str cell))
-
-(defn render-cell
- "Render this world cell as a Hiccup table cell."
- [cell]
- (let [state (:state cell)]
- [:td {:class (format-css-class state) :title (format-mouseover cell)}
- [: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]
- (apply vector
- (cons :table
- (map render-world-row world))))
diff --git a/src/mw_engine/drainage.clj b/src/mw_engine/drainage.clj
deleted file mode 100644
index 925b1a0..0000000
--- a/src/mw_engine/drainage.clj
+++ /dev/null
@@ -1,117 +0,0 @@
-;; Experimental, probably of no interest to anyone else; attempt to compute drainage on a world,
-;; assumed to have altitudes already set from a heighmap.
-
-(ns mw-engine.drainage
- (:use mw-engine.utils
- mw-engine.world
- mw-engine.core)
- (:require [mw-engine.heightmap :as heightmap]))
-
-(def ^:dynamic *sealevel* 10)
-
-;; forward declaration of flow, to allow for a wee bit of mutual recursion.
-(declare flow)
-
-(defn rain-world
- "Simulate rainfall on this `world`. TODO: Doesn't really work just now - should
- rain more on west-facing slopes, and less to the east of high ground"
- [world]
- (map-world world (fn [world cell] (merge cell {:rainfall 1}))))
-
-(defn flow-contributors
- "Return a list of the cells in this `world` which are higher than this
- `cell` and for which this cell is the lowest neighbour, or which are at the
- same altitude and have greater flow"
- [cell world]
- (filter #(map? %)
- (map
- (fn [n]
- (cond
- (= cell (get-least-cell (get-neighbours world n) :altitude)) n
- (and (= (:altitude cell) (:altitude n))
- (> (or (:flow n) 0) (or (:flow cell) 0))) n))
- (get-neighbours-with-property-value
- world (:x cell) (:y cell) 1 :altitude
- (or (:altitude cell) 0) >=))))
-
-(defn is-hollow
- "Detects point hollows - that is, individual cells all of whose neighbours
- are higher. Return true if this `cell` has an altitude lower than any of
- its neighbours in this `world`"
- [world cell]
- ;; quicker to count the elements of the list and compare equality of numbers
- ;; than recursive equality check on members, I think. But worth benchmarking.
- (let [neighbours (get-neighbours world cell)
- altitude (get-int-or-zero cell :altitude)]
- (= (count neighbours)
- (count (get-neighbours-with-property-value
- world (:x cell) (:y cell) 1 :altitude altitude >)))))
-
-(defn flood-hollow
- "Raise the altitude of a copy of this `cell` of this `world` to the altitude
- of the lowest of its `neighbours`."
- ([world cell neighbours]
- (let [lowest (get-least-cell neighbours :altitude)]
- (merge cell {:state :water :altitude (:altitude lowest)})))
- ([world cell]
- (flood-hollow world cell (get-neighbours world cell))))
-
-(defn flood-hollows
- "Flood all local hollows in this `world`. At this stage only floods single
- cell hollows."
- [world]
- (map-world world
- #(if (is-hollow %1 %2) (flood-hollow %1 %2) %2)))
-
-(def max-altitude 255)
-
-(defn flow-nr
- "Experimental non recursive flow algorithm, needs to be run on a world as
- many times as there are distinct altitude values. This algorithm works only
- if applied sequentially from the highest altitude to the lowest, see
- `flow-world-nr`."
- [cell world]
- (if (= (- max-altitude (get-int-or-zero cell :generation))
- (get-int-or-zero cell :altitude))
- (merge cell
- {:flow (reduce +
- (map
- #(+ (get-int-or-zero % :rainfall)
- (get-int-or-zero % :flow))
- (flow-contributors cell world)))})))
-
-(def flow
- "Compute the total flow upstream of this `cell` in this `world`, and return a cell identical
- to this one but having a value of its flow property set from that computation. The function is
- memoised because the consequence of mapping a recursive function across an array is that many
- cells will be revisited - potentially many times.
-
- Flow comes from a higher cell to a lower only if the lower is the lowest neighbour of the higher."
- (memoize
- (fn [cell world]
- (cond
- (not (nil? (:flow cell))) cell
- (<= (or (:altitude cell) 0) *sealevel*) cell
- true
- (merge cell
- {:flow (+ (:rainfall cell)
- (apply +
- (map (fn [neighbour] (:flow (flow neighbour world)))
- (flow-contributors cell world))))})))))
-
-(defn flow-world-nr
- "Experimental non-recursive flow-world algorithm"
- [world]
- (run-world world nil (list flow-nr) max-altitude))
-
-(defn flow-world
- "Return a world like this `world`, but with cells tagged with the amount of
- water flowing through them."
- [world]
- (map-world (rain-world world) flow))
-
-(defn run-drainage
- [hmap]
- "Create a world from the heightmap `hmap`, rain on it, and then compute river
- flows."
- (flow-world (rain-world (flood-hollows (heightmap/apply-heightmap hmap)))))
diff --git a/src/mw_engine/utils.clj b/src/mw_engine/utils.clj
deleted file mode 100644
index 279ec18..0000000
--- a/src/mw_engine/utils.clj
+++ /dev/null
@@ -1,273 +0,0 @@
-;; Utility functions needed by MicroWorld and, specifically, in the
-;; interpretation of MicroWorld rule.
-
-(ns mw-engine.utils
- (:require
-;; [clojure.core.reducers :as r]
- [clojure.math.combinatorics :as combo]))
-
-(defn abs
- "Surprisingly, Clojure doesn't seem to have an abs function, or else I've
- missed it. So here's one of my own. Maps natural numbers onto themselves,
- and negative integers onto natural numbers. Also maps negative real numbers
- onto positive real numbers.
-
- * `n` a number, on the set of real numbers."
- [n]
- (if (neg? n) (- 0 n) n))
-
-(defn member?
- "True if elt is a member of col."
- [elt col] (some #(= elt %) col))
-
-(defn get-int-or-zero
- "Return the value of this `property` from this `map` if it is a integer;
- otherwise return zero."
- [map property]
- (let [value (map property)]
- (if (integer? value) value 0)))
-
-(defn init-generation
- "Return a cell like this `cell`, but having a value for :generation, zero if
- the cell passed had no integer value for generation, otherwise the value
- taken from the cell passed. The `world` argument is present only for
- consistency with the rule engine and is ignored."
- [world 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.
-
- * `world` a world as defined above;
- * `x` a number which may or may not be a valid x coordinate within that world;
- * `y` a number which may or may not be a valid y coordinate within that world."
- [world x y]
- (and (>= x 0)(>= y 0)(< y (count world))(< x (count (first world)))))
-
-(defn map-world-n-n
- "Wholly non-parallel map world implementation"
- ([world function]
- (map-world-n-n world function nil))
- ([world function additional-args]
- (into []
- (map (fn [row]
- (into [] (map
- #(apply function
- (cons world (cons % additional-args)))
- row)))
- world))))
-
-(defn map-world-p-p
- "Wholly parallel map world implementation"
- ([world function]
- (map-world-p-p world function nil))
- ([world function additional-args]
- (into []
- (pmap (fn [row]
- (into [] (pmap
- #(apply function
- (cons world (cons % additional-args)))
- row)))
- world))))
-
-
-(defn map-world
- "Apply this `function` to each cell in this `world` to produce a new world.
- the arguments to the function will be the world, the cell, and any
- `additional-args` supplied. Note that we parallel map over rows but
- just map over cells within a row. That's because it isn't worth starting
- a new thread for each cell, but there may be efficiency gains in
- running rows in parallel."
- ([world function]
- (map-world world function nil))
- ([world function additional-args]
- (into []
- (pmap (fn [row]
- (into [] (map
- #(apply function
- (cons world (cons % additional-args)))
- row)))
- world))))
-
-(defn get-cell
- "Return the cell a x, y in this world, if any.
-
- * `world` a world as defined above;
- * `x` a number which may or may not be a valid x coordinate within that world;
- * `y` a number which may or may not be a valid y coordinate within that world."
- [world x y]
- (cond (in-bounds world x y)
- (nth (nth world y) x)))
-
-(defn get-int
- "Get the value of a property expected to be an integer from a map; if not present (or not an integer) return 0.
-
- * `map` a map;
- * `key` a symbol or keyword, presumed to be a key into the `map`."
- [map key]
- (cond (map? map)
- (let [v (map key)]
- (cond (and v (integer? v)) v
- true 0))
- true (throw (Exception. "No map passed?"))))
-
-(defn population
- "Return the population of this species in this cell. Currently a synonym for
- `get-int`, but may not always be (depending whether species are later
- implemented as actors)
-
- * `cell` a map;
- * `species` a keyword representing a species which may populate that cell."
- [cell species]
- (get-int cell species))
-
-(def memo-get-neighbours
- "Memoised get neighbours is more efficient when running deeply recursive
- algorithms on the same world. But it's less efficient when running the
- engine in its normal iterative style, because then we will rarely call
- get naighbours on the same cell of the same world twice."
- (memoize
- (fn [world x y depth]
- (remove nil?
- (map #(get-cell world (first %) (first (rest %)))
- (remove #(= % (list x y))
- (combo/cartesian-product
- (range (- x depth) (+ x depth 1))
- (range (- y depth) (+ y depth 1)))))))))
-
-(defn get-neighbours
- "Get the neighbours to distance depth of a cell in this world.
-
- Several overloads:
- * `world` a world, as described in world.clj;
- * `cell` a cell within that world
- Gets immediate neighbours of the specified cell.
-
- * `world` a world, as described in world.clj;
- * `cell` a cell within that world
- * `depth` an integer representing the depth to search from the
- `cell`
- Gets neighbours within the specified distance of the cell.
-
- * `world` a world, as described in world.clj;
- * `x` an integer representing an x coordinate in that world;
- * `y` an integer representing an y coordinate in that world;
- * `depth` an integer representing the distance from [x,y] that
- should be searched
- Gets the neighbours within the specified distance of the cell at
- coordinates [x,y] in this world."
- ([world x y depth]
- (remove nil?
- (map #(get-cell world (first %) (first (rest %)))
- (remove #(= % (list x y))
- (combo/cartesian-product
- (range (- x depth) (+ x depth 1))
- (range (- y depth) (+ y depth 1)))))))
- ([world cell depth]
- (memo-get-neighbours world (:x cell) (:y cell) depth))
- ([world cell]
- (get-neighbours world cell 1)))
-
-(defn get-neighbours-with-property-value
- "Get the neighbours to distance depth of the cell at x, y in this world which
- have this value for this property.
-
- * `world` a world, as described in `world.clj`;
- * `cell` a cell within that world;
- * `depth` an integer representing the distance from [x,y] that
- should be searched (optional);
- * `property` a keyword representing a property of the neighbours;
- * `value` a value of that property (or, possibly, the name of another);
- * `op` a comparator function to use in place of `=` (optional).
-
- It gets messy."
- ([world x y depth property value op]
- (filter
- #(eval
- (list op
- (or (get % property) (get-int % property))
- value))
- (get-neighbours world x y depth)))
- ([world x y depth property value]
- (get-neighbours-with-property-value world x y depth property value =))
- ([world cell depth property value]
- (get-neighbours-with-property-value world (:x cell) (:y cell) depth
- property value))
- ([world cell property value]
- (get-neighbours-with-property-value world cell 1
- property value)))
-
-(defn get-neighbours-with-state
- "Get the neighbours to distance depth of the cell at x, y in this world which
- have this state.
-
- * `world` a world, as described in `world.clj`;
- * `cell` a cell within that world;
- * `depth` an integer representing the distance from [x,y] that
- should be searched;
- * `state` a keyword representing a state in the world."
- ([world x y depth state]
- (filter #(= (:state %) state) (get-neighbours world x y depth)))
- ([world cell depth state]
- (get-neighbours-with-state world (:x cell) (:y cell) depth state))
- ([world cell state]
- (get-neighbours-with-state world cell 1 state)))
-
-(defn get-least-cell
- "Return the cell from among these `cells` which has the lowest numeric value
- for this `property`; if the property is absent or not a number, use this
- `default`"
- ([cells property default]
- (cond
- (empty? cells) nil
- true (let [downstream (get-least-cell (rest cells) property default)]
- (cond (<
- (or (property (first cells)) default)
- (or (property downstream) default)) (first cells)
- true downstream))))
- ([cells property]
- (get-least-cell cells property (Integer/MAX_VALUE))))
-
-
-(defn- set-cell-property
- "If this `cell`s x and y properties are equal to these `x` and `y` values,
- return a cell like this cell but with the value of this `property` set to
- this `value`. Otherwise, just return this `cell`."
- [cell x y property value]
- (cond
- (and (= x (:x cell)) (= y (:y cell)))
- (merge cell {property value :rule "Set by user"})
- true
- cell))
-
-(defn set-property
- "Return a world like this `world` but with the value of exactly one `property`
- of one `cell` changed to this `value`"
- ([world cell property value]
- (set-property world (:x cell) (:y cell) property value))
- ([world x y property value]
- (apply
- vector ;; we want a vector of vectors, not a list of lists, for efficiency
- (map
- (fn [row]
- (apply
- vector
- (map #(set-cell-property % x y property value)
- row)))
- world))))
-
-(defn merge-cell
- "Return a world like this `world`, but merge the values from this `cell` with
- those from the cell in the world with the same co-ordinates"
- [world cell]
- (if (in-bounds world (:x cell) (:y cell))
- (map-world world
- #(if
- (and
- (= (:x cell)(:x %2))
- (= (:y cell)(:y %2)))
- (merge %2 cell)
- %2))
- world))
diff --git a/src/mw_engine/version.clj b/src/mw_engine/version.clj
deleted file mode 100644
index d3fa41d..0000000
--- a/src/mw_engine/version.clj
+++ /dev/null
@@ -1,16 +0,0 @@
-(ns mw-engine.version
- (:gen-class))
-
-(defn get-implementation-version
- "Get the implementation version from the package of this namespace, which must
- be compiled into a class (see clojure.java.interop). See
- http://stackoverflow.com/questions/12599889/how-to-get-runtime-access-to-version-number-of-a-running-clojure-application
- TODO: doesn't work yet."
- []
- (try
- (.getImplementationVersion (.getPackage (eval 'mw-engine.version)))
- (catch Exception any "Unknown")
- ))
-
-(defn -main []
- (get-implementation-version ))
diff --git a/src/mw_engine/world.clj b/src/mw_engine/world.clj
deleted file mode 100644
index 2aa515f..0000000
--- a/src/mw_engine/world.clj
+++ /dev/null
@@ -1,85 +0,0 @@
-;; Functions to create and to print two dimensional cellular automata. Nothing in this
-;; file 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.
-
-(ns mw-engine.world
- (:use mw-engine.utils)
- (:require [clojure.string :as string :only [join]]))
-
-(defn- 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
- true (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
- true (cons (apply vector (make-world-row 0 width index))
- (make-world-rows (inc index) width height))))
-
-(defn make-world
- "Make a world width cells from east to west, and height cells from north to
- south.
-
- * `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)))
-
-(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)
- true s)))
-
-(defn format-cell
- "Return a formatted string summarising the current state of this cell."
- [cell]
- (format "%10s(%2d/%2d)"
- (truncate-state cell 10)
- (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.
-
- * `world` a world as defined above."
- [world]
- (println)
- (dorun
- (map
- #(println
- (format-world-row %))
- world))
- nil)
diff --git a/test/mw_engine/core_test.clj b/test/mw_engine/core_test.clj
index 1491239..dbe2add 100644
--- a/test/mw_engine/core_test.clj
+++ b/test/mw_engine/core_test.clj
@@ -1,24 +1,54 @@
(ns mw-engine.core-test
- (:require [clojure.test :refer :all]
- [mw-engine.core :refer :all]))
+ (:require [clojure.test :refer [deftest is testing]]
+ [mw-engine.core :refer [*with-history* apply-rule transform-world]]
+ [mw-engine.utils :refer [map-world]]
+ [mw-engine.world :refer [make-world]]))
(deftest apply-rule-test
(testing "Application of a single rule"
- (let [afn (eval
- (fn [cell world]
- (cond
- (= (:state cell) :new)
- (merge cell {:state :grassland}))))
- pair (list afn "Test source")]
- (is (nil? (apply-rule nil {:state :water} afn))
- "Rule shouldn't fire when state is wrong")
- (is (nil? (apply-rule nil {:state :water} pair))
- "Rule shouldn't fire when state is wrong")
- (is (= (:state (apply-rule nil {:state :new} afn)) :grassland)
- "Rule should fire when state is correct")
- (is (= (:state (apply-rule nil {:state :new} pair)) :grassland)
- "Rule should fire when state is correct")
- (is (nil? (:rule (apply-rule nil {:state :new} afn)))
- "No rule text if not provided")
- (is (= (:rule (apply-rule nil {:state :new} pair)) "Test source")
- "Rule text cached on cell if provided"))))
\ No newline at end of file
+ (binding [*with-history* true]
+ (let [afn (vary-meta
+ (eval
+ (fn [cell _world]
+ (cond
+ (= (:state cell) :new)
+ (merge cell {:state :grassland}))))
+ merge {:rule-type :production
+ :rule "Test source"})]
+ (is (nil? (apply-rule nil {:state :water} afn))
+ "Rule shouldn't fire when state is wrong")
+ (is (= (:state (apply-rule nil {:state :new} afn)) :grassland)
+ "Rule should fire when state is correct")
+ (is (seq? (:history (apply-rule nil {:state :new} afn)))
+ "Event cached on history of cell")))
+ (binding [*with-history* false]
+ (let [afn (vary-meta
+ (eval
+ (fn [cell _world]
+ (cond
+ (= (:state cell) :new)
+ (merge cell {:state :grassland}))))
+ merge {:rule-type :production
+ :rule "Test source"})
+ modified-cell (apply-rule nil {:state :new} afn)]
+ (is (= (:state modified-cell) :grassland)
+ "Rule should fire when state is correct")
+ (is (nil? (:history modified-cell))
+ "No event cached on history of cell")))))
+
+(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
+ :rule "Test source"})
+ world (make-world 3 3)
+ expected [[{:y 0, :state :grassland, :x 0} {:y 0, :state :grassland, :x 1} {:y 0, :state :grassland, :x 2}]
+ [{:y 1, :state :grassland, :x 0} {:y 1, :state :grassland, :x 1} {:y 1, :state :grassland, :x 2}]
+ [{:y 2, :state :grassland, :x 0} {:y 2, :state :grassland, :x 1} {:y 2, :state :grassland, :x 2}]]
+ actual (map-world (transform-world world (list afn)) (fn [_ c] (select-keys c [:x :y :state])))]
+ (is (= actual expected)))))
\ No newline at end of file
diff --git a/test/mw_engine/drainage_test.clj b/test/mw_engine/drainage_test.clj
index ba2c95a..3b26861 100644
--- a/test/mw_engine/drainage_test.clj
+++ b/test/mw_engine/drainage_test.clj
@@ -1,8 +1,8 @@
(ns mw-engine.drainage-test
- (:require [clojure.test :refer :all]
- [mw-engine.world :as world]
+ (:require [clojure.test :refer [deftest is testing]]
+ [mw-engine.drainage :refer [flood-hollow flood-hollows is-hollow]]
[mw-engine.utils :as utils]
- [mw-engine.drainage :refer :all]))
+ [mw-engine.world :as world]))
(deftest is-hollow-test
(testing "detection of hollows"
diff --git a/test/mw_engine/flow_test.clj b/test/mw_engine/flow_test.clj
new file mode 100644
index 0000000..eb5142b
--- /dev/null
+++ b/test/mw_engine/flow_test.clj
@@ -0,0 +1,85 @@
+(ns mw-engine.flow-test
+ (:require [clojure.test :refer [deftest is testing]]
+ [mw-engine.flow :refer [coordinate? create-flow-percent
+ create-location execute execute-flows flow?
+ location?]]
+ [mw-engine.utils :refer [get-cell merge-cell]]
+ [mw-engine.world :refer [make-world]]))
+
+(deftest coordinate-tests
+ (testing "coordinates"
+ (let [world (make-world 3 3)]
+ (is (not (coordinate? -1 world)) "Not a coordinate: negative")
+ (is (not (coordinate? 4 world)) "Not a coordinate: out of bounds")
+ (is (not (coordinate? 3 world)) "Not a coordinate: boundary")
+ (is (not (coordinate? :three world)) "Not a coordinate: keyword")
+ (is (not (coordinate? 3.14 world)) "Not a coordinate: floating point")
+ (is (coordinate? 0 world) "should be a coordinate: zero")
+ (is (coordinate? 1 world) "should be a coordinate: middle"))))
+
+(deftest location-tests
+ (testing "locations"
+ (let [world (make-world 3 3)
+ in1 {:x 0 :y 0}
+ in2 {:x 1 :y 2}
+ out1 {:p 0 :q 0}
+ out2 {:x -1 :y 2}]
+ (is (location? in1 world) "should be a location: top left")
+ (is (location? in2 world) "should be a location: middle bottom")
+ (is (not (location? out1 world)) "should not be a location: wrong keys")
+ (is (not (location? out2 world)) "should not be a location: negative coordinate"))))
+
+(deftest flow-tests
+ (testing "flows"
+ (let [world (make-world 3 3)
+ world' (merge-cell world {:x 0, :y 0, :state :new :q 5.3})
+ valid {:source {:x 0 :y 0}
+ :destination {:x 1 :y 1}
+ :property :q
+ :quantity 2.4}]
+ (is (flow? valid world))
+ (let [transferred (execute world' valid)
+ source-q (:q (get-cell transferred 0 0))
+ dest-q (:q (get-cell transferred 1 1))]
+ (is (= source-q 2.9))
+ (is (= dest-q 2.4)))
+ (let [valid2 {:source {:x 1 :y 1}
+ :destination {:x 0 :y 1}
+ :property :q
+ :quantity 1}
+ transferred (execute-flows world' (list valid valid2))
+ source-q (:q (get-cell transferred 0 0))
+ inter-q (:q (get-cell transferred 1 1))
+ dest-q (:q (get-cell transferred 0 1))]
+ (is (= source-q 2.9))
+ (is (= inter-q 1.4))
+ (is (= dest-q 1))))
+ (let [world (make-world 3 3)
+ world' (merge-cell world {:x 0, :y 0, :state :new :q 5.3})
+ highdemand {:source {:x 0 :y 0}
+ :destination {:x 1 :y 1}
+ :property :q
+ :quantity 7.4}
+ transferred (execute world' highdemand)
+ source-q (:q (get-cell transferred 0 0))
+ dest-q (:q (get-cell transferred 1 1))
+ sx 0.0
+ dx 5.3]
+ (is (= source-q sx) "The entire stock should have gone;")
+ (is (= dest-q dx) "Only as much as was available should have arrived."))))
+
+ (deftest creator-macro-tests
+ (testing "Creator macros"
+ (let [source {:x 1 :y 2 :q 5.7 :state :house}
+ dest {:x 3 :y 3 :q 1 :state :house}
+ prop :q]
+ (let [expected {:x 1, :y 2}
+ actual (create-location source)]
+ (is (= actual expected)))
+ (let [expected {:source {:x 1, :y 2},
+ :prop :q,
+ :quantity 1.425,
+ :destination {:x 3, :y 3}}
+ actual (create-flow-percent source dest prop 25)]
+ (is (= actual expected))
+ (is (= (:quantity actual) (* 0.25 (:q source))))))))
\ No newline at end of file
diff --git a/test/mw_engine/heightmap_test.clj b/test/mw_engine/heightmap_test.clj
index 23f63fa..dc1399d 100644
--- a/test/mw_engine/heightmap_test.clj
+++ b/test/mw_engine/heightmap_test.clj
@@ -1,9 +1,8 @@
(ns mw-engine.heightmap-test
- (:use clojure.java.io)
- (:require [clojure.test :refer :all]
- [mw-engine.heightmap :refer :all]
- [mw-engine.world :as world :only [make-world]]
- [clojure.math.combinatorics :as combo]))
+ (:require [clojure.java.io :refer [as-file]]
+ [clojure.test :refer [deftest is testing]]
+ [mw-engine.heightmap :refer [apply-heightmap apply-valuemap]]
+ [mw-engine.world :refer [make-world]]))
(deftest apply-heightmap-test
(testing "Heightmap functionality"
@@ -22,7 +21,7 @@
(is (> (apply + gradients) 0)
"At least some gradients must be positive, none should be negative"))
;; alternate means of making the world, same tests.
- (let [world (apply-heightmap (world/make-world 9 9) (as-file "resources/heightmaps/test9x9.png"))
+ (let [world (apply-heightmap (make-world 9 9) (as-file "resources/heightmaps/test9x9.png"))
altitudes (map #(:altitude %) (flatten world))
gradients (map #(:gradient %) (flatten world))]
(is (= (count world) 9) "World should be 9x9")
diff --git a/test/mw_engine/utils_test.clj b/test/mw_engine/utils_test.clj
index 0077ceb..21188fb 100644
--- a/test/mw_engine/utils_test.clj
+++ b/test/mw_engine/utils_test.clj
@@ -1,8 +1,11 @@
(ns mw-engine.utils-test
- (:use [mw-engine.world :as world])
- (:require [clojure.test :refer :all]
- [clojure.math.combinatorics :as combo]
- [mw-engine.utils :refer :all]))
+ (:require [clojure.math.combinatorics :as combo]
+ [clojure.test :refer [deftest is testing]]
+ [mw-engine.utils :refer [get-cell get-least-cell get-most-cell
+ get-neighbours
+ get-neighbours-with-property-value
+ map-world merge-cell set-property]]
+ [mw-engine.world :refer [make-world]]))
(deftest abs-test
(testing "Absolute value function"
@@ -116,8 +119,8 @@
(let [w1a (make-world 3 3)
w2b (set-property w1a (get-cell w1a 1 1) :location :centre)
w3c (set-property w2b 0 0 :location :top-left)]
- (is (= (:location (get-cell w3c 0 0) :top-left)))
- (is (= (:location (get-cell w3c 1 1) :centre)))
+ (is (= (:location (get-cell w3c 0 0)) :top-left))
+ (is (= (:location (get-cell w3c 1 1)) :centre))
(is (nil? (:location (get-cell w3c 2 2)))
"Cell at 2,2 should not have location set")
(is (= (count (remove nil? (map #(:location %) (flatten w3c)))) 2)
@@ -178,4 +181,20 @@
(is (:test (get-cell w3c 2 2))
"The cell with :test set is at 2, 2"))))
-
\ No newline at end of file
+(deftest most-least-tests
+ (let [cells [{:x 0, :y 0, :state :new, :prop 0.4406204774301924}
+ {:x 1, :y 0, :state :new, :prop 0.26475629405490275}
+ {:x 2, :y 0, :state :new, :prop 0.34018209505715813}
+ {:x 0, :y 1, :state :new, :prop 0.35104719397171424}
+ {:x 1, :y 1, :state :new, :prop 0.6009298123397215} ;; <- max
+ {:x 2, :y 1, :state :new, :prop 0.5580383897506066}
+ {:x 0, :y 2, :state :new, :prop 0.1780241365266907} ;; <- min
+ {:x 1, :y 2, :state :new, :prop 0.3255028139128574}
+ {:x 2, :y 2, :state :new, :prop 0.3449965660347397}]]
+ (let [expected {:x 1, :y 1, :state :new, :prop 0.6009298123397215}
+ actual (get-most-cell cells :prop)]
+ (is (= actual expected) "get-most-cell failed")
+ )
+ (let [expected {:x 0, :y 2, :state :new, :prop 0.1780241365266907}
+ actual (get-least-cell cells :prop)]
+ (is (= actual expected) "get-least-cell failed"))))
\ No newline at end of file
diff --git a/test/mw_engine/world_test.clj b/test/mw_engine/world_test.clj
index 7d50a3b..8a6a4f7 100644
--- a/test/mw_engine/world_test.clj
+++ b/test/mw_engine/world_test.clj
@@ -1,6 +1,6 @@
(ns mw-engine.world-test
- (:require [clojure.test :refer :all]
- [mw-engine.world :refer :all]
+ (:require [clojure.test :refer [deftest is testing]]
+ [mw-engine.world :refer [make-world]]
[clojure.math.combinatorics :as combo]))
(deftest genesis-test