diff --git a/docs/index.html b/docs/index.html index 1f40cc8..5370800 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,3 +1,3 @@ -The-great-game 0.1.0-SNAPSHOT

The-great-game 0.1.0-SNAPSHOT

Released under the GNU General Public License,version 2.0 or (at your option) any later version

Prototype code towards the great game I've been writing about for ten years, and know I will never finish.

Installation

To install, add the following dependency to your project or build file:

[the-great-game "0.1.0-SNAPSHOT"]

Topics

Namespaces

the-great-game.core

TODO: write docs

Public variables and functions:

the-great-game.gossip.gossip

Interchange of news events between agents agents

Public variables and functions:

the-great-game.merchants.markets

Adjusting quantities and prices in markets.

Public variables and functions:

the-great-game.merchants.merchants

Trade planning for merchants, primarily.

the-great-game.utils

TODO: write docs

Public variables and functions:

the-great-game.world.routes

Conceptual (plan level) routes, represented as tuples of location ids.

Public variables and functions:

the-great-game.world.run

Run the whole simulation

Public variables and functions:

the-great-game.world.world

Access to data about the world

Public variables and functions:

\ No newline at end of file +The-great-game 0.1.0-SNAPSHOT

The-great-game 0.1.0-SNAPSHOT

Released under the GNU General Public License,version 2.0 or (at your option) any later version

Prototype code towards the great game I've been writing about for ten years, and know I will never finish.

Installation

To install, add the following dependency to your project or build file:

[the-great-game "0.1.0-SNAPSHOT"]

Topics

Namespaces

the-great-game.core

TODO: write docs

Public variables and functions:

the-great-game.gossip.gossip

Interchange of news events between agents agents

Public variables and functions:

the-great-game.merchants.markets

Adjusting quantities and prices in markets.

Public variables and functions:

the-great-game.merchants.merchant-utils

Useful functions for doing low-level things with merchants.

the-great-game.merchants.merchants

Trade planning for merchants, primarily.

Public variables and functions:

the-great-game.merchants.planning

Trade planning for merchants, primarily.

the-great-game.merchants.strategies.simple

Default trading strategy for merchants.

Public variables and functions:

the-great-game.utils

TODO: write docs

Public variables and functions:

the-great-game.world.routes

Conceptual (plan level) routes, represented as tuples of location ids.

Public variables and functions:

the-great-game.world.run

Run the whole simulation

Public variables and functions:

the-great-game.world.world

Access to data about the world

Public variables and functions:

\ No newline at end of file diff --git a/docs/intro.html b/docs/intro.html index 7d118f7..3822424 100644 --- a/docs/intro.html +++ b/docs/intro.html @@ -1,6 +1,6 @@ -Introduction to the-great-game

Introduction to the-great-game

+Introduction to the-great-game

Introduction to the-great-game

The Great Game

In this essay I’m going to try to pull together a number of my architectural ideas about the Great Game which I know I’m never actually going to build - because it’s vastly too big for any one person to build - into one overall vision.

So, firstly, how does one characterise this game?

diff --git a/docs/the-great-game.core.html b/docs/the-great-game.core.html index 5fe59ef..504fb69 100644 --- a/docs/the-great-game.core.html +++ b/docs/the-great-game.core.html @@ -1,3 +1,3 @@ -the-great-game.core documentation

the-great-game.core

TODO: write docs

foo

(foo x)

I don’t do a whole lot.

\ No newline at end of file +the-great-game.core documentation

the-great-game.core

TODO: write docs

foo

(foo x)

I don’t do a whole lot.

\ No newline at end of file diff --git a/docs/the-great-game.gossip.gossip.html b/docs/the-great-game.gossip.gossip.html index 460937c..1245955 100644 --- a/docs/the-great-game.gossip.gossip.html +++ b/docs/the-great-game.gossip.gossip.html @@ -1,3 +1,3 @@ -the-great-game.gossip.gossip documentation

the-great-game.gossip.gossip

Interchange of news events between agents agents

dialogue

(dialogue enquirer respondent world)

Dialogue between an enquirer and an agent in this world; returns a map identical to enquirer except that its :gossip collection may have additional entries.

gather-news

(gather-news world)(gather-news world gossip)

TODO: write docs

move-gossip

(move-gossip gossip world new-location)

Return a world like this world but with this gossip moved to this new-location. Many gossips are essentially shadow-records of agents of other types, and the movement if the gossip should be controlled by the run function of the type of the record they shadow. The #run function below does NOT call this function.

run

(run world)

Return a world like this world, with news items exchanged between gossip agents.

\ No newline at end of file +the-great-game.gossip.gossip documentation

the-great-game.gossip.gossip

Interchange of news events between agents agents

dialogue

(dialogue enquirer respondent world)

Dialogue between an enquirer and an agent in this world; returns a map identical to enquirer except that its :gossip collection may have additional entries.

gather-news

(gather-news world)(gather-news world gossip)

TODO: write docs

move-gossip

(move-gossip gossip world new-location)

Return a world like this world but with this gossip moved to this new-location. Many gossips are essentially shadow-records of agents of other types, and the movement if the gossip should be controlled by the run function of the type of the record they shadow. The #run function below does NOT call this function.

