use super::*;
use crate::heuristic::r#move::ChangeMove;
use crate::heuristic::selector::EntityReference;
use solverforge_core::domain::{
EntityCollectionExtractor, EntityDescriptor, PlanningSolution, SolutionDescriptor,
};
use solverforge_core::score::SoftScore;
use solverforge_scoring::{Director, ScoreDirector};
use std::any::TypeId;
#[derive(Clone, Debug)]
struct Queen {
row: Option<i64>,
}
#[derive(Clone, Debug)]
struct NQueensSolution {
queens: Vec<Queen>,
score: Option<SoftScore>,
}
impl PlanningSolution for NQueensSolution {
type Score = SoftScore;
fn score(&self) -> Option<Self::Score> {
self.score
}
fn set_score(&mut self, score: Option<Self::Score>) {
self.score = score;
}
}
fn get_queens(s: &NQueensSolution) -> &Vec<Queen> {
&s.queens
}
fn get_queens_mut(s: &mut NQueensSolution) -> &mut Vec<Queen> {
&mut s.queens
}
fn get_queen_row(s: &NQueensSolution, idx: usize) -> Option<i64> {
s.queens.get(idx).and_then(|q| q.row)
}
fn set_queen_row(s: &mut NQueensSolution, idx: usize, v: Option<i64>) {
if let Some(queen) = s.queens.get_mut(idx) {
queen.row = v;
}
}
fn create_test_director() -> ScoreDirector<NQueensSolution, ()> {
let solution = NQueensSolution {
queens: vec![Queen { row: None }],
score: None,
};
let descriptor = create_descriptor();
ScoreDirector::simple(solution, descriptor, |s, _| s.queens.len())
}
fn create_descriptor() -> SolutionDescriptor {
let extractor = Box::new(EntityCollectionExtractor::new(
"Queen",
"queens",
get_queens,
get_queens_mut,
));
let entity_desc =
EntityDescriptor::new("Queen", TypeId::of::<Queen>(), "queens").with_extractor(extractor);
SolutionDescriptor::new("NQueensSolution", TypeId::of::<NQueensSolution>())
.with_entity(entity_desc)
}
#[derive(Clone, Debug)]
struct ScoredDirector {
working_solution: NQueensSolution,
descriptor: SolutionDescriptor,
unassigned_score: i64,
}
impl Director<NQueensSolution> for ScoredDirector {
fn working_solution(&self) -> &NQueensSolution {
&self.working_solution
}
fn working_solution_mut(&mut self) -> &mut NQueensSolution {
&mut self.working_solution
}
fn calculate_score(&mut self) -> SoftScore {
let score = SoftScore::of(
self.working_solution
.queens
.iter()
.map(|queen| queen.row.unwrap_or(self.unassigned_score))
.sum(),
);
self.working_solution.set_score(Some(score));
score
}
fn solution_descriptor(&self) -> &SolutionDescriptor {
&self.descriptor
}
fn clone_working_solution(&self) -> NQueensSolution {
self.working_solution.clone()
}
fn before_variable_changed(&mut self, _descriptor_index: usize, _entity_index: usize) {}
fn after_variable_changed(&mut self, _descriptor_index: usize, _entity_index: usize) {}
fn entity_count(&self, descriptor_index: usize) -> Option<usize> {
(descriptor_index == 0).then_some(self.working_solution.queens.len())
}
fn total_entity_count(&self) -> Option<usize> {
Some(self.working_solution.queens.len())
}
}
type TestMove = ChangeMove<NQueensSolution, i64>;
fn create_placement() -> Placement<NQueensSolution, TestMove> {
create_placement_with_values([1, 5, 3])
}
fn create_placement_with_values(
values: impl IntoIterator<Item = i64>,
) -> Placement<NQueensSolution, TestMove> {
let entity_ref = EntityReference::new(0, 0);
let moves: Vec<TestMove> = values
.into_iter()
.map(|value| ChangeMove::new(0, Some(value), get_queen_row, set_queen_row, "row", 0))
.collect();
Placement::new(entity_ref, moves)
}
fn create_scored_director(unassigned_score: i64) -> ScoredDirector {
ScoredDirector {
working_solution: NQueensSolution {
queens: vec![Queen { row: None }],
score: None,
},
descriptor: create_descriptor(),
unassigned_score,
}
}
#[test]
fn test_first_fit_forager() {
let mut director = create_test_director();
let mut placement = create_placement();
let forager = FirstFitForager::<NQueensSolution, TestMove>::new();
let selected = forager.pick_move_index(&placement, &mut director);
assert_eq!(selected, ConstructionChoice::Select(0));
if let ConstructionChoice::Select(idx) = selected {
let m = placement.moves.swap_remove(idx);
m.do_move(&mut director);
}
}
#[test]
fn test_best_fit_forager() {
let mut director = create_test_director();
let mut placement = create_placement();
let forager = BestFitForager::<NQueensSolution, TestMove>::new();
let selected = forager.pick_move_index(&placement, &mut director);
assert!(matches!(selected, ConstructionChoice::Select(_)));
if let ConstructionChoice::Select(idx) = selected {
let m = placement.moves.swap_remove(idx);
m.do_move(&mut director);
let score = director.calculate_score();
assert_eq!(score, SoftScore::of(0));
}
}
#[test]
fn test_empty_placement() {
let mut director = create_test_director();
let placement: Placement<NQueensSolution, TestMove> =
Placement::new(EntityReference::new(0, 0), vec![]);
let forager = FirstFitForager::<NQueensSolution, TestMove>::new();
let selected = forager.pick_move_index(&placement, &mut director);
assert_eq!(selected, ConstructionChoice::KeepCurrent);
}
fn value_strength(m: &TestMove) -> i64 {
m.to_value().copied().unwrap_or(0)
}
#[test]
fn test_weakest_fit_forager() {
let mut director = create_test_director();
let mut placement = create_placement();
let forager = WeakestFitForager::<NQueensSolution, TestMove>::new(value_strength);
let selected = forager.pick_move_index(&placement, &mut director);
assert_eq!(selected, ConstructionChoice::Select(0));
if let ConstructionChoice::Select(idx) = selected {
let m = placement.moves.swap_remove(idx);
m.do_move(&mut director);
let score = director.calculate_score();
assert_eq!(score, SoftScore::of(0));
}
}
#[test]
fn test_strongest_fit_forager() {
let mut director = create_test_director();
let mut placement = create_placement();
let forager = StrongestFitForager::<NQueensSolution, TestMove>::new(value_strength);
let selected = forager.pick_move_index(&placement, &mut director);
assert_eq!(selected, ConstructionChoice::Select(1));
if let ConstructionChoice::Select(idx) = selected {
let m = placement.moves.swap_remove(idx);
m.do_move(&mut director);
let score = director.calculate_score();
assert_eq!(score, SoftScore::of(0));
}
}
#[test]
fn first_fit_keeps_current_when_optional_baseline_is_not_beaten() {
let mut director = create_scored_director(0);
let placement = create_placement_with_values([-5, -1]).with_keep_current_legal(true);
let forager = FirstFitForager::<NQueensSolution, TestMove>::new();
assert_eq!(
forager.pick_move_index(&placement, &mut director),
ConstructionChoice::KeepCurrent
);
}
#[test]
fn first_fit_selects_later_improving_candidate_when_earlier_one_is_worse() {
let mut director = create_scored_director(0);
let placement = create_placement_with_values([-5, 7, -1]).with_keep_current_legal(true);
let forager = FirstFitForager::<NQueensSolution, TestMove>::new();
assert_eq!(
forager.pick_move_index(&placement, &mut director),
ConstructionChoice::Select(1)
);
}
#[test]
fn first_fit_selects_first_improving_candidate_over_optional_baseline() {
let mut director = create_scored_director(0);
let placement = create_placement_with_values([7, -5, 3]).with_keep_current_legal(true);
let forager = FirstFitForager::<NQueensSolution, TestMove>::new();
assert_eq!(
forager.pick_move_index(&placement, &mut director),
ConstructionChoice::Select(0)
);
}
#[test]
fn best_fit_prefers_first_equal_score_candidate_over_keep_current() {
let mut director = create_test_director();
let placement = create_placement().with_keep_current_legal(true);
let forager = BestFitForager::<NQueensSolution, TestMove>::new();
assert_eq!(
forager.pick_move_index(&placement, &mut director),
ConstructionChoice::Select(0)
);
}
#[test]
fn first_feasible_returns_keep_current_when_baseline_is_feasible() {
let mut director = create_test_director();
let placement = create_placement().with_keep_current_legal(true);
let forager = FirstFeasibleForager::<NQueensSolution, TestMove>::new();
assert_eq!(
forager.pick_move_index(&placement, &mut director),
ConstructionChoice::KeepCurrent
);
}
#[test]
fn best_fit_keeps_current_when_all_candidates_are_worse_than_baseline() {
let mut director = create_scored_director(0);
let placement = create_placement_with_values([-5, -1]).with_keep_current_legal(true);
let forager = BestFitForager::<NQueensSolution, TestMove>::new();
assert_eq!(
forager.pick_move_index(&placement, &mut director),
ConstructionChoice::KeepCurrent
);
}
#[test]
fn best_fit_selects_best_candidate_when_it_beats_baseline() {
let mut director = create_scored_director(0);
let placement = create_placement_with_values([-5, 7, 3]).with_keep_current_legal(true);
let forager = BestFitForager::<NQueensSolution, TestMove>::new();
assert_eq!(
forager.pick_move_index(&placement, &mut director),
ConstructionChoice::Select(1)
);
}
#[test]
fn first_feasible_selects_first_feasible_candidate_when_baseline_is_infeasible() {
let mut director = create_scored_director(-2);
let placement = create_placement_with_values([-3, 1, 5]).with_keep_current_legal(true);
let forager = FirstFeasibleForager::<NQueensSolution, TestMove>::new();
assert_eq!(
forager.pick_move_index(&placement, &mut director),
ConstructionChoice::Select(1)
);
}
#[test]
fn first_feasible_prefers_equal_score_candidate_over_infeasible_baseline() {
let mut director = create_scored_director(-1);
let placement = create_placement_with_values([-1, -2]).with_keep_current_legal(true);
let forager = FirstFeasibleForager::<NQueensSolution, TestMove>::new();
assert_eq!(
forager.pick_move_index(&placement, &mut director),
ConstructionChoice::Select(0)
);
}