From 32a6813cf472576c87d50bc0aed2c750ea05472e Mon Sep 17 00:00:00 2001 From: vandomej Date: Mon, 8 Sep 2025 11:55:32 -0700 Subject: [PATCH] Increasing code coverage --- evolved-npcs/Cargo.toml | 2 + evolved-npcs/src/fighter_nn/mod.rs | 68 +++++ .../src/fighter_nn/neural_network_utility.rs | 260 ++++++++++++++---- 3 files changed, 282 insertions(+), 48 deletions(-) diff --git a/evolved-npcs/Cargo.toml b/evolved-npcs/Cargo.toml index d29bdf6..c65dd4a 100644 --- a/evolved-npcs/Cargo.toml +++ b/evolved-npcs/Cargo.toml @@ -19,5 +19,7 @@ num_cpus = "1.17.0" rand = "0.9.2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.143" +tempfile = "3.21.0" tokio = { version = "1.47.1", features = ["full"] } +tokio-test = "0.4.4" uuid = "1.18.1" diff --git a/evolved-npcs/src/fighter_nn/mod.rs b/evolved-npcs/src/fighter_nn/mod.rs index bb4fd92..ba5d396 100644 --- a/evolved-npcs/src/fighter_nn/mod.rs +++ b/evolved-npcs/src/fighter_nn/mod.rs @@ -935,6 +935,74 @@ async fn read_score_from_file(file_path: &Path, nn_id: &str) -> Result &highest_id.0 && id < neuron_id && l == layer - }) - .collect::>(); - for (neuron_id, layer) in neurons_to_add { - new_neurons.push((*neuron_id, is_primary, *layer, 0)); + if let Some(highest_id) = highest_id + && highest_id.1 == is_primary + { + let neurons_to_add = target_neurons + .iter() + .filter(|(id, l)| { + id > &highest_id.0 && id < neuron_id && l == layer + }) + .collect::>(); + for (neuron_id, layer) in neurons_to_add { + new_neurons.push((*neuron_id, is_primary, *layer, 0)); - if is_primary { - primary_last_layer = *layer; - } else { - secondary_last_layer = *layer; - } + if is_primary { + primary_last_layer = *layer; + } else { + secondary_last_layer = *layer; } } } @@ -489,25 +486,20 @@ pub fn crossbreed_neuron_arrays( .collect::>(); let highest_id = current_layer_neurons.iter().max_by_key(|(id, _, _, _)| id); - if let Some(highest_id) = highest_id { - if highest_id.1 == is_primary { - let neurons_to_add = target_neurons - .iter() - .filter(|(id, l)| id > &highest_id.0 && *l == layer - 1) - .collect::>(); - for (neuron_id, _) in neurons_to_add { - new_neurons.push(( - *neuron_id, - is_primary, - current_layer, - 0, - )); + if let Some(highest_id) = highest_id + && highest_id.1 == is_primary + { + let neurons_to_add = target_neurons + .iter() + .filter(|(id, l)| id > &highest_id.0 && *l == layer - 1) + .collect::>(); + for (neuron_id, _) in neurons_to_add { + new_neurons.push((*neuron_id, is_primary, current_layer, 0)); - if is_primary { - primary_last_layer = current_layer; - } else { - secondary_last_layer = current_layer; - } + if is_primary { + primary_last_layer = current_layer; + } else { + secondary_last_layer = current_layer; } } } @@ -563,7 +555,9 @@ pub fn crossbreed_neuron_arrays( new_neurons.push((*neuron_id, is_primary, current_layer, 0)); } break; - } else if *neuron_id == &segments.last().unwrap().1 + 1 { + } + // If the neuron id is exactly one more than the last neuron id, we need to ensure that there's at least one neuron from the same individual in the previous layer + else if *neuron_id == &segments.last().unwrap().1 + 1 { let target_layer = if is_primary { primary_last_layer } else { @@ -579,15 +573,15 @@ pub fn crossbreed_neuron_arrays( let highest_id = earlier_layer_neurons .iter() .max_by(|a, b| a.2.cmp(&b.2).then(a.0.cmp(&b.0))); - if let Some(highest_id) = highest_id { - if highest_id.1 == is_primary { - let neurons_to_add = target_neurons - .iter() - .filter(|(id, _)| id > &highest_id.0 && id < neuron_id) - .collect::>(); - for (neuron_id, l) in neurons_to_add { - new_neurons.push((*neuron_id, is_primary, *l, 0)); - } + if let Some(highest_id) = highest_id + && highest_id.1 == is_primary + { + let neurons_to_add = target_neurons + .iter() + .filter(|(id, _)| id > &highest_id.0 && id < neuron_id) + .collect::>(); + for (neuron_id, l) in neurons_to_add { + new_neurons.push((*neuron_id, is_primary, *l, 0)); } } @@ -808,6 +802,45 @@ mod tests { use super::*; + #[test] + fn crossbreed_basic_test() -> Result<(), Box> { + // Create a dummy FighterNN + let fighter_nn = FighterNN { + id: uuid::Uuid::new_v4(), + folder: std::path::PathBuf::from("/tmp"), + population_size: 2, + generation: 0, + scores: vec![], + nn_shapes: vec![], + crossbreed_segments: 1, + weight_initialization_range: -0.5..0.5, + minor_mutation_rate: 0.1, + major_mutation_rate: 0.1, + mutation_weight_range: -0.5..0.5, + id_mapping: vec![], + lerp_amount: 0.5, + generational_lenience: 1, + survival_rate: 0.5, + }; + + // Use very small networks to avoid FANN memory/resource issues + let primary = Fann::new(&[5, 3, 3])?; + let secondary = Fann::new(&[5, 3, 3])?; + + // Run crossbreed + let result = crossbreed(&fighter_nn, &primary, &secondary, 3)?; + + // Check that the result has the correct input and output size + let shape = result.get_layer_sizes(); + assert_eq!(shape[0], 5); + assert_eq!(*shape.last().unwrap(), 3); + // All hidden layers should have at least 1 neuron + for (i, &layer_size) in shape.iter().enumerate().skip(1).take(shape.len() - 2) { + assert!(layer_size > 0, "Hidden layer {} has zero neurons", i); + } + Ok(()) + } + #[test] fn major_mutation_test() -> Result<(), Box> { // Assign @@ -1493,6 +1526,51 @@ mod tests { assert_eq!(result_set, expected); } + #[test] + fn crossbreed_neuron_arrays_secondary_last_layer_final_segment() { + // Use 3 segments and larger neuron arrays to ensure the else-if branch is covered + let segments = vec![(0, 2), (3, 5), (6, 8)]; + // secondary_neurons: (id, layer) + let primary_neurons = generate_neuron_datastructure(&vec![3, 3, 3, 3, 2]); + let secondary_neurons = generate_neuron_datastructure(&vec![3, 3, 3, 3, 3]); + // The last segment is (6, 8), so neuron 9 in secondary_neurons will trigger the else-if branch + + let result = crossbreed_neuron_arrays(segments, primary_neurons, secondary_neurons); + + // Assert: The result should contain a secondary neuron with id 9 and layer 3 + let has_secondary_9_layer_3 = result + .iter() + .any(|&(id, is_primary, layer, _)| id == 9 && !is_primary && layer == 3); + assert!( + has_secondary_9_layer_3, + "Expected a secondary neuron with id 9 in layer 3, indicating secondary_last_layer was used" + ); + } + + #[test] + fn crossbreed_neuron_arrays_prune_layer_exceeds_max() { + // Use the real constant from the module + let max = NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MAX; + let layer = 1; + // Create more than max neurons in layer 1, alternating primary/secondary + let primary_neurons = generate_neuron_datastructure(&vec![1, (max + 3) as u32, 1]); + let secondary_neurons = generate_neuron_datastructure(&vec![1, (max + 3) as u32, 1]); + // Segments: one for input, one for the large layer, one for output + let segments = vec![ + (0, 0), + (1, (max + 3) as u32), + ((max + 4) as u32, (max + 4) as u32), + ]; + let result = crossbreed_neuron_arrays(segments, primary_neurons, secondary_neurons); + + // Count neurons in layer 1 + let count = result.iter().filter(|(_, _, l, _)| *l == layer).count(); + assert!( + count <= max, + "Layer should be pruned to NEURAL_NETWORK_HIDDEN_LAYER_SIZE_MAX neurons" + ); + } + #[test] fn generate_neuron_datastructure_test() { // Assign @@ -1866,6 +1944,92 @@ mod tests { } } + // Setup for bias connection fallback + // We'll target hidden layer 3 (layer index 3), neuron 20 (first in that layer, is_primary = false) + let bias_layer = 3; + let bias_neuron_primary = get_bias_neuron_for_layer(bias_layer, &primary_shape).unwrap(); + + // Add a bias connection in primary if not present + let mut primary_connections = primary_fann.get_connections(); + for connection in primary_connections.iter_mut() { + if connection.from_neuron == bias_neuron_primary { + // Set to a unique weight to verify it's not used + connection.weight = 12345.0; + } + } + primary_fann.set_connections(&primary_connections); + + // Secondary network needs to have only 1 hidden layer, so it won't have a bias connection for layer 3 + let secondary_shape = vec![4, 8, 6]; + let mut secondary_fann = Fann::new(&secondary_shape)?; + let mut secondary_connections = secondary_fann.get_connections(); + for connection in secondary_connections.iter_mut() { + connection.weight = ((connection.from_neuron * 100) + connection.to_neuron) as f32; + connection.weight = connection.weight * -1.0; + } + secondary_fann.set_connections(&secondary_connections); + + let new_neurons = vec![ + // Input layer: Expect 4 + (0, true, 0, 0), + (1, true, 0, 1), + (2, true, 0, 2), + (3, true, 0, 3), + // Hidden Layer 1: Expect 8 + (4, false, 1, 4), + (5, false, 1, 5), + (6, false, 1, 6), + (7, true, 1, 7), + (8, true, 1, 8), + (9, true, 1, 9), + (10, true, 1, 10), + (11, true, 1, 11), + // Hidden Layer 2: Expect 6 + (12, true, 2, 12), + (13, true, 2, 13), + (14, true, 2, 14), + (15, true, 2, 15), + (16, true, 2, 16), + (17, true, 2, 17), + // Output Layer: Expect 4 + (18, true, 3, 18), + (19, true, 3, 19), + (20, false, 3, 20), + (21, true, 3, 21), + ]; + let new_shape = vec![4, 8, 6, 4]; + let mut new_fann = Fann::new(&new_shape)?; + // Initialize weights to 0 + let mut new_connections = new_fann.get_connections(); + for connection in new_connections.iter_mut() { + connection.weight = 0.0; + } + new_fann.set_connections(&new_connections); + + // Act + consolidate_old_connections( + &primary_fann, + &secondary_fann, + new_shape, + new_neurons, + &mut new_fann, + ); + + // Assert that the fallback bias connection was used + let new_connections = new_fann.get_connections(); + + for connection in new_connections.iter() { + println!("{:?}", connection); + } + + let found = new_connections + .iter() + .any(|c| c.from_neuron == bias_neuron_primary && c.weight == 12345.0); + assert!( + found, + "Expected fallback bias connection from primary network to be used" + ); + Ok(()) } }