Merchant route selection: very basic stuff works
Tests pass.
This commit is contained in:
parent
a7f90fa60f
commit
5619b9c1e5
15 changed files with 694 additions and 299 deletions
6
src/the_great_game/core.clj
Normal file
6
src/the_great_game/core.clj
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
(ns the-great-game.core)
|
||||
|
||||
(defn foo
|
||||
"I don't do a whole lot."
|
||||
[x]
|
||||
(println x "Hello, World!"))
|
||||
156
src/the_great_game/merchants/merchants.clj
Normal file
156
src/the_great_game/merchants/merchants.clj
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
(ns the-great-game.merchants.merchants
|
||||
"Trade planning for merchants, primarily."
|
||||
(:require [the-great-game.world.routes :refer [find-routes]]
|
||||
[the-great-game.world.world :refer [actual-price]]))
|
||||
|
||||
|
||||
(defn expected-price
|
||||
"Find the price anticipated, given this `world`, by this `merchant` for
|
||||
this `commodity` in this `city`. If no information, assume 1.
|
||||
`merchant` should be passed as a map, `commodity` and `city` should be passed as keywords."
|
||||
[merchant commodity city]
|
||||
(or
|
||||
(:price
|
||||
(last
|
||||
(sort-by
|
||||
:date
|
||||
(-> merchant :known-prices city commodity))))
|
||||
1))
|
||||
|
||||
|
||||
(defn burden
|
||||
"The total weight of the current cargo carried by this `merchant` in this
|
||||
`world`."
|
||||
[merchant world]
|
||||
(let [m (cond
|
||||
(keyword? merchant)
|
||||
(-> world :merchants merchant)
|
||||
(map? merchant)
|
||||
merchant)
|
||||
cargo (-> m :stock)]
|
||||
(reduce
|
||||
+
|
||||
0
|
||||
(map
|
||||
#(* (cargo %) (-> world :commodities % :weight))
|
||||
(keys cargo)))))
|
||||
|
||||
|
||||
(defn find-trade-plan
|
||||
"Find the best destination in this `world` for this `commodity` given this
|
||||
`merchant` and this `origin`. If two cities are anticipated to offer the
|
||||
same price, the nearer should be preferred; if two are equally distant, the
|
||||
ones nearer to the merchant's home should be preferred.
|
||||
`merchant` may be passed as a map or a keyword; `commodity` should be
|
||||
passed as a keyword.
|
||||
|
||||
The returned plan is a map with keys:
|
||||
|
||||
# :merchant - the id of the `merchant` for whom the plan was created;
|
||||
# :origin - the city from which the trade starts;
|
||||
# :destination - the city to which the trade is planned;
|
||||
# :commodity - the `commodity` to be carried;
|
||||
# :buy-price - the price at which that `commodity` can be bought;
|
||||
# :expected-price - the price at which the `merchant` anticipates
|
||||
that `commodity` can be sold;
|
||||
# :distance - the number of stages in the planned journey
|
||||
# :dist-to-home - the distance from `destination` to the `merchant`'s
|
||||
home city."
|
||||
[merchant world commodity]
|
||||
(let [m (cond
|
||||
(keyword? merchant)
|
||||
(-> world :merchants merchant)
|
||||
(map? merchant)
|
||||
merchant)
|
||||
origin (-> m :location)
|
||||
destinations (remove #(= % origin) (keys (-> world :cities)))
|
||||
plans (map
|
||||
#(hash-map
|
||||
:merchant (-> m :id)
|
||||
:origin origin
|
||||
:destination %
|
||||
:commodity commodity
|
||||
:buy-price (actual-price world commodity origin)
|
||||
:expected-price (expected-price
|
||||
merchant
|
||||
commodity
|
||||
%)
|
||||
:distance (count
|
||||
(first
|
||||
(find-routes (:routes world) origin %)))
|
||||
:dist-to-home (count
|
||||
(first
|
||||
(find-routes
|
||||
(:routes world)
|
||||
(-> world :merchants merchant :home)
|
||||
%)))
|
||||
)
|
||||
destinations)
|
||||
best-price (apply min (filter number? (map :expected-price plans)))
|
||||
nearest (apply min (map :distance plans))]
|
||||
(first
|
||||
(sort
|
||||
#(compare (:dist-to-home %1) (:dist-to-home %2))
|
||||
(filter
|
||||
#(and
|
||||
(= (:expected-price %) best-price)
|
||||
(= (:distance %) nearest))
|
||||
plans)))))
|
||||
|
||||
|
||||
(defn augment-plan
|
||||
"Augment this `plan` constructed in this `world` for this `merchant` with
|
||||
the `:quantity` of goods which should be bought and the `:expected-profit`
|
||||
of the trade.
|
||||
|
||||
Returns the augmented plan."
|
||||
[merchant world plan]
|
||||
(let [m (cond
|
||||
(keyword? merchant)
|
||||
(-> world :merchants merchant)
|
||||
(map? merchant)
|
||||
merchant)
|
||||
available (-> world :cities (:origin plan) :stock (:commodity plan))
|
||||
can-carry (quot
|
||||
(- (-> m :capacity) (burden m world))
|
||||
(-> world :commodities (:commodity plan) :weight))
|
||||
can-afford (quot
|
||||
(-> merchant :cash)
|
||||
(-> world :commodities (:commodity plan) :weight))
|
||||
q (min available can-carry can-afford)
|
||||
p (* q (- (:expected-price plan) (:buy-price plan)))]
|
||||
(assoc plan :quantity q :expected-profit p)))
|
||||
|
||||
|
||||
(defn select-cargo
|
||||
"A `merchant`, in a given location in a `world`, will choose to buy a cargo
|
||||
within the limit they are capable of carrying, which they can anticipate
|
||||
selling for a profit at a destination."
|
||||
[merchant world]
|
||||
(let [m (cond
|
||||
(keyword? merchant)
|
||||
(-> world :merchants merchant)
|
||||
(map? merchant)
|
||||
merchant)
|
||||
origin (-> m :location)
|
||||
available (-> world :cities origin :stock)
|
||||
plans (map
|
||||
#(augment-plan
|
||||
m
|
||||
world
|
||||
(find-trade-plan m world %))
|
||||
(filter
|
||||
#(let [q (-> world :cities origin :stock %)]
|
||||
(and (number? q) (> q 0)))
|
||||
(keys available)))
|
||||
best-profit (apply min (filter number? (map :expected-profit plans)))
|
||||
nearest (apply min (map :distance plans))]
|
||||
(first
|
||||
(sort
|
||||
#(compare (:dist-to-home %1) (:dist-to-home %2))
|
||||
(filter
|
||||
#(and
|
||||
(= (:expected-profit %) best-profit)
|
||||
(= (:distance %) nearest))
|
||||
plans))) ))
|
||||
|
||||
0
src/the_great_game/merchants/strategy.clj
Normal file
0
src/the_great_game/merchants/strategy.clj
Normal file
17
src/the_great_game/utils.clj
Normal file
17
src/the_great_game/utils.clj
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
(ns the-great-game.utils)
|
||||
|
||||
|
||||
(defn cyclic?
|
||||
"True if two or more elements of `route` are identical"
|
||||
[route]
|
||||
(not (= (count route)(count (set route)))))
|
||||
|
||||
(defn deep-merge
|
||||
"Recursively merges maps. Stolen from
|
||||
https://dnaeon.github.io/recursively-merging-maps-in-clojure/"
|
||||
[& maps]
|
||||
(letfn [(m [& xs]
|
||||
(if (some #(and (map? %) (not (record? %))) xs)
|
||||
(apply merge-with m xs)
|
||||
(last xs)))]
|
||||
(reduce m maps)))
|
||||
46
src/the_great_game/world/routes.clj
Normal file
46
src/the_great_game/world/routes.clj
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
(ns the-great-game.world.routes
|
||||
"Conceptual (plan level) routes, represented as tuples of location ids."
|
||||
(:require [the-great-game.utils :refer [cyclic?]]))
|
||||
|
||||
(defn find-routes
|
||||
"Find routes from among these `routes` from `from`; if `to` is supplied,
|
||||
to `to`, by breadth-first search."
|
||||
([routes from]
|
||||
(map
|
||||
(fn [to] (cons from to))
|
||||
(remove
|
||||
#(empty? %)
|
||||
(map
|
||||
(fn [route]
|
||||
(filter
|
||||
#(not (= from %))
|
||||
(if (some #(= % from) route) route)))
|
||||
routes))))
|
||||
([routes from to]
|
||||
(let [steps (find-routes routes from)
|
||||
found (filter
|
||||
(fn [step] (if (some #(= to %) step) step))
|
||||
steps)]
|
||||
(if
|
||||
(empty? found)
|
||||
(find-routes routes from to steps)
|
||||
found)))
|
||||
([routes from to steps]
|
||||
(if
|
||||
(not (empty? steps))
|
||||
(let [paths (remove
|
||||
cyclic?
|
||||
(apply
|
||||
concat
|
||||
(map
|
||||
(fn [path]
|
||||
(map
|
||||
(fn [x] (concat path (rest x)))
|
||||
(find-routes routes (last path))))
|
||||
steps)))
|
||||
found (filter
|
||||
#(= (last %) to) paths)]
|
||||
(if
|
||||
(empty? found)
|
||||
(find-routes routes from to paths)
|
||||
found)))))
|
||||
184
src/the_great_game/world/world.clj
Normal file
184
src/the_great_game/world/world.clj
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
(ns the-great-game.world.world
|
||||
"Access to data about the world")
|
||||
|
||||
;;; The world has to work either as map or a database. Initially, and for
|
||||
;;; unit tests, I'll use a map; later, there will be a database. But the
|
||||
;;; API needs to be agnostic, so that heirarchies which interact with
|
||||
;;; `world` don't have to know which they've got - as far as they're concerned
|
||||
;;; it's just a handle.
|
||||
|
||||
(def default-world
|
||||
"A basic world for testing concepts"
|
||||
{:cities
|
||||
{:aberdeen
|
||||
{:id :aberdeen
|
||||
:supplies
|
||||
;; `supplies` is the quantity of each commodity added to the stock
|
||||
;; each game day. If the price in the market is lower than 1 (the
|
||||
;; cost of production of a unit) no goods will be added.
|
||||
{:fish 10
|
||||
:leather 5}
|
||||
:demands
|
||||
;; `stock` is the quantity of each commodity in the market at any
|
||||
;; given time. It is adjusted for production and consumption at
|
||||
;; the end of each game day.
|
||||
{:iron 1
|
||||
:cloth 10
|
||||
:tobacco 10}
|
||||
:port true
|
||||
:prices
|
||||
;; `prices`: the current price (both buying and selling, for simplicity)
|
||||
;; of each commodity in the market. Updated each game day based on current
|
||||
;; stock.
|
||||
{:cloth 1
|
||||
:fish 1
|
||||
:leather 1
|
||||
:iron 1
|
||||
:tobacco 1}
|
||||
:stock
|
||||
;; `stock` is the quantity of each commodity in the market at any
|
||||
;; given time. It is adjusted for production and consumption at
|
||||
;; the end of each game day.
|
||||
{:cloth 0
|
||||
:fish 0
|
||||
:leather 0
|
||||
:iron 0
|
||||
:tobacco 0}
|
||||
:cash 100}
|
||||
:buckie
|
||||
{:id :buckie
|
||||
:supplies
|
||||
{:fish 20}
|
||||
:demands
|
||||
{:cloth 5
|
||||
:leather 3
|
||||
:tobacco 5
|
||||
:iron 1}
|
||||
:port true
|
||||
:prices {:cloth 1
|
||||
:fish 1
|
||||
:leather 1
|
||||
:iron 1
|
||||
:tobacco 1}
|
||||
:stock {:cloth 0
|
||||
:fish 0
|
||||
:leather 0
|
||||
:iron 0
|
||||
:tobacco 0}
|
||||
:cash 100}
|
||||
:callander
|
||||
{:id :callander
|
||||
:supplies {:leather 20}
|
||||
:demands
|
||||
{:cloth 5
|
||||
:fish 3
|
||||
:tobacco 5
|
||||
:iron 1}
|
||||
:prices {:cloth 1
|
||||
:fish 1
|
||||
:leather 1
|
||||
:iron 1
|
||||
:tobacco 1}
|
||||
:stock {:cloth 0
|
||||
:fish 0
|
||||
:leather 0
|
||||
:iron 0
|
||||
:tobacco 0}
|
||||
:cash 100}
|
||||
:dundee {:id :dundee}
|
||||
:edinburgh {:id :dundee}
|
||||
:falkirk
|
||||
{:id :falkirk
|
||||
:supplies {:iron 10}
|
||||
:demands
|
||||
{:cloth 5
|
||||
:leather 3
|
||||
:tobacco 5
|
||||
:fish 10}
|
||||
:port true
|
||||
:prices {:cloth 1
|
||||
:fish 1
|
||||
:leather 1
|
||||
:iron 1
|
||||
:tobacco 1}
|
||||
:stock {:cloth 0
|
||||
:fish 0
|
||||
:leather 0
|
||||
:iron 0
|
||||
:tobacco 0}
|
||||
:cash 100}
|
||||
:glasgow
|
||||
{:id :glasgow
|
||||
:supplies {:tobacco 10}
|
||||
:demands
|
||||
{:cloth 5
|
||||
:leather 3
|
||||
:iron 5
|
||||
:fish 10}
|
||||
:port true
|
||||
:prices {:cloth 1
|
||||
:fish 1
|
||||
:leather 1
|
||||
:iron 1
|
||||
:tobacco 1}
|
||||
:stock {:cloth 0
|
||||
:fish 0
|
||||
:leather 0
|
||||
:iron 0
|
||||
:tobacco 0}
|
||||
:cash 100}}
|
||||
:merchants
|
||||
{:archie {:id :archie
|
||||
:home :aberdeen :location :aberdeen :cash 100 :capacity 10
|
||||
:known-prices {}
|
||||
:stock {}}
|
||||
:belinda {:id :belinda
|
||||
:home :buckie :location :buckie :cash 100 :capacity 10
|
||||
:known-prices {}
|
||||
:stock {}}
|
||||
:callum {:id :callum
|
||||
:home :callander :location :calander :cash 100 :capacity 10
|
||||
:known-prices {}
|
||||
:stock {}}
|
||||
:deirdre {:id :deidre
|
||||
:home :dundee :location :dundee :cash 100 :capacity 10
|
||||
:known-prices {}
|
||||
:stock {}}
|
||||
:euan {:id :euan
|
||||
:home :edinbirgh :location :edinburgh :cash 100 :capacity 10
|
||||
:known-prices {}
|
||||
:stock {}}
|
||||
:fiona {:id :fiona
|
||||
:home :falkirk :location :falkirk :cash 100 :capacity 10
|
||||
:known-prices {}
|
||||
:stock {}}}
|
||||
:routes
|
||||
;; all routes can be traversed in either direction and are assumed to
|
||||
;; take the same amount of time.
|
||||
[[:aberdeen :buckie]
|
||||
[:aberdeen :dundee]
|
||||
[:callander :glasgow]
|
||||
[:dundee :callander]
|
||||
[:dundee :edinburgh]
|
||||
[:dundee :falkirk]
|
||||
[:edinburgh :falkirk]
|
||||
[:falkirk :glasgow]]
|
||||
:commodities
|
||||
;; cost of commodities is expressed in person/days;
|
||||
;; weight in packhorse loads. Transport in this model
|
||||
;; is all overland; you don't take bulk cargoes overland
|
||||
;; in this period, it's too expensive.
|
||||
{:cloth {:id :cloth :cost 1 :weight 0.25}
|
||||
:fish {:id :fish :cost 1 :weight 1}
|
||||
:leather {:id :leather :cost 1 :weight 0.5}
|
||||
:tobacco {:id :tobacco :cost 1 :weight 0.1}
|
||||
:iron {:id :iron :cost 1 :weight 10}}})
|
||||
|
||||
(defn actual-price
|
||||
"Find the actual current price of this `commodity` in this `city` given
|
||||
this `world`. **NOTE** that merchants can only know the actual prices in
|
||||
the city in which they are currently located."
|
||||
[world commodity city]
|
||||
(-> world :cities city :prices commodity))
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue