hivetuilib_ai/
algorithm.rs

1use std::{cmp::Ordering, convert::TryFrom, fmt::Debug, marker::PhantomData, ops::ControlFlow};
2
3use hivetuilib::{
4    engine::{logging::EventLog, CloneError, Engine, EventListener},
5    GameData, RevEffect,
6};
7
8use crate::{
9    engine_stepper::EngineStepper,
10    rater::{DecisionType, Rater},
11    search_tree_state::SearchTreeState,
12    IndexType, Params, RatingType, Sliding, INTERNAL_ERROR,
13};
14
15type MoveRating<T> = (RatingType, Box<[usize]>, <T as GameData>::Context);
16
17pub trait RateAndMap<T: GameData> {
18    fn apply_type_mapping(&self, context: &T::Context) -> DecisionType;
19
20    fn rate_moves(
21        &self,
22        rater: &mut Rater,
23        context: &[T::Context],
24        data: &T,
25        old_context: &[(T::Context, usize)],
26    );
27
28    // TODO: player probably unnecessary
29    fn rate_game_state(
30        &self,
31        data: &T,
32        old_context: &[(T::Context, usize)],
33        player: usize,
34    ) -> RatingType;
35}
36
37// TODO: lift last part (i.e. only decide on subdecision)?
38/// To apply the min-max algorithm, the engine must be in pending decision state
39/// and the decision must be a top-level decision.
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub enum InvalidEngineState {
42    PendingEffect,
43    FollowUp,
44    Finished,
45}
46
47impl From<CloneError> for InvalidEngineState {
48    fn from(e: CloneError) -> Self {
49        match e {
50            CloneError::PendingEffect => InvalidEngineState::PendingEffect,
51            CloneError::FollowUp => InvalidEngineState::FollowUp,
52        }
53    }
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum MinMaxError {
58    PendingEffect,
59    FollowUp,
60    Finished,
61    Cancelled,
62}
63
64impl MinMaxError {
65    pub fn into_engine_state_error(self) -> Option<InvalidEngineState> {
66        match self {
67            MinMaxError::PendingEffect => Some(InvalidEngineState::PendingEffect),
68            MinMaxError::FollowUp => Some(InvalidEngineState::FollowUp),
69            MinMaxError::Finished => Some(InvalidEngineState::Finished),
70            MinMaxError::Cancelled => None,
71        }
72    }
73}
74
75impl From<CloneError> for MinMaxError {
76    fn from(e: CloneError) -> Self {
77        match e {
78            CloneError::PendingEffect => MinMaxError::PendingEffect,
79            CloneError::FollowUp => MinMaxError::FollowUp,
80        }
81    }
82}
83
84pub fn add_context_to_ratings<T, L>(
85    engine: &Engine<T, L>,
86    ratings: Vec<(RatingType, Box<[usize]>)>,
87) -> Result<Vec<MoveRating<T>>, InvalidEngineState>
88where
89    T: GameData + Clone + Debug,
90    L: EventListener<T>,
91    T::EffectType: RevEffect<T>,
92{
93    if engine.is_finished() {
94        return Err(InvalidEngineState::Finished);
95    }
96    let mut engine = engine.try_clone_with_listener(EventLog::new())?;
97    let mut stepper = EngineStepper::new(&mut engine);
98
99    let result = ratings
100        .into_iter()
101        .map(|(val, path)| {
102            stepper.forward_step(&path);
103            let (ctx, _) = stepper.backward_step();
104            (val, path, ctx)
105        })
106        .collect::<Vec<_>>();
107    Ok(result)
108}
109
110pub struct PruningInput {
111    pub total_depth: usize,
112    pub current_depth: usize,
113    pub num_branches: usize,
114}
115
116pub enum PruningKind {
117    KeepAll,
118    KeepN(usize),
119    KeepByDiff(RatingType),
120    WithinBounds(usize, usize, RatingType),
121}
122
123pub struct MinMaxAlgorithm<T: GameData + Debug, R: RateAndMap<T>>
124where
125    T::EffectType: RevEffect<T>,
126{
127    params: Params,
128    rate_and_map: R,
129    pruning_fn: Box<dyn Fn(PruningInput) -> PruningKind>,
130    _t: PhantomData<T>,
131}
132
133type RatingList = Vec<(RatingType, Box<[IndexType]>)>;
134
135impl<T: GameData + Debug, R: RateAndMap<T>> MinMaxAlgorithm<T, R>
136where
137    T::EffectType: RevEffect<T>,
138{
139    pub fn new(params: Params, rate_and_map: R) -> MinMaxAlgorithm<T, R> {
140        Self::with_pruning(params, rate_and_map, |_| PruningKind::KeepAll)
141    }
142
143    pub fn with_pruning<P>(params: Params, rate_and_map: R, pruning_fn: P) -> MinMaxAlgorithm<T, R>
144    where
145        P: Fn(PruningInput) -> PruningKind + 'static,
146    {
147        MinMaxAlgorithm {
148            params,
149            rate_and_map,
150            pruning_fn: Box::new(pruning_fn),
151            _t: PhantomData,
152        }
153    }
154
155    pub fn rate_and_map(&self) -> &R {
156        &self.rate_and_map
157    }
158
159    pub fn apply<L>(&self, engine: &mut Engine<T, L>)
160    where
161        T: Clone,
162        L: EventListener<T>,
163    {
164        let (_, index_list, _) = self.run(engine).expect("Invalid engine state!");
165        for &i in index_list.iter() {
166            match engine.pull() {
167                hivetuilib::engine::GameState::PendingDecision(dec) => dec.select_option(i),
168                _ => panic!("{}", INTERNAL_ERROR),
169            }
170        }
171    }
172
173    pub fn run<L>(&self, engine: &Engine<T, L>) -> Result<MoveRating<T>, InvalidEngineState>
174    where
175        T: Clone,
176        L: EventListener<T>,
177    {
178        self.run_with_cancellation(engine, || false)
179            .map_err(|e| e.into_engine_state_error().unwrap())
180    }
181
182    pub fn run_with_cancellation<L, F>(
183        &self,
184        engine: &Engine<T, L>,
185        should_cancel: F,
186    ) -> Result<MoveRating<T>, MinMaxError>
187    where
188        T: GameData + Clone,
189        L: EventListener<T>,
190        F: Fn() -> bool,
191    {
192        let ratings = self.run_all_ratings_with_cancellation(engine, should_cancel)?;
193        let result = ratings
194            .into_iter()
195            .max_by_key(|&(r, _, _)| r)
196            .expect(INTERNAL_ERROR);
197
198        // return result
199        Ok(result)
200    }
201
202    pub fn run_all_ratings<L>(
203        &self,
204        engine: &Engine<T, L>,
205    ) -> Result<Vec<MoveRating<T>>, InvalidEngineState>
206    where
207        T: GameData + Clone,
208        L: EventListener<T>,
209    {
210        self.run_all_ratings_with_cancellation(engine, || false)
211            .map_err(|e| e.into_engine_state_error().unwrap())
212    }
213
214    pub fn run_all_ratings_with_cancellation<L, F>(
215        &self,
216        engine: &Engine<T, L>,
217        should_cancel: F,
218    ) -> Result<Vec<MoveRating<T>>, MinMaxError>
219    where
220        T: GameData + Clone,
221        L: EventListener<T>,
222        F: Fn() -> bool,
223    {
224        if engine.is_finished() {
225            return Err(MinMaxError::Finished);
226        }
227        // let now = Instant::now();
228        let mut engine = engine.try_clone_with_listener(EventLog::new())?;
229
230        self.params.integrity_check();
231        let mut stepper = EngineStepper::new(&mut engine);
232        let player = stepper.player();
233
234        // calculate move
235        let mut tree = SearchTreeState::new();
236        let num_runs = self
237            .params
238            .depth
239            .saturating_sub(self.params.first_cut_delay_depth)
240            + 1;
241        for _ in 0..num_runs {
242            self.prune(&mut tree);
243            self.extend_search_tree(&mut stepper, &mut tree, player, &should_cancel)?;
244            if tree.root_moves().count() == 1 {
245                break;
246            }
247        }
248        Ok(tree
249            .root_moves()
250            .map(|(val, path)| {
251                (
252                    val,
253                    path.iter().map(|&i| usize::try_from(i).unwrap()).collect(),
254                    {
255                        stepper.forward_step(path);
256                        let (ctx, _) = stepper.backward_step();
257                        ctx
258                    },
259                )
260            })
261            .collect::<Vec<_>>())
262    }
263
264    fn extend_search_tree<F: Fn() -> bool>(
265        &self,
266        stepper: &mut EngineStepper<T>,
267        tree: &mut SearchTreeState,
268        player: usize,
269        should_cancel: F,
270    ) -> Result<(), MinMaxError> {
271        assert!(tree.depth() < 2 * self.params.depth);
272        let depth = {
273            let mut depth = self.params.first_cut_delay_depth;
274            if tree.depth() == 0 {
275                depth += self.params.first_move_added_delay_depth;
276            }
277            2 * usize::min(depth, self.params.depth)
278        };
279        let sliding = self.params.sliding.get(tree.depth()..);
280        tree.new_levels();
281        tree.for_each_leaf(stepper, |tree, stepper, t_index| {
282            assert!(
283                stepper.is_finished() || stepper.player() == player,
284                "Min-max algorithm requires alternating turns!"
285            );
286            let new_moves = self.collect_recursive(
287                depth,
288                stepper,
289                player,
290                self.params.first_cut_delay_depth,
291                sliding,
292            );
293            for (rating, index, children) in new_moves {
294                tree.push_child(t_index, rating, index, children);
295            }
296            if should_cancel() {
297                ControlFlow::Break(MinMaxError::Cancelled)
298            } else {
299                ControlFlow::Continue(())
300            }
301        })?;
302        // TODO: end of game handling
303        tree.extend();
304        tree.update_ratings();
305        Ok(())
306    }
307
308    fn collect_recursive(
309        &self,
310        depth: usize,
311        stepper: &mut EngineStepper<T>,
312        player: usize,
313        delay_depth: usize,
314        sliding: Sliding,
315    ) -> Vec<(RatingType, Box<[IndexType]>, RatingList)> {
316        if depth == 0 || stepper.is_finished() {
317            return Vec::new();
318        }
319
320        // collect moves and calculate min-max ratings
321        let is_own_turn = stepper.player() == player;
322        let moves = self.create_move_ratings(
323            stepper,
324            sliding.move_cut_difference(),
325            sliding.move_limit(),
326            Rater::cut_and_sort_with_equivalency,
327        );
328        let mut moves = moves
329            .into_iter()
330            .map(|(_, indizes, eq)| {
331                stepper.forward_step(&indizes);
332                let (rating, children) =
333                    self.collect_and_cut(depth - 1, stepper, player, delay_depth, sliding.next());
334                stepper.backward_step();
335                (rating, indizes, eq, children)
336            })
337            .collect::<Vec<_>>();
338
339        // cut the moves to the defined limit
340        if depth >= 2 * delay_depth {
341            self.cut_moves(&mut moves, sliding, |&(r, _, _, _)| r, is_own_turn);
342        }
343
344        // resolve equivalency classes
345        let mut moves = self.resolve_equivalencies(
346            moves,
347            stepper,
348            self.params.equivalency_penalty,
349            sliding,
350            |x| x,
351            |stepper| self.collect_and_cut(depth - 1, stepper, player, delay_depth, sliding.next()),
352            is_own_turn,
353        );
354
355        // moves might exceed the limit again due to resolved equivalency classes
356        if depth >= 2 * delay_depth && moves.len() > sliding.branch_cut_limit() {
357            self.cut_moves(&mut moves, sliding, |&(r, _, _)| r, is_own_turn);
358        }
359        moves
360    }
361
362    fn collect_and_cut(
363        &self,
364        depth: usize,
365        stepper: &mut EngineStepper<T>,
366        player: usize,
367        delay_depth: usize,
368        sliding: Sliding,
369    ) -> (RatingType, RatingList) {
370        if depth == 0 || stepper.is_finished() {
371            return (self.final_rating(depth, stepper, player), Vec::new());
372        }
373
374        // collect moves and calculate min-max ratings
375        let is_own_turn = stepper.player() == player;
376        let mut moves = self.create_move_ratings(
377            stepper,
378            sliding.move_cut_difference(),
379            sliding.move_limit(),
380            Rater::cut_and_sort_with_equivalency,
381        );
382        for (rating, indizes, _) in moves.iter_mut() {
383            stepper.forward_step(indizes);
384            *rating = self.min_max(depth - 1, stepper, player, sliding.next());
385            stepper.backward_step();
386        }
387
388        // cut the moves to the defined limit
389        if depth >= (2 * delay_depth - 1) {
390            self.cut_moves(&mut moves, sliding, |&(r, _, _)| r, is_own_turn);
391        }
392
393        // resolve equivalency classes
394        let mut moves = self
395            .resolve_equivalencies(
396                moves,
397                stepper,
398                self.params.equivalency_penalty,
399                sliding,
400                |(rating, index, equivalent_moves)| (rating, index, equivalent_moves, ()),
401                |stepper| (self.min_max(depth - 1, stepper, player, sliding.next()), ()),
402                is_own_turn,
403            )
404            .into_iter()
405            .map(|(rating, idz, _)| (rating, idz))
406            .collect::<Vec<_>>();
407
408        // moves might exceed the limit again due to resolved equivalency classes
409        if depth >= (2 * delay_depth - 1) && moves.len() > sliding.branch_cut_limit() {
410            self.cut_moves(&mut moves, sliding, |&(r, _)| r, is_own_turn);
411        }
412
413        // calculate rating of best move
414        let min = moves
415            .iter()
416            .min_by(|&&(r1, _), &&(r2, _)| Self::compare(r1, r2, is_own_turn));
417        (min.unwrap().0, moves)
418    }
419
420    fn min_max(
421        &self,
422        depth: usize,
423        stepper: &mut EngineStepper<T>,
424        player: usize,
425        sliding: Sliding,
426    ) -> RatingType {
427        if depth == 0 || stepper.is_finished() {
428            return self.final_rating(depth, stepper, player);
429        }
430
431        let is_own_turn = stepper.player() == player;
432        let moves = self.create_move_ratings(
433            stepper,
434            sliding.move_cut_difference(),
435            sliding.move_limit(),
436            Rater::cut_and_sort,
437        );
438        let ratings = moves.into_iter().map(|(_, indizes)| {
439            stepper.forward_step(&indizes);
440            let result = self.min_max(depth - 1, stepper, player, sliding.next());
441            stepper.backward_step();
442            result
443        });
444        if is_own_turn {
445            ratings.max().expect(INTERNAL_ERROR)
446        } else {
447            ratings.min().expect(INTERNAL_ERROR)
448        }
449    }
450
451    fn final_rating(
452        &self,
453        depth: usize,
454        stepper: &mut EngineStepper<T>,
455        player: usize,
456    ) -> RatingType {
457        let val =
458            self.rate_and_map
459                .rate_game_state(stepper.data(), stepper.decision_context(), player);
460        if stepper.is_finished() {
461            RatingType::try_from(depth + 1).unwrap() * val
462        } else {
463            val
464        }
465    }
466
467    fn cut_moves<E, F>(
468        &self,
469        moves: &mut Vec<E>,
470        sliding: Sliding,
471        rating_key_fn: F,
472        is_own_turn: bool,
473    ) where
474        F: Fn(&E) -> RatingType,
475    {
476        moves.sort_unstable_by(|l, r| {
477            Self::compare(rating_key_fn(l), rating_key_fn(r), is_own_turn)
478        });
479        let min = rating_key_fn(moves.first().unwrap());
480        moves.retain(|entry| {
481            RatingType::abs(rating_key_fn(entry) - min) <= sliding.branch_cut_difference()
482        });
483        // TODO: Clustering
484        moves.truncate(sliding.branch_cut_limit());
485    }
486
487    fn resolve_equivalencies<E, O, FnR, FnC>(
488        &self,
489        moves: Vec<E>,
490        stepper: &mut EngineStepper<T>,
491        equiv_penalty: RatingType,
492        sliding: Sliding,
493        resolve_el: FnR,
494        recursive_call: FnC,
495        is_own_turn: bool,
496    ) -> Vec<(RatingType, Box<[IndexType]>, O)>
497    where
498        FnR: Fn(E) -> (RatingType, Box<[IndexType]>, Vec<Box<[IndexType]>>, O),
499        FnC: Fn(&mut EngineStepper<T>) -> (RatingType, O),
500    {
501        let choices = sliding.equivalency_class_choices();
502        let mut buffer = Vec::new();
503        let mut result = Vec::new();
504        for element in moves {
505            buffer.clear();
506            let (rating, indizes, equivalent_moves, other) = resolve_el(element);
507            buffer.push((rating, indizes, other));
508            for curr_idz in equivalent_moves
509                .into_iter()
510                .take(sliding.equivalency_class_limit())
511            {
512                stepper.forward_step(&curr_idz);
513                let (curr_rating, curr_other) = recursive_call(stepper);
514                buffer.push((curr_rating, curr_idz, curr_other));
515                stepper.backward_step();
516            }
517
518            // keep only few of the equivalent moves and also add an additional penalty (only applies to first branch)
519            buffer.sort_unstable_by(|&(l, _, _), &(r, _, _)| Self::compare(l, r, is_own_turn));
520            for (i, (rating, indizes, other)) in buffer.drain(..).enumerate().take(choices) {
521                let sign = if is_own_turn { -1 } else { 1 };
522                result.push((
523                    rating + sign * i as RatingType * equiv_penalty,
524                    indizes,
525                    other,
526                ))
527            }
528        }
529        result
530    }
531
532    fn compare(l: i32, r: i32, is_own_turn: bool) -> Ordering {
533        if is_own_turn {
534            r.cmp(&l)
535        } else {
536            l.cmp(&r)
537        }
538    }
539
540    fn prune(&self, tree: &mut SearchTreeState) {
541        let depth = tree.depth();
542        let mut buffer = Vec::new();
543        tree.prune(|level, moves, mut retained| {
544            let sign = if level % 2 == 0 { 1 } else { -1 };
545            let mut within_bounds = |min, max, diff| {
546                assert!(diff >= 0);
547                buffer.clear();
548                buffer.extend(moves.iter().enumerate().map(|(i, t)| (i, sign * t.rating)));
549                buffer.sort_unstable_by_key(|&(_, r)| -r); // high ratings first
550                assert!(buffer.len() > 1 && buffer[0].1 >= buffer[1].1);
551                let (_, best) = buffer[0];
552                let num_retained = {
553                    let mut index = min;
554                    while index < buffer.len() && index < max {
555                        let (_, r) = buffer[index];
556                        if r + diff < best {
557                            break;
558                        }
559                        index += 1;
560                    }
561                    index
562                };
563                buffer.truncate(num_retained);
564                buffer.sort_unstable_by_key(|&(i, _)| i);
565                for &(i, _) in buffer.iter() {
566                    retained.add(i);
567                }
568            };
569            assert!(level < depth);
570            match (self.pruning_fn)(PruningInput {
571                total_depth: depth,
572                current_depth: level,
573                num_branches: moves.len(),
574            }) {
575                PruningKind::KeepAll => (0..moves.len()).for_each(|i| retained.add(i)),
576                PruningKind::KeepN(num) => within_bounds(num, num, 0),
577                PruningKind::KeepByDiff(diff) => within_bounds(0, moves.len(), diff),
578                PruningKind::WithinBounds(min, max, diff) => within_bounds(min, max, diff),
579            }
580        });
581    }
582
583    #[inline]
584    fn create_move_ratings<E: Ord + Debug>(
585        &self,
586        stepper: &mut EngineStepper<T>,
587        move_cut_difference: RatingType,
588        move_limit: usize,
589        rater_fn: fn(Rater, RatingType) -> Vec<E>,
590    ) -> Vec<E> {
591        let (mut rater, context) = Rater::new(stepper.engine(), |context| {
592            self.rate_and_map.apply_type_mapping(context)
593        });
594        self.rate_and_map.rate_moves(
595            &mut rater,
596            &context,
597            stepper.data(),
598            stepper.decision_context(),
599        );
600        let min = rater.current_max() - move_cut_difference;
601        let mut result = rater_fn(rater, min);
602        assert!(!result.is_empty());
603        if result.len() > move_limit {
604            // TODO: Clustering
605            result.truncate(move_limit);
606        }
607        result
608    }
609}
610
611#[cfg(test)]
612mod test {
613    use hivetuilib::engine::{Engine, GameState};
614
615    use crate::{
616        engine_stepper::EngineStepper,
617        test::{RateAndMapZeroOne, ZeroOneContext, ZeroOneGame},
618        IndexType, MinMaxAlgorithm, Params, SlidingParams,
619    };
620
621    fn indizes(input: &[IndexType]) -> Box<[IndexType]> {
622        Box::from(input)
623    }
624
625    #[test]
626    fn min_max_test() {
627        let sliding = SlidingParams::with_defaults(4, 2, 4, 4, 2, 2, 4, 1);
628        let params = Params::new(4, sliding.clone(), 1);
629        let alg = MinMaxAlgorithm::new(params, RateAndMapZeroOne);
630        let data = ZeroOneGame::new(false, 6);
631        let mut engine = Engine::new_logging(2, data);
632        let mut stepper = EngineStepper::new(&mut engine);
633
634        assert_eq!(alg.min_max(0, &mut stepper, 0, sliding.get(1..)), 0);
635        assert_eq!(alg.min_max(0, &mut stepper, 1, sliding.get(1..)), 0);
636        assert_eq!(alg.min_max(1, &mut stepper, 0, sliding.get(1..)), 1);
637        assert_eq!(alg.min_max(1, &mut stepper, 1, sliding.get(1..)), -1);
638        assert_eq!(alg.min_max(2, &mut stepper, 0, sliding.get(1..)), -1);
639        assert_eq!(alg.min_max(2, &mut stepper, 1, sliding.get(1..)), 1);
640        assert_eq!(alg.min_max(3, &mut stepper, 0, sliding.get(1..)), 0);
641        assert_eq!(alg.min_max(3, &mut stepper, 1, sliding.get(1..)), 0);
642        assert_eq!(alg.min_max(4, &mut stepper, 0, sliding.get(1..)), -4);
643        assert_eq!(alg.min_max(4, &mut stepper, 1, sliding.get(1..)), 4);
644        assert_eq!(alg.min_max(6, &mut stepper, 0, sliding.get(1..)), -12);
645        assert_eq!(alg.min_max(6, &mut stepper, 1, sliding.get(1..)), 12);
646    }
647
648    #[test]
649    fn collect_and_cut_test() {
650        let sliding = SlidingParams::with_defaults(4, 2, 4, 4, 2, 2, 4, 1);
651        let params = Params::new(4, sliding.clone(), 1);
652        let alg = MinMaxAlgorithm::new(params, RateAndMapZeroOne);
653        let data = ZeroOneGame::new(false, 6);
654        let mut engine = Engine::new_logging(2, data);
655        let mut stepper = EngineStepper::new(&mut engine);
656
657        assert_eq!(
658            alg.collect_and_cut(0, &mut stepper, 0, 2, sliding.get(1..)),
659            (0, Vec::new())
660        );
661        assert_eq!(
662            alg.collect_and_cut(0, &mut stepper, 1, 2, sliding.get(1..)),
663            (0, Vec::new())
664        );
665        assert_eq!(
666            alg.collect_and_cut(1, &mut stepper, 0, 2, sliding.get(1..)),
667            (1, vec![(1, indizes(&[0])), (-1, indizes(&[1]))])
668        );
669        assert_eq!(
670            alg.collect_and_cut(1, &mut stepper, 1, 2, sliding.get(1..)),
671            (-1, vec![(-1, indizes(&[0])), (1, indizes(&[1]))])
672        );
673        assert_eq!(
674            alg.collect_and_cut(2, &mut stepper, 0, 2, sliding.get(1..)),
675            (-1, vec![(-1, indizes(&[0])), (-9, indizes(&[1]))])
676        );
677        assert_eq!(
678            alg.collect_and_cut(2, &mut stepper, 1, 2, sliding.get(1..)),
679            (1, vec![(1, indizes(&[0])), (9, indizes(&[1]))])
680        );
681        assert_eq!(
682            alg.collect_and_cut(2, &mut stepper, 0, 1, sliding.get(1..)),
683            (-1, vec![(-1, indizes(&[0]))])
684        );
685        assert_eq!(
686            alg.collect_and_cut(2, &mut stepper, 1, 1, sliding.get(1..)),
687            (1, vec![(1, indizes(&[0]))])
688        );
689    }
690
691    #[test]
692    fn collect_recursive_test() {
693        let sliding = SlidingParams::with_defaults(4, 2, 4, 4, 2, 2, 4, 1);
694        let params = Params::new(4, sliding.clone(), 1);
695        let alg = MinMaxAlgorithm::new(params, RateAndMapZeroOne);
696        let data = ZeroOneGame::new(false, 6);
697        let mut engine = Engine::new_logging(2, data);
698        let mut stepper = EngineStepper::new(&mut engine);
699
700        assert_eq!(
701            alg.collect_recursive(0, &mut stepper, 0, 2, sliding.get(1..)),
702            Vec::new()
703        );
704        assert_eq!(
705            alg.collect_recursive(0, &mut stepper, 1, 2, sliding.get(1..)),
706            Vec::new()
707        );
708        assert_eq!(
709            alg.collect_recursive(1, &mut stepper, 0, 2, sliding.get(1..)),
710            vec![
711                (1, indizes(&[0]), Vec::new()),
712                (-1, indizes(&[1]), Vec::new())
713            ]
714        );
715        assert_eq!(
716            alg.collect_recursive(1, &mut stepper, 1, 2, sliding.get(1..)),
717            vec![
718                (-1, indizes(&[0]), Vec::new()),
719                (1, indizes(&[1]), Vec::new())
720            ]
721        );
722        assert_eq!(
723            alg.collect_recursive(2, &mut stepper, 0, 2, sliding.get(1..)),
724            vec![
725                (
726                    -1,
727                    indizes(&[0]),
728                    vec![
729                        (-1, indizes(&[1, 1])),
730                        (1, indizes(&[0, 1])),
731                        (1, indizes(&[1, 0]))
732                    ]
733                ),
734                (
735                    -9,
736                    indizes(&[1]),
737                    vec![
738                        (-9, indizes(&[1, 1])),
739                        (-1, indizes(&[0, 1])),
740                        (-1, indizes(&[1, 0]))
741                    ]
742                )
743            ]
744        );
745        assert_eq!(
746            alg.collect_recursive(2, &mut stepper, 1, 2, sliding.get(1..)),
747            vec![
748                (
749                    1,
750                    indizes(&[0]),
751                    vec![
752                        (1, indizes(&[1, 1])),
753                        (-1, indizes(&[0, 1])),
754                        (-1, indizes(&[1, 0]))
755                    ]
756                ),
757                (
758                    9,
759                    indizes(&[1]),
760                    vec![
761                        (9, indizes(&[1, 1])),
762                        (1, indizes(&[0, 1])),
763                        (1, indizes(&[1, 0]))
764                    ]
765                )
766            ]
767        );
768        assert_eq!(
769            alg.collect_recursive(2, &mut stepper, 0, 1, sliding.get(1..)),
770            vec![(
771                -1,
772                indizes(&[0]),
773                vec![
774                    (-1, indizes(&[1, 1])),
775                    (1, indizes(&[0, 1])),
776                    (1, indizes(&[1, 0]))
777                ]
778            )]
779        );
780        assert_eq!(
781            alg.collect_recursive(2, &mut stepper, 1, 1, sliding.get(1..)),
782            vec![(
783                1,
784                indizes(&[0]),
785                vec![
786                    (1, indizes(&[1, 1])),
787                    (-1, indizes(&[0, 1])),
788                    (-1, indizes(&[1, 0]))
789                ]
790            )]
791        );
792    }
793
794    #[test]
795    fn run_test() {
796        let sliding = SlidingParams::with_defaults(1, 2, 4, 4, 2, 2, 4, 1);
797        let params = Params::new(1, sliding.clone(), 1);
798        let mut alg = MinMaxAlgorithm::new(params, RateAndMapZeroOne);
799        alg.params.first_cut_delay_depth = 1;
800        let data = ZeroOneGame::new(true, 8);
801        let mut engine = Engine::new_logging(2, data);
802        assert_eq!(
803            alg.run(&engine),
804            Ok((1, Box::from([0, 0]), ZeroOneContext::ZeroAnd))
805        );
806
807        let sliding = SlidingParams::with_defaults(2, 1, 4, 4, 4, 2, 4, 1);
808        let params = Params::new(2, sliding.clone(), 1);
809        let mut alg = MinMaxAlgorithm::new(params, RateAndMapZeroOne);
810        alg.params.first_cut_delay_depth = 1;
811        assert_eq!(
812            alg.run(&engine),
813            Ok((4, Box::from([0, 0]), ZeroOneContext::ZeroAnd))
814        );
815
816        match engine.pull() {
817            GameState::PendingDecision(dec) => dec.select_option(0),
818            _ => unreachable!(),
819        }
820        match engine.pull() {
821            GameState::PendingDecision(dec) => dec.select_option(1),
822            _ => unreachable!(),
823        }
824        match engine.pull() {
825            GameState::PendingEffect(eff) => eff.all_effects(),
826            _ => unreachable!(),
827        }
828        assert_eq!(
829            alg.run(&engine),
830            Ok((-4, Box::from([1]), ZeroOneContext::Flat))
831        );
832    }
833}