veloren_world/site/economy/
context.rs

1/// this contains global housekeeping info during simulation
2use 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
9// this is an empty replacement for https://github.com/cpetig/vergleich
10// which can be used to compare values acros runs
11// pub mod vergleich {
12//     pub struct Error {}
13//     impl Error {
14//         pub fn to_string(&self) -> &'static str { "" }
15//     }
16//     pub struct ProgramRun {}
17//     impl ProgramRun {
18//         pub fn new(_: &str) -> Result<Self, Error> { Ok(Self {}) }
19
20//         pub fn set_epsilon(&mut self, _: f32) {}
21
22//         pub fn context(&mut self, _: &str) -> Context { Context {} }
23
24//         //pub fn value(&mut self, _: &str, val: f32) -> f32 { val }
25//     }
26//     pub struct Context {}
27//     impl Context {
28//         #[must_use]
29//         pub fn context(&mut self, _: &str) -> Context { Context {} }
30
31//         pub fn value(&mut self, _: &str, val: f32) -> f32 { val }
32
33//         pub fn dummy() -> Self { Context {} }
34//     }
35// }
36
37const TICK_PERIOD: f32 = 3.0 * DAYS_PER_MONTH; // 3 months
38const HISTORY_DAYS: f32 = 500.0 * DAYS_PER_YEAR; // 500 years
39
40/// Statistics collector (min, max, avg)
41#[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    // context: vergleich::ProgramRun,
82}
83
84impl Environment {
85    pub fn new() -> Result<Self, std::io::Error> {
86        // let mut context = vergleich::ProgramRun::new("economy_compare.sqlite")
87        //     .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other,
88        // e.to_string()))?; context.set_epsilon(0.1);
89        let csv_file = Economy::csv_open();
90        Ok(Self {
91            csv_file, /* context */
92        })
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    //    csv_footer(f, index);
169
170    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
178// fn check_money(index: &Index) {
179//     let mut sum_stock: f32 = 0.0;
180//     for site in index.sites.values() {
181//         sum_stock += site.economy.stocks[*COIN_INDEX];
182//     }
183//     let mut sum_del: f32 = 0.0;
184//     for v in index.trade.deliveries.values() {
185//         for del in v.iter() {
186//             sum_del += del.amount[*COIN_INDEX];
187//         }
188//     }
189//     info!(
190//         "Coin amount {} + {} = {}",
191//         sum_stock,
192//         sum_del,
193//         sum_stock + sum_del
194//     );
195// }
196
197fn tick(index: &mut Index, dt: f32, _env: &mut Environment) {
198    if INTER_SITE_TRADE {
199        // move deliverables to recipient cities
200        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            // helpful for debugging but not compatible with parallel execution
213            // vc.context(&site_id.id().to_string()));
214        }
215    });
216    if INTER_SITE_TRADE {
217        // distribute orders (travelling merchants)
218        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        // trade at sites
224        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    //check_money(index);
234
235    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>, // id
279        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    /// output the economy of the currently active world
300    // this expensive test is for manual inspection, not to be run automated
301    // recommended command: cargo test test_economy0 -- --nocapture --ignored
302    #[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                //sim::FileOpts::LoadAsset("world.map.economy_8x8".into()),
313                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    /// output the economy of a small set of villages, loaded from ron
327    // this cheaper test is for manual inspection, not to be run automated
328    #[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                //sim::FileOpts::LoadAsset("world.map.economy_8x8".into()),
339                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                    // this should be a moderate compromise between regenerating the full world and
393                    // loading on demand using the public API. There is no way to set
394                    // the name, do we care?
395                    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                        //let c = sim::SimChunk::new();
415                        //settlement.economy.add_chunk(ch, distance_squared)
416                        // bypass the API for now
417                        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                // we can't add these in the first loop as neighbors will refer to later sites
430                // (which aren't valid in the first loop)
431                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    /// test whether a site in moderate climate can survive on its own
457    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, "Desert", 19.0, &[(
529            //     Good::Terrain(BiomeKind::Desert),
530            //     100.0_f32,
531            // )]);
532            // add_settlement(&mut index, &mut rng, &[
533            //     (Good::Terrain(BiomeKind::Jungle), 100.0_f32),
534            // ]);
535            // add_settlement(&mut index, &mut rng, &[
536            //     (Good::Terrain(BiomeKind::Snowland), 100.0_f32),
537            // ]);
538            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            // add_settlement(&mut env, "Mountain", 19.0, &[
544            //     (Good::Terrain(BiomeKind::Mountain), 100.0_f32),
545            //     // (Good::CaveAccess, 100.0_f32),
546            // ]);
547            // connect to neighbors (one way)
548            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            // check population (shrinks if economy gets broken)
563            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}