run

(run world)

Return a world like this world, with news items exchanged between gossip agents.

\ No newline at end of file diff --git a/docs/the-great-game.merchants.markets.html b/docs/the-great-game.merchants.markets.html index 51fd23c..6358e64 100644 --- a/docs/the-great-game.merchants.markets.html +++ b/docs/the-great-game.merchants.markets.html @@ -1,3 +1,3 @@ -the-great-game.merchants.markets documentation

the-great-game.merchants.markets

Adjusting quantities and prices in markets.

adjust-quantity-and-price

(adjust-quantity-and-price world city commodity)

Adjust the quantity of this commodity currently in stock in this city of this world. Return a fragmentary world which can be deep-merged into this world.

new-price

(new-price old stock supply demand)

If stock is greater than the maximum of supply and demand, then there is surplus and old price is too high, so shold be reduced. If lower, then it is too low and should be increased.

run

(run world)

Return a world like this world, with quantities and prices in markets updated to reflect supply and demand.

update-markets

(update-markets world)(update-markets world city)(update-markets world city commodity)

Return a world like this world, with quantities and prices in markets updated to reflect supply and demand. If city or city and commodity are specified, return a fragmentary world with only the changes for that city (and commodity if specified) populated.

\ No newline at end of file +the-great-game.merchants.markets documentation

the-great-game.merchants.markets

Adjusting quantities and prices in markets.

adjust-quantity-and-price

(adjust-quantity-and-price world city commodity)

Adjust the quantity of this commodity currently in stock in this city of this world. Return a fragmentary world which can be deep-merged into this world.

new-price

(new-price old stock supply demand)

If stock is greater than the maximum of supply and demand, then there is surplus and old price is too high, so shold be reduced. If lower, then it is too low and should be increased.

run

(run world)

Return a world like this world, with quantities and prices in markets updated to reflect supply and demand.

update-markets

(update-markets world)(update-markets world city)(update-markets world city commodity)

Return a world like this world, with quantities and prices in markets updated to reflect supply and demand. If city or city and commodity are specified, return a fragmentary world with only the changes for that city (and commodity if specified) populated.

\ No newline at end of file diff --git a/docs/the-great-game.merchants.merchant-utils.html b/docs/the-great-game.merchants.merchant-utils.html new file mode 100644 index 0000000..8936d0a --- /dev/null +++ b/docs/the-great-game.merchants.merchant-utils.html @@ -0,0 +1,3 @@ + +the-great-game.merchants.merchant-utils documentation

the-great-game.merchants.merchant-utils

Useful functions for doing low-level things with merchants.

add-known-prices

(add-known-prices merchant world)

Add the current prices at this merchant’s location in the world to a new cacke of known prices, and return it.

add-stock

(add-stock a b)

Where a and b are both maps all of whose values are numbers, return a map whose keys are a union of the keys of a and b and whose values are the sums of their respective values.

burden

(burden merchant world)

The total weight of the current cargo carried by this merchant in this world.

can-afford

(can-afford merchant world commodity)

Return the number of units of this commodity which this merchant can afford to buy in this world.

can-carry

(can-carry merchant world commodity)

Return the number of units of this commodity which this merchant can carry in this world, given their current burden.

expected-price

(expected-price merchant commodity city)

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.

\ No newline at end of file diff --git a/docs/the-great-game.merchants.merchants.html b/docs/the-great-game.merchants.merchants.html index 78436a0..f1b8e01 100644 --- a/docs/the-great-game.merchants.merchants.html +++ b/docs/the-great-game.merchants.merchants.html @@ -1,26 +1,3 @@ -the-great-game.merchants.merchants documentation

the-great-game.merchants.merchants

Trade planning for merchants, primarily.

add-known-prices

(add-known-prices merchant world)

Add the current prices at this merchant’s location in the world to a new cacke of known prices, and return it.

add-stock

(add-stock a b)

Where a and b are both maps all of whose values are numbers, return a map whose keys are a union of the keys of a and b and whose values are the sums of their respective values.

augment-plan

