use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use thiserror::Error;
pub type MemberId = String;
pub type FactionId = String;
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum RelationType {
Trust { strength: f32 },
Debt { amount: f32 },
SharedSecret { secret_id: String, sensitivity: f32 },
FactionMembership { faction_id: FactionId },
Hostility { intensity: f32 },
Custom(String),
}
impl RelationType {
pub fn description(&self) -> String {
match self {
RelationType::Trust { strength } => format!("Trust ({})", strength),
RelationType::Debt { amount } if *amount > 0.0 => {
format!("They owe me ({})", amount)
}
RelationType::Debt { amount } => format!("I owe them ({})", -amount),
RelationType::SharedSecret { secret_id, .. } => {
format!("Shared secret: {}", secret_id)
}
RelationType::FactionMembership { faction_id } => {
format!("Faction: {}", faction_id)
}
RelationType::Hostility { intensity } => format!("Hostility ({})", intensity),
RelationType::Custom(name) => name.clone(),
}
}
pub fn creates_obligation(&self) -> bool {
matches!(
self,
RelationType::Debt { .. } | RelationType::SharedSecret { .. }
)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct CentralityMetrics {
pub degree: f32,
pub betweenness: f32,
pub closeness: f32,
pub eigenvector: f32,
pub overall_influence: f32,
}
impl Default for CentralityMetrics {
fn default() -> Self {
Self {
degree: 0.0,
betweenness: 0.0,
closeness: 0.0,
eigenvector: 0.0,
overall_influence: 0.0,
}
}
}
impl CentralityMetrics {
pub fn calculate_overall(
&mut self,
degree_weight: f32,
betweenness_weight: f32,
closeness_weight: f32,
eigenvector_weight: f32,
) {
self.overall_influence = self.degree * degree_weight
+ self.betweenness * betweenness_weight
+ self.closeness * closeness_weight
+ self.eigenvector * eigenvector_weight;
}
pub fn is_shadow_leader(&self, threshold: f32) -> bool {
self.overall_influence > threshold
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct SocialCapital {
pub reputation: f32,
pub total_favors_owed_to_me: f32,
pub total_favors_i_owe: f32,
pub secrets_held: u32,
pub centrality_scores: CentralityMetrics,
}
impl Default for SocialCapital {
fn default() -> Self {
Self {
reputation: 0.5,
total_favors_owed_to_me: 0.0,
total_favors_i_owe: 0.0,
secrets_held: 0,
centrality_scores: CentralityMetrics::default(),
}
}
}
impl SocialCapital {
pub fn net_favor_balance(&self) -> f32 {
self.total_favors_owed_to_me - self.total_favors_i_owe
}
pub fn can_afford(&self, cost: f32) -> bool {
self.net_favor_balance() >= cost
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub struct Faction {
pub id: FactionId,
pub name: String,
pub members: HashSet<MemberId>,
pub leader: Option<MemberId>,
pub agenda: Vec<String>,
pub cohesion: f32,
pub inter_faction_relations: HashMap<FactionId, f32>,
}
impl Faction {
pub fn new(id: FactionId, name: String) -> Self {
Self {
id,
name,
members: HashSet::new(),
leader: None,
agenda: Vec::new(),
cohesion: 1.0,
inter_faction_relations: HashMap::new(),
}
}
pub fn add_member(&mut self, member_id: MemberId) {
self.members.insert(member_id);
}
pub fn remove_member(&mut self, member_id: &MemberId) -> bool {
self.members.remove(member_id)
}
pub fn is_allied_with(&self, other_faction_id: &FactionId) -> bool {
self.inter_faction_relations
.get(other_faction_id)
.map(|&relation| relation > 0.5)
.unwrap_or(false)
}
pub fn is_hostile_to(&self, other_faction_id: &FactionId) -> bool {
self.inter_faction_relations
.get(other_faction_id)
.map(|&relation| relation < -0.5)
.unwrap_or(false)
}
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum PoliticalAction {
Lobbying {
target: MemberId,
proposal: String,
cost: f32,
},
GrantFavor { target: MemberId, favor_value: f32 },
CallInFavor { target: MemberId, request: String },
ShareSecret {
target: MemberId,
secret_id: String,
sensitivity: f32,
},
SpreadGossip {
about: MemberId,
content: String,
is_positive: bool,
},
FormCoalition {
members: Vec<MemberId>,
agenda: String,
},
Defect {
from_faction: FactionId,
to_faction: Option<FactionId>,
},
}
impl PoliticalAction {
pub fn target(&self) -> Option<&MemberId> {
match self {
PoliticalAction::Lobbying { target, .. }
| PoliticalAction::GrantFavor { target, .. }
| PoliticalAction::CallInFavor { target, .. }
| PoliticalAction::ShareSecret { target, .. }
| PoliticalAction::SpreadGossip { about: target, .. } => Some(target),
_ => None,
}
}
pub fn cost(&self) -> f32 {
match self {
PoliticalAction::Lobbying { cost, .. } => *cost,
PoliticalAction::GrantFavor { favor_value, .. } => *favor_value,
PoliticalAction::ShareSecret { sensitivity, .. } => *sensitivity * 0.5,
_ => 0.0,
}
}
}
#[derive(Debug, Error, Clone, PartialEq)]
pub enum SocialError {
#[error("Member not found: {0}")]
MemberNotFound(MemberId),
#[error("Faction not found: {0}")]
FactionNotFound(FactionId),
#[error("Insufficient social capital: need {required}, have {available}")]
InsufficientCapital { required: f32, available: f32 },
#[error("Invalid relationship between {from} and {to}")]
InvalidRelationship { from: MemberId, to: MemberId },
#[error("Political action failed: {0}")]
ActionFailed(String),
#[error("Network analysis error: {0}")]
AnalysisError(String),
#[error("{0}")]
Custom(String),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_relation_type_description() {
let trust = RelationType::Trust { strength: 0.8 };
assert!(trust.description().contains("Trust"));
let debt = RelationType::Debt { amount: 1.0 };
assert!(debt.description().contains("They owe me"));
}
#[test]
fn test_relation_creates_obligation() {
assert!(RelationType::Debt { amount: 1.0 }.creates_obligation());
assert!(RelationType::SharedSecret {
secret_id: "test".to_string(),
sensitivity: 0.5
}
.creates_obligation());
assert!(!RelationType::Trust { strength: 0.8 }.creates_obligation());
}
#[test]
fn test_centrality_metrics_default() {
let metrics = CentralityMetrics::default();
assert_eq!(metrics.degree, 0.0);
assert_eq!(metrics.overall_influence, 0.0);
}
#[test]
fn test_centrality_calculate_overall() {
let mut metrics = CentralityMetrics {
degree: 0.5,
betweenness: 0.8,
closeness: 0.3,
eigenvector: 0.6,
overall_influence: 0.0,
};
metrics.calculate_overall(0.3, 0.3, 0.2, 0.2);
assert!((metrics.overall_influence - 0.57).abs() < 0.01);
}
#[test]
fn test_centrality_is_shadow_leader() {
let metrics = CentralityMetrics {
overall_influence: 0.85,
..Default::default()
};
assert!(metrics.is_shadow_leader(0.75));
assert!(!metrics.is_shadow_leader(0.90));
}
#[test]
fn test_social_capital_default() {
let capital = SocialCapital::default();
assert_eq!(capital.reputation, 0.5);
assert_eq!(capital.net_favor_balance(), 0.0);
}
#[test]
fn test_social_capital_net_favor_balance() {
let capital = SocialCapital {
total_favors_owed_to_me: 10.0,
total_favors_i_owe: 3.0,
..Default::default()
};
assert_eq!(capital.net_favor_balance(), 7.0);
assert!(capital.can_afford(5.0));
assert!(!capital.can_afford(10.0));
}
#[test]
fn test_faction_new() {
let faction = Faction::new("faction_1".to_string(), "Test Faction".to_string());
assert_eq!(faction.id, "faction_1");
assert_eq!(faction.name, "Test Faction");
assert_eq!(faction.cohesion, 1.0);
assert!(faction.members.is_empty());
}
#[test]
fn test_faction_add_remove_member() {
let mut faction = Faction::new("f1".to_string(), "Test".to_string());
faction.add_member("member1".to_string());
assert!(faction.members.contains("member1"));
let removed = faction.remove_member(&"member1".to_string());
assert!(removed);
assert!(!faction.members.contains("member1"));
}
#[test]
fn test_faction_relations() {
let mut faction = Faction::new("f1".to_string(), "Test".to_string());
faction
.inter_faction_relations
.insert("ally".to_string(), 0.8);
faction
.inter_faction_relations
.insert("enemy".to_string(), -0.8);
assert!(faction.is_allied_with(&"ally".to_string()));
assert!(faction.is_hostile_to(&"enemy".to_string()));
assert!(!faction.is_allied_with(&"unknown".to_string()));
}
#[test]
fn test_political_action_target() {
let action = PoliticalAction::Lobbying {
target: "member1".to_string(),
proposal: "Test".to_string(),
cost: 1.0,
};
assert_eq!(action.target(), Some(&"member1".to_string()));
assert_eq!(action.cost(), 1.0);
}
#[test]
fn test_political_action_cost() {
let grant = PoliticalAction::GrantFavor {
target: "m1".to_string(),
favor_value: 2.0,
};
assert_eq!(grant.cost(), 2.0);
let secret = PoliticalAction::ShareSecret {
target: "m2".to_string(),
secret_id: "s1".to_string(),
sensitivity: 1.0,
};
assert_eq!(secret.cost(), 0.5); }
#[test]
fn test_social_error_display() {
let err = SocialError::MemberNotFound("member1".to_string());
assert!(err.to_string().contains("member1"));
let err = SocialError::InsufficientCapital {
required: 10.0,
available: 5.0,
};
assert!(err.to_string().contains("10"));
assert!(err.to_string().contains("5"));
}
}