use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct SubjectId {
pub observer: String,
pub target: String,
}
impl SubjectId {
pub fn new(observer: impl Into<String>, target: impl Into<String>) -> Self {
Self {
observer: observer.into(),
target: target.into(),
}
}
pub fn relation(observer: &str, target: &str) -> Self {
Self::new(observer, target)
}
pub fn reverse(&self) -> Self {
Self {
observer: self.target.clone(),
target: self.observer.clone(),
}
}
}
impl fmt::Display for SubjectId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}->{}", self.observer, self.target)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReputationEntry {
pub subject_id: SubjectId,
pub score: f32,
pub category: Option<String>,
#[serde(default)]
pub metadata: serde_json::Value,
}
impl ReputationEntry {
pub fn new(subject_id: SubjectId, score: f32) -> Self {
Self {
subject_id,
score,
category: None,
metadata: serde_json::Value::Null,
}
}
pub fn with_category(mut self, category: impl Into<String>) -> Self {
self.category = Some(category.into());
self
}
pub fn with_metadata(mut self, metadata: serde_json::Value) -> Self {
self.metadata = metadata;
self
}
pub fn adjust(&mut self, delta: f32) {
self.score += delta;
}
pub fn set_score(&mut self, score: f32) {
self.score = score;
}
pub fn clamp(&mut self, min: f32, max: f32) {
self.score = self.score.clamp(min, max);
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReputationThreshold {
pub name: String,
pub min: f32,
pub max: f32,
pub color: Option<String>,
}
impl ReputationThreshold {
pub fn new(name: impl Into<String>, min: f32, max: f32) -> Self {
Self {
name: name.into(),
min,
max,
color: None,
}
}
pub fn with_color(mut self, color: impl Into<String>) -> Self {
self.color = Some(color.into());
self
}
pub fn contains(&self, score: f32) -> bool {
score >= self.min && score < self.max
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReputationError {
SubjectNotFound,
InvalidRange,
}
impl fmt::Display for ReputationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ReputationError::SubjectNotFound => write!(f, "Subject not found"),
ReputationError::InvalidRange => write!(f, "Invalid score range"),
}
}
}
impl std::error::Error for ReputationError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_subject_id() {
let id = SubjectId::new("player", "kingdom");
assert_eq!(id.observer, "player");
assert_eq!(id.target, "kingdom");
assert_eq!(id.to_string(), "player->kingdom");
}
#[test]
fn test_subject_id_relation() {
let id = SubjectId::relation("oda", "tokugawa");
assert_eq!(id.observer, "oda");
assert_eq!(id.target, "tokugawa");
}
#[test]
fn test_subject_id_reverse() {
let id = SubjectId::new("oda", "tokugawa");
let rev = id.reverse();
assert_eq!(rev.observer, "tokugawa");
assert_eq!(rev.target, "oda");
}
#[test]
fn test_reputation_entry() {
let id = SubjectId::new("player", "npc_alice");
let entry = ReputationEntry::new(id.clone(), 50.0);
assert_eq!(entry.subject_id, id);
assert_eq!(entry.score, 50.0);
assert!(entry.category.is_none());
}
#[test]
fn test_reputation_entry_with_category() {
let id = SubjectId::new("player", "npc_alice");
let entry = ReputationEntry::new(id, 75.0).with_category("romance");
assert_eq!(entry.category, Some("romance".into()));
}
#[test]
fn test_reputation_entry_adjust() {
let id = SubjectId::new("player", "kingdom");
let mut entry = ReputationEntry::new(id, 50.0);
entry.adjust(10.0);
assert_eq!(entry.score, 60.0);
entry.adjust(-15.0);
assert_eq!(entry.score, 45.0);
}
#[test]
fn test_reputation_entry_clamp() {
let id = SubjectId::new("player", "kingdom");
let mut entry = ReputationEntry::new(id, 150.0);
entry.clamp(-100.0, 100.0);
assert_eq!(entry.score, 100.0);
entry.set_score(-150.0);
entry.clamp(-100.0, 100.0);
assert_eq!(entry.score, -100.0);
}
#[test]
fn test_threshold() {
let threshold = ReputationThreshold::new("Friendly", 10.0, 50.0);
assert!(threshold.contains(10.0));
assert!(threshold.contains(25.0));
assert!(!threshold.contains(50.0));
assert!(!threshold.contains(5.0));
}
#[test]
fn test_threshold_with_color() {
let threshold = ReputationThreshold::new("Hostile", -100.0, -50.0).with_color("red");
assert_eq!(threshold.color, Some("red".into()));
}
#[test]
fn test_reputation_error_display() {
assert_eq!(
ReputationError::SubjectNotFound.to_string(),
"Subject not found"
);
assert_eq!(
ReputationError::InvalidRange.to_string(),
"Invalid score range"
);
}
}