mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'christof/shredded_economy3' into 'master'
Economic debugging and documentation See merge request veloren/veloren!3309
This commit is contained in:
commit
e30845c68f
@ -57,35 +57,68 @@ impl EconStatistics {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn csv_entry(f: &mut std::fs::File, site: &Site) -> Result<(), std::io::Error> {
|
pub fn csv_entry(f: &mut std::fs::File, site: &Site) -> Result<(), std::io::Error> {
|
||||||
|
use crate::site::economy::GoodIndex;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
write!(
|
write!(
|
||||||
*f,
|
*f,
|
||||||
"{}, {}, {}, {},",
|
"{}, {}, {}, {:.1}, {},,",
|
||||||
site.name(),
|
site.name(),
|
||||||
site.get_origin().x,
|
site.get_origin().x,
|
||||||
site.get_origin().y,
|
site.get_origin().y,
|
||||||
site.economy.pop
|
site.economy.pop,
|
||||||
|
site.economy.neighbors.len(),
|
||||||
)?;
|
)?;
|
||||||
for g in good_list() {
|
for g in good_list() {
|
||||||
write!(*f, "{:?},", site.economy.values[g].unwrap_or(-1.0))?;
|
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() {
|
for g in good_list() {
|
||||||
write!(f, "{:?},", site.economy.labor_values[g].unwrap_or(-1.0))?;
|
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() {
|
for g in good_list() {
|
||||||
write!(f, "{:?},", site.economy.stocks[g])?;
|
write!(f, "{:.1},", site.economy.stocks[g])?;
|
||||||
}
|
}
|
||||||
|
f.write_all(b",")?;
|
||||||
for g in good_list() {
|
for g in good_list() {
|
||||||
write!(f, "{:?},", site.economy.marginal_surplus[g])?;
|
write!(f, "{:.1},", site.economy.marginal_surplus[g])?;
|
||||||
}
|
}
|
||||||
|
f.write_all(b",")?;
|
||||||
for l in LaborIndex::list() {
|
for l in LaborIndex::list() {
|
||||||
write!(f, "{:?},", site.economy.labors[l] * site.economy.pop)?;
|
write!(f, "{:.1},", site.economy.labors[l] * site.economy.pop)?;
|
||||||
}
|
}
|
||||||
|
f.write_all(b",")?;
|
||||||
for l in LaborIndex::list() {
|
for l in LaborIndex::list() {
|
||||||
write!(f, "{:?},", site.economy.productivity[l])?;
|
write!(f, "{:.2},", site.economy.productivity[l])?;
|
||||||
}
|
}
|
||||||
|
f.write_all(b",")?;
|
||||||
for l in LaborIndex::list() {
|
for l in LaborIndex::list() {
|
||||||
write!(f, "{:?},", site.economy.yields[l])?;
|
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)
|
writeln!(f)
|
||||||
}
|
}
|
||||||
@ -96,28 +129,42 @@ fn simulate_return(index: &mut Index, world: &mut WorldSim) -> Result<(), std::i
|
|||||||
// here
|
// here
|
||||||
let mut f = if GENERATE_CSV {
|
let mut f = if GENERATE_CSV {
|
||||||
let mut f = std::fs::File::create("economy.csv")?;
|
let mut f = std::fs::File::create("economy.csv")?;
|
||||||
write!(f, "Site,PosX,PosY,Population,")?;
|
write!(f, "Site,PosX,PosY,Population,Neighbors,,")?;
|
||||||
for g in good_list() {
|
for g in good_list() {
|
||||||
write!(f, "{:?} Value,", g)?;
|
write!(f, "{:?} Value,", g)?;
|
||||||
}
|
}
|
||||||
|
f.write_all(b",")?;
|
||||||
for g in good_list() {
|
for g in good_list() {
|
||||||
write!(f, "{:?} LaborVal,", g)?;
|
write!(f, "{:?} LaborVal,", g)?;
|
||||||
}
|
}
|
||||||
|
f.write_all(b",")?;
|
||||||
for g in good_list() {
|
for g in good_list() {
|
||||||
write!(f, "{:?} Stock,", g)?;
|
write!(f, "{:?} Stock,", g)?;
|
||||||
}
|
}
|
||||||
|
f.write_all(b",")?;
|
||||||
for g in good_list() {
|
for g in good_list() {
|
||||||
write!(f, "{:?} Surplus,", g)?;
|
write!(f, "{:?} Surplus,", g)?;
|
||||||
}
|
}
|
||||||
|
f.write_all(b",")?;
|
||||||
for l in LaborIndex::list() {
|
for l in LaborIndex::list() {
|
||||||
write!(f, "{:?} Labor,", l)?;
|
write!(f, "{:?} Labor,", l)?;
|
||||||
}
|
}
|
||||||
|
f.write_all(b",")?;
|
||||||
for l in LaborIndex::list() {
|
for l in LaborIndex::list() {
|
||||||
write!(f, "{:?} Productivity,", l)?;
|
write!(f, "{:?} Productivity,", l)?;
|
||||||
}
|
}
|
||||||
|
f.write_all(b",")?;
|
||||||
for l in LaborIndex::list() {
|
for l in LaborIndex::list() {
|
||||||
write!(f, "{:?} Yields,", l)?;
|
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)?;
|
writeln!(f)?;
|
||||||
Some(f)
|
Some(f)
|
||||||
} else {
|
} else {
|
||||||
@ -282,24 +329,25 @@ pub fn tick(index: &mut Index, _world: &mut WorldSim, dt: f32, _vc: vergleich::C
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{sim, site::economy::GoodMap, util::seed_expan};
|
use crate::{sim, site::economy::GoodMap, util::seed_expan};
|
||||||
use common::trade::Good;
|
use common::{store::Id, terrain::BiomeKind, trade::Good};
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
use rand_chacha::ChaChaRng;
|
use rand_chacha::ChaChaRng;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::convert::TryInto;
|
use std::convert::TryInto;
|
||||||
use tracing::{info, Level};
|
use tracing::{info, Dispatch, Level};
|
||||||
use tracing_subscriber::{
|
use tracing_subscriber::{filter::EnvFilter, FmtSubscriber};
|
||||||
filter::{EnvFilter, LevelFilter},
|
|
||||||
FmtSubscriber,
|
|
||||||
};
|
|
||||||
use vek::Vec2;
|
use vek::Vec2;
|
||||||
|
|
||||||
// enable info!
|
fn execute_with_tracing(level: Level, func: fn()) {
|
||||||
fn init() {
|
tracing::dispatcher::with_default(
|
||||||
FmtSubscriber::builder()
|
&Dispatch::new(
|
||||||
.with_max_level(Level::ERROR)
|
FmtSubscriber::builder()
|
||||||
.with_env_filter(EnvFilter::from_default_env().add_directive(LevelFilter::INFO.into()))
|
.with_max_level(level)
|
||||||
.init();
|
.with_env_filter(EnvFilter::from_default_env())
|
||||||
|
.finish(),
|
||||||
|
),
|
||||||
|
func,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@ -317,131 +365,375 @@ mod tests {
|
|||||||
resources: Vec<ResourcesSetup>,
|
resources: Vec<ResourcesSetup>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
fn print_sorted(prefix: &str, mut list: Vec<(String, f32)>, threshold: f32, decimals: usize) {
|
||||||
fn test_economy() {
|
print!("{}", prefix);
|
||||||
init();
|
list.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Less));
|
||||||
let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
|
for i in list.iter() {
|
||||||
info!("init");
|
if i.1 >= threshold {
|
||||||
let seed = 59686;
|
print!("{}={:.*} ", i.0, decimals, i.1);
|
||||||
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);
|
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]);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -88,6 +88,7 @@ lazy_static! {
|
|||||||
pub struct Economy {
|
pub struct Economy {
|
||||||
/// Population
|
/// Population
|
||||||
pub pop: f32,
|
pub pop: f32,
|
||||||
|
pub population_limited_by: GoodIndex,
|
||||||
|
|
||||||
/// Total available amount of each good
|
/// Total available amount of each good
|
||||||
pub stocks: GoodMap<f32>,
|
pub stocks: GoodMap<f32>,
|
||||||
@ -97,22 +98,30 @@ pub struct Economy {
|
|||||||
pub marginal_surplus: GoodMap<f32>,
|
pub marginal_surplus: GoodMap<f32>,
|
||||||
/// amount of wares not needed by the economy (helps with trade planning)
|
/// amount of wares not needed by the economy (helps with trade planning)
|
||||||
pub unconsumed_stock: GoodMap<f32>,
|
pub unconsumed_stock: GoodMap<f32>,
|
||||||
|
/// Local availability of a good, 4.0 = starved, 2.0 = balanced, 0.1 =
|
||||||
|
/// extra, NULL = way too much
|
||||||
// For some goods, such a goods without any supply, it doesn't make sense to talk about value
|
// For some goods, such a goods without any supply, it doesn't make sense to talk about value
|
||||||
pub values: GoodMap<Option<f32>>,
|
pub values: GoodMap<Option<f32>>,
|
||||||
|
/// amount of goods exported/imported during the last cycle
|
||||||
pub last_exports: GoodMap<f32>,
|
pub last_exports: GoodMap<f32>,
|
||||||
pub active_exports: GoodMap<f32>, // unfinished trade (amount unconfirmed)
|
pub active_exports: GoodMap<f32>, // unfinished trade (amount unconfirmed)
|
||||||
//pub export_targets: GoodMap<f32>,
|
//pub export_targets: GoodMap<f32>,
|
||||||
|
/// amount of labor that went into a good, [1 man cycle=1.0]
|
||||||
pub labor_values: GoodMap<Option<f32>>,
|
pub labor_values: GoodMap<Option<f32>>,
|
||||||
|
// this assumes a single source, replace with LaborMap?
|
||||||
pub material_costs: GoodMap<f32>,
|
pub material_costs: GoodMap<f32>,
|
||||||
|
|
||||||
// Proportion of individuals dedicated to an industry
|
/// Proportion of individuals dedicated to an industry (sums to roughly 1.0)
|
||||||
pub labors: LaborMap<f32>,
|
pub labors: LaborMap<f32>,
|
||||||
// Per worker, per year, of their output good
|
// Per worker, per year, of their output good
|
||||||
pub yields: LaborMap<f32>,
|
pub yields: LaborMap<f32>,
|
||||||
|
/// [0.0..1.0]
|
||||||
pub productivity: LaborMap<f32>,
|
pub productivity: LaborMap<f32>,
|
||||||
|
/// Missing raw material which limits production
|
||||||
|
pub limited_by: LaborMap<GoodIndex>,
|
||||||
|
|
||||||
pub natural_resources: NaturalResources,
|
pub natural_resources: NaturalResources,
|
||||||
// usize is distance
|
/// Neighboring sites to trade with
|
||||||
pub neighbors: Vec<NeighborInformation>,
|
pub neighbors: Vec<NeighborInformation>,
|
||||||
|
|
||||||
/// outgoing trade, per provider
|
/// outgoing trade, per provider
|
||||||
@ -126,6 +135,7 @@ impl Default for Economy {
|
|||||||
let coin_index: GoodIndex = GoodIndex::try_from(Coin).unwrap_or_default();
|
let coin_index: GoodIndex = GoodIndex::try_from(Coin).unwrap_or_default();
|
||||||
Self {
|
Self {
|
||||||
pop: 32.0,
|
pop: 32.0,
|
||||||
|
population_limited_by: GoodIndex::default(),
|
||||||
|
|
||||||
stocks: GoodMap::from_list(&[(coin_index, Economy::STARTING_COIN)], 100.0),
|
stocks: GoodMap::from_list(&[(coin_index, Economy::STARTING_COIN)], 100.0),
|
||||||
surplus: Default::default(),
|
surplus: Default::default(),
|
||||||
@ -140,6 +150,7 @@ impl Default for Economy {
|
|||||||
labors: LaborMap::from_default(0.01),
|
labors: LaborMap::from_default(0.01),
|
||||||
yields: LaborMap::from_default(1.0),
|
yields: LaborMap::from_default(1.0),
|
||||||
productivity: LaborMap::from_default(1.0),
|
productivity: LaborMap::from_default(1.0),
|
||||||
|
limited_by: LaborMap::from_default(GoodIndex::default()),
|
||||||
|
|
||||||
natural_resources: Default::default(),
|
natural_resources: Default::default(),
|
||||||
neighbors: Default::default(),
|
neighbors: Default::default(),
|
||||||
@ -928,7 +939,7 @@ impl Economy {
|
|||||||
// available then we only need to consume 2/3rds
|
// available then we only need to consume 2/3rds
|
||||||
// of other ingredients and leave the rest in stock
|
// of other ingredients and leave the rest in stock
|
||||||
// In effect, this is the productivity
|
// In effect, this is the productivity
|
||||||
let labor_productivity = orders
|
let (labor_productivity, limited_by) = orders
|
||||||
.iter()
|
.iter()
|
||||||
.map(|(good, amount)| {
|
.map(|(good, amount)| {
|
||||||
// What quantity is this order requesting?
|
// What quantity is this order requesting?
|
||||||
@ -936,13 +947,20 @@ impl Economy {
|
|||||||
assert!(stocks_before[*good] >= 0.0);
|
assert!(stocks_before[*good] >= 0.0);
|
||||||
assert!(demand[*good] >= 0.0);
|
assert!(demand[*good] >= 0.0);
|
||||||
// What proportion of this order is the economy able to satisfy?
|
// What proportion of this order is the economy able to satisfy?
|
||||||
(stocks_before[*good] / demand[*good]).min(1.0)
|
((stocks_before[*good] / demand[*good]).min(1.0), *good)
|
||||||
})
|
})
|
||||||
.min_by(|a, b| a.partial_cmp(b).unwrap_or(Less))
|
.min_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Less))
|
||||||
.unwrap_or_else(|| {
|
.unwrap_or_else(|| {
|
||||||
panic!("Industry {:?} requires at least one input order", labor)
|
panic!("Industry {:?} requires at least one input order", labor)
|
||||||
});
|
});
|
||||||
assert!(labor_productivity >= 0.0);
|
assert!(labor_productivity >= 0.0);
|
||||||
|
if let Some(labor) = labor {
|
||||||
|
self.limited_by[*labor] = if labor_productivity >= 1.0 {
|
||||||
|
GoodIndex::default()
|
||||||
|
} else {
|
||||||
|
limited_by
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let mut total_materials_cost = 0.0;
|
let mut total_materials_cost = 0.0;
|
||||||
for (good, amount) in orders {
|
for (good, amount) in orders {
|
||||||
@ -1069,7 +1087,8 @@ impl Economy {
|
|||||||
// Births/deaths
|
// Births/deaths
|
||||||
const NATURAL_BIRTH_RATE: f32 = 0.05;
|
const NATURAL_BIRTH_RATE: f32 = 0.05;
|
||||||
const DEATH_RATE: f32 = 0.005;
|
const DEATH_RATE: f32 = 0.005;
|
||||||
let birth_rate = if self.surplus[*FOOD_INDEX] > 0.0 {
|
let population_growth = self.surplus[*FOOD_INDEX] > 0.0;
|
||||||
|
let birth_rate = if population_growth {
|
||||||
NATURAL_BIRTH_RATE
|
NATURAL_BIRTH_RATE
|
||||||
} else {
|
} else {
|
||||||
0.0
|
0.0
|
||||||
@ -1078,6 +1097,11 @@ impl Economy {
|
|||||||
"pop",
|
"pop",
|
||||||
dt / DAYS_PER_YEAR * self.pop * (birth_rate - DEATH_RATE),
|
dt / DAYS_PER_YEAR * self.pop * (birth_rate - DEATH_RATE),
|
||||||
);
|
);
|
||||||
|
self.population_limited_by = if population_growth {
|
||||||
|
GoodIndex::default()
|
||||||
|
} else {
|
||||||
|
*FOOD_INDEX
|
||||||
|
};
|
||||||
|
|
||||||
// calculate the new unclaimed stock
|
// calculate the new unclaimed stock
|
||||||
//let next_orders = self.get_orders();
|
//let next_orders = self.get_orders();
|
||||||
|
Loading…
Reference in New Issue
Block a user