Cargo selection planning now works.
This commit is contained in:
parent
11506334f1
commit
fcf8a8fa0d
|
@ -1,7 +1,7 @@
|
|||
(ns the-great-game.merchants.merchants
|
||||
"Trade planning for merchants, primarily."
|
||||
(: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
|
||||
|
@ -36,15 +36,31 @@
|
|||
(keys cargo)))))
|
||||
|
||||
|
||||
(defn find-trade-plan
|
||||
"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.
|
||||
(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)))))
|
||||
|
||||
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;
|
||||
* :origin - the city from which the trade starts;
|
||||
|
@ -62,41 +78,109 @@
|
|||
(-> world :merchants merchant)
|
||||
(map? merchant)
|
||||
merchant)
|
||||
origin (-> m :location)
|
||||
destinations (remove #(= % origin) (keys (-> world :cities)))
|
||||
plans (map
|
||||
#(hash-map
|
||||
:merchant (-> m :id)
|
||||
:origin origin
|
||||
:destination %
|
||||
:commodity commodity
|
||||
:buy-price (actual-price world commodity origin)
|
||||
:expected-price (expected-price
|
||||
merchant
|
||||
commodity
|
||||
%)
|
||||
:distance (count
|
||||
(first
|
||||
(find-routes (:routes world) origin %)))
|
||||
:dist-to-home (count
|
||||
(first
|
||||
(find-routes
|
||||
(:routes world)
|
||||
(-> world :merchants merchant :home)
|
||||
%)))
|
||||
)
|
||||
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)))))
|
||||
origin (-> m :location)]
|
||||
(map
|
||||
#(hash-map
|
||||
:merchant (-> m :id)
|
||||
:origin origin
|
||||
:destination %
|
||||
:commodity commodity
|
||||
:buy-price (actual-price world commodity origin)
|
||||
:expected-price (expected-price
|
||||
m
|
||||
commodity
|
||||
%)
|
||||
:distance (count
|
||||
(first
|
||||
(find-routes (:routes world) origin %)))
|
||||
:dist-to-home (count
|
||||
(first
|
||||
(find-routes
|
||||
(:routes world)
|
||||
(:home m)
|
||||
%)))
|
||||
)
|
||||
(remove #(= % origin) (keys (-> world :cities))))))
|
||||
|
||||
(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
|
||||
"Augment this `plan` constructed in this `world` for this `merchant` with
|
||||
|
@ -105,22 +189,21 @@
|
|||
|
||||
Returns the augmented plan."
|
||||
[merchant world plan]
|
||||
(let [m (cond
|
||||
(keyword? merchant)
|
||||
(-> world :merchants merchant)
|
||||
(map? merchant)
|
||||
merchant)
|
||||
available (-> world :cities (:origin plan) :stock (:commodity plan))
|
||||
can-carry (quot
|
||||
(- (-> m :capacity) (burden m world))
|
||||
(-> world :commodities (:commodity plan) :weight))
|
||||
can-afford (quot
|
||||
(-> merchant :cash)
|
||||
(-> world :commodities (:commodity plan) :weight))
|
||||
q (min available can-carry can-afford)
|
||||
(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
|
||||
|
@ -138,19 +221,17 @@
|
|||
#(augment-plan
|
||||
m
|
||||
world
|
||||
(find-trade-plan m world %))
|
||||
(plan-trade m world %))
|
||||
(filter
|
||||
#(let [q (-> world :cities origin :stock %)]
|
||||
(and (number? q) (> q 0)))
|
||||
(keys available)))
|
||||
best-profit (apply min (filter number? (map :expected-profit plans)))
|
||||
nearest (apply min (map :distance plans))]
|
||||
(keys available)))]
|
||||
(first
|
||||
(sort
|
||||
#(compare (:dist-to-home %1) (:dist-to-home %2))
|
||||
(sort-by
|
||||
#(- 0 (:dist-to-home %))
|
||||
(filter
|
||||
#(and
|
||||
(= (:expected-profit %) best-profit)
|
||||
(= (:distance %) nearest))
|
||||
plans))) ))
|
||||
(make-target-filter
|
||||
[[:expected-profit
|
||||
(apply max (filter number? (map :expected-profit plans)))]])
|
||||
plans)))))
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@
|
|||
;; the end of each game day.
|
||||
{:iron 1
|
||||
:cloth 10
|
||||
:tobacco 10}
|
||||
:whisky 10}
|
||||
:port true
|
||||
:prices
|
||||
;; `prices`: the current price (both buying and selling, for simplicity)
|
||||
|
@ -34,7 +34,7 @@
|
|||
:fish 1
|
||||
:leather 1
|
||||
:iron 1
|
||||
:tobacco 1}
|
||||
:whisky 1}
|
||||
:stock
|
||||
;; `stock` is the quantity of each commodity in the market at any
|
||||
;; given time. It is adjusted for production and consumption at
|
||||
|
@ -43,7 +43,7 @@
|
|||
:fish 0
|
||||
:leather 0
|
||||
:iron 0
|
||||
:tobacco 0}
|
||||
:whisky 0}
|
||||
:cash 100}
|
||||
:buckie
|
||||
{:id :buckie
|
||||
|
@ -52,19 +52,19 @@
|
|||
:demands
|
||||
{:cloth 5
|
||||
:leather 3
|
||||
:tobacco 5
|
||||
:whisky 5
|
||||
:iron 1}
|
||||
:port true
|
||||
:prices {:cloth 1
|
||||
:fish 1
|
||||
:leather 1
|
||||
:iron 1
|
||||
:tobacco 1}
|
||||
:whisky 1}
|
||||
:stock {:cloth 0
|
||||
:fish 0
|
||||
:leather 0
|
||||
:iron 0
|
||||
:tobacco 0}
|
||||
:whisky 0}
|
||||
:cash 100}
|
||||
:callander
|
||||
{:id :callander
|
||||
|
@ -72,18 +72,18 @@
|
|||
:demands
|
||||
{:cloth 5
|
||||
:fish 3
|
||||
:tobacco 5
|
||||
:whisky 5
|
||||
:iron 1}
|
||||
:prices {:cloth 1
|
||||
:fish 1
|
||||
:leather 1
|
||||
:iron 1
|
||||
:tobacco 1}
|
||||
:whisky 1}
|
||||
:stock {:cloth 0
|
||||
:fish 0
|
||||
:leather 0
|
||||
:iron 0
|
||||
:tobacco 0}
|
||||
:whisky 0}
|
||||
:cash 100}
|
||||
:dundee {:id :dundee}
|
||||
:edinburgh {:id :dundee}
|
||||
|
@ -93,23 +93,23 @@
|
|||
:demands
|
||||
{:cloth 5
|
||||
:leather 3
|
||||
:tobacco 5
|
||||
:whisky 5
|
||||
:fish 10}
|
||||
:port true
|
||||
:prices {:cloth 1
|
||||
:fish 1
|
||||
:leather 1
|
||||
:iron 1
|
||||
:tobacco 1}
|
||||
:whisky 1}
|
||||
:stock {:cloth 0
|
||||
:fish 0
|
||||
:leather 0
|
||||
:iron 0
|
||||
:tobacco 0}
|
||||
:whisky 0}
|
||||
:cash 100}
|
||||
:glasgow
|
||||
{:id :glasgow
|
||||
:supplies {:tobacco 10}
|
||||
:supplies {:whisky 10}
|
||||
:demands
|
||||
{:cloth 5
|
||||
:leather 3
|
||||
|
@ -120,12 +120,12 @@
|
|||
:fish 1
|
||||
:leather 1
|
||||
:iron 1
|
||||
:tobacco 1}
|
||||
:whisky 1}
|
||||
:stock {:cloth 0
|
||||
:fish 0
|
||||
:leather 0
|
||||
:iron 0
|
||||
:tobacco 0}
|
||||
:whisky 0}
|
||||
:cash 100}}
|
||||
:merchants
|
||||
{:archie {:id :archie
|
||||
|
@ -171,7 +171,7 @@
|
|||
{:cloth {:id :cloth :cost 1 :weight 0.25}
|
||||
:fish {:id :fish :cost 1 :weight 1}
|
||||
: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}}})
|
||||
|
||||
(defn actual-price
|
||||
|
|
|
@ -21,3 +21,89 @@
|
|||
(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"
|
||||
(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