Refactor complete, all tests still pass

This commit is contained in:
Simon Brooke 2019-05-18 10:17:15 +01:00
parent 23e24bd381
commit 66388bc944
20 changed files with 509 additions and 486 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,6 +1,6 @@
<!DOCTYPE html PUBLIC ""
"">
<html><head><meta charset="UTF-8" /><title>Introduction to the-great-game</title><link rel="stylesheet" type="text/css" href="css/default.css" /><link rel="stylesheet" type="text/css" href="css/highlight.css" /><script type="text/javascript" src="js/highlight.min.js"></script><script type="text/javascript" src="js/jquery.min.js"></script><script type="text/javascript" src="js/page_effects.js"></script><script>hljs.initHighlightingOnLoad();</script></head><body><div id="header"><h2>Generated by <a href="https://github.com/weavejester/codox">Codox</a></h2><h1><a href="index.html"><span class="project-title"><span class="project-name">The-great-game</span> <span class="project-version">0.1.0-SNAPSHOT</span></span></a></h1></div><div class="sidebar primary"><h3 class="no-link"><span class="inner">Project</span></h3><ul class="index-link"><li class="depth-1 "><a href="index.html"><div class="inner">Index</div></a></li></ul><h3 class="no-link"><span class="inner">Topics</span></h3><ul><li class="depth-1 current"><a href="intro.html"><div class="inner"><span>Introduction to the-great-game</span></div></a></li></ul><h3 class="no-link"><span class="inner">Namespaces</span></h3><ul><li class="depth-1"><div class="no-link"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>the-great-game</span></div></div></li><li class="depth-2 branch"><a href="the-great-game.core.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>core</span></div></a></li><li class="depth-2"><div class="no-link"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>gossip</span></div></div></li><li class="depth-3"><a href="the-great-game.gossip.gossip.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>gossip</span></div></a></li><li class="depth-2"><div class="no-link"><div class="inner"><span class="tree" style="top: -52px;"><span class="top" style="height: 61px;"></span><span class="bottom"></span></span><span>merchants</span></div></div></li><li class="depth-3 branch"><a href="the-great-game.merchants.markets.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>markets</span></div></a></li><li class="depth-3"><a href="the-great-game.merchants.merchants.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>merchants</span></div></a></li><li class="depth-2 branch"><a href="the-great-game.utils.html"><div class="inner"><span class="tree" style="top: -83px;"><span class="top" style="height: 92px;"></span><span class="bottom"></span></span><span>utils</span></div></a></li><li class="depth-2"><div class="no-link"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>world</span></div></div></li><li class="depth-3 branch"><a href="the-great-game.world.routes.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>routes</span></div></a></li><li class="depth-3 branch"><a href="the-great-game.world.run.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>run</span></div></a></li><li class="depth-3"><a href="the-great-game.world.world.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>world</span></div></a></li></ul></div><div class="document" id="content"><div class="doc"><div class="markdown"><h1><a href="#introduction-to-the-great-game" name="introduction-to-the-great-game"></a>Introduction to the-great-game</h1>
<html><head><meta charset="UTF-8" /><title>Introduction to the-great-game</title><link rel="stylesheet" type="text/css" href="css/default.css" /><link rel="stylesheet" type="text/css" href="css/highlight.css" /><script type="text/javascript" src="js/highlight.min.js"></script><script type="text/javascript" src="js/jquery.min.js"></script><script type="text/javascript" src="js/page_effects.js"></script><script>hljs.initHighlightingOnLoad();</script></head><body><div id="header"><h2>Generated by <a href="https://github.com/weavejester/codox">Codox</a></h2><h1><a href="index.html"><span class="project-title"><span class="project-name">The-great-game</span> <span class="project-version">0.1.0-SNAPSHOT</span></span></a></h1></div><div class="sidebar primary"><h3 class="no-link"><span class="inner">Project</span></h3><ul class="index-link"><li class="depth-1 "><a href="index.html"><div class="inner">Index</div></a></li></ul><h3 class="no-link"><span class="inner">Topics</span></h3><ul><li class="depth-1 current"><a href="intro.html"><div class="inner"><span>Introduction to the-great-game</span></div></a></li></ul><h3 class="no-link"><span class="inner">Namespaces</span></h3><ul><li class="depth-1"><div class="no-link"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>the-great-game</span></div></div></li><li class="depth-2 branch"><a href="the-great-game.core.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>core</span></div></a></li><li class="depth-2"><div class="no-link"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>gossip</span></div></div></li><li class="depth-3"><a href="the-great-game.gossip.gossip.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>gossip</span></div></a></li><li class="depth-2"><div class="no-link"><div class="inner"><span class="tree" style="top: -52px;"><span class="top" style="height: 61px;"></span><span class="bottom"></span></span><span>merchants</span></div></div></li><li class="depth-3 branch"><a href="the-great-game.merchants.markets.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>markets</span></div></a></li><li class="depth-3 branch"><a href="the-great-game.merchants.merchant-utils.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>merchant-utils</span></div></a></li><li class="depth-3 branch"><a href="the-great-game.merchants.merchants.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>merchants</span></div></a></li><li class="depth-3 branch"><a href="the-great-game.merchants.planning.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>planning</span></div></a></li><li class="depth-3"><div class="no-link"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>strategies</span></div></div></li><li class="depth-4"><a href="the-great-game.merchants.strategies.simple.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>simple</span></div></a></li><li class="depth-2 branch"><a href="the-great-game.utils.html"><div class="inner"><span class="tree" style="top: -207px;"><span class="top" style="height: 216px;"></span><span class="bottom"></span></span><span>utils</span></div></a></li><li class="depth-2"><div class="no-link"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>world</span></div></div></li><li class="depth-3 branch"><a href="the-great-game.world.routes.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>routes</span></div></a></li><li class="depth-3 branch"><a href="the-great-game.world.run.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>run</span></div></a></li><li class="depth-3"><a href="the-great-game.world.world.html"><div class="inner"><span class="tree"><span class="top"></span><span class="bottom"></span></span><span>world</span></div></a></li></ul></div><div class="document" id="content"><div class="doc"><div class="markdown"><h1><a href="#introduction-to-the-great-game" name="introduction-to-the-great-game"></a>Introduction to the-great-game</h1>
<h1><a href="#the-great-game" name="the-great-game"></a>The Great Game</h1>
<p>In this essay Im going to try to pull together a number of my architectural ideas about the Great Game which I know Im never actually going to build - because its vastly too big for any one person to build - into one overall vision.</p>
<p>So, firstly, how does one characterise this game?</p>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,92 @@
(ns the-great-game.merchants.merchant-utils
"Useful functions for doing low-level things with merchants.")
(defn expected-price
"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."
[merchant commodity city]
(or
(:price
(last
(sort-by
:date
(-> merchant :known-prices city commodity))))
1))
(defn burden
"The total weight of the current cargo carried by this `merchant` in this
`world`."
[merchant world]
(let [m (cond
(keyword? merchant)
(-> world :merchants merchant)
(map? merchant)
merchant)
cargo (:stock m)]
(reduce
+
0
(map
#(* (cargo %) (-> world :commodities % :weight))
(keys cargo)))))
(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)
(-> world :merchants merchant)
(map? merchant)
merchant)]
(quot
(- (:capacity m) (burden m world))
(-> 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)
(-> world :merchants merchant)
(map? merchant)
merchant)
l (:location m)]
(quot
(:cash m)
(-> world :cities l :prices commodity))))
(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)))))

