c4_e5_chess/engine/
game.rs

1use super::{constants::*, history::History, move_gen::MoveGenPrime, pvs::Pvs, store::Store};
2use crate::misc::types::*;
3use core::time::Duration;
4use cozy_chess::{Board, Move};
5use log::{error, info};
6use rayon::prelude::*;
7use std::{
8    cmp::max,
9    str::FromStr,
10    sync::{
11        atomic::{AtomicBool, Ordering},
12        Arc,
13    },
14    thread::{self, JoinHandle},
15};
16
17/// A chess game
18pub struct Game {
19    pub max_depth: Depth,
20    pub board: Board,
21    pub move_time: MoveTime, // in Milliseconds
22    pub move_number: MoveNumber,
23    playing: Arc<AtomicBool>,
24    pub node_count: u64,
25    game_store: Store,
26    pub game_history: History,
27}
28
29impl Game {
30    /// Create a game giving a position as a FEN, max depth and a move time.
31    pub fn new(fen: String, max_depth: Depth, move_time: MoveTime) -> Self {
32        match Board::from_str(if fen.is_empty() { FEN_START } else { &fen }) {
33            Ok(board) => Self {
34                max_depth: if max_depth == 0 {
35                    INIT_MAX_DEPTH
36                } else {
37                    max_depth
38                },
39                board,
40                playing: Arc::new(AtomicBool::new(true)),
41                move_time: if move_time == 0 {
42                    DEFAULT_TIME
43                } else {
44                    move_time
45                },
46                move_number: 0,
47                node_count: 0,
48                game_store: Store::new(),
49                game_history: History::new(),
50            },
51            Err(e) => {
52                error!("FEN not valid: {e}");
53                Self::default()
54            }
55        }
56    }
57
58    /// Set a timer to stop playing after the move time has elapsed.
59    pub fn set_timer(&mut self) -> JoinHandle<()> {
60        self.playing.store(true, Ordering::Relaxed);
61        let playing_clone = self.playing.clone();
62        let move_time = self.move_time;
63        thread::spawn(move || {
64            thread::sleep(Duration::from_millis(move_time));
65            playing_clone.store(false, Ordering::Relaxed);
66        })
67    }
68
69    /// Find the best move
70    pub fn find_move(&mut self) -> Option<Move> {
71        fn stabilise_search_results(
72            old: &[AnnotatedMove],
73            new: &[AnnotatedMove],
74        ) -> Vec<AnnotatedMove> {
75            let len = new.len();
76            let diff_mean: i32 = new
77                .iter()
78                .zip(old.iter())
79                .map(|(new_move, old_move)| old_move.sc - new_move.sc)
80                .sum::<i32>()
81                / len as i32;
82
83            new.iter()
84                .zip(old.iter())
85                .map(|(new_move, old_move)| {
86                    let mut adjusted_move = *new_move;
87                    adjusted_move.sc = (adjusted_move.sc + diff_mean).min(old_move.sc);
88                    adjusted_move
89                })
90                .collect()
91        }
92
93        fn update_node_count(prior_values: &[AnnotatedMove]) -> u64 {
94            let mut node_count = 0;
95            node_count += prior_values.iter().fold(
96                0,
97                |acc,
98                 AnnotatedMove {
99                     mv: _,
100                     sc: _,
101                     node_count: nc,
102                     ..
103                 }| acc + nc,
104            );
105            node_count
106        }
107
108        let alpha = MIN_INT;
109        let beta = MAX_INT;
110        let mut current_depth: Depth = 0;
111        let mut best_move: Option<Move> = None;
112        let mut best_value: MoveScore = MIN_INT;
113        let mut worst_value: MoveScore;
114        let mut prior_values = self.board.get_legal_sorted(None);
115        let mut prior_values_old: Vec<AnnotatedMove> = vec![];
116
117        self.set_timer();
118
119        if prior_values.len() == 1 {
120            return Some(prior_values[0].mv);
121        }
122
123        while current_depth <= self.max_depth {
124            prior_values.par_iter_mut().for_each(
125                |AnnotatedMove {
126                     mv,
127                     sc,
128                     cp,
129                     node_count,
130                 }| {
131                    let mut b1 = self.board.clone();
132                    let mut pvs = Pvs::new();
133                    pvs.store.h.clone_from(&self.game_store.h);
134                    pvs.history.h.clone_from(&self.game_history.h);
135                    b1.play_unchecked(*mv);
136                    pvs.history.inc(&b1);
137                    *sc = -pvs.execute(&b1, current_depth, -beta, -alpha, &self.playing, *cp);
138                    pvs.history.dec(&b1);
139                    *node_count = pvs.node_count;
140                },
141            );
142
143            if !self.playing.load(Ordering::Relaxed) {
144                info!("Time for this move has expired.");
145                self.node_count += update_node_count(&prior_values);
146                break;
147            }
148
149            if current_depth % 2 == 1 {
150                prior_values = stabilise_search_results(&prior_values_old, &prior_values);
151            }
152
153            prior_values.sort_by(|a, b| b.sc.cmp(&a.sc));
154
155            best_move = Some(prior_values[0].mv);
156            best_value = prior_values[0].sc;
157            if best_value > MATE_LEVEL {
158                info!(
159                    "Mate level was reached. Best move was {}",
160                    best_move.unwrap()
161                );
162                break;
163            }
164            self.node_count += update_node_count(&prior_values);
165            info!(
166                "Depth: {} Nodes examined: {}",
167                current_depth, self.node_count
168            );
169
170            info!(
171                "Moves before pruning: {}",
172                prior_values
173                    .iter()
174                    .map(|m| format!("{} (score: {})", m.mv, m.sc))
175                    .collect::<Vec<String>>()
176                    .join(", ")
177            );
178
179            // Forward pruning
180            if current_depth >= FORWARD_PRUNING_DEPTH_START {
181                let moves_count = prior_values.len();
182
183                worst_value = prior_values[moves_count - 1].sc;
184                if worst_value < best_value {
185                    let cut_index =
186                        max(FORWARD_PRUNING_MINIMUM, moves_count / FORWARD_PRUNING_RATIO);
187                    info!("cut at {cut_index}");
188                    prior_values.truncate(cut_index);
189                }
190            }
191
192            current_depth += 1;
193            prior_values_old = prior_values.clone();
194        }
195        self.game_store.put(
196            current_depth - 1,
197            best_value,
198            &self.board,
199            &best_move.unwrap(),
200        );
201
202        best_move
203    }
204}
205
206impl Default for Game {
207    fn default() -> Game {
208        Game::new(String::from(""), 0, 0)
209    }
210}