From 257d306d5d76c18578ed31ef478bacb0f4ad18f0 Mon Sep 17 00:00:00 2001 From: Simon Brooke Date: Fri, 4 Jul 2014 22:24:45 +0100 Subject: [PATCH] Now have the tiles working properly. Also, user interface somewhat improved. --- project.clj | 2 +- resources/public/css/standard.css | 18 +- resources/public/docs/mw-engine/uberdoc.html | 3459 ++++++++++++++++++ resources/public/docs/mw-parser/uberdoc.html | 3153 ++++++++++++++++ resources/public/docs/mw-ui/uberdoc.html | 3234 ++++++++++++++++ resources/public/img/tiles/climax.png | Bin 0 -> 921 bytes resources/public/img/tiles/fire.png | Bin 0 -> 977 bytes resources/public/img/tiles/forest.png | Bin 0 -> 937 bytes resources/public/img/tiles/grassland.png | Bin 0 -> 163 bytes resources/public/img/tiles/heath.png | Bin 0 -> 855 bytes resources/public/img/tiles/scrub.png | Bin 0 -> 687 bytes resources/public/img/tiles/snow.png | Bin 0 -> 782 bytes resources/public/img/tiles/waste.png | Bin 0 -> 1098 bytes resources/public/img/tiles/water.png | Bin 0 -> 621 bytes resources/templates/about.html | 2 - resources/templates/base.html | 6 +- resources/templates/docs.html | 14 + resources/templates/home.html | 1 - resources/templates/world.html | 2 - src/mw_ui/render_world.clj | 6 +- src/mw_ui/routes/home.clj | 12 +- 21 files changed, 9891 insertions(+), 18 deletions(-) create mode 100644 resources/public/docs/mw-engine/uberdoc.html create mode 100644 resources/public/docs/mw-parser/uberdoc.html create mode 100644 resources/public/docs/mw-ui/uberdoc.html create mode 100644 resources/public/img/tiles/climax.png create mode 100644 resources/public/img/tiles/fire.png create mode 100644 resources/public/img/tiles/forest.png create mode 100644 resources/public/img/tiles/grassland.png create mode 100644 resources/public/img/tiles/heath.png create mode 100644 resources/public/img/tiles/scrub.png create mode 100644 resources/public/img/tiles/snow.png create mode 100644 resources/public/img/tiles/waste.png create mode 100644 resources/public/img/tiles/water.png create mode 100644 resources/templates/docs.html diff --git a/project.clj b/project.clj index a6d081b..deb1e5c 100644 --- a/project.clj +++ b/project.clj @@ -1,5 +1,5 @@ (defproject mw-ui "0.1.0-SNAPSHOT" - :description "FIXME: write description" + :description "Web-based user interface for MicroWorld" :url "http://example.com/FIXME" :dependencies [[org.clojure/clojure "1.6.0"] [mw-engine "0.1.0-SNAPSHOT"] diff --git a/resources/public/css/standard.css b/resources/public/css/standard.css index 45138dc..92a6f5c 100644 --- a/resources/public/css/standard.css +++ b/resources/public/css/standard.css @@ -9,13 +9,14 @@ body { width:80%; margin: 0 10%; padding: 0; - padding-top: 8em; - padding-bottom: 2em; + padding-top: 12em; + padding-bottom: 2em; } /* footer of the document, within #main-container */ #footer { clear: both; + font-size: smaller; padding: 0 2em; text-align: center; color:white; @@ -33,15 +34,18 @@ body { #header { width:100%; margin: -10px; - padding: 0.5em 10%; + padding: 0.25em 10%; position: fixed; z-index: 149; background-color: black; - background-image: "../img/earth-space-strip.jpg"; background-repeat: no-repeat; color: white; } +#header h1 { + background-color: transparent; +} + #header-logo { float: left; padding-right: 2.5em; @@ -80,6 +84,10 @@ li.nav-item a:active { background: gray; color: white; } border: thin solid white; } +.world { + font-size: 8pt; +} + div.error { width: 100%; } @@ -97,7 +105,7 @@ h1 { h1, h2, h3, h4, h5 { background-color: black; color: white; - padding-left: 20px; + padding-left: -20px; } diff --git a/resources/public/docs/mw-engine/uberdoc.html b/resources/public/docs/mw-engine/uberdoc.html new file mode 100644 index 0000000..8f84d66 --- /dev/null +++ b/resources/public/docs/mw-engine/uberdoc.html @@ -0,0 +1,3459 @@ + +mw-engine -- Marginalia

mw-engine

0.1.0-SNAPSHOT


Cellular automaton world builder.

+

dependencies

org.clojure/clojure
1.5.1
org.clojure/math.combinatorics
0.0.7
net.mikera/imagez
0.3.1
fivetonine/collage
0.2.0



(this space intentionally left almost blank)
 

Functions to transform a world and run rules.

+
+
(ns mw-engine.core
+  (:require [mw-engine.world :as world]
+        mw-engine.natural-rules
+        mw-engine.utils))

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.

+ +

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.

+ +

Rules are applied in turn until one matches.

+

Derive a cell from this cell of this world by applying these rules.

+
(defn- transform-cell
+  [cell world rules]
+  (cond (empty? rules) cell
+    true (let [result (apply (eval (first rules)) (list cell world))]
+           (cond result result
+             true (transform-cell cell world (rest rules))))))

Return a row derived from this row of this world by applying these rules to each cell.

+
(defn- transform-world-row
+  [row world rules]
+  (map #(transform-cell % world rules) row))

Return a world derived from this world by applying these rules to each cell.

+
(defn transform-world
+  [world rules]
+  (map
+    #(transform-world-row % world rules)
+    world))

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.

+
(defn- transform-world-state
+  [state]
+  (let [world (transform-world (:world state) (:rules state))]
+    (world/print-world world)
+    {:world world :rules (:rules state)}))

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 definied above, to be run iteratively for each generation;
  • +
  • generations an (integer) number of generations.
  • +
+
(defn run-world
+  [world init-rules rules generations]
+  (let [state {:world (transform-world world init-rules) :rules rules}]
+    (take generations (iterate transform-world-state state))))

(defn animate-world + "Run this world with these rules for this number of generations, and return nil + to avoid cluttering the screen. Principally for debugging.

+
    +
  • 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 definied above, to be run iteratively for each generation;
  • +
  • generations an (integer) number of generations." +[world init-rules rules generations] +(let [state (list (transform-world world init-rules) rules)] +(dorun + (take generations (iterate transform-world-state state))) +state))
  • +
+
 

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
+        ;; interestingly the imagez load-image is failing for me, while the
+        ;; collage version is problem free.
+        [mikera.image.core :only [filter-image get-pixels]]
+        [mikera.image.filters]
+        [fivetonine.collage.util]))

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, but I don't care so much about them.

+ +
    +
  • n a number, on the set of real numbers.
  • +
+
(defn- abs 
+  [n]
+  (cond (< n 0) (- 0 n) true n))

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 and + it's ALL YOUR FAULT.

+ +
    +
  • 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.
  • +
+
(defn transform-altitude
+  [cell heightmap]
+  (merge cell
+         {:altitude
+          (+ (get-int cell :altitude)
+           (- 256
+              (abs
+               (mod
+                (.getRGB heightmap
+                         (get-int cell :x)
+                         (get-int cell :y)) 256))))}))

Set the altitude of each cell in this sequence from the corresponding pixel + of this heightmap. + If the heightmap you supply is smaller than the world, this will break and + it's ALL YOUR FAULT.

+ +
    +
  • row a row in a world, as discussed in world.clj, q.v. Alternatively, a + sequence of maps;
  • +
  • heightmap an (ideally) greyscale image, whose x and y dimensions should + exceed those of the world of which the cell forms part.
  • +
+
(defn- apply-heightmap-row
+  [row heightmap]
+  (apply vector (map #(transform-altitude %1 heightmap) row)))

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.

+ +
    +
  • world a world, as defined in world.clj, q.v.;
  • +
  • imagepath a file path or URL which indicates an image file.
  • +
+
(defn apply-heightmap
+  [world imagepath]
+  ;; bizarrely, the collage load-util is working for me, but the imagez version isn't.
+  (let [heightmap (filter-image (grayscale)(load-image imagepath))]
+    (apply vector (map #(apply-heightmap-row %1 heightmap) world))))
 

A set of MicroWorld rules describing a simplified natural ecosystem.

+
+
(ns mw-engine.natural-rules
+  (:use mw-engine.utils
+        mw-engine.world))

treeline at arbitrary altitude.

+
(def treeline 150)

waterline also at arbitrary altitude.

+
(def waterline 10)

and finally snowline is also arbitrary.

+
(def snowline 200)

Rare chance of lightning strikes

+
(def lightning-probability 500)

rules describing vegetation

+
(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})))
+    ;; heath below the treeline grows gradually into forest, providing browsing pressure is not to high
+    (fn [cell world]
+      (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})))
+    ;; Forest on fertile land grows to climax
+    (fn [cell world]
+      (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})))
+    ;; Climax forest neighbouring fires is likely to catch fire
+    (fn [cell world]
+      (cond
+        (and (= (:state cell) :climax)
+             (< (rand 3) 1)
+             (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})))
+    ;; 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]
+      (cond
+        (and (= (:state cell) :waste)
+             (not
+               (empty?
+                 (flatten
+                   (list
+                     (get-neighbours-with-state world (:x cell) (:y cell) 1 :scrub)
+                     (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]
+      (cond (= (:state cell) :waste)
+        (merge cell {:state :grassland})))
+    ;; Forest increases soil fertility
+    (fn [cell world]
+      (cond (member? (:state cell) '(:forest :climax))
+        (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]
+      (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.
+    (fn [cell world]
+      (cond (and (< (count (get-neighbours world cell)) 8)
+                 (< (rand 50) 1)
+                 (> (get-int cell :fertility) 0)
+                 (= (get-int cell :deer) 0))
+        (merge cell {:deer 2})))
+    ;; deer gradually spread through the world by breeding or migrating.
+    (fn [cell world]
+      (let [n (apply + (map #(get-int % :deer) (get-neighbours world cell)))]
+        (cond (and
+                (> (get-int cell :fertility) 0)
+                (= (get-int cell :deer) 0)
+                (>= n 2))
+          (merge cell {:deer (int (/ n 2))}))))
+    ;; deer breed.
+    (fn [cell world]
+      (cond
+        (>= (get-int cell :deer) 2)
+        (merge cell {:deer (int (* (:deer cell) 2))})))))

rules describing predator behaviour

+
  (def predator-rules
+    (list
+     ;; wolves eat deer
+     (fn [cell world]
+      (cond
+       (>= (get-int cell :wolves) 1)
+       (merge cell {:deer (max 0 (- (get-int cell :deer) (get-int cell :wolves)))})))
+;;      ;; not more than eight wolves in a pack, for now (hack because wolves are not dying)
+;;      (fn [cell world]
+;;        (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]
+       (cond (> (get-int cell :wolves) (get-int cell :deer))
+         (merge cell {:wolves 0})))
+    ;; wolves arrive occasionally at the edge of the map.
+    (fn [cell world]
+      (cond (and (< (count (get-neighbours world cell)) 8)
+                 (< (rand 50) 1)
+                 (not (= (:state cell) :water))
+                 (= (get-int cell :wolves) 0))
+        (merge cell {:wolves 2})))
+    ;; wolves gradually spread through the world by breeding or migrating.
+    (fn [cell world]
+      (let [n (apply + (map #(get-int % :wolves) (get-neighbours world cell)))]
+        (cond (and
+                (not (= (:state cell) :water))
+                (= (get-int cell :wolves) 0)
+                (>= n 2))
+          (merge cell {:wolves 2}))))
+    ;; wolves breed.
+    (fn [cell world]
+      (cond
+        (>= (get-int cell :wolves) 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]
+       (cond (and (= (:state cell) :new) (< (get-int cell :altitude) waterline)) (merge cell {:state :water})))
+     ;; above the snowline, we have snow.
+     (fn [cell world]
+       (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 :waste})))))
+
(def natural-rules (flatten
+                    (list
+                     vegetation-rules
+                     herbivore-rules
+                     ;; predator-rules)))
 

Utility functions needed by MicroWorld and, specifically, in the interpretation of MicroWorld rule.

+
+
(ns mw-engine.utils
+  (:require [clojure.math.combinatorics :as combo]))

True if elt is a member of col.

+
(defn member?
+  [elt col] (some #(= elt %) col))

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
+  [world x y]
+  (and (>= x 0)(>= y 0)(< y (count world))(< x (count (first 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]
+  (cond (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]
+  (cond map
+    (let [v (map key)]
+      (cond (and v (integer? v)) v
+            true 0))
+        true (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))
+
(defn get-neighbours
+  ([world x y depth]
+    "Get the neighbours to distance depth of the cell at x, y in this world.
+    * `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."
+     (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] 
+    "Get the neighbours to distance depth of this cell in this world.
+    * `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."
+    (get-neighbours world (:x cell) (:y cell) depth))
+   ([world cell]
+    "Get the immediate neighbours of this cell in this world
+    * `world` a world, as described in world.clj;
+    * `cell` a cell within that world."
+    (get-neighbours world cell 1)))

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

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

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.
  • +
+
(defn- make-cell
+  [x y]
+  {:x x :y y :state :new})

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.
  • +
+
(defn- make-world-row
+  [index width height]
+  (cond (= index width) nil
+    true (cons (make-cell index height)
+               (make-world-row (+ index 1) width height))))
+
(defn- make-world-rows [index width height]
+  "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."
+  (cond (= index height) nil
+    true (cons (make-world-row 0 width index)
+               (make-world-rows (+ index 1) width height))))

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.
  • +
+
(defn make-world
+  [width height]
+  (make-world-rows 0 width height))

Truncate the print name of the state of this cell to at most limit characters.

+
(defn truncate-state
+  [cell limit]
+  (let [s (:state cell)]
+    (cond (> (count (.toString s)) 10) (subs s 0 10)
+      true s)))

Return a formatted string summarising the current state of this cell.

+
(defn format-cell
+  [cell]
+  (format "%10s(%2d/%2d)"
+          (truncate-state cell 10)
+          (population cell :deer)
+          (population cell :wolves)))

Format one row in the state of a world for printing.

+
(defn- format-world-row
+  [row]
+  (apply str
+         (map format-cell row)))

Print the current state of this world, and return nil.

+ +
    +
  • world a world as defined above.
  • +
+
(defn print-world
+  [world]
+  (println)
+  (dorun
+    (map
+      #(println
+         (format-world-row %))
+      world))
+  nil)
 
\ No newline at end of file diff --git a/resources/public/docs/mw-parser/uberdoc.html b/resources/public/docs/mw-parser/uberdoc.html new file mode 100644 index 0000000..acd9ee8 --- /dev/null +++ b/resources/public/docs/mw-parser/uberdoc.html @@ -0,0 +1,3153 @@ + +mw-parser -- Marginalia

mw-parser

0.1.0-SNAPSHOT


Parser for production rules for MicroWorld engine

+

dependencies

org.clojure/clojure
1.5.1
mw-engine
0.1.0-SNAPSHOT



(this space intentionally left almost blank)
 

A very simple parser which parses production rules of the following forms:

+ +
    +
  • "if altitude is less than 100 and state is forest then state should be climax and deer should be 3"
  • +
  • "if altitude is 100 or fertility is 25 then state should be heath and fertility should be 24.3"
  • +
  • "if altitude is 100 or fertility is 25 then state should be heath"
  • +
  • "if deer is more than 2 and wolves is 0 and fertility is more than 20 then deer should be deer + 2"
  • +
  • "if deer is more than 1 and wolves is more than 1 then deer should be deer - wolves"
  • +
+ +

It should also but does not yet parse rules of the form:

+
    +
  • "if 6 neighbours have state is water then state should be fishery"
  • +
  • "if state is forest or state is climax and some neighbours have state is fire then 3 in 5 chance that state should be fire"
  • +
  • "if state is pasture and more than 3 neighbours have state is scrub then state should be scrub"
  • +
+ +

it generates rules in the form expected by mw-engine.core

+
+
(ns mw-parser.core
+  (:use mw-engine.utils
+        [clojure.string :only [split triml]]))
+
(declare parse-conditions)
+(declare parse-not-condition)
+(declare parse-simple-condition)

a regular expression which matches string representation of numbers

+
(def re-number #"^[0-9.]*$")

Parse '[property] is less than [value]'.

+
(defn parse-less-condition
+  [[property is less than value & rest]]
+  (cond (and (member? is '("is" "are")) (= less "less") (= than "than"))
+        [(list '< (list 'get-int 'cell (keyword property)) (read-string value)) rest]))

Parse '[property] is more than [value]'.

+
(defn parse-more-condition
+  [[property is more than value & rest]]
+  (cond (and (member? is '("is" "are")) (= more "more") (= than "than"))
+        [(list '> (list 'get-int 'cell (keyword property)) (read-string value)) rest]))

Parse clauses of the form 'x is y', but not 'x is more than y' or 'x is less than y'. + It is necessary to disambiguate whether value is a numeric or keyword.

+
(defn parse-is-condition
+  [[property is value & rest]]
+  (cond (and (member? is '("is" "are"))
+             (not (member? value '("more" "less" "exactly" "not"))))
+        [(cond
+          (re-matches re-number value)(list '= (list 'get-int 'cell (keyword property)) (read-string value))
+          true (list '= (list (keyword property) 'cell) (keyword value)))
+         rest]))

Parse the negation of a simple condition.

+
(defn parse-not-condition 
+  [[property is not & rest]]
+  (cond (and (member? is '("is" "are")) (= not "not"))
+        (let [partial (parse-simple-condition (cons property (cons is rest)))]
+          (cond partial
+                (let [[condition remainder] partial]
+                  [(list 'not condition) remainder])))))

Parse conditions of the form '[property] [comparison] [value]'.

+
(defn parse-simple-condition
+  [tokens]
+  (or (parse-is-condition tokens)
+      (parse-not-condition tokens)
+      (parse-less-condition tokens)
+      (parse-more-condition tokens)))

Parse '... or [condition]' from tokens, where left is the already parsed first disjunct.

+
(defn parse-disjunction-condition
+  [left tokens]
+  (let [partial (parse-conditions tokens)]
+    (if
+       partial
+           (let [[right remainder] partial]
+             [(list 'or left right) remainder]))))

Parse '... and [condition]' from tokens, where left is the already parsed first conjunct.

+
(defn parse-conjunction-condition
+  [left tokens]
+  (let [partial (parse-conditions tokens)]
+    (if partial
+           (let [[right remainder] partial]
+             [(list 'and left right) remainder]))))

Parse conditions from tokens, where conditions may be linked by either 'and' or 'or'.

+
(defn parse-conditions
+  [tokens]
+  (let [partial (parse-simple-condition tokens)]
+    (if partial
+           (let [[left [next & remainder]] partial]
+             (cond
+              (= next "and") (parse-conjunction-condition left remainder)
+              (= next "or") (parse-disjunction-condition left remainder)
+              true partial)))))

Parse the left hand side ('if...') of a production rule.

+
(defn parse-left-hand-side
+  [tokens]
+  (if
+   (= (first tokens) "if")
+   (parse-conditions (rest tokens))))

Parse actions of the form '[property] should be [property] [arithmetic-operator] [value]', + e.g. 'fertility should be fertility + 1', or 'deer should be deer - wolves'.

+
(defn parse-arithmetic-action 
+  [previous [prop1 should be prop2 operator value & rest]]
+  (if (and (= should "should")
+           (= be "be")
+           (member? operator '("+" "-" "*" "/")))
+    [(list 'merge (or previous 'cell)
+           {(keyword prop1) (list (symbol operator) (list 'get-int 'cell (keyword prop2))
+                                  (cond
+                                     (re-matches re-number value) (read-string value)
+                                     true (list 'get-int 'cell (keyword value))))}) rest]))

Parse actions of the form '[property] should be [value].'

+
(defn parse-set-action 
+  [previous [property should be value & rest]]
+  (if (and (= should "should") (= be "be"))
+    [(list 'merge (or previous 'cell)
+           {(keyword property) (cond (re-matches re-number value) (read-string value) true (keyword value))}) rest]))
+
(defn parse-simple-action [previous tokens]
+    (or (parse-arithmetic-action previous tokens)
+        (parse-set-action previous tokens)))

Parse actions from tokens.

+
(defn parse-actions
+  [previous tokens]
+  (let [[left remainder] (parse-simple-action previous tokens)]
+    (cond left
+          (cond (= (first remainder) "and")
+                (parse-actions left (rest remainder))
+                true (list left)))))

Parse the right hand side ('then...') of a production rule.

+
(defn parse-right-hand-side
+  [tokens]
+  (if (= (first tokens) "then")
+    (parse-actions nil (rest tokens))))

Parse a complete rule from this string or sequence of string tokens.

+
(defn parse-rule 
+  [line]
+  (cond
+   (string? line) (parse-rule (split (triml line) #"\s+"))
+   true (let [[left remainder] (parse-left-hand-side line)
+              [right junk] (parse-right-hand-side remainder)]
+          ;; there shouldn't be any junk (should be null)
+          (list 'fn ['cell 'world] (list 'if left right)))))
 
\ No newline at end of file diff --git a/resources/public/docs/mw-ui/uberdoc.html b/resources/public/docs/mw-ui/uberdoc.html new file mode 100644 index 0000000..296aaea --- /dev/null +++ b/resources/public/docs/mw-ui/uberdoc.html @@ -0,0 +1,3234 @@ + +mw-ui -- Marginalia

mw-ui

0.1.0-SNAPSHOT


Web-based user interface for MicroWorld

+

dependencies

org.clojure/clojure
1.6.0
mw-engine
0.1.0-SNAPSHOT
mw-parser
0.1.0-SNAPSHOT
lib-noir
0.8.4
ring-server
0.3.1
selmer
0.6.8
com.taoensso/timbre
3.2.1
com.taoensso/tower
2.0.2
markdown-clj
0.9.44
environ
0.5.0
noir-exception
0.2.2



(this space intentionally left almost blank)
 
+
(ns mw-ui.handler
+  (:require [compojure.core :refer [defroutes]]
+            [mw-ui.routes.home :refer [home-routes]]
+            [mw-ui.middleware :refer [load-middleware]]
+            [noir.response :refer [redirect]]
+            [noir.util.middleware :refer [app-handler]]
+            [compojure.route :as route]
+            [taoensso.timbre :as timbre]
+            [taoensso.timbre.appenders.rotor :as rotor]
+            [selmer.parser :as parser]
+            [environ.core :refer [env]]))
+
(defroutes app-routes
+  (route/resources "/")
+  (route/not-found "Not Found"))

init will be called once when + app is deployed as a servlet on + an app server such as Tomcat + put any initialization code here

+
(defn init
+  []
+  (timbre/set-config!
+    [:appenders :rotor]
+    {:min-level :info
+     :enabled? true
+     :async? false ; should be always false for rotor
+     :max-message-per-msecs nil
+     :fn rotor/appender-fn})
+  (timbre/set-config!
+    [:shared-appender-config :rotor]
+    {:path "mw_ui.log" :max-size (* 512 1024) :backlog 10})
+  (if (env :dev) (parser/cache-off!))
+  (timbre/info "mw-ui started successfully"))

destroy will be called when your application + shuts down, put any clean up code here

+
(defn destroy
+  []
+  (timbre/info "mw-ui is shutting down..."))
+
(def app (app-handler
+           ;; add your application routes here
+           [home-routes app-routes]
+           ;; add custom middleware here
+           :middleware (load-middleware)
+           ;; timeout sessions after 30 minutes
+           :session-options {:timeout (* 60 30)
+                             :timeout-response (redirect "/")}
+           ;; add access rules here
+           :access-rules []
+           ;; serialize/deserialize the following data formats
+           ;; available formats:
+           ;; :json :json-kw :yaml :yaml-kw :edn :yaml-in-html
+           :formats [:json-kw :edn]))
 
+
(ns mw-ui.layout
+  (:require [selmer.parser :as parser]
+            [clojure.string :as s]
+            [ring.util.response :refer [content-type response]]
+            [compojure.response :refer [Renderable]]))
+
(def template-path "templates/")
+
(deftype RenderableTemplate [template params]
+  Renderable
+  (render [this request]
+    (content-type
+      (->> (assoc params
+                  (keyword (s/replace template #".html" "-selected")) "active"
+                  :servlet-context
+                  (if-let [context (:servlet-context request)]
+                    (.getContextPath context)))
+        (parser/render-file (str template-path template))
+        response)
+      "text/html; charset=utf-8")))
+
(defn render [template & [params]]
+  (RenderableTemplate. template params))
 
+
(ns mw-ui.middleware
+  (:require [taoensso.timbre :as timbre]
+            [selmer.parser :as parser]
+            [environ.core :refer [env]]
+            [selmer.middleware :refer [wrap-error-page]]
+            [noir-exception.core
+              :refer [wrap-internal-error wrap-exceptions]]))
+
(defn log-request [handler]
+  (fn [req]
+    (timbre/debug req)
+    (handler req)))
+
(def development-middleware
+  [log-request
+   wrap-error-page
+   wrap-exceptions])
+
(def production-middleware
+  [#(wrap-internal-error % :log (fn [e] (timbre/error e)))])
+
(defn load-middleware []
+  (concat (when (env :dev) development-middleware)
+          production-middleware))
 
+
(ns mw-ui.render-world
+  (:require [mw-engine.core :as engine]
+            [mw-engine.world :as world]
+            [mw-engine.heightmap :as heightmap]
+            [mw-engine.natural-rules :as rules]
+            [hiccup.core :refer [html]]
+            [noir.session :as session]))
+
(defn format-css-class [statekey]
+  "Format this statekey, assumed to be a keyword indicating a state in the
+   world, into a CSS class"
+  (subs (str statekey) 1))

Render this statekey, assumed to be a keyword indicating a state in the + world, into a path which should recover the corresponding image file.

+
(defn format-image-path
+  [statekey]
+  (format "img/tiles/%s.png" (format-css-class statekey)))

Render this world cell as a Hiccup table cell.

+
(defn render-cell
+  [cell]
+  (let [state (:state cell)]
+    [:td {:class (format-css-class state)}
+            [:img {:alt (world/format-cell cell) :img (format-image-path state)}]]))

Render this world row as a Hiccup table row.

+
(defn render-world-row
+  [row]
+  (apply vector (cons :tr (map render-cell row))))

Render the world implied by the session as a complete HTML page.

+
(defn render-world-table
+  []
+  (let [world (or (session/get :world)
+                  (engine/transform-world
+                   (heightmap/apply-heightmap
+                    (world/make-world 20 20)
+                    "resources/public/img/20x20/hill.png")
+                   rules/init-rules))
+        rules (or (session/get :rules) rules/natural-rules)
+        generation (+ (or (session/get :generation) 0) 1)
+        w2 (engine/transform-world world rules)]
+    (session/put! :world w2)
+    (session/put! :generation generation)
+    [:div {:class "world"}
+     [:p (str "Generation " generation)]
+      (apply vector
+                 (cons :table
+                       (map render-world-row w2)))]))

Render the world implied by the session as a complete HTML page.

+
(defn render-world
+  []
+  (html
+   [:html
+    [:head
+     [:title "MicroWorld demo"]
+     [:link {:media "only screen and (max-device-width: 480px)" :href "css/phone.css" :type  "text/css" :rel "stylesheet"}]
+     [:link {:media "only screen and (min-device-width: 481px) and (max-device-width: 1024px)" :href "css/tablet.css" :type "text/css" :rel "stylesheet"}]
+     [:link {:media "screen and (min-device-width: 1025px)" :href "css/standard.css" :type "text/css" :rel "stylesheet"}]
+     [:link {:media "print" :href "css/print.css" :type "text/css" :rel "stylesheet"}]
+     [:link {:href "css/states.css" :type "text/css" :rel "stylesheet"}]
+     [:meta {:http-equiv "refresh" :content "5"}]]
+    [:body
+     [:h1 "MicroWorld"]
+     (render-world-table)
+     ]]))
 
+
(ns mw-ui.repl
+  (:use mw-ui.handler
+        ring.server.standalone
+        [ring.middleware file-info file]))
+
(defonce server (atom nil))
+
(defn get-handler []
+  ;; #'app expands to (var app) so that when we reload our code,
+  ;; the server is forced to re-resolve the symbol in the var
+  ;; rather than having its own copy. When the root binding
+  ;; changes, the server picks it up without having to restart.
+  (-> #'app
+      ; Makes static assets in $PROJECT_DIR/resources/public/ available.
+      (wrap-file "resources")
+      ; Content-Type, Content-Length, and Last Modified headers for files in body
+      (wrap-file-info)))

used for starting the server in development mode from REPL

+
(defn start-server
+  [& [port]]
+  (let [port (if port (Integer/parseInt port) 3000)]
+    (reset! server
+            (serve (get-handler)
+                   {:port port
+                    :init init
+                    :auto-reload? true
+                    :destroy destroy
+                    :join? false}))
+    (println (str "You can view the site at http://localhost:" port))))
+
(defn stop-server []
+  (.stop @server)
+  (reset! server nil))
 
+
(ns mw-ui.routes.home
+  (:use compojure.core)
+  (:require [hiccup.core :refer [html]]
+            [mw-ui.layout :as layout]
+            [mw-ui.util :as util]
+            [mw-ui.render-world :as world]
+            [noir.session :as session]))
+
(defn home-page []
+  (layout/render
+    "home.html" {:title "Welcome to MicroWorld" :content (util/md->html "/md/docs.md")}))
+
(defn about-page []
+  (layout/render "about.html" {:title "About MicroWorld" :content (util/md->html "/md/about.md")}))
+
(defn world-page []
+  (layout/render "world.html" {:title "Watch your world grow" :content (html (world/render-world-table)) :seconds (or (session/get :seconds) 5) :maybe-refresh "refresh"}))
+
(defroutes home-routes
+  (GET "/" [] (home-page))
+  (GET "/about" [] (about-page))
+  (GET "/world" [] (world-page)))
 
+
(ns mw-ui.util
+  (:require [noir.io :as io]
+            [markdown.core :as md]))

reads a markdown file from public/md and returns an HTML string

+
(defn md->html
+  [filename]
+  (->>
+    (io/slurp-resource filename)
+    (md/md-to-html-string)))
 
\ No newline at end of file diff --git a/resources/public/img/tiles/climax.png b/resources/public/img/tiles/climax.png new file mode 100644 index 0000000000000000000000000000000000000000..03f027343d4b7c5d9662c8f5c1e3721b1274d54d GIT binary patch literal 921 zcmV;K17`e*P)>!@1W$29 zy7=C+p`8z}Uw!?2INC?HjLtSHIX%EcHS1dFopGnAd%y_AS=AK-(REyIpG0+roexja zu+^NUS1_E#%!6ZYfKowq$e=3`Toxr9GDhD%)r)<9`tE&xd&m7M*Uj_DMZr4zrDJx9 zF{tjv@Tq&$UYY5N(y=FrM$Pl z-u#YQ_nN^zqs{kWmBHx*8z@8WS>}e*5)YA4>)dfrUn!Sh*LUyYy?JC`#fdQ}Bo*SY z0dZhZ_vU3_34IH}Ll=Y;!a3O2)Gnp4-x+2%`|1=@=dFybp!L)sU;u28ilO33 zcV%o;z=li}I=BzAi-jK_@LBL*@poJOaGi2X07(+DAu3%k1Ve=sC;~`|Hi%5ddb=-P zpW1Fa9)N#^m9dBeJx~FV(uInqXcDN>6_I+Fk9PW7u70nv5sulzk?a@>YowzYdK6Dc z6=85p+>ch5xYUrQVBybtDt%(#7H<=Bm}rY5HwV)yP|50cMX)38cf z4{7UiN1f?W04UKSwy5()5_yvQD;W>r@hW0c7cXmjvE43DV|hM=&#{;NZj}b5r0UX9 zS8-7^valr8FH(3i%bic!7Wf#pAHC20_~jWd&d|!pMyYuZ*IkGaM7ta+nZ+M;dz}Jo z9Jh`>>AthbCdLK2PZ`k^D2Yf6AHdxcp$H=oV7PVnX!!N)7Jj&pQ z8g6Yz&bH;25yj>-bRepjX!Dp|26Je7oGc^@;jkR+JU<@3{o#-L=JwCq@M)XQURTdk v)5$_ID7ymgYEo9d%${;TVm{C3$Mxxdk|;XiEn*Cf00000NkvXXu0mjfC7GBo literal 0 HcmV?d00001 diff --git a/resources/public/img/tiles/fire.png b/resources/public/img/tiles/fire.png new file mode 100644 index 0000000000000000000000000000000000000000..e4537beb3182a0416fc69dc538360788ba12627a GIT binary patch literal 977 zcmV;?11|iDP)Fj%aUL7o*{%lkyNEfC|L0` zSWyM6Sn!Ww0abw{WL~ys#+GG0Tdm7E^4ssD$JJnJ7PY!^pwZ)nym_gSMJz{`=!7Cj zevt0T*uCE6=8uEDAENoGu|NGR)0vWJp{!bSvt{QsKE}>TPc=J1BFSqGo?)ae#YQ?B z4?fmXjXSX{Q%)`UL?6exQ4TqOePmU0rF=OB)!Q|@M`DLdFbTowQQG@XoLn>SWC<6J zuv80e&V9pO$-}j1Pc~y$uvrmzL{5E-chW{f*L#=$Nn~~Em_%5^!ZR^68Ed!^@*TiD zqdSxRHSwOA!4JBH+z0!1L1>VcP~j`J4M|UCSVo+UbY_SH63E8HR))YxSQwE();0Fk z3!FieL(gMF-Gd=r5KWkjW$t8&EJThR%LyjN3#d4uHDZ7HRj!`vY$1cC2>RGjSh2_S zDVm~!fjE%$WP!wCCiD?jNSwH@-*Pg>?&ypUJsbw|5IbA{sI*62G)*W~E_?lV5@swFAyo{vu4#I zm!e0q-H^BZUCL&qGm$S2JipLyk4YT6!{9DoU;>5JWGTz3EI;SyJ=ZyjByr$jkN5KG8CNP-2G{iB8>%2p!)MCbT#vrOx0L@;SNKHo3h(i?9%|_-=?3ks zTpST4V!!{vt#7pYM0vx*1HKn8vMZ(9GpZQZxSm0&I}R01OWkr1v!B|3QfFVjPV0hE zu4zvB$ZDheO11}mC?pI_g^QTWlr+(HkZsAKVqeO&MC*&xJ;P^u0000PbVXQnLvL+uWo~o;Lvm$dbY)~9 zcWHEJAV*0}P*;Ht7XSbQBS}O-R5;58!MTnkMHB_ld*el9W-UEa)ih`%W&}b44~a1` zkZ^D@aPas9eu?iwLc&3_0Mm4LEfE><7S7@8-@bbI@Zj@EH{$B+W7pNVg&b1;IA^5I zY1yBj?bp3Ev~ycqn>c^|(HE~ie<{ztTk5&4*D)>{7O2D}YZBa`WhkF^u=%>+pe|~&+ z*dIis_OqIjZindXZ|Yw%earXjyLaa|PkO59+S$!KvfBthyRAOFYZ|ghE`<|p(R4!; zWG(rR_kXUv#o}p(bvN@EK>`67++wJn+Ih8gl9DxZ0@uiMF1Jfoj?q-l1_^FK0US|* z05brFVqDD^AJfPtR7z!jR$>!*!HmM%s}2hbE=W6a0zi^50^y8E2nkbXg*W;&c#C|v zJ7AUT)A`}?vGx`gU?3$HaD%iGDq-dtAX6{}!-7UhlJ{S4rRIRPrxwmwIM}#8Qh#4^>s6?V8MM@!=;ubTe zJGsld)Bc^lJ>zV%+U)5Fdkb{Lis-=ALlVtF1i{22d7h@(XI0DkI~ZAq5A!I_J-C1Z zkkKQQKp>F?1VFmD(R=j&c%SQ2Ejr~ouckhMXHdwDQ~;*v@Zm^EGxMC6GSg_P#?e#F z=kj>7>_5TbVN8$uh#n*(Jb(nI5OcR?7^%!<-WG?`LoL)T>vml8<+wX+E}M;QE`G^O zZLF;|1_-7Q1gK1%gvgwnmKfnFzc{`)olZXcvMdKnJ8o_Dt+%K4*t=3W+%OnU@5m#I zglCO{JSF-0*PpPmKdEb6rt>AAuJyvqvYDKgill|^uHBqr81p<8PFZBP*`?!r{O0BB z<9u5m*6ZJ`)qX{LTF-SFdgVkDW!BRnB3T$9ABQWQl7;iF1B#Zfaf$gL6@8Vo7R>LV0FMhJw4NZ$Nk>pEyvF ztfz}(h{fsTDff;Zb-g|EsNl3K=|I{an^LB{Ts5 D4pu5) literal 0 HcmV?d00001 diff --git a/resources/public/img/tiles/heath.png b/resources/public/img/tiles/heath.png new file mode 100644 index 0000000000000000000000000000000000000000..eba54f5c519aa6e93aeef3716dac4bad7301596c GIT binary patch literal 855 zcmV-d1E~CoP)R5;5e(%Eg~MiccM@YE&+@dLcpls*%$G?x;%WR5Hl9WbhYMa~$2sbQ*hT?Ep zYh1_U_5JHeh3mdV7weLbuMe4|P8>|dZfjXIHx_ZQxph}cgskl}4sOQal;`J7f(zT$ zCPC^#$=kfoafL=mG(ZC&5EW6fuJQc-0Ay?y1S*0xd~Q>Y8vs)aHtz37aV}eHtdJ^B zgih+PaxJd0y-NTntXH}OH^P3lc#SzYkxFzSHqGry$z`$ZIv(%PECx=K{e81Z;xI1b z;FJIWfR%6}|f4#!WEwMz@`Tjk05eH)v0$>qz6?J15H5O7gH2^gL z0rIuawS_Ja4&5LlODK_klheo~mu4H~FOf|I>4gPe*AV;UF?XhKXbXvm@f#N-sW^}f3<;2=&y z7fR+vEaf5qXzYe^b%c(pHLmyln!~9@_QM+8bs0&ax-NC4Bu=zBB($+Z;<~v@geZXr_WSyzy_Xd?g+RjkdI#kJ*iYq#_f(um_SPQZec`n-Q4 zs4ZO^@R5;5`lueH8HVlNnA}PBkK`xQ|-z}S9rrVNOS=cXl zgrJQevVKVF?|=S^2FS_rc1?D1o!SF6cVtr+vE4z3cH+Q6MU#_U)b#mHO*%NgJzsPx z7dF{wI~$9Z*TMDD3JP<)eSUMiIe(Ddp)QUOEfZj9!o)@f@%ds8dQiEDCYqY$bWJ+l z!8}CLdR}88qs9rQ%PqzDJlq^~(S-vi*#Kd7(xMXqV2GyPoF7*g-RxowusZlZ)O6zH zN`TsEgII%k(o)TX4yheY2l4~2Cf1We$qudqHEQ;ti!~^qF2+S&L}Ouyg&K)HSc!uN z(O`=n?9?hM7w5NM|C0Nue0v^^e*@pi8tl*>>|`J8r0%lE$9dxv4q_qb5c7e0A5J^8 z6EzBIe4O*4nu|`{$OzQ9=)giv!0|?HtO=v3F0L;?BZ=+UAX<+xm|RZ^85Ysxw5;nz zT`yU-=t3=-nqUwZ2{u}k5i~eZLby}4i<*xfcLO>&-e}`s1u3-1MJ{7@u~YMlH7JYN zD>a0000PbVXQnLvL+uWo~o;Lvm$dbY)~9 zcWHEJAV*0}P*;Ht7XSbPh)G02R5;5mkj-)&WfVgt-P=8}9Rn$-;{6|tB|m|JIPrjI zy1y&2NOq^RIiNis@1u>NgGOJYs{Im z10`5a48$Y=DU?Wp5;nqtm%X?qBH`d%$GufDn6+jlAQ8qxN|d0YC{@C3I6;ho7tj0g z_SzC+P@?P!jEqBuOfx~`DvCfUHqcP)^Z4?y1ClI(EzmH7z(FxUCI$$IpuV<-2?@3 zEd!{c3M$&iy1iUq|LM=Kzdk+uyw}sVH!}Cz-^r`}+_iD*?j}jWF_mfozFn`^FCSly zYyb7kI&T%>eHv+gzEk2`TdXYo}PQ#L?&?7tT~U{=a-jH|K6^3_UG$$ zox?fc58LIteZOq={Q2c?Y0hIla2_8YOYMx0`)$qrcHGZNaeH_9AHHni_lOM6;s5{u M07*qoM6N<$f+loYod5s; literal 0 HcmV?d00001 diff --git a/resources/public/img/tiles/waste.png b/resources/public/img/tiles/waste.png new file mode 100644 index 0000000000000000000000000000000000000000..0f6b6a44ce8c20c9c902d9607a3949f4e234e699 GIT binary patch literal 1098 zcmV-Q1hxB#P)iV=bU*s?6n z$D@b6StJ(g+j)`zV3n%i@4x-(`>|9V51H-;nak7Zhxt2O(&h#`6fFq-k_KiNi2BpN>f~zkPRu5yJb!%O$%;TvU}h zolkAoVb{eNDw+j8)ii| z4wl8t4+5nWgh24p zDj7#YYXu>kPNo)E2%zWF%{s!?*zN9F)rt$@AP4~fz%J&CcR&6#4qcjFrqekK*)_j* zUFTqTy}1=3-~4bhpGBB>lz1cv0>(J7;Cf9drJLJ#Wts1GJI8U19c3-81@G_f@9y8n zBHnD)e&7qv@agSwpEEa~;KP+nUh?0k7*i2trC}+qK`me|(Pv!>xMzN-@oMf)uV(`GRR)Z+VgM}O{%J53{_GAQ~sHqkmFzoq1JlrdSHR;dU=tR zvTVDSvyc#CtywMCgBfZilf_~_Uto-+Qeo&XlR3J(yTcfd#vlMEacltq0K}8n^>EWR z2q9f7t!k~CFyul+gi;ZSo7Iv99u7JA^y!n7by4I)*VQUQ5CUNHvPhCJjwfaWWl_$6 zy??kL`rbklhEw5tS&`!Ox@&60SxhOZibBfEe7=Sbc|E+XR?Co!rs?)Cd&c=}I!&{S zPXiGWV5A-ztfW+`?{tHZOQ=Vve@XXaKl+r}amdmO7h<(a;#fdrSQwabsHzH6*kg#L zuKTt(eLHq`1UOBz*Tc20W#9)~l#FxE{msq1ZJQ#y&XUB8wk!&R>@=DG59c~H)FzED QiU0rr07*qoM6N<$f?sL==Kufz literal 0 HcmV?d00001 diff --git a/resources/public/img/tiles/water.png b/resources/public/img/tiles/water.png new file mode 100644 index 0000000000000000000000000000000000000000..008b7051b2ade0cdfee32f184f029794bf980ec8 GIT binary patch literal 621 zcmV-z0+RiSP)Bl4Kx+Bnj_jTN2O-39w(iVcWvzSC!UE_nq$ia3NW;D$FE(UDpG) z4ItMTV;rz#r|<42>Fy*=4lU~_*StGPcbo6fs8&fp6|SRdc+E!Isji-J$(BXT2KRN@ zhMrwvxdg1DO69{6O=yy&)xubQ65csTqC*r7Kve)G7K;UXEV*>MsCTt*P6BjCLDEB& zJ_aO^LQ?mSWXR`>&!Bun}WTvbbA zPUKFLY@DQTyn%G5%xF+}Ka>ij0kKRVX(HIlKj7|$*H~X@0xJu0Q#(FLB}7z5dC9o@ z^-A(cm4w{Fl8Or1E*5|ygk@E!#!v>(uh&y^61q2ilB9#I-4*Xi>BShP%0sqh_83V* z0s^Er{j!tL%4UnK)?{K-%1R!#ZOfhSdFy!nN|g zJ6qM8D>~5O^Y}IEzFvfeCL+BFe+FY{=ydtel|1;&Os`+(`6a8g_BB6x?qU1S_2;b8 z(Eda|eG~{jv>@F-oAd$iKDqLreQtWbvE5hF=`(+SOndzg;=`I~^)7P&00000NkvXX Hu0mjf;2aZ5 literal 0 HcmV?d00001 diff --git a/resources/templates/about.html b/resources/templates/about.html index c16d879..94f188b 100644 --- a/resources/templates/about.html +++ b/resources/templates/about.html @@ -1,8 +1,6 @@ {% extends "templates/base.html" %} {% block content %}
-

{{title}}

- {{content|safe}}
{% endblock %} diff --git a/resources/templates/base.html b/resources/templates/base.html index 7d07a5f..f09b769 100644 --- a/resources/templates/base.html +++ b/resources/templates/base.html @@ -19,14 +19,16 @@ -