1use serde::{Deserialize, Serialize};
2use uuid::Uuid;
3
4use crate::capability::Capability;
5use crate::config::ModelConfig;
6use crate::tenant::TenantId;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10#[serde(transparent)]
11pub struct FighterId(pub Uuid);
12
13impl FighterId {
14 pub fn new() -> Self {
15 Self(Uuid::new_v4())
16 }
17}
18
19impl Default for FighterId {
20 fn default() -> Self {
21 Self::new()
22 }
23}
24
25impl std::fmt::Display for FighterId {
26 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
27 write!(f, "{}", self.0)
28 }
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
35#[serde(rename_all = "snake_case")]
36pub enum WeightClass {
37 Featherweight,
39 Middleweight,
41 Heavyweight,
43 Champion,
45}
46
47impl std::fmt::Display for WeightClass {
48 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49 match self {
50 Self::Featherweight => write!(f, "featherweight"),
51 Self::Middleweight => write!(f, "middleweight"),
52 Self::Heavyweight => write!(f, "heavyweight"),
53 Self::Champion => write!(f, "champion"),
54 }
55 }
56}
57
58#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct FighterManifest {
61 pub name: String,
63 pub description: String,
65 pub model: ModelConfig,
67 pub system_prompt: String,
69 pub capabilities: Vec<Capability>,
71 pub weight_class: WeightClass,
73 #[serde(default, skip_serializing_if = "Option::is_none")]
75 pub tenant_id: Option<TenantId>,
76}
77
78#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
80#[serde(rename_all = "snake_case")]
81pub enum FighterStatus {
82 Idle,
84 Fighting,
86 Resting,
88 KnockedOut,
90 Training,
92}
93
94impl std::fmt::Display for FighterStatus {
95 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
96 match self {
97 Self::Idle => write!(f, "idle"),
98 Self::Fighting => write!(f, "fighting"),
99 Self::Resting => write!(f, "resting"),
100 Self::KnockedOut => write!(f, "knocked_out"),
101 Self::Training => write!(f, "training"),
102 }
103 }
104}
105
106#[derive(Debug, Clone, Default, Serialize, Deserialize)]
108pub struct FighterStats {
109 pub messages_sent: u64,
111 pub tokens_used: u64,
113 pub bouts_won: u64,
115 pub knockouts: u64,
117}
118
119#[cfg(test)]
120mod tests {
121 use super::*;
122 use crate::config::{ModelConfig, Provider};
123
124 #[test]
125 fn test_fighter_id_display() {
126 let uuid = Uuid::nil();
127 let id = FighterId(uuid);
128 assert_eq!(id.to_string(), uuid.to_string());
129 }
130
131 #[test]
132 fn test_fighter_id_new_is_unique() {
133 let id1 = FighterId::new();
134 let id2 = FighterId::new();
135 assert_ne!(id1, id2);
136 }
137
138 #[test]
139 fn test_fighter_id_default() {
140 let id = FighterId::default();
141 assert_ne!(id.0, Uuid::nil());
142 }
143
144 #[test]
145 fn test_fighter_id_serde_transparent() {
146 let uuid = Uuid::new_v4();
147 let id = FighterId(uuid);
148 let json = serde_json::to_string(&id).expect("serialize");
149 assert_eq!(json, format!("\"{}\"", uuid));
151 let deser: FighterId = serde_json::from_str(&json).expect("deserialize");
152 assert_eq!(deser, id);
153 }
154
155 #[test]
156 fn test_fighter_id_copy_clone() {
157 let id = FighterId::new();
158 let copied = id; let cloned = id.clone();
160 assert_eq!(id, copied);
161 assert_eq!(id, cloned);
162 }
163
164 #[test]
165 fn test_fighter_id_hash() {
166 let id = FighterId::new();
167 let mut set = std::collections::HashSet::new();
168 set.insert(id);
169 set.insert(id); assert_eq!(set.len(), 1);
171 }
172
173 #[test]
174 fn test_weight_class_display() {
175 assert_eq!(WeightClass::Featherweight.to_string(), "featherweight");
176 assert_eq!(WeightClass::Middleweight.to_string(), "middleweight");
177 assert_eq!(WeightClass::Heavyweight.to_string(), "heavyweight");
178 assert_eq!(WeightClass::Champion.to_string(), "champion");
179 }
180
181 #[test]
182 fn test_weight_class_serde_roundtrip() {
183 let classes = vec![
184 WeightClass::Featherweight,
185 WeightClass::Middleweight,
186 WeightClass::Heavyweight,
187 WeightClass::Champion,
188 ];
189 for wc in &classes {
190 let json = serde_json::to_string(wc).expect("serialize");
191 let deser: WeightClass = serde_json::from_str(&json).expect("deserialize");
192 assert_eq!(&deser, wc);
193 }
194 }
195
196 #[test]
197 fn test_weight_class_serde_values() {
198 assert_eq!(
199 serde_json::to_string(&WeightClass::Featherweight).unwrap(),
200 "\"featherweight\""
201 );
202 assert_eq!(
203 serde_json::to_string(&WeightClass::Champion).unwrap(),
204 "\"champion\""
205 );
206 }
207
208 #[test]
209 fn test_fighter_status_display() {
210 assert_eq!(FighterStatus::Idle.to_string(), "idle");
211 assert_eq!(FighterStatus::Fighting.to_string(), "fighting");
212 assert_eq!(FighterStatus::Resting.to_string(), "resting");
213 assert_eq!(FighterStatus::KnockedOut.to_string(), "knocked_out");
214 assert_eq!(FighterStatus::Training.to_string(), "training");
215 }
216
217 #[test]
218 fn test_fighter_status_serde_roundtrip() {
219 let statuses = vec![
220 FighterStatus::Idle,
221 FighterStatus::Fighting,
222 FighterStatus::Resting,
223 FighterStatus::KnockedOut,
224 FighterStatus::Training,
225 ];
226 for status in &statuses {
227 let json = serde_json::to_string(status).expect("serialize");
228 let deser: FighterStatus = serde_json::from_str(&json).expect("deserialize");
229 assert_eq!(&deser, status);
230 }
231 }
232
233 #[test]
234 fn test_fighter_status_equality() {
235 assert_eq!(FighterStatus::Idle, FighterStatus::Idle);
236 assert_ne!(FighterStatus::Idle, FighterStatus::Fighting);
237 }
238
239 #[test]
240 fn test_fighter_stats_default() {
241 let stats = FighterStats::default();
242 assert_eq!(stats.messages_sent, 0);
243 assert_eq!(stats.tokens_used, 0);
244 assert_eq!(stats.bouts_won, 0);
245 assert_eq!(stats.knockouts, 0);
246 }
247
248 #[test]
249 fn test_fighter_stats_serde_roundtrip() {
250 let stats = FighterStats {
251 messages_sent: 100,
252 tokens_used: 50000,
253 bouts_won: 10,
254 knockouts: 2,
255 };
256 let json = serde_json::to_string(&stats).expect("serialize");
257 let deser: FighterStats = serde_json::from_str(&json).expect("deserialize");
258 assert_eq!(deser.messages_sent, 100);
259 assert_eq!(deser.tokens_used, 50000);
260 assert_eq!(deser.bouts_won, 10);
261 assert_eq!(deser.knockouts, 2);
262 }
263
264 #[test]
265 fn test_fighter_manifest_serde() {
266 let manifest = FighterManifest {
267 name: "TestFighter".to_string(),
268 description: "A test fighter".to_string(),
269 model: ModelConfig {
270 provider: Provider::Anthropic,
271 model: "claude-sonnet-4-20250514".to_string(),
272 api_key_env: None,
273 base_url: None,
274 max_tokens: None,
275 temperature: None,
276 },
277 system_prompt: "You are helpful".to_string(),
278 capabilities: vec![Capability::Memory],
279 weight_class: WeightClass::Middleweight,
280 tenant_id: None,
281 };
282 let json = serde_json::to_string(&manifest).expect("serialize");
283 let deser: FighterManifest = serde_json::from_str(&json).expect("deserialize");
284 assert_eq!(deser.name, "TestFighter");
285 assert_eq!(deser.weight_class, WeightClass::Middleweight);
286 assert_eq!(deser.capabilities.len(), 1);
287 assert!(deser.tenant_id.is_none());
288 }
289}