veloren_world/site/economy/
context.rs

1/// this contains global housekeeping info during simulation
2use 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
12// this is an empty replacement for https://github.com/cpetig/vergleich
13// which can be used to compare values acros runs
14// pub mod vergleich {
15//     pub struct Error {}
16//     impl Error {
17//         pub fn to_string(&self) -> &'static str { "" }
18//     }
19//     pub struct ProgramRun {}
20//     impl ProgramRun {
21//         pub fn new(_: &str) -> Result<Self, Error> { Ok(Self {}) }
22
23//         pub fn set_epsilon(&mut self, _: f32) {}
24
25//         pub fn context(&mut self, _: &str) -> Context { Context {} }
26
27//         //pub fn value(&mut self, _: &str, val: f32) -> f32 { val }
28//     }
29//     pub struct Context {}
30//     impl Context {
31//         #[must_use]
32//         pub fn context(&mut self, _: &str) -> Context { Context {} }
33
34//         pub fn value(&mut self, _: &str, val: f32) -> f32 { val }
35
36//         pub fn dummy() -> Self { Context {} }
37//     }
38// }
39
40const TICK_PERIOD: f32 = 3.0 * DAYS_PER_MONTH; // 3 months
41const HISTORY_DAYS: f32 = 500.0 * DAYS_PER_YEAR; // 500 years
42
43/// Statistics collector (min, max, avg)
44#[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    // context: vergleich::ProgramRun,
85}
86
87impl Environment {
88    pub fn new() -> Result<Self, std::io::Error> {
89        // let mut context = vergleich::ProgramRun::new("economy_compare.sqlite")
90        //     .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other,
91        // e.to_string()))?; context.set_epsilon(0.1);
92        let csv_file = Economy::csv_open();
93        Ok(Self {
94            csv_file, /* context */
95        })
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    //    csv_footer(f, index);
220
221    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
229// fn check_money(index: &Index) {
230//     let mut sum_stock: f32 = 0.0;
231//     for site in index.sites.values() {
232//         sum_stock += site.economy.stocks[*COIN_INDEX];
233//     }
234//     let mut sum_del: f32 = 0.0;
235//     for v in index.trade.deliveries.values() {
236//         for del in v.iter() {
237//             sum_del += del.amount[*COIN_INDEX];
238//         }
239//     }
240//     info!(
241//         "Coin amount {} + {} = {}",
242//         sum_stock,
243//         sum_del,
244//         sum_stock + sum_del
245//     );
246// }
247
248fn tick(index: &mut Index, dt: f32, _env: &mut Environment) {
249    if INTER_SITE_TRADE {
250        // move deliverables to recipient cities
251        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            // helpful for debugging but not compatible with parallel execution
259            // vc.context(&site_id.id().to_string()));
260        }
261    });
262    if INTER_SITE_TRADE {
263        // distribute orders (travelling merchants)
264        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        // trade at sites
270        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    //check_money(index);
280
281    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>, // id
325        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    /// output the economy of the currently active world
342    // this expensive test is for manual inspection, not to be run automated
343    // recommended command: cargo test test_economy0 -- --nocapture --ignored
344    #[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                //sim::FileOpts::LoadAsset("world.map.economy_8x8".into()),
355                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    /// output the economy of a small set of villages, loaded from ron
369    // this cheaper test is for manual inspection, not to be run automated
370    #[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                //sim::FileOpts::LoadAsset("world.map.economy_8x8".into()),
381                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                    // this should be a moderate compromise between regenerating the full world and
431                    // loading on demand using the public API. There is no way to set
432                    // the name, do we care?
433                    let mut settlement = match i.kind {
434                        SiteKindMeta::Castle => crate::site::Site::castle(
435                            crate::site::Castle::generate(wpos, None, &mut rng),
436                        ),
437                        // common::terrain::site::SitesKind::Settlement |
438                        _ => crate::site::Site::settlement(crate::site::Settlement::generate(
439                            wpos, None, &mut rng,
440                        )),
441                    };
442                    for g in i.resources.iter() {
443                        //let c = sim::SimChunk::new();
444                        //settlement.economy.add_chunk(ch, distance_squared)
445                        // bypass the API for now
446                        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                // we can't add these in the first loop as neighbors will refer to later sites
455                // (which aren't valid in the first loop)
456                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    /// test whether a site in moderate climate can survive on its own
481    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, "Desert", 19.0, &[(
539            //     Good::Terrain(BiomeKind::Desert),
540            //     100.0_f32,
541            // )]);
542            // add_settlement(&mut index, &mut rng, &[
543            //     (Good::Terrain(BiomeKind::Jungle), 100.0_f32),
544            // ]);
545            // add_settlement(&mut index, &mut rng, &[
546            //     (Good::Terrain(BiomeKind::Snowland), 100.0_f32),
547            // ]);
548            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            // add_settlement(&mut env, "Mountain", 19.0, &[
554            //     (Good::Terrain(BiomeKind::Mountain), 100.0_f32),
555            //     // (Good::CaveAccess, 100.0_f32),
556            // ]);
557            // connect to neighbors (one way)
558            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            // check population (shrinks if economy gets broken)
573            for (id, site) in env.index.sites.iter() {
574                assert!(site.economy.pop >= env.targets[&id]);
575            }
576        });
577    }
578}