feat(mc-turn): ✨ Introduce worker categories and expertise tiers in game state and processing logic
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
de9944a6ed
commit
4131642d0b
2 changed files with 104 additions and 1 deletions
|
|
@ -342,6 +342,15 @@ pub struct GameState {
|
|||
/// persisted in save files.
|
||||
#[serde(skip)]
|
||||
pub units_catalog: mc_units::UnitsCatalog,
|
||||
/// p3-05e: civic-axis modifier catalog loaded from
|
||||
/// `public/resources/civics/*.json`. Used at turn-end to resolve each
|
||||
/// player's `ResolvedModifiers` (notably `specialist_xp_rate`, consumed by
|
||||
/// the expertise-XP tick in `process_city_production`). `#[serde(skip)]`
|
||||
/// mirrors `units_catalog` — boot-loaded, not save-persisted. An empty
|
||||
/// (Default) catalog resolves to identity modifiers (rate 1.0), so test
|
||||
/// and pre-load paths see no civic effect.
|
||||
#[serde(skip)]
|
||||
pub civic_catalog: mc_civics::CivicCatalog,
|
||||
/// p2-71: tactical-AI view of the producible-unit catalog. Mirrors
|
||||
/// `TacticalState::unit_catalog` and is populated once at harness boot
|
||||
/// by `GdPlayerApi::set_units_catalog_json` (or directly in Rust tests).
|
||||
|
|
|
|||
|
|
@ -1067,6 +1067,19 @@ impl TurnProcessor {
|
|||
use mc_city::Queueable;
|
||||
use mc_core::WonderId;
|
||||
|
||||
// p3-05e: resolve the Labor-axis `specialist_xp_rate` for this player
|
||||
// from their active civics + the boot-loaded civic catalog, *before*
|
||||
// the mutable `player` borrow below (immutable borrows of
|
||||
// `state.players[pi].civic_state` + `state.civic_catalog` must drop
|
||||
// first). An empty catalog (tests / pre-boot) resolves to identity
|
||||
// (rate 1.0), so no civic effect — see `resolve_modifiers`. The rate
|
||||
// scales XP earned by assigned workers in `tick_xp_gain` below.
|
||||
let specialist_xp_rate = mc_civics::resolve_modifiers(
|
||||
&state.players[pi].civic_state,
|
||||
&state.civic_catalog,
|
||||
)
|
||||
.specialist_xp_rate;
|
||||
|
||||
let player = &mut state.players[pi];
|
||||
let prod_axis = *player.strategic_axes.get("production").unwrap_or(&3) as i32;
|
||||
let exp_axis = *player.strategic_axes.get("expansion").unwrap_or(&3) as u32;
|
||||
|
|
@ -1142,11 +1155,24 @@ impl TurnProcessor {
|
|||
];
|
||||
for (cat, amount) in earned {
|
||||
if amount > 0 {
|
||||
// p3-05e: scale the earned XP by the player's civic
|
||||
// `specialist_xp_rate` (Labor axis). Round to nearest and
|
||||
// clamp into `u32`; a positive yield with a positive rate
|
||||
// <1.0 keeps at least 1 XP so a slow-but-nonzero rate never
|
||||
// silently truncates a working worker to zero progress.
|
||||
let scaled = (amount as f64 * specialist_xp_rate).round();
|
||||
let xp_amount: u32 = if scaled <= 0.0 {
|
||||
if specialist_xp_rate > 0.0 { 1 } else { 0 }
|
||||
} else if scaled >= u32::MAX as f64 {
|
||||
u32::MAX
|
||||
} else {
|
||||
scaled as u32
|
||||
};
|
||||
let entry = city
|
||||
.worker_expertise
|
||||
.entry(cat)
|
||||
.or_insert_with(WorkerExpertise::default);
|
||||
entry.tick_xp_gain(amount as u32, &expertise_cfg);
|
||||
entry.tick_xp_gain(xp_amount, &expertise_cfg);
|
||||
}
|
||||
}
|
||||
// Idle decay: any category with a ledger entry that did NOT earn
|
||||
|
|
@ -6802,6 +6828,74 @@ mod tests {
|
|||
assert_eq!(state.players[0].city_buildings[0], vec!["longhouse"]);
|
||||
}
|
||||
|
||||
/// p3-05e: the Labor-axis civic `specialist_xp_rate` scales the XP that
|
||||
/// assigned workers earn in `process_city_production`. A player running
|
||||
/// `guild_apprenticeship` (rate 1.50) must accumulate strictly more
|
||||
/// Construction expertise over the same number of ticks than a player with
|
||||
/// no active civic (anarchy / identity rate 1.0), all else equal.
|
||||
#[test]
|
||||
fn test_specialist_xp_rate_scales_expertise_gain() {
|
||||
use mc_civics::CivicCatalog;
|
||||
use mc_core::civic::{AxisChoice, CivicState};
|
||||
|
||||
// Weighted "progress" reading so a higher tier always outranks more
|
||||
// residual XP — tier index dominates, xp_in_tier breaks ties.
|
||||
fn construction_progress(state: &GameState) -> u64 {
|
||||
let we = state.players[0]
|
||||
.cities[0]
|
||||
.worker_expertise
|
||||
.get(&WorkerCategory::Construction)
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
let tier_ord = mc_core::expertise::ALL
|
||||
.iter()
|
||||
.position(|t| *t == we.tier)
|
||||
.unwrap_or(0) as u64;
|
||||
tier_ord * 1_000_000 + we.xp_in_tier as u64
|
||||
}
|
||||
|
||||
// Real Game-1 civic catalog (CARGO_MANIFEST_DIR-rooted; tests only).
|
||||
let catalog =
|
||||
CivicCatalog::load_from_dir(&CivicCatalog::workspace_default_path())
|
||||
.expect("load civic catalog");
|
||||
|
||||
// Build one runner; identical except the Labor-axis civic choice.
|
||||
fn build_state(catalog: &CivicCatalog, labor: AxisChoice) -> GameState {
|
||||
let mut player = systems_b_player(3, 5, 3, 3);
|
||||
push_starter_city(&mut player, 0, 0);
|
||||
// High pop so neither growth nor starvation perturbs the run; the
|
||||
// Construction (prod) XP path is independent of the food path.
|
||||
player.cities[0].population = 30;
|
||||
let mut cs = CivicState::default();
|
||||
cs.labor = labor;
|
||||
cs.anarchy_turns_remaining = 0;
|
||||
player.civic_state = cs;
|
||||
let mut state = systems_b_state(player);
|
||||
state.civic_catalog = catalog.clone();
|
||||
state
|
||||
}
|
||||
|
||||
let mut guilds = build_state(&catalog, AxisChoice::Guilds); // rate 1.50
|
||||
let mut anarchy = build_state(&catalog, AxisChoice::Anarchy); // rate 1.00
|
||||
|
||||
let processor = TurnProcessor::new(100);
|
||||
for _ in 0..12 {
|
||||
processor.process_city_production(&mut guilds, 0, &mut Vec::new());
|
||||
processor.process_city_production(&mut anarchy, 0, &mut Vec::new());
|
||||
}
|
||||
|
||||
let g = construction_progress(&guilds);
|
||||
let a = construction_progress(&anarchy);
|
||||
assert!(
|
||||
g > a,
|
||||
"guild_apprenticeship (specialist_xp_rate 1.50) must out-accumulate \
|
||||
anarchy (1.00): guilds={g} vs anarchy={a}"
|
||||
);
|
||||
// Sanity: the baseline actually earned something, so the comparison is
|
||||
// meaningful (not 0 vs 0).
|
||||
assert!(a > 0, "baseline anarchy run must earn some Construction XP");
|
||||
}
|
||||
|
||||
/// `try_found_city` seeds the founding palace into the new city's
|
||||
/// building list, so the auto-evolution hook has a starting tier.
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue