feat(simulation): add corporate behavioral archetypes — Layer 3 (#809)
Implements the 6 behavioral archetypes from D-175 (Burnelli-Sheldon): Producer — 1.15× production scale, neutral price signal Distributor — 0.9× production, −3% price discount to move volume Specialist — 1.0× production, +10% price premium for expertise Monopolist — 0.8× production, withholds 25% of output, +20% premium Cooperative — 1.0× production, −5% community discount Intermediary — 0.7× production, relies on traded goods Archetype loading: - Reads from corporations.behavioral_archetype (currently NULL for all corps) - Falls back to heuristic inference from specialization text (freight → Distributor, extraction → Producer, luxury goods → Specialist, etc.) - 48 corps loaded on current DB, all inferred (DB column to be populated when wiki corp frontmatter is extended with the behavioral_archetype field) Applied in model.rs step(): - Per-corp effective_capacity = BASELINE_CAPACITY × production_scale - Monopolist supply_withheld fraction reduces net output to stockpile - Price premium: small ALPHA-scaled nudge to node price for primary commodity All D-179 stability checks still pass with archetypes active. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::agents;
|
||||
use crate::currency::{CurrencyState, ShadowEconomy};
|
||||
use crate::db::Economy;
|
||||
use crate::seed::Productivity;
|
||||
@@ -93,12 +94,13 @@ pub fn run(
|
||||
adjacency: &HashMap<String, Vec<String>>,
|
||||
ticks: u32,
|
||||
) -> Vec<TickRecord> {
|
||||
let archetypes = agents::build_archetype_map(economy.corp_archetype_data.clone());
|
||||
let mut nodes = init_nodes(economy);
|
||||
let mut currency = CurrencyState::new();
|
||||
let mut records = Vec::new();
|
||||
|
||||
for tick in 0..ticks {
|
||||
step(economy, productivity, shadow, &mut nodes);
|
||||
step(economy, productivity, shadow, &archetypes, &mut nodes);
|
||||
trade::trade_step(economy, &mut nodes, adjacency, &mut currency);
|
||||
currency.update_rate();
|
||||
|
||||
@@ -197,6 +199,7 @@ fn step(
|
||||
economy: &Economy,
|
||||
productivity: &HashMap<(String, String), Productivity>,
|
||||
shadow: &ShadowEconomy,
|
||||
archetypes: &HashMap<String, agents::Archetype>,
|
||||
nodes: &mut HashMap<String, NodeState>,
|
||||
) {
|
||||
// Process each active node independently (Layer 1: no inter-system trade)
|
||||
@@ -235,6 +238,15 @@ fn step(
|
||||
None => continue,
|
||||
};
|
||||
|
||||
// Layer 3: behavioral archetype parameters for this corporation
|
||||
let arch_params = archetypes
|
||||
.get(&corp_presence.corp_id)
|
||||
.map(|a| a.params())
|
||||
.unwrap_or_else(|| agents::Archetype::Producer.params());
|
||||
|
||||
// Effective baseline = BASELINE_CAPACITY scaled by archetype
|
||||
let effective_capacity = BASELINE_CAPACITY * arch_params.production_scale;
|
||||
|
||||
// Determine the tier of the primary_operation commodity
|
||||
let tier = economy
|
||||
.commodity_map
|
||||
@@ -244,10 +256,11 @@ fn step(
|
||||
|
||||
if tier == "raw" {
|
||||
// Raw materials: direct extraction — no chain inputs required (D-177).
|
||||
// Extraction rate multiplier applies.
|
||||
let output = BASELINE_CAPACITY * prod.extraction_rate;
|
||||
let gross_output = effective_capacity * prod.extraction_rate;
|
||||
// Monopolist withholds a fraction of output
|
||||
let net_output = gross_output * (1.0 - arch_params.supply_withheld);
|
||||
if let Some(state) = node.commodities.get_mut(&primary_op) {
|
||||
state.supply += output;
|
||||
state.supply += net_output;
|
||||
}
|
||||
} else {
|
||||
// Intermediate / final goods: run production chain with Leontief inputs.
|
||||
@@ -262,7 +275,7 @@ fn step(
|
||||
for input in &chain.inputs {
|
||||
if let Some(state) = node.commodities.get(&input.commodity_id) {
|
||||
let available = state.stockpile;
|
||||
let required = input.quantity * BASELINE_CAPACITY;
|
||||
let required = input.quantity * effective_capacity;
|
||||
if required > 0.0 {
|
||||
capacity_fraction = capacity_fraction
|
||||
.min(available / required)
|
||||
@@ -276,23 +289,39 @@ fn step(
|
||||
|
||||
// Apply productivity multiplier
|
||||
let prod_mult = prod.for_tier(&chain_output_tier(economy, chain));
|
||||
let actual_output =
|
||||
BASELINE_CAPACITY * chain.output_quantity * capacity_fraction * prod_mult;
|
||||
let gross_output =
|
||||
effective_capacity * chain.output_quantity * capacity_fraction * prod_mult;
|
||||
let net_output = gross_output * (1.0 - arch_params.supply_withheld);
|
||||
|
||||
// Consume inputs (Leontief: fixed-coefficient deduction)
|
||||
for input in &chain.inputs {
|
||||
if let Some(state) = node.commodities.get_mut(&input.commodity_id) {
|
||||
let consumed = input.quantity * BASELINE_CAPACITY * capacity_fraction;
|
||||
let consumed = input.quantity * effective_capacity * capacity_fraction;
|
||||
state.stockpile = (state.stockpile - consumed).max(0.0);
|
||||
}
|
||||
}
|
||||
|
||||
// Add output to supply
|
||||
// Add net output to supply
|
||||
if let Some(state) = node.commodities.get_mut(&chain.output_commodity_id) {
|
||||
state.supply += actual_output;
|
||||
state.supply += net_output;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Price premium: apply archetype price signal to primary commodity at this node.
|
||||
// Positive premium pushes price up; negative discounts it.
|
||||
// Applied as a small additive tâtonnement nudge capped to avoid instability.
|
||||
if arch_params.price_premium.abs() > 1e-6 {
|
||||
if let Some(state) = node.commodities.get_mut(&primary_op) {
|
||||
let base_price = economy
|
||||
.commodity_map
|
||||
.get(&primary_op)
|
||||
.map_or(1.0, |c| c.base_price);
|
||||
let nudge = base_price * arch_params.price_premium * ALPHA;
|
||||
state.price = (state.price + nudge)
|
||||
.clamp(base_price * 0.05, base_price * 20.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// --- Demand step ---
|
||||
|
||||
Reference in New Issue
Block a user