only the code move part of shredded_economy4

This commit is contained in:
Christof Petig 2022-06-01 21:02:50 +02:00
parent 35f9c5cbdf
commit edffd576d4
3 changed files with 772 additions and 767 deletions

View File

@ -1,739 +0,0 @@
use crate::{
sim::WorldSim,
site::{
economy::{
good_list, vergleich, LaborIndex, COIN_INDEX, DAYS_PER_MONTH, DAYS_PER_YEAR,
INTER_SITE_TRADE,
},
Site, SiteKind,
},
Index,
};
use rayon::prelude::*;
use tracing::{debug, info};
const TICK_PERIOD: f32 = 3.0 * DAYS_PER_MONTH; // 3 months
const HISTORY_DAYS: f32 = 500.0 * DAYS_PER_YEAR; // 500 years
const GENERATE_CSV: bool = false;
/// 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 crate::site::economy::GoodIndex;
use std::io::Write;
write!(
*f,
"{}, {}, {}, {:.1}, {},,",
site.name(),
site.get_origin().x,
site.get_origin().y,
site.economy.pop,
site.economy.neighbors.len(),
)?;
for g in good_list() {
if let Some(value) = site.economy.values[g] {
write!(*f, "{:.2},", value)?;
} else {
f.write_all(b",")?;
}
}
f.write_all(b",")?;
for g in good_list() {
if let Some(labor_value) = site.economy.labor_values[g] {
write!(f, "{:.2},", labor_value)?;
} else {
f.write_all(b",")?;
}
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:.1},", site.economy.stocks[g])?;
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:.1},", site.economy.marginal_surplus[g])?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:.1},", site.economy.labors[l] * site.economy.pop)?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:.2},", site.economy.productivity[l])?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:.1},", site.economy.yields[l])?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
let limit = site.economy.limited_by[l];
if limit == GoodIndex::default() {
f.write_all(b",")?;
} else {
write!(f, "{:?},", limit)?;
}
}
f.write_all(b",")?;
for g in good_list() {
if site.economy.last_exports[g] >= 0.1 || site.economy.last_exports[g] <= -0.1 {
write!(f, "{:.1},", site.economy.last_exports[g])?;
} else {
f.write_all(b",")?;
}
}
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,Neighbors,,")?;
for g in good_list() {
write!(f, "{:?} Value,", g)?;
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:?} LaborVal,", g)?;
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:?} Stock,", g)?;
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:?} Surplus,", g)?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:?} Labor,", l)?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:?} Productivity,", l)?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:?} Yields,", l)?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:?} limit,", l)?;
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:?} trade,", g)?;
}
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 / DAYS_PER_YEAR) as i32 % 50 == 0 && (index.time % DAYS_PER_YEAR) as i32 == 0
{
debug!("Year {}", (index.time / DAYS_PER_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();
let giant_trees = EconStatistics::default();
for site in index.sites.ids() {
let site = &index.sites[site];
match site.kind {
SiteKind::Settlement(_) | SiteKind::Refactor(_) | SiteKind::CliffTown(_) => {
towns += site.economy.pop
},
SiteKind::Dungeon(_) => dungeons += site.economy.pop,
SiteKind::Castle(_) => castles += site.economy.pop,
SiteKind::Tree(_) => (),
SiteKind::GiantTree(_) => (),
SiteKind::Gnarling(_) => {},
}
}
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)
);
}
if giant_trees.valid() {
info!(
"Giant Trees {:.0}-{:.0} avg {:.0}",
giant_trees.min,
giant_trees.max,
giant_trees.sum / (giant_trees.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, _vc: vergleich::Context) {
if INTER_SITE_TRADE {
// move deliverables to recipient cities
for (id, deliv) in index.trade.deliveries.drain() {
index.sites.get_mut(id).economy.deliveries.extend(deliv);
}
}
index.sites.par_iter_mut().for_each(|(site_id, site)| {
if site.do_economic_simulation() {
site.economy.tick(site_id, dt, vergleich::Context::dummy());
// helpful for debugging but not compatible with parallel execution
// vc.context(&site_id.id().to_string()));
}
});
if INTER_SITE_TRADE {
// distribute orders (travelling merchants)
for (_id, site) in index.sites.iter_mut() {
for (i, mut v) in site.economy.orders.drain() {
index
.trade
.orders
.entry(i)
.or_insert(Vec::new())
.append(&mut v);
}
}
// trade at sites
for (&site, orders) in index.trade.orders.iter_mut() {
let siteinfo = index.sites.get_mut(site);
if siteinfo.do_economic_simulation() {
siteinfo
.economy
.trade_at_site(site, orders, &mut index.trade.deliveries);
}
}
}
//check_money(index);
index.time += dt;
}
#[cfg(test)]
mod tests {
use crate::{sim, site::economy::GoodMap, util::seed_expan};
use common::{store::Id, terrain::BiomeKind, trade::Good};
use rand::SeedableRng;
use rand_chacha::ChaChaRng;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use tracing::{info, Dispatch, Level};
use tracing_subscriber::{filter::EnvFilter, FmtSubscriber};
use vek::Vec2;
fn execute_with_tracing(level: Level, func: fn()) {
tracing::dispatcher::with_default(
&Dispatch::new(
FmtSubscriber::builder()
.with_max_level(level)
.with_env_filter(EnvFilter::from_default_env())
.finish(),
),
func,
);
}
#[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>,
}
fn print_sorted(prefix: &str, mut list: Vec<(String, f32)>, threshold: f32, decimals: usize) {
print!("{}", prefix);
list.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Less));
for i in list.iter() {
if i.1 >= threshold {
print!("{}={:.*} ", i.0, decimals, i.1);
}
}
println!();
}
fn show_economy(sites: &common::store::Store<crate::site::Site>) {
use crate::site::economy::good_list;
for (id, site) in sites.iter() {
println!("Site id {:?} name {}", id, site.name());
//assert!(site.economy.sanity_check());
print!(" Resources: ");
for i in good_list() {
let amount = site.economy.natural_resources.chunks_per_resource[i];
if amount > 0.0 {
print!("{:?}={} ", i, amount);
}
}
println!();
println!(
" Population {:.1}, limited by {:?}",
site.economy.pop, site.economy.population_limited_by
);
let idle: f32 =
site.economy.pop * (1.0 - site.economy.labors.iter().map(|(_, a)| *a).sum::<f32>());
print_sorted(
&format!(" Professions: idle={:.1} ", idle),
site.economy
.labors
.iter()
.map(|(l, a)| (format!("{:?}", l), *a * site.economy.pop))
.collect(),
site.economy.pop * 0.05,
1,
);
print_sorted(
" Stock: ",
site.economy
.stocks
.iter()
.map(|(l, a)| (format!("{:?}", l), *a))
.collect(),
1.0,
0,
);
print_sorted(
" Values: ",
site.economy
.values
.iter()
.map(|(l, a)| {
(
format!("{:?}", l),
a.map(|v| if v > 3.9 { 0.0 } else { v }).unwrap_or(0.0),
)
})
.collect(),
0.1,
1,
);
print_sorted(
" Labor Values: ",
site.economy
.labor_values
.iter()
.map(|(l, a)| (format!("{:?}", l), a.unwrap_or(0.0)))
.collect(),
0.1,
1,
);
print!(" Limited: ");
for (limit, prod) in site
.economy
.limited_by
.iter()
.zip(site.economy.productivity.iter())
{
if (0.01..=0.99).contains(prod.1) {
print!("{:?}:{:?}={} ", limit.0, limit.1, *prod.1);
}
}
println!();
print!(" Trade({}): ", site.economy.neighbors.len());
for (g, &amt) in site.economy.active_exports.iter() {
if !(-0.1..=0.1).contains(&amt) {
print!("{:?}={:.2} ", g, amt);
}
}
println!();
// check population (shrinks if economy gets broken)
// assert!(site.economy.pop >= env.targets[&id]);
}
}
/// output the economy of the currently active world
// this expensive test is for manual inspection, not to be run automated
// recommended command: cargo test test_economy0 -- --nocapture --ignored
#[test]
#[ignore]
fn test_economy0() {
execute_with_tracing(Level::INFO, || {
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 _civs = crate::civ::Civs::generate(seed, &mut sim, &mut index);
info!("Civs created");
crate::sim2::simulate(&mut index, &mut sim);
show_economy(&index.sites);
});
}
/// output the economy of a small set of villages, loaded from ron
// this cheaper test is for manual inspection, not to be run automated
#[test]
#[ignore]
fn test_economy1() {
execute_with_tracing(Level::INFO, || {
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(_)
| crate::site::SiteKind::Refactor(_)
| crate::site::SiteKind::CliffTown(_) => {
common::terrain::site::SitesKind::Settlement
},
crate::site::SiteKind::Dungeon(_) => {
common::terrain::site::SitesKind::Dungeon
},
crate::site::SiteKind::Castle(_) => {
common::terrain::site::SitesKind::Castle
},
_ => 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);
show_economy(&index.sites);
});
}
struct Simenv {
index: crate::index::Index,
rng: ChaChaRng,
targets: hashbrown::HashMap<Id<crate::site::Site>, f32>,
}
#[test]
/// test whether a site in moderate climate can survive on its own
fn test_economy_moderate_standalone() {
fn add_settlement(
env: &mut Simenv,
// index: &mut crate::index::Index,
// rng: &mut ChaCha20Rng,
_name: &str,
target: f32,
resources: &[(Good, f32)],
) -> Id<crate::site::Site> {
let wpos = Vec2 { x: 42, y: 42 };
let mut settlement = crate::site::Site::settlement(crate::site::Settlement::generate(
wpos,
None,
&mut env.rng,
//Some(name),
));
for (good, amount) in resources.iter() {
settlement.economy.natural_resources.chunks_per_resource
[(*good).try_into().unwrap_or_default()] = *amount;
settlement.economy.natural_resources.average_yield_per_chunk
[(*good).try_into().unwrap_or_default()] = 1.0;
}
let id = env.index.sites.insert(settlement);
env.targets.insert(id, target);
id
}
execute_with_tracing(Level::ERROR, || {
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()),
calendar: Default::default(),
};
let index = crate::index::Index::new(seed);
info!("Index created");
let mut sim = sim::WorldSim::generate(seed, opts, &threadpool);
info!("World loaded");
let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
let mut env = Simenv {
index,
rng,
targets: hashbrown::HashMap::new(),
};
add_settlement(&mut env, "Forest", 5000.0, &[(
Good::Terrain(BiomeKind::Forest),
100.0_f32,
)]);
add_settlement(&mut env, "Grass", 900.0, &[(
Good::Terrain(BiomeKind::Grassland),
100.0_f32,
)]);
add_settlement(&mut env, "Mountain", 3.0, &[(
Good::Terrain(BiomeKind::Mountain),
100.0_f32,
)]);
// add_settlement(&mut env, "Desert", 19.0, &[(
// Good::Terrain(BiomeKind::Desert),
// 100.0_f32,
// )]);
// add_settlement(&mut index, &mut rng, &[
// (Good::Terrain(BiomeKind::Jungle), 100.0_f32),
// ]);
// add_settlement(&mut index, &mut rng, &[
// (Good::Terrain(BiomeKind::Snowland), 100.0_f32),
// ]);
add_settlement(&mut env, "GrFoMo", 12000.0, &[
(Good::Terrain(BiomeKind::Grassland), 100.0_f32),
(Good::Terrain(BiomeKind::Forest), 100.0_f32),
(Good::Terrain(BiomeKind::Mountain), 10.0_f32),
]);
// add_settlement(&mut env, "Mountain", 19.0, &[
// (Good::Terrain(BiomeKind::Mountain), 100.0_f32),
// // (Good::CaveAccess, 100.0_f32),
// ]);
// connect to neighbors (one way)
for i in 1..(env.index.sites.ids().count() as u64 - 1) {
let previous = env.index.sites.recreate_id(i - 1);
let center = env.index.sites.recreate_id(i);
center.zip(previous).map(|(center, previous)| {
env.index.sites[center]
.economy
.add_neighbor(previous, i as usize);
env.index.sites[previous]
.economy
.add_neighbor(center, i as usize);
});
}
crate::sim2::simulate(&mut env.index, &mut sim);
show_economy(&env.index.sites);
// check population (shrinks if economy gets broken)
for (id, site) in env.index.sites.iter() {
assert!(site.economy.pop >= env.targets[&id]);
}
});
}
}

View File

@ -0,0 +1,560 @@
use crate::{
sim::WorldSim,
site::{
economy::{
good_list, vergleich, LaborIndex, COIN_INDEX, DAYS_PER_MONTH, DAYS_PER_YEAR,
INTER_SITE_TRADE,
},
Site, SiteKind,
},
Index,
};
use rayon::prelude::*;
use tracing::{debug, info};
// this is an empty replacement for https://github.com/cpetig/vergleich
// which can be used to compare values acros runs
pub 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 {
#[must_use]
pub fn context(&mut self, _: &str) -> Context { Context {} }
pub fn value(&mut self, _: &str, val: f32) -> f32 { val }
pub fn dummy() -> Self { Context {} }
}
}
const TICK_PERIOD: f32 = 3.0 * DAYS_PER_MONTH; // 3 months
const HISTORY_DAYS: f32 = 500.0 * DAYS_PER_YEAR; // 500 years
/// 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() }
}
fn simulate_return(index: &mut Index, world: &mut WorldSim) -> Result<(), std::io::Error> {
// ...
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();
let giant_trees = EconStatistics::default();
for site in index.sites.ids() {
let site = &index.sites[site];
match site.kind {
SiteKind::Settlement(_) | SiteKind::Refactor(_) | SiteKind::CliffTown(_) => {
towns += site.economy.pop
},
SiteKind::Dungeon(_) => dungeons += site.economy.pop,
SiteKind::Castle(_) => castles += site.economy.pop,
SiteKind::Tree(_) => (),
SiteKind::GiantTree(_) => (),
SiteKind::Gnarling(_) => {},
}
}
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)
);
}
if giant_trees.valid() {
info!(
"Giant Trees {:.0}-{:.0} avg {:.0}",
giant_trees.min,
giant_trees.max,
giant_trees.sum / (giant_trees.count as f32)
)
}
check_money(index);
}
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 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 / DAYS_PER_YEAR) as i32 % 50 == 0 && (index.time % DAYS_PER_YEAR) as i32 == 0
{
debug!("Year {}", (index.time / DAYS_PER_YEAR) as i32);
}
tick(index, world, TICK_PERIOD, vr.context(&i.to_string()));
}
tracing::info!("economy simulation end");
}
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, _vc: vergleich::Context) {
if INTER_SITE_TRADE {
// move deliverables to recipient cities
for (id, deliv) in index.trade.deliveries.drain() {
index.sites.get_mut(id).economy.deliveries.extend(deliv);
}
}
index.sites.par_iter_mut().for_each(|(site_id, site)| {
if site.do_economic_simulation() {
site.economy.tick(site_id, dt, vergleich::Context::dummy());
// helpful for debugging but not compatible with parallel execution
// vc.context(&site_id.id().to_string()));
}
});
if INTER_SITE_TRADE {
// distribute orders (travelling merchants)
for (_id, site) in index.sites.iter_mut() {
for (i, mut v) in site.economy.orders.drain() {
index
.trade
.orders
.entry(i)
.or_insert(Vec::new())
.append(&mut v);
}
}
// trade at sites
for (&site, orders) in index.trade.orders.iter_mut() {
let siteinfo = index.sites.get_mut(site);
if siteinfo.do_economic_simulation() {
siteinfo
.economy
.trade_at_site(site, orders, &mut index.trade.deliveries);
}
}
}
//check_money(index);
index.time += dt;
}
#[cfg(test)]
mod tests {
use crate::{sim, site::economy::GoodMap, util::seed_expan};
use common::{store::Id, terrain::BiomeKind, trade::Good};
use rand::SeedableRng;
use rand_chacha::ChaChaRng;
use serde::{Deserialize, Serialize};
use std::convert::TryInto;
use tracing::{info, Dispatch, Level};
use tracing_subscriber::{filter::EnvFilter, FmtSubscriber};
use vek::Vec2;
fn execute_with_tracing(level: Level, func: fn()) {
tracing::dispatcher::with_default(
&Dispatch::new(
FmtSubscriber::builder()
.with_max_level(level)
.with_env_filter(EnvFilter::from_default_env())
.finish(),
),
func,
);
}
#[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>,
}
fn show_economy(sites: &common::store::Store<crate::site::Site>) {
use crate::site::economy::good_list;
for (id, site) in sites.iter() {
println!("Site id {:?} name {}", id, site.name());
}
}
/// output the economy of the currently active world
// this expensive test is for manual inspection, not to be run automated
// recommended command: cargo test test_economy0 -- --nocapture --ignored
#[test]
#[ignore]
fn test_economy0() {
execute_with_tracing(Level::INFO, || {
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 _civs = crate::civ::Civs::generate(seed, &mut sim, &mut index);
info!("Civs created");
crate::sim2::simulate(&mut index, &mut sim);
show_economy(&index.sites);
});
}
/// output the economy of a small set of villages, loaded from ron
// this cheaper test is for manual inspection, not to be run automated
#[test]
#[ignore]
fn test_economy1() {
execute_with_tracing(Level::INFO, || {
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(_)
| crate::site::SiteKind::Refactor(_)
| crate::site::SiteKind::CliffTown(_) => {
common::terrain::site::SitesKind::Settlement
},
crate::site::SiteKind::Dungeon(_) => {
common::terrain::site::SitesKind::Dungeon
},
crate::site::SiteKind::Castle(_) => {
common::terrain::site::SitesKind::Castle
},
_ => 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);
show_economy(&index.sites);
});
}
struct Simenv {
index: crate::index::Index,
rng: ChaChaRng,
targets: hashbrown::HashMap<Id<crate::site::Site>, f32>,
}
#[test]
/// test whether a site in moderate climate can survive on its own
fn test_economy_moderate_standalone() {
fn add_settlement(
env: &mut Simenv,
// index: &mut crate::index::Index,
// rng: &mut ChaCha20Rng,
_name: &str,
target: f32,
resources: &[(Good, f32)],
) -> Id<crate::site::Site> {
let wpos = Vec2 { x: 42, y: 42 };
let mut settlement = crate::site::Site::settlement(crate::site::Settlement::generate(
wpos,
None,
&mut env.rng,
//Some(name),
));
for (good, amount) in resources.iter() {
settlement.economy.natural_resources.chunks_per_resource
[(*good).try_into().unwrap_or_default()] = *amount;
settlement.economy.natural_resources.average_yield_per_chunk
[(*good).try_into().unwrap_or_default()] = 1.0;
}
let id = env.index.sites.insert(settlement);
env.targets.insert(id, target);
id
}
execute_with_tracing(Level::ERROR, || {
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()),
calendar: Default::default(),
};
let index = crate::index::Index::new(seed);
info!("Index created");
let mut sim = sim::WorldSim::generate(seed, opts, &threadpool);
info!("World loaded");
let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
let mut env = Simenv {
index,
rng,
targets: hashbrown::HashMap::new(),
};
add_settlement(&mut env, "Forest", 5000.0, &[(
Good::Terrain(BiomeKind::Forest),
100.0_f32,
)]);
add_settlement(&mut env, "Grass", 900.0, &[(
Good::Terrain(BiomeKind::Grassland),
100.0_f32,
)]);
add_settlement(&mut env, "Mountain", 3.0, &[(
Good::Terrain(BiomeKind::Mountain),
100.0_f32,
)]);
// add_settlement(&mut env, "Desert", 19.0, &[(
// Good::Terrain(BiomeKind::Desert),
// 100.0_f32,
// )]);
// add_settlement(&mut index, &mut rng, &[
// (Good::Terrain(BiomeKind::Jungle), 100.0_f32),
// ]);
// add_settlement(&mut index, &mut rng, &[
// (Good::Terrain(BiomeKind::Snowland), 100.0_f32),
// ]);
add_settlement(&mut env, "GrFoMo", 12000.0, &[
(Good::Terrain(BiomeKind::Grassland), 100.0_f32),
(Good::Terrain(BiomeKind::Forest), 100.0_f32),
(Good::Terrain(BiomeKind::Mountain), 10.0_f32),
]);
// add_settlement(&mut env, "Mountain", 19.0, &[
// (Good::Terrain(BiomeKind::Mountain), 100.0_f32),
// // (Good::CaveAccess, 100.0_f32),
// ]);
// connect to neighbors (one way)
for i in 1..(env.index.sites.ids().count() as u64 - 1) {
let previous = env.index.sites.recreate_id(i - 1);
let center = env.index.sites.recreate_id(i);
center.zip(previous).map(|(center, previous)| {
env.index.sites[center]
.economy
.add_neighbor(previous, i as usize);
env.index.sites[previous]
.economy
.add_neighbor(center, i as usize);
});
}
crate::sim2::simulate(&mut env.index, &mut sim);
show_economy(&env.index.sites);
// check population (shrinks if economy gets broken)
for (id, site) in env.index.sites.iter() {
assert!(site.economy.pop >= env.targets[&id]);
}
});
}
}

View File

@ -19,34 +19,7 @@ pub use map_types::{GoodIndex, GoodMap, Labor, LaborIndex, LaborMap, NaturalReso
pub const INTER_SITE_TRADE: bool = true;
pub const DAYS_PER_MONTH: f32 = 30.0;
pub const DAYS_PER_YEAR: f32 = 12.0 * DAYS_PER_MONTH;
// this is an empty replacement for https://github.com/cpetig/vergleich
// which can be used to compare values acros runs
pub 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 {
#[must_use]
pub fn context(&mut self, _: &str) -> Context { Context {} }
pub fn value(&mut self, _: &str, val: f32) -> f32 { val }
pub fn dummy() -> Self { Context {} }
}
}
const GENERATE_CSV: bool = false;
#[derive(Debug)]
pub struct TradeOrder {
@ -1136,6 +1109,217 @@ impl Economy {
0.0,
);
}
pub fn csv_entry(f: &mut std::fs::File, site: &Site) -> Result<(), std::io::Error> {
use crate::site::economy::GoodIndex;
use std::io::Write;
write!(
*f,
"{}, {}, {}, {:.1}, {},,",
site.name(),
site.get_origin().x,
site.get_origin().y,
site.economy.pop,
site.economy.neighbors.len(),
)?;
for g in good_list() {
if let Some(value) = site.economy.values[g] {
write!(*f, "{:.2},", value)?;
} else {
f.write_all(b",")?;
}
}
f.write_all(b",")?;
for g in good_list() {
if let Some(labor_value) = site.economy.labor_values[g] {
write!(f, "{:.2},", labor_value)?;
} else {
f.write_all(b",")?;
}
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:.1},", site.economy.stocks[g])?;
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:.1},", site.economy.marginal_surplus[g])?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:.1},", site.economy.labors[l] * site.economy.pop)?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:.2},", site.economy.productivity[l])?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:.1},", site.economy.yields[l])?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
let limit = site.economy.limited_by[l];
if limit == GoodIndex::default() {
f.write_all(b",")?;
} else {
write!(f, "{:?},", limit)?;
}
}
f.write_all(b",")?;
for g in good_list() {
if site.economy.last_exports[g] >= 0.1 || site.economy.last_exports[g] <= -0.1 {
write!(f, "{:.1},", site.economy.last_exports[g])?;
} else {
f.write_all(b",")?;
}
}
writeln!(f)
}
fn csv_header(f: &mut std::fs::File) -> Result<(), std::io::Error> {
use std::io::Write;
write!(f, "Site,PosX,PosY,Population,Neighbors,,")?;
for g in good_list() {
write!(f, "{:?} Value,", g)?;
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:?} LaborVal,", g)?;
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:?} Stock,", g)?;
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:?} Surplus,", g)?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:?} Labor,", l)?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:?} Productivity,", l)?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:?} Yields,", l)?;
}
f.write_all(b",")?;
for l in LaborIndex::list() {
write!(f, "{:?} limit,", l)?;
}
f.write_all(b",")?;
for g in good_list() {
write!(f, "{:?} trade,", g)?;
}
writeln!(f)?;
Ok(())
let mut f = if GENERATE_CSV {
let mut f = std::fs::File::create("economy.csv")?;
Some(f)
} else {
None
}
}
fn print_sorted(prefix: &str, mut list: Vec<(String, f32)>, threshold: f32, decimals: usize) {
print!("{}", prefix);
list.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Less));
for i in list.iter() {
if i.1 >= threshold {
print!("{}={:.*} ", i.0, decimals, i.1);
}
}
println!();
}
fn show_economy(sites: &common::store::Store<crate::site::Site>) {
print!(" Resources: ");
for i in good_list() {
let amount = site.economy.natural_resources.chunks_per_resource[i];
if amount > 0.0 {
print!("{:?}={} ", i, amount);
}
}
println!();
println!(
" Population {:.1}, limited by {:?}",
site.economy.pop, site.economy.population_limited_by
);
let idle: f32 =
site.economy.pop * (1.0 - site.economy.labors.iter().map(|(_, a)| *a).sum::<f32>());
print_sorted(
&format!(" Professions: idle={:.1} ", idle),
site.economy
.labors
.iter()
.map(|(l, a)| (format!("{:?}", l), *a * site.economy.pop))
.collect(),
site.economy.pop * 0.05,
1,
);
print_sorted(
" Stock: ",
site.economy
.stocks
.iter()
.map(|(l, a)| (format!("{:?}", l), *a))
.collect(),
1.0,
0,
);
print_sorted(
" Values: ",
site.economy
.values
.iter()
.map(|(l, a)| {
(
format!("{:?}", l),
a.map(|v| if v > 3.9 { 0.0 } else { v }).unwrap_or(0.0),
)
})
.collect(),
0.1,
1,
);
print_sorted(
" Labor Values: ",
site.economy
.labor_values
.iter()
.map(|(l, a)| (format!("{:?}", l), a.unwrap_or(0.0)))
.collect(),
0.1,
1,
);
print!(" Limited: ");
for (limit, prod) in site
.economy
.limited_by
.iter()
.zip(site.economy.productivity.iter())
{
if (0.01..=0.99).contains(prod.1) {
print!("{:?}:{:?}={} ", limit.0, limit.1, *prod.1);
}
}
println!();
print!(" Trade({}): ", site.economy.neighbors.len());
for (g, &amt) in site.economy.active_exports.iter() {
if !(-0.1..=0.1).contains(&amt) {
print!("{:?}={:.2} ", g, amt);
}
}
println!();
// check population (shrinks if economy gets broken)
// assert!(site.economy.pop >= env.targets[&id]);
}
}
pub fn good_list() -> impl Iterator<Item = GoodIndex> {