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            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    //    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.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    /// output the economy of the currently active world
298    // this expensive test is for manual inspection, not to be run automated
299    // recommended command: cargo test test_economy0 -- --nocapture --ignored
300    #[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                //sim::FileOpts::LoadAsset("world.map.economy_8x8".into()),
311                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    /// output the economy of a small set of villages, loaded from ron
325    // this cheaper test is for manual inspection, not to be run automated
326    #[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                //sim::FileOpts::LoadAsset("world.map.economy_8x8".into()),
337                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                    // this should be a moderate compromise between regenerating the full world and
391                    // loading on demand using the public API. There is no way to set
392                    // the name, do we care?
393                    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                        //let c = sim::SimChunk::new();
413                        //settlement.economy.add_chunk(ch, distance_squared)
414                        // bypass the API for now
415                        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                // we can't add these in the first loop as neighbors will refer to later sites
428                // (which aren't valid in the first loop)
429                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    /// test whether a site in moderate climate can survive on its own
455    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, "Desert", 19.0, &[(
527            //     Good::Terrain(BiomeKind::Desert),
528            //     100.0_f32,
529            // )]);
530            // add_settlement(&mut index, &mut rng, &[
531            //     (Good::Terrain(BiomeKind::Jungle), 100.0_f32),
532            // ]);
533            // add_settlement(&mut index, &mut rng, &[
534            //     (Good::Terrain(BiomeKind::Snowland), 100.0_f32),
535            // ]);
536            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            // add_settlement(&mut env, "Mountain", 19.0, &[
542            //     (Good::Terrain(BiomeKind::Mountain), 100.0_f32),
543            //     // (Good::CaveAccess, 100.0_f32),
544            // ]);
545            // connect to neighbors (one way)
546            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            // check population (shrinks if economy gets broken)
561            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}