1#![allow(dead_code)]
5#![allow(clippy::too_many_arguments)]
6
7use crate::params::ParamState;
14use std::collections::HashMap;
15
16const B64_CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
21
22#[derive(Debug, Clone, PartialEq)]
28pub struct CharacterDna {
29 pub bytes: Vec<u8>,
31 pub version: u8,
33}
34
35#[derive(Debug, Clone, PartialEq)]
37pub struct ExtendedDna {
38 pub core: CharacterDna,
39 pub extra_keys: Vec<String>,
40}
41
42fn lcg(state: &mut u64) -> u8 {
47 *state = state
48 .wrapping_mul(6_364_136_223_846_793_005)
49 .wrapping_add(1_442_695_040_888_963_407);
50 ((*state >> 33) & 0xFF) as u8
51}
52
53pub fn encode_dna(params: &ParamState) -> CharacterDna {
65 let mut bytes = vec![
66 1u8,
67 f32_to_u8(params.height),
68 f32_to_u8(params.weight),
69 f32_to_u8(params.muscle),
70 f32_to_u8(params.age),
71 ];
72
73 let mut extras: Vec<(&String, &f32)> = params.extra.iter().collect();
75 extras.sort_by_key(|(k, _)| k.as_str());
76
77 for (key, val) in extras {
78 let key_bytes = key.as_bytes();
79 let key_len = key_bytes.len().min(255) as u8;
81 bytes.push(key_len);
82 bytes.extend_from_slice(&key_bytes[..key_len as usize]);
83 bytes.push(f32_to_u8(*val));
84 }
85
86 CharacterDna { bytes, version: 1 }
87}
88
89pub fn decode_dna(dna: &CharacterDna) -> ParamState {
93 let bytes = &dna.bytes;
94
95 if bytes.is_empty() {
96 return ParamState::default();
97 }
98
99 let mut cursor = 1usize;
101
102 let height = read_u8_f32(bytes, &mut cursor);
103 let weight = read_u8_f32(bytes, &mut cursor);
104 let muscle = read_u8_f32(bytes, &mut cursor);
105 let age = read_u8_f32(bytes, &mut cursor);
106
107 let mut extra = HashMap::new();
108
109 while cursor < bytes.len() {
110 let key_len = bytes[cursor] as usize;
112 cursor += 1;
113
114 if cursor + key_len + 1 > bytes.len() {
115 break; }
117
118 let key_bytes = &bytes[cursor..cursor + key_len];
119 cursor += key_len;
120
121 let key = String::from_utf8_lossy(key_bytes).into_owned();
122 let val = u8_to_f32(bytes[cursor]);
123 cursor += 1;
124
125 extra.insert(key, val);
126 }
127
128 ParamState {
129 height,
130 weight,
131 muscle,
132 age,
133 extra,
134 }
135}
136
137pub fn dna_to_hex(dna: &CharacterDna) -> String {
143 dna.bytes
144 .iter()
145 .map(|b| format!("{:02x}", b))
146 .collect::<String>()
147}
148
149pub fn dna_from_hex(hex: &str) -> anyhow::Result<CharacterDna> {
153 let hex = hex.trim();
154 if !hex.len().is_multiple_of(2) {
155 anyhow::bail!("hex string has odd length: {}", hex.len());
156 }
157
158 let mut bytes = Vec::with_capacity(hex.len() / 2);
159 let chars: Vec<char> = hex.chars().collect();
160 let mut i = 0;
161 while i < chars.len() {
162 let hi = hex_char(chars[i])?;
163 let lo = hex_char(chars[i + 1])?;
164 bytes.push((hi << 4) | lo);
165 i += 2;
166 }
167
168 let version = bytes.first().copied().unwrap_or(1);
169 Ok(CharacterDna { bytes, version })
170}
171
172pub fn dna_to_base64(dna: &CharacterDna) -> String {
178 let input = &dna.bytes;
179 let mut out = Vec::with_capacity(input.len().div_ceil(3) * 4);
180
181 let mut i = 0;
182 while i + 2 < input.len() {
183 let b0 = input[i] as u32;
184 let b1 = input[i + 1] as u32;
185 let b2 = input[i + 2] as u32;
186 let n = (b0 << 16) | (b1 << 8) | b2;
187 out.push(B64_CHARS[((n >> 18) & 0x3F) as usize]);
188 out.push(B64_CHARS[((n >> 12) & 0x3F) as usize]);
189 out.push(B64_CHARS[((n >> 6) & 0x3F) as usize]);
190 out.push(B64_CHARS[(n & 0x3F) as usize]);
191 i += 3;
192 }
193
194 let rem = input.len() - i;
195 if rem == 1 {
196 let b0 = input[i] as u32;
197 let n = b0 << 16;
198 out.push(B64_CHARS[((n >> 18) & 0x3F) as usize]);
199 out.push(B64_CHARS[((n >> 12) & 0x3F) as usize]);
200 out.push(b'=');
201 out.push(b'=');
202 } else if rem == 2 {
203 let b0 = input[i] as u32;
204 let b1 = input[i + 1] as u32;
205 let n = (b0 << 16) | (b1 << 8);
206 out.push(B64_CHARS[((n >> 18) & 0x3F) as usize]);
207 out.push(B64_CHARS[((n >> 12) & 0x3F) as usize]);
208 out.push(B64_CHARS[((n >> 6) & 0x3F) as usize]);
209 out.push(b'=');
210 }
211
212 unsafe { String::from_utf8_unchecked(out) }
214}
215
216pub fn dna_from_base64(s: &str) -> anyhow::Result<CharacterDna> {
218 let s = s.trim();
219 if !s.len().is_multiple_of(4) {
220 anyhow::bail!(
221 "base64 string length must be a multiple of 4, got {}",
222 s.len()
223 );
224 }
225
226 let chars: Vec<u8> = s.bytes().collect();
227 let mut bytes: Vec<u8> = Vec::with_capacity(s.len() / 4 * 3);
228 let mut i = 0;
229
230 while i < chars.len() {
231 let c0 = b64_val(chars[i])?;
232 let c1 = b64_val(chars[i + 1])?;
233 let c2_raw = chars[i + 2];
234 let c3_raw = chars[i + 3];
235
236 bytes.push((c0 << 2) | (c1 >> 4));
237
238 if c2_raw != b'=' {
239 let c2 = b64_val(c2_raw)?;
240 bytes.push(((c1 & 0x0F) << 4) | (c2 >> 2));
241 if c3_raw != b'=' {
242 let c3 = b64_val(c3_raw)?;
243 bytes.push(((c2 & 0x03) << 6) | c3);
244 }
245 }
246
247 i += 4;
248 }
249
250 let version = bytes.first().copied().unwrap_or(1);
251 Ok(CharacterDna { bytes, version })
252}
253
254pub fn dna_distance(a: &CharacterDna, b: &CharacterDna) -> f32 {
261 let min_len = a.bytes.len().min(b.bytes.len());
262 let sum: u32 = a.bytes[..min_len]
263 .iter()
264 .zip(b.bytes[..min_len].iter())
265 .map(|(x, y)| (*x as i32 - *y as i32).unsigned_abs())
266 .sum();
267 sum as f32
268}
269
270pub fn mutate_dna(dna: &CharacterDna, rate: f32, seed: u64) -> CharacterDna {
275 let rate = rate.clamp(0.0, 1.0);
276 let threshold = (rate * 255.0) as u8;
277 let mut state = seed;
278 let mut bytes = dna.bytes.clone();
279
280 for (i, byte) in bytes.iter_mut().enumerate() {
281 if i == 0 {
282 continue; }
284 let roll = lcg(&mut state);
285 if roll < threshold {
286 let delta = lcg(&mut state);
287 *byte = byte.wrapping_add(delta).wrapping_sub(128);
288 }
289 }
290
291 CharacterDna {
292 bytes,
293 version: dna.version,
294 }
295}
296
297pub fn crossover_dna(a: &CharacterDna, b: &CharacterDna, seed: u64) -> CharacterDna {
302 let len = a.bytes.len().max(b.bytes.len());
303 let mut state = seed;
304 let mut bytes = Vec::with_capacity(len);
305
306 for i in 0..len {
307 if i == 0 {
308 bytes.push(a.version);
309 continue;
310 }
311 let pick_a = (lcg(&mut state) & 1) == 0;
312 let byte = if pick_a {
313 a.bytes.get(i).copied().unwrap_or(0)
314 } else {
315 b.bytes.get(i).copied().unwrap_or(0)
316 };
317 bytes.push(byte);
318 }
319
320 CharacterDna {
321 bytes,
322 version: a.version,
323 }
324}
325
326pub fn dna_to_params_map(dna: &CharacterDna) -> HashMap<String, f32> {
329 let params = decode_dna(dna);
330 let mut map = HashMap::new();
331 map.insert("height".to_string(), params.height);
332 map.insert("weight".to_string(), params.weight);
333 map.insert("muscle".to_string(), params.muscle);
334 map.insert("age".to_string(), params.age);
335 for (k, v) in params.extra {
336 map.insert(k, v);
337 }
338 map
339}
340
341#[inline]
346fn f32_to_u8(v: f32) -> u8 {
347 (v.clamp(0.0, 1.0) * 255.0).round() as u8
348}
349
350#[inline]
351fn u8_to_f32(b: u8) -> f32 {
352 b as f32 / 255.0
353}
354
355#[inline]
356fn read_u8_f32(bytes: &[u8], cursor: &mut usize) -> f32 {
357 if *cursor < bytes.len() {
358 let v = u8_to_f32(bytes[*cursor]);
359 *cursor += 1;
360 v
361 } else {
362 0.0
363 }
364}
365
366fn hex_char(c: char) -> anyhow::Result<u8> {
367 match c {
368 '0'..='9' => Ok(c as u8 - b'0'),
369 'a'..='f' => Ok(c as u8 - b'a' + 10),
370 'A'..='F' => Ok(c as u8 - b'A' + 10),
371 _ => anyhow::bail!("invalid hex character: {:?}", c),
372 }
373}
374
375fn b64_val(c: u8) -> anyhow::Result<u8> {
376 match c {
377 b'A'..=b'Z' => Ok(c - b'A'),
378 b'a'..=b'z' => Ok(c - b'a' + 26),
379 b'0'..=b'9' => Ok(c - b'0' + 52),
380 b'+' => Ok(62),
381 b'/' => Ok(63),
382 _ => anyhow::bail!("invalid base64 character: {:?}", c as char),
383 }
384}
385
386#[cfg(test)]
391mod tests {
392 use super::*;
393
394 fn make_params(height: f32, weight: f32, muscle: f32, age: f32) -> ParamState {
395 ParamState::new(height, weight, muscle, age)
396 }
397
398 fn make_params_with_extra(
399 height: f32,
400 weight: f32,
401 muscle: f32,
402 age: f32,
403 extra: &[(&str, f32)],
404 ) -> ParamState {
405 let mut p = make_params(height, weight, muscle, age);
406 for (k, v) in extra {
407 p.extra.insert(k.to_string(), *v);
408 }
409 p
410 }
411
412 #[test]
414 fn test_encode_decode_roundtrip_core() {
415 let params = make_params(0.7, 0.4, 0.6, 0.3);
416 let dna = encode_dna(¶ms);
417 let decoded = decode_dna(&dna);
418 assert!(
420 (decoded.height - params.height).abs() < 0.005,
421 "height mismatch"
422 );
423 assert!(
424 (decoded.weight - params.weight).abs() < 0.005,
425 "weight mismatch"
426 );
427 assert!(
428 (decoded.muscle - params.muscle).abs() < 0.005,
429 "muscle mismatch"
430 );
431 assert!((decoded.age - params.age).abs() < 0.005, "age mismatch");
432 }
433
434 #[test]
436 fn test_encode_decode_roundtrip_with_extra() {
437 let params = make_params_with_extra(
438 0.5,
439 0.5,
440 0.5,
441 0.5,
442 &[("nose_width", 0.8), ("jaw_size", 0.2)],
443 );
444 let dna = encode_dna(¶ms);
445 let decoded = decode_dna(&dna);
446 assert!((decoded.extra["nose_width"] - 0.8).abs() < 0.005);
447 assert!((decoded.extra["jaw_size"] - 0.2).abs() < 0.005);
448 }
449
450 #[test]
452 fn test_version_field_is_one() {
453 let params = make_params(0.5, 0.5, 0.5, 0.5);
454 let dna = encode_dna(¶ms);
455 assert_eq!(dna.version, 1);
456 assert_eq!(dna.bytes[0], 1);
457 }
458
459 #[test]
461 fn test_dna_to_hex_roundtrip() {
462 let params = make_params(0.2, 0.8, 0.4, 0.9);
463 let dna = encode_dna(¶ms);
464 let hex = dna_to_hex(&dna);
465 let dna2 = dna_from_hex(&hex).expect("should succeed");
466 assert_eq!(dna.bytes, dna2.bytes);
467 }
468
469 #[test]
471 fn test_dna_from_hex_invalid_char() {
472 let result = dna_from_hex("01ZZ");
473 assert!(result.is_err());
474 }
475
476 #[test]
478 fn test_dna_from_hex_odd_length() {
479 let result = dna_from_hex("01a");
480 assert!(result.is_err());
481 }
482
483 #[test]
485 fn test_dna_to_base64_roundtrip() {
486 let params = make_params(0.3, 0.6, 0.9, 0.1);
487 let dna = encode_dna(¶ms);
488 let b64 = dna_to_base64(&dna);
489 let dna2 = dna_from_base64(&b64).expect("should succeed");
490 assert_eq!(dna.bytes, dna2.bytes);
491 }
492
493 #[test]
495 fn test_dna_to_base64_padding() {
496 let params = make_params(0.5, 0.5, 0.5, 0.5);
498 let dna = encode_dna(¶ms);
499 let b64 = dna_to_base64(&dna);
500 assert_eq!(b64.len() % 4, 0);
501 }
502
503 #[test]
505 fn test_dna_from_base64_invalid_length() {
506 let result = dna_from_base64("ABC"); assert!(result.is_err());
508 }
509
510 #[test]
512 fn test_dna_distance_identical() {
513 let params = make_params(0.5, 0.5, 0.5, 0.5);
514 let dna = encode_dna(¶ms);
515 assert_eq!(dna_distance(&dna, &dna), 0.0);
516 }
517
518 #[test]
520 fn test_dna_distance_different() {
521 let dna_a = encode_dna(&make_params(0.0, 0.0, 0.0, 0.0));
522 let dna_b = encode_dna(&make_params(1.0, 1.0, 1.0, 1.0));
523 let dist = dna_distance(&dna_a, &dna_b);
524 assert!(dist > 0.0, "distance should be positive");
526 assert!(
527 (dist - 255.0 * 4.0).abs() < 1.0,
528 "expected ~1020, got {}",
529 dist
530 );
531 }
532
533 #[test]
535 fn test_mutate_dna_deterministic() {
536 let params = make_params(0.5, 0.5, 0.5, 0.5);
537 let dna = encode_dna(¶ms);
538 let m1 = mutate_dna(&dna, 0.5, 42);
539 let m2 = mutate_dna(&dna, 0.5, 42);
540 assert_eq!(m1.bytes, m2.bytes);
541 }
542
543 #[test]
545 fn test_mutate_dna_zero_rate_unchanged() {
546 let params = make_params(0.5, 0.5, 0.5, 0.5);
547 let dna = encode_dna(¶ms);
548 let mutated = mutate_dna(&dna, 0.0, 99);
549 assert_eq!(dna.bytes, mutated.bytes);
550 }
551
552 #[test]
554 fn test_mutate_dna_preserves_version() {
555 let params = make_params(0.4, 0.6, 0.3, 0.7);
556 let dna = encode_dna(¶ms);
557 let mutated = mutate_dna(&dna, 1.0, 12345);
558 assert_eq!(mutated.version, 1);
559 assert_eq!(mutated.bytes[0], 1);
560 }
561
562 #[test]
564 fn test_crossover_dna_deterministic() {
565 let dna_a = encode_dna(&make_params(0.1, 0.2, 0.3, 0.4));
566 let dna_b = encode_dna(&make_params(0.9, 0.8, 0.7, 0.6));
567 let c1 = crossover_dna(&dna_a, &dna_b, 7);
568 let c2 = crossover_dna(&dna_a, &dna_b, 7);
569 assert_eq!(c1.bytes, c2.bytes);
570 }
571
572 #[test]
574 fn test_crossover_dna_bytes_come_from_parents() {
575 let dna_a = encode_dna(&make_params(0.0, 0.0, 0.0, 0.0));
576 let dna_b = encode_dna(&make_params(1.0, 1.0, 1.0, 1.0));
577 let child = crossover_dna(&dna_a, &dna_b, 99);
578 for (i, &byte) in child.bytes.iter().enumerate() {
579 if i == 0 {
580 assert_eq!(byte, 1, "version byte must be 1");
581 continue;
582 }
583 let a_byte = dna_a.bytes.get(i).copied().unwrap_or(0);
584 let b_byte = dna_b.bytes.get(i).copied().unwrap_or(0);
585 assert!(
586 byte == a_byte || byte == b_byte,
587 "byte {} = {} must come from a ({}) or b ({})",
588 i,
589 byte,
590 a_byte,
591 b_byte
592 );
593 }
594 }
595
596 #[test]
598 fn test_dna_to_params_map_core_keys_present() {
599 let dna = encode_dna(&make_params(0.25, 0.75, 0.5, 0.0));
600 let map = dna_to_params_map(&dna);
601 assert!(map.contains_key("height"));
602 assert!(map.contains_key("weight"));
603 assert!(map.contains_key("muscle"));
604 assert!(map.contains_key("age"));
605 }
606
607 #[test]
609 fn test_dna_to_params_map_values_accurate() {
610 let dna = encode_dna(&make_params(0.25, 0.75, 0.5, 1.0));
611 let map = dna_to_params_map(&dna);
612 assert!((map["height"] - 0.25).abs() < 0.005);
613 assert!((map["weight"] - 0.75).abs() < 0.005);
614 assert!((map["muscle"] - 0.5).abs() < 0.005);
615 assert!((map["age"] - 1.0).abs() < 0.005);
616 }
617
618 #[test]
620 fn test_empty_extra_params() {
621 let params = make_params(0.5, 0.5, 0.5, 0.5);
622 let dna = encode_dna(¶ms);
623 assert_eq!(dna.bytes.len(), 5);
625 let decoded = decode_dna(&dna);
626 assert!(decoded.extra.is_empty());
627 }
628
629 #[test]
631 fn test_hex_output_is_lowercase() {
632 let params = make_params(0.5, 0.5, 0.5, 0.5);
633 let dna = encode_dna(¶ms);
634 let hex = dna_to_hex(&dna);
635 assert_eq!(hex, hex.to_lowercase());
636 }
637
638 #[test]
640 fn test_hex_length_matches_bytes() {
641 let params = make_params_with_extra(0.1, 0.2, 0.3, 0.4, &[("x", 0.5), ("y", 0.6)]);
642 let dna = encode_dna(¶ms);
643 let hex = dna_to_hex(&dna);
644 assert_eq!(hex.len(), dna.bytes.len() * 2);
645 }
646
647 #[test]
649 fn test_dna_boundary_values_zero() {
650 let params = make_params(0.0, 0.0, 0.0, 0.0);
651 let dna = encode_dna(¶ms);
652 let decoded = decode_dna(&dna);
653 assert!(decoded.height.abs() < 0.005);
654 assert!(decoded.weight.abs() < 0.005);
655 assert!(decoded.muscle.abs() < 0.005);
656 assert!(decoded.age.abs() < 0.005);
657 }
658
659 #[test]
661 fn test_dna_boundary_values_one() {
662 let params = make_params(1.0, 1.0, 1.0, 1.0);
663 let dna = encode_dna(¶ms);
664 let decoded = decode_dna(&dna);
665 assert!((decoded.height - 1.0).abs() < 0.005);
666 assert!((decoded.weight - 1.0).abs() < 0.005);
667 assert!((decoded.muscle - 1.0).abs() < 0.005);
668 assert!((decoded.age - 1.0).abs() < 0.005);
669 }
670
671 #[test]
673 fn test_extended_dna_struct() {
674 let dna = encode_dna(&make_params(0.5, 0.5, 0.5, 0.5));
675 let ext = ExtendedDna {
676 core: dna.clone(),
677 extra_keys: vec!["nose_width".to_string(), "jaw_size".to_string()],
678 };
679 assert_eq!(ext.core, dna);
680 assert_eq!(ext.extra_keys.len(), 2);
681 }
682
683 #[test]
685 fn test_lcg_produces_deterministic_sequence() {
686 let mut s1 = 12345u64;
687 let mut s2 = 12345u64;
688 let seq1: Vec<u8> = (0..10).map(|_| lcg(&mut s1)).collect();
689 let seq2: Vec<u8> = (0..10).map(|_| lcg(&mut s2)).collect();
690 assert_eq!(seq1, seq2);
691 }
692
693 #[test]
695 fn test_extra_keys_sorted_deterministically() {
696 let mut p1 = make_params(0.5, 0.5, 0.5, 0.5);
698 p1.extra.insert("z_key".to_string(), 0.3);
699 p1.extra.insert("a_key".to_string(), 0.7);
700
701 let mut p2 = make_params(0.5, 0.5, 0.5, 0.5);
702 p2.extra.insert("a_key".to_string(), 0.7);
703 p2.extra.insert("z_key".to_string(), 0.3);
704
705 let d1 = encode_dna(&p1);
706 let d2 = encode_dna(&p2);
707 assert_eq!(d1.bytes, d2.bytes);
708 }
709}