Skip to content

Instantly share code, notes, and snippets.

@zesterer
Created July 20, 2021 21:36
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save zesterer/8f19144430973929eb23a1e2e0db4e12 to your computer and use it in GitHub Desktop.
Save zesterer/8f19144430973929eb23a1e2e0db4e12 to your computer and use it in GitHub Desktop.
Economy with labour value but prices instead of derived consumption value
use std::collections::BTreeMap as HashMap;//HashMap;
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Good {
Log, // Units: Kg
Wood, // Units Kg
Meat, // Units: Kg
Food,
}
const GOODS: [Good; 4] = [
Good::Log,
Good::Wood,
Good::Meat,
Good::Food,
];
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum Labor {
Lumberjack,
Carpenter,
Fisher,
Hunter,
Cook,
}
const LABORS: [Labor; 5] = [
Labor::Lumberjack,
Labor::Carpenter,
Labor::Fisher,
Labor::Hunter,
Labor::Cook,
];
impl Labor {
fn industry(&self) -> Industry {
match self {
Labor::Lumberjack => Industry {
inputs: &[],
outputs: &[(Good::Log, 10.0)],
},
Labor::Carpenter => Industry {
inputs: &[(Good::Log, 10.0)],
outputs: &[(Good::Wood, 10.0)], // 1/3rd is 'wasted' (sawdust, etc.)
},
Labor::Fisher => Industry {
inputs: &[(Good::Wood, 0.1)],
outputs: &[(Good::Meat, 1.0)],
},
Labor::Hunter => Industry {
inputs: &[],
outputs: &[(Good::Meat, 1.0)],
},
Labor::Cook => Industry {
inputs: &[(Good::Wood, 0.2), (Good::Meat, 1.0)],
outputs: &[(Good::Food, 1.0)], // Some fish is wasted (gutting)
},
}
}
}
struct Industry {
inputs: &'static [(Good, f32)],
outputs: &'static [(Good, f32)],
}
struct Economy {
// Economy population
pop: f32,
// Number of laborers allocated to each industry
laborers: HashMap<Labor, f32>,
// The relative productivity of each labor in the last tick
// 0.0 = At least one of the required input goods was not available
// 1.0 = All of the required input goods were available, sufficiently to saturate demand
// This is the minimum of the proportion that each input was supplied
productivity: HashMap<Labor, (f32, Option<Good>)>,
// Given current workforce allocation, how much of each good will be produced on the next tick?
// This is expressed as a proportion of the total required for industry. i.e:
// >= 1.0 => supply completely saturates industry, oversupply
// <= 1.0 => supply is insufficient to satisfy industry, undersupply
available: HashMap<Good, f32>,
// Labor value and consumption value are in the same units:
// - Labor value are the average number of labor hours required to produce 1 unit
// - Consumption values are the number of labor hours that workers would be willing to exchange for 1 unit
// During each tick, labor values are propagated forwards through the supply chain and consumption values are
// propagated backwards through the supply change, accounting for scarcity.
labor_value: HashMap<Good, f32>,
// consumption_value: HashMap<Good, f32>,
// The relative value of goods. Goods that are produced optimally are at 1.0 (i.e: labor value matches consumption value).
// > 1.0 => production of this good should increase
// < 1.0 => production of this good should reduce
// value: HashMap<Good, f32>,
price: HashMap<Good, f32>,
// Total output of this good that occured in the last tick
output: HashMap<Good, f32>,
}
impl Economy {
// Calculate to what extent supply will satisfy demand for each good on the upcoming tick. See Economy::available.
fn derive_available_goods(&mut self) {
let mut total_demand = HashMap::new();
let mut total_supply = HashMap::new();
for labor in LABORS {
let industry = labor.industry();
let laborers = self.laborers.get(&labor).unwrap_or(&0.0);
// Productivity may limit goods that can be produced if inputs are undersupplied
// If 1.0, all industry inputs are satisfied. If 0.0, no industry inputs are satisfied.
let (limiting_good, productivity) = industry.inputs
.iter()
// Productivity can never be lower than 0% or higher than 100%. You can throw capital at a tree as much
// as you like: labor is required for economic output!
.map(|(good, _)| (Some(*good), self.available.get(good).unwrap_or(&0.0).max(0.0).min(1.0)))
.min_by_key(|(_, available)| (*available * 100000.0) as i64) // PartialOrd hack
.unwrap_or((None, 1.0));
for &(good, input) in industry.inputs {
*total_demand.entry(good).or_insert(0.0) += input * laborers;
}
self.productivity.insert(labor, (productivity, limiting_good));
for &(good, output) in industry.outputs {
*total_supply.entry(good).or_insert(0.0) += output * laborers * productivity;
}
}
// TODO: determine required food based on consumption value & Maslow hierachy
total_demand.insert(Good::Food, self.pop * 0.5);
for good in GOODS {
let total_supply = total_supply.get(&good).unwrap_or(&0.0);
let total_demand = total_demand.get(&good).unwrap_or(&0.0);
// println!("{:?}, total_supply = {}, total_demand = {}", good, total_supply, total_demand);
self.available.insert(good, total_supply / total_demand.max(0.00001));
self.price.insert(good, total_demand / total_supply.max(0.00001));
}
}
// Calculate labor values for each good by propagating its value forward through the supply chain (this is the easy
// part). The labor value of each good is simply the sum of the labor values of its inputs, in addition to the
// labor time required to create a unit of the input.
//
// Because more than one industry might produce the same good, we keep a running total of labour values vs outputs
// so that we can normalise this value across the industries afterwards.
fn derive_labor_values(&mut self) {
let mut total_labor_values = HashMap::<Good, f32>::new();
let mut total_produced = HashMap::<Good, f32>::new();
for labor in LABORS {
let industry = labor.industry();
let laborers = self.laborers.get(&labor).unwrap_or(&0.0);
let total_input_value = industry.inputs
.iter()
.map(|(good, input)| {
*self.labor_value.get(good).unwrap_or(&0.0) * input
})
.sum::<f32>();
let labor_time = 1.0;
let productivity = self.productivity.get(&labor).unwrap_or(&(0.0, None)).0;
for &(good, output) in industry.outputs {
let volume = laborers * productivity;
*total_labor_values
.entry(good)
.or_insert(0.0) += (total_input_value + labor_time) / output * volume;
*total_produced.entry(good).or_insert(0.0) += volume;
}
}
for good in GOODS {
let total_labor_value = total_labor_values.get(&good).unwrap_or(&0.0);
let total_produced = total_produced.get(&good).unwrap_or(&0.0);
self.labor_value.insert(good, total_labor_value / total_produced.max(0.00001));
self.output.insert(good, *total_produced);
}
}
// Calculate the consumption value for each good. This one is a bit more complicated. For each industry, we take a
// look at the outputs. We sum up the consumption values of the outputs derived from the last tick to get the total
// output's consumption value of that industry (based on the consumption value of the last tick). Then, we
// distribute that consumption value to industry inputs. Obviously, we weight this based on the total outputs of
// each industry.
// fn derive_consumption_values(&mut self) {
// let mut total_consumption_values = HashMap::<Good, f32>::new();
// let mut total_used = HashMap::<Good, f32>::new();
// for labor in LABORS {
// let industry = labor.industry();
// let laborers = self.laborers.get(&labor).unwrap_or(&0.0);
// let productivity = self.productivity.get(&labor).unwrap_or(&(0.0, None)).0;
// let total_output_value = industry.outputs
// .iter()
// .map(|(good, output)| {
// *self.consumption_value.get(good).unwrap_or(&0.0) * output
// })
// .sum::<f32>();
// let total_inputs = industry.inputs
// .iter()
// .map(|(_, input)| input)
// .sum::<f32>();
// let total_outputs = industry.outputs
// .iter()
// .map(|(_, output)| output)
// .sum::<f32>();
// for &(good, input) in industry.inputs {
// let volume = laborers * productivity * input;
// *total_consumption_values
// .entry(good)
// .or_insert(0.0) += total_output_value * input * volume / total_inputs;// / self.available.get(&good).unwrap_or(&0.0).max(0.01);
// *total_used.entry(good).or_insert(0.0) += volume * total_outputs;
// }
// }
// for good in GOODS {
// let total_consumption_value = total_consumption_values.get(&good).unwrap_or(&0.0);
// let total_used = total_used.get(&good).unwrap_or(&0.0);
// self.consumption_value.insert(good, total_consumption_value / total_used.max(0.00001));
// }
// // Automatically derived (TODO: use a Maslow hierarchy to derive the value of consumables)
// // This value is "How many hours of labor a citizen would be willing to trade for 1 unit of this good".
// // The 'value' of food increases as population increases, and decreases as food production decreases
// let food_per_person = self.output.get(&Good::Food).unwrap_or(&0.0) / self.pop;
// let starvation = 1.5; // 1.5 hours of work for 1 unit of food when there is no food
// let satisfied = 1.0; // Units of food at which value of food falls to 0 no matter how little work is needed
// let value_of_food = (1.0 - food_per_person / satisfied).max(0.0) * starvation;
// self.consumption_value.insert(Good::Food, 5.0);//1.75 * value_of_food);
// }
// Derive relative values for goods from consumption value / labour value. See Economy::value.
// fn derive_values(&mut self) {
// for good in GOODS {
// let consumption_value = self.consumption_value.get(&good).unwrap_or(&0.0);
// let labor_value = self.labor_value.get(&good).unwrap_or(&0.0);
// let availibility = self.available.get(&good).unwrap_or(&0.0).max(0.00001);
// println!("Availability of {:?} = {}", good, availibility);
// self.value.insert(good, consumption_value / labor_value.max(0.00001) / availibility);
// }
// }
fn redistribute_laborers(&mut self) {
// Redistribute labor according to relative values of industry outputs
for labor in LABORS {
let industry = labor.industry();
// let total_consumption_value = industry.outputs
// .iter()
// .map(|(good, output)| {
// *self.consumption_value.get(good).unwrap_or(&0.001) * output
// })
// .sum::<f32>();
let total_output = industry.outputs
.iter()
.map(|(good, output)| output)
.sum::<f32>();
let labor_time = 1.0;
// Total industry output (used to normalise later, meaningless except in relation to `total_value`)
let total_labor_value = industry.inputs
.iter()
.map(|(good, input)| {
*self.labor_value.get(good).unwrap_or(&0.001) * input
})
.sum::<f32>() + labor_time;
// let total_input_values = industry.inputs
// .iter()
// .map(|(good, input)| {
// *self.value.get(good).unwrap_or(&0.001) * input
// })
// .sum::<f32>();
// let total_output_values = industry.outputs
// .iter()
// .map(|(good, output)| {
// *self.value.get(good).unwrap_or(&0.001) * output
// })
// .sum::<f32>();
let total_input_price = industry.inputs
.iter()
.map(|(good, input)| {
*self.price.get(good).unwrap_or(&0.001) * input
})
.sum::<f32>();
let total_output_price = industry.outputs
.iter()
.map(|(good, output)| {
*self.price.get(good).unwrap_or(&0.001) * output
})
.sum::<f32>();
let total_oversupply = industry.outputs
.iter()
.map(|(good, output)| {
output * self.available.get(&good).unwrap_or(&0.0).max(0.01)
})
.sum::<f32>();
let oversupply = total_oversupply / total_output;
// If the productivity of an industry is 0, obviously there's no point allocating laborers to it, no matter
// how valuable the outputs are!
let (productivity, limiting_good) = *self.productivity.get(&labor).unwrap_or(&(0.0, None));
// TODO: Use Maslow hierarchy to determine this
let labor_price = 10.0 / self.price.get(&Good::Food).unwrap_or(&0.0).max(0.00001);
// Average normalised value of outputs (remember, all values the same = pareto efficiency)
let avg_value = (total_output_price - total_input_price) * productivity - labor_price;//1.0 / oversupply;//total_consumption_value / total_labor_value.max(0.001);// * productivity;
// What proportion of the existing workforce should move industries each tick? (at maximum)
let rate = 0.1;
let change = (1.0 + avg_value) * rate + (1.0 - rate);
// println!("change {:?} = {}, avg_value = {}, productivity = {}, total_output_values = {}, total_input_values + labor_time = {}, oversupply = {}, limiting_good = {:?}", labor, change, avg_value, productivity, total_output_values, total_input_values + labor_time, oversupply, limiting_good);
*self.laborers.entry(labor).or_insert(0.0) *= change;
}
// The allocation of the workforce might now be *higher* than the working population! If this is the case, we normalise
// the workforce over the population to ensure realism is maintained.
let working_pop = self.pop * 1.0; // For now, assume everybody in the economy can work (1.0)
let total_laborers = self.laborers.values().sum::<f32>();
self.laborers.values_mut().for_each(|l| {
// This prevents any industry becoming completely drained of workforce, thereby inhibiting any production.
// Keeping a small number of laborers in every industry keeps things 'ticking over' so the economy can
// quickly adapt to changing conditions
let min_workforce_alloc = 0.01;
let factor = if total_laborers > working_pop { working_pop / total_laborers } else { 1.0 };
*l = (*l * factor).max(min_workforce_alloc);
});
}
fn tick(&mut self) {
self.derive_available_goods();
self.derive_labor_values();
// self.derive_consumption_values();
// self.derive_values();
self.redistribute_laborers();
}
}
fn main() {
let mut economy = Economy {
pop: 100.0,
laborers: HashMap::new(),
productivity: HashMap::new(),
available: HashMap::new(),
labor_value: HashMap::new(),
// consumption_value: HashMap::new(),
// value: HashMap::new(),
price: HashMap::new(),
output: HashMap::new(),
};
economy.laborers.insert(Labor::Lumberjack, 1.0);
economy.laborers.insert(Labor::Carpenter, 1.0);
economy.laborers.insert(Labor::Fisher, 1.0);
economy.laborers.insert(Labor::Hunter, 1.0);
economy.laborers.insert(Labor::Cook, 1.0);
for i in 0..100 {
println!("--- Tick {} ---", i);
economy.tick();
println!("Laborers: {:?} ({}% lazy, pop = {})", economy.laborers, 100.0 * (economy.pop - economy.laborers.values().sum::<f32>()) / economy.pop, economy.pop);
println!("Available: {:?}", economy.available);
// println!("Consumption value: {:?}", economy.consumption_value);
println!("Labor value: {:?}", economy.labor_value);
// println!("Value: {:?}", economy.value);
println!("Price: {:?}", economy.price);
println!("Total output: {:?}", economy.output);
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment