001 (ns ^{:doc "Functions to transform a world and run rules.
002
003 Every rule is a function of two arguments, a cell and a world. If the rule
004 fires, it returns a new cell, which should have the same values for `:x` and
005 `:y` as the old cell. Anything else can be modified.
006
007 While any function of two arguments can be used as a rule, a special high
008 level rule language is provided by the `mw-parser` package, which compiles
009 rules expressed in a subset of English rules into suitable functions.
010
011 A cell is a map containing at least values for the keys :x, :y, and :state;
012 a transformation should not alter the values of :x or :y, and should not
013 return a cell without a keyword as the value of :state. Anything else is
014 legal.
015
016 A world is a two dimensional matrix (sequence of sequences) of cells, such
017 that every cell's `:x` and `:y` properties reflect its place in the matrix.
018 See `world.clj`.
019
020 Each time the world is transformed (see `transform-world`), for each cell,
021 rules are applied in turn until one matches. Once one rule has matched no
022 further rules can be applied to that cell."
023 :author "Simon Brooke"}
024 mw-engine.core
025 (:require [mw-engine.flow :refer [flow-world]]
026 [mw-engine.utils :refer [add-history-event get-int-or-zero map-world rule-type]]
027 [taoensso.timbre :as l]))
028
029 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
030 ;;;;
031 ;;;; mw-engine: the state/transition engine of MicroWorld.
032 ;;;;
033 ;;;; This program is free software; you can redistribute it and/or
034 ;;;; modify it under the terms of the GNU General Public License
035 ;;;; as published by the Free Software Foundation; either version 2
036 ;;;; of the License, or (at your option) any later version.
037 ;;;;
038 ;;;; This program is distributed in the hope that it will be useful,
039 ;;;; but WITHOUT ANY WARRANTY; without even the implied warranty of
040 ;;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
041 ;;;; GNU General Public License for more details.
042 ;;;;
043 ;;;; You should have received a copy of the GNU General Public License
044 ;;;; along with this program; if not, write to the Free Software
045 ;;;; Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301,
046 ;;;; USA.
047 ;;;;
048 ;;;; Copyright (C) 2014 Simon Brooke
049 ;;;;
050 ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
051
052 (def ^:dynamic *with-history*
053 "I suspect that caching history on the cells is greatly worsening the
054 memory problems. Make it optional, but by default false."
055 false)
056
057 (defn apply-rule
058 "Apply a single `rule` to a `cell`. What this is about is that I want to be able,
059 for debugging purposes, to tag a cell with the rule text of the rule which
060 fired (and especially so when an exception is thrown). "
061 ;; as of version 0-3-0, metadata for rules is now passed around on the metadata
062 ;; of the rule function itself. Yes, I know, this is obvious; but I'll confess
063 ;; I didn't think of it before.
064 [world cell rule]
065 (let [result (try
066 (apply rule (list cell world))
067 (catch Exception e
068 (l/warn e
069 (format
070 "Error in `apply-rule`: `%s` (%s) while executing rule `%s` on cell `%s`"
071 e
072 (.getMessage e)
073 (-> rule meta :lisp)
074 cell))))]
075 (if *with-history*
076 (add-history-event result rule)
077 result)))
078
079 (defn- apply-rules
080 "Derive a cell from this `cell` of this `world` by applying these `rules`."
081 [world cell rules]
082 (or
083 (first
084 (remove
085 nil?
086 (try
087 (map #(apply-rule world cell %) rules)
088 (catch Exception e
089 (l/warn e
090 (format
091 "Error in `apply-rules`: `%s` (%s) while executing rules on cell `%s`"
092 (-> e .getClass .getName)
093 (.getMessage e)
094 cell))))))
095 cell))
096
097 (defn- transform-cell
098 "Derive a cell from this `cell` of this `world` by applying these `rules`. If an
099 exception is thrown, cache its message on the cell and set it's state to error"
100 [world cell rules]
101 (try
102 (merge
103 (apply-rules world cell rules)
104 {:generation (+ (get-int-or-zero cell :generation) 1)})
105 (catch Exception e
106 (let [narrative (format "Error in `transform-cell`: `%s` (%s) at generation %d when in state %s;"
107 (-> e .getClass .getName)
108 (.getMessage e)
109 (:generation cell)
110 (:state cell))]
111 (l/warn e narrative)
112 cell))))
113
114 (defn transform-world
115 "Return a world derived from this `world` by applying the production rules
116 found among these `rules` to each cell."
117 [world rules]
118 (map-world world transform-cell
119 ;; Yes, that `list` is there for a reason!
120 (list
121 (filter
122 #(= :production (rule-type %))
123 rules))))
124
125 (defn run-world
126 "Run this world with these rules for this number of generations.
127
128 * `world` a world as discussed above;
129 * `init-rules` a sequence of rules as defined above, to be run once to initialise the world;
130 * `rules` a sequence of rules as defined above, to be run iteratively for each generation;
131 * `generations` an (integer) number of generations.
132
133 **NOTE THAT** all rules **must** be tagged with `rule-type` metadata, or thet **will not**
134 be executed.
135
136 Return the final generation of the world."
137 ([world rules generations]
138 (run-world world rules rules (dec generations)))
139 ([world init-rules rules generations]
140 (reduce (fn [world iteration]
141 (l/info "Running iteration " iteration)
142 (let [w' (transform-world world rules)]
143 (flow-world w' rules)))
144 (transform-world world init-rules)
145 (range generations))))