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 && let Some(site) = index.sites.values().find(|s| s.do_economic_simulation())
143 {
144 Economy::csv_entry(f, site).unwrap_or_else(|_| {
145 self.csv_file.take();
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
288 .as_ref()
289 .and_then(|map| Some(map.get(&id)?.as_str()))
290 .or(site.name())
291 .unwrap_or("");
292 println!("Site id {:?} name {}", id.id(), name);
293 if let Some(econ) = site.economy.as_ref() {
294 econ.print_details();
295 }
296 }
297 }
298
299 #[test]
303 #[ignore]
304 fn test_economy0() {
305 execute_with_tracing(Level::INFO, || {
306 let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
307 info!("init");
308 let seed = sim::DEFAULT_WORLD_SEED;
309 let opts = sim::WorldOpts {
310 seed_elements: true,
311 world_file: sim::FileOpts::LoadAsset(sim::DEFAULT_WORLD_MAP.into()),
312 calendar: None,
314 };
315 let mut index = crate::index::Index::new(seed);
316 info!("Index created");
317 let mut sim = sim::WorldSim::generate(seed, opts, &threadpool, &|_| {});
318 info!("World loaded");
319 let _civs = crate::civ::Civs::generate(seed, &mut sim, &mut index, None, &|_| {});
320 info!("Civs created");
321 crate::sim2::simulate(&mut index, &mut sim);
322 show_economy(&index.sites, &None);
323 });
324 }
325
326 #[test]
329 #[ignore]
330 fn test_economy1() {
331 execute_with_tracing(Level::INFO, || {
332 let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
333 info!("init");
334 let seed = sim::DEFAULT_WORLD_SEED;
335 let opts = sim::WorldOpts {
336 seed_elements: true,
337 world_file: sim::FileOpts::LoadAsset(sim::DEFAULT_WORLD_MAP.into()),
338 calendar: None,
340 };
341 let mut index = crate::index::Index::new(seed);
342 info!("Index created");
343 let mut sim = sim::WorldSim::generate(seed, opts, &threadpool, &|_| {});
344 info!("World loaded");
345 let mut names = None;
346 let regenerate_input = false;
347 if regenerate_input {
348 let _civs = crate::civ::Civs::generate(seed, &mut sim, &mut index, None, &|_| {});
349 info!("Civs created");
350 let mut outarr: Vec<EconomySetup> = Vec::new();
351 for i in index.sites.values() {
352 let Some(econ) = i.economy.as_ref() else {
353 continue;
354 };
355 let resources: Vec<ResourcesSetup> = econ
356 .natural_resources
357 .chunks_per_resource
358 .iter()
359 .map(|(good, a)| ResourcesSetup {
360 good: good.into(),
361 amount: *a * econ.natural_resources.average_yield_per_chunk[good],
362 })
363 .collect();
364 let neighbors = econ.neighbors.iter().map(|j| j.id.id()).collect();
365 let val = EconomySetup {
366 name: i.name().unwrap_or("").into(),
367 position: (i.origin.x, i.origin.y),
368 resources,
369 neighbors,
370 kind: i.meta().unwrap_or_default(),
371 };
372 outarr.push(val);
373 }
374 let pretty = ron::ser::PrettyConfig::new();
375 if let Ok(result) = ron::ser::to_string_pretty(&outarr, pretty) {
376 info!("RON {}", result);
377 }
378 } else {
379 let mut rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
380 let ron_file = std::fs::File::open("economy_testinput2.ron")
381 .expect("economy_testinput2.ron not found");
382 let econ_testinput: Vec<EconomySetup> =
383 ron::de::from_reader(ron_file).expect("economy_testinput2.ron parse error");
384 names = Some(HashMap::new());
385 let land = crate::Land::from_sim(&sim);
386 let mut meta = crate::site::SitesGenMeta::new(rng.random());
387 for i in econ_testinput.iter() {
388 let wpos = Vec2 {
389 x: i.position.0,
390 y: i.position.1,
391 };
392 let mut settlement = match i.kind {
396 SiteKindMeta::Castle => {
397 crate::site::Site::generate_citadel(&land, &mut rng, wpos)
398 },
399 _ => crate::site::Site::generate_city(
400 &land,
401 crate::IndexRef {
402 colors: &index.colors(),
403 features: &index.features(),
404 index: &index,
405 },
406 &mut rng,
407 wpos,
408 1.0,
409 None,
410 &mut meta,
411 ),
412 };
413 for g in i.resources.iter() {
414 settlement
418 .economy_mut()
419 .natural_resources
420 .chunks_per_resource[g.good.try_into().unwrap_or_default()] = g.amount;
421 settlement
422 .economy_mut()
423 .natural_resources
424 .average_yield_per_chunk[g.good.try_into().unwrap_or_default()] = 1.0;
425 }
426 let id = index.sites.insert(settlement);
427 names.as_mut().map(|map| map.insert(id, i.name.clone()));
428 }
429 for (id, econ) in econ_testinput.iter().enumerate() {
432 if let Some(id) = index.sites.recreate_id(id as u64) {
433 for nid in econ.neighbors.iter() {
434 if let Some(nid) = index.sites.recreate_id(*nid) {
435 let town = index.sites.get_mut(id).economy_mut();
436 town.add_neighbor(nid, 0);
437 }
438 }
439 }
440 }
441 }
442 crate::sim2::simulate(&mut index, &mut sim);
443 show_economy(&index.sites, &names);
444 });
445 }
446
447 struct Simenv {
448 index: crate::index::Index,
449 sim: sim::WorldSim,
450 rng: ChaChaRng,
451 targets: HashMap<Id<crate::site::Site>, f32>,
452 names: HashMap<Id<crate::site::Site>, String>,
453 }
454
455 #[test]
456 fn test_economy_moderate_standalone() {
458 fn add_settlement(
459 env: &mut Simenv,
460 name: &str,
461 target: f32,
462 resources: &[(Good, f32)],
463 ) -> Id<crate::site::Site> {
464 let wpos = Vec2 { x: 42, y: 42 };
465 let mut meta = crate::site::SitesGenMeta::new(env.rng.random());
466 let mut settlement = crate::site::Site::generate_city(
467 &crate::Land::from_sim(&env.sim),
468 crate::IndexRef {
469 colors: &env.index.colors(),
470 features: &env.index.features(),
471 index: &env.index,
472 },
473 &mut env.rng,
474 wpos,
475 1.0,
476 None,
477 &mut meta,
478 );
479 for (good, amount) in resources.iter() {
480 settlement
481 .economy_mut()
482 .natural_resources
483 .chunks_per_resource[(*good).try_into().unwrap_or_default()] = *amount;
484 settlement
485 .economy_mut()
486 .natural_resources
487 .average_yield_per_chunk[(*good).try_into().unwrap_or_default()] = 1.0;
488 }
489 let id = env.index.sites.insert(settlement);
490 env.targets.insert(id, target);
491 env.names.insert(id, name.into());
492 id
493 }
494
495 execute_with_tracing(Level::ERROR, || {
496 let threadpool = rayon::ThreadPoolBuilder::new().build().unwrap();
497 info!("init");
498 let seed = sim::DEFAULT_WORLD_SEED;
499 let opts = sim::WorldOpts {
500 seed_elements: true,
501 world_file: sim::FileOpts::LoadAsset(sim::DEFAULT_WORLD_MAP.into()),
502 calendar: Default::default(),
503 };
504 let index = crate::index::Index::new(seed);
505 info!("Index created");
506 let sim = sim::WorldSim::generate(seed, opts, &threadpool, &|_| {});
507 info!("World loaded");
508 let rng = ChaChaRng::from_seed(seed_expan::rng_state(seed));
509 let mut env = Simenv {
510 index,
511 sim,
512 rng,
513 targets: HashMap::new(),
514 names: HashMap::new(),
515 };
516 add_settlement(&mut env, "Forest", 5000.0, &[(
517 Good::Terrain(BiomeKind::Forest),
518 100.0_f32,
519 )]);
520 add_settlement(&mut env, "Grass", 700.0, &[(
521 Good::Terrain(BiomeKind::Grassland),
522 100.0_f32,
523 )]);
524 add_settlement(&mut env, "Mountain", 3.0, &[(
525 Good::Terrain(BiomeKind::Mountain),
526 100.0_f32,
527 )]);
528 add_settlement(&mut env, "GrFoMo", 12000.0, &[
539 (Good::Terrain(BiomeKind::Grassland), 100.0_f32),
540 (Good::Terrain(BiomeKind::Forest), 100.0_f32),
541 (Good::Terrain(BiomeKind::Mountain), 10.0_f32),
542 ]);
543 for i in 1..(env.index.sites.ids().count() as u64 - 1) {
549 let previous = env.index.sites.recreate_id(i - 1);
550 let center = env.index.sites.recreate_id(i);
551 center.zip(previous).map(|(center, previous)| {
552 env.index.sites[center]
553 .economy_mut()
554 .add_neighbor(previous, i as usize);
555 env.index.sites[previous]
556 .economy_mut()
557 .add_neighbor(center, i as usize);
558 });
559 }
560 crate::sim2::simulate(&mut env.index, &mut env.sim);
561 show_economy(&env.index.sites, &Some(env.names));
562 for (id, site) in env.index.sites.iter() {
564 if let Some(econ) = site.economy.as_ref() {
565 assert!(econ.pop >= env.targets[&id]);
566 }
567 }
568 });
569 }
570}