\ No newline at end of file
diff --git a/docs/intro.html b/docs/intro.html
index a34ef21..7d118f7 100644
--- a/docs/intro.html
+++ b/docs/intro.html
@@ -1,6 +1,6 @@
-Introduction to the-great-game
In this essay I’m going to try to pull together a number of my architectural ideas about the Great Game which I know I’m never actually going to build - because it’s vastly too big for any one person to build - into one overall vision.
\ No newline at end of file
diff --git a/docs/the-great-game.gossip.gossip.html b/docs/the-great-game.gossip.gossip.html
new file mode 100644
index 0000000..ac9d3d8
--- /dev/null
+++ b/docs/the-great-game.gossip.gossip.html
@@ -0,0 +1,3 @@
+
+the-great-game.gossip.gossip documentation
Dialogue between an enquirer and an agent in this world; returns a map identical to enquirer except that its :gossip collection may have additional entries.
\ No newline at end of file
diff --git a/docs/the-great-game.merchants.markets.html b/docs/the-great-game.merchants.markets.html
index 670da43..cc83ba1 100644
--- a/docs/the-great-game.merchants.markets.html
+++ b/docs/the-great-game.merchants.markets.html
@@ -1,3 +1,3 @@
-the-great-game.merchants.markets documentation
Adjust the quantity of this commodity currently in stock in this city of this world. Return a fragmentary world which can be deep-merged into this world.
If stock is greater than the maximum of supply and demand, then there is surplus and old price is too high, so shold be reduced. If lower, then it is too low and should be increased.
(update-markets world)(update-markets world city)(update-markets world city commodity)
Return a world like this world, with quantities and prices in markets updated to reflect supply and demand. If city or city and commodity are specified, return a fragmentary world with only the changes for that city (and commodity if specified) populated.
Adjust the quantity of this commodity currently in stock in this city of this world. Return a fragmentary world which can be deep-merged into this world.
If stock is greater than the maximum of supply and demand, then there is surplus and old price is too high, so shold be reduced. If lower, then it is too low and should be increased.
(update-markets world)(update-markets world city)(update-markets world city commodity)
Return a world like this world, with quantities and prices in markets updated to reflect supply and demand. If city or city and commodity are specified, return a fragmentary world with only the changes for that city (and commodity if specified) populated.
\ No newline at end of file
diff --git a/docs/the-great-game.merchants.merchants.html b/docs/the-great-game.merchants.merchants.html
index 30ea5d6..f1512a2 100644
--- a/docs/the-great-game.merchants.merchants.html
+++ b/docs/the-great-game.merchants.merchants.html
@@ -1,7 +1,7 @@
-the-great-game.merchants.merchants documentation
Augment this plan constructed in this world for this merchant with the :quantity of goods which should be bought and the :expected-profit of the trade.
Find the price anticipated, given this world, by this merchant for this commodity in this city. If no information, assume 1. merchant should be passed as a map, commodity and city should be passed as keywords.
Where a and b are both maps all of whose values are numbers, return a map whose keys are a union of the keys of a and b and whose values are the sums of their respective values.
Augment this plan constructed in this world for this merchant with the :quantity of goods which should be bought and the :expected-profit of the trade.
Find the price anticipated, given this world, by this merchant for this commodity in this city. If no information, assume 1. merchant should be passed as a map, commodity and city should be passed as keywords.
Return the distance to the nearest destination among those of these plans which match these targets. Plans are expected to be plans as returned by generate-trade-plans, q.v.; targets are expected to be as accepted by make-target-filter, q.v.
Find the best destination in this world for this commodity given this merchant and this origin. If two cities are anticipated to offer the same price, the nearer should be preferred; if two are equally distant, the ones nearer to the merchant’s home should be preferred. merchant may be passed as a map or a keyword; commodity should be passed as a keyword.
Return the distance to the nearest destination among those of these plans which match these targets. Plans are expected to be plans as returned by generate-trade-plans, q.v.; targets are expected to be as accepted by make-target-filter, q.v.
Return a world like this world, in which this merchant has planned a new trade, and bought appropriate stock for it. If no profitable trade can be planned, the merchant is simply moved towards their home.
Find the best destination in this world for this commodity given this merchant and this origin. If two cities are anticipated to offer the same price, the nearer should be preferred; if two are equally distant, the ones nearer to the merchant’s home should be preferred. merchant may be passed as a map or a keyword; commodity should be passed as a keyword.
The returned plan is a map with keys:
:merchant - the id of the merchant for whom the plan was created;
@@ -23,4 +23,4 @@
:expected-price - the price at which the merchant anticipates that commodity can be sold;
:distance - the number of stages in the planned journey
:dist-to-home - the distance from destination to the merchant’s home city.
A merchant, in a given location in a world, will choose to buy a cargo within the limit they are capable of carrying, which they can anticipate selling for a profit at a destination.
A merchant, in a given location in a world, will choose to buy a cargo within the limit they are capable of carrying, which they can anticipate selling for a profit at a destination.
Return a new world like this world, in which this merchant has sold their current stock in their current location, and planned a new trade, and bought appropriate stock for it.
\ No newline at end of file
diff --git a/docs/the-great-game.utils.html b/docs/the-great-game.utils.html
index ec15aeb..bb4ad9f 100644
--- a/docs/the-great-game.utils.html
+++ b/docs/the-great-game.utils.html
@@ -1,3 +1,3 @@
-the-great-game.utils documentation
\ No newline at end of file
diff --git a/docs/the-great-game.world.routes.html b/docs/the-great-game.world.routes.html
index 77af9bb..d5d1cbd 100644
--- a/docs/the-great-game.world.routes.html
+++ b/docs/the-great-game.world.routes.html
@@ -1,3 +1,3 @@
-the-great-game.world.routes documentation
Conceptual (plan level) routes, represented as tuples of location ids.
find-route
(find-route world-or-routes from to)
Find a single route from from to to in this world-or-routes, which may be either a world as defined in the-great-game.world.world or else a sequence of tuples of keywords.
\ No newline at end of file
diff --git a/docs/the-great-game.world.run.html b/docs/the-great-game.world.run.html
new file mode 100644
index 0000000..dd8152d
--- /dev/null
+++ b/docs/the-great-game.world.run.html
@@ -0,0 +1,3 @@
+
+the-great-game.world.run documentation
\ No newline at end of file
diff --git a/docs/the-great-game.world.world.html b/docs/the-great-game.world.world.html
index dc7acdd..7d840bd 100644
--- a/docs/the-great-game.world.world.html
+++ b/docs/the-great-game.world.world.html
@@ -1,3 +1,3 @@
-the-great-game.world.world documentation
Find the actual current price of this commodity in this city given this world. NOTE that merchants can only know the actual prices in the city in which they are currently located.
Find the actual current price of this commodity in this city given this world. NOTE that merchants can only know the actual prices in the city in which they are currently located.
Return a world like this world with only the :date value updated (incremented by one). For running other aspects of the simulation, see the-great-game.world.run#var-run.
\ No newline at end of file
diff --git a/project.clj b/project.clj
index 2bc9bcc..4ffd6a4 100644
--- a/project.clj
+++ b/project.clj
@@ -3,7 +3,8 @@
:doc/format :markdown}
:output-path "docs"
:source-uri "https://github.com/simon-brooke/the-great-game/blob/master/{filepath}#L{line}"}
- :dependencies [[org.clojure/clojure "1.8.0"]]
+ :dependencies [[org.clojure/clojure "1.8.0"]
+ [com.taoensso/timbre "4.10.0"]]
:description "Prototype code towards the great game I've been writing about for ten years, and know I will never finish."
:license {:name "GNU General Public License,version 2.0 or (at your option) any later version"
:url "https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html"}
diff --git a/src/the_great_game/gossip/gossip.clj b/src/the_great_game/gossip/gossip.clj
new file mode 100644
index 0000000..99957a9
--- /dev/null
+++ b/src/the_great_game/gossip/gossip.clj
@@ -0,0 +1,47 @@
+(ns the-great-game.gossip.gossip
+ "Interchange of news events between agents agents"
+ (:require [the-great-game.utils :refer [deep-merge]]))
+
+;; Note that habitual travellers are all gossip agents; specifically, at this
+;; stage, that means merchants. When merchants are moved we also need to
+;; update the location of the gossip with the same key.
+
+(defn dialogue
+ "Dialogue between an `enquirer` and an `agent` in this `world`; returns a
+ map identical to `enquirer` except that its `:gossip` collection may have
+ additional entries."
+ ;; TODO: not yet written, this is a stub.
+ [enquirer respondent world]
+ enquirer)
+
+(defn gather-news
+ ([world]
+ (reduce
+ deep-merge
+ world
+ (map
+ #(gather-news world %)
+ (keys (:gossips world)))))
+ ([world gossip]
+ (let [g (cond (keyword? gossip)
+ (-> world :gossips gossip)
+ (map? gossip)
+ gossip)]
+ {:gossips
+ {(:id g)
+ (reduce
+ deep-merge
+ {}
+ (map
+ #(dialogue g % world)
+ (remove
+ #( = g %)
+ (filter
+ #(= (:location %) (:location g))
+ (vals (:gossips world))))))}})))
+
+(defn run
+ "Return a world like this `world`, with news items exchanged between gossip
+ agents."
+ [world]
+ (gather-news world))
diff --git a/src/the_great_game/merchants/merchants.clj b/src/the_great_game/merchants/merchants.clj
index 195495f..1a691ae 100644
--- a/src/the_great_game/merchants/merchants.clj
+++ b/src/the_great_game/merchants/merchants.clj
@@ -1,6 +1,8 @@
(ns the-great-game.merchants.merchants
"Trade planning for merchants, primarily."
- (:require [the-great-game.world.routes :refer [find-routes]]
+ (:require [taoensso.timbre :as l :refer [info error]]
+ [the-great-game.utils :refer [deep-merge]]
+ [the-great-game.world.routes :refer [find-route]]
[the-great-game.world.world :refer [actual-price default-world]]))
@@ -91,15 +93,12 @@
commodity
%)
:distance (count
- (first
- (find-routes (:routes world) origin %)))
+ (find-route world origin %))
:dist-to-home (count
- (first
- (find-routes
- (:routes world)
+ (find-route
+ world
(:home m)
%)))
- )
(remove #(= % origin) (keys (-> world :cities))))))
(defn nearest-with-targets
@@ -160,6 +159,8 @@
(defn can-carry
+ "Return the number of units of this `commodity` which this `merchant`
+ can carry in this `world`, given their current burden."
[merchant world commodity]
(let [m (cond
(keyword? merchant)
@@ -171,6 +172,8 @@
(-> world :commodities commodity :weight))))
(defn can-afford
+ "Return the number of units of this `commodity` which this `merchant`
+ can afford to buy in this `world`."
[merchant world commodity]
(let [m (cond
(keyword? merchant)
@@ -226,12 +229,201 @@
#(let [q (-> world :cities origin :stock %)]
(and (number? q) (> q 0)))
(keys available)))]
- (first
- (sort-by
- #(- 0 (:dist-to-home %))
- (filter
- (make-target-filter
- [[:expected-profit
- (apply max (filter number? (map :expected-profit plans)))]])
- plans)))))
+ (if
+ (not (empty? plans))
+ (first
+ (sort-by
+ #(- 0 (:dist-to-home %))
+ (filter
+ (make-target-filter
+ [[:expected-profit
+ (apply max (filter number? (map :expected-profit plans)))]])
+ plans))))))
+
+(defn add-stock
+ "Where `a` and `b` are both maps all of whose values are numbers, return
+ a map whose keys are a union of the keys of `a` and `b` and whose values
+ are the sums of their respective values."
+ [a b]
+ (reduce
+ merge
+ a
+ (map
+ #(hash-map % (+ (or (a %) 0) (or (b %) 0)))
+ (keys b))))
+
+(defn add-known-prices
+ "Add the current prices at this `merchant`'s location in the `world`
+ to a new cacke of known prices, and return it."
+ [merchant world]
+ (let [m (cond
+ (keyword? merchant)
+ (-> world :merchants merchant)
+ (map? merchant)
+ merchant)
+ k (:known-prices m)
+ l (:location m)
+ d (:date world)
+ p (-> world :cities l :prices)]
+ (reduce
+ merge
+ k
+ (map
+ #(hash-map % (apply vector cons {:price (p %) :date d} (k %)))
+ (-> world :commodities keys)))))
+
+;;; Right, from here on in we're actually moving things in the world, so
+;;; functions generally return modified partial worlds.
+
+(defn plan-and-buy
+ "Return a world like this `world`, in which this `merchant` has planned
+ a new trade, and bought appropriate stock for it. If no profitable trade
+ can be planned, the merchant is simply moved towards their home."
+ [merchant world]
+ (deep-merge
+ world
+ (let [m (cond
+ (keyword? merchant)
+ (-> world :merchants merchant)
+ (map? merchant)
+ merchant)
+ id (:id m)
+ location (:location m)
+ market (-> world :cities location)
+ plan (select-cargo merchant world)]
+ (cond
+ (not (empty? plan))
+ (let
+ [c (:commodity plan)
+ p (* (:quantity plan) (:buy-price plan))
+ q (:quantity plan)]
+ (l/info "Merchant " id " bought " q " units of " c " at " location " for " p)
+ {:merchants
+ {id
+ {:stock (add-stock (:stock m) {c q})
+ :cash (- (:cash m) p)
+ :known-prices (add-known-prices m world)}}
+ :cities
+ {location
+ {:stock (assoc (:stock market) c (- (-> market :stock c) q))
+ :cash (+ (:cash market) p)}}})
+ ;; if no plan, then if at home stay put
+ (= (:location m) (:home m))
+ {}
+ ;; else move towards home
+ true
+ (let [route (find-route world location (:home m))
+ next-location (nth route 1)]
+ (l/info "No trade possible at " location "; merchant " id " moves to " next-location)
+ {:merchants
+ {id
+ {:location next-location}}})))))
+
+
+(defn re-plan
+ "Having failed to sell a cargo at current location, re-plan a route to
+ sell the current cargo. Returns a revised world."
+ [merchant world]
+ (let [m (cond
+ (keyword? merchant)
+ (-> world :merchants merchant)
+ (map? merchant)
+ merchant)
+ id (:id m)
+ location (:location m)
+ plan (augment-plan m world (plan-trade m world (-> m :plan :commodity)))]
+ (deep-merge
+ world
+ {:merchants
+ {id
+ {:plan plan}}})))
+
+
+(defn sell-and-buy
+ "Return a new world like this `world`, in which this `merchant` has sold
+ their current stock in their current location, and planned a new trade, and
+ bought appropriate stock for it."
+ ;; TODO: this either sells the entire cargo, or, if the market can't afford
+ ;; it, none of it. And it does not cope with selling different commodities
+ ;; in different markets.
+ [merchant world]
+ (let [m (cond
+ (keyword? merchant)
+ (-> world :merchants merchant)
+ (map? merchant)
+ merchant)
+ id (:id m)
+ location (:location m)
+ market (-> world :cities location)
+ stock-value (reduce
+ +
+ (map
+ #(* (-> m :stock %) (-> market :prices m))
+ (keys (:stock m))))]
+ (if
+ (>= (:cash market) stock-value)
+ (do
+ (l/info
+ (apply str (flatten (list "Merchant " id " sells " (:stock m) " at " location " for " stock-value))))
+ (plan-and-buy
+ merchant
+ (deep-merge
+ world
+ {:merchants
+ {id
+ {:stock {}
+ :cash (+ (:cash m) stock-value)
+ :known-prices (add-known-prices m world)}}
+ :cities
+ {location
+ {:stock (add-stock (:stock m) (:stock market))
+ :cash (- (:cash market) stock-value)}}})))
+ ;; else
+ (re-plan merchant world))))
+
+
+(defn move-merchant
+ "Handle general en route movement of this `merchant` in this `world`."
+ [merchant world]
+ (let [m (cond
+ (keyword? merchant)
+ (-> world :merchants merchant)
+ (map? merchant)
+ merchant)
+ id (:id m)
+ at-destination? (and (:plan m) (= (:location m) (-> m :plan :destination)))
+ plan (:plan m)
+ next-location (if plan
+ (nth 1 (find-route world (:location m) (:destination plan)))
+ (:location m))]
+ (l/info "Merchant " id " at " (:location m))
+ (cond at-destination?
+ (sell-and-buy merchant world plan)
+ (nil? (:plan m))
+ (plan-and-buy merchant world)
+ true
+ {:merchants
+ {id
+ {:id id
+ :location next-location
+ :known-prices (add-known-prices m world)}}})))
+
+
+(defn run
+ "Return a world like this `world`, but with each merchant moved."
+ [world]
+ (try
+ (reduce
+ deep-merge
+ world
+ (map
+ #(try
+ (move-merchant % world)
+ (catch Exception any
+ (l/error any "Failure while moving merchant " %)
+ {}))
+ (keys (:merchants world))))
+ (catch Exception any
+ (l/error any "Failure while moving merchants")
+ world)))
diff --git a/src/the_great_game/utils.clj b/src/the_great_game/utils.clj
index ed6247b..6ea3c84 100644
--- a/src/the_great_game/utils.clj
+++ b/src/the_great_game/utils.clj
@@ -1,6 +1,5 @@
(ns the-great-game.utils)
-
(defn cyclic?
"True if two or more elements of `route` are identical"
[route]
@@ -15,3 +14,4 @@
(apply merge-with m xs)
(last xs)))]
(reduce m maps)))
+
diff --git a/src/the_great_game/world/routes.clj b/src/the_great_game/world/routes.clj
index 0bf2b3f..93926ee 100644
--- a/src/the_great_game/world/routes.clj
+++ b/src/the_great_game/world/routes.clj
@@ -44,3 +44,14 @@
(empty? found)
(find-routes routes from to paths)
found)))))
+
+(defn find-route
+ "Find a single route from `from` to `to` in this `world-or-routes`, which
+ may be either a world as defined in [[the-great-game.world.world]] or else
+ a sequence of tuples of keywords."
+ [world-or-routes from to]
+ (first
+ (find-routes
+ (or (:routes world-or-routes) world-or-routes)
+ from
+ to)))
diff --git a/src/the_great_game/world/run.clj b/src/the_great_game/world/run.clj
new file mode 100644
index 0000000..5abcc2d
--- /dev/null
+++ b/src/the_great_game/world/run.clj
@@ -0,0 +1,17 @@
+(ns the-great-game.world.run
+ "Run the whole simulation"
+ (:require [taoensso.timbre :as log]
+ [the-great-game.gossip.gossip :as g]
+ [the-great-game.merchants.merchants :as m]
+ [the-great-game.merchants.markets :as k]
+ [the-great-game.world.world :as w]))
+
+
+(defn run
+ "The pipeline to run the simulation each game day. Returns a world like
+ this world, with all the various active elements updated."
+ [world]
+ (g/run
+ (m/run
+ (k/run
+ (w/run world)))))
diff --git a/src/the_great_game/world/world.clj b/src/the_great_game/world/world.clj
index 4e914fc..1811dd0 100644
--- a/src/the_great_game/world/world.clj
+++ b/src/the_great_game/world/world.clj
@@ -9,7 +9,8 @@
(def default-world
"A basic world for testing concepts"
- {:cities
+ {:date 0 ;; the age of this world in game days
+ :cities
{:aberdeen
{:id :aberdeen
:supplies
@@ -181,4 +182,9 @@
[world commodity city]
(-> world :cities city :prices commodity))
-
+(defn run
+ "Return a world like this `world` with only the `:date` value updated
+ (incremented by one). For running other aspects of the simulation, see
+ [[the-great-game.world.run#var-run]]."
+ [world]
+ (assoc world :date (inc (or (:date world) 0))))