veloren/world/src/sim2/mod.rs
2022-01-25 16:31:12 +00:00

1304 lines
49 KiB
Rust

use crate::{
sim::WorldSim,
site::{
economy::{
decay_rate, direct_use_goods, good_list, transportation_effort, Economy, GoodIndex,
GoodMap, LaborIndex, LaborMap, TradeDelivery, TradeOrder,
},
Site, SiteKind,
},
util::{DHashMap, DHashSet},
Index,
};
use common::{
store::Id,
trade::{
Good,
Good::{Coin, Transportation},
},
};
use lazy_static::lazy_static;
use std::{cmp::Ordering::Less, convert::TryInto};
use tracing::{debug, info};
const MONTH: f32 = 30.0;
const YEAR: f32 = 12.0 * MONTH;
const TICK_PERIOD: f32 = 3.0 * MONTH; // 3 months
const HISTORY_DAYS: f32 = 500.0 * YEAR; // 500 years
const GENERATE_CSV: bool = false;
const INTER_SITE_TRADE: bool = true;
// this is an empty replacement for https://github.com/cpetig/vergleich
// which can be used to compare values acros runs
mod vergleich {
pub struct Error {}
impl Error {
pub fn to_string(&self) -> &'static str { "" }
}
pub struct ProgramRun {}
impl ProgramRun {
pub fn new(_: &str) -> Result<Self, Error> { Ok(Self {}) }
pub fn set_epsilon(&mut self, _: f32) {}
pub fn context(&mut self, _: &str) -> Context { Context {} }
//pub fn value(&mut self, _: &str, val: f32) -> f32 { val }
}
pub struct Context {}
impl Context {
pub fn context(&mut self, _: &str) -> Context { Context {} }
pub fn value(&mut self, _: &str, val: f32) -> f32 { val }
}
}
/// Statistics collector (min, max, avg)
#[derive(Debug)]
struct EconStatistics {
pub count: u32,
pub sum: f32,
pub min: f32,
pub max: f32,
}
impl Default for EconStatistics {
fn default() -> Self {
Self {
count: 0,
sum: 0.0,
min: f32::INFINITY,
max: -f32::INFINITY,
}
}
}
impl std::ops::AddAssign<f32> for EconStatistics {
fn add_assign(&mut self, rhs: f32) { self.collect(rhs); }
}
impl EconStatistics {
fn collect(&mut self, value: f32) {
self.count += 1;
self.sum += value;
if value > self.max {
self.max = value;
}
if value < self.min {
self.min = value;
}
}
fn valid(&self) -> bool { self.min.is_finite() }
}
pub fn csv_entry(f: &mut std::fs::File, site: &Site) -> Result<(), std::io::Error> {
use std::io::Write;
write!(
*f,
"{}, {}, {}, {},",
site.name(),
site.get_origin().x,
site.get_origin().y,
site.economy.pop
)?;
for g in good_list() {
write!(*f, "{:?},", site.economy.values[g].unwrap_or(-1.0))?;
}
for g in good_list() {
write!(f, "{:?},", site.economy.labor_values[g].unwrap_or(-1.0))?;
}
for g in good_list() {
write!(f, "{:?},", site.economy.stocks[g])?;
}
for g in good_list() {
write!(f, "{:?},", site.economy.marginal_surplus[g])?;
}
for l in LaborIndex::list() {
write!(f, "{:?},", site.economy.labors[l] * site.economy.pop)?;
}
for l in LaborIndex::list() {
write!(f, "{:?},", site.economy.productivity[l])?;
}
for l in LaborIndex::list() {
write!(f, "{:?},", site.economy.yields[l])?;
}
writeln!(f)
}
fn simulate_return(index: &mut Index, world: &mut WorldSim) -> Result<(), std::io::Error> {
use std::io::Write;
// please not that GENERATE_CSV is off by default, so panicing is not harmful
// here
let mut f = if GENERATE_CSV {
let mut f = std::fs::File::create("economy.csv")?;
write!(f, "Site,PosX,PosY,Population,")?;
for g in good_list() {
write!(f, "{:?} Value,", g)?;
}
for g in good_list() {
write!(f, "{:?} LaborVal,", g)?;
}
for g in good_list() {
write!(f, "{:?} Stock,", g)?;
}
for g in good_list() {
write!(f, "{:?} Surplus,", g)?;
}
for l in LaborIndex::list() {
write!(f, "{:?} Labor,", l)?;
}
for l in LaborIndex::list() {
write!(f, "{:?} Productivity,", l)?;
}
for l in LaborIndex::list() {
write!(f, "{:?} Yields,", l)?;
}
writeln!(f)?;
Some(f)
} else {
None
};
tracing::info!("economy simulation start");
let mut vr = vergleich::ProgramRun::new("economy_compare.sqlite")
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
vr.set_epsilon(0.1);
for i in 0..(HISTORY_DAYS / TICK_PERIOD) as i32 {
if (index.time / YEAR) as i32 % 50 == 0 && (index.time % YEAR) as i32 == 0 {
debug!("Year {}", (index.time / YEAR) as i32);
}
tick(index, world, TICK_PERIOD, vr.context(&i.to_string()));
if let Some(f) = f.as_mut() {
if i % 5 == 0 {
if let Some(site) = index
.sites
.values()
.find(|s| !matches!(s.kind, SiteKind::Dungeon(_)))
{
csv_entry(f, site)?;
}
}
}
}
tracing::info!("economy simulation end");
if let Some(f) = f.as_mut() {
writeln!(f)?;
for site in index.sites.ids() {
let site = index.sites.get(site);
csv_entry(f, site)?;
}
}
{
let mut castles = EconStatistics::default();
let mut towns = EconStatistics::default();
let mut dungeons = EconStatistics::default();
for site in index.sites.ids() {
let site = &index.sites[site];
match site.kind {
SiteKind::Dungeon(_) => dungeons += site.economy.pop,
SiteKind::Settlement(_) => towns += site.economy.pop,
SiteKind::Castle(_) => castles += site.economy.pop,
SiteKind::Tree(_) => (),
SiteKind::Refactor(_) => towns += site.economy.pop,
}
}
if towns.valid() {
info!(
"Towns {:.0}-{:.0} avg {:.0} inhabitants",
towns.min,
towns.max,
towns.sum / (towns.count as f32)
);
}
if castles.valid() {
info!(
"Castles {:.0}-{:.0} avg {:.0}",
castles.min,
castles.max,
castles.sum / (castles.count as f32)
);
}
if dungeons.valid() {
info!(
"Dungeons {:.0}-{:.0} avg {:.0}",
dungeons.min,
dungeons.max,
dungeons.sum / (dungeons.count as f32)
);
}
check_money(index);
}
Ok(())
}
pub fn simulate(index: &mut Index, world: &mut WorldSim) {
simulate_return(index, world)
.unwrap_or_else(|err| info!("I/O error in simulate (economy.csv not writable?): {}", err));
}
fn check_money(index: &mut Index) {
let mut sum_stock: f32 = 0.0;
for site in index.sites.values() {
sum_stock += site.economy.stocks[*COIN_INDEX];
}
let mut sum_del: f32 = 0.0;
for v in index.trade.deliveries.values() {
for del in v.iter() {
sum_del += del.amount[*COIN_INDEX];
}
}
info!(
"Coin amount {} + {} = {}",
sum_stock,
sum_del,
sum_stock + sum_del
);
}
pub fn tick(index: &mut Index, _world: &mut WorldSim, dt: f32, mut vc: vergleich::Context) {
let site_ids = index.sites.ids().collect::<Vec<_>>();
for site in site_ids {
tick_site_economy(index, site, dt, vc.context(&site.id().to_string()));
}
if INTER_SITE_TRADE {
for (&site, orders) in index.trade.orders.iter_mut() {
let siteinfo = index.sites.get_mut(site);
if siteinfo.do_economic_simulation() {
// let name: String = siteinfo.name().into();
trade_at_site(
site,
orders,
&mut siteinfo.economy,
&mut index.trade.deliveries,
);
}
}
}
//check_money(index);
index.time += dt;
}
lazy_static! {
static ref COIN_INDEX: GoodIndex = Coin.try_into().unwrap_or_default();
static ref FOOD_INDEX: GoodIndex = Good::Food.try_into().unwrap_or_default();
static ref TRANSPORTATION_INDEX: GoodIndex = Transportation.try_into().unwrap_or_default();
}
/// plan the trading according to missing goods and prices at neighboring sites
/// (1st step of trading)
// returns wares spent (-) and procured (+)
// potential_trade: positive = buy, (negative = sell, unused)
fn plan_trade_for_site(
site: &mut Site,
site_id: &Id<Site>,
transportation_capacity: f32,
external_orders: &mut DHashMap<Id<Site>, Vec<TradeOrder>>,
potential_trade: &mut GoodMap<f32>,
) -> GoodMap<f32> {
// TODO: Do we have some latency of information here (using last years
// capacity?)
//let total_transport_capacity = site.economy.stocks[Transportation];
// TODO: We don't count the capacity per site, but globally (so there might be
// some imbalance in dispatch vs collection across sites (e.g. more dispatch
// than collection at one while more collection than dispatch at another))
// transport capacity works both ways (going there and returning)
let mut dispatch_capacity = transportation_capacity;
let mut collect_capacity = transportation_capacity;
let mut missing_dispatch: f32 = 0.0;
let mut missing_collect: f32 = 0.0;
let mut result = GoodMap::default();
const MIN_SELL_PRICE: f32 = 1.0;
// value+amount per good
let mut missing_goods: Vec<(GoodIndex, (f32, f32))> = site
.economy
.surplus
.iter()
.filter(|(g, a)| (**a < 0.0 && *g != *TRANSPORTATION_INDEX))
.map(|(g, a)| {
(
g,
(
site.economy.values[g].unwrap_or(Economy::MINIMUM_PRICE),
-*a,
),
)
})
.collect();
missing_goods.sort_by(|a, b| b.1.0.partial_cmp(&a.1.0).unwrap_or(Less));
let mut extra_goods: GoodMap<f32> = GoodMap::from_iter(
site.economy
.surplus
.iter()
.chain(core::iter::once((
*COIN_INDEX,
&site.economy.stocks[*COIN_INDEX],
)))
.filter(|(g, a)| (**a > 0.0 && *g != *TRANSPORTATION_INDEX))
.map(|(g, a)| (g, *a)),
0.0,
);
// ratio+price per good and site
type GoodRatioPrice = Vec<(GoodIndex, (f32, f32))>;
let good_payment: DHashMap<Id<Site>, GoodRatioPrice> = site
.economy
.neighbors
.iter()
.map(|n| {
let mut rel_value = extra_goods
.iter()
.map(|(g, _)| (g, n.last_values[g]))
.filter(|(_, last_val)| *last_val >= MIN_SELL_PRICE)
.map(|(g, last_val)| {
(
g,
(
last_val
/ site.economy.values[g]
.unwrap_or(-1.0)
.max(Economy::MINIMUM_PRICE),
last_val,
),
)
})
.collect::<Vec<_>>();
rel_value.sort_by(|a, b| b.1.0.partial_cmp(&a.1.0).unwrap_or(Less));
(n.id, rel_value)
})
.collect();
// price+stock per site and good
type SitePriceStock = Vec<(Id<Site>, (f32, f32))>;
let mut good_price: DHashMap<GoodIndex, SitePriceStock> = missing_goods
.iter()
.map(|(g, _)| {
(*g, {
let mut neighbor_prices: Vec<(Id<Site>, (f32, f32))> = site
.economy
.neighbors
.iter()
.filter(|n| n.last_supplies[*g] > 0.0)
.map(|n| (n.id, (n.last_values[*g], n.last_supplies[*g])))
.collect();
neighbor_prices.sort_by(|a, b| a.1.0.partial_cmp(&b.1.0).unwrap_or(Less));
neighbor_prices
})
})
.collect();
// TODO: we need to introduce priority (according to available transportation
// capacity)
let mut neighbor_orders: DHashMap<Id<Site>, GoodMap<f32>> = site
.economy
.neighbors
.iter()
.map(|n| (n.id, GoodMap::default()))
.collect();
if site_id.id() == 1 {
// cut down number of lines printed
debug!(
"Site {} #neighbors {} Transport capacity {}",
site_id.id(),
site.economy.neighbors.len(),
transportation_capacity,
);
debug!("missing {:#?} extra {:#?}", missing_goods, extra_goods,);
debug!("buy {:#?} pay {:#?}", good_price, good_payment);
}
// === the actual planning is here ===
for (g, (_, a)) in missing_goods.iter() {
let mut amount = *a;
if let Some(site_price_stock) = good_price.get_mut(g) {
for (s, (price, supply)) in site_price_stock.iter_mut() {
// how much to buy, limit by supply and transport budget
let mut buy_target = amount.min(*supply);
let effort = transportation_effort(*g);
let collect = buy_target * effort;
let mut potential_balance: f32 = 0.0;
if collect > collect_capacity && effort > 0.0 {
let transportable_amount = collect_capacity / effort;
let missing_trade = buy_target - transportable_amount;
potential_trade[*g] += missing_trade;
potential_balance += missing_trade * *price;
buy_target = transportable_amount; // (buy_target - missing_trade).max(0.0); // avoid negative buy target caused by numeric inaccuracies
missing_collect += collect - collect_capacity;
debug!(
"missing capacity {:?}/{:?} {:?}",
missing_trade, amount, potential_balance,
);
amount = (amount - missing_trade).max(0.0); // you won't be able to transport it from elsewhere either, so don't count multiple times
}
let mut balance: f32 = *price * buy_target;
debug!(
"buy {:?} at {:?} amount {:?} balance {:?}",
*g,
s.id(),
buy_target,
balance,
);
if let Some(neighbor_orders) = neighbor_orders.get_mut(s) {
// find suitable goods in exchange
let mut acute_missing_dispatch: f32 = 0.0; // only count the highest priority (not multiple times)
for (g2, (_, price2)) in good_payment[s].iter() {
let mut amount2 = extra_goods[*g2];
// good available for trading?
if amount2 > 0.0 {
amount2 = amount2.min(balance / price2); // pay until balance is even
let effort2 = transportation_effort(*g2);
let mut dispatch = amount2 * effort2;
// limit by separate transport budget (on way back)
if dispatch > dispatch_capacity && effort2 > 0.0 {
let transportable_amount = dispatch_capacity / effort2;
let missing_trade = amount2 - transportable_amount;
amount2 = transportable_amount;
if acute_missing_dispatch == 0.0 {
acute_missing_dispatch = missing_trade * effort2;
}
debug!(
"can't carry payment {:?} {:?} {:?}",
g2, dispatch, dispatch_capacity
);
dispatch = dispatch_capacity;
}
extra_goods[*g2] -= amount2;
debug!("pay {:?} {:?} = {:?}", g2, amount2, balance);
balance -= amount2 * price2;
neighbor_orders[*g2] -= amount2;
dispatch_capacity = (dispatch_capacity - dispatch).max(0.0);
if balance == 0.0 {
break;
}
}
}
missing_dispatch += acute_missing_dispatch;
// adjust order if we are unable to pay for it
buy_target -= balance / *price;
buy_target = buy_target.min(amount);
collect_capacity = (collect_capacity - buy_target * effort).max(0.0);
neighbor_orders[*g] += buy_target;
amount -= buy_target;
debug!(
"deal amount {:?} end_balance {:?} price {:?} left {:?}",
buy_target, balance, *price, amount
);
}
}
}
}
// if site_id.id() == 1 {
// // cut down number of lines printed
// info!("orders {:#?}", neighbor_orders,);
// }
// TODO: Use planned orders and calculate value, stock etc. accordingly
for n in &site.economy.neighbors {
if let Some(orders) = neighbor_orders.get(&n.id) {
for (g, a) in orders.iter() {
result[g] += *a;
}
let to = TradeOrder {
customer: *site_id,
amount: *orders,
};
if let Some(o) = external_orders.get_mut(&n.id) {
// this is just to catch unbound growth (happened in development)
if o.len() < 100 {
o.push(to);
} else {
debug!("overflow {:?}", o);
}
} else {
external_orders.insert(n.id, vec![to]);
}
}
}
// return missing transport capacity
//missing_collect.max(missing_dispatch)
debug!(
"Tranportation {:?} {:?} {:?} {:?} {:?}",
transportation_capacity,
collect_capacity,
dispatch_capacity,
missing_collect,
missing_dispatch,
);
result[*TRANSPORTATION_INDEX] = -(transportation_capacity
- collect_capacity.min(dispatch_capacity)
+ missing_collect.max(missing_dispatch));
if site_id.id() == 1 {
debug!("Trade {:?}", result);
}
result
}
/// perform trade using neighboring orders (2nd step of trading)
fn trade_at_site(
site: Id<Site>,
orders: &mut Vec<TradeOrder>,
economy: &mut Economy,
deliveries: &mut DHashMap<Id<Site>, Vec<TradeDelivery>>,
) {
// make sure that at least this amount of stock remains available
// TODO: rework using economy.unconsumed_stock
let internal_orders = economy.get_orders();
let mut next_demand = GoodMap::from_default(0.0);
for (labor, orders) in &internal_orders {
let workers = if let Some(labor) = labor {
economy.labors[*labor]
} else {
1.0
} * economy.pop;
for (good, amount) in orders {
next_demand[*good] += *amount * workers;
assert!(next_demand[*good] >= 0.0);
}
}
//info!("Trade {} {}", site.id(), orders.len());
let mut total_orders: GoodMap<f32> = GoodMap::from_default(0.0);
for i in orders.iter() {
for (g, &a) in i.amount.iter().filter(|(_, a)| **a > 0.0) {
total_orders[g] += a;
}
}
let order_stock_ratio: GoodMap<Option<f32>> = GoodMap::from_iter(
economy
.stocks
.iter()
.map(|(g, a)| (g, *a, next_demand[g]))
.filter(|(_, a, s)| *a > *s)
.map(|(g, a, s)| (g, Some(total_orders[g] / (a - s)))),
None,
);
debug!("trade {} {:?}", site.id(), order_stock_ratio);
let prices = GoodMap::from_iter(
economy
.values
.iter()
.map(|(g, o)| (g, o.unwrap_or(0.0).max(Economy::MINIMUM_PRICE))),
0.0,
);
for o in orders.drain(..) {
// amount, local value (sell low value, buy high value goods first (trading
// town's interest))
let mut sorted_sell: Vec<(GoodIndex, f32, f32)> = o
.amount
.iter()
.filter(|(_, &a)| a > 0.0)
.map(|(g, a)| (g, *a, prices[g]))
.collect();
sorted_sell.sort_by(|a, b| (a.2.partial_cmp(&b.2).unwrap_or(Less)));
let mut sorted_buy: Vec<(GoodIndex, f32, f32)> = o
.amount
.iter()
.filter(|(_, &a)| a < 0.0)
.map(|(g, a)| (g, *a, prices[g]))
.collect();
sorted_buy.sort_by(|a, b| (b.2.partial_cmp(&a.2).unwrap_or(Less)));
debug!(
"with {} {:?} buy {:?}",
o.customer.id(),
sorted_sell,
sorted_buy
);
let mut good_delivery = GoodMap::from_default(0.0);
for (g, amount, price) in sorted_sell.iter() {
if let Some(order_stock_ratio) = order_stock_ratio[*g] {
let allocated_amount = *amount / order_stock_ratio.max(1.0);
let mut balance = allocated_amount * *price;
for (g2, avail, price2) in sorted_buy.iter_mut() {
let amount2 = (-*avail).min(balance / *price2);
assert!(amount2 >= 0.0);
economy.stocks[*g2] += amount2;
balance = (balance - amount2 * *price2).max(0.0);
*avail += amount2; // reduce (negative) brought stock
debug!("paid with {:?} {} {}", *g2, amount2, *price2);
if balance == 0.0 {
break;
}
}
let mut paid_amount = (allocated_amount - balance / *price).min(economy.stocks[*g]);
if paid_amount / allocated_amount < 0.95 {
debug!(
"Client {} is broke on {:?} : {} {} severity {}",
o.customer.id(),
*g,
paid_amount,
allocated_amount,
order_stock_ratio,
);
} else {
debug!("bought {:?} {} {}", *g, paid_amount, *price);
}
if economy.stocks[*g] - paid_amount < 0.0 {
info!(
"BUG {:?} {:?} {} TO {:?} OSR {:?} ND {:?}",
economy.stocks[*g],
*g,
paid_amount,
total_orders[*g],
order_stock_ratio,
next_demand[*g]
);
paid_amount = economy.stocks[*g];
}
good_delivery[*g] += paid_amount;
economy.stocks[*g] -= paid_amount;
}
}
for (g, amount, _) in sorted_buy.drain(..) {
if amount < 0.0 {
debug!("shipping back unsold {} of {:?}", amount, g);
good_delivery[g] += -amount;
}
}
let delivery = TradeDelivery {
supplier: site,
prices,
supply: GoodMap::from_iter(
economy.stocks.iter().map(|(g, a)| {
(g, {
(a - next_demand[g] - total_orders[g]).max(0.0) + good_delivery[g]
})
}),
0.0,
),
amount: good_delivery,
};
debug!(?delivery);
if let Some(deliveries) = deliveries.get_mut(&o.customer) {
deliveries.push(delivery);
} else {
deliveries.insert(o.customer, vec![delivery]);
}
}
if !orders.is_empty() {
info!("non empty orders {:?}", orders);
orders.clear();
}
}
/// 3rd step of trading
fn collect_deliveries(
site: &mut Site,
deliveries: &mut Vec<TradeDelivery>,
ctx: &mut vergleich::Context,
) {
// collect all the goods we shipped
let mut last_exports = GoodMap::from_iter(
site.economy
.active_exports
.iter()
.filter(|(_g, a)| **a > 0.0)
.map(|(g, a)| (g, *a)),
0.0,
);
// TODO: properly rate benefits created by merchants (done below?)
for mut d in deliveries.drain(..) {
let mut ictx = ctx.context(&format!("suppl {}", d.supplier.id()));
for i in d.amount.iter() {
last_exports[i.0] -= ictx.value(&format!("{:?}", i.0), *i.1);
}
// remember price
if let Some(n) = site
.economy
.neighbors
.iter_mut()
.find(|n| n.id == d.supplier)
{
// remember (and consume) last values
std::mem::swap(&mut n.last_values, &mut d.prices);
std::mem::swap(&mut n.last_supplies, &mut d.supply);
// add items to stock
for (g, a) in d.amount.iter() {
if *a < 0.0 {
// likely rounding error, ignore
debug!("Unexpected delivery for {:?} {}", g, *a);
} else {
site.economy.stocks[g] += *a;
}
}
}
}
if !deliveries.is_empty() {
info!("non empty deliveries {:?}", deliveries);
deliveries.clear();
}
std::mem::swap(&mut last_exports, &mut site.economy.last_exports);
//site.economy.active_exports.clear();
}
/// Simulate a site's economy. This simulation is roughly equivalent to the
/// Lange-Lerner model's solution to the socialist calculation problem. The
/// simulation begins by assigning arbitrary values to each commodity and then
/// incrementally updates them according to the final scarcity of the commodity
/// at the end of the tick. This results in the formulation of values that are
/// roughly analogous to prices for each commodity. The workforce is then
/// reassigned according to the respective commodity values. The simulation also
/// includes damping terms that prevent cyclical inconsistencies in value
/// rationalisation magnifying enough to crash the economy. We also ensure that
/// a small number of workers are allocated to every industry (even inactive
/// ones) each tick. This is not an accident: a small amount of productive
/// capacity in one industry allows the economy to quickly pivot to a different
/// production configuration should an additional commodity that acts as
/// production input become available. This means that the economy will
/// dynamically react to environmental changes. If a product becomes available
/// through a mechanism such as trade, an entire arm of the economy may
/// materialise to take advantage of this.
pub fn tick_site_economy(
index: &mut Index,
site_id: Id<Site>,
dt: f32,
mut vc: vergleich::Context,
) {
let site = &mut index.sites[site_id];
if !site.do_economic_simulation() {
return;
}
// collect goods from trading
if INTER_SITE_TRADE {
let deliveries = index.trade.deliveries.get_mut(&site_id);
if let Some(deliveries) = deliveries {
collect_deliveries(site, deliveries, &mut vc);
}
}
let orders = site.economy.get_orders();
let productivity = site.economy.get_productivity();
for i in productivity.iter() {
vc.context("productivity")
.value(&std::format!("{:?}{:?}", i.0, Good::from(i.1.0)), i.1.1);
}
let mut demand = GoodMap::from_default(0.0);
for (labor, orders) in &orders {
let workers = if let Some(labor) = labor {
site.economy.labors[*labor]
} else {
1.0
} * site.economy.pop;
for (good, amount) in orders {
demand[*good] += *amount * workers;
}
}
if INTER_SITE_TRADE {
demand[*COIN_INDEX] += Economy::STARTING_COIN; // if we spend coin value increases
}
// which labor is the merchant
let merchant_labor = productivity
.iter()
.find(|(_, v)| v.0 == *TRANSPORTATION_INDEX)
.map(|(l, _)| l);
let mut supply = site.economy.stocks; //GoodMap::from_default(0.0);
for (labor, goodvec) in productivity.iter() {
//for (output_good, _) in goodvec.iter() {
//info!("{} supply{:?}+={}", site_id.id(), Good::from(goodvec.0),
// site.economy.yields[labor] * site.economy.labors[labor] * site.economy.pop);
supply[goodvec.0] +=
site.economy.yields[labor] * site.economy.labors[labor] * site.economy.pop;
vc.context(&std::format!("{:?}-{:?}", Good::from(goodvec.0), labor))
.value("yields", site.economy.yields[labor]);
vc.context(&std::format!("{:?}-{:?}", Good::from(goodvec.0), labor))
.value("labors", site.economy.labors[labor]);
//}
}
for i in supply.iter() {
vc.context("supply")
.value(&std::format!("{:?}", Good::from(i.0)), *i.1);
}
let stocks = &site.economy.stocks;
for i in stocks.iter() {
vc.context("stocks")
.value(&std::format!("{:?}", Good::from(i.0)), *i.1);
}
site.economy.surplus = demand.map(|g, demand| supply[g] + stocks[g] - demand);
site.economy.marginal_surplus = demand.map(|g, demand| supply[g] - demand);
// plan trading with other sites
let external_orders = &mut index.trade.orders;
let mut potential_trade = GoodMap::from_default(0.0);
// use last year's generated transportation for merchants (could we do better?
// this is in line with the other professions)
let transportation_capacity = site.economy.stocks[*TRANSPORTATION_INDEX];
let trade = if INTER_SITE_TRADE {
let trade = plan_trade_for_site(
site,
&site_id,
transportation_capacity,
external_orders,
&mut potential_trade,
);
site.economy.active_exports = GoodMap::from_iter(trade.iter().map(|(g, a)| (g, -*a)), 0.0); // TODO: check for availability?
// add the wares to sell to demand and the goods to buy to supply
for (g, a) in trade.iter() {
vc.context("trade")
.value(&std::format!("{:?}", Good::from(g)), *a);
if *a > 0.0 {
supply[g] += *a;
assert!(supply[g] >= 0.0);
} else {
demand[g] -= *a;
assert!(demand[g] >= 0.0);
}
}
trade
} else {
GoodMap::default()
};
// Update values according to the surplus of each stock
// Note that values are used for workforce allocation and are not the same thing
// as price
// fall back to old (less wrong than other goods) coin logic
let old_coin_surplus = site.economy.stocks[*COIN_INDEX] - demand[*COIN_INDEX];
let values = &mut site.economy.values;
site.economy.surplus.iter().for_each(|(good, surplus)| {
let old_surplus = if good == *COIN_INDEX {
old_coin_surplus
} else {
*surplus
};
// Value rationalisation
let goodname = std::format!("{:?}", Good::from(good));
vc.context("old_surplus").value(&goodname, old_surplus);
vc.context("demand").value(&goodname, demand[good]);
let val = 2.0f32.powf(1.0 - old_surplus / demand[good]);
let smooth = 0.8;
values[good] = if val > 0.001 && val < 1000.0 {
Some(vc.context("values").value(
&goodname,
smooth * values[good].unwrap_or(val) + (1.0 - smooth) * val,
))
} else {
None
};
});
let all_trade_goods: DHashSet<GoodIndex> = trade
.iter()
.chain(potential_trade.iter())
.filter(|(_, a)| **a > 0.0)
.map(|(g, _)| g)
.collect();
//let empty_goods: DHashSet<GoodIndex> = DHashSet::default();
// TODO: Does avg/max/sum make most sense for labors creating more than one good
// summing favors merchants too much (as they will provide multiple
// goods, so we use max instead)
let labor_ratios: LaborMap<f32> = LaborMap::from_iter(
productivity.iter().map(|(labor, goodvec)| {
(
labor,
if Some(labor) == merchant_labor {
all_trade_goods
.iter()
.chain(std::iter::once(&goodvec.0))
.map(|&output_good| site.economy.values[output_good].unwrap_or(0.0))
.max_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap_or(Less))
} else {
site.economy.values[goodvec.0]
}
.unwrap_or(0.0)
* site.economy.productivity[labor],
)
}),
0.0,
);
debug!(?labor_ratios);
let labor_ratio_sum = labor_ratios.iter().map(|(_, r)| *r).sum::<f32>().max(0.01);
let mut labor_context = vc.context("labor");
productivity.iter().for_each(|(labor, _)| {
let smooth = 0.8;
site.economy.labors[labor] = labor_context.value(
&format!("{:?}", labor),
smooth * site.economy.labors[labor]
+ (1.0 - smooth)
* (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum),
);
assert!(site.economy.labors[labor] >= 0.0);
});
// Production
let stocks_before = site.economy.stocks;
// TODO: Should we recalculate demand after labor reassignment?
let direct_use = direct_use_goods();
// Handle the stocks you can't pile (decay)
for g in direct_use {
site.economy.stocks[*g] = 0.0;
}
let mut total_labor_values = GoodMap::<f32>::default();
// TODO: trade
let mut total_outputs = GoodMap::<f32>::default();
for (labor, orders) in orders.iter() {
let workers = if let Some(labor) = labor {
site.economy.labors[*labor]
} else {
1.0
} * site.economy.pop;
assert!(workers >= 0.0);
let is_merchant = merchant_labor == *labor;
// For each order, we try to find the minimum satisfaction rate - this limits
// how much we can produce! For example, if we need 0.25 fish and
// 0.75 oats to make 1 unit of food, but only 0.5 units of oats are
// available then we only need to consume 2/3rds
// of other ingredients and leave the rest in stock
// In effect, this is the productivity
let labor_productivity = orders
.iter()
.map(|(good, amount)| {
// What quantity is this order requesting?
let _quantity = *amount * workers;
assert!(stocks_before[*good] >= 0.0);
assert!(demand[*good] >= 0.0);
// What proportion of this order is the economy able to satisfy?
(stocks_before[*good] / demand[*good]).min(1.0)
})
.min_by(|a, b| a.partial_cmp(b).unwrap_or(Less))
.unwrap_or_else(|| panic!("Industry {:?} requires at least one input order", labor));
assert!(labor_productivity >= 0.0);
let mut total_materials_cost = 0.0;
for (good, amount) in orders {
// What quantity is this order requesting?
let quantity = *amount * workers;
// What amount gets actually used in production?
let used = quantity * labor_productivity;
// Material cost of each factor of production
total_materials_cost += used * site.economy.labor_values[*good].unwrap_or(0.0);
// Deplete stocks accordingly
if !direct_use.contains(good) {
site.economy.stocks[*good] = (site.economy.stocks[*good] - used).max(0.0);
}
}
let mut produced_goods: GoodMap<f32> = GoodMap::from_default(0.0);
if INTER_SITE_TRADE && is_merchant {
// TODO: replan for missing merchant productivity???
for (g, a) in trade.iter() {
if !direct_use.contains(&g) {
if *a < 0.0 {
// take these goods to the road
if site.economy.stocks[g] + *a < 0.0 {
// we have a problem: Probably due to a shift in productivity we have
// less goods available than planned,
// so we would need to reduce the amount shipped
debug!("NEG STOCK {:?} {} {}", g, site.economy.stocks[g], *a);
let reduced_amount = site.economy.stocks[g];
let planned_amount: f32 = external_orders
.iter()
.map(|i| {
i.1.iter()
.filter(|o| o.customer == site_id)
.map(|j| j.amount[g])
.sum::<f32>()
})
.sum();
let scale = reduced_amount / planned_amount.abs();
debug!("re-plan {} {} {}", reduced_amount, planned_amount, scale);
for k in external_orders.iter_mut() {
for l in k.1.iter_mut().filter(|o| o.customer == site_id) {
l.amount[g] *= scale;
}
}
site.economy.stocks[g] = 0.0;
}
// assert!(site.economy.stocks[g] + *a >= 0.0);
else {
site.economy.stocks[g] += *a;
}
}
total_materials_cost += (-*a) * site.economy.labor_values[g].unwrap_or(0.0);
} else {
// count on receiving these
produced_goods[g] += *a;
}
}
debug!(
"merchant {} {}: {:?} {} {:?}",
site_id.id(),
site.economy.pop,
produced_goods,
total_materials_cost,
trade
);
}
// Industries produce things
if let Some(labor) = labor {
let work_products = &productivity[*labor];
//let workers = site.economy.labors[*labor] * site.economy.pop;
//let final_rate = rate;
//let yield_per_worker = labor_productivity;
site.economy.yields[*labor] = labor_productivity * work_products.1;
site.economy.productivity[*labor] = labor_productivity;
//let total_product_rate: f32 = work_products.iter().map(|(_, r)| *r).sum();
let (stock, rate) = work_products;
let total_output = labor_productivity * *rate * workers;
assert!(total_output >= 0.0);
site.economy.stocks[*stock] += total_output;
produced_goods[*stock] += total_output;
let produced_amount: f32 = produced_goods.iter().map(|(_, a)| *a).sum();
for (stock, amount) in produced_goods.iter() {
let cost_weight = amount / produced_amount.max(0.001);
// Materials cost per unit
// TODO: How to handle this reasonably for multiple producers (collect upper and
// lower term separately)
site.economy.material_costs[stock] =
total_materials_cost / amount.max(0.001) * cost_weight;
// Labor costs
let wages = 1.0;
let total_labor_cost = workers * wages;
total_labor_values[stock] +=
(total_materials_cost + total_labor_cost) * cost_weight;
total_outputs[stock] += amount;
}
}
}
// Update labour values per unit
site.economy.labor_values = total_labor_values.map(|stock, tlv| {
let total_output = total_outputs[stock];
if total_output > 0.01 {
Some(tlv / total_output)
} else {
None
}
});
// Decay stocks (the ones which totally decay are handled later)
site.economy
.stocks
.iter_mut()
.map(|(c, v)| (v, 1.0 - decay_rate(c)))
.for_each(|(v, factor)| *v *= factor);
// Decay stocks
site.economy.replenish(index.time);
// Births/deaths
const NATURAL_BIRTH_RATE: f32 = 0.05;
const DEATH_RATE: f32 = 0.005;
let birth_rate = if site.economy.surplus[*FOOD_INDEX] > 0.0 {
NATURAL_BIRTH_RATE
} else {
0.0
};
site.economy.pop += vc.value(
"pop",
dt / YEAR * site.economy.pop * (birth_rate - DEATH_RATE),
);
// calculate the new unclaimed stock
//let next_orders = site.economy.get_orders();
// orders are static
let mut next_demand = GoodMap::from_default(0.0);
for (labor, orders) in orders.iter() {
let workers = if let Some(labor) = labor {
site.economy.labors[*labor]
} else {
1.0
} * site.economy.pop;
for (good, amount) in orders {
next_demand[*good] += *amount * workers;
assert!(next_demand[*good] >= 0.0);
}
}
let mut us = vc.context("unconsumed");
site.economy.unconsumed_stock = GoodMap::from_iter(
site.economy.stocks.iter().map(|(g, a)| {
(
g,
us.value(&format!("{:?}", Good::from(g)), *a - next_demand[g]),
)
}),
0.0,
);
}
#[cfg(test)]
mod tests {
use crate::{sim, site::economy::GoodMap, util::seed_expan};
use common::trade::Good;
use rand::SeedableRng;
use rand_chacha::ChaChaRng;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use tracing::{info, Level};
use tracing_subscriber::{
filter::{EnvFilter, LevelFilter},
FmtSubscriber,
};
use vek::Vec2;
// enable info!
fn init() {
FmtSubscriber::builder()
.with_max_level(Level::ERROR)
.with_env_filter(EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into()))
.init();
}
#[derive(Debug, Serialize, Deserialize)]
struct ResourcesSetup {
good: Good,
amount: f32,
}
#[derive(Debug, Serialize, Deserialize)]
struct EconomySetup {
name: String,
position: (i32, i32),
kind: common::terrain::site::SitesKind,
neighbors: Vec<(u64, usize)>, // id, travel_distance
resources: Vec<ResourcesSetup>,
}
#[test]
fn test_economy() {
init();
let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
info!("init");
let seed = 59686;
let opts = sim::WorldOpts {
seed_elements: true,
world_file: sim::FileOpts::LoadAsset(sim::DEFAULT_WORLD_MAP.into()),
//sim::FileOpts::LoadAsset("world.map.economy_8x8".into()),
calendar: None,
};
let mut index = crate::index::Index::new(seed);
info!("Index created");
let mut sim = sim::WorldSim::generate(seed, opts, &threadpool);
info!("World loaded");
let regenerate_input = false;
if regenerate_input {
let _civs = crate::civ::Civs::generate(seed, &mut sim, &mut index);
info!("Civs created");
let mut outarr: Vec<EconomySetup> = Vec::new();
for i in index.sites.values() {
let resources: Vec<ResourcesSetup> = i
.economy
.natural_resources
.chunks_per_resource
.iter()
.map(|(good, a)| ResourcesSetup {
good: good.into(),
amount: *a * i.economy.natural_resources.average_yield_per_chunk[good],
})
.collect();
let neighbors = i
.economy
.neighbors
.iter()
.map(|j| (j.id.id(), j.travel_distance))
.collect();
let val = EconomySetup {
name: i.name().into(),
position: (i.get_origin().x, i.get_origin().y),
resources,
neighbors,
kind: match i.kind {
crate::site::SiteKind::Settlement(_) => {
common::terrain::site::SitesKind::Settlement
},
crate::site::SiteKind::Dungeon(_) => {
common::terrain::site::SitesKind::Dungeon
},
crate::site::SiteKind::Castle(_) => {
common::terrain::site::SitesKind::Castle
},
crate::site::SiteKind::Refactor(_) => {
common::terrain::site::SitesKind::Settlement
},
_ => common::terrain::site::SitesKind::Void,
},
};
outarr.push(val);
}
let pretty = ron::ser::PrettyConfig::new();
if let Ok(result) = ron::ser::to_string_pretty(&outarr, pretty) {
info!("RON {}", result);
}
} else {
let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
let ron_file = std::fs::File::open("economy_testinput2.ron")
.expect("economy_testinput2.ron not found");
let econ_testinput: Vec<EconomySetup> =
ron::de::from_reader(ron_file).expect("economy_testinput2.ron parse error");
for i in econ_testinput.iter() {
let wpos = Vec2 {
x: i.position.0,
y: i.position.1,
};
// this should be a moderate compromise between regenerating the full world and
// loading on demand using the public API. There is no way to set
// the name, do we care?
let mut settlement = match i.kind {
common::terrain::site::SitesKind::Castle => crate::site::Site::castle(
crate::site::Castle::generate(wpos, None, &mut rng),
),
common::terrain::site::SitesKind::Dungeon => crate::site::Site::dungeon(
crate::site2::Site::generate_dungeon(&crate::Land::empty(), &mut rng, wpos),
),
// common::terrain::site::SitesKind::Settlement |
_ => crate::site::Site::settlement(crate::site::Settlement::generate(
wpos, None, &mut rng,
)),
};
for g in i.resources.iter() {
//let c = sim::SimChunk::new();
//settlement.economy.add_chunk(ch, distance_squared)
// bypass the API for now
settlement.economy.natural_resources.chunks_per_resource
[g.good.try_into().unwrap_or_default()] = g.amount;
settlement.economy.natural_resources.average_yield_per_chunk
[g.good.try_into().unwrap_or_default()] = 1.0;
}
index.sites.insert(settlement);
}
// we can't add these in the first loop as neighbors will refer to later sites
// (which aren't valid in the first loop)
for (i, e) in econ_testinput.iter().enumerate() {
if let Some(id) = index.sites.recreate_id(i as u64) {
let mut neighbors: Vec<crate::site::economy::NeighborInformation> = e
.neighbors
.iter()
.flat_map(|(nid, dist)| index.sites.recreate_id(*nid).map(|i| (i, dist)))
.map(|(nid, dist)| crate::site::economy::NeighborInformation {
id: nid,
travel_distance: *dist,
last_values: GoodMap::from_default(0.0),
last_supplies: GoodMap::from_default(0.0),
})
.collect();
index
.sites
.get_mut(id)
.economy
.neighbors
.append(&mut neighbors);
}
}
}
crate::sim2::simulate(&mut index, &mut sim);
}
}