1use crate::{
3 Index,
4 site::{
5 SiteKind,
6 economy::{DAYS_PER_MONTH, DAYS_PER_YEAR, Economy, INTER_SITE_TRADE},
7 },
8};
9use rayon::prelude::*;
10use tracing::{debug, info};
11
12const TICK_PERIOD: f32 = 3.0 * DAYS_PER_MONTH; const HISTORY_DAYS: f32 = 500.0 * DAYS_PER_YEAR; #[derive(Debug)]
45struct EconStatistics {
46 count: u32,
47 sum: f32,
48 min: f32,
49 max: f32,
50}
51
52impl Default for EconStatistics {
53 fn default() -> Self {
54 Self {
55 count: 0,
56 sum: 0.0,
57 min: f32::INFINITY,
58 max: -f32::INFINITY,
59 }
60 }
61}
62
63impl std::ops::AddAssign<f32> for EconStatistics {
64 fn add_assign(&mut self, rhs: f32) { self.collect(rhs); }
65}
66
67impl EconStatistics {
68 fn collect(&mut self, value: f32) {
69 self.count += 1;
70 self.sum += value;
71 if value > self.max {
72 self.max = value;
73 }
74 if value < self.min {
75 self.min = value;
76 }
77 }
78
79 fn valid(&self) -> bool { self.min.is_finite() }
80}
81
82pub struct Environment {
83 csv_file: Option<std::fs::File>,
84 }
86
87impl Environment {
88 pub fn new() -> Result<Self, std::io::Error> {
89 let csv_file = Economy::csv_open();
93 Ok(Self {
94 csv_file, })
96 }
97
98 fn iteration(&mut self, _: i32) {}
99
100 fn end(mut self, index: &Index) {
101 if let Some(f) = self.csv_file.as_mut() {
102 use std::io::Write;
103 let err = writeln!(f);
104 if err.is_ok() {
105 for site in index.sites.ids() {
106 let site = index.sites.get(site);
107 if Economy::csv_entry(f, site).is_err() {
108 break;
109 }
110 }
111 }
112 self.csv_file.take();
113 }
114
115 {
116 let mut castles = EconStatistics::default();
117 let mut towns = EconStatistics::default();
118 let dungeons = EconStatistics::default();
119 for site in index.sites.ids() {
120 let site = &index.sites[site];
121 match site.kind {
122 SiteKind::Settlement(_)
123 | SiteKind::Refactor(_)
124 | SiteKind::CliffTown(_)
125 | SiteKind::SavannahTown(_)
126 | SiteKind::CoastalTown(_)
127 | SiteKind::DesertCity(_) => towns += site.economy.pop,
128 SiteKind::Castle(_) => castles += site.economy.pop,
129 SiteKind::Tree(_)
130 | SiteKind::GiantTree(_)
131 | SiteKind::Gnarling(_)
132 | SiteKind::Adlet(_)
133 | SiteKind::Cultist(_)
134 | SiteKind::Sahagin(_)
135 | SiteKind::Haniwa(_)
136 | SiteKind::JungleRuin(_)
137 | SiteKind::Myrmidon(_)
138 | SiteKind::ChapelSite(_)
139 | SiteKind::DwarvenMine(_)
140 | SiteKind::Terracotta(_)
141 | SiteKind::Bridge(_)
142 | SiteKind::PirateHideout(_)
143 | SiteKind::RockCircle(_)
144 | SiteKind::TrollCave(_)
145 | SiteKind::VampireCastle(_)
146 | SiteKind::GliderCourse(_)
147 | SiteKind::Camp(_) => {},
148 }
149 }
150 if towns.valid() {
151 info!(
152 "Towns {:.0}-{:.0} avg {:.0} inhabitants",
153 towns.min,
154 towns.max,
155 towns.sum / (towns.count as f32)
156 );
157 }
158 if castles.valid() {
159 info!(
160 "Castles {:.0}-{:.0} avg {:.0}",
161 castles.min,
162 castles.max,
163 castles.sum / (castles.count as f32)
164 );
165 }
166 if dungeons.valid() {
167 info!(
168 "Dungeons {:.0}-{:.0} avg {:.0}",
169 dungeons.min,
170 dungeons.max,
171 dungeons.sum / (dungeons.count as f32)
172 );
173 }
174 }
175 }
176
177 fn csv_tick(&mut self, index: &Index) {
178 if let Some(f) = self.csv_file.as_mut() {
179 if let Some(site) = index.sites.values().find(|s| {
180 !matches!(
181 s.kind,
182 SiteKind::Terracotta(_)
183 | SiteKind::Haniwa(_)
184 | SiteKind::Myrmidon(_)
185 | SiteKind::Adlet(_)
186 | SiteKind::DwarvenMine(_)
187 | SiteKind::ChapelSite(_)
188 | SiteKind::Cultist(_)
189 | SiteKind::Gnarling(_)
190 | SiteKind::Sahagin(_)
191 | SiteKind::VampireCastle(_),
192 )
193 }) {
194 Economy::csv_entry(f, site).unwrap_or_else(|_| {
195 self.csv_file.take();
196 });
197 }
198 }
199 }
200}
201
202fn simulate_return(index: &mut Index) -> Result<(), std::io::Error> {
203 let mut env = Environment::new()?;
204
205 info!("economy simulation start");
206 for i in 0..(HISTORY_DAYS / TICK_PERIOD) as i32 {
207 if (index.time / DAYS_PER_YEAR) as i32 % 50 == 0 && (index.time % DAYS_PER_YEAR) as i32 == 0
208 {
209 debug!("Year {}", (index.time / DAYS_PER_YEAR) as i32);
210 }
211 env.iteration(i);
212 tick(index, TICK_PERIOD, &mut env);
213 if i % 5 == 0 {
214 env.csv_tick(index);
215 }
216 }
217 info!("economy simulation end");
218 env.end(index);
219 Ok(())
222}
223
224pub fn simulate_economy(index: &mut Index) {
225 simulate_return(index)
226 .unwrap_or_else(|err| info!("I/O error in simulate (economy.csv not writable?): {}", err));
227}
228
229fn tick(index: &mut Index, dt: f32, _env: &mut Environment) {
249 if INTER_SITE_TRADE {
250 for (id, deliv) in index.trade.deliveries.drain() {
252 index.sites.get_mut(id).economy.deliveries.extend(deliv);
253 }
254 }
255 index.sites.par_iter_mut().for_each(|(site_id, site)| {
256 if site.do_economic_simulation() {
257 site.economy.tick(site_id, dt);
258 }
261 });
262 if INTER_SITE_TRADE {
263 for (_id, site) in index.sites.iter_mut() {
265 for (i, mut v) in site.economy.orders.drain() {
266 index.trade.orders.entry(i).or_default().append(&mut v);
267 }
268 }
269 for (&site, orders) in index.trade.orders.iter_mut() {
271 let siteinfo = index.sites.get_mut(site);
272 if siteinfo.do_economic_simulation() {
273 siteinfo
274 .economy
275 .trade_at_site(site, orders, &mut index.trade.deliveries);
276 }
277 }
278 }
279 index.time += dt;
282}
283
284#[cfg(test)]
285mod tests {
286 use crate::{sim, util::seed_expan};
287 use common::{
288 store::Id,
289 terrain::{BiomeKind, site::SiteKindMeta},
290 trade::Good,
291 };
292 use hashbrown::HashMap;
293 use rand::SeedableRng;
294 use rand_chacha::ChaChaRng;
295 use serde::{Deserialize, Serialize};
296 use std::convert::TryInto;
297 use tracing::{Dispatch, Level, info};
298 use tracing_subscriber::{FmtSubscriber, filter::EnvFilter};
299 use vek::Vec2;
300
301 fn execute_with_tracing(level: Level, func: fn()) {
302 tracing::dispatcher::with_default(
303 &Dispatch::new(
304 FmtSubscriber::builder()
305 .with_max_level(level)
306 .with_env_filter(EnvFilter::from_default_env())
307 .finish(),
308 ),
309 func,
310 );
311 }
312
313 #[derive(Debug, Serialize, Deserialize)]
314 struct ResourcesSetup {
315 good: Good,
316 amount: f32,
317 }
318
319 #[derive(Debug, Serialize, Deserialize)]
320 struct EconomySetup {
321 name: String,
322 position: (i32, i32),
323 kind: common::terrain::site::SiteKindMeta,
324 neighbors: Vec<u64>, resources: Vec<ResourcesSetup>,
326 }
327
328 fn show_economy(
329 sites: &common::store::Store<crate::site::Site>,
330 names: &Option<HashMap<Id<crate::site::Site>, String>>,
331 ) {
332 for (id, site) in sites.iter() {
333 let name = names.as_ref().map_or(site.name().into(), |map| {
334 map.get(&id).cloned().unwrap_or_else(|| site.name().into())
335 });
336 println!("Site id {:?} name {}", id.id(), name);
337 site.economy.print_details();
338 }
339 }
340
341 #[test]
345 #[ignore]
346 fn test_economy0() {
347 execute_with_tracing(Level::INFO, || {
348 let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
349 info!("init");
350 let seed = sim::DEFAULT_WORLD_SEED;
351 let opts = sim::WorldOpts {
352 seed_elements: true,
353 world_file: sim::FileOpts::LoadAsset(sim::DEFAULT_WORLD_MAP.into()),
354 calendar: None,
356 };
357 let mut index = crate::index::Index::new(seed);
358 info!("Index created");
359 let mut sim = sim::WorldSim::generate(seed, opts, &threadpool, &|_| {});
360 info!("World loaded");
361 let _civs = crate::civ::Civs::generate(seed, &mut sim, &mut index, None, &|_| {});
362 info!("Civs created");
363 crate::sim2::simulate(&mut index, &mut sim);
364 show_economy(&index.sites, &None);
365 });
366 }
367
368 #[test]
371 #[ignore]
372 fn test_economy1() {
373 execute_with_tracing(Level::INFO, || {
374 let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
375 info!("init");
376 let seed = sim::DEFAULT_WORLD_SEED;
377 let opts = sim::WorldOpts {
378 seed_elements: true,
379 world_file: sim::FileOpts::LoadAsset(sim::DEFAULT_WORLD_MAP.into()),
380 calendar: None,
382 };
383 let mut index = crate::index::Index::new(seed);
384 info!("Index created");
385 let mut sim = sim::WorldSim::generate(seed, opts, &threadpool, &|_| {});
386 info!("World loaded");
387 let mut names = None;
388 let regenerate_input = false;
389 if regenerate_input {
390 let _civs = crate::civ::Civs::generate(seed, &mut sim, &mut index, None, &|_| {});
391 info!("Civs created");
392 let mut outarr: Vec<EconomySetup> = Vec::new();
393 for i in index.sites.values() {
394 let resources: Vec<ResourcesSetup> = i
395 .economy
396 .natural_resources
397 .chunks_per_resource
398 .iter()
399 .map(|(good, a)| ResourcesSetup {
400 good: good.into(),
401 amount: *a * i.economy.natural_resources.average_yield_per_chunk[good],
402 })
403 .collect();
404 let neighbors = i.economy.neighbors.iter().map(|j| j.id.id()).collect();
405 let val = EconomySetup {
406 name: i.name().into(),
407 position: (i.get_origin().x, i.get_origin().y),
408 resources,
409 neighbors,
410 kind: i.kind.convert_to_meta().unwrap_or_default(),
411 };
412 outarr.push(val);
413 }
414 let pretty = ron::ser::PrettyConfig::new();
415 if let Ok(result) = ron::ser::to_string_pretty(&outarr, pretty) {
416 info!("RON {}", result);
417 }
418 } else {
419 let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
420 let ron_file = std::fs::File::open("economy_testinput2.ron")
421 .expect("economy_testinput2.ron not found");
422 let econ_testinput: Vec<EconomySetup> =
423 ron::de::from_reader(ron_file).expect("economy_testinput2.ron parse error");
424 names = Some(HashMap::new());
425 for i in econ_testinput.iter() {
426 let wpos = Vec2 {
427 x: i.position.0,
428 y: i.position.1,
429 };
430 let mut settlement = match i.kind {
434 SiteKindMeta::Castle => crate::site::Site::castle(
435 crate::site::Castle::generate(wpos, None, &mut rng),
436 ),
437 _ => crate::site::Site::settlement(crate::site::Settlement::generate(
439 wpos, None, &mut rng,
440 )),
441 };
442 for g in i.resources.iter() {
443 settlement.economy.natural_resources.chunks_per_resource
447 [g.good.try_into().unwrap_or_default()] = g.amount;
448 settlement.economy.natural_resources.average_yield_per_chunk
449 [g.good.try_into().unwrap_or_default()] = 1.0;
450 }
451 let id = index.sites.insert(settlement);
452 names.as_mut().map(|map| map.insert(id, i.name.clone()));
453 }
454 for (id, econ) in econ_testinput.iter().enumerate() {
457 if let Some(id) = index.sites.recreate_id(id as u64) {
458 for nid in econ.neighbors.iter() {
459 if let Some(nid) = index.sites.recreate_id(*nid) {
460 let town = &mut index.sites.get_mut(id).economy;
461 town.add_neighbor(nid, 0);
462 }
463 }
464 }
465 }
466 }
467 crate::sim2::simulate(&mut index, &mut sim);
468 show_economy(&index.sites, &names);
469 });
470 }
471
472 struct Simenv {
473 index: crate::index::Index,
474 rng: ChaChaRng,
475 targets: HashMap<Id<crate::site::Site>, f32>,
476 names: HashMap<Id<crate::site::Site>, String>,
477 }
478
479 #[test]
480 fn test_economy_moderate_standalone() {
482 fn add_settlement(
483 env: &mut Simenv,
484 name: &str,
485 target: f32,
486 resources: &[(Good, f32)],
487 ) -> Id<crate::site::Site> {
488 let wpos = Vec2 { x: 42, y: 42 };
489 let mut settlement = crate::site::Site::settlement(crate::site::Settlement::generate(
490 wpos,
491 None,
492 &mut env.rng,
493 ));
494 for (good, amount) in resources.iter() {
495 settlement.economy.natural_resources.chunks_per_resource
496 [(*good).try_into().unwrap_or_default()] = *amount;
497 settlement.economy.natural_resources.average_yield_per_chunk
498 [(*good).try_into().unwrap_or_default()] = 1.0;
499 }
500 let id = env.index.sites.insert(settlement);
501 env.targets.insert(id, target);
502 env.names.insert(id, name.into());
503 id
504 }
505
506 execute_with_tracing(Level::ERROR, || {
507 let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
508 info!("init");
509 let seed = sim::DEFAULT_WORLD_SEED;
510 let opts = sim::WorldOpts {
511 seed_elements: true,
512 world_file: sim::FileOpts::LoadAsset(sim::DEFAULT_WORLD_MAP.into()),
513 calendar: Default::default(),
514 };
515 let index = crate::index::Index::new(seed);
516 info!("Index created");
517 let mut sim = sim::WorldSim::generate(seed, opts, &threadpool, &|_| {});
518 info!("World loaded");
519 let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
520 let mut env = Simenv {
521 index,
522 rng,
523 targets: HashMap::new(),
524 names: HashMap::new(),
525 };
526 add_settlement(&mut env, "Forest", 5000.0, &[(
527 Good::Terrain(BiomeKind::Forest),
528 100.0_f32,
529 )]);
530 add_settlement(&mut env, "Grass", 700.0, &[(
531 Good::Terrain(BiomeKind::Grassland),
532 100.0_f32,
533 )]);
534 add_settlement(&mut env, "Mountain", 3.0, &[(
535 Good::Terrain(BiomeKind::Mountain),
536 100.0_f32,
537 )]);
538 add_settlement(&mut env, "GrFoMo", 12000.0, &[
549 (Good::Terrain(BiomeKind::Grassland), 100.0_f32),
550 (Good::Terrain(BiomeKind::Forest), 100.0_f32),
551 (Good::Terrain(BiomeKind::Mountain), 10.0_f32),
552 ]);
553 for i in 1..(env.index.sites.ids().count() as u64 - 1) {
559 let previous = env.index.sites.recreate_id(i - 1);
560 let center = env.index.sites.recreate_id(i);
561 center.zip(previous).map(|(center, previous)| {
562 env.index.sites[center]
563 .economy
564 .add_neighbor(previous, i as usize);
565 env.index.sites[previous]
566 .economy
567 .add_neighbor(center, i as usize);
568 });
569 }
570 crate::sim2::simulate(&mut env.index, &mut sim);
571 show_economy(&env.index.sites, &Some(env.names));
572 for (id, site) in env.index.sites.iter() {
574 assert!(site.economy.pop >= env.targets[&id]);
575 }
576 });
577 }
578}