(augment-plan merchant world 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.

burden

(burden merchant world)

The total weight of the current cargo carried by this merchant in this world.

can-afford

(can-afford merchant world commodity)

Return the number of units of this commodity which this merchant can afford to buy in this world.

can-carry

(can-carry merchant world commodity)

Return the number of units of this commodity which this merchant can carry in this world, given their current burden.

expected-price

(expected-price merchant commodity city)

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.

generate-trade-plans

(generate-trade-plans merchant world commodity)

Generate all possible trade plans for this merchant and this commodity in this world.

-

Returned plans are maps 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.
  • -

make-target-filter

(make-target-filter targets)

Construct a filter which, when applied to a list of maps, will pass those which match these targets, where each target is a tuple [key value].

move-merchant

(move-merchant merchant world)

Handle general en route movement of this merchant in this world.

nearest-with-targets

(nearest-with-targets plans targets)

Return the distance to the nearest destination among those of these plans which match these targets. Plans are expected to be plans as returned by generate-trade-plans, q.v.; targets are expected to be as accepted by make-target-filter, q.v.

plan-and-buy

(plan-and-buy merchant world)

Return a world like this world, in which this merchant has planned a new trade, and bought appropriate stock for it. If no profitable trade can be planned, the merchant is simply moved towards their home.

plan-trade

(plan-trade merchant world commodity)

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.
  • -

re-plan

(re-plan merchant world)

Having failed to sell a cargo at current location, re-plan a route to sell the current cargo. Returns a revised world.

run

(run world)

Return a world like this world, but with each merchant moved.

select-cargo

(select-cargo merchant world)

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.

sell-and-buy

(sell-and-buy merchant world)

Return a new world like this world, in which this merchant has sold their current stock in their current location, and planned a new trade, and bought appropriate stock for it.

\ No newline at end of file +the-great-game.merchants.merchants documentation

the-great-game.merchants.merchants

Trade planning for merchants, primarily.

run

(run world)

Return a partial world based on this world, but with each merchant moved.

\ No newline at end of file diff --git a/docs/the-great-game.merchants.planning.html b/docs/the-great-game.merchants.planning.html new file mode 100644 index 0000000..9e74759 --- /dev/null +++ b/docs/the-great-game.merchants.planning.html @@ -0,0 +1,26 @@ + +the-great-game.merchants.planning documentation

the-great-game.merchants.planning

Trade planning for merchants, primarily.

augment-plan

(augment-plan merchant world 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.

generate-trade-plans

(generate-trade-plans merchant world commodity)

Generate all possible trade plans for this merchant and this commodity in this world.

+

Returned plans are maps 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.
  • +

nearest-with-targets

(nearest-with-targets plans targets)

Return the distance to the nearest destination among those of these plans which match these targets. Plans are expected to be plans as returned by generate-trade-plans, q.v.; targets are expected to be as accepted by make-target-filter, q.v.

plan-trade

(plan-trade merchant world commodity)

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.
  • +

select-cargo

(select-cargo merchant world)

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.

\ No newline at end of file diff --git a/docs/the-great-game.merchants.strategies.simple.html b/docs/the-great-game.merchants.strategies.simple.html new file mode 100644 index 0000000..6b707f0 --- /dev/null +++ b/docs/the-great-game.merchants.strategies.simple.html @@ -0,0 +1,4 @@ + +the-great-game.merchants.strategies.simple documentation

the-great-game.merchants.strategies.simple

Default trading strategy for merchants.

+

The simple strategy buys a single product in the local market if there is one which can be traded profitably, trades it to the chosen target market, and sells it there. If there is no commodity locally which can be traded profitably, moves towards home with no cargo. If at home and no commodity can be traded profitably, does not move.

move-merchant

(move-merchant merchant world)

Handle general en route movement of this merchant in this world; return a (partial or full) world like this world but in which the merchant may have been moved ot updated.

plan-and-buy

(plan-and-buy merchant world)

Return a world like this world, in which this merchant has planned a new trade, and bought appropriate stock for it. If no profitable trade can be planned, the merchant is simply moved towards their home.

re-plan

(re-plan merchant world)

Having failed to sell a cargo at current location, re-plan a route to sell the current cargo. Returns a revised world.

sell-and-buy

(sell-and-buy merchant world)

Return a new world like this world, in which this merchant has sold their current stock in their current location, and planned a new trade, and bought appropriate stock for it.

\ No newline at end of file diff --git a/docs/the-great-game.utils.html b/docs/the-great-game.utils.html index bb4ad9f..b46fa14 100644 --- a/docs/the-great-game.utils.html +++ b/docs/the-great-game.utils.html @@ -1,3 +1,3 @@ -the-great-game.utils documentation

the-great-game.utils

TODO: write docs

cyclic?

(cyclic? route)

True if two or more elements of route are identical

deep-merge

(deep-merge & maps)
\ No newline at end of file +the-great-game.utils documentation

the-great-game.utils

TODO: write docs

cyclic?

(cyclic? route)

True if two or more elements of route are identical

deep-merge

(deep-merge & maps)

make-target-filter

(make-target-filter targets)

Construct a filter which, when applied to a list of maps, will pass those which match these targets, where each target is a tuple [key value].

\ No newline at end of file diff --git a/docs/the-great-game.world.routes.html b/docs/the-great-game.world.routes.html index d5d1cbd..8108252 100644 --- a/docs/the-great-game.world.routes.html +++ b/docs/the-great-game.world.routes.html @@ -1,3 +1,3 @@ -the-great-game.world.routes documentation

the-great-game.world.routes

Conceptual (plan level) routes, represented as tuples of location ids.

find-route

(find-route world-or-routes from to)

Find a single route from from to to in this world-or-routes, which may be either a world as defined in the-great-game.world.world or else a sequence of tuples of keywords.

find-routes

(find-routes routes from)(find-routes routes from to)(find-routes routes from to steps)

Find routes from among these routes from from; if to is supplied, to to, by breadth-first search.

\ No newline at end of file +the-great-game.world.routes documentation

the-great-game.world.routes

Conceptual (plan level) routes, represented as tuples of location ids.

find-route

(find-route world-or-routes from to)

Find a single route from from to to in this world-or-routes, which may be either a world as defined in the-great-game.world.world or else a sequence of tuples of keywords.

find-routes

(find-routes routes from)(find-routes routes from to)(find-routes routes from to steps)

Find routes from among these routes from from; if to is supplied, to to, by breadth-first search.

\ No newline at end of file diff --git a/docs/the-great-game.world.run.html b/docs/the-great-game.world.run.html index dd8152d..2ae1008 100644 --- a/docs/the-great-game.world.run.html +++ b/docs/the-great-game.world.run.html @@ -1,3 +1,3 @@ -the-great-game.world.run documentation

the-great-game.world.run

Run the whole simulation

run

(run world)

The pipeline to run the simulation each game day. Returns a world like this world, with all the various active elements updated.

\ No newline at end of file +the-great-game.world.run documentation

the-great-game.world.run

Run the whole simulation

init

(init)(init config)

TODO: write docs

run

(run world)(run world date)

The pipeline to run the simulation each game day. Returns a world like this world, with all the various active elements updated. The optional date argument, if supplied, is set as the :date of the returned world.

\ No newline at end of file diff --git a/docs/the-great-game.world.world.html b/docs/the-great-game.world.world.html index f2d2aa0..efe608f 100644 --- a/docs/the-great-game.world.world.html +++ b/docs/the-great-game.world.world.html @@ -1,3 +1,3 @@ -the-great-game.world.world documentation

the-great-game.world.world

Access to data about the world

actual-price

(actual-price world commodity city)

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.

default-world

A basic world for testing concepts

run

(run world)

Return a world like this world with only the :date value updated (incremented by one). For running other aspects of the simulation, see the-great-game.world.run.

\ No newline at end of file +the-great-game.world.world documentation

the-great-game.world.world

Access to data about the world

actual-price

(actual-price world commodity city)

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.

default-world

A basic world for testing concepts

run

(run world)(run world date)

Return a world like this world with only the :date to this date (or id date not supplied, the current value incremented by one). For running other aspects of the simulation, see the-great-game.world.run.

\ No newline at end of file diff --git a/src/the_great_game/merchants/merchant_utils.clj b/src/the_great_game/merchants/merchant_utils.clj new file mode 100644 index 0000000..b8cd969 --- /dev/null +++ b/src/the_great_game/merchants/merchant_utils.clj @@ -0,0 +1,92 @@ +(ns the-great-game.merchants.merchant-utils + "Useful functions for doing low-level things with merchants.") + +(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 (:stock m)] + (reduce + + + 0 + (map + #(* (cargo %) (-> world :commodities % :weight)) + (keys cargo))))) + + +(defn can-carry + "Return the number of units of this `commodity` which this `merchant` + can carry in this `world`, given their current burden." + [merchant world commodity] + (let [m (cond + (keyword? merchant) + (-> world :merchants merchant) + (map? merchant) + merchant)] + (quot + (- (:capacity m) (burden m world)) + (-> world :commodities commodity :weight)))) + +(defn can-afford + "Return the number of units of this `commodity` which this `merchant` + can afford to buy in this `world`." + [merchant world commodity] + (let [m (cond + (keyword? merchant) + (-> world :merchants merchant) + (map? merchant) + merchant) + l (:location m)] + (quot + (:cash m) + (-> world :cities l :prices commodity)))) + +(defn add-stock + "Where `a` and `b` are both maps all of whose values are numbers, return + a map whose keys are a union of the keys of `a` and `b` and whose values + are the sums of their respective values." + [a b] + (reduce + merge + a + (map + #(hash-map % (+ (or (a %) 0) (or (b %) 0))) + (keys b)))) + +(defn add-known-prices + "Add the current prices at this `merchant`'s location in the `world` + to a new cacke of known prices, and return it." + [merchant world] + (let [m (cond + (keyword? merchant) + (-> world :merchants merchant) + (map? merchant) + merchant) + k (:known-prices m) + l (:location m) + d (:date world) + p (-> world :cities l :prices)] + (reduce + merge + k + (map + #(hash-map % (apply vector cons {:price (p %) :date d} (k %))) + (-> world :commodities keys))))) diff --git a/src/the_great_game/merchants/merchants.clj b/src/the_great_game/merchants/merchants.clj index 7b10dd2..9ef160d 100644 --- a/src/the_great_game/merchants/merchants.clj +++ b/src/the_great_game/merchants/merchants.clj @@ -2,437 +2,9 @@ "Trade planning for merchants, primarily." (:require [taoensso.timbre :as l :refer [info error spy]] [the-great-game.utils :refer [deep-merge]] - [the-great-game.gossip.gossip :refer [move-gossip]] - [the-great-game.world.routes :refer [find-route]] - [the-great-game.world.world :refer [actual-price default-world]])) + [the-great-game.merchants.strategies.simple :refer [move-merchant]])) -(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 (:stock m)] - (reduce - + - 0 - (map - #(* (cargo %) (-> world :commodities % :weight)) - (keys cargo))))) - - -(defn make-target-filter - "Construct a filter which, when applied to a list of maps, - will pass those which match these `targets`, where each target - is a tuple [key value]." - ;; TODO: this would probably be more elegant as a macro - [targets] - (eval - (list - 'fn - (vector 'plan) - (cons - 'and - (map - #(list - '= - (list (first %) 'plan) - (nth % 1)) - targets))))) - - -(defn generate-trade-plans - "Generate all possible trade plans for this `merchant` and this `commodity` - in this `world`. - - Returned plans are maps 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 (:location m)] - (map - #(hash-map - :merchant (:id m) - :origin origin - :destination % - :commodity commodity - :buy-price (actual-price world commodity origin) - :expected-price (expected-price - m - commodity - %) - :distance (count - (find-route world origin %)) - :dist-to-home (count - (find-route - world - (:home m) - %))) - (remove #(= % origin) (-> world :cities keys))))) - -(defn nearest-with-targets - "Return the distance to the nearest destination among those of these - `plans` which match these `targets`. Plans are expected to be plans - as returned by `generate-trade-plans`, q.v.; `targets` are expected to be - as accepted by `make-target-filter`, q.v." - [plans targets] - (apply - min - (map - :distance - (filter - (make-target-filter targets) - plans)))) - -(defn plan-trade - "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 [plans (generate-trade-plans merchant world commodity) - best-prices (filter - (make-target-filter - [[:expected-price - (apply - max - (filter number? (map :expected-price plans)))]]) - plans)] - (first - (sort-by - ;; all other things being equal, a merchant would prefer to end closer - ;; to home. - #(- 0 (:dist-to-home %)) - ;; a merchant will seek the best price, but won't go further than - ;; needed to get it. - (filter - (make-target-filter - [[:distance - (apply min (filter number? (map :distance best-prices)))]]) - best-prices))))) - - -(defn can-carry - "Return the number of units of this `commodity` which this `merchant` - can carry in this `world`, given their current burden." - [merchant world commodity] - (let [m (cond - (keyword? merchant) - (-> world :merchants merchant) - (map? merchant) - merchant)] - (quot - (- (:capacity m) (burden m world)) - (-> world :commodities commodity :weight)))) - -(defn can-afford - "Return the number of units of this `commodity` which this `merchant` - can afford to buy in this `world`." - [merchant world commodity] - (let [m (cond - (keyword? merchant) - (-> world :merchants merchant) - (map? merchant) - merchant) - l (:location m)] - (quot - (:cash m) - (-> world :cities l :prices commodity)))) - -(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 [c (:commodity plan) - o (:origin plan) - q (min - (or - (-> world :cities o :stock c) - 0) - (can-carry merchant world c) - (can-afford merchant world c)) - p (* q (- (:expected-price plan) (:buy-price plan)))] - (assoc plan :quantity q :expected-profit p))) - -;; (-> default-world :cities :buckie :stock :iron) -;; (burden :fiona default-world) -;; (-> default-world :commodities :iron :weight) -;; (quot 0 10) - -(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 (:location m) - available (-> world :cities origin :stock) - plans (map - #(augment-plan - m - world - (plan-trade m world %)) - (filter - #(let [q (-> world :cities origin :stock %)] - (and (number? q) (pos? q))) - (keys available)))] - (if - (not (empty? plans)) - (first - (sort-by - #(- 0 (:dist-to-home %)) - (filter - (make-target-filter - [[:expected-profit - (apply max (filter number? (map :expected-profit plans)))]]) - plans)))))) - -(defn add-stock - "Where `a` and `b` are both maps all of whose values are numbers, return - a map whose keys are a union of the keys of `a` and `b` and whose values - are the sums of their respective values." - [a b] - (reduce - merge - a - (map - #(hash-map % (+ (or (a %) 0) (or (b %) 0))) - (keys b)))) - -(defn add-known-prices - "Add the current prices at this `merchant`'s location in the `world` - to a new cacke of known prices, and return it." - [merchant world] - (let [m (cond - (keyword? merchant) - (-> world :merchants merchant) - (map? merchant) - merchant) - k (:known-prices m) - l (:location m) - d (:date world) - p (-> world :cities l :prices)] - (reduce - merge - k - (map - #(hash-map % (apply vector cons {:price (p %) :date d} (k %))) - (-> world :commodities keys))))) - -;;; Right, from here on in we're actually moving things in the world, so -;;; functions generally return modified partial worlds. - -(defn plan-and-buy - "Return a world like this `world`, in which this `merchant` has planned - a new trade, and bought appropriate stock for it. If no profitable trade - can be planned, the merchant is simply moved towards their home." - [merchant world] - (let [m (cond - (keyword? merchant) - (-> world :merchants merchant) - (map? merchant) - merchant) - id (:id m) - location (:location m) - market (-> world :cities location) - plan (select-cargo merchant world)] - (l/debug "plan-and-buy: merchant" id) - (cond - (not (empty? plan)) - (let - [c (:commodity plan) - p (* (:quantity plan) (:buy-price plan)) - q (:quantity plan)] - (l/info "Merchant" id "bought" q "units of" c "at" location "for" p plan) - {:merchants - {id - {:stock (add-stock (:stock m) {c q}) - :cash (- (:cash m) p) - :known-prices (add-known-prices m world) - :plan plan}} - :cities - {location - {:stock (assoc (:stock market) c (- (-> market :stock c) q)) - :cash (+ (:cash market) p)}}}) - ;; if no plan, then if at home stay put - (= (:location m) (:home m)) - (do - (l/info "Merchant" id "remains at home in" location) - {}) - ;; else move towards home - :else - (let [route (find-route world location (:home m)) - next-location (nth route 1)] - (l/info "No trade possible at" location "; merchant" id "moves to" next-location) - (merge - {:merchants - {id - {:location next-location}}} - (move-gossip id world next-location)))))) - -(defn re-plan - "Having failed to sell a cargo at current location, re-plan a route to - sell the current cargo. Returns a revised world." - [merchant world] - (let [m (cond - (keyword? merchant) - (-> world :merchants merchant) - (map? merchant) - merchant) - id (:id m) - location (:location m) - plan (augment-plan m world (plan-trade m world (-> m :plan :commodity)))] - (l/debug "re-plan: merchant" id) - (deep-merge - world - {:merchants - {id - {:plan plan}}}))) - -(defn sell-and-buy - "Return a new world like this `world`, in which this `merchant` has sold - their current stock in their current location, and planned a new trade, and - bought appropriate stock for it." - ;; TODO: this either sells the entire cargo, or, if the market can't afford - ;; it, none of it. And it does not cope with selling different commodities - ;; in different markets. - [merchant world] - (let [m (cond - (keyword? merchant) - (-> world :merchants merchant) - (map? merchant) - merchant) - id (:id m) - location (:location m) - market (-> world :cities location) - stock-value (reduce - + - (map - #(* (-> m :stock %) (-> market :prices m)) - (keys (:stock m))))] - (l/debug "sell-and-buy: merchant" id) - (if - (>= (:cash market) stock-value) - (do - (l/info "Merchant" id "sells" (:stock m) "at" location "for" stock-value) - (plan-and-buy - merchant - (deep-merge - world - {:merchants - {id - {:stock {} - :cash (+ (:cash m) stock-value) - :known-prices (add-known-prices m world)}} - :cities - {location - {:stock (add-stock (:stock m) (:stock market)) - :cash (- (:cash market) stock-value)}}}))) - ;; else - (re-plan merchant world)))) - -(defn move-merchant - "Handle general en route movement of this `merchant` in this `world`; - return a (partial or full) world like this `world` but in which the - merchant may have been moved ot updated." - [merchant world] - (let [m (cond - (keyword? merchant) - (-> world :merchants merchant) - (map? merchant) - merchant) - id (:id m) - at-destination? (and (:plan m) (= (:location m) (-> m :plan :destination))) - plan (:plan m) - next-location (if plan - (nth - (find-route - world - (:location m) - (:destination plan)) - 1) - (:location m))] - (l/debug "move-merchant: merchant" id "at" (:location m) - "destination" (-> m :plan :destination) "next" next-location - "at destination" at-destination?) - (cond - ;; if the merchant is at the destination of their current plan - ;; sell all cargo and repurchase. - at-destination? - (sell-and-buy merchant world) - ;; if they don't have a plan, seek to create one - (nil? plan) - (plan-and-buy merchant world) - ;; otherwise, move one step towards their destination - (and next-location (not= next-location (:location m))) - (do - (l/info "Merchant " id " moving from " (:location m) " to " next-location) - (deep-merge - {:merchants - {id - {:location next-location - :known-prices (add-known-prices m world)}}} - (move-gossip id world next-location))) - :else - (do - (l/info "Merchant" id "has plan but no next-location; currently at" - (:location m) ", destination is" (:destination plan)) - world)))) - (defn run "Return a partial world based on this `world`, but with each merchant moved." [world] @@ -442,11 +14,14 @@ world (map #(try - (move-merchant % world) + (let [move-fn (or + (-> world :merchants % :move-fn) + move-merchant)] + (apply move-fn (list % world))) (catch Exception any (l/error any "Failure while moving merchant " %) {})) - (keys (:merchants world)))) + (keys (:merchants world)))) (catch Exception any (l/error any "Failure while moving merchants") world))) diff --git a/src/the_great_game/merchants/planning.clj b/src/the_great_game/merchants/planning.clj index eb2a2ef..8ac2d01 100644 --- a/src/the_great_game/merchants/planning.clj +++ b/src/the_great_game/merchants/planning.clj @@ -1,6 +1,156 @@ (ns the-great-game.merchants.planning "Trade planning for merchants, primarily." - (:require [taoensso.timbre :as l :refer [info error spy]] - [the-great-game.utils :refer [deep-merge]] + (:require [the-great-game.utils :refer [deep-merge make-target-filter]] + [the-great-game.merchants.merchant-utils :refer :all] [the-great-game.world.routes :refer [find-route]] [the-great-game.world.world :refer [actual-price default-world]])) + +(defn generate-trade-plans + "Generate all possible trade plans for this `merchant` and this `commodity` + in this `world`. + + Returned plans are maps 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 (:location m)] + (map + #(hash-map + :merchant (:id m) + :origin origin + :destination % + :commodity commodity + :buy-price (actual-price world commodity origin) + :expected-price (expected-price + m + commodity + %) + :distance (count + (find-route world origin %)) + :dist-to-home (count + (find-route + world + (:home m) + %))) + (remove #(= % origin) (-> world :cities keys))))) + +(defn nearest-with-targets + "Return the distance to the nearest destination among those of these + `plans` which match these `targets`. Plans are expected to be plans + as returned by `generate-trade-plans`, q.v.; `targets` are expected to be + as accepted by `make-target-filter`, q.v." + [plans targets] + (apply + min + (map + :distance + (filter + (make-target-filter targets) + plans)))) + +(defn plan-trade + "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 [plans (generate-trade-plans merchant world commodity) + best-prices (filter + (make-target-filter + [[:expected-price + (apply + max + (filter number? (map :expected-price plans)))]]) + plans)] + (first + (sort-by + ;; all other things being equal, a merchant would prefer to end closer + ;; to home. + #(- 0 (:dist-to-home %)) + ;; a merchant will seek the best price, but won't go further than + ;; needed to get it. + (filter + (make-target-filter + [[:distance + (apply min (filter number? (map :distance best-prices)))]]) + best-prices))))) + +(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 [c (:commodity plan) + o (:origin plan) + q (min + (or + (-> world :cities o :stock c) + 0) + (can-carry merchant world c) + (can-afford merchant world c)) + 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 (:location m) + available (-> world :cities origin :stock) + plans (map + #(augment-plan + m + world + (plan-trade m world %)) + (filter + #(let [q (-> world :cities origin :stock %)] + (and (number? q) (pos? q))) + (keys available)))] + (if + (not (empty? plans)) + (first + (sort-by + #(- 0 (:dist-to-home %)) + (filter + (make-target-filter + [[:expected-profit + (apply max (filter number? (map :expected-profit plans)))]]) + plans)))))) + diff --git a/src/the_great_game/merchants/strategies/simple.clj b/src/the_great_game/merchants/strategies/simple.clj index 621d29c..d43e952 100644 --- a/src/the_great_game/merchants/strategies/simple.clj +++ b/src/the_great_game/merchants/strategies/simple.clj @@ -1,2 +1,173 @@ (ns the-great-game.merchants.strategies.simple - ) + "Default trading strategy for merchants. + + The simple strategy buys a single product in the local market if there is + one which can be traded profitably, trades it to the chosen target market, + and sells it there. If there is no commodity locally which can be traded + profitably, moves towards home with no cargo. If at home and no commodity + can be traded profitably, does not move." + (:require [taoensso.timbre :as l :refer [info error spy]] + [the-great-game.utils :refer [deep-merge]] + [the-great-game.gossip.gossip :refer [move-gossip]] + [the-great-game.merchants.planning :refer :all] + [the-great-game.merchants.merchant-utils :refer + [add-stock add-known-prices]] + [the-great-game.world.routes :refer [find-route]])) + +(defn plan-and-buy + "Return a world like this `world`, in which this `merchant` has planned + a new trade, and bought appropriate stock for it. If no profitable trade + can be planned, the merchant is simply moved towards their home." + [merchant world] + (let [m (cond + (keyword? merchant) + (-> world :merchants merchant) + (map? merchant) + merchant) + id (:id m) + location (:location m) + market (-> world :cities location) + plan (select-cargo merchant world)] + (l/debug "plan-and-buy: merchant" id) + (cond + (not (empty? plan)) + (let + [c (:commodity plan) + p (* (:quantity plan) (:buy-price plan)) + q (:quantity plan)] + (l/info "Merchant" id "bought" q "units of" c "at" location "for" p plan) + {:merchants + {id + {:stock (add-stock (:stock m) {c q}) + :cash (- (:cash m) p) + :known-prices (add-known-prices m world) + :plan plan}} + :cities + {location + {:stock (assoc (:stock market) c (- (-> market :stock c) q)) + :cash (+ (:cash market) p)}}}) + ;; if no plan, then if at home stay put + (= (:location m) (:home m)) + (do + (l/info "Merchant" id "remains at home in" location) + {}) + ;; else move towards home + :else + (let [route (find-route world location (:home m)) + next-location (nth route 1)] + (l/info "No trade possible at" location "; merchant" id "moves to" next-location) + (merge + {:merchants + {id + {:location next-location}}} + (move-gossip id world next-location)))))) + +(defn re-plan + "Having failed to sell a cargo at current location, re-plan a route to + sell the current cargo. Returns a revised world." + [merchant world] + (let [m (cond + (keyword? merchant) + (-> world :merchants merchant) + (map? merchant) + merchant) + id (:id m) + location (:location m) + plan (augment-plan m world (plan-trade m world (-> m :plan :commodity)))] + (l/debug "re-plan: merchant" id) + (deep-merge + world + {:merchants + {id + {:plan plan}}}))) + +(defn sell-and-buy + "Return a new world like this `world`, in which this `merchant` has sold + their current stock in their current location, and planned a new trade, and + bought appropriate stock for it." + ;; TODO: this either sells the entire cargo, or, if the market can't afford + ;; it, none of it. And it does not cope with selling different commodities + ;; in different markets. + [merchant world] + (let [m (cond + (keyword? merchant) + (-> world :merchants merchant) + (map? merchant) + merchant) + id (:id m) + location (:location m) + market (-> world :cities location) + stock-value (reduce + + + (map + #(* (-> m :stock %) (-> market :prices m)) + (keys (:stock m))))] + (l/debug "sell-and-buy: merchant" id) + (if + (>= (:cash market) stock-value) + (do + (l/info "Merchant" id "sells" (:stock m) "at" location "for" stock-value) + (plan-and-buy + merchant + (deep-merge + world + {:merchants + {id + {:stock {} + :cash (+ (:cash m) stock-value) + :known-prices (add-known-prices m world)}} + :cities + {location + {:stock (add-stock (:stock m) (:stock market)) + :cash (- (:cash market) stock-value)}}}))) + ;; else + (re-plan merchant world)))) + +(defn move-merchant + "Handle general en route movement of this `merchant` in this `world`; + return a (partial or full) world like this `world` but in which the + merchant may have been moved ot updated." + [merchant world] + (let [m (cond + (keyword? merchant) + (-> world :merchants merchant) + (map? merchant) + merchant) + id (:id m) + at-destination? (and (:plan m) (= (:location m) (-> m :plan :destination))) + plan (:plan m) + next-location (if plan + (nth + (find-route + world + (:location m) + (:destination plan)) + 1) + (:location m))] + (l/debug "move-merchant: merchant" id "at" (:location m) + "destination" (-> m :plan :destination) "next" next-location + "at destination" at-destination?) + (cond + ;; if the merchant is at the destination of their current plan + ;; sell all cargo and repurchase. + at-destination? + (sell-and-buy merchant world) + ;; if they don't have a plan, seek to create one + (nil? plan) + (plan-and-buy merchant world) + ;; otherwise, move one step towards their destination + (and next-location (not= next-location (:location m))) + (do + (l/info "Merchant " id " moving from " (:location m) " to " next-location) + (deep-merge + {:merchants + {id + {:location next-location + :known-prices (add-known-prices m world)}}} + (move-gossip id world next-location))) + :else + (do + (l/info "Merchant" id "has plan but no next-location; currently at" + (:location m) ", destination is" (:destination plan)) + world)))) + diff --git a/src/the_great_game/utils.clj b/src/the_great_game/utils.clj index 60cee43..98c2f6f 100644 --- a/src/the_great_game/utils.clj +++ b/src/the_great_game/utils.clj @@ -15,3 +15,21 @@ (last xs)))] (reduce m maps))) +(defn make-target-filter + "Construct a filter which, when applied to a list of maps, + will pass those which match these `targets`, where each target + is a tuple [key value]." + ;; TODO: this would probably be more elegant as a macro + [targets] + (eval + (list + 'fn + (vector 'm) + (cons + 'and + (map + #(list + '= + (list (first %) 'm) + (nth % 1)) + targets))))) diff --git a/test/the_great_game/merchants/merchant_utils_test.clj b/test/the_great_game/merchants/merchant_utils_test.clj new file mode 100644 index 0000000..2f9071e --- /dev/null +++ b/test/the_great_game/merchants/merchant_utils_test.clj @@ -0,0 +1,24 @@ +(ns the-great-game.merchants.merchant-utils-test + (:require [clojure.test :refer :all] + [the-great-game.utils :refer [deep-merge]] + [the-great-game.world.world :refer [default-world]] + [the-great-game.merchants.merchant-utils :refer :all])) + +(deftest expected-price-test + (testing "Anticipated prices in markets" + (let [world (deep-merge + default-world + {:merchants + {:archie + {:known-prices + {:buckie + {:iron + [{:price 1.7 :date 1} + {:price 2 :date 0}]}}}}})] + (let [actual (expected-price (-> world :merchants :archie) :fish :edinburgh) + expected 1] ;; + (is (= actual expected) "if no information assume 1")) + (let [actual (expected-price (-> world :merchants :archie) :iron :buckie) + expected 1.7] ;; + (is (= actual expected) "if information select the most recent"))))) + diff --git a/test/the_great_game/merchants/merchants_test.clj b/test/the_great_game/merchants/planning_test.clj similarity index 81% rename from test/the_great_game/merchants/merchants_test.clj rename to test/the_great_game/merchants/planning_test.clj index 8089c86..5662e35 100644 --- a/test/the_great_game/merchants/merchants_test.clj +++ b/test/the_great_game/merchants/planning_test.clj @@ -1,26 +1,9 @@ -(ns the-great-game.merchants.merchants-test +(ns the-great-game.merchants.planning-test (:require [clojure.test :refer :all] [the-great-game.utils :refer [deep-merge]] [the-great-game.world.world :refer [default-world]] - [the-great-game.merchants.merchants :refer :all])) + [the-great-game.merchants.planning :refer :all])) -(deftest expected-price-test - (testing "Anticipated prices in markets" - (let [world (deep-merge - default-world - {:merchants - {:archie - {:known-prices - {:buckie - {:iron - [{:price 1.7 :date 1} - {:price 2 :date 0}]}}}}})] - (let [actual (expected-price (-> world :merchants :archie) :fish :edinburgh) - expected 1] ;; - (is (= actual expected) "if no information assume 1")) - (let [actual (expected-price (-> world :merchants :archie) :iron :buckie) - expected 1.7] ;; - (is (= actual expected) "if information select the most recent"))))) (deftest plan-trade-test (testing "Lower level trade planning"