001  (ns cc.journeyman.the-great-game.merchants.strategies.simple
002    "Default trading strategy for merchants.
003  
004    The simple strategy buys a single product in the local market if there is
005    one which can be traded profitably, trades it to the chosen target market,
006    and sells it there. If there is no commodity locally which can be traded
007    profitably, moves towards home with no cargo. If at home and no commodity
008    can be traded profitably, does not move."
009    (:require [taoensso.timbre :as l :refer [info error spy]]
010              [cc.journeyman.the-great-game.utils :refer [deep-merge]]
011              [cc.journeyman.the-great-game.gossip.gossip :refer [move-gossip]]
012              [cc.journeyman.the-great-game.merchants.planning :refer [augment-plan plan-trade select-cargo]]
013              [cc.journeyman.the-great-game.merchants.merchant-utils :refer
014               [add-stock add-known-prices]]
015              [cc.journeyman.the-great-game.world.routes :refer [find-route]]))
016  
017  (defn plan-and-buy
018    "Return a world like this `world`, in which this `merchant` has planned
019    a new trade, and bought appropriate stock for it. If no profitable trade
020    can be planned, the merchant is simply moved towards their home."
021    [merchant world]
022    (let [m (cond
023              (keyword? merchant)
024              (-> world :merchants merchant)
025              (map? merchant)
026              merchant)
027          id (:id m)
028          location (:location m)
029          market (-> world :cities location)
030          plan (select-cargo merchant world)]
031      (l/debug "plan-and-buy: merchant" id)
032      (cond
033        (seq? plan)
034        (let
035          [c (:commodity plan)
036           p (* (:quantity plan) (:buy-price plan))
037           q (:quantity plan)]
038          (l/info "Merchant" id "bought" q "units of" c "at" location "for" p plan)
039          {:merchants
040           {id
041            {:stock (add-stock (:stock m) {c q})
042             :cash (- (:cash m) p)
043             :known-prices (add-known-prices m world)
044             :plan plan}}
045           :cities
046           {location
047            {:stock (assoc (:stock market) c (- (-> market :stock c) q))
048             :cash (+ (:cash market) p)}}})
049        ;; if no plan, then if at home stay put
050        (= (:location m) (:home m))
051        (do
052          (l/info "Merchant" id "remains at home in" location)
053          {})
054        ;; else move towards home
055        :else
056        (let [route (find-route world location (:home m))
057              next-location (nth route 1)]
058          (l/info "No trade possible at" location "; merchant" id "moves to" next-location)
059          (merge
060            {:merchants
061             {id
062              {:location next-location}}}
063            (move-gossip id world next-location))))))
064  
065  (defn re-plan
066    "Having failed to sell a cargo at current location, re-plan a route to
067    sell the current cargo. Returns a revised world."
068    [merchant world]
069    (let [m (cond
070              (keyword? merchant)
071              (-> world :merchants merchant)
072              (map? merchant)
073              merchant)
074          id (:id m)
075          location (:location m)
076          plan (augment-plan m world (plan-trade m world (-> m :plan :commodity)))]
077      (l/debug "re-plan: merchant" id)
078      (deep-merge
079        world
080        {:merchants
081         {id
082          {:plan plan}}})))
083  
084  (defn sell-and-buy
085    "Return a new world like this `world`, in which this `merchant` has sold
086    their current stock in their current location, and planned a new trade, and
087    bought appropriate stock for it."
088    ;; TODO: this either sells the entire cargo, or, if the market can't afford
089    ;; it, none of it. And it does not cope with selling different commodities
090    ;; in different markets.
091    [merchant world]
092    (let [m (cond
093              (keyword? merchant)
094              (-> world :merchants merchant)
095              (map? merchant)
096              merchant)
097          id (:id m)
098          location (:location m)
099          market (-> world :cities location)
100          stock-value (reduce
101                        +
102                        (map
103                          #(* (-> m :stock %) (-> market :prices m))
104                          (keys (:stock m))))]
105      (l/debug "sell-and-buy: merchant" id)
106      (if
107        (>= (:cash market) stock-value)
108        (do
109          (l/info "Merchant" id "sells" (:stock m) "at" location "for" stock-value)
110          (plan-and-buy
111            merchant
112            (deep-merge
113              world
114              {:merchants
115               {id
116                {:stock {}
117                 :cash (+ (:cash m) stock-value)
118                 :known-prices (add-known-prices m world)}}
119               :cities
120               {location
121                {:stock (add-stock (:stock m) (:stock market))
122                 :cash (- (:cash market) stock-value)}}})))
123        ;; else
124        (re-plan merchant world))))
125  
126  (defn move-merchant
127    "Handle general en route movement of this `merchant` in this `world`;
128    return a (partial or full) world like this `world` but in which the
129    merchant may have been moved ot updated."
130    [merchant world]
131    (let [m (cond
132              (keyword? merchant)
133              (-> world :merchants merchant)
134              (map? merchant)
135              merchant)
136          id (:id m)
137          at-destination? (and (:plan m) (= (:location m) (-> m :plan :destination)))
138          plan (:plan m)
139          next-location (if plan
140                          (nth
141                            (find-route
142                              world
143                              (:location m)
144                              (:destination plan))
145                            1)
146                          (:location m))]
147      (l/debug "move-merchant: merchant" id "at" (:location m)
148               "destination" (-> m :plan :destination) "next" next-location
149               "at destination" at-destination?)
150      (cond
151        ;; if the merchant is at the destination of their current plan
152        ;; sell all cargo and repurchase.
153        at-destination?
154        (sell-and-buy merchant world)
155        ;; if they don't have a plan, seek to create one
156        (nil? plan)
157        (plan-and-buy merchant world)
158        ;; otherwise, move one step towards their destination
159        (and next-location (not= next-location (:location m)))
160        (do
161          (l/info "Merchant " id " moving from " (:location m) " to " next-location)
162          (deep-merge
163            {:merchants
164             {id
165              {:location next-location
166               :known-prices (add-known-prices m world)}}}
167            (move-gossip id world next-location)))
168        :else
169        (do
170          (l/info "Merchant" id "has plan but no next-location; currently at"
171                  (:location m) ", destination is" (:destination plan))
172          world))))
173