diff --git a/crates/solverforge-solver/src/heuristic/selector/list_ruin.rs b/crates/solverforge-solver/src/heuristic/selector/list_ruin.rs index 90f32f2f..22fc4853 100644 --- a/crates/solverforge-solver/src/heuristic/selector/list_ruin.rs +++ b/crates/solverforge-solver/src/heuristic/selector/list_ruin.rs @@ -224,16 +224,23 @@ where let max_ruin = self.max_ruin_count; let moves_count = self.moves_per_step; + let non_empty_entities: Vec<(usize, usize)> = (0..total_entities) + .filter_map(|entity_idx| { + let len = list_len(solution, entity_idx); + (len > 0).then_some((entity_idx, len)) + }) + .collect(); + // Pre-generate moves using RNG (empty if no entities) let mut rng = self.rng.borrow_mut(); - let moves: Vec> = if total_entities == 0 { + let moves: Vec> = if non_empty_entities.is_empty() { Vec::new() } else { (0..moves_count) .filter_map(|_| { // Pick a random entity - let entity_idx = rng.random_range(0..total_entities); - let list_length = list_len(solution, entity_idx); + let (entity_idx, list_length) = + non_empty_entities[rng.random_range(0..non_empty_entities.len())]; if list_length == 0 { return None; diff --git a/crates/solverforge-solver/src/heuristic/selector/tests/list_ruin.rs b/crates/solverforge-solver/src/heuristic/selector/tests/list_ruin.rs index 7e89365d..cdad08d1 100644 --- a/crates/solverforge-solver/src/heuristic/selector/tests/list_ruin.rs +++ b/crates/solverforge-solver/src/heuristic/selector/tests/list_ruin.rs @@ -135,7 +135,7 @@ fn empty_solution_yields_no_moves() { } #[test] -fn empty_list_yields_no_moves_for_that_entity() { +fn empty_lists_should_not_reduce_moves_per_step() { let director = create_director(vec![vec![], vec![1, 2, 3]]); let selector = ListRuinMoveSelector::::new( @@ -154,10 +154,17 @@ fn empty_list_yields_no_moves_for_that_entity() { let moves: Vec<_> = selector.iter_moves(&director).collect(); + assert_eq!( + moves.len(), + 10, + "empty routes should not consume moves_per_step attempts when non-empty routes exist" + ); + // Some moves may be None due to empty list selection // All returned moves should be valid for m in &moves { - assert!(m.ruin_count() >= 1); + assert_eq!(m.entity_index(), 1); + assert!((1..=2).contains(&m.ruin_count())); } }