001 (ns 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 [the-great-game.utils :refer [deep-merge]]
011 [the-great-game.gossip.gossip :refer [move-gossip]]
012 [the-great-game.merchants.planning :refer :all]
013 [the-great-game.merchants.merchant-utils :refer
014 [add-stock add-known-prices]]
015 [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 (not (empty? 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