Cargo selection planning now works.
This commit is contained in:
parent
11506334f1
commit
fcf8a8fa0d
|
@ -1,7 +1,7 @@
|
||||||
(ns the-great-game.merchants.merchants
|
(ns the-great-game.merchants.merchants
|
||||||
"Trade planning for merchants, primarily."
|
"Trade planning for merchants, primarily."
|
||||||
(:require [the-great-game.world.routes :refer [find-routes]]
|
(:require [the-great-game.world.routes :refer [find-routes]]
|
||||||
[the-great-game.world.world :refer [actual-price]]))
|
[the-great-game.world.world :refer [actual-price default-world]]))
|
||||||
|
|
||||||
|
|
||||||
(defn expected-price
|
(defn expected-price
|
||||||
|
@ -36,15 +36,31 @@
|
||||||
(keys cargo)))))
|
(keys cargo)))))
|
||||||
|
|
||||||
|
|
||||||
(defn find-trade-plan
|
(defn make-target-filter
|
||||||
"Find the best destination in this `world` for this `commodity` given this
|
"Construct a filter which, when applied to a list of maps,
|
||||||
`merchant` and this `origin`. If two cities are anticipated to offer the
|
will pass those which match these `targets`, where each target
|
||||||
same price, the nearer should be preferred; if two are equally distant, the
|
is a tuple [key value]."
|
||||||
ones nearer to the merchant's home should be preferred.
|
;; TODO: this would probably be more elegant as a macro
|
||||||
`merchant` may be passed as a map or a keyword; `commodity` should be
|
[targets]
|
||||||
passed as a keyword.
|
(eval
|
||||||
|
(list
|
||||||
|
'fn
|
||||||
|
(vector 'plan)
|
||||||
|
(cons
|
||||||
|
'and
|
||||||
|
(map
|
||||||
|
#(list
|
||||||
|
'=
|
||||||
|
(list (first %) 'plan)
|
||||||
|
(nth % 1))
|
||||||
|
targets)))))
|
||||||
|
|
||||||
The returned plan is a map with keys:
|
|
||||||
|
(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;
|
* :merchant - the id of the `merchant` for whom the plan was created;
|
||||||
* :origin - the city from which the trade starts;
|
* :origin - the city from which the trade starts;
|
||||||
|
@ -62,41 +78,109 @@
|
||||||
(-> world :merchants merchant)
|
(-> world :merchants merchant)
|
||||||
(map? merchant)
|
(map? merchant)
|
||||||
merchant)
|
merchant)
|
||||||
origin (-> m :location)
|
origin (-> m :location)]
|
||||||
destinations (remove #(= % origin) (keys (-> world :cities)))
|
(map
|
||||||
plans (map
|
#(hash-map
|
||||||
#(hash-map
|
:merchant (-> m :id)
|
||||||
:merchant (-> m :id)
|
:origin origin
|
||||||
:origin origin
|
:destination %
|
||||||
:destination %
|
:commodity commodity
|
||||||
:commodity commodity
|
:buy-price (actual-price world commodity origin)
|
||||||
:buy-price (actual-price world commodity origin)
|
:expected-price (expected-price
|
||||||
:expected-price (expected-price
|
m
|
||||||
merchant
|
commodity
|
||||||
commodity
|
%)
|
||||||
%)
|
:distance (count
|
||||||
:distance (count
|
(first
|
||||||
(first
|
(find-routes (:routes world) origin %)))
|
||||||
(find-routes (:routes world) origin %)))
|
:dist-to-home (count
|
||||||
:dist-to-home (count
|
(first
|
||||||
(first
|
(find-routes
|
||||||
(find-routes
|
(:routes world)
|
||||||
(:routes world)
|
(:home m)
|
||||||
(-> world :merchants merchant :home)
|
%)))
|
||||||
%)))
|
)
|
||||||
)
|
(remove #(= % origin) (keys (-> world :cities))))))
|
||||||
destinations)
|
|
||||||
best-price (apply min (filter number? (map :expected-price plans)))
|
|
||||||
nearest (apply min (map :distance plans))]
|
|
||||||
(first
|
|
||||||
(sort
|
|
||||||
#(compare (:dist-to-home %1) (:dist-to-home %2))
|
|
||||||
(filter
|
|
||||||
#(and
|
|
||||||
(= (:expected-price %) best-price)
|
|
||||||
(= (:distance %) nearest))
|
|
||||||
plans)))))
|
|
||||||
|
|
||||||
|
(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
|
||||||
|
[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
|
||||||
|
[merchant world commodity]
|
||||||
|
(let [m (cond
|
||||||
|
(keyword? merchant)
|
||||||
|
(-> world :merchants merchant)
|
||||||
|
(map? merchant)
|
||||||
|
merchant)
|
||||||
|
l (:location m)]
|
||||||
|
(quot
|
||||||
|
(-> m :cash)
|
||||||
|
(-> world :cities l :prices commodity))))
|
||||||
|
|
||||||
(defn augment-plan
|
(defn augment-plan
|
||||||
"Augment this `plan` constructed in this `world` for this `merchant` with
|
"Augment this `plan` constructed in this `world` for this `merchant` with
|
||||||
|
@ -105,22 +189,21 @@
|
||||||
|
|
||||||
Returns the augmented plan."
|
Returns the augmented plan."
|
||||||
[merchant world plan]
|
[merchant world plan]
|
||||||
(let [m (cond
|
(let [c (:commodity plan)
|
||||||
(keyword? merchant)
|
o (:origin plan)
|
||||||
(-> world :merchants merchant)
|
q (min
|
||||||
(map? merchant)
|
(or
|
||||||
merchant)
|
(-> world :cities o :stock c)
|
||||||
available (-> world :cities (:origin plan) :stock (:commodity plan))
|
0)
|
||||||
can-carry (quot
|
(can-carry merchant world c)
|
||||||
(- (-> m :capacity) (burden m world))
|
(can-afford merchant world c))
|
||||||
(-> world :commodities (:commodity plan) :weight))
|
|
||||||
can-afford (quot
|
|
||||||
(-> merchant :cash)
|
|
||||||
(-> world :commodities (:commodity plan) :weight))
|
|
||||||
q (min available can-carry can-afford)
|
|
||||||
p (* q (- (:expected-price plan) (:buy-price plan)))]
|
p (* q (- (:expected-price plan) (:buy-price plan)))]
|
||||||
(assoc plan :quantity q :expected-profit p)))
|
(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
|
(defn select-cargo
|
||||||
"A `merchant`, in a given location in a `world`, will choose to buy a cargo
|
"A `merchant`, in a given location in a `world`, will choose to buy a cargo
|
||||||
|
@ -138,19 +221,17 @@
|
||||||
#(augment-plan
|
#(augment-plan
|
||||||
m
|
m
|
||||||
world
|
world
|
||||||
(find-trade-plan m world %))
|
(plan-trade m world %))
|
||||||
(filter
|
(filter
|
||||||
#(let [q (-> world :cities origin :stock %)]
|
#(let [q (-> world :cities origin :stock %)]
|
||||||
(and (number? q) (> q 0)))
|
(and (number? q) (> q 0)))
|
||||||
(keys available)))
|
(keys available)))]
|
||||||
best-profit (apply min (filter number? (map :expected-profit plans)))
|
|
||||||
nearest (apply min (map :distance plans))]
|
|
||||||
(first
|
(first
|
||||||
(sort
|
(sort-by
|
||||||
#(compare (:dist-to-home %1) (:dist-to-home %2))
|
#(- 0 (:dist-to-home %))
|
||||||
(filter
|
(filter
|
||||||
#(and
|
(make-target-filter
|
||||||
(= (:expected-profit %) best-profit)
|
[[:expected-profit
|
||||||
(= (:distance %) nearest))
|
(apply max (filter number? (map :expected-profit plans)))]])
|
||||||
plans))) ))
|
plans)))))
|
||||||
|
|
||||||
|
|
|
@ -24,7 +24,7 @@
|
||||||
;; the end of each game day.
|
;; the end of each game day.
|
||||||
{:iron 1
|
{:iron 1
|
||||||
:cloth 10
|
:cloth 10
|
||||||
:tobacco 10}
|
:whisky 10}
|
||||||
:port true
|
:port true
|
||||||
:prices
|
:prices
|
||||||
;; `prices`: the current price (both buying and selling, for simplicity)
|
;; `prices`: the current price (both buying and selling, for simplicity)
|
||||||
|
@ -34,7 +34,7 @@
|
||||||
:fish 1
|
:fish 1
|
||||||
:leather 1
|
:leather 1
|
||||||
:iron 1
|
:iron 1
|
||||||
:tobacco 1}
|
:whisky 1}
|
||||||
:stock
|
:stock
|
||||||
;; `stock` is the quantity of each commodity in the market at any
|
;; `stock` is the quantity of each commodity in the market at any
|
||||||
;; given time. It is adjusted for production and consumption at
|
;; given time. It is adjusted for production and consumption at
|
||||||
|
@ -43,7 +43,7 @@
|
||||||
:fish 0
|
:fish 0
|
||||||
:leather 0
|
:leather 0
|
||||||
:iron 0
|
:iron 0
|
||||||
:tobacco 0}
|
:whisky 0}
|
||||||
:cash 100}
|
:cash 100}
|
||||||
:buckie
|
:buckie
|
||||||
{:id :buckie
|
{:id :buckie
|
||||||
|
@ -52,19 +52,19 @@
|
||||||
:demands
|
:demands
|
||||||
{:cloth 5
|
{:cloth 5
|
||||||
:leather 3
|
:leather 3
|
||||||
:tobacco 5
|
:whisky 5
|
||||||
:iron 1}
|
:iron 1}
|
||||||
:port true
|
:port true
|
||||||
:prices {:cloth 1
|
:prices {:cloth 1
|
||||||
:fish 1
|
:fish 1
|
||||||
:leather 1
|
:leather 1
|
||||||
:iron 1
|
:iron 1
|
||||||
:tobacco 1}
|
:whisky 1}
|
||||||
:stock {:cloth 0
|
:stock {:cloth 0
|
||||||
:fish 0
|
:fish 0
|
||||||
:leather 0
|
:leather 0
|
||||||
:iron 0
|
:iron 0
|
||||||
:tobacco 0}
|
:whisky 0}
|
||||||
:cash 100}
|
:cash 100}
|
||||||
:callander
|
:callander
|
||||||
{:id :callander
|
{:id :callander
|
||||||
|
@ -72,18 +72,18 @@
|
||||||
:demands
|
:demands
|
||||||
{:cloth 5
|
{:cloth 5
|
||||||
:fish 3
|
:fish 3
|
||||||
:tobacco 5
|
:whisky 5
|
||||||
:iron 1}
|
:iron 1}
|
||||||
:prices {:cloth 1
|
:prices {:cloth 1
|
||||||
:fish 1
|
:fish 1
|
||||||
:leather 1
|
:leather 1
|
||||||
:iron 1
|
:iron 1
|
||||||
:tobacco 1}
|
:whisky 1}
|
||||||
:stock {:cloth 0
|
:stock {:cloth 0
|
||||||
:fish 0
|
:fish 0
|
||||||
:leather 0
|
:leather 0
|
||||||
:iron 0
|
:iron 0
|
||||||
:tobacco 0}
|
:whisky 0}
|
||||||
:cash 100}
|
:cash 100}
|
||||||
:dundee {:id :dundee}
|
:dundee {:id :dundee}
|
||||||
:edinburgh {:id :dundee}
|
:edinburgh {:id :dundee}
|
||||||
|
@ -93,23 +93,23 @@
|
||||||
:demands
|
:demands
|
||||||
{:cloth 5
|
{:cloth 5
|
||||||
:leather 3
|
:leather 3
|
||||||
:tobacco 5
|
:whisky 5
|
||||||
:fish 10}
|
:fish 10}
|
||||||
:port true
|
:port true
|
||||||
:prices {:cloth 1
|
:prices {:cloth 1
|
||||||
:fish 1
|
:fish 1
|
||||||
:leather 1
|
:leather 1
|
||||||
:iron 1
|
:iron 1
|
||||||
:tobacco 1}
|
:whisky 1}
|
||||||
:stock {:cloth 0
|
:stock {:cloth 0
|
||||||
:fish 0
|
:fish 0
|
||||||
:leather 0
|
:leather 0
|
||||||
:iron 0
|
:iron 0
|
||||||
:tobacco 0}
|
:whisky 0}
|
||||||
:cash 100}
|
:cash 100}
|
||||||
:glasgow
|
:glasgow
|
||||||
{:id :glasgow
|
{:id :glasgow
|
||||||
:supplies {:tobacco 10}
|
:supplies {:whisky 10}
|
||||||
:demands
|
:demands
|
||||||
{:cloth 5
|
{:cloth 5
|
||||||
:leather 3
|
:leather 3
|
||||||
|
@ -120,12 +120,12 @@
|
||||||
:fish 1
|
:fish 1
|
||||||
:leather 1
|
:leather 1
|
||||||
:iron 1
|
:iron 1
|
||||||
:tobacco 1}
|
:whisky 1}
|
||||||
:stock {:cloth 0
|
:stock {:cloth 0
|
||||||
:fish 0
|
:fish 0
|
||||||
:leather 0
|
:leather 0
|
||||||
:iron 0
|
:iron 0
|
||||||
:tobacco 0}
|
:whisky 0}
|
||||||
:cash 100}}
|
:cash 100}}
|
||||||
:merchants
|
:merchants
|
||||||
{:archie {:id :archie
|
{:archie {:id :archie
|
||||||
|
@ -171,7 +171,7 @@
|
||||||
{:cloth {:id :cloth :cost 1 :weight 0.25}
|
{:cloth {:id :cloth :cost 1 :weight 0.25}
|
||||||
:fish {:id :fish :cost 1 :weight 1}
|
:fish {:id :fish :cost 1 :weight 1}
|
||||||
:leather {:id :leather :cost 1 :weight 0.5}
|
:leather {:id :leather :cost 1 :weight 0.5}
|
||||||
:tobacco {:id :tobacco :cost 1 :weight 0.1}
|
:whisky {:id :whisky :cost 1 :weight 0.1}
|
||||||
:iron {:id :iron :cost 1 :weight 10}}})
|
:iron {:id :iron :cost 1 :weight 10}}})
|
||||||
|
|
||||||
(defn actual-price
|
(defn actual-price
|
||||||
|
|
|
@ -21,3 +21,89 @@
|
||||||
(let [actual (expected-price (-> world :merchants :archie) :iron :buckie)
|
(let [actual (expected-price (-> world :merchants :archie) :iron :buckie)
|
||||||
expected 1.7] ;;
|
expected 1.7] ;;
|
||||||
(is (= actual expected) "if information select the most recent")))))
|
(is (= actual expected) "if information select the most recent")))))
|
||||||
|
|
||||||
|
(deftest plan-trade-test
|
||||||
|
(testing "Lower level trade planning"
|
||||||
|
(let [world (deep-merge
|
||||||
|
default-world
|
||||||
|
{:merchants
|
||||||
|
{:fiona
|
||||||
|
{:known-prices
|
||||||
|
{:aberdeen
|
||||||
|
{:iron
|
||||||
|
[{:price 1.5 :date 1}
|
||||||
|
{:price 1.3 :date 0}]}
|
||||||
|
:buckie
|
||||||
|
{:iron
|
||||||
|
[{:price 1.7 :date 1}
|
||||||
|
{:price 2 :date 0}]}}}}
|
||||||
|
:cities
|
||||||
|
{:falkirk
|
||||||
|
{:stock {:iron 20}}}})
|
||||||
|
actual (plan-trade :fiona world :iron)]
|
||||||
|
(is (= (:origin actual) :falkirk)
|
||||||
|
"Fiona is in Falkirk, so her plan must originate there")
|
||||||
|
(is (= (:commodity actual) :iron)
|
||||||
|
"Iron is the only thing available in Falkirk, so plan must carry iron")
|
||||||
|
(is (= (:destination actual) :buckie)
|
||||||
|
"Fiona believes Buckie offers the best price for iron, so should go there"))))
|
||||||
|
|
||||||
|
|
||||||
|
(deftest select-cargo-test
|
||||||
|
(testing "Top level single trade planning: single candidate commodity"
|
||||||
|
(let [world (deep-merge
|
||||||
|
default-world
|
||||||
|
{:merchants
|
||||||
|
{:fiona
|
||||||
|
{:known-prices
|
||||||
|
{:aberdeen
|
||||||
|
{:iron
|
||||||
|
[{:price 1.5 :date 1}
|
||||||
|
{:price 1.3 :date 0}]}
|
||||||
|
:buckie
|
||||||
|
{:iron
|
||||||
|
[{:price 1.7 :date 1}
|
||||||
|
{:price 2 :date 0}]}}}}
|
||||||
|
:cities
|
||||||
|
{:falkirk
|
||||||
|
{:stock {:iron 20}}}})
|
||||||
|
actual (select-cargo :fiona world)]
|
||||||
|
(is (= (:origin actual) :falkirk)
|
||||||
|
"Fiona is in Falkirk, so her plan must originate there")
|
||||||
|
(is (= (:commodity actual) :iron)
|
||||||
|
"Iron is the only thing available in Falkirk, so plan must carry iron")
|
||||||
|
(is (= (:destination actual) :buckie)
|
||||||
|
"Fiona believes Buckie offers the best price for iron, so should go there")
|
||||||
|
(is (= (:quantity actual) 1)
|
||||||
|
"Fiona can carry only one unit of iron.")
|
||||||
|
(is (= (:expected-profit actual) 0.7))))
|
||||||
|
(testing "Top level single trade planning: multiple candidate commodities"
|
||||||
|
(let [world (deep-merge
|
||||||
|
default-world
|
||||||
|
{:merchants
|
||||||
|
{:fiona
|
||||||
|
{:known-prices
|
||||||
|
{:aberdeen
|
||||||
|
{:iron
|
||||||
|
[{:price 1.5 :date 1}
|
||||||
|
{:price 1.3 :date 0}]
|
||||||
|
:whisky [{:price 4 :date 0}]}
|
||||||
|
:buckie
|
||||||
|
{:iron
|
||||||
|
[{:price 1.7 :date 1}
|
||||||
|
{:price 2 :date 0}]}}}}
|
||||||
|
:cities
|
||||||
|
{:falkirk
|
||||||
|
{:stock
|
||||||
|
{:iron 20
|
||||||
|
:whisky 50}}}})
|
||||||
|
actual (select-cargo :fiona world)]
|
||||||
|
(is (= (:origin actual) :falkirk)
|
||||||
|
"Fiona is in Falkirk, so her plan must originate there")
|
||||||
|
(is (= (:commodity actual) :whisky)
|
||||||
|
"Whisky has the higher profit, so plan must carry whisky")
|
||||||
|
(is (= (:destination actual) :aberdeen)
|
||||||
|
"Fiona believes Aberdeen offers the best price for whisky, so should go there")
|
||||||
|
(is (= (:quantity actual) 50)
|
||||||
|
"Fiona can carry 100 units of whisky, but only 50 are available.")
|
||||||
|
(is (= (:expected-profit actual) 150)))))
|
||||||
|
|
Loading…
Reference in a new issue