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