View file

@ -2,437 +2,9 @@
"Trade planning for merchants, primarily."
(:require [taoensso.timbre :as l :refer [info error spy]]
[the-great-game.utils :refer [deep-merge]]
[the-great-game.gossip.gossip :refer [move-gossip]]
[the-great-game.world.routes :refer [find-route]]
[the-great-game.world.world :refer [actual-price default-world]]))
[the-great-game.merchants.strategies.simple :refer [move-merchant]]))
(defn expected-price
"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."
[merchant commodity city]
(or
(:price
(last
(sort-by
:date
(-> merchant :known-prices city commodity))))
1))
(defn burden
"The total weight of the current cargo carried by this `merchant` in this
`world`."
[merchant world]
(let [m (cond
(keyword? merchant)
(-> world :merchants merchant)
(map? merchant)
merchant)
cargo (:stock m)]
(reduce
+
0
(map
#(* (cargo %) (-> world :commodities % :weight))
(keys cargo)))))
(defn make-target-filter
"Construct a filter which, when applied to a list of maps,
will pass those which match these `targets`, where each target
is a tuple [key value]."
;; TODO: this would probably be more elegant as a macro
[targets]
(eval
(list
'fn
(vector 'plan)
(cons
'and
(map
#(list
'=
(list (first %) 'plan)
(nth % 1))
targets)))))
(defn generate-trade-plans
"Generate all possible trade plans for this `merchant` and this `commodity`
in this `world`.
Returned plans are maps with keys:
* :merchant - the id of the `merchant` for whom the plan was created;
* :origin - the city from which the trade starts;
* :destination - the city to which the trade is planned;
* :commodity - the `commodity` to be carried;
* :buy-price - the price at which that `commodity` can be bought;
* :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."
[merchant world commodity]
(let [m (cond
(keyword? merchant)
(-> world :merchants merchant)
(map? merchant)
merchant)
origin (:location m)]
(map
#(hash-map
:merchant (:id m)
:origin origin
:destination %
:commodity commodity
:buy-price (actual-price world commodity origin)
:expected-price (expected-price
m
commodity
%)
:distance (count
(find-route world origin %))
:dist-to-home (count
(find-route
world
(:home m)
%)))
(remove #(= % origin) (-> world :cities keys)))))
(defn nearest-with-targets
"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."
[plans targets]
(apply
min
(map
:distance
(filter
(make-target-filter targets)
plans))))
(defn plan-trade
"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;
* :origin - the city from which the trade starts;
* :destination - the city to which the trade is planned;
* :commodity - the `commodity` to be carried;
* :buy-price - the price at which that `commodity` can be bought;
* :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."
[merchant world commodity]
(let [plans (generate-trade-plans merchant world commodity)
best-prices (filter
(make-target-filter
[[:expected-price
(apply
max
(filter number? (map :expected-price plans)))]])
plans)]
(first
(sort-by
;; all other things being equal, a merchant would prefer to end closer
;; to home.
#(- 0 (:dist-to-home %))
;; a merchant will seek the best price, but won't go further than
;; needed to get it.
(filter
(make-target-filter
[[:distance
(apply min (filter number? (map :distance best-prices)))]])
best-prices)))))
(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)
(-> world :merchants merchant)
(map? merchant)
merchant)]
(quot
(- (:capacity m) (burden m world))
(-> 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)
(-> world :merchants merchant)
(map? merchant)
merchant)
l (:location m)]
(quot
(:cash m)
(-> world :cities l :prices commodity))))
(defn augment-plan
"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.
Returns the augmented plan."
[merchant world plan]
(let [c (:commodity plan)
o (:origin plan)
q (min
(or
(-> world :cities o :stock c)
0)
(can-carry merchant world c)
(can-afford merchant world c))
p (* q (- (:expected-price plan) (:buy-price plan)))]
(assoc plan :quantity q :expected-profit p)))
;; (-> default-world :cities :buckie :stock :iron)
;; (burden :fiona default-world)
;; (-> default-world :commodities :iron :weight)
;; (quot 0 10)
(defn select-cargo
"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."
[merchant world]
(let [m (cond
(keyword? merchant)
(-> world :merchants merchant)
(map? merchant)
merchant)
origin (:location m)
available (-> world :cities origin :stock)
plans (map
#(augment-plan
m
world
(plan-trade m world %))
(filter
#(let [q (-> world :cities origin :stock %)]
(and (number? q) (pos? q)))
(keys available)))]
(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]
(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)]
(l/debug "plan-and-buy: merchant" id)
(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 plan)
{:merchants
{id
{:stock (add-stock (:stock m) {c q})
:cash (- (:cash m) p)
:known-prices (add-known-prices m world)
:plan plan}}
: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))
(do
(l/info "Merchant" id "remains at home in" location)
{})
;; else move towards home
:else
(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)
(merge
{:merchants
{id
{:location next-location}}}
(move-gossip id world 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)))]
(l/debug "re-plan: merchant" id)
(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))))]
(l/debug "sell-and-buy: merchant" id)
(if
(>= (:cash market) stock-value)
(do
(l/info "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`;
return a (partial or full) world like this `world` but in which the
merchant may have been moved ot updated."
[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
(find-route
world
(:location m)
(:destination plan))
1)
(:location m))]
(l/debug "move-merchant: merchant" id "at" (:location m)
"destination" (-> m :plan :destination) "next" next-location
"at destination" at-destination?)
(cond
;; if the merchant is at the destination of their current plan
;; sell all cargo and repurchase.
at-destination?
(sell-and-buy merchant world)
;; if they don't have a plan, seek to create one
(nil? plan)
(plan-and-buy merchant world)
;; otherwise, move one step towards their destination
(and next-location (not= next-location (:location m)))
(do
(l/info "Merchant " id " moving from " (:location m) " to " next-location)
(deep-merge
{:merchants
{id
{:location next-location
:known-prices (add-known-prices m world)}}}
(move-gossip id world next-location)))
:else
(do
(l/info "Merchant" id "has plan but no next-location; currently at"
(:location m) ", destination is" (:destination plan))
world))))
(defn run
"Return a partial world based on this `world`, but with each merchant moved."
[world]
@ -442,11 +14,14 @@
world
(map
#(try
(move-merchant % world)
(let [move-fn (or
(-> world :merchants % :move-fn)
move-merchant)]
(apply move-fn (list % world)))
(catch Exception any
(l/error any "Failure while moving merchant " %)
{}))
(keys (:merchants world))))
(keys (:merchants world))))
(catch Exception any
(l/error any "Failure while moving merchants")
world)))

