the-great-game/src/the_great_game/merchants/merchants.clj

157 lines
5.5 KiB
Clojure

(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]]))
(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 (-> m :stock)]
(reduce
+
0
(map
#(* (cargo %) (-> world :commodities % :weight))
(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.
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 [m (cond
(keyword? merchant)
(-> 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)))))
(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 [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)
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 (-> m :location)
available (-> world :cities origin :stock)
plans (map
#(augment-plan
m
world
(find-trade-plan 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))]
(first
(sort
#(compare (:dist-to-home %1) (:dist-to-home %2))
(filter
#(and
(= (:expected-profit %) best-profit)
(= (:distance %) nearest))
plans))) ))