Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion crates/arcane-infra/src/cluster_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ pub struct ClusterManager {
spatial_index: SpatialIndex,
/// Allocated cluster servers. active_count = allocated_servers.len().
allocated_servers: Vec<ServerHandle>,
/// entity_id → party_id. Empty by default; set via set_party_assignments().
/// Populated into PlayerInfo.party_id each evaluation cycle so AffinityEngine
/// Phase 1b receives party signals.
party_assignments: HashMap<Uuid, Uuid>,
}

impl ClusterManager {
Expand All @@ -32,6 +36,7 @@ impl ClusterManager {
pool,
spatial_index,
allocated_servers: Vec::new(),
party_assignments: HashMap::new(),
}
}

Expand All @@ -44,6 +49,12 @@ impl ClusterManager {
)
}

/// Register entity → party_id assignments. Called once at simulation setup.
/// Entities absent from this map continue to have party_id = None in PlayerInfo.
pub fn set_party_assignments(&mut self, assignments: HashMap<Uuid, Uuid>) {
self.party_assignments = assignments;
}

/// Create with a named clustering model. Supported values: "rules" (default), "affinity".
/// The "affinity" variant requires the `affinity-clustering` feature flag.
pub fn with_model(model_type: &str) -> Self {
Expand Down Expand Up @@ -116,7 +127,7 @@ impl ClusterManager {
position: Vec2::new(pos.x, pos.z),
velocity: Vec2::new(0.0, 0.0),
guild_id: None,
party_id: None,
party_id: self.party_assignments.get(&entity_id).copied(),
})
.collect();

Expand Down
89 changes: 88 additions & 1 deletion crates/arcane-infra/tests/cluster_manager_tests.rs
Original file line number Diff line number Diff line change
@@ -1,13 +1,58 @@
//! Tests for ClusterManager (IN-01). Define expected behavior; implementation must satisfy these.

use arcane_core::Vec3;
use arcane_core::{
clustering_model::{ModelInfo, ValidationResult, WorldStateView},
IClusteringModel, Vec3,
};
use arcane_infra::ClusterManager;
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use uuid::Uuid;

fn uuid(i: u8) -> Uuid {
Uuid::from_bytes([i, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0])
}

struct CapturingModel {
last_view: Mutex<Option<WorldStateView>>,
}

impl CapturingModel {
fn new() -> Arc<Self> {
Arc::new(Self {
last_view: Mutex::new(None),
})
}
fn last_view(&self) -> Option<WorldStateView> {
self.last_view.lock().unwrap().clone()
}
}

impl IClusteringModel for CapturingModel {
fn evaluate(
&self,
view: &WorldStateView,
) -> Vec<arcane_core::clustering_model::ClusterDecision> {
*self.last_view.lock().unwrap() = Some(view.clone());
vec![]
}
fn get_model_info(&self) -> ModelInfo {
ModelInfo {
model_type: "capturing".into(),
version: "0".into(),
trained_at: None,
feature_count: None,
}
}
fn validate_view(&self, _view: &WorldStateView) -> ValidationResult {
ValidationResult {
valid: true,
warnings: vec![],
errors: vec![],
}
}
}

#[test]
fn with_defaults_creates_manager() {
let _manager = ClusterManager::with_defaults();
Expand Down Expand Up @@ -41,3 +86,45 @@ fn get_neighbors_for_cluster_returns_neighbors_from_spatial_index() {
"B's neighbors should include A"
);
}

/// Test D: party_assignments populate PlayerInfo.party_id in WorldStateView.
#[test]
fn party_assignments_populate_player_info() {
use arcane_pool::LocalPool;
use arcane_spatial::SpatialIndex;

let model = CapturingModel::new();
let pool = Arc::new(LocalPool::default());
let mut mgr = ClusterManager::new(model.clone(), pool, SpatialIndex::new());

let (e1, e2, e3) = (uuid(1), uuid(2), uuid(3));
let party = uuid(99);

// e1 and e2 are in the same party; e3 is ungrouped
let mut assignments = HashMap::new();
assignments.insert(e1, party);
assignments.insert(e2, party);
mgr.set_party_assignments(assignments);

let cluster = uuid(10);
mgr.update_entity(e1, cluster, Vec3::new(0.0, 0.0, 0.0));
mgr.update_entity(e2, cluster, Vec3::new(1.0, 0.0, 0.0));
mgr.update_entity(e3, cluster, Vec3::new(2.0, 0.0, 0.0));

mgr.run_evaluation_cycle().unwrap();

let view = model
.last_view()
.expect("model should have received a view");
let p1 = view.players.iter().find(|p| p.player_id == e1).unwrap();
let p2 = view.players.iter().find(|p| p.player_id == e2).unwrap();
let p3 = view.players.iter().find(|p| p.player_id == e3).unwrap();

assert_eq!(p1.party_id, Some(party), "e1 should have party_id set");
assert_eq!(p2.party_id, Some(party), "e2 should have party_id set");
assert_eq!(p3.party_id, None, "e3 should have no party_id");
assert_eq!(
p1.party_id, p2.party_id,
"e1 and e2 must share the same party_id"
);
}
Loading