Files
settled-reach/tooling/econ-sim/src/agents.rs
T
jpmschweitzerandClaude Sonnet 4.6 5f4139bc9e feat(simulation): implement economics integration sprint — #810 #821 #822 #823
Implements the full D-180/D-181 economics pipeline:

**#810 — Event input port (D-180)**
- Add EconEvent struct with Target/Effect/Duration/Visibility variants
- Implement EventPort as typed input queue for external disruptions
- Apply events in simulation step; D-179 Test 3 now uses real shock injection

**#821 — Integrate econ-sim into server tick loop**
- Extract econ-sim as library crate (lib.rs + sim.rs, Cargo.toml [lib] section)
- Add Simulation stateful runner; step() advances one economy tick
- Add EconSimResource, EconStateResource (7 D-181 signals), tick_economy_simulation
- Economy loads once at startup; graceful no-op when systems.db absent
- Server advances economy 1 tick per 10 game ticks (D-031)

**#822 — Expose economy state over IPC bridge**
- Protocol version 20 → 21
- Add EconomySnapshot, EconNodeSnapshot wire types
- Add EconStateQuery PlayerAction variant; response in economy_snapshot field
- Add EconQueryBuffer resource + serve_econ_state_query system

**#823 — Economics debug commands**
- Add InjectEconEvent, SetEconParam, GetEconState to DebugCommandKind
- Add EconDebugEffect, EconParamKind enums
- SetEconParam mutates α/β at runtime (α/β promoted to pub const + Simulation fields)
- ALPHA and BETA constants threaded through step_inner/trade_step signatures

All 1147 unit tests pass; zero warnings.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-10 13:13:52 +02:00

175 lines
6.6 KiB
Rust

//! 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.
//!
//! The D-180 event port stubs previously in this file have been replaced by
//! the full implementation in `events.rs`.
use std::collections::BTreeMap;
// ---------------------------------------------------------------------------
// 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>)>,
) -> BTreeMap<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()
}