chess/
board.rs

1use core::fmt;
2use std::rc::Rc;
3
4use ahash;
5use log;
6
7use crate::engine;
8use crate::errors::BoardStateError;
9use crate::errors::PGNParseError;
10use crate::fen::FEN;
11use crate::log_and_return_error;
12use crate::movegen::*;
13use crate::pgn;
14use crate::pgn::notation::Notation;
15use crate::pgn::tag::Tag;
16use crate::position::*;
17use crate::transposition;
18use crate::util;
19use crate::zobrist;
20use crate::zobrist::PositionHash;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum GameState {
24    Check,
25    Checkmate,
26    Stalemate,
27    Repetition,
28    FiftyMove,
29    InsufficientMaterial,
30    Active,
31}
32impl GameState {
33    // gamestates that are draws
34    #[inline]
35    pub fn is_draw(&self) -> bool {
36        matches!(
37            self,
38            Self::Stalemate | Self::FiftyMove | Self::Repetition | Self::InsufficientMaterial
39        )
40    }
41    // gamestates that are wins
42    #[inline]
43    pub fn is_win(&self) -> bool {
44        matches!(self, Self::Checkmate)
45    }
46    // gamestates that end game
47    #[inline]
48    pub fn is_game_over(&self) -> bool {
49        self.is_win() || self.is_draw()
50    }
51}
52// String representation of GameState
53impl fmt::Display for GameState {
54    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
55        let state_str = match self {
56            Self::Check => "Check",
57            Self::Checkmate => "Checkmate",
58            Self::Stalemate => "Stalemate",
59            Self::Repetition => "Repetition",
60            Self::FiftyMove => "Fifty Move Draw",
61            Self::InsufficientMaterial => "Insufficient Material",
62            Self::Active => "",
63        };
64        write!(f, "{}", state_str)
65    }
66}
67
68#[derive(Debug, Clone)]
69pub struct BoardState {
70    pub side_to_move: PieceColour,
71    pub last_move: Move,
72    legal_moves: Vec<Move>,
73    pub board_hash: u64,
74    pub position_hash: u64,
75    position: Position,
76    move_count: u32,
77    halfmove_count: u32,
78    position_occurences: ahash::AHashMap<PositionHash, u8>,
79    lazy_legal_moves: bool,
80}
81
82impl PartialEq for BoardState {
83    fn eq(&self, other: &Self) -> bool {
84        self.board_hash == other.board_hash && self.position_hash == other.position_hash
85    }
86}
87
88impl From<FEN> for BoardState {
89    fn from(fen: FEN) -> Self {
90        let pos = Position::from(fen);
91        Self::from_parts(pos, fen.halfmove_count(), fen.move_count())
92    }
93}
94
95impl BoardState {
96    pub fn new_starting() -> Self {
97        let position = Position::new_starting();
98        log::info!("New starting Position created");
99        let position_hash: PositionHash = position.pos_hash();
100        let board_hash = zobrist::board_state_hash(position_hash, 1, 0);
101        let side_to_move = position.side;
102        // deref all legal moves, performance isn't as important here, so avoid lifetime specifiers to make things easier to look at
103        let legal_moves = position.get_legal_moves().into_iter().cloned().collect();
104        log::trace!("Legal moves generated: {legal_moves:?}");
105        let mut position_occurences = ahash::AHashMap::default();
106        position_occurences.insert(position_hash, 1);
107        log::info!("New starting BoardState created");
108        BoardState {
109            position,
110            move_count: 1, // movecount starts at 1
111            halfmove_count: 0,
112            position_hash,
113            board_hash,
114            side_to_move,
115            last_move: NULL_MOVE,
116            legal_moves,
117            position_occurences,
118            lazy_legal_moves: false,
119        }
120    }
121
122    pub(crate) fn from_parts(position: Position, halfmove_count: u32, move_count: u32) -> Self {
123        let position_hash: PositionHash = position.pos_hash();
124        let board_hash = zobrist::board_state_hash(position_hash, 1, halfmove_count);
125        let side_to_move = position.side;
126        // deref all legal moves, performance isn't as important here, so avoid lifetime specifiers to make things easier to look at
127        let legal_moves = position.get_legal_moves().into_iter().cloned().collect();
128        let mut position_occurences = ahash::AHashMap::default();
129        position_occurences.insert(position_hash, 1);
130        log::info!("New BoardState created from parts");
131        BoardState {
132            position,
133            move_count,
134            halfmove_count,
135            position_hash,
136            board_hash,
137            side_to_move,
138            last_move: NULL_MOVE,
139            legal_moves,
140            position_occurences,
141            lazy_legal_moves: false,
142        }
143    }
144
145    pub(crate) fn position(&self) -> &Position {
146        &self.position
147    }
148
149    pub fn halfmove_count(&self) -> u32 {
150        self.halfmove_count
151    }
152
153    pub fn move_count(&self) -> u32 {
154        self.move_count
155    }
156
157    pub fn get_pseudo_legal_moves(&self) -> &Vec<Move> {
158        self.position.get_pseudo_legal_moves()
159    }
160
161    // checks if a move would create a legal position, does not check for boardstate legality
162    pub fn is_move_legal_position(&self, mv: &Move) -> bool {
163        self.position.is_move_legal(mv)
164    }
165
166    // lazily do legality check on pseudo legal moves as the iterator is used
167    pub fn lazy_get_legal_moves(&self) -> impl Iterator<Item = &Move> {
168        self.position
169            .get_pseudo_legal_moves()
170            .iter()
171            .filter(|mv| self.position.is_move_legal(mv))
172    }
173
174    // next state without legality and gamestate checks done (legal_moves is empty), may panic if unreachable code is hit e.g. in zobrist hash generation if position occurrences ever gets above 3
175    // USERS MUST CHECK IF GAMESTATE IS VALID (E.G THREEFOLD REPETITION, 50 MOVE RULE) AS THIS FUNCTION DOES NOT
176    pub fn next_state_unchecked(&self, mv: &Move) -> Self {
177        let position = self.position.new_position(mv);
178        log::trace!("New Position created from move: {:?}", mv);
179        let position_hash = zobrist::pos_next_hash(
180            &self.position.movegen_flags,
181            &position.movegen_flags,
182            self.position_hash,
183            mv,
184        );
185        log::trace!(
186            "New position hash generated: {}",
187            util::hash_to_string(position_hash)
188        );
189        let side_to_move = position.side;
190        let last_move = *mv;
191        // deref all legal moves
192        let legal_moves = Vec::with_capacity(0); // empty vec as we don't need to generate legal moves ahead of time
193
194        let move_count = if side_to_move == PieceColour::White {
195            self.move_count + 1
196        } else {
197            self.move_count
198        };
199
200        let halfmove_reset = matches!(
201            mv.move_type,
202            MoveType::PawnPush | MoveType::DoublePawnPush | MoveType::Capture(_)
203        );
204        let halfmove_count = if halfmove_reset {
205            0
206        } else {
207            self.halfmove_count + 1
208        };
209
210        let mut position_occurences = self.position_occurences.clone();
211        let po = position_occurences.entry(position_hash).or_insert(0);
212        *po += 1;
213
214        let board_hash = zobrist::board_state_hash(position_hash, *po, halfmove_count);
215        //let board_hash = position_hash ^ (*po as u64) ^ (halfmove_count as u64);
216        log::trace!("Board hash: {}", util::hash_to_string(board_hash));
217
218        log::trace!("New BoardState created from move: {:?}", mv);
219        Self {
220            side_to_move,
221            last_move,
222            legal_moves,
223            position,
224            board_hash,
225            position_hash,
226            move_count,
227            halfmove_count,
228            position_occurences,
229            lazy_legal_moves: true,
230        }
231    }
232
233    pub fn next_state(&self, mv: &Move) -> Result<Self, BoardStateError> {
234        if mv == &NULL_MOVE {
235            let err = BoardStateError::NullMove(
236                "&NULL_MOVE was passed as an argument to BoardState::next_state()".to_string(),
237            );
238            log_and_return_error!(err)
239        }
240        if self.lazy_legal_moves {
241            let err = BoardStateError::LazyIncompatiblity("next_state called on BoardState with lazy_legal_moves flag set, cannot generate next state without all legal moves being generated.".to_string());
242            log_and_return_error!(err)
243        }
244        if !self.legal_moves.contains(mv) {
245            let err = BoardStateError::IllegalMove(format!("{:?} is not a legal move", mv));
246            log_and_return_error!(err)
247        }
248
249        let current_game_state = self.get_gamestate();
250
251        if current_game_state == GameState::Checkmate
252            || current_game_state == GameState::Stalemate
253            || current_game_state == GameState::FiftyMove
254            || current_game_state == GameState::Repetition
255        {
256            let err = BoardStateError::NoLegalMoves(current_game_state);
257            log_and_return_error!(err)
258        }
259
260        let position = self.position.new_position(mv);
261        log::trace!("New Position created from move: {:?}", mv);
262        let position_hash = zobrist::pos_next_hash(
263            &self.position.movegen_flags,
264            &position.movegen_flags,
265            self.position_hash,
266            mv,
267        );
268        log::trace!(
269            "New position hash generated: {}",
270            util::hash_to_string(position_hash)
271        );
272        let side_to_move = position.side;
273        let last_move = *mv;
274        // deref all legal moves
275        let legal_moves = position.get_legal_moves().into_iter().cloned().collect();
276        log::trace!("Legal moves generated: {legal_moves:?}");
277
278        let move_count = if side_to_move == PieceColour::White {
279            self.move_count + 1
280        } else {
281            self.move_count
282        };
283
284        let halfmove_reset = matches!(
285            mv.move_type,
286            MoveType::PawnPush | MoveType::DoublePawnPush | MoveType::Capture(_)
287        );
288        let halfmove_count = if halfmove_reset {
289            0
290        } else {
291            self.halfmove_count + 1
292        };
293
294        let mut position_occurences = self.position_occurences.clone();
295        let po = position_occurences.entry(position_hash).or_insert(0);
296        *po += 1;
297
298        let board_hash = zobrist::board_state_hash(position_hash, *po, halfmove_count);
299        //let board_hash = position_hash ^ (*po as u64) ^ (halfmove_count as u64);
300        log::trace!("Board hash: {}", util::hash_to_string(board_hash));
301
302        log::trace!("New BoardState created from move: {:?}", mv);
303        Ok(Self {
304            side_to_move,
305            last_move,
306            legal_moves,
307            position,
308            board_hash,
309            position_hash,
310            move_count,
311            halfmove_count,
312            position_occurences,
313            lazy_legal_moves: false,
314        })
315    }
316
317    // fn gen_legal_moves(&mut self) {
318    //     self.legal_moves = self
319    //         .position
320    //         .get_legal_moves()
321    //         .into_iter()
322    //         .cloned()
323    //         .collect();
324    // }
325
326    pub fn get_legal_moves(&self) -> Result<&[Move], BoardStateError> {
327        if self.lazy_legal_moves {
328            let err = BoardStateError::LazyIncompatiblity("get_legal_moves called on BoardState with lazy_legal_moves flag set, legal_moves vec is empty".to_string());
329            log_and_return_error!(err)
330        }
331        Ok(&self.legal_moves)
332    }
333
334    pub fn get_occurences_of_current_position(&self) -> u8 {
335        *self
336            .position_occurences
337            .get(&self.position_hash)
338            .unwrap_or(&1)
339    }
340    // TODO add check for insufficient material
341    pub fn get_gamestate(&self) -> GameState {
342        let legal_moves_empty = if self.lazy_legal_moves {
343            self.lazy_get_legal_moves().peekable().peek().is_none()
344        } else {
345            self.legal_moves.is_empty()
346        };
347        let is_in_check = self.position.is_in_check();
348
349        // checkmate has to be checked for first, as it supercedes other states like the 50 move rule
350        if is_in_check && legal_moves_empty {
351            GameState::Checkmate
352        } else if !is_in_check && legal_moves_empty {
353            GameState::Stalemate
354        } else if self.halfmove_count >= 100 {
355            GameState::FiftyMove
356        } else if self.get_occurences_of_current_position() >= 3 {
357            GameState::Repetition
358        } else if is_in_check {
359            GameState::Check
360        } else if false {
361            //placeholder
362            GameState::InsufficientMaterial
363        } else {
364            GameState::Active
365        }
366    }
367
368    // fn is_in_check(&self) -> bool {
369    //     self.position.is_in_check()
370    // }
371
372    // fn is_checkmate(&self) -> bool {
373    //     return if self.lazy_legal_moves {
374    //         self.lazy_is_checkmate()
375    //     } else {
376    //         self.legal_moves.is_empty() && self.position.is_in_check()
377    //     };
378    // }
379
380    // fn is_draw(&self) -> bool {
381    //     return if self.lazy_legal_moves {
382    //         self.lazy_is_draw()
383    //     } else {
384    //         (self.legal_moves.is_empty() && !self.position.is_in_check())
385    //             || self.halfmove_count >= 100
386    //             || self.get_occurences_of_current_position() >= 3
387    //     };
388    // }
389
390    // // is_checkmate only checking if the lazy legal moves iterator returns None on peek
391    // fn lazy_is_checkmate(&self) -> bool {
392    //     self.lazy_get_legal_moves().peekable().peek().is_none() && self.position.is_in_check()
393    // }
394
395    // // is draw only checking if the lazy legal moves iterator returns None on peek
396    // fn lazy_is_draw(&self) -> bool {
397    //     self.halfmove_count >= 100
398    //         || self.get_occurences_of_current_position() >= 3
399    //         || (self.lazy_get_legal_moves().peekable().peek().is_none()
400    //             && !self.position.is_in_check())
401    // }
402
403    pub fn get_pos64(&self) -> &Pos64 {
404        &self.position.pos64
405    }
406}
407
408#[derive(Debug, Clone, Copy, PartialEq, Eq)]
409pub enum GameOverState {
410    WhiteResign,
411    BlackResign,
412    AgreedDraw,
413    Forced(GameState),
414}
415
416#[derive(Debug)]
417pub struct Board {
418    current_state: BoardState,
419    state_history: Vec<BoardState>,
420    move_history: Vec<Move>,
421    game_over_state: Option<GameOverState>,
422    transposition_table: transposition::TranspositionTable,
423}
424
425impl Default for Board {
426    fn default() -> Self {
427        Self::new()
428    }
429}
430
431impl From<FEN> for Board {
432    fn from(fen: FEN) -> Self {
433        let current_state = BoardState::from(fen);
434        let state_history: Vec<BoardState> = vec![current_state.clone()];
435        let transposition_table = transposition::TranspositionTable::new();
436        // TODO gos
437        log::info!("New Board created from FEN: {}", fen.to_string());
438        Board {
439            current_state,
440            state_history,
441            move_history: Vec::new(),
442            game_over_state: None,
443            transposition_table,
444        }
445    }
446}
447
448impl TryFrom<pgn::PGN> for Board {
449    type Error = PGNParseError;
450
451    fn try_from(pgn: pgn::PGN) -> Result<Self, PGNParseError> {
452        let mut board = Self::new();
453        for notation in pgn.moves() {
454            let mv = notation.to_move_with_context(board.get_current_state())?;
455            match board.make_move(&mv) {
456                Ok(_) => {}
457                Err(e) => log_and_return_error!(PGNParseError::NotationParseError(e.to_string())),
458            }
459        }
460        //TODO when board can store more info set it here
461        for tag in pgn.tags() {
462            if let Tag::Result(result) = tag {
463                match result.as_str() {
464                    // these will be ignored if game over state is already set in Board, priority is given to Forced(GameState) FIXME this needs to be clearer
465                    "1-0" => board.set_resign(PieceColour::Black),
466                    "0-1" => board.set_resign(PieceColour::White),
467                    "1/2-1/2" => board.set_draw(),
468                    _ => {}
469                }
470            }
471        }
472        Ok(board)
473    }
474}
475
476impl Board {
477    pub fn new() -> Self {
478        let current_state = BoardState::new_starting();
479        let mut state_history: Vec<BoardState> = Vec::new();
480        log::info!("State history created");
481        state_history.push(current_state.clone());
482
483        let transposition_table = transposition::TranspositionTable::new();
484        log::info!("Transposition table created");
485        log::info!("New Board created");
486        Board {
487            current_state,
488            state_history,
489            move_history: Vec::new(),
490            game_over_state: None,
491            transposition_table,
492        }
493    }
494
495    pub fn set_resign(&mut self, side: PieceColour) {
496        let gos = match side {
497            PieceColour::White => GameOverState::WhiteResign,
498            PieceColour::Black => GameOverState::BlackResign,
499        };
500        if self.game_over_state.is_none() {
501            self.game_over_state = Some(gos);
502        } else {
503            log::warn!("Game over state already set, ignoring set_resign");
504        }
505    }
506
507    pub fn set_draw(&mut self) {
508        if self.game_over_state.is_none() {
509            self.game_over_state = Some(GameOverState::AgreedDraw);
510        } else {
511            log::warn!("Game over state already set, ignoring set_draw");
512        }
513    }
514
515    pub fn get_starting_state(&self) -> &BoardState {
516        // first element in state_history is guarenteed to be initialised as starting BoardState
517        &self.state_history[0]
518    }
519
520    pub fn get_side_to_move(&self) -> PieceColour {
521        self.current_state.side_to_move
522    }
523
524    pub fn get_current_state(&self) -> &BoardState {
525        &self.current_state
526    }
527
528    pub fn get_state_history(&self) -> &Vec<BoardState> {
529        &self.state_history
530    }
531
532    pub fn get_game_over_state(&self) -> Option<GameOverState> {
533        self.game_over_state
534    }
535
536    pub fn make_move(&mut self, mv: &Move) -> Result<GameState, BoardStateError> {
537        if let Some(gos) = self.game_over_state {
538            let err = BoardStateError::GameOver(gos);
539            log_and_return_error!(err)
540        }
541        let next_state = self.current_state.next_state(mv)?;
542        self.current_state = next_state;
543        self.state_history.push(self.current_state.clone());
544        self.move_history.push(*mv);
545
546        let game_state = self.current_state.get_gamestate();
547        if game_state.is_game_over() {
548            self.game_over_state = Some(GameOverState::Forced(game_state));
549        }
550        Ok(game_state)
551    }
552
553    pub fn make_engine_move(&mut self, depth: u8) -> Result<GameState, BoardStateError> {
554        if let Some(gos) = self.game_over_state {
555            let err = BoardStateError::GameOver(gos);
556            log_and_return_error!(err)
557        }
558        let (eval, engine_move) =
559            engine::choose_move(&self.current_state, depth, &mut self.transposition_table);
560        let mv = *engine_move;
561        log::info!("Engine move chosen: {:?} @ eval: {}", engine_move, eval);
562
563        self.make_move(&mv)
564    }
565
566    pub fn move_history_string_notation(&self) -> Vec<String> {
567        let mut notations_string = Vec::new();
568        let notations = self.move_history_notation();
569        for n in notations {
570            notations_string.push(n.to_string());
571        }
572        notations_string
573    }
574
575    pub fn move_history_notation(&self) -> Vec<Notation> {
576        let mut notations = Vec::new();
577        for (state, mv) in self.state_history.iter().zip(self.move_history.iter()) {
578            // move will all be legal, so unwrap is safe
579            let notation = Notation::from_mv_with_context(state, mv).unwrap();
580            notations.push(notation);
581        }
582        notations
583    }
584
585    pub fn unmake_move(&mut self) -> Result<Rc<BoardState>, BoardStateError> {
586        todo!()
587    }
588
589    pub fn get_current_gamestate(&self) -> GameState {
590        self.current_state.get_gamestate()
591    }
592}