veloren_world/site/economy/
mod.rs

1/// This file contains a single economy
2/// and functions to simulate it
3use crate::world_msg::EconomyInfo;
4use crate::{
5    sim::SimChunk,
6    site::Site,
7    util::{DHashMap, DHashSet, map_array::GenericIndex},
8};
9use common::{
10    store::Id,
11    terrain::BiomeKind,
12    trade::{Good, SitePrices},
13};
14use hashbrown::HashMap;
15use lazy_static::lazy_static;
16use std::{cmp::Ordering::Less, convert::TryFrom};
17use tracing::{debug, info, trace, warn};
18
19use Good::*;
20mod map_types;
21pub use map_types::Labor;
22use map_types::{GoodIndex, GoodMap, LaborIndex, LaborMap, NaturalResources};
23mod context;
24pub use context::simulate_economy;
25mod cache;
26
27const INTER_SITE_TRADE: bool = true;
28const DAYS_PER_MONTH: f32 = 30.0;
29const DAYS_PER_YEAR: f32 = 12.0 * DAYS_PER_MONTH;
30const GENERATE_CSV: bool = false;
31
32#[derive(Debug)]
33pub struct TradeOrder {
34    customer: Id<Site>,
35    amount: GoodMap<f32>, // positive for orders, negative for exchange
36}
37
38#[derive(Debug)]
39pub struct TradeDelivery {
40    supplier: Id<Site>,
41    amount: GoodMap<f32>, // positive for orders, negative for exchange
42    prices: GoodMap<f32>, // at the time of interaction
43    supply: GoodMap<f32>, // maximum amount available, at the time of interaction
44}
45
46#[derive(Debug, Default)]
47pub struct TradeInformation {
48    orders: DHashMap<Id<Site>, Vec<TradeOrder>>, // per provider
49    deliveries: DHashMap<Id<Site>, Vec<TradeDelivery>>, // per receiver
50}
51
52#[derive(Debug)]
53pub struct NeighborInformation {
54    id: Id<Site>,
55    //travel_distance: usize,
56
57    // remembered from last interaction
58    last_values: GoodMap<f32>,
59    last_supplies: GoodMap<f32>,
60}
61
62lazy_static! {
63    static ref COIN_INDEX: GoodIndex = Coin.try_into().unwrap_or_default();
64    static ref FOOD_INDEX: GoodIndex = Good::Food.try_into().unwrap_or_default();
65    static ref TRANSPORTATION_INDEX: GoodIndex = Transportation.try_into().unwrap_or_default();
66}
67
68#[derive(Debug)]
69pub struct Economy {
70    /// Population
71    pop: f32,
72    population_limited_by: GoodIndex,
73
74    /// Total available amount of each good
75    stocks: GoodMap<f32>,
76    /// Surplus stock compared to demand orders
77    surplus: GoodMap<f32>,
78    /// change rate (derivative) of stock in the current situation
79    marginal_surplus: GoodMap<f32>,
80    /// amount of wares not needed by the economy (helps with trade planning)
81    unconsumed_stock: GoodMap<f32>,
82    /// Local availability of a good, 4.0 = starved, 2.0 = balanced, 0.1 =
83    /// extra, NULL = way too much
84    // For some goods, such a goods without any supply, it doesn't make sense to talk about value
85    values: GoodMap<Option<f32>>,
86    /// amount of goods exported/imported during the last cycle
87    last_exports: GoodMap<f32>,
88    active_exports: GoodMap<f32>, // unfinished trade (amount unconfirmed)
89    //pub export_targets: GoodMap<f32>,
90    /// amount of labor that went into a good, [1 man cycle=1.0]
91    labor_values: GoodMap<Option<f32>>,
92    // this assumes a single source, replace with LaborMap?
93    material_costs: GoodMap<f32>,
94
95    /// Proportion of individuals dedicated to an industry (sums to roughly 1.0)
96    labors: LaborMap<f32>,
97    // Per worker, per year, of their output good
98    yields: LaborMap<f32>,
99    /// [0.0..1.0]
100    productivity: LaborMap<f32>,
101    /// Missing raw material which limits production
102    limited_by: LaborMap<GoodIndex>,
103
104    natural_resources: NaturalResources,
105    /// Neighboring sites to trade with
106    neighbors: Vec<NeighborInformation>,
107
108    /// outgoing trade, per provider
109    orders: DHashMap<Id<Site>, Vec<TradeOrder>>,
110    /// incoming trade - only towards this site
111    deliveries: Vec<TradeDelivery>,
112}
113
114impl Default for Economy {
115    fn default() -> Self {
116        let coin_index: GoodIndex = GoodIndex::try_from(Coin).unwrap_or_default();
117        Self {
118            pop: 32.0,
119            population_limited_by: GoodIndex::default(),
120
121            stocks: GoodMap::from_list(&[(coin_index, Economy::STARTING_COIN)], 100.0),
122            surplus: Default::default(),
123            marginal_surplus: Default::default(),
124            values: GoodMap::from_list(&[(coin_index, Some(2.0))], None),
125            last_exports: Default::default(),
126            active_exports: Default::default(),
127
128            labor_values: Default::default(),
129            material_costs: Default::default(),
130
131            labors: LaborMap::from_default(0.01),
132            yields: LaborMap::from_default(1.0),
133            productivity: LaborMap::from_default(1.0),
134            limited_by: LaborMap::from_default(GoodIndex::default()),
135
136            natural_resources: Default::default(),
137            neighbors: Default::default(),
138            unconsumed_stock: Default::default(),
139
140            orders: Default::default(),
141            deliveries: Default::default(),
142        }
143    }
144}
145
146impl Economy {
147    const MINIMUM_PRICE: f32 = 0.1;
148    const STARTING_COIN: f32 = 1000.0;
149    const _NATURAL_RESOURCE_SCALE: f32 = 1.0 / 9.0;
150
151    pub fn population(&self) -> f32 { self.pop }
152
153    pub fn get_available_stock(&self) -> HashMap<Good, f32> {
154        self.unconsumed_stock
155            .iter()
156            .map(|(g, a)| (g.into(), *a))
157            .collect()
158    }
159
160    pub fn get_information(&self, id: Id<Site>) -> EconomyInfo {
161        EconomyInfo {
162            id: id.id(),
163            population: self.pop.floor() as u32,
164            stock: self
165                .stocks
166                .iter()
167                .map(|(g, a)| (Good::from(g), *a))
168                .collect(),
169            labor_values: self
170                .labor_values
171                .iter()
172                .filter_map(|(g, a)| a.map(|a| (Good::from(g), a)))
173                .collect(),
174            values: self
175                .values
176                .iter()
177                .filter_map(|(g, a)| a.map(|a| (Good::from(g), a)))
178                .collect(),
179            labors: self.labors.iter().map(|(_, a)| (*a)).collect(),
180            last_exports: self
181                .last_exports
182                .iter()
183                .map(|(g, a)| (Good::from(g), *a))
184                .collect(),
185            resources: self
186                .natural_resources
187                .chunks_per_resource
188                .iter()
189                .map(|(g, a)| {
190                    (
191                        Good::from(g),
192                        (*a) * self.natural_resources.average_yield_per_chunk[g],
193                    )
194                })
195                .collect(),
196        }
197    }
198
199    pub fn cache_economy(&mut self) {
200        for g in good_list() {
201            let amount: f32 = self
202                .natural_resources
203                .per_area
204                .iter()
205                .map(|a| a.resource_sum[g])
206                .sum();
207            let chunks = self
208                .natural_resources
209                .per_area
210                .iter()
211                .map(|a| a.resource_chunks[g])
212                .sum();
213            if chunks > 0.001 {
214                self.natural_resources.chunks_per_resource[g] = chunks;
215                self.natural_resources.average_yield_per_chunk[g] = amount / chunks;
216            }
217        }
218    }
219
220    /// orders per profession (excluding everyone)
221    fn get_orders(&self) -> &'static LaborMap<Vec<(GoodIndex, f32)>> {
222        lazy_static! {
223            static ref ORDERS: LaborMap<Vec<(GoodIndex, f32)>> = {
224                let mut res: LaborMap<Vec<(GoodIndex, f32)>> = LaborMap::default();
225                res.iter_mut()
226                    .for_each(|(i, e)| e.extend(i.orders().copied()));
227                res
228            };
229        }
230        &ORDERS
231    }
232
233    /// resources consumed by everyone (no matter which profession)
234    fn get_orders_everyone(&self) -> impl Iterator<Item = &'static (GoodIndex, f32)> {
235        Labor::orders_everyone()
236    }
237
238    fn get_production(&self) -> LaborMap<(GoodIndex, f32)> {
239        // cache the site independent part of production
240        lazy_static! {
241            static ref PRODUCTS: LaborMap<(GoodIndex, f32)> = LaborMap::from_iter(
242                Labor::list().map(|p| { (p, p.products(),) }),
243                (GoodIndex::default(), 0.0),
244            );
245        }
246        PRODUCTS.map(|l, vec| {
247            //dbg!((l,vec));
248            let labor_ratio = self.labors[l];
249            let total_workers = labor_ratio * self.pop;
250            // apply economy of scale (workers get more productive in numbers)
251            let relative_scale = 1.0 + labor_ratio;
252            let absolute_scale = (1.0 + total_workers / 100.0).min(3.0);
253            let scale = relative_scale * absolute_scale;
254            (vec.0, vec.1 * scale)
255        })
256    }
257
258    fn replenish(&mut self, _time: f32) {
259        for (good, &ch) in self.natural_resources.chunks_per_resource.iter() {
260            let per_year = self.natural_resources.average_yield_per_chunk[good] * ch;
261            self.stocks[good] = self.stocks[good].max(per_year);
262        }
263        // info!("resources {:?}", self.stocks);
264    }
265
266    pub fn add_chunk(&mut self, ch: &SimChunk, distance_squared: i64) {
267        // let biome = ch.get_biome();
268        // we don't scale by pi, although that would be correct
269        let distance_bin = (distance_squared >> 16).min(64) as usize;
270        if self.natural_resources.per_area.len() <= distance_bin {
271            self.natural_resources
272                .per_area
273                .resize_with(distance_bin + 1, Default::default);
274        }
275        self.natural_resources.per_area[distance_bin].chunks += 1;
276
277        let mut add_biome = |biome, amount| {
278            if let Ok(idx) = GoodIndex::try_from(Terrain(biome)) {
279                self.natural_resources.per_area[distance_bin].resource_sum[idx] += amount;
280                self.natural_resources.per_area[distance_bin].resource_chunks[idx] += amount;
281            }
282        };
283        if ch.river.is_ocean() {
284            add_biome(BiomeKind::Ocean, 1.0);
285        } else if ch.river.is_lake() {
286            add_biome(BiomeKind::Lake, 1.0);
287        } else {
288            add_biome(BiomeKind::Forest, 0.5 + ch.tree_density);
289            add_biome(BiomeKind::Grassland, 0.5 + ch.humidity);
290            add_biome(BiomeKind::Jungle, 0.5 + ch.humidity * ch.temp.max(0.0));
291            add_biome(BiomeKind::Mountain, 0.5 + (ch.alt / 4000.0).max(0.0));
292            add_biome(
293                BiomeKind::Desert,
294                0.5 + (1.0 - ch.humidity) * ch.temp.max(0.0),
295            );
296            add_biome(BiomeKind::Snowland, 0.5 + (-ch.temp).max(0.0));
297        }
298    }
299
300    pub fn add_neighbor(&mut self, id: Id<Site>, _distance: usize) {
301        self.neighbors.push(NeighborInformation {
302            id,
303            //travel_distance: distance,
304            last_values: GoodMap::from_default(Economy::MINIMUM_PRICE),
305            last_supplies: Default::default(),
306        });
307    }
308
309    pub fn get_site_prices(&self) -> SitePrices {
310        let normalize = |xs: GoodMap<Option<f32>>| {
311            let sum = xs
312                .iter()
313                .map(|(_, x)| (*x).unwrap_or(0.0))
314                .sum::<f32>()
315                .max(0.001);
316            xs.map(|_, x| Some(x? / sum))
317        };
318
319        SitePrices {
320            values: {
321                let labor_values = normalize(self.labor_values);
322                // Use labor values as prices. Not correct (doesn't care about exchange value)
323                let prices = normalize(self.values).map(|good, value| {
324                    ((labor_values[good].unwrap_or(Economy::MINIMUM_PRICE)
325                        + value.unwrap_or(Economy::MINIMUM_PRICE))
326                        * 0.5)
327                        .max(Economy::MINIMUM_PRICE)
328                });
329                prices.iter().map(|(g, v)| (Good::from(g), *v)).collect()
330            },
331        }
332    }
333
334    /// plan the trading according to missing goods and prices at neighboring
335    /// sites (1st step of trading)
336    // returns wares spent (-) and procured (+)
337    // potential_trade: positive = buy, (negative = sell, unused)
338    fn plan_trade_for_site(
339        // site: &mut Site,
340        &mut self,
341        site_id: &Id<Site>,
342        transportation_capacity: f32,
343        // external_orders: &mut DHashMap<Id<Site>, Vec<TradeOrder>>,
344        potential_trade: &mut GoodMap<f32>,
345    ) -> GoodMap<f32> {
346        // TODO: Do we have some latency of information here (using last years
347        // capacity?)
348        //let total_transport_capacity = self.stocks[Transportation];
349        // TODO: We don't count the capacity per site, but globally (so there might be
350        // some imbalance in dispatch vs collection across sites (e.g. more dispatch
351        // than collection at one while more collection than dispatch at another))
352        // transport capacity works both ways (going there and returning)
353        let mut dispatch_capacity = transportation_capacity;
354        let mut collect_capacity = transportation_capacity;
355        let mut missing_dispatch: f32 = 0.0;
356        let mut missing_collect: f32 = 0.0;
357        let mut result = GoodMap::default();
358        const MIN_SELL_PRICE: f32 = 1.0;
359        // value+amount per good
360        let mut missing_goods: Vec<(GoodIndex, (f32, f32))> = self
361            .surplus
362            .iter()
363            .filter(|(g, a)| (**a < 0.0 && *g != *TRANSPORTATION_INDEX))
364            .map(|(g, a)| (g, (self.values[g].unwrap_or(Economy::MINIMUM_PRICE), -*a)))
365            .collect();
366        missing_goods.sort_by(|a, b| b.1.0.partial_cmp(&a.1.0).unwrap_or(Less));
367        let mut extra_goods: GoodMap<f32> = GoodMap::from_iter(
368            self.surplus
369                .iter()
370                .chain(core::iter::once((*COIN_INDEX, &self.stocks[*COIN_INDEX])))
371                .filter(|(g, a)| (**a > 0.0 && *g != *TRANSPORTATION_INDEX))
372                .map(|(g, a)| (g, *a)),
373            0.0,
374        );
375        // ratio+price per good and site
376        type GoodRatioPrice = Vec<(GoodIndex, (f32, f32))>;
377        let good_payment: DHashMap<Id<Site>, GoodRatioPrice> = self
378            .neighbors
379            .iter()
380            .map(|n| {
381                let mut rel_value = extra_goods
382                    .iter()
383                    .map(|(g, _)| (g, n.last_values[g]))
384                    .filter(|(_, last_val)| *last_val >= MIN_SELL_PRICE)
385                    .map(|(g, last_val)| {
386                        (
387                            g,
388                            (
389                                last_val
390                                    / self.values[g].unwrap_or(-1.0).max(Economy::MINIMUM_PRICE),
391                                last_val,
392                            ),
393                        )
394                    })
395                    .collect::<Vec<_>>();
396                rel_value.sort_by(|a, b| b.1.0.partial_cmp(&a.1.0).unwrap_or(Less));
397                (n.id, rel_value)
398            })
399            .collect();
400        // price+stock per site and good
401        type SitePriceStock = Vec<(Id<Site>, (f32, f32))>;
402        let mut good_price: DHashMap<GoodIndex, SitePriceStock> = missing_goods
403            .iter()
404            .map(|(g, _)| {
405                (*g, {
406                    let mut neighbor_prices: Vec<(Id<Site>, (f32, f32))> = self
407                        .neighbors
408                        .iter()
409                        .filter(|n| n.last_supplies[*g] > 0.0)
410                        .map(|n| (n.id, (n.last_values[*g], n.last_supplies[*g])))
411                        .collect();
412                    neighbor_prices.sort_by(|a, b| a.1.0.partial_cmp(&b.1.0).unwrap_or(Less));
413                    neighbor_prices
414                })
415            })
416            .collect();
417        // TODO: we need to introduce priority (according to available transportation
418        // capacity)
419        let mut neighbor_orders: DHashMap<Id<Site>, GoodMap<f32>> = self
420            .neighbors
421            .iter()
422            .map(|n| (n.id, GoodMap::default()))
423            .collect();
424        if site_id.id() == 1 {
425            // cut down number of lines printed
426            trace!(
427                "Site {} #neighbors {} Transport capacity {}",
428                site_id.id(),
429                self.neighbors.len(),
430                transportation_capacity,
431            );
432            trace!("missing {:#?} extra {:#?}", missing_goods, extra_goods,);
433            trace!("buy {:#?} pay {:#?}", good_price, good_payment);
434        }
435        // === the actual planning is here ===
436        for (g, (_, a)) in missing_goods.iter() {
437            let mut amount = *a;
438            if let Some(site_price_stock) = good_price.get_mut(g) {
439                for (s, (price, supply)) in site_price_stock.iter_mut() {
440                    // how much to buy, limit by supply and transport budget
441                    let mut buy_target = amount.min(*supply);
442                    let effort = transportation_effort(*g);
443                    let collect = buy_target * effort;
444                    let mut potential_balance: f32 = 0.0;
445                    if collect > collect_capacity && effort > 0.0 {
446                        let transportable_amount = collect_capacity / effort;
447                        let missing_trade = buy_target - transportable_amount;
448                        potential_trade[*g] += missing_trade;
449                        potential_balance += missing_trade * *price;
450                        buy_target = transportable_amount; // (buy_target - missing_trade).max(0.0); // avoid negative buy target caused by numeric inaccuracies
451                        missing_collect += collect - collect_capacity;
452                        trace!(
453                            "missing capacity {:?}/{:?} {:?}",
454                            missing_trade, amount, potential_balance,
455                        );
456                        amount = (amount - missing_trade).max(0.0); // you won't be able to transport it from elsewhere either, so don't count multiple times
457                    }
458                    let mut balance: f32 = *price * buy_target;
459                    trace!(
460                        "buy {:?} at {:?} amount {:?} balance {:?}",
461                        *g,
462                        s.id(),
463                        buy_target,
464                        balance,
465                    );
466                    if let Some(neighbor_orders) = neighbor_orders.get_mut(s) {
467                        // find suitable goods in exchange
468                        let mut acute_missing_dispatch: f32 = 0.0; // only count the highest priority (not multiple times)
469                        for (g2, (_, price2)) in good_payment[s].iter() {
470                            let mut amount2 = extra_goods[*g2];
471                            // good available for trading?
472                            if amount2 > 0.0 {
473                                amount2 = amount2.min(balance / price2); // pay until balance is even
474                                let effort2 = transportation_effort(*g2);
475                                let mut dispatch = amount2 * effort2;
476                                // limit by separate transport budget (on way back)
477                                if dispatch > dispatch_capacity && effort2 > 0.0 {
478                                    let transportable_amount = dispatch_capacity / effort2;
479                                    let missing_trade = amount2 - transportable_amount;
480                                    amount2 = transportable_amount;
481                                    if acute_missing_dispatch == 0.0 {
482                                        acute_missing_dispatch = missing_trade * effort2;
483                                    }
484                                    trace!(
485                                        "can't carry payment {:?} {:?} {:?}",
486                                        g2, dispatch, dispatch_capacity
487                                    );
488                                    dispatch = dispatch_capacity;
489                                }
490
491                                extra_goods[*g2] -= amount2;
492                                trace!("pay {:?} {:?} = {:?}", g2, amount2, balance);
493                                balance -= amount2 * price2;
494                                neighbor_orders[*g2] -= amount2;
495                                dispatch_capacity = (dispatch_capacity - dispatch).max(0.0);
496                                if balance == 0.0 {
497                                    break;
498                                }
499                            }
500                        }
501                        missing_dispatch += acute_missing_dispatch;
502                        // adjust order if we are unable to pay for it
503                        buy_target -= balance / *price;
504                        buy_target = buy_target.min(amount);
505                        collect_capacity = (collect_capacity - buy_target * effort).max(0.0);
506                        neighbor_orders[*g] += buy_target;
507                        amount -= buy_target;
508                        trace!(
509                            "deal amount {:?} end_balance {:?} price {:?} left {:?}",
510                            buy_target, balance, *price, amount
511                        );
512                    }
513                }
514            }
515        }
516        // if site_id.id() == 1 {
517        //     // cut down number of lines printed
518        //     info!("orders {:#?}", neighbor_orders,);
519        // }
520        // TODO: Use planned orders and calculate value, stock etc. accordingly
521        for n in &self.neighbors {
522            if let Some(orders) = neighbor_orders.get(&n.id) {
523                for (g, a) in orders.iter() {
524                    result[g] += *a;
525                }
526                let to = TradeOrder {
527                    customer: *site_id,
528                    amount: *orders,
529                };
530                if let Some(o) = self.orders.get_mut(&n.id) {
531                    // this is just to catch unbound growth (happened in development)
532                    if o.len() < 100 {
533                        o.push(to);
534                    } else {
535                        warn!("overflow {:?}", o);
536                    }
537                } else {
538                    self.orders.insert(n.id, vec![to]);
539                }
540            }
541        }
542        // return missing transport capacity
543        //missing_collect.max(missing_dispatch)
544        trace!(
545            "Tranportation {:?} {:?} {:?} {:?} {:?}",
546            transportation_capacity,
547            collect_capacity,
548            dispatch_capacity,
549            missing_collect,
550            missing_dispatch,
551        );
552        result[*TRANSPORTATION_INDEX] = -(transportation_capacity
553            - collect_capacity.min(dispatch_capacity)
554            + missing_collect.max(missing_dispatch));
555        if site_id.id() == 1 {
556            trace!("Trade {:?}", result);
557        }
558        result
559    }
560
561    /// perform trade using neighboring orders (2nd step of trading)
562    pub fn trade_at_site(
563        &mut self,
564        site_id: Id<Site>,
565        orders: &mut Vec<TradeOrder>,
566        // economy: &mut Economy,
567        deliveries: &mut DHashMap<Id<Site>, Vec<TradeDelivery>>,
568    ) {
569        // make sure that at least this amount of stock remains available
570        // TODO: rework using economy.unconsumed_stock
571
572        let internal_orders = self.get_orders();
573        let mut next_demand = GoodMap::from_default(0.0);
574        for (labor, orders) in internal_orders.iter() {
575            let workers = self.labors[labor] * self.pop;
576            for (good, amount) in orders {
577                next_demand[*good] += *amount * workers;
578                assert!(next_demand[*good] >= 0.0);
579            }
580        }
581        for (good, amount) in self.get_orders_everyone() {
582            next_demand[*good] += *amount * self.pop;
583            assert!(next_demand[*good] >= 0.0);
584        }
585        //info!("Trade {} {}", site.id(), orders.len());
586        let mut total_orders: GoodMap<f32> = GoodMap::from_default(0.0);
587        for i in orders.iter() {
588            for (g, &a) in i.amount.iter().filter(|(_, a)| **a > 0.0) {
589                total_orders[g] += a;
590            }
591        }
592        let order_stock_ratio: GoodMap<Option<f32>> = GoodMap::from_iter(
593            self.stocks
594                .iter()
595                .map(|(g, a)| (g, *a, next_demand[g]))
596                .filter(|(_, a, s)| *a > *s)
597                .map(|(g, a, s)| (g, Some(total_orders[g] / (a - s)))),
598            None,
599        );
600        trace!("trade {} {:?}", site_id.id(), order_stock_ratio);
601        let prices = GoodMap::from_iter(
602            self.values
603                .iter()
604                .map(|(g, o)| (g, o.unwrap_or(0.0).max(Economy::MINIMUM_PRICE))),
605            0.0,
606        );
607        for o in orders.drain(..) {
608            // amount, local value (sell low value, buy high value goods first (trading
609            // town's interest))
610            let mut sorted_sell: Vec<(GoodIndex, f32, f32)> = o
611                .amount
612                .iter()
613                .filter(|(_, &a)| a > 0.0)
614                .map(|(g, a)| (g, *a, prices[g]))
615                .collect();
616            sorted_sell.sort_by(|a, b| (a.2.partial_cmp(&b.2).unwrap_or(Less)));
617            let mut sorted_buy: Vec<(GoodIndex, f32, f32)> = o
618                .amount
619                .iter()
620                .filter(|(_, &a)| a < 0.0)
621                .map(|(g, a)| (g, *a, prices[g]))
622                .collect();
623            sorted_buy.sort_by(|a, b| (b.2.partial_cmp(&a.2).unwrap_or(Less)));
624            trace!(
625                "with {} {:?} buy {:?}",
626                o.customer.id(),
627                sorted_sell,
628                sorted_buy
629            );
630            let mut good_delivery = GoodMap::from_default(0.0);
631            for (g, amount, price) in sorted_sell.iter() {
632                if let Some(order_stock_ratio) = order_stock_ratio[*g] {
633                    let allocated_amount = *amount / order_stock_ratio.max(1.0);
634                    let mut balance = allocated_amount * *price;
635                    for (g2, avail, price2) in sorted_buy.iter_mut() {
636                        let amount2 = (-*avail).min(balance / *price2);
637                        assert!(amount2 >= 0.0);
638                        self.stocks[*g2] += amount2;
639                        balance = (balance - amount2 * *price2).max(0.0);
640                        *avail += amount2; // reduce (negative) brought stock
641                        trace!("paid with {:?} {} {}", *g2, amount2, *price2);
642                        if balance == 0.0 {
643                            break;
644                        }
645                    }
646                    let mut paid_amount =
647                        (allocated_amount - balance / *price).min(self.stocks[*g]);
648                    if paid_amount / allocated_amount < 0.95 {
649                        trace!(
650                            "Client {} is broke on {:?} : {} {} severity {}",
651                            o.customer.id(),
652                            *g,
653                            paid_amount,
654                            allocated_amount,
655                            order_stock_ratio,
656                        );
657                    } else {
658                        trace!("bought {:?} {} {}", *g, paid_amount, *price);
659                    }
660                    if self.stocks[*g] - paid_amount < 0.0 {
661                        info!(
662                            "BUG {:?} {:?} {} TO {:?} OSR {:?} ND {:?}",
663                            self.stocks[*g],
664                            *g,
665                            paid_amount,
666                            total_orders[*g],
667                            order_stock_ratio,
668                            next_demand[*g]
669                        );
670                        paid_amount = self.stocks[*g];
671                    }
672                    good_delivery[*g] += paid_amount;
673                    self.stocks[*g] -= paid_amount;
674                }
675            }
676            for (g, amount, _) in sorted_buy.drain(..) {
677                if amount < 0.0 {
678                    trace!("shipping back unsold {} of {:?}", amount, g);
679                    good_delivery[g] += -amount;
680                }
681            }
682            let delivery = TradeDelivery {
683                supplier: site_id,
684                prices,
685                supply: GoodMap::from_iter(
686                    self.stocks.iter().map(|(g, a)| {
687                        (g, {
688                            (a - next_demand[g] - total_orders[g]).max(0.0) + good_delivery[g]
689                        })
690                    }),
691                    0.0,
692                ),
693                amount: good_delivery,
694            };
695            trace!(?delivery);
696            if let Some(deliveries) = deliveries.get_mut(&o.customer) {
697                deliveries.push(delivery);
698            } else {
699                deliveries.insert(o.customer, vec![delivery]);
700            }
701        }
702        if !orders.is_empty() {
703            info!("non empty orders {:?}", orders);
704            orders.clear();
705        }
706    }
707
708    /// 3rd step of trading
709    fn collect_deliveries(
710        // site: &mut Site,
711        &mut self,
712        // deliveries: &mut Vec<TradeDelivery>,
713        // ctx: &mut vergleich::Context,
714    ) {
715        // collect all the goods we shipped
716        let mut last_exports = GoodMap::from_iter(
717            self.active_exports
718                .iter()
719                .filter(|(_g, a)| **a > 0.0)
720                .map(|(g, a)| (g, *a)),
721            0.0,
722        );
723        // TODO: properly rate benefits created by merchants (done below?)
724        for mut d in self.deliveries.drain(..) {
725            // let mut ictx = ctx.context(&format!("suppl {}", d.supplier.id()));
726            for i in d.amount.iter() {
727                last_exports[i.0] -= *i.1;
728            }
729            // remember price
730            if let Some(n) = self.neighbors.iter_mut().find(|n| n.id == d.supplier) {
731                // remember (and consume) last values
732                std::mem::swap(&mut n.last_values, &mut d.prices);
733                std::mem::swap(&mut n.last_supplies, &mut d.supply);
734                // add items to stock
735                for (g, a) in d.amount.iter() {
736                    if *a < 0.0 {
737                        // likely rounding error, ignore
738                        trace!("Unexpected delivery for {:?} {}", g, *a);
739                    } else {
740                        self.stocks[g] += *a;
741                    }
742                }
743            }
744        }
745        if !self.deliveries.is_empty() {
746            info!("non empty deliveries {:?}", self.deliveries);
747            self.deliveries.clear();
748        }
749        std::mem::swap(&mut last_exports, &mut self.last_exports);
750        //self.active_exports.clear();
751    }
752
753    /// Simulate one step of economic interaction:
754    /// - collect returned goods from trade
755    /// - calculate demand, production and their ratio
756    /// - reassign workers based on missing goods
757    /// - change stock due to raw material use and production
758    /// - send out traders with goods and orders
759    /// - calculate good decay and population change
760    ///
761    /// Simulate a site's economy. This simulation is roughly equivalent to the
762    /// Lange-Lerner model's solution to the socialist calculation problem. The
763    /// simulation begins by assigning arbitrary values to each commodity and
764    /// then incrementally updates them according to the final scarcity of
765    /// the commodity at the end of the tick. This results in the
766    /// formulation of values that are roughly analogous to prices for each
767    /// commodity. The workforce is then reassigned according to the
768    /// respective commodity values. The simulation also includes damping
769    /// terms that prevent cyclical inconsistencies in value rationalisation
770    /// magnifying enough to crash the economy. We also ensure that
771    /// a small number of workers are allocated to every industry (even inactive
772    /// ones) each tick. This is not an accident: a small amount of productive
773    /// capacity in one industry allows the economy to quickly pivot to a
774    /// different production configuration should an additional commodity
775    /// that acts as production input become available. This means that the
776    /// economy will dynamically react to environmental changes. If a
777    /// product becomes available through a mechanism such as trade, an
778    /// entire arm of the economy may materialise to take advantage of this.
779    pub fn tick(&mut self, site_id: Id<Site>, dt: f32) {
780        // collect goods from trading
781        if INTER_SITE_TRADE {
782            self.collect_deliveries();
783        }
784
785        let orders = self.get_orders();
786        let production = self.get_production();
787
788        // for i in production.iter() {
789        //     vc.context("production")
790        //         .value(&std::format!("{:?}{:?}", i.0, Good::from(i.1.0)), i.1.1);
791        // }
792
793        let mut demand = GoodMap::from_default(0.0);
794        for (labor, orders) in orders.iter() {
795            let workers = self.labors[labor] * self.pop;
796            for (good, amount) in orders {
797                demand[*good] += *amount * workers;
798            }
799        }
800        for (good, amount) in self.get_orders_everyone() {
801            demand[*good] += *amount * self.pop;
802        }
803        if INTER_SITE_TRADE {
804            demand[*COIN_INDEX] += Economy::STARTING_COIN; // if we spend coin value increases
805        }
806
807        // which labor is the merchant
808        let merchant_labor = production
809            .iter()
810            .find(|(_, v)| v.0 == *TRANSPORTATION_INDEX)
811            .map(|(l, _)| l)
812            .unwrap_or_default();
813
814        let mut supply = self.stocks; //GoodMap::from_default(0.0);
815        for (labor, goodvec) in production.iter() {
816            //for (output_good, _) in goodvec.iter() {
817            //info!("{} supply{:?}+={}", site_id.id(), Good::from(goodvec.0),
818            // self.yields[labor] * self.labors[labor] * self.pop);
819            supply[goodvec.0] += self.yields[labor] * self.labors[labor] * self.pop;
820            // vc.context(&std::format!("{:?}-{:?}", Good::from(goodvec.0),
821            // labor))     .value("yields", self.yields[labor]);
822            // vc.context(&std::format!("{:?}-{:?}", Good::from(goodvec.0),
823            // labor))     .value("labors", self.labors[labor]);
824            //}
825        }
826
827        // for i in supply.iter() {
828        //     vc.context("supply")
829        //         .value(&std::format!("{:?}", Good::from(i.0)), *i.1);
830        // }
831
832        let stocks = &self.stocks;
833        // for i in stocks.iter() {
834        //     vc.context("stocks")
835        //         .value(&std::format!("{:?}", Good::from(i.0)), *i.1);
836        // }
837        self.surplus = demand.map(|g, demand| supply[g] + stocks[g] - demand);
838        self.marginal_surplus = demand.map(|g, demand| supply[g] - demand);
839
840        // plan trading with other sites
841        // let external_orders = &mut index.trade.orders;
842        let mut potential_trade = GoodMap::from_default(0.0);
843        // use last year's generated transportation for merchants (could we do better?
844        // this is in line with the other professions)
845        let transportation_capacity = self.stocks[*TRANSPORTATION_INDEX];
846        let trade = if INTER_SITE_TRADE {
847            let trade =
848                self.plan_trade_for_site(&site_id, transportation_capacity, &mut potential_trade);
849            self.active_exports = GoodMap::from_iter(trade.iter().map(|(g, a)| (g, -*a)), 0.0); // TODO: check for availability?
850
851            // add the wares to sell to demand and the goods to buy to supply
852            for (g, a) in trade.iter() {
853                // vc.context("trade")
854                //     .value(&std::format!("{:?}", Good::from(g)), *a);
855                if *a > 0.0 {
856                    supply[g] += *a;
857                    assert!(supply[g] >= 0.0);
858                } else {
859                    demand[g] -= *a;
860                    assert!(demand[g] >= 0.0);
861                }
862            }
863            trade
864        } else {
865            GoodMap::default()
866        };
867
868        // Update values according to the surplus of each stock
869        // Note that values are used for workforce allocation and are not the same thing
870        // as price
871        // fall back to old (less wrong than other goods) coin logic
872        let old_coin_surplus = self.stocks[*COIN_INDEX] - demand[*COIN_INDEX];
873        let values = &mut self.values;
874
875        self.surplus.iter().for_each(|(good, surplus)| {
876            let old_surplus = if good == *COIN_INDEX {
877                old_coin_surplus
878            } else {
879                *surplus
880            };
881            // Value rationalisation
882            // let goodname = std::format!("{:?}", Good::from(good));
883            // vc.context("old_surplus").value(&goodname, old_surplus);
884            // vc.context("demand").value(&goodname, demand[good]);
885            let val = 2.0f32.powf(1.0 - old_surplus / demand[good]);
886            let smooth = 0.8;
887            values[good] = if val > 0.001 && val < 1000.0 {
888                Some(
889                    // vc.context("values").value(
890                    // &goodname,
891                    smooth * values[good].unwrap_or(val) + (1.0 - smooth) * val,
892                )
893            } else {
894                None
895            };
896        });
897
898        let all_trade_goods: DHashSet<GoodIndex> = trade
899            .iter()
900            .chain(potential_trade.iter())
901            .filter(|(_, a)| **a > 0.0)
902            .map(|(g, _)| g)
903            .collect();
904        //let empty_goods: DHashSet<GoodIndex> = DHashSet::default();
905        // TODO: Does avg/max/sum make most sense for labors creating more than one good
906        // summing favors merchants too much (as they will provide multiple
907        // goods, so we use max instead)
908        let labor_ratios: LaborMap<f32> = LaborMap::from_iter(
909            production.iter().map(|(labor, goodvec)| {
910                (
911                    labor,
912                    if labor == merchant_labor {
913                        all_trade_goods
914                            .iter()
915                            .chain(std::iter::once(&goodvec.0))
916                            .map(|&output_good| self.values[output_good].unwrap_or(0.0))
917                            .max_by(|a, b| a.abs().partial_cmp(&b.abs()).unwrap_or(Less))
918                    } else {
919                        self.values[goodvec.0]
920                    }
921                    .unwrap_or(0.0)
922                        * self.productivity[labor],
923                )
924            }),
925            0.0,
926        );
927        trace!(?labor_ratios);
928
929        let labor_ratio_sum = labor_ratios.iter().map(|(_, r)| *r).sum::<f32>().max(0.01);
930        //let mut labor_context = vc.context("labor");
931        production.iter().for_each(|(labor, _)| {
932            let smooth = 0.8;
933            self.labors[labor] =
934            // labor_context.value(
935            //     &format!("{:?}", labor),
936                smooth * self.labors[labor]
937                    + (1.0 - smooth)
938                        * (labor_ratios[labor].max(labor_ratio_sum / 1000.0) / labor_ratio_sum);
939            assert!(self.labors[labor] >= 0.0);
940        });
941
942        // Production
943        let stocks_before = self.stocks;
944        // TODO: Should we recalculate demand after labor reassignment?
945
946        let direct_use = direct_use_goods();
947        // Handle the stocks you can't pile (decay)
948        for g in direct_use {
949            self.stocks[*g] = 0.0;
950        }
951
952        let mut total_labor_values = GoodMap::<f32>::default();
953        // TODO: trade
954        let mut total_outputs = GoodMap::<f32>::default();
955        for (labor, orders) in orders.iter() {
956            let workers = self.labors[labor] * self.pop;
957            assert!(workers >= 0.0);
958            let is_merchant = merchant_labor == labor;
959
960            // For each order, we try to find the minimum satisfaction rate - this limits
961            // how much we can produce! For example, if we need 0.25 fish and
962            // 0.75 oats to make 1 unit of food, but only 0.5 units of oats are
963            // available then we only need to consume 2/3rds
964            // of other ingredients and leave the rest in stock
965            // In effect, this is the productivity
966            let (labor_productivity, limited_by) = orders
967                .iter()
968                .map(|(good, amount)| {
969                    // What quantity is this order requesting?
970                    let _quantity = *amount * workers;
971                    assert!(stocks_before[*good] >= 0.0);
972                    assert!(demand[*good] >= 0.0);
973                    // What proportion of this order is the economy able to satisfy?
974                    ((stocks_before[*good] / demand[*good]).min(1.0), *good)
975                })
976                .min_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(Less))
977                .unwrap_or_else(|| {
978                    panic!("Industry {:?} requires at least one input order", labor)
979                });
980            assert!(labor_productivity >= 0.0);
981            self.limited_by[labor] = if labor_productivity >= 1.0 {
982                GoodIndex::default()
983            } else {
984                limited_by
985            };
986
987            let mut total_materials_cost = 0.0;
988            for (good, amount) in orders {
989                // What quantity is this order requesting?
990                let quantity = *amount * workers;
991                // What amount gets actually used in production?
992                let used = quantity * labor_productivity;
993
994                // Material cost of each factor of production
995                total_materials_cost += used * self.labor_values[*good].unwrap_or(0.0);
996
997                // Deplete stocks accordingly
998                if !direct_use.contains(good) {
999                    self.stocks[*good] = (self.stocks[*good] - used).max(0.0);
1000                }
1001            }
1002            let mut produced_goods: GoodMap<f32> = GoodMap::from_default(0.0);
1003            if INTER_SITE_TRADE && is_merchant {
1004                // TODO: replan for missing merchant productivity???
1005                for (g, a) in trade.iter() {
1006                    if !direct_use.contains(&g) {
1007                        if *a < 0.0 {
1008                            // take these goods to the road
1009                            if self.stocks[g] + *a < 0.0 {
1010                                // we have a problem: Probably due to a shift in productivity we
1011                                // have less goods available than
1012                                // planned, so we would need to
1013                                // reduce the amount shipped
1014                                debug!("NEG STOCK {:?} {} {}", g, self.stocks[g], *a);
1015                                let reduced_amount = self.stocks[g];
1016                                let planned_amount: f32 = self
1017                                    .orders
1018                                    .iter()
1019                                    .map(|i| {
1020                                        i.1.iter()
1021                                            .filter(|o| o.customer == site_id)
1022                                            .map(|j| j.amount[g])
1023                                            .sum::<f32>()
1024                                    })
1025                                    .sum();
1026                                let scale = reduced_amount / planned_amount.abs();
1027                                trace!("re-plan {} {} {}", reduced_amount, planned_amount, scale);
1028                                for k in self.orders.iter_mut() {
1029                                    for l in k.1.iter_mut().filter(|o| o.customer == site_id) {
1030                                        l.amount[g] *= scale;
1031                                    }
1032                                }
1033                                self.stocks[g] = 0.0;
1034                            }
1035                            //                    assert!(self.stocks[g] + *a >= 0.0);
1036                            else {
1037                                self.stocks[g] += *a;
1038                            }
1039                        }
1040                        total_materials_cost += (-*a) * self.labor_values[g].unwrap_or(0.0);
1041                    } else {
1042                        // count on receiving these
1043                        produced_goods[g] += *a;
1044                    }
1045                }
1046                trace!(
1047                    "merchant {} {}: {:?} {} {:?}",
1048                    site_id.id(),
1049                    self.pop,
1050                    produced_goods,
1051                    total_materials_cost,
1052                    trade
1053                );
1054            }
1055
1056            // Industries produce things
1057            let work_products = &production[labor];
1058            self.yields[labor] = labor_productivity * work_products.1;
1059            self.productivity[labor] = labor_productivity;
1060            let (stock, rate) = work_products;
1061            let total_output = labor_productivity * *rate * workers;
1062            assert!(total_output >= 0.0);
1063            self.stocks[*stock] += total_output;
1064            produced_goods[*stock] += total_output;
1065
1066            let produced_amount: f32 = produced_goods.iter().map(|(_, a)| *a).sum();
1067            for (stock, amount) in produced_goods.iter() {
1068                let cost_weight = amount / produced_amount.max(0.001);
1069                // Materials cost per unit
1070                // TODO: How to handle this reasonably for multiple producers (collect upper and
1071                // lower term separately)
1072                self.material_costs[stock] = total_materials_cost / amount.max(0.001) * cost_weight;
1073                // Labor costs
1074                let wages = 1.0;
1075                let total_labor_cost = workers * wages;
1076
1077                total_labor_values[stock] +=
1078                    (total_materials_cost + total_labor_cost) * cost_weight;
1079                total_outputs[stock] += amount;
1080            }
1081        }
1082        // consume goods needed by everyone
1083        for &(good, amount) in self.get_orders_everyone() {
1084            let needed = amount * self.pop;
1085            let available = stocks_before[good];
1086            self.stocks[good] = (self.stocks[good] - needed.min(available)).max(0.0);
1087            //info!("Ev {:.1} {:?} {} - {:.1} {:.1}", self.pop, good,
1088            // self.stocks[good], needed, available);
1089        }
1090
1091        // Update labour values per unit
1092        self.labor_values = total_labor_values.map(|stock, tlv| {
1093            let total_output = total_outputs[stock];
1094            if total_output > 0.01 {
1095                Some(tlv / total_output)
1096            } else {
1097                None
1098            }
1099        });
1100
1101        // Decay stocks (the ones which totally decay are handled later)
1102        self.stocks
1103            .iter_mut()
1104            .map(|(c, v)| (v, 1.0 - decay_rate(c)))
1105            .for_each(|(v, factor)| *v *= factor);
1106
1107        // Decay stocks
1108        self.replenish(dt);
1109
1110        // Births/deaths
1111        const NATURAL_BIRTH_RATE: f32 = 0.05;
1112        const DEATH_RATE: f32 = 0.005;
1113        let population_growth = self.surplus[*FOOD_INDEX] > 0.0;
1114        let birth_rate = if population_growth {
1115            NATURAL_BIRTH_RATE
1116        } else {
1117            0.0
1118        };
1119        self.pop += //vc.value(
1120            //"pop",
1121            dt / DAYS_PER_YEAR * self.pop * (birth_rate - DEATH_RATE);
1122        //);
1123        self.population_limited_by = if population_growth {
1124            GoodIndex::default()
1125        } else {
1126            *FOOD_INDEX
1127        };
1128
1129        // calculate the new unclaimed stock
1130        //let next_orders = self.get_orders();
1131        // orders are static
1132        let mut next_demand = GoodMap::from_default(0.0);
1133        for (labor, orders) in orders.iter() {
1134            let workers = self.labors[labor] * self.pop;
1135            for (good, amount) in orders {
1136                next_demand[*good] += *amount * workers;
1137                assert!(next_demand[*good] >= 0.0);
1138            }
1139        }
1140        for (good, amount) in self.get_orders_everyone() {
1141            next_demand[*good] += *amount * self.pop;
1142            assert!(next_demand[*good] >= 0.0);
1143        }
1144        //let mut us = vc.context("unconsumed");
1145        self.unconsumed_stock = GoodMap::from_iter(
1146            self.stocks.iter().map(|(g, a)| {
1147                (
1148                    g,
1149                    //us.value(&format!("{:?}", Good::from(g)),
1150                    *a - next_demand[g],
1151                )
1152            }),
1153            0.0,
1154        );
1155    }
1156
1157    pub fn csv_entry(f: &mut std::fs::File, site: &Site) -> Result<(), std::io::Error> {
1158        use std::io::Write;
1159        write!(
1160            *f,
1161            "{}, {}, {}, {:.1}, {},,",
1162            site.name(),
1163            site.get_origin().x,
1164            site.get_origin().y,
1165            site.economy.pop,
1166            site.economy.neighbors.len(),
1167        )?;
1168        for g in good_list() {
1169            if let Some(value) = site.economy.values[g] {
1170                write!(*f, "{:.2},", value)?;
1171            } else {
1172                f.write_all(b",")?;
1173            }
1174        }
1175        f.write_all(b",")?;
1176        for g in good_list() {
1177            if let Some(labor_value) = site.economy.labor_values[g] {
1178                write!(f, "{:.2},", labor_value)?;
1179            } else {
1180                f.write_all(b",")?;
1181            }
1182        }
1183        f.write_all(b",")?;
1184        for g in good_list() {
1185            write!(f, "{:.1},", site.economy.stocks[g])?;
1186        }
1187        f.write_all(b",")?;
1188        for g in good_list() {
1189            write!(f, "{:.1},", site.economy.marginal_surplus[g])?;
1190        }
1191        f.write_all(b",")?;
1192        for l in LaborIndex::list() {
1193            write!(f, "{:.1},", site.economy.labors[l] * site.economy.pop)?;
1194        }
1195        f.write_all(b",")?;
1196        for l in LaborIndex::list() {
1197            write!(f, "{:.2},", site.economy.productivity[l])?;
1198        }
1199        f.write_all(b",")?;
1200        for l in LaborIndex::list() {
1201            write!(f, "{:.1},", site.economy.yields[l])?;
1202        }
1203        f.write_all(b",")?;
1204        for l in LaborIndex::list() {
1205            let limit = site.economy.limited_by[l];
1206            if limit == GoodIndex::default() {
1207                f.write_all(b",")?;
1208            } else {
1209                write!(f, "{:?},", limit)?;
1210            }
1211        }
1212        f.write_all(b",")?;
1213        for g in good_list() {
1214            if site.economy.last_exports[g] >= 0.1 || site.economy.last_exports[g] <= -0.1 {
1215                write!(f, "{:.1},", site.economy.last_exports[g])?;
1216            } else {
1217                f.write_all(b",")?;
1218            }
1219        }
1220        writeln!(f)
1221    }
1222
1223    fn csv_header(f: &mut std::fs::File) -> Result<(), std::io::Error> {
1224        use std::io::Write;
1225        write!(f, "Site,PosX,PosY,Population,Neighbors,,")?;
1226        for g in good_list() {
1227            write!(f, "{:?} Value,", g)?;
1228        }
1229        f.write_all(b",")?;
1230        for g in good_list() {
1231            write!(f, "{:?} LaborVal,", g)?;
1232        }
1233        f.write_all(b",")?;
1234        for g in good_list() {
1235            write!(f, "{:?} Stock,", g)?;
1236        }
1237        f.write_all(b",")?;
1238        for g in good_list() {
1239            write!(f, "{:?} Surplus,", g)?;
1240        }
1241        f.write_all(b",")?;
1242        for l in LaborIndex::list() {
1243            write!(f, "{:?} Labor,", l)?;
1244        }
1245        f.write_all(b",")?;
1246        for l in LaborIndex::list() {
1247            write!(f, "{:?} Productivity,", l)?;
1248        }
1249        f.write_all(b",")?;
1250        for l in LaborIndex::list() {
1251            write!(f, "{:?} Yields,", l)?;
1252        }
1253        f.write_all(b",")?;
1254        for l in LaborIndex::list() {
1255            write!(f, "{:?} limit,", l)?;
1256        }
1257        f.write_all(b",")?;
1258        for g in good_list() {
1259            write!(f, "{:?} trade,", g)?;
1260        }
1261        writeln!(f)
1262    }
1263
1264    pub fn csv_open() -> Option<std::fs::File> {
1265        if GENERATE_CSV {
1266            let mut f = std::fs::File::create("economy.csv").ok()?;
1267            if Self::csv_header(&mut f).is_err() {
1268                None
1269            } else {
1270                Some(f)
1271            }
1272        } else {
1273            None
1274        }
1275    }
1276
1277    #[cfg(test)]
1278    fn print_details(&self) {
1279        fn print_sorted(
1280            prefix: &str,
1281            mut list: Vec<(String, f32)>,
1282            threshold: f32,
1283            decimals: usize,
1284        ) {
1285            print!("{}", prefix);
1286            list.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Less));
1287            for i in list.iter() {
1288                if i.1 >= threshold {
1289                    print!("{}={:.*} ", i.0, decimals, i.1);
1290                }
1291            }
1292            println!();
1293        }
1294
1295        print!(" Resources: ");
1296        for i in good_list() {
1297            let amount = self.natural_resources.chunks_per_resource[i];
1298            if amount > 0.0 {
1299                print!("{:?}={} ", i, amount);
1300            }
1301        }
1302        println!();
1303        println!(
1304            " Population {:.1}, limited by {:?}",
1305            self.pop, self.population_limited_by
1306        );
1307        let idle: f32 = self.pop * (1.0 - self.labors.iter().map(|(_, a)| *a).sum::<f32>());
1308        print_sorted(
1309            &format!(" Professions: idle={:.1} ", idle),
1310            self.labors
1311                .iter()
1312                .map(|(l, a)| (format!("{:?}", l), *a * self.pop))
1313                .collect(),
1314            self.pop * 0.05,
1315            1,
1316        );
1317        print_sorted(
1318            " Stock: ",
1319            self.stocks
1320                .iter()
1321                .map(|(l, a)| (format!("{:?}", l), *a))
1322                .collect(),
1323            1.0,
1324            0,
1325        );
1326        print_sorted(
1327            " Values: ",
1328            self.values
1329                .iter()
1330                .map(|(l, a)| {
1331                    (
1332                        format!("{:?}", l),
1333                        a.map(|v| if v > 3.9 { 0.0 } else { v }).unwrap_or(0.0),
1334                    )
1335                })
1336                .collect(),
1337            0.1,
1338            1,
1339        );
1340        print_sorted(
1341            " Labor Values: ",
1342            self.labor_values
1343                .iter()
1344                .map(|(l, a)| (format!("{:?}", l), a.unwrap_or(0.0)))
1345                .collect(),
1346            0.1,
1347            1,
1348        );
1349        print!(" Limited: ");
1350        for (limit, prod) in self.limited_by.iter().zip(self.productivity.iter()) {
1351            if (0.01..=0.99).contains(prod.1) {
1352                print!("{:?}:{:?}={:.2} ", limit.0, limit.1, *prod.1);
1353            }
1354        }
1355        println!();
1356        print!(" Trade({}): ", self.neighbors.len());
1357        for (g, &amt) in self.active_exports.iter() {
1358            if !(-0.1..=0.1).contains(&amt) {
1359                print!("{:?}={:.2} ", g, amt);
1360            }
1361        }
1362        println!();
1363    }
1364}
1365
1366fn good_list() -> impl Iterator<Item = GoodIndex> {
1367    (0..GoodIndex::LENGTH).map(GoodIndex::from_usize)
1368}
1369
1370fn transportation_effort(g: GoodIndex) -> f32 { cache::cache().transport_effort[g] }
1371
1372fn decay_rate(g: GoodIndex) -> f32 { cache::cache().decay_rate[g] }
1373
1374/** you can't accumulate or save these options/resources for later */
1375fn direct_use_goods() -> &'static [GoodIndex] { &cache::cache().direct_use_goods }
1376
1377pub struct GraphInfo {
1378    dummy: Economy,
1379}
1380
1381impl Default for GraphInfo {
1382    fn default() -> Self {
1383        // avoid economy of scale
1384        Self {
1385            dummy: Economy {
1386                pop: 0.0,
1387                labors: LaborMap::from_default(0.0),
1388                ..Default::default()
1389            },
1390        }
1391    }
1392}
1393
1394impl GraphInfo {
1395    pub fn get_orders(&self) -> &'static LaborMap<Vec<(GoodIndex, f32)>> { self.dummy.get_orders() }
1396
1397    pub fn get_orders_everyone(&self) -> impl Iterator<Item = &'static (GoodIndex, f32)> {
1398        self.dummy.get_orders_everyone()
1399    }
1400
1401    pub fn get_production(&self) -> LaborMap<(GoodIndex, f32)> { self.dummy.get_production() }
1402
1403    pub fn good_list(&self) -> impl Iterator<Item = GoodIndex> { good_list() }
1404
1405    pub fn labor_list(&self) -> impl Iterator<Item = Labor> { Labor::list() }
1406
1407    pub fn can_store(&self, g: &GoodIndex) -> bool { direct_use_goods().contains(g) }
1408}