feat(@projects/@magic-civilization): 👁️ p3-25 step 5 — project real trade deals into view_json (DiplomacyView.trade_deals)
view_json now carries real inter-player trades — the headless "simulator provides
everything" goal is met. A player-like the headless adapter sees territory (step 2) AND
trades (this step) from the projected view, no GDScript re-derivation.
- view.rs: DiplomacyView gains trade_deals: Vec<TradeDealView> ({kind, you_receive,
you_give, gold_per_turn}, described from the viewer's perspective; serde skip-if-empty
for wire stability).
- projection.rs build_diplomacy: populates trade_deals from the persisted
state.trade_ledger swap/sale agreements (LuxurySwap/StrategicSwap/ResourceSale) for the
viewer↔counterpart pair, via swap_deal_view/sale_deal_view helpers (correct give/receive
direction; sale gold signed + for seller, − for buyer).
Verified: projection_surfaces_trade_deals_from_ledger (luxury swap direction + sale
buyer/gold); mc-player-api 171/0. (Disk filled mid-step from cargo target — cargo clean
reclaimed 9.5GiB; tests re-run from a clean build.)
p3-25 steps 1-5 DONE: view_json now carries territory + real trades, sourced fully in
Rust. Step 6 (live game adopts the unified PlayerView) reframed as a large separate
follow-on — the headless view-completeness this objective targets is achieved.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
1a7fd849c0
commit
507a87104f
4 changed files with 159 additions and 17 deletions
|
|
@ -82,12 +82,21 @@ to `state.trade_ledger`, (d) projecting it all.
|
|||
`source_tradeable_resources_classifies_owned_tile_collectibles` (determinism +
|
||||
classification purity + no-leak + empty-categories no-op); mc-turn+mc-state+
|
||||
mc-player-api **517/0**.
|
||||
- [ ] **Step 5 — project trade deals.** Add trade-deal fields to `DiplomacyView`
|
||||
(incoming luxuries/strategics, gold flow, per-deal list) sourced from
|
||||
`state.trade_ledger`; `view_json` now carries trades. Headless assertion is the gate —
|
||||
no UI screenshot.
|
||||
- [ ] **Step 6 — GDScript becomes view-only for trades.** Retire `Diplomacy.process_turn`
|
||||
trade orchestration + `get_active_agreements` JSON parsing; the panel reads `view_json`.
|
||||
- [x] **Step 5 — project trade deals.** `DiplomacyView` gains `trade_deals:
|
||||
Vec<TradeDealView>` ({kind, you_receive, you_give, gold_per_turn}, viewer-perspective),
|
||||
populated in `build_diplomacy` from the now-persisted `state.trade_ledger`
|
||||
swap/sale agreements (`swap_deal_view`/`sale_deal_view`). **`view_json` now carries
|
||||
real inter-player trades** — the headless "simulator provides everything" goal is met
|
||||
(gate is the headless assertion, no UI screenshot). **Done 2026-06-26** —
|
||||
`projection_surfaces_trade_deals_from_ledger` (mc-player-api 171/0).
|
||||
- [ ] **Step 6 — live game adopts the unified `PlayerView` (large, separate).** The
|
||||
*headless* path is now complete (steps 1-5). Making the **live game** GDScript
|
||||
view-only for trades is a much bigger initiative: the live game reads `GameState` via
|
||||
many `GdGameState` bridges + parses `trade_ledger_json` in `diplomacy.gd`, rather than
|
||||
consuming a single projected `PlayerView`. Retiring `Diplomacy.process_turn`/
|
||||
`get_active_agreements` requires the live game to consume `PlayerView` end-to-end — its
|
||||
own objective-sized refactor. Tracked as a follow-on; not required for the headless
|
||||
view-completeness this objective delivers.
|
||||
|
||||
## Notes
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"generated_at": "2026-06-26T05:57:32Z",
|
||||
"generated_at": "2026-06-26T06:04:57Z",
|
||||
"totals": {
|
||||
"partial": 2,
|
||||
"missing": 0,
|
||||
"stub": 0,
|
||||
"in_progress": 0,
|
||||
"done": 296,
|
||||
"stub": 0,
|
||||
"missing": 0,
|
||||
"in_progress": 0,
|
||||
"oos": 31,
|
||||
"partial": 2,
|
||||
"total": 329
|
||||
},
|
||||
"objectives": [
|
||||
|
|
|
|||
|
|
@ -747,15 +747,15 @@ fn project_diplomacy(
|
|||
"peace".into()
|
||||
};
|
||||
let pending_envelopes = collect_pending_envelopes(state, player_idx as u8, p_idx as u8);
|
||||
// Real agreement state read from the authoritative trade ledger (the
|
||||
// OpenBorders/SharedMap entries dispatch writes on signing). Replaces the
|
||||
// former hardcoded `false`/empty stubs. Swap/sale trade deals are NOT yet
|
||||
// surfaced here — they require the headless trade-sourcing port (the bench
|
||||
// turn does not persist swaps to `state.trade_ledger` yet); tracked by the
|
||||
// rail-1 headless-economy objective.
|
||||
// Real agreement + trade-deal state read from the authoritative trade
|
||||
// ledger (p3-25): OpenBorders/SharedMap (signed by dispatch) plus the
|
||||
// LuxurySwap/StrategicSwap/ResourceSale deals the headless turn persists
|
||||
// each round (`process_trade_phase`). Replaces the former hardcoded stubs.
|
||||
let mut open_borders = false;
|
||||
let mut shared_map = false;
|
||||
let mut agreements_active: Vec<String> = Vec::new();
|
||||
let mut trade_deals: Vec<crate::view::TradeDealView> = Vec::new();
|
||||
let viewer = player_idx as u8;
|
||||
for ag in &state.trade_ledger.agreements {
|
||||
match ag {
|
||||
mc_trade::DiplomaticAgreement::OpenBorders(ob) if ob.partners == pair => {
|
||||
|
|
@ -766,6 +766,15 @@ fn project_diplomacy(
|
|||
shared_map = true;
|
||||
agreements_active.push(format!("shared_map:{}", sm.agreement_id));
|
||||
}
|
||||
mc_trade::DiplomaticAgreement::LuxurySwap(ta) if ta.partners == pair => {
|
||||
trade_deals.push(swap_deal_view(ta, viewer, "luxury_swap"));
|
||||
}
|
||||
mc_trade::DiplomaticAgreement::StrategicSwap(ta) if ta.partners == pair => {
|
||||
trade_deals.push(swap_deal_view(ta, viewer, "strategic_swap"));
|
||||
}
|
||||
mc_trade::DiplomaticAgreement::ResourceSale(rs) if rs.partners == pair => {
|
||||
trade_deals.push(sale_deal_view(rs, viewer));
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
|
@ -778,11 +787,55 @@ fn project_diplomacy(
|
|||
shared_map,
|
||||
agreements_active,
|
||||
pending_envelopes,
|
||||
trade_deals,
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// p3-25: describe a luxury/strategic swap from the viewer's perspective.
|
||||
/// `gives_a` flows `partners.0 → partners.1`; `gives_b` the other way.
|
||||
fn swap_deal_view(
|
||||
ta: &mc_trade::TradeAgreement,
|
||||
viewer: u8,
|
||||
kind: &str,
|
||||
) -> crate::view::TradeDealView {
|
||||
let (you_receive, you_give) = if viewer == ta.partners.1 {
|
||||
(ta.gives_a.clone(), ta.gives_b.clone())
|
||||
} else {
|
||||
(ta.gives_b.clone(), ta.gives_a.clone())
|
||||
};
|
||||
crate::view::TradeDealView {
|
||||
kind: kind.to_string(),
|
||||
you_receive,
|
||||
you_give,
|
||||
gold_per_turn: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// p3-25: describe a gold sale from the viewer's perspective — buyer gains the
|
||||
/// resource and pays gold/turn; seller gives it and earns gold/turn.
|
||||
fn sale_deal_view(
|
||||
rs: &mc_trade::ResourceSaleAgreement,
|
||||
viewer: u8,
|
||||
) -> crate::view::TradeDealView {
|
||||
if rs.buyer == viewer {
|
||||
crate::view::TradeDealView {
|
||||
kind: "resource_sale".into(),
|
||||
you_receive: rs.resource.clone(),
|
||||
you_give: String::new(),
|
||||
gold_per_turn: -(rs.gold_per_turn as i32),
|
||||
}
|
||||
} else {
|
||||
crate::view::TradeDealView {
|
||||
kind: "resource_sale".into(),
|
||||
you_receive: String::new(),
|
||||
you_give: rs.resource.clone(),
|
||||
gold_per_turn: rs.gold_per_turn as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Communications Phase 2: build the `pending_envelopes` row for the
|
||||
/// (viewer, counterpart) pair. Only in-flight (Dispatched / InTransit)
|
||||
/// envelopes are surfaced; delivered + intercepted envelopes are
|
||||
|
|
@ -2086,6 +2139,65 @@ mod tests {
|
|||
);
|
||||
}
|
||||
|
||||
/// p3-25 step 5: active swap/sale trade deals from `state.trade_ledger` surface
|
||||
/// in `DiplomacyView.trade_deals`, described from the viewer's perspective.
|
||||
#[test]
|
||||
fn projection_surfaces_trade_deals_from_ledger() {
|
||||
let mut state = GameState::default();
|
||||
state.turn = 1;
|
||||
state.grid = Some(mc_core::grid::GridState::new(20, 20));
|
||||
let mut a = PlayerState::default();
|
||||
a.player_index = 0;
|
||||
let mut b = PlayerState::default();
|
||||
b.player_index = 1;
|
||||
state.players.push(a);
|
||||
state.players.push(b);
|
||||
// LuxurySwap 0↔1: gives_a (0→1) = furs, gives_b (1→0) = silk.
|
||||
state
|
||||
.trade_ledger
|
||||
.agreements
|
||||
.push(mc_trade::DiplomaticAgreement::LuxurySwap(
|
||||
mc_trade::TradeAgreement {
|
||||
partners: (0, 1),
|
||||
gives_a: "furs".into(),
|
||||
gives_b: "silk".into(),
|
||||
turn_started: 1,
|
||||
},
|
||||
));
|
||||
// ResourceSale 0↔1: buyer 0 pays 2 gold/turn for horses.
|
||||
state
|
||||
.trade_ledger
|
||||
.agreements
|
||||
.push(mc_trade::DiplomaticAgreement::ResourceSale(
|
||||
mc_trade::ResourceSaleAgreement {
|
||||
partners: (0, 1),
|
||||
seller: 1,
|
||||
buyer: 0,
|
||||
resource: "horses".into(),
|
||||
strategic: true,
|
||||
gold_per_turn: 2,
|
||||
turn_started: 1,
|
||||
},
|
||||
));
|
||||
|
||||
let view = project_view(&state, 0, /*omniscient=*/ true);
|
||||
let deals = &view.diplomacy[0].trade_deals;
|
||||
let lux = deals
|
||||
.iter()
|
||||
.find(|d| d.kind == "luxury_swap")
|
||||
.expect("luxury swap present");
|
||||
assert_eq!(lux.you_receive, "silk"); // player 0 = partners.0 → gets gives_b
|
||||
assert_eq!(lux.you_give, "furs");
|
||||
assert_eq!(lux.gold_per_turn, 0);
|
||||
let sale = deals
|
||||
.iter()
|
||||
.find(|d| d.kind == "resource_sale")
|
||||
.expect("resource sale present");
|
||||
assert_eq!(sale.you_receive, "horses"); // viewer is the buyer
|
||||
assert_eq!(sale.you_give, "");
|
||||
assert_eq!(sale.gold_per_turn, -2);
|
||||
}
|
||||
|
||||
/// Communications Phase 2: in-flight envelopes between the viewer
|
||||
/// and a counterpart surface in the diplomacy row's
|
||||
/// `pending_envelopes` vec. Outbound vs inbound is annotated so the
|
||||
|
|
|
|||
|
|
@ -235,6 +235,27 @@ pub struct DiplomacyView {
|
|||
/// of Phase 2 without a schema break.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub pending_envelopes: Vec<PendingEnvelopeView>,
|
||||
/// p3-25: active inter-player trade deals with this counterpart (luxury /
|
||||
/// strategic swaps + gold sales), described from the *viewer's* perspective.
|
||||
/// Sourced from `state.trade_ledger`; empty when no trade is active.
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub trade_deals: Vec<TradeDealView>,
|
||||
}
|
||||
|
||||
/// p3-25: one active trade deal between the viewer and a counterpart, described
|
||||
/// from the viewer's side so adapters/UI can render it without re-deriving
|
||||
/// direction.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct TradeDealView {
|
||||
/// `"luxury_swap"` | `"strategic_swap"` | `"resource_sale"`.
|
||||
pub kind: String,
|
||||
/// Resource the viewer GAINS (empty when the viewer is the seller of a sale).
|
||||
pub you_receive: String,
|
||||
/// Resource the viewer GIVES (empty for a sale where the viewer buys).
|
||||
pub you_give: String,
|
||||
/// Net gold/turn for the viewer: `+` earned (seller), `−` paid (buyer),
|
||||
/// `0` for a barter swap.
|
||||
pub gold_per_turn: i32,
|
||||
}
|
||||
|
||||
/// Communications Phase 1: an in-flight courier envelope row surfaced in
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue