feat(mc-replay): ✨ Introduce VisibilityFilter logic for replay history filtering with new functions and comprehensive tests
Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
parent
4884545694
commit
7d77fe7289
3 changed files with 156 additions and 1 deletions
|
|
@ -1,6 +1,8 @@
|
|||
//! [`GameHistory`] container + the [`TurnEventCollector`] resource that
|
||||
//! emitter crates push events into during turn processing.
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::archive::{GameOutcome, MapDescriptor, HISTORY_SCHEMA_VERSION};
|
||||
|
|
@ -8,6 +10,20 @@ use crate::event::TurnEvent;
|
|||
use crate::ids::{ClanId, GameId, LeaderId, PackId, PackVersion};
|
||||
use crate::snapshot::TurnSnapshot;
|
||||
|
||||
/// Contact ledger for the statistics-modal visibility filter (p2-47).
|
||||
///
|
||||
/// Maps each rival `ClanId` a viewer has met to the turn on which first
|
||||
/// contact occurred. A clan absent from the map has never been met (its
|
||||
/// snapshots are fully hidden); a clan present at turn `t` is visible only
|
||||
/// from turn `t` forward — no retroactive backfill of the pre-contact history.
|
||||
///
|
||||
/// The viewer's *own* clan is conventionally included with contact turn `0`
|
||||
/// (you have always "met" yourself) so a player's own rows are never filtered.
|
||||
/// The live emission of this map (first-contact detection in `mc-turn` /
|
||||
/// `mc-comms`) is a separate wiring objective; this type + the filter below
|
||||
/// are the pure, save-decoupled core that consumers project through.
|
||||
pub type MetSet = BTreeMap<ClanId, u32>;
|
||||
|
||||
/// Per-clan static metadata captured once at game-start.
|
||||
///
|
||||
/// Distinct from [`TurnSnapshot`] (which is per-turn dynamic). The Past Games
|
||||
|
|
@ -90,6 +106,30 @@ impl GameHistory {
|
|||
final_turn: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Project this history's snapshots through a viewer's contact ledger
|
||||
/// (p2-47 contact-state visibility).
|
||||
///
|
||||
/// Returns only the snapshots whose `clan_id` the viewer has met, and only
|
||||
/// from the contact turn forward. A clan absent from `met_at` contributes
|
||||
/// no rows; a clan met on turn `t` contributes only rows with
|
||||
/// `snapshot.turn >= t` (no retroactive pre-contact backfill). Append
|
||||
/// order is preserved — sorting remains the consumer's responsibility,
|
||||
/// matching [`GameHistory::snapshots`]'s documented contract.
|
||||
///
|
||||
/// The filter is computed here in Rust (Rail 1); the UI receives a
|
||||
/// pre-filtered list and never re-applies a GDScript fog rule.
|
||||
#[must_use]
|
||||
pub fn snapshots_visible_to(&self, met_at: &MetSet) -> Vec<&TurnSnapshot> {
|
||||
self.snapshots
|
||||
.iter()
|
||||
.filter(|snap| {
|
||||
met_at
|
||||
.get(&snap.clan_id)
|
||||
.is_some_and(|&contact_turn| snap.turn >= contact_turn)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Per-turn buffer of [`TurnEvent`]s.
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ pub use archive::{
|
|||
};
|
||||
pub use awards::{compute_awards, AwardDef, AwardDefs, AwardWinner};
|
||||
pub use event::{LeaderChangeCause, TurnEvent};
|
||||
pub use history::{ClanDescriptor, GameHistory, TurnEventCollector};
|
||||
pub use history::{ClanDescriptor, GameHistory, MetSet, TurnEventCollector};
|
||||
pub use ids::{
|
||||
CityName, ClanId, EraId, GameId, LeaderId, PackId, PackVersion, TechId, TileCoord, UnitKind,
|
||||
WonderId,
|
||||
|
|
|
|||
115
src/simulator/crates/mc-replay/tests/visibility_filter.rs
Normal file
115
src/simulator/crates/mc-replay/tests/visibility_filter.rs
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
//! Integration test: contact-state visibility filter (p2-47 bullet 4).
|
||||
//!
|
||||
//! Covers `GameHistory::snapshots_visible_to(&MetSet)`:
|
||||
//! - a clan never met contributes zero rows;
|
||||
//! - a clan met on turn `t` contributes only rows with `turn >= t`
|
||||
//! (no retroactive pre-contact backfill);
|
||||
//! - the viewer's own clan (contact turn 0) is never filtered.
|
||||
//!
|
||||
//! Run with: `cargo test -p mc-replay --test visibility_filter`
|
||||
|
||||
use mc_replay::history::{ClanDescriptor, GameHistory, MetSet};
|
||||
use mc_replay::ids::{ClanId, GameId, LeaderId, PackId, PackVersion};
|
||||
use mc_replay::archive::MapDescriptor;
|
||||
use mc_replay::TurnSnapshot;
|
||||
|
||||
fn clan(id: u32, name: &str) -> ClanDescriptor {
|
||||
ClanDescriptor {
|
||||
id: ClanId(id),
|
||||
name: name.into(),
|
||||
sigil_key: format!("{name}.png"),
|
||||
colour_rgba: 0xCC_44_11_FF,
|
||||
starting_leader: LeaderId("durin".into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn snap(turn: u32, clan_id: u32) -> TurnSnapshot {
|
||||
TurnSnapshot {
|
||||
turn,
|
||||
clan_id: ClanId(clan_id),
|
||||
population: 1,
|
||||
cities: 1,
|
||||
army_strength: 0.0,
|
||||
gold: 0,
|
||||
gold_per_turn: 0,
|
||||
culture_per_turn: 0.0,
|
||||
tech_count: 0,
|
||||
land_area: 0,
|
||||
buildings_built_total: 0,
|
||||
culture_total: 0.0,
|
||||
score: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Build a 3-clan history (A=1, B=2, C=3) with one snapshot per clan for
|
||||
/// turns 10..=20.
|
||||
fn three_clan_history() -> GameHistory {
|
||||
let mut hist = GameHistory::new(
|
||||
GameId::new_v4(),
|
||||
PackId("age-of-dwarves".into()),
|
||||
PackVersion("0.1.0".into()),
|
||||
42,
|
||||
MapDescriptor { kind: "continents".into(), width: 32, height: 24 },
|
||||
vec![clan(1, "Ardent"), clan(2, "Bronzebeard"), clan(3, "Cragmaw")],
|
||||
);
|
||||
for turn in 10u32..=20 {
|
||||
for cid in 1u32..=3 {
|
||||
hist.snapshots.push(snap(turn, cid));
|
||||
}
|
||||
}
|
||||
hist
|
||||
}
|
||||
|
||||
/// The spec scenario: viewer B (clan 2) contacts C (clan 3) on turn 17.
|
||||
/// B's view of C must contain only snapshots from turn 17 onward; B sees its
|
||||
/// own rows for the full range; B has never met A (clan 1) → zero A rows.
|
||||
#[test]
|
||||
fn met_clan_visible_from_contact_turn_forward() {
|
||||
let hist = three_clan_history();
|
||||
|
||||
// B's contact ledger: self (B) from turn 0; C met on turn 17. A absent.
|
||||
let mut met_at: MetSet = MetSet::new();
|
||||
met_at.insert(ClanId(2), 0); // own clan, always visible
|
||||
met_at.insert(ClanId(3), 17); // C contacted on turn 17
|
||||
|
||||
let visible = hist.snapshots_visible_to(&met_at);
|
||||
|
||||
// No rows for the never-met clan A.
|
||||
assert!(
|
||||
visible.iter().all(|s| s.clan_id != ClanId(1)),
|
||||
"unmet clan A must contribute zero rows"
|
||||
);
|
||||
|
||||
// C (clan 3) visible only from turn 17 forward (17..=20 → 4 rows).
|
||||
let c_rows: Vec<&&TurnSnapshot> =
|
||||
visible.iter().filter(|s| s.clan_id == ClanId(3)).collect();
|
||||
assert_eq!(c_rows.len(), 4, "C visible for turns 17,18,19,20");
|
||||
assert!(
|
||||
c_rows.iter().all(|s| s.turn >= 17),
|
||||
"no pre-contact backfill: every visible C row is turn >= 17"
|
||||
);
|
||||
|
||||
// B (clan 2, self) visible for the full range 10..=20 → 11 rows.
|
||||
let b_rows = visible.iter().filter(|s| s.clan_id == ClanId(2)).count();
|
||||
assert_eq!(b_rows, 11, "own clan B visible across the full turn range");
|
||||
}
|
||||
|
||||
/// Empty contact ledger → fully hidden history (defensive boundary).
|
||||
#[test]
|
||||
fn empty_met_set_hides_everything() {
|
||||
let hist = three_clan_history();
|
||||
let visible = hist.snapshots_visible_to(&MetSet::new());
|
||||
assert!(visible.is_empty(), "no clans met → no snapshots visible");
|
||||
}
|
||||
|
||||
/// Contact on the exact first turn of a clan's data → that turn is included
|
||||
/// (boundary: `turn >= contact_turn`, not `>`).
|
||||
#[test]
|
||||
fn contact_on_boundary_turn_is_inclusive() {
|
||||
let hist = three_clan_history();
|
||||
let mut met_at: MetSet = MetSet::new();
|
||||
met_at.insert(ClanId(3), 20); // contacted on the very last turn
|
||||
let visible = hist.snapshots_visible_to(&met_at);
|
||||
let c_rows = visible.iter().filter(|s| s.clan_id == ClanId(3)).count();
|
||||
assert_eq!(c_rows, 1, "contact on turn 20 → exactly the turn-20 row");
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue