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:
@@ -0,0 +1,157 @@
|
||||
//! Layer 3: Corporate behavioral agents (D-178).
|
||||
//!
|
||||
//! Six behavioral archetypes from D-175 / Burnelli-Sheldon:
|
||||
//!
|
||||
//! Producer — maximises output, low trade aggression
|
||||
//! Distributor — volume-focused, aggressive trade, thin margin
|
||||
//! Specialist — premium pricing, narrow focus, low trade
|
||||
//! Monopolist — withholds supply to maintain scarcity premium
|
||||
//! Cooperative — fair pricing, community stability orientation
|
||||
//! Intermediary — arbitrage-focused, high trade, lower own production
|
||||
//!
|
||||
//! Archetypes are loaded from `corporations.behavioral_archetype` in the DB.
|
||||
//! If NULL, the archetype is inferred from the `specialization` field text.
|
||||
//!
|
||||
//! Parameters apply to per-corp production in each simulation tick.
|
||||
//! Trade-layer archetype effects (corp-level bid/ask) are deferred to a
|
||||
//! future sprint when the event port (D-180) and IPC bridge are in place.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Archetype enum
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Archetype {
|
||||
Producer,
|
||||
Distributor,
|
||||
Specialist,
|
||||
Monopolist,
|
||||
Cooperative,
|
||||
Intermediary,
|
||||
}
|
||||
|
||||
impl Archetype {
|
||||
/// Parse from DB string (case-insensitive).
|
||||
pub fn from_str(s: &str) -> Option<Self> {
|
||||
match s.to_lowercase().trim() {
|
||||
"producer" => Some(Archetype::Producer),
|
||||
"distributor" => Some(Archetype::Distributor),
|
||||
"specialist" => Some(Archetype::Specialist),
|
||||
"monopolist" => Some(Archetype::Monopolist),
|
||||
"cooperative" => Some(Archetype::Cooperative),
|
||||
"intermediary" => Some(Archetype::Intermediary),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer archetype from `specialization` field free text.
|
||||
///
|
||||
/// Heuristic: look for domain keywords that map to behavioral patterns.
|
||||
/// Falls back to `Producer` (the most neutral, maximises output).
|
||||
pub fn infer_from_specialization(spec: &str) -> Self {
|
||||
let s = spec.to_lowercase();
|
||||
if s.contains("freight") || s.contains("logistics") || s.contains("hauler") || s.contains("cargo") {
|
||||
Archetype::Distributor
|
||||
} else if s.contains("arbitr") || s.contains("trading company") || s.contains("brokerage") || s.contains("intermediar") {
|
||||
Archetype::Intermediary
|
||||
} else if s.contains("cooperative") || s.contains("mutu") || s.contains("negociant") {
|
||||
Archetype::Cooperative
|
||||
} else if s.contains("whisky") || s.contains("wine") || s.contains("lager") || s.contains("precision") || s.contains("bespoke") || s.contains("longevity") {
|
||||
Archetype::Specialist
|
||||
} else if s.contains("infrastructure") && (s.contains("gate") || s.contains("span")) {
|
||||
// Gate Corp maintains infrastructure monopoly
|
||||
Archetype::Monopolist
|
||||
} else {
|
||||
Archetype::Producer
|
||||
}
|
||||
}
|
||||
|
||||
/// Behavioral parameters for this archetype.
|
||||
pub fn params(self) -> ArchetypeParams {
|
||||
match self {
|
||||
// Producer: higher output, normal trade participation
|
||||
Archetype::Producer => ArchetypeParams {
|
||||
production_scale: 1.15,
|
||||
supply_withheld: 0.0,
|
||||
price_premium: 0.0,
|
||||
},
|
||||
// Distributor: leaner production, price discount to move volume
|
||||
Archetype::Distributor => ArchetypeParams {
|
||||
production_scale: 0.90,
|
||||
supply_withheld: 0.0,
|
||||
price_premium: -0.03,
|
||||
},
|
||||
// Specialist: normal production, commands a premium
|
||||
Archetype::Specialist => ArchetypeParams {
|
||||
production_scale: 1.0,
|
||||
supply_withheld: 0.0,
|
||||
price_premium: 0.10,
|
||||
},
|
||||
// Monopolist: constrained output, withholds supply, premium
|
||||
Archetype::Monopolist => ArchetypeParams {
|
||||
production_scale: 0.80,
|
||||
supply_withheld: 0.25,
|
||||
price_premium: 0.20,
|
||||
},
|
||||
// Cooperative: normal production, slight discount for community access
|
||||
Archetype::Cooperative => ArchetypeParams {
|
||||
production_scale: 1.0,
|
||||
supply_withheld: 0.0,
|
||||
price_premium: -0.05,
|
||||
},
|
||||
// Intermediary: lower own production, relies on traded goods
|
||||
Archetype::Intermediary => ArchetypeParams {
|
||||
production_scale: 0.70,
|
||||
supply_withheld: 0.0,
|
||||
price_premium: -0.01,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Parameter struct
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Per-tick behavioral parameters for a corporation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ArchetypeParams {
|
||||
/// Multiplier on BASELINE_CAPACITY for this corp's production.
|
||||
pub production_scale: f64,
|
||||
/// Fraction of this tick's output that is withheld from the node's
|
||||
/// stockpile (Monopolist strategy). Range [0.0, 1.0].
|
||||
pub supply_withheld: f64,
|
||||
/// Additive price premium on goods this corp produces.
|
||||
/// Applied to the node price signal for their primary commodity.
|
||||
/// Positive → price pressure up. Negative → price pressure down.
|
||||
pub price_premium: f64,
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Corpus load
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Build archetype map from the raw DB data supplied by the caller.
|
||||
///
|
||||
/// `corp_data`: Vec of (corp_id, behavioral_archetype_opt, specialization_opt)
|
||||
pub fn build_archetype_map(
|
||||
corp_data: Vec<(String, Option<String>, Option<String>)>,
|
||||
) -> HashMap<String, Archetype> {
|
||||
corp_data
|
||||
.into_iter()
|
||||
.map(|(corp_id, archetype_str, specialization)| {
|
||||
let archetype = archetype_str
|
||||
.as_deref()
|
||||
.and_then(Archetype::from_str)
|
||||
.unwrap_or_else(|| {
|
||||
specialization
|
||||
.as_deref()
|
||||
.map(Archetype::infer_from_specialization)
|
||||
.unwrap_or(Archetype::Producer)
|
||||
});
|
||||
(corp_id, archetype)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
Reference in New Issue
Block a user