c4_e5_chess/engine/
game.rs1use 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
17pub struct Game {
19 pub max_depth: Depth,
20 pub board: Board,
21 pub move_time: MoveTime, 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 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 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 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 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}