feat(@projects/@magic-civilization): ✨ add new rewards to buildings
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
fa84810da4
commit
afce3602f7
29 changed files with 576 additions and 23 deletions
|
|
@ -32,6 +32,7 @@
|
|||
]
|
||||
},
|
||||
"produces": [
|
||||
"dwarf_gyrocopter",
|
||||
"dwarf_steam_bomber"
|
||||
],
|
||||
"stack_mode": "parallel"
|
||||
|
|
|
|||
|
|
@ -42,6 +42,11 @@
|
|||
},
|
||||
"stack_mode": "parallel",
|
||||
"requires_existing": "great_hall",
|
||||
"consumes_existing": false
|
||||
"consumes_existing": false,
|
||||
"produces": [
|
||||
"ancestral_walker",
|
||||
"mountain_king",
|
||||
"dwarf_adamantine_champion"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -42,6 +42,12 @@
|
|||
"dwarven"
|
||||
]
|
||||
},
|
||||
"stack_mode": "parallel"
|
||||
"stack_mode": "parallel",
|
||||
"produces": [
|
||||
"dwarf_adamantine_champion",
|
||||
"iron_halberd",
|
||||
"dwarf_ascendant_sapper",
|
||||
"dwarf_ascendant_engineer"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -40,5 +40,10 @@
|
|||
"world"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"dwarf_river_galley",
|
||||
"dwarf_war_galley",
|
||||
"cartographer"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@
|
|||
"infrastructure"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"field_medic"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -40,5 +40,9 @@
|
|||
"world"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"saga_singer",
|
||||
"loremaster"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,5 +40,9 @@
|
|||
"world"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"merchant",
|
||||
"caravan_master"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,5 +27,8 @@
|
|||
"infrastructure"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"hold_courier"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,5 +40,9 @@
|
|||
"world"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"dwarf_high_engineer",
|
||||
"dwarf_grand_engineer"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,5 +40,9 @@
|
|||
"world"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"merchant",
|
||||
"loremaster"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@
|
|||
"modern"
|
||||
]
|
||||
},
|
||||
"stack_mode": "parallel"
|
||||
"stack_mode": "parallel",
|
||||
"produces": [
|
||||
"motorized_artillery",
|
||||
"rocket_trooper"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -40,5 +40,10 @@
|
|||
"world"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"dwarf_ascendant_smith",
|
||||
"dwarf_grand_smith",
|
||||
"forge_titan"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,5 +36,9 @@
|
|||
"world"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"dwarf_grand_smith",
|
||||
"dwarf_ascendant_smith"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -35,5 +35,9 @@
|
|||
"infrastructure"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"dwarf_deep_guard",
|
||||
"combat_engineer"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,5 +41,9 @@
|
|||
"world"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"battle_priest",
|
||||
"loremaster"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -53,5 +53,9 @@
|
|||
"ecology"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"beast_scout",
|
||||
"boar_scout"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,5 +28,8 @@
|
|||
"infrastructure"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"field_medic"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@
|
|||
]
|
||||
},
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"founder",
|
||||
"worker"
|
||||
],
|
||||
"palace_tier": "longhouse",
|
||||
"evolves_to": "great_hall",
|
||||
"unlocked_by": null,
|
||||
|
|
|
|||
|
|
@ -37,7 +37,10 @@
|
|||
"military"
|
||||
]
|
||||
},
|
||||
"produces": [],
|
||||
"produces": [
|
||||
"dwarf_scout",
|
||||
"foot_runner"
|
||||
],
|
||||
"stack_mode": "parallel",
|
||||
"flavor": "The hold's name carved into a post. That is enough.",
|
||||
"lore": "Dwarven frontier doctrine does not wait for roads. An outpost is raised in a day from pre-cut stone and fitted timber, planted in the ground to say: this is ours now. When the front moves, the outpost moves with it. Deepforge Clan codified the collapsible outpost design after losing three permanent fortifications in a single season to flank-and-bypass tactics. The lesson was not to build stronger — it was to build lighter and faster."
|
||||
|
|
|
|||
|
|
@ -35,6 +35,11 @@
|
|||
"future"
|
||||
]
|
||||
},
|
||||
"stack_mode": "parallel"
|
||||
"stack_mode": "parallel",
|
||||
"produces": [
|
||||
"emp_trooper",
|
||||
"stormbolt_trooper",
|
||||
"commando"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -40,5 +40,9 @@
|
|||
"world"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"saga_singer",
|
||||
"bard"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,5 +27,8 @@
|
|||
"infrastructure"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"worker"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,5 +56,9 @@
|
|||
"tier10"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"dwarf_adamantine_champion",
|
||||
"mountain_king"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -50,5 +50,9 @@
|
|||
"tier8"
|
||||
]
|
||||
},
|
||||
"stack_mode": "amplify"
|
||||
"stack_mode": "amplify",
|
||||
"produces": [
|
||||
"merchant",
|
||||
"caravan_master"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,5 +38,9 @@
|
|||
},
|
||||
"specialist_slots": [
|
||||
"tradeswright"
|
||||
],
|
||||
"produces": [
|
||||
"merchant",
|
||||
"caravan_master"
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@
|
|||
"defense"
|
||||
]
|
||||
},
|
||||
"stack_mode": "parallel"
|
||||
"stack_mode": "parallel",
|
||||
"produces": [
|
||||
"defender",
|
||||
"shield_bearer"
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -33,6 +33,8 @@
|
|||
]
|
||||
},
|
||||
"produces": [
|
||||
"dwarf_iron_hawk",
|
||||
"dwarf_mithril_hawk",
|
||||
"dwarf_war_zeppelin",
|
||||
"dwarf_sky_fortress"
|
||||
],
|
||||
|
|
|
|||
|
|
@ -62,6 +62,7 @@ pub struct AwardDef {
|
|||
/// Top-level envelope matching the JSON structure.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct AwardDefs {
|
||||
/// All award definitions from `awards.json`, in declaration order.
|
||||
pub awards: Vec<AwardDef>,
|
||||
}
|
||||
|
||||
|
|
|
|||
448
src/simulator/crates/mc-replay/tests/award_computation.rs
Normal file
448
src/simulator/crates/mc-replay/tests/award_computation.rs
Normal file
|
|
@ -0,0 +1,448 @@
|
|||
//! Integration tests for `mc_replay::compute_awards`.
|
||||
//!
|
||||
//! Acceptance bullets from p2-48 §3:
|
||||
//! - `compute_awards_returns_8`: all eight awards from `awards.json` are
|
||||
//! returned, one per entry, in definition order.
|
||||
//! - `compute_awards_handles_ties`: when two clans share the winning metric,
|
||||
//! the clan with the lowest numeric [`ClanId`] wins deterministically.
|
||||
//!
|
||||
//! Run with: `cargo test -p mc-replay --test award_computation`
|
||||
|
||||
use mc_replay::awards::{AwardDefs, AwardWinner};
|
||||
use mc_replay::history::{ClanDescriptor, GameHistory, TurnEventCollector};
|
||||
use mc_replay::ids::{
|
||||
CityName, ClanId, GameId, LeaderId, PackId, PackVersion, TechId, TileCoord, UnitKind,
|
||||
WonderId,
|
||||
};
|
||||
use mc_replay::snapshot::TurnSnapshot;
|
||||
use mc_replay::{compute_awards, TurnEvent};
|
||||
use mc_replay::archive::{GameOutcome, MapDescriptor};
|
||||
|
||||
// ── Fixture helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
/// Load the real `awards.json` at compile-time.
|
||||
const AWARDS_JSON: &str =
|
||||
include_str!("../../../../../public/games/age-of-dwarves/data/awards.json");
|
||||
|
||||
fn load_defs() -> AwardDefs {
|
||||
AwardDefs::from_json(AWARDS_JSON).expect("awards.json must parse")
|
||||
}
|
||||
|
||||
/// Three-clan, 50-turn game fixture where each clan has a clear lead in one
|
||||
/// metric category, allowing all eight awards to resolve to a known winner.
|
||||
///
|
||||
/// | Clan | Lead metric |
|
||||
/// |------|-----------------------------------------------------|
|
||||
/// | 1 | buildings_built_total (greatest_builder) |
|
||||
/// | 1 | wonders_built count (master_architect) |
|
||||
/// | 2 | units_killed events (war_chief) |
|
||||
/// | 3 | gold peak (wealthiest_clan) |
|
||||
/// | 1 | culture_total (most_cultured) |
|
||||
/// | 2 | techs_researched events (greatest_scholar) |
|
||||
/// | 1 | turns_leading_city_count (longest_reign) |
|
||||
/// | 2 | turns_survived (survivor — clan 3 eliminated early) |
|
||||
fn make_fixture() -> GameHistory {
|
||||
let mut hist = GameHistory::new(
|
||||
GameId::new_v4(),
|
||||
PackId("age-of-dwarves".into()),
|
||||
PackVersion("0.1.0-test".into()),
|
||||
42,
|
||||
MapDescriptor {
|
||||
kind: "highlands".into(),
|
||||
width: 48,
|
||||
height: 36,
|
||||
},
|
||||
vec![
|
||||
ClanDescriptor {
|
||||
id: ClanId(1),
|
||||
name: "Ironhold".into(),
|
||||
sigil_key: "ironhold.png".into(),
|
||||
colour_rgba: 0xCC_44_11_FF,
|
||||
starting_leader: LeaderId("durin".into()),
|
||||
},
|
||||
ClanDescriptor {
|
||||
id: ClanId(2),
|
||||
name: "Stonebeard".into(),
|
||||
sigil_key: "stonebeard.png".into(),
|
||||
colour_rgba: 0x22_88_CC_FF,
|
||||
starting_leader: LeaderId("brenna".into()),
|
||||
},
|
||||
ClanDescriptor {
|
||||
id: ClanId(3),
|
||||
name: "Deepdelve".into(),
|
||||
sigil_key: "deepdelve.png".into(),
|
||||
colour_rgba: 0x44_AA_44_FF,
|
||||
starting_leader: LeaderId("torga".into()),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// ── Snapshots (one per clan at final turn = 50) ──────────────────────────
|
||||
//
|
||||
// We push one representative mid-game snapshot (turn 25) plus the final
|
||||
// snapshot (turn 50) for each clan so peak-value aggregations have two data
|
||||
// points.
|
||||
|
||||
// Turn 25 — midpoints (gold values used for peak detection).
|
||||
hist.snapshots.push(TurnSnapshot {
|
||||
turn: 25,
|
||||
clan_id: ClanId(1),
|
||||
population: 600,
|
||||
cities: 4, // leading city count at t25
|
||||
army_strength: 50.0,
|
||||
gold: 200,
|
||||
gold_per_turn: 8,
|
||||
culture_per_turn: 5.0,
|
||||
tech_count: 8,
|
||||
land_area: 40,
|
||||
buildings_built_total: 10,
|
||||
culture_total: 125.0,
|
||||
score: 0.3,
|
||||
});
|
||||
hist.snapshots.push(TurnSnapshot {
|
||||
turn: 25,
|
||||
clan_id: ClanId(2),
|
||||
population: 500,
|
||||
cities: 3,
|
||||
army_strength: 80.0,
|
||||
gold: 150,
|
||||
gold_per_turn: 6,
|
||||
culture_per_turn: 2.0,
|
||||
tech_count: 12,
|
||||
land_area: 35,
|
||||
buildings_built_total: 5,
|
||||
culture_total: 50.0,
|
||||
score: 0.25,
|
||||
});
|
||||
hist.snapshots.push(TurnSnapshot {
|
||||
turn: 25,
|
||||
clan_id: ClanId(3),
|
||||
population: 400,
|
||||
cities: 2,
|
||||
army_strength: 30.0,
|
||||
gold: 500, // midpoint peak for clan 3
|
||||
gold_per_turn: 10,
|
||||
culture_per_turn: 1.0,
|
||||
tech_count: 5,
|
||||
land_area: 20,
|
||||
buildings_built_total: 3,
|
||||
culture_total: 25.0,
|
||||
score: 0.2,
|
||||
});
|
||||
|
||||
// Turn 50 — final values. Clan 3 was eliminated at turn 30 so its final
|
||||
// snapshot uses turn 30 (appended after events below; see note).
|
||||
hist.snapshots.push(TurnSnapshot {
|
||||
turn: 50,
|
||||
clan_id: ClanId(1),
|
||||
population: 1200,
|
||||
cities: 6, // still leading city count
|
||||
army_strength: 100.0,
|
||||
gold: 300,
|
||||
gold_per_turn: 12,
|
||||
culture_per_turn: 8.0,
|
||||
tech_count: 15,
|
||||
land_area: 90,
|
||||
buildings_built_total: 25, // highest buildings
|
||||
culture_total: 525.0, // highest culture_total
|
||||
score: 0.65,
|
||||
});
|
||||
hist.snapshots.push(TurnSnapshot {
|
||||
turn: 50,
|
||||
clan_id: ClanId(2),
|
||||
population: 900,
|
||||
cities: 4,
|
||||
army_strength: 120.0,
|
||||
gold: 250,
|
||||
gold_per_turn: 9,
|
||||
culture_per_turn: 3.0,
|
||||
tech_count: 20, // highest tech_count in snapshot, but award uses events
|
||||
land_area: 60,
|
||||
buildings_built_total: 12,
|
||||
culture_total: 150.0,
|
||||
score: 0.55,
|
||||
});
|
||||
// Clan 3 final snapshot at turn 30 (eliminated then).
|
||||
hist.snapshots.push(TurnSnapshot {
|
||||
turn: 30,
|
||||
clan_id: ClanId(3),
|
||||
population: 200,
|
||||
cities: 1,
|
||||
army_strength: 10.0,
|
||||
gold: 60,
|
||||
gold_per_turn: 2,
|
||||
culture_per_turn: 0.5,
|
||||
tech_count: 3,
|
||||
land_area: 10,
|
||||
buildings_built_total: 2,
|
||||
culture_total: 30.0,
|
||||
score: 0.08,
|
||||
});
|
||||
|
||||
// ── Events ───────────────────────────────────────────────────────────────
|
||||
|
||||
let mut col = TurnEventCollector::new();
|
||||
|
||||
// war_chief: clan 2 kills 8 units, clan 1 kills 3, clan 3 kills 0.
|
||||
for t in [3u32, 7, 12, 18, 22, 28, 35, 42] {
|
||||
col.push(TurnEvent::UnitKilled {
|
||||
turn: t,
|
||||
attacker: ClanId(2),
|
||||
defender: ClanId(1),
|
||||
unit_kind: UnitKind("warrior".into()),
|
||||
hex: TileCoord::new(0, 0),
|
||||
});
|
||||
}
|
||||
for t in [5u32, 15, 25] {
|
||||
col.push(TurnEvent::UnitKilled {
|
||||
turn: t,
|
||||
attacker: ClanId(1),
|
||||
defender: ClanId(2),
|
||||
unit_kind: UnitKind("warrior".into()),
|
||||
hex: TileCoord::new(1, 0),
|
||||
});
|
||||
}
|
||||
|
||||
// master_architect: clan 1 builds 2 wonders, clan 2 builds 1.
|
||||
col.push(TurnEvent::WonderBuilt {
|
||||
turn: 20,
|
||||
clan: ClanId(1),
|
||||
wonder: WonderId("great_forge".into()),
|
||||
city: CityName("Ironhold".into()),
|
||||
});
|
||||
col.push(TurnEvent::WonderBuilt {
|
||||
turn: 35,
|
||||
clan: ClanId(1),
|
||||
wonder: WonderId("deep_library".into()),
|
||||
city: CityName("Ironhold".into()),
|
||||
});
|
||||
col.push(TurnEvent::WonderBuilt {
|
||||
turn: 28,
|
||||
clan: ClanId(2),
|
||||
wonder: WonderId("the_vault".into()),
|
||||
city: CityName("Stonebeard".into()),
|
||||
});
|
||||
|
||||
// greatest_scholar: clan 2 researches 5 techs, clan 1 researches 3.
|
||||
for (t, tid) in [
|
||||
(4u32, "bronze_working"),
|
||||
(10, "masonry"),
|
||||
(18, "smelting"),
|
||||
(26, "engineering"),
|
||||
(38, "clockwork"),
|
||||
] {
|
||||
col.push(TurnEvent::TechResearched {
|
||||
turn: t,
|
||||
clan: ClanId(2),
|
||||
tech: TechId(tid.into()),
|
||||
});
|
||||
}
|
||||
for (t, tid) in [(6u32, "agriculture"), (14, "pottery"), (22, "writing")] {
|
||||
col.push(TurnEvent::TechResearched {
|
||||
turn: t,
|
||||
clan: ClanId(1),
|
||||
tech: TechId(tid.into()),
|
||||
});
|
||||
}
|
||||
|
||||
// survivor: clan 3 eliminated at turn 30.
|
||||
col.push(TurnEvent::ClanEliminated {
|
||||
turn: 30,
|
||||
clan: ClanId(3),
|
||||
by: ClanId(2),
|
||||
});
|
||||
|
||||
col.flush_to_history(&mut hist);
|
||||
|
||||
hist.final_turn = 50;
|
||||
hist.outcome = GameOutcome::Victor {
|
||||
clan: 1,
|
||||
reason: "score".into(),
|
||||
turn: 50,
|
||||
};
|
||||
|
||||
hist
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// All eight award definitions in `awards.json` produce exactly one
|
||||
/// `AwardWinner` each, in definition order, with non-empty `award_id`.
|
||||
#[test]
|
||||
fn compute_awards_returns_8() {
|
||||
let defs = load_defs();
|
||||
assert_eq!(
|
||||
defs.awards.len(),
|
||||
8,
|
||||
"awards.json must define exactly 8 awards"
|
||||
);
|
||||
|
||||
let hist = make_fixture();
|
||||
let winners = compute_awards(&hist, &defs);
|
||||
|
||||
assert_eq!(
|
||||
winners.len(),
|
||||
8,
|
||||
"compute_awards must return one winner per award definition"
|
||||
);
|
||||
|
||||
// Winners are in the same order as the definitions.
|
||||
for (winner, def) in winners.iter().zip(defs.awards.iter()) {
|
||||
assert_eq!(
|
||||
winner.award_id, def.id,
|
||||
"winner award_id must match definition id"
|
||||
);
|
||||
assert!(
|
||||
!winner.award_id.is_empty(),
|
||||
"award_id must be non-empty"
|
||||
);
|
||||
}
|
||||
|
||||
// Spot-check expected winners from the fixture design.
|
||||
let by_id: std::collections::HashMap<&str, &AwardWinner> = winners
|
||||
.iter()
|
||||
.map(|w| (w.award_id.as_str(), w))
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
by_id["greatest_builder"].clan,
|
||||
ClanId(1),
|
||||
"clan 1 has the most buildings_built_total (25)"
|
||||
);
|
||||
assert_eq!(
|
||||
by_id["master_architect"].clan,
|
||||
ClanId(1),
|
||||
"clan 1 built 2 wonders vs clan 2's 1"
|
||||
);
|
||||
assert_eq!(
|
||||
by_id["war_chief"].clan,
|
||||
ClanId(2),
|
||||
"clan 2 killed 8 units vs clan 1's 3"
|
||||
);
|
||||
assert_eq!(
|
||||
by_id["wealthiest_clan"].clan,
|
||||
ClanId(3),
|
||||
"clan 3 peaked at 500 gold, higher than clans 1 or 2"
|
||||
);
|
||||
assert_eq!(
|
||||
by_id["most_cultured"].clan,
|
||||
ClanId(1),
|
||||
"clan 1 has culture_total 525 at final snapshot"
|
||||
);
|
||||
assert_eq!(
|
||||
by_id["greatest_scholar"].clan,
|
||||
ClanId(2),
|
||||
"clan 2 researched 5 techs vs clan 1's 3"
|
||||
);
|
||||
assert_eq!(
|
||||
by_id["longest_reign"].clan,
|
||||
ClanId(1),
|
||||
"clan 1 led city count on most turns (6 cities by final turn)"
|
||||
);
|
||||
assert_eq!(
|
||||
by_id["survivor"].clan,
|
||||
ClanId(1),
|
||||
"clans 1+2 survived full 50 turns; clan 1 (lowest id) wins tie"
|
||||
);
|
||||
}
|
||||
|
||||
/// When two clans share the exact winning metric value the clan with the
|
||||
/// lowest numeric `ClanId` wins deterministically.
|
||||
#[test]
|
||||
fn compute_awards_handles_ties() {
|
||||
let defs = load_defs();
|
||||
|
||||
// Build a minimal history where clan 1 and clan 2 have identical
|
||||
// `buildings_built_total` (10) — the `greatest_builder` award must go to
|
||||
// clan 1 (lower id).
|
||||
let mut hist = GameHistory::new(
|
||||
GameId::new_v4(),
|
||||
PackId("age-of-dwarves".into()),
|
||||
PackVersion("0.1.0-test".into()),
|
||||
7,
|
||||
MapDescriptor {
|
||||
kind: "continents".into(),
|
||||
width: 32,
|
||||
height: 24,
|
||||
},
|
||||
vec![
|
||||
ClanDescriptor {
|
||||
id: ClanId(1),
|
||||
name: "Ironhold".into(),
|
||||
sigil_key: "ironhold.png".into(),
|
||||
colour_rgba: 0xCC_44_11_FF,
|
||||
starting_leader: LeaderId("durin".into()),
|
||||
},
|
||||
ClanDescriptor {
|
||||
id: ClanId(2),
|
||||
name: "Stonebeard".into(),
|
||||
sigil_key: "stonebeard.png".into(),
|
||||
colour_rgba: 0x22_88_CC_FF,
|
||||
starting_leader: LeaderId("brenna".into()),
|
||||
},
|
||||
],
|
||||
);
|
||||
|
||||
// Both clans: buildings_built_total = 10, culture_total = 50, gold = 100.
|
||||
for clan in [ClanId(1), ClanId(2)] {
|
||||
hist.snapshots.push(TurnSnapshot {
|
||||
turn: 10,
|
||||
clan_id: clan,
|
||||
population: 500,
|
||||
cities: 3,
|
||||
army_strength: 40.0,
|
||||
gold: 100,
|
||||
gold_per_turn: 5,
|
||||
culture_per_turn: 5.0,
|
||||
tech_count: 5,
|
||||
land_area: 30,
|
||||
buildings_built_total: 10,
|
||||
culture_total: 50.0,
|
||||
score: 0.3,
|
||||
});
|
||||
}
|
||||
|
||||
hist.final_turn = 10;
|
||||
hist.outcome = GameOutcome::InProgress; // outcome doesn't affect award logic
|
||||
|
||||
let winners = compute_awards(&hist, &defs);
|
||||
let by_id: std::collections::HashMap<&str, &AwardWinner> = winners
|
||||
.iter()
|
||||
.map(|w| (w.award_id.as_str(), w))
|
||||
.collect();
|
||||
|
||||
// greatest_builder: tie at 10 buildings → clan 1 (lowest id) wins.
|
||||
assert_eq!(
|
||||
by_id["greatest_builder"].clan,
|
||||
ClanId(1),
|
||||
"tie on buildings_built_total must resolve to lowest clan_id"
|
||||
);
|
||||
|
||||
// most_cultured: tie at 50.0 → clan 1 wins.
|
||||
assert_eq!(
|
||||
by_id["most_cultured"].clan,
|
||||
ClanId(1),
|
||||
"tie on culture_total must resolve to lowest clan_id"
|
||||
);
|
||||
|
||||
// wealthiest_clan: both peak at 100 gold → clan 1 wins.
|
||||
assert_eq!(
|
||||
by_id["wealthiest_clan"].clan,
|
||||
ClanId(1),
|
||||
"tie on peak gold must resolve to lowest clan_id"
|
||||
);
|
||||
|
||||
// survivor: both survived all 10 turns → clan 1 wins.
|
||||
assert_eq!(
|
||||
by_id["survivor"].clan,
|
||||
ClanId(1),
|
||||
"tie on turns_survived must resolve to lowest clan_id"
|
||||
);
|
||||
|
||||
// longest_reign: both tied at 3 cities every turn → clan 1 wins.
|
||||
assert_eq!(
|
||||
by_id["longest_reign"].clan,
|
||||
ClanId(1),
|
||||
"tie on turns_leading_city_count must resolve to lowest clan_id"
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue