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

174 lines
6.3 KiB
Clojure

(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))))