From b23aae26ce4a7c0df457d46c2e7484acdb174fe4 Mon Sep 17 00:00:00 2001
From: Simon Brooke <simon@journeyman.cc>
Date: Sat, 2 Jan 2016 14:24:40 +0000
Subject: [PATCH] Commit before alph-ordering grammar.

---
 src/mw_parser/declarative.clj       |  63 +++--
 test/mw_parser/declarative_test.clj | 410 +++++++++++++++++++++++++++-
 2 files changed, 446 insertions(+), 27 deletions(-)

diff --git a/src/mw_parser/declarative.clj b/src/mw_parser/declarative.clj
index 1801b12..2a274b4 100644
--- a/src/mw_parser/declarative.clj
+++ b/src/mw_parser/declarative.clj
@@ -29,7 +29,8 @@
    DISJUNCT-EXPRESSION := IN SPACE DISJUNCT-VALUE;
    RANGE-EXPRESSION := BETWEEN SPACE NUMERIC-EXPRESSION SPACE AND SPACE NUMERIC-EXPRESSION;
    NUMERIC-EXPRESSION := VALUE | VALUE SPACE OPERATOR SPACE NUMERIC-EXPRESSION;
-   QUALIFIER := COMPARATIVE SPACE THAN | EQUIVALENCE | IS SPACE QUALIFIER;
+   NEGATED-QUALIFIER := QUALIFIER SPACE NOT | NOT SPACE QUALIFIER;
+   QUALIFIER := NEGATED-QUALIFIER | IS COMPARATIVE SPACE THAN | EQUIVALENCE | IS SPACE QUALIFIER;
    QUANTIFIER := NUMBER | SOME | NONE | ALL;
    EQUIVALENCE := IS SPACE EQUAL | EQUAL | IS ;
    COMPARATIVE := MORE | LESS;
@@ -38,6 +39,7 @@
    THEN := 'then';
    THAN := 'than';
    OR := 'or';
+   NOT := 'not';
    AND := 'and';
    SOME := 'some';
    NONE := 'no';