View file

@ -1,6 +1,156 @@
(ns the-great-game.merchants.planning
"Trade planning for merchants, primarily."
(:require [taoensso.timbre :as l :refer [info error spy]]
[the-great-game.utils :refer [deep-merge]]
(:require [the-great-game.utils :refer [deep-merge make-target-filter]]
[the-great-game.merchants.merchant-utils :refer :all]
[the-great-game.world.routes :refer [find-route]]
[the-great-game.world.world :refer [actual-price default-world]]))
(defn generate-trade-plans
"Generate all possible trade plans for this `merchant` and this `commodity`
in this `world`.
Returned plans are maps with keys:
* :merchant - the id of the `merchant` for whom the plan was created;
* :origin - the city from which the trade starts;
* :destination - the city to which the trade is planned;
* :commodity - the `commodity` to be carried;
* :buy-price - the price at which that `commodity` can be bought;
* :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."
[merchant world commodity]
(let [m (cond
(keyword? merchant)
(-> world :merchants merchant)
(map? merchant)
merchant)
origin (:location m)]
(map
#(hash-map
:merchant (:id m)
:origin origin
:destination %
:commodity commodity
:buy-price (actual-price world commodity origin)
:expected-price (expected-price
m
commodity
%)
:distance (count
(find-route world origin %))
:dist-to-home (count
(find-route
world
(:home m)
%)))
(remove #(= % origin) (-> world :cities keys)))))
(defn nearest-with-targets
"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."
[plans targets]
(apply
min
(map
:distance
(filter
(make-target-filter targets)
plans))))
(defn plan-trade
"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;
* :origin - the city from which the trade starts;
* :destination - the city to which the trade is planned;
* :commodity - the `commodity` to be carried;
* :buy-price - the price at which that `commodity` can be bought;
* :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."
[merchant world commodity]
(let [plans (generate-trade-plans merchant world commodity)
best-prices (filter
(make-target-filter
[[:expected-price
(apply
max
(filter number? (map :expected-price plans)))]])
plans)]
(first
(sort-by
;; all other things being equal, a merchant would prefer to end closer
;; to home.
#(- 0 (:dist-to-home %))
;; a merchant will seek the best price, but won't go further than
;; needed to get it.
(filter
(make-target-filter
[[:distance
(apply min (filter number? (map :distance best-prices)))]])
best-prices)))))
(defn augment-plan
"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.
Returns the augmented plan."
[merchant world plan]
(let [c (:commodity plan)
o (:origin plan)
q (min
(or
(-> world :cities o :stock c)
0)
(can-carry merchant world c)
(can-afford merchant world c))
p (* q (- (:expected-price plan) (:buy-price plan)))]
(assoc plan :quantity q :expected-profit p)))
(defn select-cargo
"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."
[merchant world]
(let [m (cond
(keyword? merchant)
(-> world :merchants merchant)
(map? merchant)
merchant)
origin (:location m)
available (-> world :cities origin :stock)
plans (map
#(augment-plan
m
world
(plan-trade m world %))
(filter
#(let [q (-> world :cities origin :stock %)]
(and (number? q) (pos? q)))
(keys available)))]
(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))))))

