001 (ns cc.journeyman.the-great-game.merchants.planning
002 "Trade planning for merchants, primarily. This follows a simple-minded
003 generate-and-test strategy and currently generates plans for all possible
004 routes from the current location. This may not scale. Also, routes do not
005 currently have cost or risk associated with them."
006 (:require [cc.journeyman.the-great-game.utils :refer [deep-merge make-target-filter]]
007 [cc.journeyman.the-great-game.merchants.merchant-utils :refer [can-afford can-carry expected-price]]
008 [cc.journeyman.the-great-game.world.routes :refer [find-route]]
009 [cc.journeyman.the-great-game.world.world :refer [actual-price default-world]]))
010
011 (defn generate-trade-plans
012 "Generate all possible trade plans for this `merchant` and this `commodity`
013 in this `world`.
014
015 Returned plans are maps with keys:
016
017 * :merchant - the id of the `merchant` for whom the plan was created;
018 * :origin - the city from which the trade starts;
019 * :destination - the city to which the trade is planned;
020 * :commodity - the `commodity` to be carried;
021 * :buy-price - the price at which that `commodity` can be bought;
022 * :expected-price - the price at which the `merchant` anticipates
023 that `commodity` can be sold;
024 * :distance - the number of stages in the planned journey
025 * :dist-to-home - the distance from `destination` to the `merchant`'s
026 home city."
027 [merchant world commodity]
028 (let [m (cond
029 (keyword? merchant)
030 (-> world :merchants merchant)
031 (map? merchant)
032 merchant)
033 origin (:location m)]
034 (map
035 #(hash-map
036 :merchant (:id m)
037 :origin origin
038 :destination %
039 :commodity commodity
040 :buy-price (actual-price world commodity origin)
041 :expected-price (expected-price
042 m
043 commodity
044 %)
045 :distance (count
046 (find-route world origin %))
047 :dist-to-home (count
048 (find-route
049 world
050 (:home m)
051 %)))
052 (remove #(= % origin) (-> world :cities keys)))))
053
054 (defn nearest-with-targets
055 "Return the distance to the nearest destination among those of these
056 `plans` which match these `targets`. Plans are expected to be plans
057 as returned by `generate-trade-plans`, q.v.; `targets` are expected to be
058 as accepted by `make-target-filter`, q.v."
059 [plans targets]
060 (apply
061 min
062 (map
063 :distance
064 (filter
065 (make-target-filter targets)
066 plans))))
067
068 (defn plan-trade
069 "Find the best destination in this `world` for this `commodity` given this
070 `merchant` and this `origin`. If two cities are anticipated to offer the
071 same price, the nearer should be preferred; if two are equally distant, the
072 ones nearer to the merchant's home should be preferred.
073 `merchant` may be passed as a map or a keyword; `commodity` should be
074 passed as a keyword.
075
076 The returned plan is a map with keys:
077
078 * :merchant - the id of the `merchant` for whom the plan was created;
079 * :origin - the city from which the trade starts;
080 * :destination - the city to which the trade is planned;
081 * :commodity - the `commodity` to be carried;
082 * :buy-price - the price at which that `commodity` can be bought;
083 * :expected-price - the price at which the `merchant` anticipates
084 that `commodity` can be sold;
085 * :distance - the number of stages in the planned journey
086 * :dist-to-home - the distance from `destination` to the `merchant`'s
087 home city."
088 [merchant world commodity]
089 (let [plans (generate-trade-plans merchant world commodity)
090 best-prices (filter
091 (make-target-filter
092 [[:expected-price
093 (apply
094 max
095 (filter number? (map :expected-price plans)))]])
096 plans)]
097 (first
098 (sort-by
099 ;; all other things being equal, a merchant would prefer to end closer
100 ;; to home.
101 #(- 0 (:dist-to-home %))
102 ;; a merchant will seek the best price, but won't go further than
103 ;; needed to get it.
104 (filter
105 (make-target-filter
106 [[:distance
107 (apply min (filter number? (map :distance best-prices)))]])
108 best-prices)))))
109
110 (defn augment-plan
111 "Augment this `plan` constructed in this `world` for this `merchant` with
112 the `:quantity` of goods which should be bought and the `:expected-profit`
113 of the trade.
114
115 Returns the augmented plan."
116 [merchant world plan]
117 (let [c (:commodity plan)
118 o (:origin plan)
119 q (min
120 (or
121 (-> world :cities o :stock c)
122 0)
123 (can-carry merchant world c)
124 (can-afford merchant world c))
125 p (* q (- (:expected-price plan) (:buy-price plan)))]
126 (assoc plan :quantity q :expected-profit p)))
127
128 (defn select-cargo
129 "A `merchant`, in a given location in a `world`, will choose to buy a cargo
130 within the limit they are capable of carrying, which they can anticipate
131 selling for a profit at a destination."
132 [merchant world]
133 (let [m (cond
134 (keyword? merchant)
135 (-> world :merchants merchant)
136 (map? merchant)
137 merchant)
138 origin (:location m)
139 available (-> world :cities origin :stock)
140 plans (map
141 #(augment-plan
142 m
143 world
144 (plan-trade m world %))
145 (filter
146 #(let [q (-> world :cities origin :stock %)]
147 (and (number? q) (pos? q)))
148 (keys available)))]
149 (if
150 (not (empty? plans))
151 (first
152 (sort-by
153 #(- 0 (:dist-to-home %))
154 (filter
155 (make-target-filter
156 [[:expected-profit
157 (apply max (filter number? (map :expected-profit plans)))]])
158 plans))))))
159