Trying to address bit-rot

This commit is contained in:
Simon Brooke 2021-12-09 20:02:33 +00:00
parent 2f2463da0e
commit 2cb3a6af6f
No known key found for this signature in database
GPG key ID: A7A4F18D1D4DF987
10 changed files with 717 additions and 637 deletions

8
.gitignore vendored
View file

@ -8,3 +8,11 @@ pom.xml
.lein-failures
eastwood.txt
.clj-kondo/
.lsp/
.project
.settings/
.nrepl-port
.classpath

File diff suppressed because one or more lines are too long

View file

@ -1,22 +1,23 @@
(defproject mw-engine "0.1.6-SNAPSHOT"
:dependencies [[org.clojure/clojure "1.10.3"]
[org.clojure/clojurescript "1.10.896" :scope "provided"]
[org.clojure/math.combinatorics "0.1.6"]
[org.clojure/tools.trace "0.7.11"]
[org.clojure/tools.namespace "1.1.1"]
[com.taoensso/timbre "5.1.2"]
[fivetonine/collage "0.3.0"]
[hiccup "1.0.5"]
[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.8.0"]
[org.clojure/math.combinatorics "0.0.7"]
[org.clojure/tools.trace "0.7.8"]
[org.clojure/tools.namespace "0.2.4"]
[com.taoensso/timbre "4.10.0"]
[fivetonine/collage "0.2.0"]
[hiccup "1.0.5"]
[net.mikera/imagez "0.3.1"]])
:min-lein-version "2.0.0"
:plugins [[lein-cljsbuild "1.1.7"]
[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/"
)

134
src/cljc/mw_engine/core.clj Normal file
View file

@ -0,0 +1,134 @@
(ns ^{:doc "Functions to transform a world and run rules."
:author "Simon Brooke"}
mw-engine.core
(:require [clojure.core.reducers :as r]
[clojure.string :refer [join]]
[mw-engine.world :as world]
[mw-engine.utils :refer [get-int-or-zero map-world]]
[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
;;;;
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;;;
;;;; 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 world cell rule nil)
(seq? rule) (let [[afn src] rule] (apply-rule world cell afn src))))
([world cell rule source]
(let [result (apply rule (list cell world))]
(cond
(and result source) (merge result {:rule source})
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))
:stacktrace (map #(.toString %) (.getStackTrace e))
: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]
(l/info "Running iteration " iteration)
(transform-world world rules))
(transform-world world init-rules)
(range generations)))

View file

@ -1,9 +1,7 @@
(ns ^{:doc "Functions to apply a heightmap to a world."
:author "Simon Brooke"}
mw-engine.heightmap
(:import [java.awt.image BufferedImage])
(:require [fivetonine.collage.util :as collage :only [load-image]]
[mikera.image.core :as imagez :only [filter-image get-pixels]]
(:require [mikera.image.core :refer [load-image filter-image get-pixels]]
[mikera.image.filters :as filters]
[mw-engine.utils :refer [abs get-int get-neighbours map-world]]
[mw-engine.world :refer [make-world]]))
@ -67,9 +65,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})))
@ -106,16 +104,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
(let [heightmap (filter-image
(filters/grayscale)
(collage/load-image imagepath))]
(load-image imagepath))]
(map-world
(map-world world tag-altitude (list heightmap))
tag-gradient)))
([imagepath]
(let [heightmap (imagez/filter-image
(let [heightmap (filter-image
(filters/grayscale)
(collage/load-image imagepath))
(load-image imagepath))
world (make-world (.getWidth heightmap) (.getHeight heightmap))]
(map-world
(map-world world tag-altitude (list heightmap))
@ -131,7 +129,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
(let [heightmap (filter-image
(filters/grayscale)
(collage/load-image imagepath))]
(load-image imagepath))]
(map-world world tag-property (list property heightmap))))

View file

@ -62,16 +62,27 @@
(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 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."
{: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 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."
{: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]
@ -126,7 +137,7 @@
* `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)
(when (in-bounds world x y)
(nth (nth world y) x)))
@ -136,11 +147,11 @@
* `map` a map;
* `key` a symbol or keyword, presumed to be a key into the `map`."
[map key]
(cond (map? map)
(if (map? map)
(let [v (map key)]
(cond (and v (integer? v)) v
true 0))
true (throw (Exception. "No map passed?"))))
(throw (Exception. "No map passed?"))))
(defn population
@ -256,11 +267,11 @@
([cells property default]
(cond
(empty? cells) nil
true (let [downstream (get-least-cell (rest cells) property default)]
:else (let [downstream (get-least-cell (rest cells) property default)]
(cond (<
(or (property (first cells)) default)
(or (property downstream) default)) (first cells)
true downstream))))
:else downstream))))
([cells property]
(get-least-cell cells property (Integer/MAX_VALUE))))
@ -273,8 +284,7 @@
(cond
(and (= x (:x cell)) (= y (:y cell)))
(merge cell {property value :rule "Set by user"})
true
cell))
:else cell))
(defn set-property

View file

@ -1,7 +1,7 @@
(ns ^{:doc "Functions to create and to print two dimensional cellular automata."
:author "Simon Brooke"}
mw-engine.world
(:require [clojure.string :as string :only [join]]
(:require [clojure.string :as string]
[mw-engine.utils :refer [population]]))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;