View file

@ -1,2 +1,173 @@
(ns the-great-game.merchants.strategies.simple
)
"Default trading strategy for merchants.
The simple strategy buys a single product in the local market if there is
one which can be traded profitably, trades it to the chosen target market,
and sells it there. If there is no commodity locally which can be traded
profitably, moves towards home with no cargo. If at home and no commodity
can be traded profitably, does not move."
(:require [taoensso.timbre :as l :refer [info error spy]]
[the-great-game.utils :refer [deep-merge]]
[the-great-game.gossip.gossip :refer [move-gossip]]
[the-great-game.merchants.planning :refer :all]
[the-great-game.merchants.merchant-utils :refer
[add-stock add-known-prices]]
[the-great-game.world.routes :refer [find-route]]))
(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]
(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)]
(l/debug "plan-and-buy: merchant" id)
(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 plan)
{:merchants
{id
{:stock (add-stock (:stock m) {c q})
:cash (- (:cash m) p)
:known-prices (add-known-prices m world)
:plan plan}}
: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))
(do
(l/info "Merchant" id "remains at home in" location)
{})
;; else move towards home
:else
(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)
(merge
{:merchants
{id
{:location next-location}}}
(move-gossip id world 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)))]
(l/debug "re-plan: merchant" id)
(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))))]
(l/debug "sell-and-buy: merchant" id)
(if
(>= (:cash market) stock-value)
(do
(l/info "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`;
return a (partial or full) world like this `world` but in which the
merchant may have been moved ot updated."
[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
(find-route
world
(:location m)
(:destination plan))
1)
(:location m))]
(l/debug "move-merchant: merchant" id "at" (:location m)
"destination" (-> m :plan :destination) "next" next-location
"at destination" at-destination?)
(cond
;; if the merchant is at the destination of their current plan
;; sell all cargo and repurchase.
at-destination?
(sell-and-buy merchant world)
;; if they don't have a plan, seek to create one
(nil? plan)
(plan-and-buy merchant world)
;; otherwise, move one step towards their destination
(and next-location (not= next-location (:location m)))
(do
(l/info "Merchant " id " moving from " (:location m) " to " next-location)
(deep-merge
{:merchants
{id
{:location next-location
:known-prices (add-known-prices m world)}}}
(move-gossip id world next-location)))
:else
(do
(l/info "Merchant" id "has plan but no next-location; currently at"
(:location m) ", destination is" (:destination plan))
world))))

