Skip to main content

proof_engine/economy/
factions.rs

1//! Faction economy simulation.
2//!
3//! Each faction has a treasury funded by taxation, income from trade routes,
4//! and reparations/tribute. Factions can impose embargoes, conduct economic
5//! espionage, and rank themselves by total wealth.
6
7use std::collections::{HashMap, VecDeque};
8
9// ---------------------------------------------------------------------------
10// IDs
11// ---------------------------------------------------------------------------
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub struct FactionId(pub u32);
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
17pub struct TradeRouteId(pub u32);
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub struct TributeAgreementId(pub u32);
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
23pub struct EspionageOperationId(pub u32);
24
25// ---------------------------------------------------------------------------
26// Treasury
27// ---------------------------------------------------------------------------
28
29/// A faction's treasury: tracks gold reserves, income, and expenditure.
30#[derive(Debug, Clone)]
31pub struct FactionTreasury {
32    pub faction: FactionId,
33    /// Current gold balance.
34    pub gold: f64,
35    /// Total gold received over the faction's lifetime.
36    pub total_income: f64,
37    /// Total gold spent over the faction's lifetime.
38    pub total_expenditure: f64,
39    /// Per-category income breakdown, last 32 entries per category.
40    income_log: HashMap<String, VecDeque<f64>>,
41    /// Per-category expenditure breakdown.
42    expenditure_log: HashMap<String, VecDeque<f64>>,
43}
44
45impl FactionTreasury {
46    pub fn new(faction: FactionId, starting_gold: f64) -> Self {
47        Self {
48            faction,
49            gold: starting_gold,
50            total_income: starting_gold,
51            total_expenditure: 0.0,
52            income_log: HashMap::new(),
53            expenditure_log: HashMap::new(),
54        }
55    }
56
57    /// Add income and log it under a category label.
58    pub fn receive(&mut self, amount: f64, category: &str) {
59        self.gold += amount;
60        self.total_income += amount;
61        let log = self.income_log.entry(category.to_string()).or_insert_with(|| VecDeque::with_capacity(32));
62        log.push_back(amount);
63        if log.len() > 32 { log.pop_front(); }
64    }
65
66    /// Deduct an expenditure. Returns false if insufficient funds (still deducts to allow debt).
67    pub fn spend(&mut self, amount: f64, category: &str) -> bool {
68        let had_enough = self.gold >= amount;
69        self.gold -= amount;
70        self.total_expenditure += amount;
71        let log = self.expenditure_log.entry(category.to_string()).or_insert_with(|| VecDeque::with_capacity(32));
72        log.push_back(amount);
73        if log.len() > 32 { log.pop_front(); }
74        had_enough
75    }
76
77    /// Moving average income for a category over last n entries.
78    pub fn avg_income(&self, category: &str, n: usize) -> f64 {
79        match self.income_log.get(category) {
80            None => 0.0,
81            Some(log) => {
82                let slice: Vec<_> = log.iter().rev().take(n).collect();
83                if slice.is_empty() { return 0.0; }
84                slice.iter().copied().sum::<f64>() / slice.len() as f64
85            }
86        }
87    }
88
89    /// Net income estimate: mean income across all categories per tick.
90    pub fn net_income_rate(&self) -> f64 {
91        let income: f64 = self.income_log.values()
92            .filter_map(|log| {
93                if log.is_empty() { return None; }
94                Some(log.iter().sum::<f64>() / log.len() as f64)
95            })
96            .sum();
97        let expenditure: f64 = self.expenditure_log.values()
98            .filter_map(|log| {
99                if log.is_empty() { return None; }
100                Some(log.iter().sum::<f64>() / log.len() as f64)
101            })
102            .sum();
103        income - expenditure
104    }
105}
106
107// ---------------------------------------------------------------------------
108// Tax Policy
109// ---------------------------------------------------------------------------
110
111/// The tax regime applied to a faction's subjects.
112#[derive(Debug, Clone)]
113pub struct TaxPolicy {
114    pub faction: FactionId,
115    /// Flat tax rate applied to all income (0.0 – 1.0).
116    pub flat_rate: f64,
117    /// Progressive brackets: (threshold, marginal_rate).
118    pub brackets: Vec<(f64, f64)>,
119    /// Percentage of trade route income taxed.
120    pub trade_tax_rate: f64,
121    /// Import duty (applied to value of goods arriving from other factions).
122    pub import_duty: f64,
123    /// Export duty.
124    pub export_duty: f64,
125    /// Luxury tax multiplier (applied to items tagged as luxury in markets).
126    pub luxury_multiplier: f64,
127    /// Tax evasion estimate (fraction that slips through).
128    pub evasion_rate: f64,
129}
130
131impl TaxPolicy {
132    pub fn default_for(faction: FactionId) -> Self {
133        Self {
134            faction,
135            flat_rate: 0.15,
136            brackets: vec![(1000.0, 0.10), (5000.0, 0.20), (20000.0, 0.35)],
137            trade_tax_rate: 0.10,
138            import_duty: 0.05,
139            export_duty: 0.03,
140            luxury_multiplier: 1.5,
141            evasion_rate: 0.05,
142        }
143    }
144
145    /// Compute tax owed on `income` using progressive brackets.
146    pub fn compute_tax(&self, income: f64) -> f64 {
147        let mut tax = 0.0;
148        let mut remaining = income;
149        let mut prev_threshold = 0.0;
150        for &(threshold, rate) in &self.brackets {
151            if remaining <= 0.0 { break; }
152            let bracket_income = (threshold - prev_threshold).min(remaining);
153            tax += bracket_income * rate;
154            remaining -= bracket_income;
155            prev_threshold = threshold;
156        }
157        if remaining > 0.0 {
158            tax += remaining * self.flat_rate;
159        }
160        // Subtract evasion
161        tax * (1.0 - self.evasion_rate)
162    }
163
164    /// Tax on a trade transaction.
165    pub fn trade_tax(&self, value: f64, is_import: bool, is_luxury: bool) -> f64 {
166        let duty = if is_import { self.import_duty } else { self.export_duty };
167        let base = value * (self.trade_tax_rate + duty);
168        let lux = if is_luxury { base * self.luxury_multiplier } else { base };
169        lux * (1.0 - self.evasion_rate)
170    }
171}
172
173// ---------------------------------------------------------------------------
174// Trade Routes
175// ---------------------------------------------------------------------------
176
177#[derive(Debug, Clone, Copy, PartialEq, Eq)]
178pub enum TradeRouteStatus {
179    Active,
180    Disrupted,
181    Embargoed,
182    Closed,
183}
184
185/// A trade route connecting two factions, carrying goods in both directions.
186#[derive(Debug, Clone)]
187pub struct TradeRoute {
188    pub id: TradeRouteId,
189    pub from_faction: FactionId,
190    pub to_faction: FactionId,
191    pub status: TradeRouteStatus,
192    /// Base gold value transacted per tick.
193    pub base_volume: f64,
194    /// Current actual volume (may differ due to disruptions).
195    pub current_volume: f64,
196    /// Disruption factor (0.0 = fully blocked, 1.0 = full volume).
197    pub disruption: f64,
198    /// Fraction of volume that goes to `from_faction` as export income.
199    pub export_share_from: f64,
200    /// Fraction of volume that goes to `to_faction` as import income.
201    pub import_share_to: f64,
202    /// Ticks this route has been active.
203    pub age_ticks: u64,
204    /// History of per-tick volumes.
205    pub volume_history: VecDeque<f64>,
206    /// Whether a military escort is assigned (reduces disruption).
207    pub escorted: bool,
208    /// Infrastructure investment level (improves volume and reduces disruption risk).
209    pub infrastructure: f64,
210}
211
212impl TradeRoute {
213    pub fn new(
214        id: TradeRouteId,
215        from: FactionId,
216        to: FactionId,
217        base_volume: f64,
218        export_share: f64,
219        import_share: f64,
220    ) -> Self {
221        Self {
222            id,
223            from_faction: from,
224            to_faction: to,
225            status: TradeRouteStatus::Active,
226            base_volume,
227            current_volume: base_volume,
228            disruption: 1.0,
229            export_share_from: export_share,
230            import_share_to: import_share,
231            age_ticks: 0,
232            volume_history: VecDeque::with_capacity(64),
233            escorted: false,
234            infrastructure: 0.0,
235        }
236    }
237
238    /// Apply a disruption event (bandits, war, weather). Value in [0,1].
239    pub fn apply_disruption(&mut self, severity: f64) {
240        let mitigation = if self.escorted { 0.5 } else { 0.0 };
241        let infra_mitigation = (self.infrastructure / 200.0).min(0.3);
242        self.disruption = (self.disruption - severity * (1.0 - mitigation - infra_mitigation)).max(0.0);
243        self.update_status();
244    }
245
246    /// Recover disruption over time.
247    pub fn recover(&mut self, rate: f64) {
248        self.disruption = (self.disruption + rate).min(1.0);
249        self.update_status();
250    }
251
252    fn update_status(&mut self) {
253        if self.status == TradeRouteStatus::Embargoed || self.status == TradeRouteStatus::Closed {
254            return;
255        }
256        self.status = if self.disruption < 0.1 {
257            TradeRouteStatus::Disrupted
258        } else {
259            TradeRouteStatus::Active
260        };
261    }
262
263    /// Tick: compute actual volume and return (export_income, import_income).
264    pub fn tick(&mut self) -> (f64, f64) {
265        self.age_ticks += 1;
266        if self.status == TradeRouteStatus::Embargoed || self.status == TradeRouteStatus::Closed {
267            self.current_volume = 0.0;
268            self.volume_history.push_back(0.0);
269            if self.volume_history.len() > 64 { self.volume_history.pop_front(); }
270            return (0.0, 0.0);
271        }
272        // Infrastructure bonus: each 100 infra adds 5% volume
273        let infra_bonus = 1.0 + (self.infrastructure / 100.0) * 0.05;
274        self.current_volume = self.base_volume * self.disruption * infra_bonus;
275        self.volume_history.push_back(self.current_volume);
276        if self.volume_history.len() > 64 { self.volume_history.pop_front(); }
277        let export_income = self.current_volume * self.export_share_from;
278        let import_income = self.current_volume * self.import_share_to;
279        (export_income, import_income)
280    }
281
282    /// Average volume over last n ticks.
283    pub fn avg_volume(&self, n: usize) -> f64 {
284        let slice: Vec<_> = self.volume_history.iter().rev().take(n).collect();
285        if slice.is_empty() { return 0.0; }
286        slice.iter().copied().sum::<f64>() / slice.len() as f64
287    }
288}
289
290// ---------------------------------------------------------------------------
291// Embargo / Sanctions
292// ---------------------------------------------------------------------------
293
294/// Penalty applied by one faction against another.
295#[derive(Debug, Clone)]
296pub struct EmbargoPenalty {
297    pub imposing_faction: FactionId,
298    pub target_faction: FactionId,
299    /// All trade routes between these two factions are closed.
300    pub blocks_trade: bool,
301    /// Asset freeze: prevents target from spending from accounts held jointly.
302    pub asset_freeze: bool,
303    /// Tariff surcharge applied on top of normal duties (0.0 – 1.0).
304    pub extra_tariff: f64,
305    /// Tick when imposed.
306    pub imposed_tick: u64,
307    /// Tick when lifted (None = indefinite).
308    pub lifted_tick: Option<u64>,
309    pub reason: String,
310}
311
312// ---------------------------------------------------------------------------
313// War Reparations
314// ---------------------------------------------------------------------------
315
316#[derive(Debug, Clone)]
317pub struct WarReparations {
318    pub paying_faction: FactionId,
319    pub receiving_faction: FactionId,
320    /// Total amount owed.
321    pub total_amount: f64,
322    /// Amount paid so far.
323    pub paid: f64,
324    /// Per-tick installment.
325    pub installment: f64,
326    pub imposed_tick: u64,
327    pub completed: bool,
328}
329
330impl WarReparations {
331    pub fn new(payer: FactionId, receiver: FactionId, total: f64, installment: f64, tick: u64) -> Self {
332        Self {
333            paying_faction: payer,
334            receiving_faction: receiver,
335            total_amount: total,
336            paid: 0.0,
337            installment,
338            imposed_tick: tick,
339            completed: false,
340        }
341    }
342
343    /// Process one installment. Returns amount actually paid.
344    pub fn process(&mut self) -> f64 {
345        if self.completed { return 0.0; }
346        let amount = self.installment.min(self.total_amount - self.paid);
347        self.paid += amount;
348        if self.paid >= self.total_amount - 1e-9 { self.completed = true; }
349        amount
350    }
351
352    pub fn remaining(&self) -> f64 {
353        self.total_amount - self.paid
354    }
355}
356
357// ---------------------------------------------------------------------------
358// Tribute System
359// ---------------------------------------------------------------------------
360
361/// A recurring tribute agreement between two factions.
362#[derive(Debug, Clone)]
363pub struct TributeAgreement {
364    pub id: TributeAgreementId,
365    pub paying_faction: FactionId,
366    pub receiving_faction: FactionId,
367    /// Amount paid per tick.
368    pub amount_per_tick: f64,
369    /// Tick when agreement starts.
370    pub start_tick: u64,
371    /// Duration in ticks (None = indefinite).
372    pub duration: Option<u64>,
373    pub total_paid: f64,
374    pub active: bool,
375    /// Coercion factor: if > 0, paying faction has no choice but to pay.
376    pub coercion_level: f64,
377    /// Chance per tick of the paying faction rebelling (refusing tribute).
378    pub rebellion_chance: f64,
379}
380
381impl TributeAgreement {
382    /// Process one tick of tribute. Returns amount paid or 0 if rebelled/ended.
383    pub fn process(&mut self, current_tick: u64, rng_val: f64) -> f64 {
384        if !self.active { return 0.0; }
385        if let Some(dur) = self.duration {
386            if current_tick >= self.start_tick + dur {
387                self.active = false;
388                return 0.0;
389            }
390        }
391        // Check rebellion (only if coercion is low)
392        if self.coercion_level < 0.5 && rng_val < self.rebellion_chance {
393            self.active = false;
394            return 0.0;
395        }
396        self.total_paid += self.amount_per_tick;
397        self.amount_per_tick
398    }
399}
400
401// ---------------------------------------------------------------------------
402// Economic Espionage
403// ---------------------------------------------------------------------------
404
405#[derive(Debug, Clone, Copy, PartialEq, Eq)]
406pub enum EspionageObjective {
407    /// Steal treasury balance information.
408    IntelTreasury,
409    /// Sabotage a trade route.
410    SabotageRoute,
411    /// Steal a tax policy document.
412    StealTaxPolicy,
413    /// Plant misinformation to inflate enemy trade costs.
414    Disinformation,
415    /// Corrupt production facilities (handled by production module).
416    CorruptProduction,
417}
418
419#[derive(Debug, Clone)]
420pub struct EspionageOperation {
421    pub id: EspionageOperationId,
422    pub instigator: FactionId,
423    pub target: FactionId,
424    pub objective: EspionageObjective,
425    pub started_tick: u64,
426    pub completion_tick: u64,
427    pub success_probability: f64,
428    pub resolved: bool,
429    pub succeeded: Option<bool>,
430}
431
432/// The outcome of a resolved espionage operation.
433#[derive(Debug, Clone)]
434pub struct EspionageReport {
435    pub operation_id: EspionageOperationId,
436    pub instigator: FactionId,
437    pub target: FactionId,
438    pub objective: EspionageObjective,
439    pub succeeded: bool,
440    pub resolved_tick: u64,
441    pub intel: Option<EspionageIntel>,
442}
443
444#[derive(Debug, Clone)]
445pub enum EspionageIntel {
446    TreasuryBalance(f64),
447    TaxRates { flat: f64, trade: f64, import_duty: f64 },
448    RouteDisrupted { route_id: TradeRouteId, disruption: f64 },
449    DisinformationPlanted,
450    ProductionCorrupted,
451}
452
453// ---------------------------------------------------------------------------
454// Wealth Ranking
455// ---------------------------------------------------------------------------
456
457#[derive(Debug, Clone)]
458pub struct WealthRanking {
459    /// Factions ordered from richest to poorest.
460    pub ranked: Vec<(FactionId, f64)>,
461    pub computed_tick: u64,
462}
463
464impl WealthRanking {
465    /// The wealthiest faction.
466    pub fn richest(&self) -> Option<(FactionId, f64)> {
467        self.ranked.first().copied()
468    }
469
470    /// The poorest faction.
471    pub fn poorest(&self) -> Option<(FactionId, f64)> {
472        self.ranked.last().copied()
473    }
474
475    /// Rank of a specific faction (1 = richest).
476    pub fn rank_of(&self, faction: FactionId) -> Option<usize> {
477        self.ranked.iter().position(|(id, _)| *id == faction).map(|i| i + 1)
478    }
479
480    /// Gini coefficient (0 = perfect equality, 1 = total inequality).
481    pub fn gini(&self) -> f64 {
482        let n = self.ranked.len();
483        if n == 0 { return 0.0; }
484        let mut sorted: Vec<f64> = self.ranked.iter().map(|(_, w)| w.max(0.0)).collect();
485        sorted.sort_by(|a, b| a.partial_cmp(b).unwrap());
486        let total: f64 = sorted.iter().sum();
487        if total < 1e-9 { return 0.0; }
488        let sum_of_ranks: f64 = sorted.iter().enumerate()
489            .map(|(i, w)| (2 * (i as i64 + 1) - n as i64 - 1) as f64 * w)
490            .sum();
491        sum_of_ranks / (n as f64 * total)
492    }
493}
494
495// ---------------------------------------------------------------------------
496// Faction descriptor
497// ---------------------------------------------------------------------------
498
499/// Full descriptor for a single faction in the economy.
500#[derive(Debug, Clone)]
501pub struct Faction {
502    pub id: FactionId,
503    pub name: String,
504    pub treasury: FactionTreasury,
505    pub tax_policy: TaxPolicy,
506    /// IDs of trade routes this faction participates in.
507    pub trade_route_ids: Vec<TradeRouteId>,
508    /// Factions this faction has imposed embargoes on.
509    pub embargoes: Vec<EmbargoPenalty>,
510    /// Active reparations this faction must pay.
511    pub reparations_paying: Vec<WarReparations>,
512    /// Reparations this faction is receiving.
513    pub reparations_receiving: Vec<WarReparations>,
514    /// Tribute agreements where this faction pays.
515    pub tribute_paying: Vec<TributeAgreementId>,
516    /// Tribute agreements where this faction receives.
517    pub tribute_receiving: Vec<TributeAgreementId>,
518    /// Ongoing espionage operations this faction launched.
519    pub espionage_ops: Vec<EspionageOperationId>,
520    /// Disinformation penalty to outgoing trade routes (fraction of volume lost).
521    pub disinformation_penalty: f64,
522    /// Cumulative GDP proxy (sum of all trade route volumes over lifetime).
523    pub gdp_proxy: f64,
524    /// Whether this faction is at war with any others.
525    pub at_war_with: Vec<FactionId>,
526}
527
528impl Faction {
529    pub fn new(id: FactionId, name: &str, starting_gold: f64) -> Self {
530        Self {
531            id,
532            name: name.to_string(),
533            treasury: FactionTreasury::new(id, starting_gold),
534            tax_policy: TaxPolicy::default_for(id),
535            trade_route_ids: Vec::new(),
536            embargoes: Vec::new(),
537            reparations_paying: Vec::new(),
538            reparations_receiving: Vec::new(),
539            tribute_paying: Vec::new(),
540            tribute_receiving: Vec::new(),
541            espionage_ops: Vec::new(),
542            disinformation_penalty: 0.0,
543            gdp_proxy: 0.0,
544            at_war_with: Vec::new(),
545        }
546    }
547
548    pub fn net_worth(&self) -> f64 {
549        self.treasury.gold
550    }
551}
552
553// ---------------------------------------------------------------------------
554// FactionEconomy — the top-level manager
555// ---------------------------------------------------------------------------
556
557/// Manages all factions, their trade routes, tribute, reparations,
558/// espionage operations, and wealth rankings.
559pub struct FactionEconomy {
560    next_faction_id: u32,
561    next_route_id: u32,
562    next_tribute_id: u32,
563    next_espionage_id: u32,
564    pub current_tick: u64,
565
566    pub factions: HashMap<FactionId, Faction>,
567    pub trade_routes: HashMap<TradeRouteId, TradeRoute>,
568    pub tribute_agreements: HashMap<TributeAgreementId, TributeAgreement>,
569    pub espionage_ops: HashMap<EspionageOperationId, EspionageOperation>,
570    pub espionage_reports: Vec<EspionageReport>,
571    pub wealth_ranking: Option<WealthRanking>,
572
573    /// Simple deterministic pseudo-random state for rebellion/espionage rolls.
574    rng_state: u64,
575}
576
577impl FactionEconomy {
578    pub fn new() -> Self {
579        Self {
580            next_faction_id: 1,
581            next_route_id: 1,
582            next_tribute_id: 1,
583            next_espionage_id: 1,
584            current_tick: 0,
585            factions: HashMap::new(),
586            trade_routes: HashMap::new(),
587            tribute_agreements: HashMap::new(),
588            espionage_ops: HashMap::new(),
589            espionage_reports: Vec::new(),
590            wealth_ranking: None,
591            rng_state: 0xDEAD_BEEF_CAFE_1234,
592        }
593    }
594
595    // Simple xorshift64 for deterministic rolls
596    fn next_rand(&mut self) -> f64 {
597        let mut x = self.rng_state;
598        x ^= x << 13;
599        x ^= x >> 7;
600        x ^= x << 17;
601        self.rng_state = x;
602        (x as f64) / (u64::MAX as f64)
603    }
604
605    // -----------------------------------------------------------------------
606    // Faction Management
607    // -----------------------------------------------------------------------
608
609    pub fn add_faction(&mut self, name: &str, starting_gold: f64) -> FactionId {
610        let id = FactionId(self.next_faction_id);
611        self.next_faction_id += 1;
612        self.factions.insert(id, Faction::new(id, name, starting_gold));
613        id
614    }
615
616    pub fn faction(&self, id: FactionId) -> Option<&Faction> {
617        self.factions.get(&id)
618    }
619
620    pub fn faction_mut(&mut self, id: FactionId) -> Option<&mut Faction> {
621        self.factions.get_mut(&id)
622    }
623
624    /// Declare war between two factions (disables existing trade routes between them).
625    pub fn declare_war(&mut self, faction_a: FactionId, faction_b: FactionId) {
626        if let Some(f) = self.factions.get_mut(&faction_a) {
627            if !f.at_war_with.contains(&faction_b) { f.at_war_with.push(faction_b); }
628        }
629        if let Some(f) = self.factions.get_mut(&faction_b) {
630            if !f.at_war_with.contains(&faction_a) { f.at_war_with.push(faction_a); }
631        }
632        // Close trade routes between them
633        let tick = self.current_tick;
634        for route in self.trade_routes.values_mut() {
635            let involves_a = route.from_faction == faction_a || route.to_faction == faction_a;
636            let involves_b = route.from_faction == faction_b || route.to_faction == faction_b;
637            if involves_a && involves_b {
638                route.status = TradeRouteStatus::Closed;
639                let _ = tick; // suppress unused warning
640            }
641        }
642    }
643
644    /// End war between two factions.
645    pub fn end_war(&mut self, faction_a: FactionId, faction_b: FactionId) {
646        if let Some(f) = self.factions.get_mut(&faction_a) {
647            f.at_war_with.retain(|&id| id != faction_b);
648        }
649        if let Some(f) = self.factions.get_mut(&faction_b) {
650            f.at_war_with.retain(|&id| id != faction_a);
651        }
652    }
653
654    // -----------------------------------------------------------------------
655    // Trade Routes
656    // -----------------------------------------------------------------------
657
658    pub fn add_trade_route(
659        &mut self,
660        from: FactionId,
661        to: FactionId,
662        base_volume: f64,
663        export_share: f64,
664        import_share: f64,
665    ) -> TradeRouteId {
666        let id = TradeRouteId(self.next_route_id);
667        self.next_route_id += 1;
668        let route = TradeRoute::new(id, from, to, base_volume, export_share, import_share);
669        self.trade_routes.insert(id, route);
670        if let Some(f) = self.factions.get_mut(&from) { f.trade_route_ids.push(id); }
671        if let Some(f) = self.factions.get_mut(&to) { f.trade_route_ids.push(id); }
672        id
673    }
674
675    pub fn disrupt_route(&mut self, id: TradeRouteId, severity: f64) {
676        if let Some(route) = self.trade_routes.get_mut(&id) {
677            route.apply_disruption(severity);
678        }
679    }
680
681    pub fn close_route(&mut self, id: TradeRouteId) {
682        if let Some(route) = self.trade_routes.get_mut(&id) {
683            route.status = TradeRouteStatus::Closed;
684        }
685    }
686
687    pub fn reopen_route(&mut self, id: TradeRouteId) {
688        if let Some(route) = self.trade_routes.get_mut(&id) {
689            if route.status == TradeRouteStatus::Closed {
690                route.status = TradeRouteStatus::Active;
691            }
692        }
693    }
694
695    pub fn invest_infrastructure(&mut self, id: TradeRouteId, amount: f64) {
696        if let Some(route) = self.trade_routes.get_mut(&id) {
697            route.infrastructure += amount;
698        }
699    }
700
701    // -----------------------------------------------------------------------
702    // Embargo / Sanctions
703    // -----------------------------------------------------------------------
704
705    pub fn impose_embargo(
706        &mut self,
707        imposing: FactionId,
708        target: FactionId,
709        extra_tariff: f64,
710        blocks_trade: bool,
711        asset_freeze: bool,
712        reason: &str,
713    ) {
714        let tick = self.current_tick;
715        let embargo = EmbargoPenalty {
716            imposing_faction: imposing,
717            target_faction: target,
718            blocks_trade,
719            asset_freeze,
720            extra_tariff,
721            imposed_tick: tick,
722            lifted_tick: None,
723            reason: reason.to_string(),
724        };
725        if blocks_trade {
726            // Mark all routes between them as embargoed
727            for route in self.trade_routes.values_mut() {
728                let involves_i = route.from_faction == imposing || route.to_faction == imposing;
729                let involves_t = route.from_faction == target || route.to_faction == target;
730                if involves_i && involves_t {
731                    route.status = TradeRouteStatus::Embargoed;
732                }
733            }
734        }
735        if let Some(f) = self.factions.get_mut(&imposing) {
736            f.embargoes.push(embargo);
737        }
738    }
739
740    pub fn lift_embargo(&mut self, imposing: FactionId, target: FactionId) {
741        let tick = self.current_tick;
742        if let Some(f) = self.factions.get_mut(&imposing) {
743            for emb in f.embargoes.iter_mut() {
744                if emb.target_faction == target && emb.lifted_tick.is_none() {
745                    emb.lifted_tick = Some(tick);
746                }
747            }
748        }
749        // Restore routes if both sides agree
750        for route in self.trade_routes.values_mut() {
751            let involves_i = route.from_faction == imposing || route.to_faction == imposing;
752            let involves_t = route.from_faction == target || route.to_faction == target;
753            if involves_i && involves_t && route.status == TradeRouteStatus::Embargoed {
754                route.status = TradeRouteStatus::Active;
755            }
756        }
757    }
758
759    // -----------------------------------------------------------------------
760    // War Reparations
761    // -----------------------------------------------------------------------
762
763    pub fn impose_reparations(
764        &mut self,
765        payer: FactionId,
766        receiver: FactionId,
767        total: f64,
768        installment: f64,
769    ) {
770        let tick = self.current_tick;
771        let reps = WarReparations::new(payer, receiver, total, installment, tick);
772        if let Some(f) = self.factions.get_mut(&payer) {
773            f.reparations_paying.push(reps.clone());
774        }
775        if let Some(f) = self.factions.get_mut(&receiver) {
776            f.reparations_receiving.push(reps);
777        }
778    }
779
780    // -----------------------------------------------------------------------
781    // Tribute
782    // -----------------------------------------------------------------------
783
784    pub fn add_tribute(
785        &mut self,
786        payer: FactionId,
787        receiver: FactionId,
788        amount_per_tick: f64,
789        duration: Option<u64>,
790        coercion_level: f64,
791        rebellion_chance: f64,
792    ) -> TributeAgreementId {
793        let id = TributeAgreementId(self.next_tribute_id);
794        self.next_tribute_id += 1;
795        let tick = self.current_tick;
796        let agreement = TributeAgreement {
797            id,
798            paying_faction: payer,
799            receiving_faction: receiver,
800            amount_per_tick,
801            start_tick: tick,
802            duration,
803            total_paid: 0.0,
804            active: true,
805            coercion_level,
806            rebellion_chance,
807        };
808        self.tribute_agreements.insert(id, agreement);
809        if let Some(f) = self.factions.get_mut(&payer) { f.tribute_paying.push(id); }
810        if let Some(f) = self.factions.get_mut(&receiver) { f.tribute_receiving.push(id); }
811        id
812    }
813
814    // -----------------------------------------------------------------------
815    // Espionage
816    // -----------------------------------------------------------------------
817
818    /// Launch an espionage operation. It completes after `duration_ticks`.
819    pub fn launch_espionage(
820        &mut self,
821        instigator: FactionId,
822        target: FactionId,
823        objective: EspionageObjective,
824        success_probability: f64,
825        duration_ticks: u64,
826    ) -> EspionageOperationId {
827        let id = EspionageOperationId(self.next_espionage_id);
828        self.next_espionage_id += 1;
829        let tick = self.current_tick;
830        let op = EspionageOperation {
831            id,
832            instigator,
833            target,
834            objective,
835            started_tick: tick,
836            completion_tick: tick + duration_ticks,
837            success_probability,
838            resolved: false,
839            succeeded: None,
840        };
841        self.espionage_ops.insert(id, op);
842        if let Some(f) = self.factions.get_mut(&instigator) {
843            f.espionage_ops.push(id);
844        }
845        id
846    }
847
848    /// Resolve operations that have completed this tick.
849    fn resolve_espionage(&mut self) {
850        let tick = self.current_tick;
851        let ready: Vec<EspionageOperationId> = self.espionage_ops.iter()
852            .filter(|(_, op)| !op.resolved && op.completion_tick <= tick)
853            .map(|(id, _)| *id)
854            .collect();
855
856        for op_id in ready {
857            let roll = self.next_rand();
858            let op = match self.espionage_ops.get_mut(&op_id) {
859                Some(o) => o,
860                None => continue,
861            };
862            let success = roll < op.success_probability;
863            op.resolved = true;
864            op.succeeded = Some(success);
865
866            let instigator = op.instigator;
867            let target = op.target;
868            let objective = op.objective;
869
870            let intel = if success {
871                match objective {
872                    EspionageObjective::IntelTreasury => {
873                        let balance = self.factions.get(&target)
874                            .map(|f| f.treasury.gold)
875                            .unwrap_or(0.0);
876                        Some(EspionageIntel::TreasuryBalance(balance))
877                    }
878                    EspionageObjective::StealTaxPolicy => {
879                        let (flat, trade, import) = self.factions.get(&target)
880                            .map(|f| (f.tax_policy.flat_rate, f.tax_policy.trade_tax_rate, f.tax_policy.import_duty))
881                            .unwrap_or((0.0, 0.0, 0.0));
882                        Some(EspionageIntel::TaxRates { flat, trade, import_duty: import })
883                    }
884                    EspionageObjective::SabotageRoute => {
885                        // Find a random route involving target
886                        let route_ids: Vec<TradeRouteId> = self.trade_routes.keys()
887                            .filter(|&&rid| {
888                                let r = &self.trade_routes[&rid];
889                                r.from_faction == target || r.to_faction == target
890                            })
891                            .copied()
892                            .collect();
893                        if let Some(&rid) = route_ids.first() {
894                            let disruption_applied = 0.4;
895                            if let Some(route) = self.trade_routes.get_mut(&rid) {
896                                route.apply_disruption(disruption_applied);
897                                Some(EspionageIntel::RouteDisrupted { route_id: rid, disruption: route.disruption })
898                            } else {
899                                None
900                            }
901                        } else {
902                            None
903                        }
904                    }
905                    EspionageObjective::Disinformation => {
906                        if let Some(f) = self.factions.get_mut(&target) {
907                            f.disinformation_penalty = (f.disinformation_penalty + 0.15).min(0.5);
908                        }
909                        Some(EspionageIntel::DisinformationPlanted)
910                    }
911                    EspionageObjective::CorruptProduction => {
912                        Some(EspionageIntel::ProductionCorrupted)
913                    }
914                }
915            } else {
916                None
917            };
918
919            self.espionage_reports.push(EspionageReport {
920                operation_id: op_id,
921                instigator,
922                target,
923                objective,
924                succeeded: success,
925                resolved_tick: tick,
926                intel,
927            });
928        }
929        // Keep reports to last 128
930        if self.espionage_reports.len() > 128 {
931            let drain = self.espionage_reports.len() - 128;
932            self.espionage_reports.drain(0..drain);
933        }
934    }
935
936    // -----------------------------------------------------------------------
937    // Wealth Ranking
938    // -----------------------------------------------------------------------
939
940    pub fn compute_wealth_ranking(&mut self) {
941        let tick = self.current_tick;
942        let mut ranked: Vec<(FactionId, f64)> = self.factions.iter()
943            .map(|(&id, f)| (id, f.net_worth()))
944            .collect();
945        ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
946        self.wealth_ranking = Some(WealthRanking { ranked, computed_tick: tick });
947    }
948
949    // -----------------------------------------------------------------------
950    // Taxation Collection
951    // -----------------------------------------------------------------------
952
953    /// Collect taxes for all factions based on their trade route income.
954    fn collect_taxes(&mut self, route_incomes: &HashMap<FactionId, f64>) {
955        for (&faction_id, &income) in route_incomes {
956            let tax = self.factions.get(&faction_id)
957                .map(|f| f.tax_policy.trade_tax(income, false, false))
958                .unwrap_or(0.0);
959            if let Some(f) = self.factions.get_mut(&faction_id) {
960                f.treasury.receive(tax, "trade_tax");
961            }
962        }
963    }
964
965    // -----------------------------------------------------------------------
966    // Main Tick
967    // -----------------------------------------------------------------------
968
969    /// Advance the faction economy by one tick.
970    ///
971    /// - Ticks all trade routes and distributes income.
972    /// - Processes reparation installments.
973    /// - Processes tribute payments.
974    /// - Collects trade taxes.
975    /// - Decays disinformation penalties.
976    /// - Resolves completed espionage operations.
977    /// - Recomputes wealth rankings.
978    pub fn tick(&mut self) {
979        self.current_tick += 1;
980        let tick = self.current_tick;
981
982        // --- Trade routes ---
983        let route_ids: Vec<TradeRouteId> = self.trade_routes.keys().copied().collect();
984        let mut route_incomes: HashMap<FactionId, f64> = HashMap::new();
985        for &rid in &route_ids {
986            let (from_id, to_id, export_income, import_income) = {
987                let route = self.trade_routes.get_mut(&rid).unwrap();
988                let (exp, imp) = route.tick();
989                (route.from_faction, route.to_faction, exp, imp)
990            };
991            // Apply disinformation penalty to exporter
992            let dis_penalty = self.factions.get(&from_id).map(|f| f.disinformation_penalty).unwrap_or(0.0);
993            let actual_export = export_income * (1.0 - dis_penalty);
994            let actual_import = import_income;
995
996            if let Some(f) = self.factions.get_mut(&from_id) {
997                f.treasury.receive(actual_export, "trade_export");
998                f.gdp_proxy += actual_export;
999            }
1000            if let Some(f) = self.factions.get_mut(&to_id) {
1001                f.treasury.receive(actual_import, "trade_import");
1002                f.gdp_proxy += actual_import;
1003            }
1004            *route_incomes.entry(from_id).or_insert(0.0) += actual_export;
1005            *route_incomes.entry(to_id).or_insert(0.0) += actual_import;
1006            // Route recovery over time
1007            if let Some(route) = self.trade_routes.get_mut(&rid) {
1008                route.recover(0.01);
1009            }
1010        }
1011
1012        // Collect taxes on route income
1013        self.collect_taxes(&route_incomes);
1014
1015        // --- Reparations ---
1016        // Collect paying faction IDs so we can process them separately
1017        let paying_faction_ids: Vec<FactionId> = self.factions.keys().copied().collect();
1018        for fid in &paying_faction_ids {
1019            // Process reparations the faction is paying
1020            let mut to_transfer: Vec<(FactionId, f64)> = Vec::new();
1021            if let Some(f) = self.factions.get_mut(fid) {
1022                for rep in f.reparations_paying.iter_mut() {
1023                    if rep.completed { continue; }
1024                    let amount = rep.process();
1025                    if amount > 0.0 {
1026                        f.treasury.spend(amount, "reparations");
1027                        to_transfer.push((rep.receiving_faction, amount));
1028                    }
1029                }
1030            }
1031            for (receiver_id, amount) in to_transfer {
1032                if let Some(rf) = self.factions.get_mut(&receiver_id) {
1033                    rf.treasury.receive(amount, "reparations_received");
1034                }
1035            }
1036        }
1037
1038        // --- Tribute ---
1039        let rng_val = self.next_rand();
1040        let tribute_ids: Vec<TributeAgreementId> = self.tribute_agreements.keys().copied().collect();
1041        for &tid in &tribute_ids {
1042            let (payer, receiver, amount) = {
1043                let agr = self.tribute_agreements.get_mut(&tid).unwrap();
1044                let amount = agr.process(tick, rng_val);
1045                (agr.paying_faction, agr.receiving_faction, amount)
1046            };
1047            if amount > 0.0 {
1048                if let Some(f) = self.factions.get_mut(&payer) {
1049                    f.treasury.spend(amount, "tribute");
1050                }
1051                if let Some(f) = self.factions.get_mut(&receiver) {
1052                    f.treasury.receive(amount, "tribute_received");
1053                }
1054            }
1055        }
1056
1057        // --- Decay disinformation ---
1058        for f in self.factions.values_mut() {
1059            f.disinformation_penalty = (f.disinformation_penalty - 0.005).max(0.0);
1060        }
1061
1062        // --- Espionage ---
1063        self.resolve_espionage();
1064
1065        // --- Wealth Ranking ---
1066        self.compute_wealth_ranking();
1067    }
1068
1069    // -----------------------------------------------------------------------
1070    // Query Helpers
1071    // -----------------------------------------------------------------------
1072
1073    /// All trade routes involving a specific faction.
1074    pub fn routes_for_faction(&self, faction: FactionId) -> Vec<&TradeRoute> {
1075        self.trade_routes.values()
1076            .filter(|r| r.from_faction == faction || r.to_faction == faction)
1077            .collect()
1078    }
1079
1080    /// Total GDP proxy across all factions.
1081    pub fn global_gdp(&self) -> f64 {
1082        self.factions.values().map(|f| f.gdp_proxy).sum()
1083    }
1084
1085    /// Dominant faction (highest gold).
1086    pub fn dominant_faction(&self) -> Option<FactionId> {
1087        self.wealth_ranking.as_ref()?.richest().map(|(id, _)| id)
1088    }
1089
1090    /// Active embargoes imposed by a faction.
1091    pub fn active_embargoes(&self, faction: FactionId) -> Vec<&EmbargoPenalty> {
1092        self.factions.get(&faction)
1093            .map(|f| f.embargoes.iter().filter(|e| e.lifted_tick.is_none()).collect())
1094            .unwrap_or_default()
1095    }
1096
1097    /// Espionage reports involving a faction (as instigator or target).
1098    pub fn reports_for(&self, faction: FactionId) -> Vec<&EspionageReport> {
1099        self.espionage_reports.iter()
1100            .filter(|r| r.instigator == faction || r.target == faction)
1101            .collect()
1102    }
1103}
1104
1105impl Default for FactionEconomy {
1106    fn default() -> Self {
1107        Self::new()
1108    }
1109}
1110
1111// ---------------------------------------------------------------------------
1112// Tests
1113// ---------------------------------------------------------------------------
1114
1115#[cfg(test)]
1116mod tests {
1117    use super::*;
1118
1119    #[test]
1120    fn test_trade_route_income() {
1121        let mut fe = FactionEconomy::new();
1122        let a = fe.add_faction("Alpha", 10_000.0);
1123        let b = fe.add_faction("Beta", 8_000.0);
1124        let _rid = fe.add_trade_route(a, b, 1000.0, 0.4, 0.3);
1125        fe.tick();
1126        let alpha = fe.faction(a).unwrap();
1127        // Alpha should have received export income
1128        assert!(alpha.treasury.gold > 10_000.0);
1129    }
1130
1131    #[test]
1132    fn test_embargo_blocks_route() {
1133        let mut fe = FactionEconomy::new();
1134        let a = fe.add_faction("Aland", 5_000.0);
1135        let b = fe.add_faction("Bland", 5_000.0);
1136        let rid = fe.add_trade_route(a, b, 500.0, 0.5, 0.5);
1137        fe.impose_embargo(a, b, 0.2, true, false, "Political dispute");
1138        let route = &fe.trade_routes[&rid];
1139        assert_eq!(route.status, TradeRouteStatus::Embargoed);
1140        fe.tick();
1141        // No income should flow
1142        let fa = fe.faction(a).unwrap();
1143        assert!(fa.treasury.gold <= 5_001.0, "embargoed route should not produce income");
1144    }
1145
1146    #[test]
1147    fn test_reparations() {
1148        let mut fe = FactionEconomy::new();
1149        let loser = fe.add_faction("Loser", 10_000.0);
1150        let winner = fe.add_faction("Winner", 5_000.0);
1151        fe.impose_reparations(loser, winner, 1_000.0, 100.0);
1152        for _ in 0..10 { fe.tick(); }
1153        let w = fe.faction(winner).unwrap();
1154        assert!(w.treasury.gold > 5_000.0 + 999.0);
1155        let l = fe.faction(loser).unwrap();
1156        assert!(l.treasury.gold < 10_000.0);
1157    }
1158
1159    #[test]
1160    fn test_wealth_ranking_gini() {
1161        let mut fe = FactionEconomy::new();
1162        fe.add_faction("Rich", 100_000.0);
1163        fe.add_faction("Poor", 1_000.0);
1164        fe.compute_wealth_ranking();
1165        let wk = fe.wealth_ranking.as_ref().unwrap();
1166        let gini = wk.gini();
1167        assert!(gini > 0.3, "highly unequal distribution should yield gini > 0.3: {}", gini);
1168    }
1169
1170    #[test]
1171    fn test_progressive_tax() {
1172        let f = FactionId(1);
1173        let policy = TaxPolicy {
1174            faction: f,
1175            flat_rate: 0.40,
1176            brackets: vec![(1_000.0, 0.10), (5_000.0, 0.25)],
1177            trade_tax_rate: 0.10,
1178            import_duty: 0.05,
1179            export_duty: 0.03,
1180            luxury_multiplier: 1.5,
1181            evasion_rate: 0.0,
1182        };
1183        let tax = policy.compute_tax(6_000.0);
1184        // Bracket 1: 1000 * 0.10 = 100
1185        // Bracket 2: 4000 * 0.25 = 1000
1186        // Flat:      1000 * 0.40 = 400
1187        let expected = 100.0 + 1000.0 + 400.0;
1188        assert!((tax - expected).abs() < 1e-6, "tax={} expected={}", tax, expected);
1189    }
1190
1191    #[test]
1192    fn test_tribute_rebellion() {
1193        let mut fe = FactionEconomy::new();
1194        let payer = fe.add_faction("Vassal", 3_000.0);
1195        let recvr = fe.add_faction("Overlord", 10_000.0);
1196        // Very high rebellion chance with zero coercion -> will likely rebel quickly
1197        fe.add_tribute(payer, recvr, 100.0, None, 0.0, 1.0);
1198        // With rebellion_chance=1.0, the agreement should collapse immediately
1199        fe.tick();
1200        let agreement = fe.tribute_agreements.values().next().unwrap();
1201        assert!(!agreement.active, "tribute should have been rebelled against");
1202    }
1203
1204    #[test]
1205    fn test_war_closes_routes() {
1206        let mut fe = FactionEconomy::new();
1207        let a = fe.add_faction("North", 5_000.0);
1208        let b = fe.add_faction("South", 5_000.0);
1209        fe.add_trade_route(a, b, 500.0, 0.4, 0.4);
1210        fe.declare_war(a, b);
1211        let route = fe.trade_routes.values().next().unwrap();
1212        assert_eq!(route.status, TradeRouteStatus::Closed);
1213    }
1214}