mirror of
https://gitlab.com/veloren/veloren.git
synced 2024-08-30 18:12:32 +00:00
Merge branch 'christof/shredded_economy4b' into 'master'
Simplify economy code by separating professions and everyone See merge request veloren/veloren!3420
This commit is contained in:
commit
972cb26b2a
@ -1,5 +1,4 @@
|
||||
use crate::{client::Client, Server};
|
||||
use common::trade::Good;
|
||||
use common_net::msg::{world_msg::EconomyInfo, ServerGeneral};
|
||||
use specs::{Entity as EcsEntity, WorldExt};
|
||||
use std::collections::HashMap;
|
||||
@ -30,47 +29,7 @@ pub fn handle_site_info(server: &Server, entity: EcsEntity, id: u64) {
|
||||
let site_id = server.index.sites.recreate_id(id);
|
||||
let info = if let Some(site_id) = site_id {
|
||||
let site = server.index.sites.get(site_id);
|
||||
EconomyInfo {
|
||||
id,
|
||||
population: site.economy.pop.floor() as u32,
|
||||
stock: site
|
||||
.economy
|
||||
.stocks
|
||||
.iter()
|
||||
.map(|(g, a)| (Good::from(g), *a))
|
||||
.collect(),
|
||||
labor_values: site
|
||||
.economy
|
||||
.labor_values
|
||||
.iter()
|
||||
.filter_map(|(g, a)| a.map(|a| (Good::from(g), a)))
|
||||
.collect(),
|
||||
values: site
|
||||
.economy
|
||||
.values
|
||||
.iter()
|
||||
.filter_map(|(g, a)| a.map(|a| (Good::from(g), a)))
|
||||
.collect(),
|
||||
labors: site.economy.labors.iter().map(|(_, a)| (*a)).collect(),
|
||||
last_exports: site
|
||||
.economy
|
||||
.last_exports
|
||||
.iter()
|
||||
.map(|(g, a)| (Good::from(g), *a))
|
||||
.collect(),
|
||||
resources: site
|
||||
.economy
|
||||
.natural_resources
|
||||
.chunks_per_resource
|
||||
.iter()
|
||||
.map(|(g, a)| {
|
||||
(
|
||||
Good::from(g),
|
||||
((*a) as f32) * site.economy.natural_resources.average_yield_per_chunk[g],
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
site.economy.get_information(site_id)
|
||||
} else {
|
||||
EconomyInfo {
|
||||
id,
|
||||
|
@ -208,7 +208,7 @@ pub fn init(
|
||||
},
|
||||
SiteKind::Refactor(site2) => {
|
||||
// villagers
|
||||
for _ in 0..site.economy.pop.min(site2.plots().len() as f32) as usize {
|
||||
for _ in 0..site.economy.population().min(site2.plots().len() as f32) as usize {
|
||||
rtsim.entities.insert(Entity {
|
||||
is_loaded: false,
|
||||
pos: site2
|
||||
|
@ -1,220 +0,0 @@
|
||||
[
|
||||
(
|
||||
name: "Credge",
|
||||
position: (4176, 4080),
|
||||
kind: Settlement,
|
||||
neighbors: [
|
||||
(2, 112),
|
||||
(1, 146),
|
||||
(6, 98),
|
||||
(9, 145),
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
good: Terrain(Lake),
|
||||
amount: 4,
|
||||
),
|
||||
(
|
||||
good: Terrain(Mountain),
|
||||
amount: 52,
|
||||
),
|
||||
(
|
||||
good: Terrain(Grassland),
|
||||
amount: 7073,
|
||||
),
|
||||
(
|
||||
good: Terrain(Ocean),
|
||||
amount: 4929,
|
||||
),
|
||||
(
|
||||
good: Terrain(Forest),
|
||||
amount: 6360,
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
name: "Etodren",
|
||||
position: (1200, 2928),
|
||||
kind: Settlement,
|
||||
neighbors: [
|
||||
(2, 258),
|
||||
(0, 146),
|
||||
(6, 60),
|
||||
(9, 158),
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
good: Terrain(Mountain),
|
||||
amount: 288,
|
||||
),
|
||||
(
|
||||
good: Terrain(Grassland),
|
||||
amount: 4129,
|
||||
),
|
||||
(
|
||||
good: Terrain(Desert),
|
||||
amount: 230,
|
||||
),
|
||||
(
|
||||
good: Terrain(Ocean),
|
||||
amount: 2923,
|
||||
),
|
||||
(
|
||||
good: Terrain(Forest),
|
||||
amount: 3139,
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
name: "Twige",
|
||||
position: (2000, 7632),
|
||||
kind: Settlement,
|
||||
neighbors: [
|
||||
(0, 112),
|
||||
(1, 258),
|
||||
(6, 109),
|
||||
(9, 32),
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
good: Terrain(Lake),
|
||||
amount: 1,
|
||||
),
|
||||
(
|
||||
good: Terrain(Mountain),
|
||||
amount: 231,
|
||||
),
|
||||
(
|
||||
good: Terrain(Grassland),
|
||||
amount: 3308,
|
||||
),
|
||||
(
|
||||
good: Terrain(Desert),
|
||||
amount: 1695,
|
||||
),
|
||||
(
|
||||
good: Terrain(Ocean),
|
||||
amount: 487,
|
||||
),
|
||||
(
|
||||
good: Terrain(Forest),
|
||||
amount: 1338,
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
name: "Pleed Dungeon",
|
||||
position: (6922, 1034),
|
||||
kind: Dungeon,
|
||||
neighbors: [],
|
||||
resources: [],
|
||||
),
|
||||
(
|
||||
name: "Fred Lair",
|
||||
position: (3786, 2250),
|
||||
kind: Dungeon,
|
||||
neighbors: [],
|
||||
resources: [],
|
||||
),
|
||||
(
|
||||
name: "Frer Dungeon",
|
||||
position: (6602, 2250),
|
||||
kind: Dungeon,
|
||||
neighbors: [],
|
||||
resources: [],
|
||||
),
|
||||
(
|
||||
name: "Inige Castle",
|
||||
position: (1360, 4304),
|
||||
kind: Castle,
|
||||
neighbors: [
|
||||
(0, 98),
|
||||
(1, 60),
|
||||
(2, 109),
|
||||
(9, 99),
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
good: Terrain(Mountain),
|
||||
amount: 424,
|
||||
),
|
||||
(
|
||||
good: Terrain(Grassland),
|
||||
amount: 958,
|
||||
),
|
||||
(
|
||||
good: Terrain(Desert),
|
||||
amount: 1285,
|
||||
),
|
||||
(
|
||||
good: Terrain(Ocean),
|
||||
amount: 669,
|
||||
),
|
||||
(
|
||||
good: Terrain(Forest),
|
||||
amount: 1781,
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
name: "Estedock Catacombs",
|
||||
position: (4650, 330),
|
||||
kind: Dungeon,
|
||||
neighbors: [],
|
||||
resources: [],
|
||||
),
|
||||
(
|
||||
name: "Oreefey Lair",
|
||||
position: (1578, 3754),
|
||||
kind: Dungeon,
|
||||
neighbors: [],
|
||||
resources: [],
|
||||
),
|
||||
(
|
||||
name: "Lasnast Keep",
|
||||
position: (1136, 6832),
|
||||
kind: Castle,
|
||||
neighbors: [
|
||||
(0, 145),
|
||||
(2, 32),
|
||||
(1, 158),
|
||||
(6, 99),
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
good: Terrain(Mountain),
|
||||
amount: 352,
|
||||
),
|
||||
(
|
||||
good: Terrain(Grassland),
|
||||
amount: 887,
|
||||
),
|
||||
(
|
||||
good: Terrain(Desert),
|
||||
amount: 2606,
|
||||
),
|
||||
(
|
||||
good: Terrain(Ocean),
|
||||
amount: 286,
|
||||
),
|
||||
(
|
||||
good: Terrain(Forest),
|
||||
amount: 679,
|
||||
),
|
||||
],
|
||||
),
|
||||
(
|
||||
name: "Oren Lair",
|
||||
position: (6730, 2506),
|
||||
kind: Dungeon,
|
||||
neighbors: [],
|
||||
resources: [],
|
||||
),
|
||||
(
|
||||
name: "Ween Crib",
|
||||
position: (2250, 8010),
|
||||
kind: Dungeon,
|
||||
neighbors: [],
|
||||
resources: [],
|
||||
),
|
||||
]
|
@ -4,8 +4,8 @@
|
||||
position: (1, 1),
|
||||
kind: Settlement,
|
||||
neighbors: [
|
||||
(1, 10),
|
||||
(2, 10),
|
||||
1,
|
||||
2,
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
@ -19,7 +19,7 @@
|
||||
position: (10, 10),
|
||||
kind: Settlement,
|
||||
neighbors: [
|
||||
(0, 10),
|
||||
0,
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
@ -33,7 +33,7 @@
|
||||
position: (20, 10),
|
||||
kind: Settlement,
|
||||
neighbors: [
|
||||
(0, 10),
|
||||
0,
|
||||
],
|
||||
resources: [
|
||||
(
|
||||
|
@ -1,6 +1,6 @@
|
||||
use common::trade::Good;
|
||||
use std::io::Write;
|
||||
use veloren_world::site::economy::{self, good_list, Economy};
|
||||
use veloren_world::site::economy::{GraphInfo, Labor};
|
||||
//use regex::Regex::replace_all;
|
||||
|
||||
fn good_name(g: Good) -> String {
|
||||
@ -9,20 +9,18 @@ fn good_name(g: Good) -> String {
|
||||
res.replace(')', "_")
|
||||
}
|
||||
|
||||
fn labor_name(l: economy::Labor) -> String {
|
||||
fn labor_name(l: Labor) -> String {
|
||||
let res = format!("{:?}", l);
|
||||
res.replace(' ', "_")
|
||||
}
|
||||
|
||||
fn main() -> Result<(), std::io::Error> {
|
||||
let eco = Economy::default();
|
||||
let o = eco.get_orders();
|
||||
let p = eco.get_productivity();
|
||||
let eco = GraphInfo::default();
|
||||
|
||||
let mut f = std::fs::File::create("economy.gv")?;
|
||||
writeln!(f, "digraph economy {{")?;
|
||||
for i in good_list() {
|
||||
let color = if economy::direct_use_goods().contains(&i) {
|
||||
for i in eco.good_list() {
|
||||
let color = if !eco.can_store(&i) {
|
||||
"green"
|
||||
} else {
|
||||
"orange"
|
||||
@ -33,44 +31,42 @@ fn main() -> Result<(), std::io::Error> {
|
||||
writeln!(f)?;
|
||||
writeln!(f, "// Professions")?;
|
||||
writeln!(f, "Everyone [shape=doubleoctagon];")?;
|
||||
for i in economy::Labor::list() {
|
||||
for i in eco.labor_list() {
|
||||
writeln!(f, "{:?} [shape=box];", labor_name(i))?;
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
writeln!(f, "// Orders")?;
|
||||
let o = eco.get_orders();
|
||||
for i in o.iter() {
|
||||
for j in i.1.iter() {
|
||||
if i.0.is_some() {
|
||||
let style = if matches!(j.0.into(), Good::Tools)
|
||||
|| matches!(j.0.into(), Good::Armor)
|
||||
|| matches!(j.0.into(), Good::Potions)
|
||||
{
|
||||
", style=dashed, color=orange"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
writeln!(
|
||||
f,
|
||||
"{:?} -> {:?} [label=\"{:.1}\"{}];",
|
||||
good_name(j.0.into()),
|
||||
labor_name(i.0.unwrap()),
|
||||
j.1,
|
||||
style
|
||||
)?;
|
||||
let style = if matches!(j.0.into(), Good::Tools | Good::Armor | Good::Potions) {
|
||||
", style=dashed, color=orange"
|
||||
} else {
|
||||
writeln!(
|
||||
f,
|
||||
"{:?} -> Everyone [label=\"{:.1}\"];",
|
||||
good_name(j.0.into()),
|
||||
j.1
|
||||
)?;
|
||||
}
|
||||
""
|
||||
};
|
||||
writeln!(
|
||||
f,
|
||||
"{:?} -> {:?} [label=\"{:.1}\"{}];",
|
||||
good_name(j.0.into()),
|
||||
labor_name(i.0),
|
||||
j.1,
|
||||
style
|
||||
)?;
|
||||
}
|
||||
}
|
||||
for j in eco.get_orders_everyone() {
|
||||
writeln!(
|
||||
f,
|
||||
"{:?} -> Everyone [label=\"{:.1}\"];",
|
||||
good_name(j.0.into()),
|
||||
j.1
|
||||
)?;
|
||||
}
|
||||
|
||||
writeln!(f)?;
|
||||
writeln!(f, "// Products")?;
|
||||
let p = eco.get_production();
|
||||
for i in p.iter() {
|
||||
writeln!(
|
||||
f,
|
||||
|
@ -1,739 +1,3 @@
|
||||
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};
|
||||
use crate::{sim::WorldSim, site::economy::simulate_economy, Index};
|
||||
|
||||
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]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
pub fn simulate(index: &mut Index, _world: &mut WorldSim) { simulate_economy(index); }
|
||||
|
571
world/src/site/economy/context.rs
Normal file
571
world/src/site/economy/context.rs
Normal file
@ -0,0 +1,571 @@
|
||||
/// this contains global housekeeping info during simulation
|
||||
use crate::{
|
||||
site::{
|
||||
economy::{self, Economy, DAYS_PER_MONTH, DAYS_PER_YEAR, INTER_SITE_TRADE},
|
||||
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 {
|
||||
count: u32,
|
||||
sum: f32,
|
||||
min: f32,
|
||||
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 struct Environment {
|
||||
csv_file: Option<std::fs::File>,
|
||||
// context: vergleich::ProgramRun,
|
||||
}
|
||||
|
||||
impl Environment {
|
||||
pub fn new() -> Result<Self, std::io::Error> {
|
||||
// let mut context = vergleich::ProgramRun::new("economy_compare.sqlite")
|
||||
// .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other,
|
||||
// e.to_string()))?; context.set_epsilon(0.1);
|
||||
let csv_file = Economy::csv_open();
|
||||
Ok(Self {
|
||||
csv_file, /* context */
|
||||
})
|
||||
}
|
||||
|
||||
fn iteration(&mut self, _: i32) {}
|
||||
|
||||
fn end(mut self, index: &Index) {
|
||||
if let Some(f) = self.csv_file.as_mut() {
|
||||
use std::io::Write;
|
||||
let err = writeln!(f);
|
||||
if err.is_ok() {
|
||||
for site in index.sites.ids() {
|
||||
let site = index.sites.get(site);
|
||||
if Economy::csv_entry(f, site).is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.csv_file.take();
|
||||
}
|
||||
|
||||
{
|
||||
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::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)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn csv_tick(&mut self, index: &Index) {
|
||||
if let Some(f) = self.csv_file.as_mut() {
|
||||
if let Some(site) = index
|
||||
.sites
|
||||
.values()
|
||||
.find(|s| !matches!(s.kind, SiteKind::Dungeon(_)))
|
||||
{
|
||||
Economy::csv_entry(f, site).unwrap_or_else(|_| {
|
||||
self.csv_file.take();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn simulate_return(index: &mut Index) -> Result<(), std::io::Error> {
|
||||
let mut env = economy::Environment::new()?;
|
||||
|
||||
tracing::info!("economy simulation start");
|
||||
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);
|
||||
}
|
||||
env.iteration(i);
|
||||
tick(index, TICK_PERIOD, &mut env);
|
||||
if i % 5 == 0 {
|
||||
env.csv_tick(index);
|
||||
}
|
||||
}
|
||||
tracing::info!("economy simulation end");
|
||||
env.end(index);
|
||||
// csv_footer(f, index);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn simulate_economy(index: &mut Index) {
|
||||
simulate_return(index)
|
||||
.unwrap_or_else(|err| info!("I/O error in simulate (economy.csv not writable?): {}", err));
|
||||
}
|
||||
|
||||
// fn check_money(index: &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
|
||||
// );
|
||||
// }
|
||||
|
||||
fn tick(index: &mut Index, dt: f32, _env: &mut Environment) {
|
||||
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);
|
||||
// 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, util::seed_expan};
|
||||
use common::{store::Id, terrain::BiomeKind, trade::Good};
|
||||
use hashbrown::HashMap;
|
||||
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>, // id
|
||||
resources: Vec<ResourcesSetup>,
|
||||
}
|
||||
|
||||
fn show_economy(
|
||||
sites: &common::store::Store<crate::site::Site>,
|
||||
names: &Option<HashMap<Id<crate::site::Site>, String>>,
|
||||
) {
|
||||
for (id, site) in sites.iter() {
|
||||
let name = names.as_ref().map_or(site.name().into(), |map| {
|
||||
map.get(&id).cloned().unwrap_or_else(|| site.name().into())
|
||||
});
|
||||
println!("Site id {:?} name {}", id.id(), name);
|
||||
site.economy.print_details();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, &None);
|
||||
});
|
||||
}
|
||||
|
||||
/// 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 mut names = None;
|
||||
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()).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");
|
||||
names = Some(HashMap::new());
|
||||
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;
|
||||
}
|
||||
let id = index.sites.insert(settlement);
|
||||
names.as_mut().map(|map| map.insert(id, i.name.clone()));
|
||||
}
|
||||
// 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 (id, econ) in econ_testinput.iter().enumerate() {
|
||||
if let Some(id) = index.sites.recreate_id(id as u64) {
|
||||
for nid in econ.neighbors.iter() {
|
||||
if let Some(nid) = index.sites.recreate_id(*nid) {
|
||||
let town = &mut index.sites.get_mut(id).economy;
|
||||
town.add_neighbor(nid, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
crate::sim2::simulate(&mut index, &mut sim);
|
||||
show_economy(&index.sites, &names);
|
||||
});
|
||||
}
|
||||
|
||||
struct Simenv {
|
||||
index: crate::index::Index,
|
||||
rng: ChaChaRng,
|
||||
targets: HashMap<Id<crate::site::Site>, f32>,
|
||||
names: HashMap<Id<crate::site::Site>, String>,
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// test whether a site in moderate climate can survive on its own
|
||||
fn test_economy_moderate_standalone() {
|
||||
fn add_settlement(
|
||||
env: &mut Simenv,
|
||||
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,
|
||||
));
|
||||
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);
|
||||
env.names.insert(id, name.into());
|
||||
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: HashMap::new(),
|
||||
names: HashMap::new(),
|
||||
};
|
||||
add_settlement(&mut env, "Forest", 5000.0, &[(
|
||||
Good::Terrain(BiomeKind::Forest),
|
||||
100.0_f32,
|
||||
)]);
|
||||
add_settlement(&mut env, "Grass", 880.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, &Some(env.names));
|
||||
// check population (shrinks if economy gets broken)
|
||||
for (id, site) in env.index.sites.iter() {
|
||||
assert!(site.economy.pop >= env.targets[&id]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
@ -194,7 +194,7 @@ pub struct LaborMap<V> {
|
||||
data: Vec<V>,
|
||||
}
|
||||
|
||||
impl<V: Default + Copy> Default for LaborMap<V> {
|
||||
impl<V: Default + Clone> Default for LaborMap<V> {
|
||||
fn default() -> Self {
|
||||
LaborMap {
|
||||
data: std::iter::repeat(V::default()).take(*LABOR_COUNT).collect(),
|
||||
@ -233,6 +233,13 @@ impl<V> LaborMap<V> {
|
||||
.enumerate()
|
||||
.map(|(idx, v)| (LaborIndex::from_usize(idx), v))
|
||||
}
|
||||
|
||||
pub fn iter_mut(&mut self) -> impl Iterator<Item = (LaborIndex, &mut V)> + '_ {
|
||||
(&mut self.data)
|
||||
.iter_mut()
|
||||
.enumerate()
|
||||
.map(|(idx, v)| (LaborIndex::from_usize(idx), v))
|
||||
}
|
||||
}
|
||||
|
||||
impl<V: Copy + Default> LaborMap<V> {
|
||||
@ -347,10 +354,16 @@ impl Labor {
|
||||
|
||||
pub fn is_everyone(&self) -> bool { self.0 == DUMMY_LABOR.0 }
|
||||
|
||||
pub fn orders(&self) -> Vec<(GoodIndex, f32)> {
|
||||
pub fn orders_everyone() -> impl Iterator<Item = &'static (GoodIndex, f32)> {
|
||||
LABOR
|
||||
.get(DUMMY_LABOR.0 as usize)
|
||||
.map_or([].iter(), |l| l.orders.iter())
|
||||
}
|
||||
|
||||
pub fn orders(&self) -> impl Iterator<Item = &'static (GoodIndex, f32)> {
|
||||
LABOR
|
||||
.get(self.0 as usize)
|
||||
.map_or(Vec::new(), |l| l.orders.clone())
|
||||
.map_or([].iter(), |l| l.orders.iter())
|
||||
}
|
||||
|
||||
pub fn products(&self) -> (GoodIndex, f32) {
|
||||
|
@ -1,3 +1,6 @@
|
||||
/// This file contains a single economy
|
||||
/// and functions to simulate it
|
||||
use crate::world_msg::EconomyInfo;
|
||||
use crate::{
|
||||
sim::SimChunk,
|
||||
site::Site,
|
||||
@ -8,78 +11,56 @@ use common::{
|
||||
terrain::BiomeKind,
|
||||
trade::{Good, SitePrices},
|
||||
};
|
||||
use hashbrown::HashMap;
|
||||
use lazy_static::lazy_static;
|
||||
use std::{cmp::Ordering::Less, convert::TryFrom};
|
||||
use tracing::{debug, info, trace, warn};
|
||||
|
||||
use Good::*;
|
||||
mod map_types;
|
||||
pub use map_types::{GoodIndex, GoodMap, Labor, LaborIndex, LaborMap, NaturalResources};
|
||||
pub use map_types::Labor;
|
||||
use map_types::{GoodIndex, GoodMap, LaborIndex, LaborMap, NaturalResources};
|
||||
mod context;
|
||||
pub use context::simulate_economy;
|
||||
use context::Environment;
|
||||
|
||||
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 INTER_SITE_TRADE: bool = true;
|
||||
const DAYS_PER_MONTH: f32 = 30.0;
|
||||
const DAYS_PER_YEAR: f32 = 12.0 * DAYS_PER_MONTH;
|
||||
const GENERATE_CSV: bool = false;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TradeOrder {
|
||||
pub customer: Id<Site>,
|
||||
pub amount: GoodMap<f32>, // positive for orders, negative for exchange
|
||||
customer: Id<Site>,
|
||||
amount: GoodMap<f32>, // positive for orders, negative for exchange
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TradeDelivery {
|
||||
pub supplier: Id<Site>,
|
||||
pub amount: GoodMap<f32>, // positive for orders, negative for exchange
|
||||
pub prices: GoodMap<f32>, // at the time of interaction
|
||||
pub supply: GoodMap<f32>, // maximum amount available, at the time of interaction
|
||||
supplier: Id<Site>,
|
||||
amount: GoodMap<f32>, // positive for orders, negative for exchange
|
||||
prices: GoodMap<f32>, // at the time of interaction
|
||||
supply: GoodMap<f32>, // maximum amount available, at the time of interaction
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TradeInformation {
|
||||
pub orders: DHashMap<Id<Site>, Vec<TradeOrder>>, // per provider
|
||||
pub deliveries: DHashMap<Id<Site>, Vec<TradeDelivery>>, // per receiver
|
||||
orders: DHashMap<Id<Site>, Vec<TradeOrder>>, // per provider
|
||||
deliveries: DHashMap<Id<Site>, Vec<TradeDelivery>>, // per receiver
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct NeighborInformation {
|
||||
pub id: Id<Site>,
|
||||
pub travel_distance: usize,
|
||||
id: Id<Site>,
|
||||
//travel_distance: usize,
|
||||
|
||||
// remembered from last interaction
|
||||
pub last_values: GoodMap<f32>,
|
||||
pub last_supplies: GoodMap<f32>,
|
||||
last_values: GoodMap<f32>,
|
||||
last_supplies: GoodMap<f32>,
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
pub static ref COIN_INDEX: GoodIndex = Coin.try_into().unwrap_or_default();
|
||||
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();
|
||||
}
|
||||
@ -87,47 +68,47 @@ lazy_static! {
|
||||
#[derive(Debug)]
|
||||
pub struct Economy {
|
||||
/// Population
|
||||
pub pop: f32,
|
||||
pub population_limited_by: GoodIndex,
|
||||
pop: f32,
|
||||
population_limited_by: GoodIndex,
|
||||
|
||||
/// Total available amount of each good
|
||||
pub stocks: GoodMap<f32>,
|
||||
stocks: GoodMap<f32>,
|
||||
/// Surplus stock compared to demand orders
|
||||
pub surplus: GoodMap<f32>,
|
||||
surplus: GoodMap<f32>,
|
||||
/// change rate (derivative) of stock in the current situation
|
||||
pub marginal_surplus: GoodMap<f32>,
|
||||
marginal_surplus: GoodMap<f32>,
|
||||
/// amount of wares not needed by the economy (helps with trade planning)
|
||||
pub unconsumed_stock: GoodMap<f32>,
|
||||
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
|
||||
pub values: GoodMap<Option<f32>>,
|
||||
values: GoodMap<Option<f32>>,
|
||||
/// amount of goods exported/imported during the last cycle
|
||||
pub last_exports: GoodMap<f32>,
|
||||
pub active_exports: GoodMap<f32>, // unfinished trade (amount unconfirmed)
|
||||
last_exports: GoodMap<f32>,
|
||||
active_exports: GoodMap<f32>, // unfinished trade (amount unconfirmed)
|
||||
//pub export_targets: GoodMap<f32>,
|
||||
/// amount of labor that went into a good, [1 man cycle=1.0]
|
||||
pub labor_values: GoodMap<Option<f32>>,
|
||||
labor_values: GoodMap<Option<f32>>,
|
||||
// this assumes a single source, replace with LaborMap?
|
||||
pub material_costs: GoodMap<f32>,
|
||||
material_costs: GoodMap<f32>,
|
||||
|
||||
/// Proportion of individuals dedicated to an industry (sums to roughly 1.0)
|
||||
pub labors: LaborMap<f32>,
|
||||
labors: LaborMap<f32>,
|
||||
// Per worker, per year, of their output good
|
||||
pub yields: LaborMap<f32>,
|
||||
yields: LaborMap<f32>,
|
||||
/// [0.0..1.0]
|
||||
pub productivity: LaborMap<f32>,
|
||||
productivity: LaborMap<f32>,
|
||||
/// Missing raw material which limits production
|
||||
pub limited_by: LaborMap<GoodIndex>,
|
||||
limited_by: LaborMap<GoodIndex>,
|
||||
|
||||
pub natural_resources: NaturalResources,
|
||||
natural_resources: NaturalResources,
|
||||
/// Neighboring sites to trade with
|
||||
pub neighbors: Vec<NeighborInformation>,
|
||||
neighbors: Vec<NeighborInformation>,
|
||||
|
||||
/// outgoing trade, per provider
|
||||
pub orders: DHashMap<Id<Site>, Vec<TradeOrder>>,
|
||||
orders: DHashMap<Id<Site>, Vec<TradeOrder>>,
|
||||
/// incoming trade - only towards this site
|
||||
pub deliveries: Vec<TradeDelivery>,
|
||||
deliveries: Vec<TradeDelivery>,
|
||||
}
|
||||
|
||||
impl Default for Economy {
|
||||
@ -163,10 +144,58 @@ impl Default for Economy {
|
||||
}
|
||||
|
||||
impl Economy {
|
||||
pub const MINIMUM_PRICE: f32 = 0.1;
|
||||
pub const STARTING_COIN: f32 = 1000.0;
|
||||
const MINIMUM_PRICE: f32 = 0.1;
|
||||
const STARTING_COIN: f32 = 1000.0;
|
||||
const _NATURAL_RESOURCE_SCALE: f32 = 1.0 / 9.0;
|
||||
|
||||
pub fn population(&self) -> f32 { self.pop }
|
||||
|
||||
pub fn get_available_stock(&self) -> HashMap<Good, f32> {
|
||||
self.unconsumed_stock
|
||||
.iter()
|
||||
.map(|(g, a)| (g.into(), *a))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_information(&self, id: Id<Site>) -> EconomyInfo {
|
||||
EconomyInfo {
|
||||
id: id.id(),
|
||||
population: self.pop.floor() as u32,
|
||||
stock: self
|
||||
.stocks
|
||||
.iter()
|
||||
.map(|(g, a)| (Good::from(g), *a))
|
||||
.collect(),
|
||||
labor_values: self
|
||||
.labor_values
|
||||
.iter()
|
||||
.filter_map(|(g, a)| a.map(|a| (Good::from(g), a)))
|
||||
.collect(),
|
||||
values: self
|
||||
.values
|
||||
.iter()
|
||||
.filter_map(|(g, a)| a.map(|a| (Good::from(g), a)))
|
||||
.collect(),
|
||||
labors: self.labors.iter().map(|(_, a)| (*a)).collect(),
|
||||
last_exports: self
|
||||
.last_exports
|
||||
.iter()
|
||||
.map(|(g, a)| (Good::from(g), *a))
|
||||
.collect(),
|
||||
resources: self
|
||||
.natural_resources
|
||||
.chunks_per_resource
|
||||
.iter()
|
||||
.map(|(g, a)| {
|
||||
(
|
||||
Good::from(g),
|
||||
((*a) as f32) * self.natural_resources.average_yield_per_chunk[g],
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cache_economy(&mut self) {
|
||||
for g in good_list() {
|
||||
let amount: f32 = self
|
||||
@ -188,13 +217,25 @@ impl Economy {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_orders(&self) -> DHashMap<Option<LaborIndex>, Vec<(GoodIndex, f32)>> {
|
||||
Labor::list_full()
|
||||
.map(|l| (if l.is_everyone() { None } else { Some(l) }, l.orders()))
|
||||
.collect()
|
||||
/// orders per profession (excluding everyone)
|
||||
fn get_orders(&self) -> &'static LaborMap<Vec<(GoodIndex, f32)>> {
|
||||
lazy_static! {
|
||||
static ref ORDERS: LaborMap<Vec<(GoodIndex, f32)>> = {
|
||||
let mut res: LaborMap<Vec<(GoodIndex, f32)>> = LaborMap::default();
|
||||
res.iter_mut()
|
||||
.for_each(|(i, e)| e.extend(i.orders().copied()));
|
||||
res
|
||||
};
|
||||
}
|
||||
&ORDERS
|
||||
}
|
||||
|
||||
pub fn get_productivity(&self) -> LaborMap<(GoodIndex, f32)> {
|
||||
/// resources consumed by everyone (no matter which profession)
|
||||
fn get_orders_everyone(&self) -> impl Iterator<Item = &'static (GoodIndex, f32)> {
|
||||
Labor::orders_everyone()
|
||||
}
|
||||
|
||||
fn get_production(&self) -> LaborMap<(GoodIndex, f32)> {
|
||||
// cache the site independent part of production
|
||||
lazy_static! {
|
||||
static ref PRODUCTS: LaborMap<(GoodIndex, f32)> = LaborMap::from_iter(
|
||||
@ -214,7 +255,7 @@ impl Economy {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn replenish(&mut self, _time: f32) {
|
||||
fn replenish(&mut self, _time: f32) {
|
||||
for (good, &ch) in self.natural_resources.chunks_per_resource.iter() {
|
||||
let per_year = self.natural_resources.average_yield_per_chunk[good] * ch;
|
||||
self.stocks[good] = self.stocks[good].max(per_year);
|
||||
@ -256,11 +297,10 @@ impl Economy {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_neighbor(&mut self, id: Id<Site>, distance: usize) {
|
||||
pub fn add_neighbor(&mut self, id: Id<Site>, _distance: usize) {
|
||||
self.neighbors.push(NeighborInformation {
|
||||
id,
|
||||
travel_distance: distance,
|
||||
|
||||
//travel_distance: distance,
|
||||
last_values: GoodMap::from_default(Economy::MINIMUM_PRICE),
|
||||
last_supplies: Default::default(),
|
||||
});
|
||||
@ -538,17 +578,17 @@ impl Economy {
|
||||
|
||||
let internal_orders = self.get_orders();
|
||||
let mut next_demand = GoodMap::from_default(0.0);
|
||||
for (labor, orders) in &internal_orders {
|
||||
let workers = if let Some(labor) = labor {
|
||||
self.labors[*labor]
|
||||
} else {
|
||||
1.0
|
||||
} * self.pop;
|
||||
for (labor, orders) in internal_orders.iter() {
|
||||
let workers = self.labors[labor] * self.pop;
|
||||
for (good, amount) in orders {
|
||||
next_demand[*good] += *amount * workers;
|
||||
assert!(next_demand[*good] >= 0.0);
|
||||
}
|
||||
}
|
||||
for (good, amount) in self.get_orders_everyone() {
|
||||
next_demand[*good] += *amount * self.pop;
|
||||
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() {
|
||||
@ -744,72 +784,64 @@ impl Economy {
|
||||
/// 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(
|
||||
&mut self,
|
||||
//deliveries: Option<&mut Vec<TradeDelivery>>,
|
||||
site_id: Id<Site>,
|
||||
dt: f32,
|
||||
mut vc: vergleich::Context,
|
||||
) {
|
||||
pub fn tick(&mut self, site_id: Id<Site>, dt: f32) {
|
||||
// collect goods from trading
|
||||
if INTER_SITE_TRADE {
|
||||
// if let Some(deliveries) = deliveries {
|
||||
self.collect_deliveries();
|
||||
// }
|
||||
}
|
||||
|
||||
let orders = self.get_orders();
|
||||
let productivity = self.get_productivity();
|
||||
let production = self.get_production();
|
||||
|
||||
for i in productivity.iter() {
|
||||
vc.context("productivity")
|
||||
.value(&std::format!("{:?}{:?}", i.0, Good::from(i.1.0)), i.1.1);
|
||||
}
|
||||
// for i in production.iter() {
|
||||
// vc.context("production")
|
||||
// .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 {
|
||||
self.labors[*labor]
|
||||
} else {
|
||||
1.0
|
||||
} * self.pop;
|
||||
for (labor, orders) in orders.iter() {
|
||||
let workers = self.labors[labor] * self.pop;
|
||||
for (good, amount) in orders {
|
||||
demand[*good] += *amount * workers;
|
||||
}
|
||||
}
|
||||
for (good, amount) in self.get_orders_everyone() {
|
||||
demand[*good] += *amount * self.pop;
|
||||
}
|
||||
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
|
||||
let merchant_labor = production
|
||||
.iter()
|
||||
.find(|(_, v)| v.0 == *TRANSPORTATION_INDEX)
|
||||
.map(|(l, _)| l);
|
||||
.map(|(l, _)| l)
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut supply = self.stocks; //GoodMap::from_default(0.0);
|
||||
for (labor, goodvec) in productivity.iter() {
|
||||
for (labor, goodvec) in production.iter() {
|
||||
//for (output_good, _) in goodvec.iter() {
|
||||
//info!("{} supply{:?}+={}", site_id.id(), Good::from(goodvec.0),
|
||||
// self.yields[labor] * self.labors[labor] * self.pop);
|
||||
supply[goodvec.0] += self.yields[labor] * self.labors[labor] * self.pop;
|
||||
vc.context(&std::format!("{:?}-{:?}", Good::from(goodvec.0), labor))
|
||||
.value("yields", self.yields[labor]);
|
||||
vc.context(&std::format!("{:?}-{:?}", Good::from(goodvec.0), labor))
|
||||
.value("labors", self.labors[labor]);
|
||||
// vc.context(&std::format!("{:?}-{:?}", Good::from(goodvec.0),
|
||||
// labor)) .value("yields", self.yields[labor]);
|
||||
// vc.context(&std::format!("{:?}-{:?}", Good::from(goodvec.0),
|
||||
// labor)) .value("labors", self.labors[labor]);
|
||||
//}
|
||||
}
|
||||
|
||||
for i in supply.iter() {
|
||||
vc.context("supply")
|
||||
.value(&std::format!("{:?}", Good::from(i.0)), *i.1);
|
||||
}
|
||||
// for i in supply.iter() {
|
||||
// vc.context("supply")
|
||||
// .value(&std::format!("{:?}", Good::from(i.0)), *i.1);
|
||||
// }
|
||||
|
||||
let stocks = &self.stocks;
|
||||
for i in stocks.iter() {
|
||||
vc.context("stocks")
|
||||
.value(&std::format!("{:?}", Good::from(i.0)), *i.1);
|
||||
}
|
||||
// for i in stocks.iter() {
|
||||
// vc.context("stocks")
|
||||
// .value(&std::format!("{:?}", Good::from(i.0)), *i.1);
|
||||
// }
|
||||
self.surplus = demand.map(|g, demand| supply[g] + stocks[g] - demand);
|
||||
self.marginal_surplus = demand.map(|g, demand| supply[g] - demand);
|
||||
|
||||
@ -820,18 +852,14 @@ impl Economy {
|
||||
// this is in line with the other professions)
|
||||
let transportation_capacity = self.stocks[*TRANSPORTATION_INDEX];
|
||||
let trade = if INTER_SITE_TRADE {
|
||||
let trade = self.plan_trade_for_site(
|
||||
&site_id,
|
||||
transportation_capacity,
|
||||
// external_orders,
|
||||
&mut potential_trade,
|
||||
);
|
||||
let trade =
|
||||
self.plan_trade_for_site(&site_id, transportation_capacity, &mut potential_trade);
|
||||
self.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);
|
||||
// vc.context("trade")
|
||||
// .value(&std::format!("{:?}", Good::from(g)), *a);
|
||||
if *a > 0.0 {
|
||||
supply[g] += *a;
|
||||
assert!(supply[g] >= 0.0);
|
||||
@ -859,16 +887,17 @@ impl Economy {
|
||||
*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 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,
|
||||
Some(
|
||||
// vc.context("values").value(
|
||||
// &goodname,
|
||||
smooth * values[good].unwrap_or(val) + (1.0 - smooth) * val,
|
||||
))
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
@ -885,10 +914,10 @@ impl Economy {
|
||||
// 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)| {
|
||||
production.iter().map(|(labor, goodvec)| {
|
||||
(
|
||||
labor,
|
||||
if Some(labor) == merchant_labor {
|
||||
if labor == merchant_labor {
|
||||
all_trade_goods
|
||||
.iter()
|
||||
.chain(std::iter::once(&goodvec.0))
|
||||
@ -906,15 +935,15 @@ impl Economy {
|
||||
trace!(?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 mut labor_context = vc.context("labor");
|
||||
production.iter().for_each(|(labor, _)| {
|
||||
let smooth = 0.8;
|
||||
self.labors[labor] = labor_context.value(
|
||||
&format!("{:?}", labor),
|
||||
self.labors[labor] =
|
||||
// labor_context.value(
|
||||
// &format!("{:?}", labor),
|
||||
smooth * self.labors[labor]
|
||||
+ (1.0 - smooth)
|
||||
* (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum),
|
||||
);
|
||||
* (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum);
|
||||
assert!(self.labors[labor] >= 0.0);
|
||||
});
|
||||
|
||||
@ -932,13 +961,9 @@ impl Economy {
|
||||
// TODO: trade
|
||||
let mut total_outputs = GoodMap::<f32>::default();
|
||||
for (labor, orders) in orders.iter() {
|
||||
let workers = if let Some(labor) = labor {
|
||||
self.labors[*labor]
|
||||
} else {
|
||||
1.0
|
||||
} * self.pop;
|
||||
let workers = self.labors[labor] * self.pop;
|
||||
assert!(workers >= 0.0);
|
||||
let is_merchant = merchant_labor == *labor;
|
||||
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
|
||||
@ -961,13 +986,11 @@ impl Economy {
|
||||
panic!("Industry {:?} requires at least one input order", labor)
|
||||
});
|
||||
assert!(labor_productivity >= 0.0);
|
||||
if let Some(labor) = labor {
|
||||
self.limited_by[*labor] = if labor_productivity >= 1.0 {
|
||||
GoodIndex::default()
|
||||
} else {
|
||||
limited_by
|
||||
};
|
||||
}
|
||||
self.limited_by[labor] = if labor_productivity >= 1.0 {
|
||||
GoodIndex::default()
|
||||
} else {
|
||||
limited_by
|
||||
};
|
||||
|
||||
let mut total_materials_cost = 0.0;
|
||||
for (good, amount) in orders {
|
||||
@ -1039,38 +1062,39 @@ impl Economy {
|
||||
}
|
||||
|
||||
// Industries produce things
|
||||
if let Some(labor) = labor {
|
||||
let work_products = &productivity[*labor];
|
||||
//let workers = self.labors[*labor] * self.pop;
|
||||
//let final_rate = rate;
|
||||
//let yield_per_worker = labor_productivity;
|
||||
self.yields[*labor] = labor_productivity * work_products.1;
|
||||
self.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);
|
||||
self.stocks[*stock] += total_output;
|
||||
produced_goods[*stock] += total_output;
|
||||
let work_products = &production[labor];
|
||||
self.yields[labor] = labor_productivity * work_products.1;
|
||||
self.productivity[labor] = labor_productivity;
|
||||
let (stock, rate) = work_products;
|
||||
let total_output = labor_productivity * *rate * workers;
|
||||
assert!(total_output >= 0.0);
|
||||
self.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)
|
||||
self.material_costs[stock] =
|
||||
total_materials_cost / amount.max(0.001) * cost_weight;
|
||||
// Labor costs
|
||||
let wages = 1.0;
|
||||
let total_labor_cost = workers * wages;
|
||||
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)
|
||||
self.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;
|
||||
}
|
||||
total_labor_values[stock] +=
|
||||
(total_materials_cost + total_labor_cost) * cost_weight;
|
||||
total_outputs[stock] += amount;
|
||||
}
|
||||
}
|
||||
// consume goods needed by everyone
|
||||
for &(good, amount) in self.get_orders_everyone() {
|
||||
let needed = amount * self.pop;
|
||||
let available = stocks_before[good];
|
||||
self.stocks[good] = (self.stocks[good] - needed.min(available)).max(0.0);
|
||||
//info!("Ev {:.1} {:?} {} - {:.1} {:.1}", self.pop, good,
|
||||
// self.stocks[good], needed, available);
|
||||
}
|
||||
|
||||
// Update labour values per unit
|
||||
self.labor_values = total_labor_values.map(|stock, tlv| {
|
||||
@ -1100,10 +1124,10 @@ impl Economy {
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
self.pop += vc.value(
|
||||
"pop",
|
||||
dt / DAYS_PER_YEAR * self.pop * (birth_rate - DEATH_RATE),
|
||||
);
|
||||
self.pop += //vc.value(
|
||||
//"pop",
|
||||
dt / DAYS_PER_YEAR * self.pop * (birth_rate - DEATH_RATE);
|
||||
//);
|
||||
self.population_limited_by = if population_growth {
|
||||
GoodIndex::default()
|
||||
} else {
|
||||
@ -1115,35 +1139,244 @@ impl Economy {
|
||||
// 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 {
|
||||
self.labors[*labor]
|
||||
} else {
|
||||
1.0
|
||||
} * self.pop;
|
||||
let workers = self.labors[labor] * self.pop;
|
||||
for (good, amount) in orders {
|
||||
next_demand[*good] += *amount * workers;
|
||||
assert!(next_demand[*good] >= 0.0);
|
||||
}
|
||||
}
|
||||
let mut us = vc.context("unconsumed");
|
||||
for (good, amount) in self.get_orders_everyone() {
|
||||
next_demand[*good] += *amount * self.pop;
|
||||
assert!(next_demand[*good] >= 0.0);
|
||||
}
|
||||
//let mut us = vc.context("unconsumed");
|
||||
self.unconsumed_stock = GoodMap::from_iter(
|
||||
self.stocks.iter().map(|(g, a)| {
|
||||
(
|
||||
g,
|
||||
us.value(&format!("{:?}", Good::from(g)), *a - next_demand[g]),
|
||||
//us.value(&format!("{:?}", Good::from(g)),
|
||||
*a - next_demand[g],
|
||||
)
|
||||
}),
|
||||
0.0,
|
||||
);
|
||||
}
|
||||
|
||||
pub fn csv_entry(f: &mut std::fs::File, site: &Site) -> Result<(), std::io::Error> {
|
||||
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)
|
||||
}
|
||||
|
||||
pub fn csv_open() -> Option<std::fs::File> {
|
||||
if GENERATE_CSV {
|
||||
let mut f = std::fs::File::create("economy.csv").ok()?;
|
||||
if Self::csv_header(&mut f).is_err() {
|
||||
None
|
||||
} else {
|
||||
Some(f)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn print_details(&self) {
|
||||
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!();
|
||||
}
|
||||
|
||||
print!(" Resources: ");
|
||||
for i in good_list() {
|
||||
let amount = self.natural_resources.chunks_per_resource[i];
|
||||
if amount > 0.0 {
|
||||
print!("{:?}={} ", i, amount);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
println!(
|
||||
" Population {:.1}, limited by {:?}",
|
||||
self.pop, self.population_limited_by
|
||||
);
|
||||
let idle: f32 = self.pop * (1.0 - self.labors.iter().map(|(_, a)| *a).sum::<f32>());
|
||||
print_sorted(
|
||||
&format!(" Professions: idle={:.1} ", idle),
|
||||
self.labors
|
||||
.iter()
|
||||
.map(|(l, a)| (format!("{:?}", l), *a * self.pop))
|
||||
.collect(),
|
||||
self.pop * 0.05,
|
||||
1,
|
||||
);
|
||||
print_sorted(
|
||||
" Stock: ",
|
||||
self.stocks
|
||||
.iter()
|
||||
.map(|(l, a)| (format!("{:?}", l), *a))
|
||||
.collect(),
|
||||
1.0,
|
||||
0,
|
||||
);
|
||||
print_sorted(
|
||||
" Values: ",
|
||||
self.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: ",
|
||||
self.labor_values
|
||||
.iter()
|
||||
.map(|(l, a)| (format!("{:?}", l), a.unwrap_or(0.0)))
|
||||
.collect(),
|
||||
0.1,
|
||||
1,
|
||||
);
|
||||
print!(" Limited: ");
|
||||
for (limit, prod) in self.limited_by.iter().zip(self.productivity.iter()) {
|
||||
if (0.01..=0.99).contains(prod.1) {
|
||||
print!("{:?}:{:?}={:.2} ", limit.0, limit.1, *prod.1);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
print!(" Trade({}): ", self.neighbors.len());
|
||||
for (g, &amt) in self.active_exports.iter() {
|
||||
if !(-0.1..=0.1).contains(&amt) {
|
||||
print!("{:?}={:.2} ", g, amt);
|
||||
}
|
||||
}
|
||||
println!();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn good_list() -> impl Iterator<Item = GoodIndex> {
|
||||
fn good_list() -> impl Iterator<Item = GoodIndex> {
|
||||
(0..GoodIndex::LENGTH).map(GoodIndex::from_usize)
|
||||
}
|
||||
|
||||
// cache in GoodMap ?
|
||||
pub fn transportation_effort(g: GoodIndex) -> f32 {
|
||||
fn transportation_effort(g: GoodIndex) -> f32 {
|
||||
match Good::from(g) {
|
||||
Terrain(_) | Territory(_) | RoadSecurity => 0.0,
|
||||
Coin => 0.01,
|
||||
@ -1156,7 +1389,7 @@ pub fn transportation_effort(g: GoodIndex) -> f32 {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn decay_rate(g: GoodIndex) -> f32 {
|
||||
fn decay_rate(g: GoodIndex) -> f32 {
|
||||
match Good::from(g) {
|
||||
Food => 0.2,
|
||||
Flour => 0.1,
|
||||
@ -1167,7 +1400,7 @@ pub fn decay_rate(g: GoodIndex) -> f32 {
|
||||
}
|
||||
|
||||
/** you can't accumulate or save these options/resources for later */
|
||||
pub fn direct_use_goods() -> &'static [GoodIndex] {
|
||||
fn direct_use_goods() -> &'static [GoodIndex] {
|
||||
lazy_static! {
|
||||
static ref DIRECT_USE: [GoodIndex; 13] = [
|
||||
GoodIndex::try_from(Transportation).unwrap_or_default(),
|
||||
@ -1187,3 +1420,36 @@ pub fn direct_use_goods() -> &'static [GoodIndex] {
|
||||
}
|
||||
&*DIRECT_USE
|
||||
}
|
||||
|
||||
pub struct GraphInfo {
|
||||
dummy: Economy,
|
||||
}
|
||||
|
||||
impl Default for GraphInfo {
|
||||
fn default() -> Self {
|
||||
// avoid economy of scale
|
||||
Self {
|
||||
dummy: Economy {
|
||||
pop: 0.0,
|
||||
labors: LaborMap::from_default(0.0),
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GraphInfo {
|
||||
pub fn get_orders(&self) -> &'static LaborMap<Vec<(GoodIndex, f32)>> { self.dummy.get_orders() }
|
||||
|
||||
pub fn get_orders_everyone(&self) -> impl Iterator<Item = &'static (GoodIndex, f32)> {
|
||||
self.dummy.get_orders_everyone()
|
||||
}
|
||||
|
||||
pub fn get_production(&self) -> LaborMap<(GoodIndex, f32)> { self.dummy.get_production() }
|
||||
|
||||
pub fn good_list(&self) -> impl Iterator<Item = GoodIndex> { good_list() }
|
||||
|
||||
pub fn labor_list(&self) -> impl Iterator<Item = Labor> { Labor::list() }
|
||||
|
||||
pub fn can_store(&self, g: &GoodIndex) -> bool { direct_use_goods().contains(g) }
|
||||
}
|
||||
|
@ -187,12 +187,7 @@ impl Site {
|
||||
SiteKind::Settlement(_) | SiteKind::Refactor(_) | SiteKind::CliffTown(_) => {
|
||||
Some(common::trade::SiteInformation {
|
||||
id: site_id,
|
||||
unconsumed_stock: self
|
||||
.economy
|
||||
.unconsumed_stock
|
||||
.iter()
|
||||
.map(|(g, a)| (g.into(), *a))
|
||||
.collect(),
|
||||
unconsumed_stock: self.economy.get_available_stock(),
|
||||
})
|
||||
},
|
||||
_ => None,
|
||||
|
Loading…
Reference in New Issue
Block a user