diff --git a/src/simulator/crates/mc-trade/src/lib.rs b/src/simulator/crates/mc-trade/src/lib.rs index 0681bfe4..41722944 100644 --- a/src/simulator/crates/mc-trade/src/lib.rs +++ b/src/simulator/crates/mc-trade/src/lib.rs @@ -194,6 +194,102 @@ pub fn break_trades_on_war(ledger: &mut TradeLedger, at_war_a: u8, at_war_b: u8) broken } +// ── Diplomacy actions ────────────────────────────────────────────────────── + +/// Outcome of a diplomacy action, returned to the turn processor for logging. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum DiplomacyEvent { + WarDeclared { by: u8, against: u8 }, + PeaceOffered { by: u8, to: u8 }, + PeaceAccepted { by: u8, with: u8 }, + /// EA: AI always rejects player-initiated peace offers. + PeaceRejected { by: u8, with: u8 }, + TradeOfferAccepted { by: u8, with: u8, gold: u32, luxury_id: String }, + /// EA: AI always rejects gold-for-luxury offers. + TradeOfferRejected { by: u8, with: u8 }, +} + +/// A player-initiated offer of gold in exchange for a luxury resource. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TradeOffer { + pub from: u8, + pub to: u8, + /// Gold the offering player pays. + pub gold: u32, + /// Luxury ID the offering player wants in return. + pub luxury_id: String, +} + +/// Declare war between two players. Sets relation to War immediately, +/// breaks all active trades between them, resets counters. +/// Returns the event plus any broken agreements. +pub fn declare_war( + relations: &mut BTreeMap<(u8, u8), RelationState>, + ledger: &mut TradeLedger, + by: u8, + against: u8, +) -> (DiplomacyEvent, Vec) { + let key = pair_key(by, against); + let rs = relations.entry(key).or_default(); + rs.relation = Relation::War; + rs.peaceful_turns = 0; + rs.trade_turns = 0; + rs.war_idle_turns = 0; + let broken = break_trades_on_war(ledger, by, against); + (DiplomacyEvent::WarDeclared { by, against }, broken) +} + +/// Offer peace to an opponent. In Early Access the AI always rejects, so +/// this returns `PeaceRejected`. The caller may choose to accept on behalf +/// of a human opponent by calling `accept_peace` directly. +pub fn offer_peace(by: u8, to: u8) -> DiplomacyEvent { + DiplomacyEvent::PeaceRejected { by, with: to } +} + +/// Force-accept a peace offer (used when the receiving side is human or when +/// the armistice auto-fires). Sets relation to Neutral so the peace counter +/// can accumulate toward Peace. +pub fn accept_peace( + relations: &mut BTreeMap<(u8, u8), RelationState>, + by: u8, + with: u8, +) -> DiplomacyEvent { + let key = pair_key(by, with); + let rs = relations.entry(key).or_default(); + rs.relation = Relation::Neutral; + rs.war_idle_turns = 0; + DiplomacyEvent::PeaceAccepted { by, with } +} + +/// Evaluate a `TradeOffer` (gold ↔ luxury). In EA the AI always rejects +/// player-initiated offers — automated luxury swaps go through `evaluate_trades`. +/// Returns `TradeOfferRejected`. Deduct gold only on acceptance; callers +/// handle the ledger update if they override this for tests. +pub fn evaluate_trade_offer(offer: &TradeOffer) -> DiplomacyEvent { + DiplomacyEvent::TradeOfferRejected { by: offer.to, with: offer.from } +} + +/// Apply an accepted trade offer: deduct gold from `from`, credit `luxury_id` +/// into `to`'s `traded_luxuries`. Called only when the offer is accepted +/// (human vs human, or test override). +pub fn apply_trade_offer( + from_gold: &mut i32, + to_traded_luxuries: &mut BTreeSet, + offer: &TradeOffer, +) -> DiplomacyEvent { + *from_gold -= offer.gold as i32; + if *from_gold < 0 { + *from_gold = 0; + } + to_traded_luxuries.insert(offer.luxury_id.clone()); + DiplomacyEvent::TradeOfferAccepted { + by: offer.to, + with: offer.from, + gold: offer.gold, + luxury_id: offer.luxury_id.clone(), + } +} + /// Advance all relation states for one turn. /// /// `combat_pairs`: set of `pair_key(a, b)` where combat occurred this turn.