feat(@projects/@magic-civilization): ✨ add player action dispatch module
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
7eb0849258
commit
9260496ab2
3 changed files with 145 additions and 7 deletions
17
src/simulator/Cargo.lock
generated
17
src/simulator/Cargo.lock
generated
|
|
@ -879,6 +879,7 @@ dependencies = [
|
|||
"mc-items",
|
||||
"mc-mapgen",
|
||||
"mc-mcts-service",
|
||||
"mc-player-api",
|
||||
"mc-replay",
|
||||
"mc-save",
|
||||
"mc-score",
|
||||
|
|
@ -988,6 +989,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"siphasher",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1111,6 +1113,21 @@ dependencies = [
|
|||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mc-player-api"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"mc-city",
|
||||
"mc-combat",
|
||||
"mc-core",
|
||||
"mc-tech",
|
||||
"mc-trade",
|
||||
"mc-turn",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 1.0.69",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mc-replay"
|
||||
version = "0.1.0"
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ mc-tech = { path = "../mc-tech" }
|
|||
mc-trade = { path = "../mc-trade" }
|
||||
mc-turn = { path = "../mc-turn" }
|
||||
mc-combat = { path = "../mc-combat" }
|
||||
mc-items = { path = "../mc-items" }
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
thiserror = "1"
|
||||
|
|
|
|||
|
|
@ -139,13 +139,44 @@ pub fn apply_action(
|
|||
PlayerAction::RemoveFromQueue { city_id, index: _ } => {
|
||||
apply_clear_queue(state, player, city_id)
|
||||
}
|
||||
PlayerAction::QueueReorder { .. }
|
||||
| PlayerAction::RushBuy { .. }
|
||||
| PlayerAction::BuyTile { .. }
|
||||
| PlayerAction::SetFocus { .. }
|
||||
| PlayerAction::MergeBuildings { .. } => Err(ActionError::NotYetImplemented {
|
||||
message: "reorder / rush_buy / buy_tile / set_focus / merge_buildings need \
|
||||
bench-vs-full City reconciliation — TRACKED: p2-67 Phase 1 follow-up"
|
||||
PlayerAction::RushBuy { city_id } => apply_rush_buy(state, player, city_id),
|
||||
|
||||
PlayerAction::BuyTile { .. } => Err(ActionError::NotYetImplemented {
|
||||
message: "buy_tile requires per-city tile-ownership state; bench \
|
||||
CityState carries no `owned_tiles: HashSet<HexCoord>` field \
|
||||
and widening it cascades into mc-sim/solo_dominion + \
|
||||
fauna_pressure_bench serde compat. The full `City` struct \
|
||||
in mc-city/src/city.rs owns tile ownership; a per-player \
|
||||
parallel `owned_tiles: Vec<HexCoord>` on GameState is the \
|
||||
lighter alternative. TRACKED: p2-67 Phase 7 follow-up."
|
||||
.into(),
|
||||
}),
|
||||
|
||||
PlayerAction::SetFocus { .. } => Err(ActionError::NotYetImplemented {
|
||||
message: "set_focus targets `City::set_focus` on the full City struct \
|
||||
(mc-city/src/city.rs); bench CityState has no `focus` field. \
|
||||
Adding it requires widening every bench consumer (mc-sim, \
|
||||
fauna_pressure_bench, MCTS rollout snapshots) for a field \
|
||||
they ignore. TRACKED: p2-67 Phase 7 follow-up."
|
||||
.into(),
|
||||
}),
|
||||
|
||||
PlayerAction::QueueReorder { .. } => Err(ActionError::NotYetImplemented {
|
||||
message: "queue_reorder operates on a Vec<QueueEntry>; bench CityState \
|
||||
holds a single `queue: Option<Queueable>` so there is \
|
||||
nothing to reorder. Upgrading to a vec cascades through \
|
||||
every TurnProcessor production path. TRACKED: p2-67 Phase 7 \
|
||||
follow-up (bench-queue-as-vec migration)."
|
||||
.into(),
|
||||
}),
|
||||
|
||||
PlayerAction::MergeBuildings { .. } => Err(ActionError::NotYetImplemented {
|
||||
message: "merge_buildings calls mc_city::merge::apply_merge which \
|
||||
requires &mut City + &BuildingRegistry + researched techs. \
|
||||
Threading the registry through GameState is the larger \
|
||||
lift; bench City struct (CityState) carries no instance \
|
||||
data the merge engine needs. TRACKED: p2-67 Phase 7 \
|
||||
follow-up (building-registry on GameState)."
|
||||
.into(),
|
||||
}),
|
||||
|
||||
|
|
@ -522,6 +553,95 @@ fn apply_queue_production(
|
|||
Ok(Vec::new())
|
||||
}
|
||||
|
||||
/// RushBuy: pay `2 × queue_cost` gold to immediately complete the current
|
||||
/// queue head.
|
||||
///
|
||||
/// Mirrors what `TurnProcessor::process_city_production` does on natural
|
||||
/// completion — only the timing differs. State changes:
|
||||
/// - `state.players[pi].gold -= rush_cost`
|
||||
/// - For `Queueable::Wonder`: insert into `player.wonders_built` at the
|
||||
/// stored tier, clear queue fields. Emits `Event::WonderBuilt`.
|
||||
/// - For `Queueable::Unit`: clear queue fields, emit `Event::CityUnitCompleted`.
|
||||
/// Does NOT spawn the unit on the map — the bench `TurnProcessor` itself
|
||||
/// does not spawn units from non-wonder queue heads in Phase 7's scope
|
||||
/// (that ticking lands in Phase 11). The wire event is the honest
|
||||
/// observable; the next `view()` shows the cleared queue + reduced gold.
|
||||
/// - For `Queueable::Item`: clear queue fields, emit `Event::CityBuildingCompleted`
|
||||
/// (the closest existing semantic — items don't currently have a
|
||||
/// dedicated completion event variant).
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// - [`ActionError::UnknownCity`] — `city_id` doesn't resolve.
|
||||
/// - [`ActionError::IllegalAction`] — empty queue (nothing to rush) or
|
||||
/// insufficient gold.
|
||||
fn apply_rush_buy(
|
||||
state: &mut GameState,
|
||||
_player: PlayerId,
|
||||
city_id: &str,
|
||||
) -> Result<Vec<Event>, ActionError> {
|
||||
let (pi, ci) = find_city_indices(state, city_id)?;
|
||||
|
||||
// Snapshot the queue head + cost before mutating; the `Queueable` clone
|
||||
// is cheap and lets us emit the right event variant after the borrow ends.
|
||||
let (queueable, queue_cost, queue_tier) = {
|
||||
let city = &state.players[pi].cities[ci];
|
||||
let q = city.queue.clone().ok_or_else(|| ActionError::IllegalAction {
|
||||
message: format!("city '{city_id}' has nothing queued to rush"),
|
||||
})?;
|
||||
let cost = city.queue_cost.ok_or_else(|| ActionError::IllegalAction {
|
||||
message: format!(
|
||||
"city '{city_id}' queue head has no `queue_cost`; rush requires a known base cost"
|
||||
),
|
||||
})?;
|
||||
(q, cost, city.queue_tier)
|
||||
};
|
||||
|
||||
let rush_cost: i32 = mc_items::ItemSystem::rush_buy_cost(queue_cost as i32);
|
||||
if state.players[pi].gold < rush_cost {
|
||||
return Err(ActionError::IllegalAction {
|
||||
message: format!(
|
||||
"rush_buy on '{city_id}' costs {rush_cost} gold; player has {}",
|
||||
state.players[pi].gold
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
// Deduct gold first; any further failure is unreachable after the snapshot.
|
||||
state.players[pi].gold -= rush_cost;
|
||||
|
||||
// Build the completion event from the queue-head snapshot.
|
||||
let event: Event = match &queueable {
|
||||
mc_city::Queueable::Wonder { wonder_id } => {
|
||||
let tier = queue_tier.unwrap_or(1);
|
||||
state.players[pi]
|
||||
.wonders_built
|
||||
.insert(wonder_id.clone(), tier);
|
||||
Event::WonderBuilt {
|
||||
wonder_id: wonder_id.0.clone(),
|
||||
player: pi as u8,
|
||||
}
|
||||
}
|
||||
mc_city::Queueable::Unit { unit_id } => Event::CityUnitCompleted {
|
||||
city_id: city_id.to_string(),
|
||||
unit_id: unit_id.0.clone(),
|
||||
},
|
||||
mc_city::Queueable::Item { item_id } => Event::CityBuildingCompleted {
|
||||
city_id: city_id.to_string(),
|
||||
building_id: item_id.clone(),
|
||||
},
|
||||
};
|
||||
|
||||
// Clear the queue head — matches `TurnProcessor` wonder-completion semantics.
|
||||
let city: &mut mc_city::CityState = &mut state.players[pi].cities[ci];
|
||||
city.queue = None;
|
||||
city.queue_cost = None;
|
||||
city.queue_tier = None;
|
||||
city.production_stored = 0;
|
||||
|
||||
Ok(vec![event])
|
||||
}
|
||||
|
||||
fn apply_clear_queue(
|
||||
state: &mut GameState,
|
||||
_player: PlayerId,
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue