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