@@ -132,7 +134,10 @@
          qualifier (generate (nth tree 2))
          expression (generate (nth tree 3))]
      (case expression-type
-       :DISJUNCT-EXPRESSION (list 'let ['value (list property 'cell)] (list 'some (list 'fn ['i] '(= i value)) (list 'quote expression)))
+       :DISJUNCT-EXPRESSION (let [e (list 'some (list 'fn ['i] '(= i value)) (list 'quote expression))]
+                              (list 'let ['value (list property 'cell)]
+                                  (if (= qualifier '=) e
+                                    (list 'not e))))
        :RANGE-EXPRESSION (generate-ranged-property-condition tree property expression)
        (list qualifier (list property 'cell) expression)))))
 
@@ -173,26 +178,32 @@
   (if
     (coll? tree)
     (case (first tree)
-      :RULE (generate-rule tree)
-      :CONDITIONS (generate-conditions tree)
+      :ACTIONS (generate-multiple-actions tree)
+      :COMPARATIVE (generate (second tree))
       :CONDITION (generate-condition tree)
-      ;;    :NEIGHBOURS-CONDITION (generate-neighbours-condition tree)
-      :DISJUNCT-CONDITION (generate-disjunct-condition tree)
+      :CONDITIONS (generate-conditions tree)
       :CONJUNCT-CONDITION (generate-conjunct-condition tree)
+      :DISJUNCT-CONDITION (generate-disjunct-condition tree)
       :PROPERTY-CONDITION (generate-property-condition tree)
       :DISJUNCT-EXPRESSION (generate (nth tree 2))
-      :NUMERIC-EXPRESSION (generate-numeric-expression tree)
       :DISJUNCT-VALUE (generate-disjunct-value tree)
-      :SIMPLE-ACTION (generate-simple-action tree)
-      :ACTIONS (generate-multiple-actions tree)
-      :SYMBOL (keyword (second tree))
-      :NUMBER (read-string (second tree))
       :EQUIVALENCE '=
-      :MORE '>
+      :EXPRESSION (generate (second tree))
       :LESS '<
-      :COMPARATIVE (generate (second tree))
-      ;;    :EXPRESSION (generate-expression tree)
-      ;;    :SIMPLE-EXPRESSION
+      :MORE '>
+      :NEGATED-QUALIFIER (case (generate (second tree))
+                                 = 'not=
+                                 > '<
+                                 < '>)
+      ;;    :NEIGHBOURS-CONDITION (generate-neighbours-condition tree)
+      :NUMERIC-EXPRESSION (generate-numeric-expression tree)
+      :NUMBER (read-string (second tree))
+      :PROPERTY (list (generate (second tree)) 'cell) ;; dubious - may not be right
+      :QUALIFIER (generate (second tree))
+      :RULE (generate-rule tree)
+      :SIMPLE-ACTION (generate-simple-action tree)
+      :SYMBOL (keyword (second tree))
+      :VALUE (generate (second tree))
       (map generate tree))
     tree))
 
@@ -229,22 +240,24 @@
   (if
     (coll? tree)
     (case (first tree)
+      :ACTION (simplify-second-of-two tree)
+      :ACTIONS (simplify-second-of-two tree)
+      :COMPARATIVE (simplify-second-of-two tree)
+      :CONDITION (simplify-second-of-two tree)
+      :CONDITIONS (simplify-second-of-two tree)
+      :EXPRESSION (simplify-second-of-two tree)
+      :QUANTIFIER (simplify-second-of-two tree)
+      :NOT nil
+      :PROPERTY (simplify-second-of-two tree)
       :SPACE nil
       :THEN nil
-      :QUALIFIER (simplify-qualifier tree)
-      :CONDITIONS (simplify-second-of-two tree)
-      :CONDITION (simplify-second-of-two tree)
-      :EXPRESSION (simplify-second-of-two tree)
-      :COMPARATIVE (simplify-second-of-two tree)
-      :QUANTIFIER (simplify-second-of-two tree)
+      ;; :QUALIFIER (simplify-qualifier tree)
       :VALUE (simplify-second-of-two tree)
-      :PROPERTY (simplify-second-of-two tree)
-      :ACTIONS (simplify-second-of-two tree)
-      :ACTION (simplify-second-of-two tree)
       (remove nil? (map simplify tree)))
     tree))
 
 (def parse-rule
+  "Parse the argument, assumed to be a string in the correct syntax, and return a parse tree."
   (insta/parser grammar))
 
 (defn explain-parse-error-reason
@@ -277,7 +290,7 @@
   [rule]
   (assert (string? rule))
   (let [tree (simplify (parse-rule rule))]
-    (if (rule? rule) (generate tree)
+    (if (rule? tree) (eval (generate tree))
       (throw-parse-exception tree))))
 
 
diff --git a/test/mw_parser/declarative_test.clj b/test/mw_parser/declarative_test.clj
index 67b31d1..1b40eb9 100644
--- a/test/mw_parser/declarative_test.clj
+++ b/test/mw_parser/declarative_test.clj
@@ -95,7 +95,413 @@
          "Exception thrown on attempt to set 'y'")
     ))
 
-(deftest compilation-tests
-  (testing "Full compilation of rules"
+(deftest correctness-tests
+  ;; these are, in so far as possible, the same as the correctness-tests in core-tests - i.e., the two compilers
+  ;; compile the same language.
+  (testing "Simplest possible rule"
+           (let [afn (compile-rule "if state is new then state should be grassland")]
+                 (is (= (apply afn (list {:state :new} nil))
+                            {:state :grassland})
+                     "Rule fires when condition is met")
+                 (is (nil? (apply afn (list {:state :forest} nil))))
+                     "Rule doesn't fire when condition isn't met"))
 
+  (testing "Condition conjunction rule"
+           (let [afn (compile-rule "if state is new and altitude is 0 then state should be water")]
+                 (is (= (apply afn (list {:state :new :altitude 0} nil))
+                            {:state :water :altitude 0})
+                     "Rule fires when conditions are met")
+                 (is (nil? (apply afn (list {:state :new :altitude 5} nil)))
+                     "Rule does not fire: second condition not met")
+                 (is (nil?  (apply afn (list {:state :forest :altitude 0} nil)))
+                     "Rule does not fire: first condition not met")))
+
+  (testing "Condition disjunction rule"
+           (let [afn (compile-rule "if state is new or state is waste then state should be grassland")]
+                 (is (= (apply afn (list {:state :new} nil))
+                            {:state :grassland})
+                     "Rule fires: first condition met")
+                 (is (= (apply afn (list {:state :waste} nil))
+                            {:state :grassland})
+                     "Rule fires: second condition met")
+                 (is (nil?  (apply afn (list {:state :forest} nil)))
+                     "Rule does not fire: neither condition met")))
+
+  (testing "Simple negation rule"
+           (let [afn (compile-rule "if state is not new then state should be grassland")]
+                 (is (nil? (apply afn (list {:state :new} nil)))
+                     "Rule doesn't fire when condition isn't met")
+                 (is (= (apply afn (list {:state :forest} nil))
+                            {:state :grassland})
+                     "Rule fires when condition is met")))
+
+  (testing "Can't set x or y properties"
+         (is (thrown-with-msg?
+          Exception #"The properties 'x' and 'y' of a cell are reserved and should not be set in rule actions"
+          (compile-rule "if state is new then x should be 0"))
+         "Exception thrown on attempt to set 'x'")
+     (is (thrown-with-msg?
+          Exception #"The properties 'x' and 'y' of a cell are reserved and should not be set in rule actions"
+          (compile-rule "if state is new then y should be 0"))
+         "Exception thrown on attempt to set 'y'"))
+
+  (testing "Simple list membership rule"
+           (let [afn (compile-rule "if state is in heath or scrub or forest then state should be climax")]
+             (is (= (apply afn (list {:state :heath} nil))
+                    {:state :climax})
+                 "Rule fires when condition is met")
+             (is (= (apply afn (list {:state :scrub} nil))
+                    {:state :climax})
+                 "Rule fires when condition is met")
+             (is (= (apply afn (list {:state :forest} nil))
+                    {:state :climax})
+                 "Rule fires when condition is met")
+             (is (nil? (apply afn (list {:state :grassland} nil)))
+                 "Rule does not fire when condition is not met")))
+
+  (testing "Negated list membership rule"
+           (let [afn (compile-rule "if state is not in heath or scrub or forest then state should be climax")]
+             (is (nil? (apply afn (list {:state :heath} nil)))
+                 "Rule does not fire when condition is not met")
+             (is (nil? (apply afn (list {:state :scrub} nil)))
+                 "Rule does not fire when condition is not met")
+             (is (nil? (apply afn (list {:state :forest} nil)))
+                 "Rule does not fire when condition is not met")
+             (is (= (apply afn (list {:state :grassland} nil))
+                    {:state :climax})
+                 "Rule fires when condition is met")))
+
+  (testing "Property is more than numeric-value"
+           (let [afn (compile-rule "if altitude is more than 200 then state should be snow")]
+             (is (= (apply afn (list {:altitude 201} nil))
+                    {:state :snow :altitude 201})
+                 "Rule fires when condition is met")
+             (is (nil? (apply afn (list {:altitude 200} nil)))
+                 "Rule does not fire when condition is not met")))
+
+  (testing "Property is more than property"
+           (let [afn (compile-rule "if wolves are more than deer then deer should be 0")]
+             (is (= (apply afn (list {:deer 2 :wolves 3} nil))
+                    {:deer 0 :wolves 3})
+                 "Rule fires when condition is met")
+             (is (nil? (apply afn (list {:deer 3 :wolves 2} nil)))
+                 "Rule does not fire when condition is not met")))
+
+  (testing "Property is less than numeric-value"
+           (let [afn (compile-rule "if altitude is less than 10 then state should be water")]
+             (is (= (apply afn (list {:altitude 9} nil))
+                    {:state :water :altitude 9})
+                 "Rule fires when condition is met")
+             (is (nil? (apply afn (list {:altitude 10} nil)))
+                 "Rule does not fire when condition is not met")))
+
+  (testing "Property is less than property"
+           (let [afn (compile-rule "if wolves are less than deer then deer should be deer - wolves")]
+             (is (= (apply afn (list {:deer 3 :wolves 2} nil))
+                    {:deer 1 :wolves 2})
+                 "Rule fires when condition is met")
+             (is (nil? (apply afn (list {:deer 2 :wolves 3} nil)))
+                 "Rule does not fire when condition is not met")))
+
+  (testing "Number neighbours have property equal to value"
+           (let [afn (compile-rule "if 3 neighbours have state equal to new then state should be water")
+                 world (make-world 3 3)]
+             (is (= (apply afn (list {:x 0 :y 0} world))
+                    {:state :water :x 0 :y 0})
+                 "Rule fires when condition is met (in a new world all cells are new, corner cell has three neighbours)")
+             (is (nil? (apply afn (list {:x 1 :y 1} world)))
+                 "Middle cell has eight neighbours, so rule does not fire."))
+           (let [afn (compile-rule "if 3 neighbours are new then state should be water")
+                 world (make-world 3 3)]
+             ;; 'are new' should be the same as 'have state equal to new'
+             (is (= (apply afn (list {:x 0 :y 0} world))
+                    {:state :water :x 0 :y 0})
+                 "Rule fires when condition is met (in a new world all cells are new, corner cell has three neighbours)")
+             (is (nil? (apply afn (list {:x 1 :y 1} world)))
+                 "Middle cell has eight neighbours, so rule does not fire.")))
+
+  (testing "Number neighbours have property more than numeric-value"
+           (let [afn (compile-rule "if 3 neighbours have altitude more than 10 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11")
+                               (compile-rule "if x is less than 2 then altitude should be 0")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 2 :y 1} world)))
+                 "Middle cell of the strip has only two high neighbours, so rule should not fire.")))
+
+  (testing "Number neighbours have property less than numeric-value"
+           (let [afn (compile-rule "if 5 neighbours have altitude less than 10 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11")
+                               (compile-rule "if x is less than 2 then altitude should be 0")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 2 :y 1} world)))
+                 "Middle cell of the strip has two high neighbours, so rule should not fire.")))
+
+  (testing "More than number neighbours have property equal to numeric-value"
+           (let [afn (compile-rule "if more than 2 neighbours have altitude equal to 11 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11")
+                               (compile-rule "if x is less than 2 then altitude should be 0")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 2 :y 1} world)))
+                 "Middle cell of the strip has only two high neighbours, so rule should not fire.")))
+
+  (testing "More than number neighbours have property equal to symbolic-value"
+           (let [afn (compile-rule "if more than 2 neighbours have state equal to grassland then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11 and state should be grassland")
+                               (compile-rule "if x is less than 2 then altitude should be 0 and state should be water")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 2 :y 1} world)))
+                 "Middle cell of the strip has only two high neighbours, so rule should not fire."))
+           (let [afn (compile-rule "if more than 2 neighbours are grassland then state should be beach")
+                 ;; 'are grassland' should mean the same as 'have state equal to grassland'.
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11 and state should be grassland")
+                               (compile-rule "if x is less than 2 then altitude should be 0 and state should be water")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 2 :y 1} world)))
+                 "Middle cell of the strip has only two high neighbours, so rule should not fire."))
+           )
+
+  (testing "Fewer than number neighbours have property equal to numeric-value"
+           (let [afn (compile-rule "if fewer than 3 neighbours have altitude equal to 11 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11")
+                               (compile-rule "if x is less than 2 then altitude should be 0")))]
+             (is (= (:state (apply afn (list {:x 2 :y 1} world))) :beach)
+                 "Rule fires when condition is met (Middle cell of the strip has only two high neighbours)")
+             (is (nil? (apply afn (list {:x 1 :y 1} world)))
+                 "Middle cell of world has three high neighbours, so rule should not fire.")))
+
+  (testing "Fewer than number neighbours have property equal to symbolic-value"
+           (let [afn (compile-rule "if fewer than 3 neighbours have state equal to grassland then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11 and state should be grassland")
+                               (compile-rule "if x is less than 2 then altitude should be 0 and state should be water")))]
+             (is (= (:state (apply afn (list {:x 2 :y 1} world))) :beach)
+                 "Rule fires when condition is met (Middle cell of the strip has only two high neighbours)")
+             (is (nil? (apply afn (list {:x 1 :y 1} world)))
+                 "Middle cell of world has three high neighbours, so rule should not fire.")))
+
+;; some neighbours have property equal to value
+  (testing "Some neighbours have property equal to numeric-value"
+           (let [afn (compile-rule "if some neighbours have altitude equal to 11 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11")
+                               (compile-rule "if x is less than 2 then altitude should be 0")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 0 :y 1} world)))
+                 "Left hand side of world has no high neighbours, so rule should not fire.")))
+
+  (testing "Some neighbours have property equal to symbolic-value"
+           (let [afn (compile-rule "if some neighbours have state equal to grassland then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11 and state should be grassland")
+                               (compile-rule "if x is less than 2 then altitude should be 0 and state should be water")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 0 :y 1} world)))
+                 "Left hand side of world has no high neighbours, so rule should not fire.")))
+
+;; more than number neighbours have property more than numeric-value
+  (testing "More than number neighbours have property more than symbolic-value"
+           (let [afn (compile-rule "if more than 2 neighbours have altitude more than 10 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11 and state should be grassland")
+                               (compile-rule "if x is less than 2 then altitude should be 0 and state should be water")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 2 :y 1} world)))
+                 "Middle cell of the strip has only two high neighbours, so rule should not fire.")))
+
+;; fewer than number neighbours have property more than numeric-value
+  (testing "Fewer than number neighbours have property more than numeric-value"
+           (let [afn (compile-rule "if fewer than 3 neighbours have altitude more than 10 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11")
+                               (compile-rule "if x is less than 2 then altitude should be 0")))]
+             (is (= (:state (apply afn (list {:x 2 :y 1} world))) :beach)
+                 "Rule fires when condition is met (Middle cell of the strip has only two high neighbours)")
+             (is (nil? (apply afn (list {:x 1 :y 1} world)))
+                 "Middle cell of world has three high neighbours, so rule should not fire.")))
+
+;; some neighbours have property more than numeric-value
+  (testing "Some neighbours have property more than numeric-value"
+           (let [afn (compile-rule "if some neighbours have altitude more than 10 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11")
+                               (compile-rule "if x is less than 2 then altitude should be 0")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 0 :y 1} world)))
+                 "Left hand side of world has no high neighbours, so rule should not fire.")))
+
+;; more than number neighbours have property less than numeric-value
+  (testing "More than number neighbours have property less than numeric-value"
+           (let [afn (compile-rule "if more than 4 neighbours have altitude less than 10 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11")
+                               (compile-rule "if x is less than 2 then altitude should be 0")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 2 :y 1} world)))
+                 "Middle cell of the strip has only three low neighbours, so rule should not fire.")))
+
+;; fewer than number neighbours have property less than numeric-value
+  (testing "Fewer than number neighbours have property less than numeric-value"
+           (let [afn (compile-rule "if fewer than 4 neighbours have altitude less than 10 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is 2 then altitude should be 11")
+                               (compile-rule "if x is less than 2 then altitude should be 0")))]
+             (is (nil? (apply afn (list {:x 1 :y 1} world)))
+                 "Centre cell has five low neighbours, so rule should not fire")
+             (is (= (:state (apply afn (list {:x 2 :y 1} world))) :beach)
+                 "Middle cell of the strip has only three low neighbours, so rule should  fire.")))
+
+;; some neighbours have property less than numeric-value
+  (testing "Some number neighbours have property less than numeric-value"
+           (let [afn (compile-rule "if some neighbours have altitude less than 10 then state should be beach")
+                 world (transform-world
+                         (make-world 3 3)
+                         (list (compile-rule "if x is less than 2 then altitude should be 11")
+                               (compile-rule "if x is 2 then altitude should be 0")))]
+             (is (= (:state (apply afn (list {:x 1 :y 1} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 0 down right hand side)")
+             (is (nil? (apply afn (list {:x 0 :y 1} world)))
+                 "Left of world is all high, so rule should not fire.")))
+
+
+;; 'single action' already tested in 'condition' tests above
+;; action and actions
+  (testing "Conjunction of actions"
+           (let [afn (compile-rule "if state is new then state should be grassland and fertility should be 0")]
+                 (is (= (apply afn (list {:state :new} nil))
+                            {:state :grassland :fertility 0})
+                     "Both actions are executed")))
+
+;; 'property should be symbolic-value' and 'property should be numeric-value'
+;; already tested in tests above
+
+;; number chance in number property should be value
+  (testing "Syntax of probability rule - action of real probability very hard to test"
+           (let [afn (compile-rule "if state is forest then 5 chance in 5 state should be climax")]
+             (is (= (:state (apply afn (list {:state :forest} nil))) :climax)
+                 "five chance in five should fire every time"))
+           (let [afn (compile-rule "if state is forest then 0 chance in 5 state should be climax")]
+             (is (nil? (apply afn (list {:state :forest} nil)))
+                 "zero chance in five should never fire")))
+
+;; property operator numeric-value
+  (testing "Arithmetic action: addition of number"
+           (let [afn (compile-rule "if state is climax then fertility should be fertility + 1")]
+                 (is (= (:fertility
+                          (apply afn (list {:state :climax :fertility 0} nil)))
+                        1)
+                     "Addition is executed")))
+
+  (testing "Arithmetic action: addition of property value"
+           (let [afn (compile-rule "if state is climax then fertility should be fertility + leaf-fall")]
+                 (is (= (:fertility
+                          (apply afn
+                                 (list {:state :climax
+                                        :fertility 0
+                                        :leaf-fall 1} nil)))
+                        1)
+                     "Addition is executed")))
+
+  (testing "Arithmetic action: subtraction of number"
+           (let [afn (compile-rule "if state is crop then fertility should be fertility - 1")]
+                 (is (= (:fertility
+                          (apply afn (list {:state :crop :fertility 2} nil)))
+                        1)
+                     "Action is executed")))
+
+  (testing "Arithmetic action: subtraction of property value"
+           (let [afn (compile-rule "if wolves are more than 0 then deer should be deer - wolves")]
+                 (is (= (:deer
+                          (apply afn
+                                 (list {:deer 3
+                                        :wolves 2} nil)))
+                        1)
+                     "Action is executed")))
+
+  (testing "Arithmetic action: multiplication by number"
+           (let [afn (compile-rule "if deer are more than 1 then deer should be deer * 2")]
+                 (is (= (:deer
+                          (apply afn (list {:deer 2} nil)))
+                        4)
+                     "Action is executed")))
+
+  (testing "Arithmetic action: multiplication by property value"
+           (let [afn (compile-rule "if state is crop then deer should be deer * deer")]
+                 (is (= (:deer
+                          (apply afn
+                                 (list {:state :crop :deer 2} nil)))
+                        4)
+                     "Action is executed")))
+
+  (testing "Arithmetic action: division by number"
+           (let [afn (compile-rule "if wolves are more than 0 then deer should be deer / 2")]
+                 (is (= (:deer
+                          (apply afn (list {:deer 2 :wolves 1} nil)))
+                        1)
+                     "Action is executed")))
+
+  (testing "Arithmetic action: division by property value"
+           (let [afn (compile-rule "if wolves are more than 0 then deer should be deer / wolves")]
+                 (is (= (:deer
+                          (apply afn
+                                 (list {:deer 2 :wolves 2} nil)))
+                        1)
+                     "Action is executed")))
+
+;; simple within distance
+  (testing "Number neighbours within distance have property equal to value"
+           (let [afn (compile-rule "if 8 neighbours within 2 have state equal to new then state should be water")
+                 world (make-world 5 5)]
+              (is (= (apply afn (list {:x 0 :y 0} world))
+                    {:state :water :x 0 :y 0})
+                 "Rule fires when condition is met (in a new world all cells are new, corner cell has eight neighbours within two)")
+             (is (nil? (apply afn (list {:x 1 :y 1} world)))
+                 "Middle cell has twenty-four neighbours within two, so rule does not fire.")))
+
+;; comparator within distance
+  (testing "More than number neighbours within distance have property equal to symbolic-value"
+           (let [afn (compile-rule "if more than 7 neighbours within 2 have state equal to grassland and more than 7 neighbours within 2 have state equal to water then state should be beach")
+                 ;; 5x5 world, strip of high ground two cells wide down left hand side
+                 ;; xxooo
+                 ;; xxooo
+                 ;; xxooo
+                 ;; xxooo
+                 ;; xxooo
+                 world (transform-world
+                         (make-world 5 5)
+                         (list (compile-rule "if x is less than 2 then altitude should be 11 and state should be grassland")
+                               (compile-rule "if x is more than 1 then altitude should be 0 and state should be water")))]
+             (is (= (:state (apply afn (list {:x 2 :y 2} world))) :beach)
+                 "Rule fires when condition is met (strip of altitude 11 down right hand side)")
+             (is (nil? (apply afn (list {:x 0 :y 1} world)))
+                 "Middle cell of the strip has only two high neighbours, so rule should not fire."))
     ))