1#![allow(dead_code)]
5#![allow(clippy::too_many_arguments)]
6
7use std::collections::HashMap;
13
14#[derive(Debug, Clone)]
20pub struct GeneticParams {
21 pub height: f32,
22 pub weight: f32,
23 pub muscle: f32,
24 pub age: f32,
25 pub extra: HashMap<String, f32>,
27}
28
29impl GeneticParams {
30 pub fn new() -> Self {
32 Self {
33 height: 0.0,
34 weight: 0.0,
35 muscle: 0.0,
36 age: 0.0,
37 extra: HashMap::new(),
38 }
39 }
40}
41
42impl Default for GeneticParams {
43 fn default() -> Self {
44 Self::new()
45 }
46}
47
48#[derive(Debug, Clone)]
52pub struct GeneticProfile {
53 pub name: String,
54 pub parent_a: GeneticParams,
55 pub parent_b: GeneticParams,
56 pub dominance: f32,
58 pub seed: Option<u32>,
60}
61
62impl GeneticProfile {
63 pub fn new(name: impl Into<String>, parent_a: GeneticParams, parent_b: GeneticParams) -> Self {
65 Self {
66 name: name.into(),
67 parent_a,
68 parent_b,
69 dominance: 0.5,
70 seed: None,
71 }
72 }
73}
74
75#[derive(Debug, Clone, Default)]
79pub struct GeneticPopulation {
80 pub profiles: Vec<GeneticProfile>,
81}
82
83impl GeneticPopulation {
84 pub fn new() -> Self {
86 Self {
87 profiles: Vec::new(),
88 }
89 }
90
91 pub fn add(&mut self, profile: GeneticProfile) {
93 self.profiles.push(profile);
94 }
95
96 pub fn count(&self) -> usize {
98 self.profiles.len()
99 }
100
101 pub fn blend_all(&self) -> Vec<GeneticParams> {
103 self.profiles.iter().map(dominant_blend).collect()
104 }
105
106 pub fn diversity_score(&self) -> f32 {
110 let blended = self.blend_all();
111 let n = blended.len();
112 if n < 2 {
113 return 0.0;
114 }
115 let mut total = 0.0_f32;
116 let mut count = 0u32;
117 for i in 0..n {
118 for j in (i + 1)..n {
119 total += params_distance(&blended[i], &blended[j]);
120 count += 1;
121 }
122 }
123 if count == 0 {
124 0.0
125 } else {
126 total / count as f32
127 }
128 }
129}
130
131pub fn lcg_f32(seed: &mut u32) -> f32 {
139 *seed = seed.wrapping_mul(1_664_525).wrapping_add(1_013_904_223);
140 (*seed >> 9) as f32 / (1u32 << 23) as f32
142}
143
144pub fn params_distance(a: &GeneticParams, b: &GeneticParams) -> f32 {
146 let dh = a.height - b.height;
147 let dw = a.weight - b.weight;
148 let dm = a.muscle - b.muscle;
149 let da = a.age - b.age;
150 (dh * dh + dw * dw + dm * dm + da * da).sqrt()
151}
152
153pub fn clamp_params(p: &mut GeneticParams) {
155 p.height = p.height.clamp(0.0, 1.0);
156 p.weight = p.weight.clamp(0.0, 1.0);
157 p.muscle = p.muscle.clamp(0.0, 1.0);
158 p.age = p.age.clamp(0.0, 1.0);
159 for v in p.extra.values_mut() {
160 *v = v.clamp(0.0, 1.0);
161 }
162}
163
164pub fn average_params(params: &[GeneticParams]) -> Option<GeneticParams> {
168 if params.is_empty() {
169 return None;
170 }
171 let n = params.len() as f32;
172 let mut acc = GeneticParams::new();
173 for p in params {
174 acc.height += p.height;
175 acc.weight += p.weight;
176 acc.muscle += p.muscle;
177 acc.age += p.age;
178 for (k, v) in &p.extra {
179 *acc.extra.entry(k.clone()).or_insert(0.0) += v;
180 }
181 }
182 acc.height /= n;
183 acc.weight /= n;
184 acc.muscle /= n;
185 acc.age /= n;
186 for v in acc.extra.values_mut() {
187 *v /= n;
188 }
189 Some(acc)
190}
191
192pub fn blend_params(a: &GeneticParams, b: &GeneticParams, t: f32) -> GeneticParams {
202 let lerp = |va: f32, vb: f32| va * t + vb * (1.0 - t);
203
204 let mut extra: HashMap<String, f32> = HashMap::new();
205
206 for (k, va) in &a.extra {
208 let vb = b.extra.get(k).copied().unwrap_or(0.0);
209 extra.insert(k.clone(), lerp(*va, vb));
210 }
211 for (k, vb) in &b.extra {
213 if !a.extra.contains_key(k) {
214 extra.insert(k.clone(), lerp(0.0, *vb));
215 }
216 }
217
218 GeneticParams {
219 height: lerp(a.height, b.height),
220 weight: lerp(a.weight, b.weight),
221 muscle: lerp(a.muscle, b.muscle),
222 age: lerp(a.age, b.age),
223 extra,
224 }
225}
226
227pub fn dominant_blend(profile: &GeneticProfile) -> GeneticParams {
233 let mut result = blend_params(&profile.parent_a, &profile.parent_b, profile.dominance);
234
235 if let Some(s) = profile.seed {
236 let mut s_local = s;
237 let noise_scale = 0.05_f32;
238 result.height += (lcg_f32(&mut s_local) - 0.5) * noise_scale;
239 result.weight += (lcg_f32(&mut s_local) - 0.5) * noise_scale;
240 result.muscle += (lcg_f32(&mut s_local) - 0.5) * noise_scale;
241 result.age += (lcg_f32(&mut s_local) - 0.5) * noise_scale;
242 clamp_params(&mut result);
243 }
244
245 result
246}
247
248pub fn inherit_random(profile: &GeneticProfile, seed: u32) -> GeneticParams {
251 let mut s = seed;
252
253 let pick = |va: f32, vb: f32, s: &mut u32| -> f32 {
254 if lcg_f32(s) >= 0.5 {
255 va
256 } else {
257 vb
258 }
259 };
260
261 let height = pick(profile.parent_a.height, profile.parent_b.height, &mut s);
262 let weight = pick(profile.parent_a.weight, profile.parent_b.weight, &mut s);
263 let muscle = pick(profile.parent_a.muscle, profile.parent_b.muscle, &mut s);
264 let age = pick(profile.parent_a.age, profile.parent_b.age, &mut s);
265
266 let mut extra: HashMap<String, f32> = HashMap::new();
268 let mut all_keys: Vec<String> = profile.parent_a.extra.keys().cloned().collect();
269 for k in profile.parent_b.extra.keys() {
270 if !profile.parent_a.extra.contains_key(k) {
271 all_keys.push(k.clone());
272 }
273 }
274 for k in all_keys {
275 let va = profile.parent_a.extra.get(&k).copied().unwrap_or(0.0);
276 let vb = profile.parent_b.extra.get(&k).copied().unwrap_or(0.0);
277 extra.insert(k, pick(va, vb, &mut s));
278 }
279
280 GeneticParams {
281 height,
282 weight,
283 muscle,
284 age,
285 extra,
286 }
287}
288
289pub fn crossover_blend(a: &GeneticParams, b: &GeneticParams, crossover_mask: u64) -> GeneticParams {
301 let pick = |va: f32, vb: f32, bit: u64| -> f32 {
302 if (crossover_mask >> bit) & 1 == 1 {
303 va
304 } else {
305 vb
306 }
307 };
308
309 let height = pick(a.height, b.height, 0);
310 let weight = pick(a.weight, b.weight, 1);
311 let muscle = pick(a.muscle, b.muscle, 2);
312 let age = pick(a.age, b.age, 3);
313
314 let mut extra: HashMap<String, f32> = HashMap::new();
315 for k in a.extra.keys().chain(b.extra.keys()) {
316 if extra.contains_key(k) {
317 continue;
318 }
319 let va = a.extra.get(k).copied().unwrap_or(0.0);
320 let vb = b.extra.get(k).copied().unwrap_or(0.0);
321 extra.insert(k.clone(), pick(va, vb, 0));
323 }
324
325 GeneticParams {
326 height,
327 weight,
328 muscle,
329 age,
330 extra,
331 }
332}
333
334#[cfg(test)]
339mod tests {
340 use super::*;
341
342 fn make_a() -> GeneticParams {
343 let mut a = GeneticParams::new();
344 a.height = 1.0;
345 a.weight = 0.8;
346 a.muscle = 0.6;
347 a.age = 0.4;
348 a.extra.insert("nose".to_string(), 0.9);
349 a
350 }
351
352 fn make_b() -> GeneticParams {
353 let mut b = GeneticParams::new();
354 b.height = 0.0;
355 b.weight = 0.2;
356 b.muscle = 0.4;
357 b.age = 0.6;
358 b.extra.insert("nose".to_string(), 0.1);
359 b
360 }
361
362 fn make_profile(dominance: f32, seed: Option<u32>) -> GeneticProfile {
363 GeneticProfile {
364 name: "test".to_string(),
365 parent_a: make_a(),
366 parent_b: make_b(),
367 dominance,
368 seed,
369 }
370 }
371
372 #[test]
373 fn test_genetic_params_default() {
374 let p = GeneticParams::default();
375 assert_eq!(p.height, 0.0);
376 assert_eq!(p.weight, 0.0);
377 assert_eq!(p.muscle, 0.0);
378 assert_eq!(p.age, 0.0);
379 assert!(p.extra.is_empty());
380 }
381
382 #[test]
383 fn test_blend_params_midpoint() {
384 let a = make_a();
385 let b = make_b();
386 let mid = blend_params(&a, &b, 0.5);
387 assert!((mid.height - 0.5).abs() < 1e-5);
388 assert!((mid.weight - 0.5).abs() < 1e-5);
389 assert!((mid.muscle - 0.5).abs() < 1e-5);
390 assert!((mid.age - 0.5).abs() < 1e-5);
391 assert!((mid.extra["nose"] - 0.5).abs() < 1e-5);
392 }
393
394 #[test]
395 fn test_blend_params_full_a() {
396 let a = make_a();
397 let b = make_b();
398 let result = blend_params(&a, &b, 1.0);
399 assert!((result.height - a.height).abs() < 1e-5);
400 assert!((result.weight - a.weight).abs() < 1e-5);
401 assert!((result.muscle - a.muscle).abs() < 1e-5);
402 assert!((result.age - a.age).abs() < 1e-5);
403 }
404
405 #[test]
406 fn test_blend_params_full_b() {
407 let a = make_a();
408 let b = make_b();
409 let result = blend_params(&a, &b, 0.0);
410 assert!((result.height - b.height).abs() < 1e-5);
411 assert!((result.weight - b.weight).abs() < 1e-5);
412 assert!((result.muscle - b.muscle).abs() < 1e-5);
413 assert!((result.age - b.age).abs() < 1e-5);
414 }
415
416 #[test]
417 fn test_dominant_blend_no_seed() {
418 let profile = make_profile(1.0, None);
419 let result = dominant_blend(&profile);
420 assert!((result.height - 1.0).abs() < 1e-5);
422 assert!((result.weight - 0.8).abs() < 1e-5);
423 }
424
425 #[test]
426 fn test_dominant_blend_with_seed() {
427 let profile = make_profile(0.5, Some(42));
428 let result = dominant_blend(&profile);
429 assert!(result.height >= 0.0 && result.height <= 1.0);
431 assert!(result.weight >= 0.0 && result.weight <= 1.0);
432 assert!(result.muscle >= 0.0 && result.muscle <= 1.0);
433 assert!(result.age >= 0.0 && result.age <= 1.0);
434 let profile_no_seed = make_profile(0.5, None);
436 let no_seed = dominant_blend(&profile_no_seed);
437 let differs = (result.height - no_seed.height).abs() > 1e-6
439 || (result.weight - no_seed.weight).abs() > 1e-6
440 || (result.muscle - no_seed.muscle).abs() > 1e-6
441 || (result.age - no_seed.age).abs() > 1e-6;
442 assert!(differs, "noise should affect at least one field");
443 }
444
445 #[test]
446 fn test_inherit_random_valid_range() {
447 let profile = make_profile(0.5, None);
448 let result = inherit_random(&profile, 1234);
449 let valid_h = result.height == 1.0 || result.height == 0.0;
451 let valid_w = result.weight == 0.8 || result.weight == 0.2;
452 let valid_m = result.muscle == 0.6 || result.muscle == 0.4;
453 let valid_a = result.age == 0.4 || result.age == 0.6;
454 assert!(valid_h, "height must be from one of the parents");
455 assert!(valid_w, "weight must be from one of the parents");
456 assert!(valid_m, "muscle must be from one of the parents");
457 assert!(valid_a, "age must be from one of the parents");
458 }
459
460 #[test]
461 fn test_crossover_blend_all_a() {
462 let a = make_a();
463 let b = make_b();
464 let result = crossover_blend(&a, &b, 0b1111);
466 assert!((result.height - a.height).abs() < 1e-5);
467 assert!((result.weight - a.weight).abs() < 1e-5);
468 assert!((result.muscle - a.muscle).abs() < 1e-5);
469 assert!((result.age - a.age).abs() < 1e-5);
470 }
471
472 #[test]
473 fn test_crossover_blend_all_b() {
474 let a = make_a();
475 let b = make_b();
476 let result = crossover_blend(&a, &b, 0b0000);
478 assert!((result.height - b.height).abs() < 1e-5);
479 assert!((result.weight - b.weight).abs() < 1e-5);
480 assert!((result.muscle - b.muscle).abs() < 1e-5);
481 assert!((result.age - b.age).abs() < 1e-5);
482 }
483
484 #[test]
485 fn test_crossover_blend_mixed() {
486 let a = make_a();
487 let b = make_b();
488 let result = crossover_blend(&a, &b, 0b0101);
491 assert!(
492 (result.height - a.height).abs() < 1e-5,
493 "bit0 set → height from A"
494 );
495 assert!(
496 (result.weight - b.weight).abs() < 1e-5,
497 "bit1 clear → weight from B"
498 );
499 assert!(
500 (result.muscle - a.muscle).abs() < 1e-5,
501 "bit2 set → muscle from A"
502 );
503 assert!((result.age - b.age).abs() < 1e-5, "bit3 clear → age from B");
504 }
505
506 #[test]
507 fn test_genetic_population() {
508 let mut pop = GeneticPopulation::new();
509 assert_eq!(pop.count(), 0);
510
511 pop.add(make_profile(0.3, None));
512 pop.add(make_profile(0.7, None));
513 pop.add(make_profile(0.5, Some(99)));
514 assert_eq!(pop.count(), 3);
515
516 let blended = pop.blend_all();
517 assert_eq!(blended.len(), 3);
518
519 for bp in &blended {
521 assert!(bp.height >= 0.0 && bp.height <= 1.0);
522 }
523 }
524
525 #[test]
526 fn test_diversity_score_identical() {
527 let mut pop = GeneticPopulation::new();
528 pop.add(make_profile(0.5, None));
530 pop.add(make_profile(0.5, None));
531 let score = pop.diversity_score();
532 assert!(score.abs() < 1e-5, "identical profiles → diversity = 0");
533 }
534
535 #[test]
536 fn test_params_distance() {
537 let a = make_a();
538 let b = make_b();
539 let d = params_distance(&a, &b);
540 let expected = (1.0_f32 * 1.0 + 0.6 * 0.6 + 0.2 * 0.2 + 0.2 * 0.2_f32).sqrt();
542 assert!(
543 (d - expected).abs() < 1e-4,
544 "L2 distance mismatch: got {d}, expected {expected}"
545 );
546
547 assert!(params_distance(&a, &a).abs() < 1e-6);
549 }
550
551 #[test]
552 fn test_clamp_params() {
553 let mut p = GeneticParams {
554 height: 1.5,
555 weight: -0.3,
556 muscle: 0.5,
557 age: 2.0,
558 extra: {
559 let mut m = HashMap::new();
560 m.insert("x".to_string(), -1.0);
561 m.insert("y".to_string(), 3.0);
562 m
563 },
564 };
565 clamp_params(&mut p);
566 assert_eq!(p.height, 1.0);
567 assert_eq!(p.weight, 0.0);
568 assert_eq!(p.muscle, 0.5);
569 assert_eq!(p.age, 1.0);
570 assert_eq!(p.extra["x"], 0.0);
571 assert_eq!(p.extra["y"], 1.0);
572 }
573
574 #[test]
575 fn test_average_params() {
576 assert!(average_params(&[]).is_none());
578
579 let a = make_a();
580 let b = make_b();
581 let avg = average_params(&[a.clone(), b.clone()]).expect("should succeed");
582 assert!((avg.height - 0.5).abs() < 1e-5);
583 assert!((avg.weight - 0.5).abs() < 1e-5);
584 assert!((avg.muscle - 0.5).abs() < 1e-5);
585 assert!((avg.age - 0.5).abs() < 1e-5);
586
587 let single = average_params(std::slice::from_ref(&a)).expect("should succeed");
589 assert!((single.height - a.height).abs() < 1e-5);
590 }
591}