View file

@ -15,3 +15,21 @@
(last xs)))]
(reduce m maps)))
(defn make-target-filter
"Construct a filter which, when applied to a list of maps,
will pass those which match these `targets`, where each target
is a tuple [key value]."
;; TODO: this would probably be more elegant as a macro
[targets]
(eval
(list
'fn
(vector 'm)
(cons
'and
(map
#(list
'=
(list (first %) 'm)
(nth % 1))
targets)))))

View file

@ -0,0 +1,24 @@
(ns the-great-game.merchants.merchant-utils-test
(:require [clojure.test :refer :all]
[the-great-game.utils :refer [deep-merge]]
[the-great-game.world.world :refer [default-world]]
[the-great-game.merchants.merchant-utils :refer :all]))
(deftest expected-price-test
(testing "Anticipated prices in markets"
(let [world (deep-merge
default-world
{:merchants
{:archie
{:known-prices
{:buckie
{:iron
[{:price 1.7 :date 1}
{:price 2 :date 0}]}}}}})]
(let [actual (expected-price (-> world :merchants :archie) :fish :edinburgh)
expected 1] ;;
(is (= actual expected) "if no information assume 1"))
(let [actual (expected-price (-> world :merchants :archie) :iron :buckie)
expected 1.7] ;;
(is (= actual expected) "if information select the most recent")))))

View file

@ -1,26 +1,9 @@
(ns the-great-game.merchants.merchants-test
(ns the-great-game.merchants.planning-test
(:require [clojure.test :refer :all]
[the-great-game.utils :refer [deep-merge]]
[the-great-game.world.world :refer [default-world]]
[the-great-game.merchants.merchants :refer :all]))
[the-great-game.merchants.planning :refer :all]))
(deftest expected-price-test
(testing "Anticipated prices in markets"
(let [world (deep-merge
default-world
{:merchants
{:archie
{:known-prices
{:buckie
{:iron
[{:price 1.7 :date 1}
{:price 2 :date 0}]}}}}})]
(let [actual (expected-price (-> world :merchants :archie) :fish :edinburgh)
expected 1] ;;
(is (= actual expected) "if no information assume 1"))
(let [actual (expected-price (-> world :merchants :archie) :iron :buckie)
expected 1.7] ;;
(is (= actual expected) "if information select the most recent")))))
(deftest plan-trade-test
(testing "Lower level trade planning"