Skip to main content

chess/formats/chessbase/game/
convert.rs

1use crate::{
2    Player, Side,
3    board::{Board, Role},
4    game::CursorMut,
5    position::{Castles as ChessCastles, EnPassant, Parts, Special},
6    square::{File, Rank, Square},
7};
8
9use super::{Game, Move, PawnMove, Pieces, Position, Token};
10
11#[derive(Debug, thiserror::Error)]
12pub enum Error {
13    #[error("custom ChessBase starting positions are not supported yet")]
14    Start,
15    #[error("ChessBase null moves are not supported")]
16    Null,
17    #[error("missing ChessBase {player} {role} number {number}")]
18    Piece { player: Player, role: Role, number: u8 },
19    #[error("invalid ChessBase move from {from} by ({file}, {rank})")]
20    Offset { from: Square, file: i8, rank: i8 },
21    #[error("invalid ChessBase move from {from} to {to}")]
22    Move { from: Square, to: Square },
23    #[error(transparent)]
24    Game(#[from] crate::game::Error),
25    #[error(transparent)]
26    Options(#[from] crate::game::options::Error),
27}
28
29pub type Result<T, E = Error> = core::result::Result<T, E>;
30
31impl TryFrom<Game> for crate::Game {
32    type Error = Error;
33
34    fn try_from(encoded: Game) -> Result<Self> {
35        Self::from_chessbase(encoded)
36    }
37}
38
39impl crate::Game {
40    pub fn from_chessbase(encoded: Game) -> Result<Self> {
41        let Game { position, tokens } = encoded;
42        let mut pieces = position.pieces;
43        let mut game = Self::new(crate::Position::try_from(position)?);
44        Self::decode_tokens(game.cursor_mut(), &mut tokens.into_iter(), &mut pieces)?;
45        Ok(game)
46    }
47
48    fn decode_tokens(
49        mut cursor: CursorMut<'_>,
50        tokens: &mut impl Iterator<Item = Token>,
51        pieces: &mut Pieces,
52    ) -> Result<()> {
53        while let Some(token) = tokens.next() {
54            match token {
55                Token::Move(encoded) => {
56                    let position = cursor.position();
57                    let play = pieces.resolve(&position, encoded)?;
58                    pieces.apply(&position, play);
59                    cursor.option_mut_or_push(play)?;
60                }
61                Token::Push => {
62                    let saved = *pieces;
63                    Self::decode_tokens(cursor.cursor_mut(), tokens, pieces)?;
64                    *pieces = saved;
65                }
66                Token::Pop => return Ok(()),
67                Token::Skip => {}
68            }
69        }
70        Ok(())
71    }
72}
73
74impl TryFrom<Position> for crate::Position {
75    type Error = Error;
76
77    fn try_from(encoded: Position) -> Result<Self> {
78        let mut board = Board::EMPTY;
79        for player in Player::iter() {
80            for role in Role::iter() {
81                for square in encoded.pieces[player][role].into_iter().flatten() {
82                    if board.get(square).is_some() {
83                        return Err(Error::Start);
84                    }
85                    board.insert(square, role.of(player));
86                }
87            }
88        }
89
90        let mut castles = ChessCastles::empty();
91        for player in Player::iter() {
92            for side in Side::iter() {
93                if encoded.castles[player][side] {
94                    let king = board.king_of(player).ok_or(Error::Start)?;
95                    let mut rooks = board.rooks().intersection(board.player(player));
96                    let file = loop {
97                        let rook = rooks.pop_first().ok_or(Error::Start)?;
98                        if rook.rank() == player.backrank()
99                            && Side::of_rook(king, rook.file()) == side
100                        {
101                            break rook.file();
102                        }
103                    };
104                    castles.set(player, side, file);
105                }
106            }
107        }
108
109        let en_passant = encoded.en_passant.and_then(|file| {
110            let rank = if encoded.turn.is_white() { Rank::Six } else { Rank::Three };
111            EnPassant::from_square(Square::new(file, rank))
112        });
113        Parts {
114            board,
115            turn: encoded.turn,
116            castles,
117            en_passant,
118            reversible: 0,
119            round: encoded.round,
120        }
121        .validate()
122        .map_err(|_| Error::Start)
123    }
124}
125
126impl Pieces {
127    fn resolve(&self, position: &crate::Position, encoded: Move) -> Result<crate::Move> {
128        use Move::*;
129        use PawnMove::*;
130
131        let player = position.turn();
132        let (from, to, promotion) = match encoded {
133            Null => return Err(Error::Null),
134            Castle(dx) => {
135                let from = self.piece(player, Role::King, 0)?;
136                let side = if dx > 0 { Side::King } else { Side::Queen };
137                let to = player.castle_king_to(side);
138                return position
139                    .legal_plays()
140                    .into_iter()
141                    .find(|play| {
142                        play.from == from
143                            && play
144                                .kind
145                                .castle_rook_file()
146                                .is_some_and(|file| Side::of_rook(from, file) == side)
147                    })
148                    .ok_or(Error::Move { from, to });
149            }
150            King(dx, dy) => {
151                let from = self.piece(player, Role::King, 0)?;
152                let to =
153                    checked_add(from, dx, dy).ok_or(Error::Offset { from, file: dx, rank: dy })?;
154                (from, to, None)
155            }
156            Queen(number, dx, dy) => {
157                let from = self.piece(player, Role::Queen, number)?;
158                (from, wrapping_add(from, dx, dy), None)
159            }
160            Rook(number, dx, dy) => {
161                let from = self.piece(player, Role::Rook, number)?;
162                (from, wrapping_add(from, dx, dy), None)
163            }
164            Bishop(number, dx, dy) => {
165                let from = self.piece(player, Role::Bishop, number)?;
166                (from, wrapping_add(from, dx, dy), None)
167            }
168            Knight(number, dx, dy) => {
169                let from = self.piece(player, Role::Knight, number)?;
170                let to =
171                    checked_add(from, dx, dy).ok_or(Error::Offset { from, file: dx, rank: dy })?;
172                (from, to, None)
173            }
174            Pawn(file, movement) => {
175                let from = self.piece(player, Role::Pawn, file as u8)?;
176                let dy = if player.is_white() { 1 } else { -1 };
177                let (dx, dy) = match movement {
178                    One => (0, dy),
179                    Two => (0, 2 * dy),
180                    Right => (dy, dy),
181                    Left => (-dy, dy),
182                };
183                let to =
184                    checked_add(from, dx, dy).ok_or(Error::Offset { from, file: dx, rank: dy })?;
185                (from, to, None)
186            }
187            FromTo(from, to, role) => {
188                let promotion =
189                    (position.board().role_at(from) == Some(Role::Pawn)).then_some(role);
190                (from, to, promotion)
191            }
192        };
193
194        position
195            .legal_plays()
196            .into_iter()
197            .find(|play| {
198                play.from == from
199                    && play.to == to
200                    && match promotion {
201                        Some(role) => play.kind.is_promote_role(role),
202                        None => !play.kind.is_promote(),
203                    }
204            })
205            .ok_or(Error::Move { from, to })
206    }
207
208    fn piece(&self, player: Player, role: Role, number: u8) -> Result<Square> {
209        self[player][role].get(number as usize).copied().flatten().ok_or(Error::Piece {
210            player,
211            role,
212            number,
213        })
214    }
215
216    fn apply(&mut self, position: &crate::Position, play: crate::Move) {
217        let player = position.turn();
218        let board = position.board();
219        let captured = if play.kind.is_en_passant() {
220            Some(Square::new(play.to.file(), play.from.rank()))
221        } else {
222            board.get(play.to).map(|_| play.to)
223        };
224        if let Some(square) = captured {
225            let piece = board.get(square).expect("captured square contains a piece");
226            self.capture(piece.player, piece.role, square);
227        }
228
229        self.move_piece(player, play.role, play.from, play.to);
230        match play.kind.specials() {
231            Some(Special::Castle(file)) => {
232                let side = crate::Side::of_rook(play.from, file);
233                let from = player.castle_rook_from(file);
234                let to = player.castle_rook_to(side);
235                self.move_piece(player, Role::Rook, from, to);
236            }
237            Some(Special::Promote(role)) => {
238                self.remove(player, Role::Pawn, play.to);
239                let promoted = self[player][role]
240                    .iter_mut()
241                    .find(|square| square.is_none())
242                    .expect("ChessBase supports at most ten pieces of one role");
243                *promoted = Some(play.to);
244            }
245            Some(Special::EnPassant) | None => {}
246        }
247    }
248
249    fn move_piece(&mut self, player: Player, role: Role, from: Square, to: Square) {
250        let square = self[player][role]
251            .iter_mut()
252            .find(|square| **square == Some(from))
253            .expect("resolved ChessBase piece identity exists");
254        *square = Some(to);
255    }
256
257    fn remove(&mut self, player: Player, role: Role, square: Square) {
258        let pieces = &mut self[player][role];
259        let index = pieces
260            .iter()
261            .position(|candidate| *candidate == Some(square))
262            .expect("captured ChessBase piece identity exists");
263        pieces[index] = None;
264    }
265
266    fn capture(&mut self, player: Player, role: Role, square: Square) {
267        let pieces = &mut self[player][role];
268        let index = pieces
269            .iter()
270            .position(|candidate| *candidate == Some(square))
271            .expect("captured ChessBase piece identity exists");
272        if role == Role::Pawn {
273            pieces[index] = None;
274        } else {
275            pieces[index..].rotate_left(1);
276            *pieces.last_mut().unwrap() = None;
277        }
278    }
279}
280
281fn checked_add(from: Square, file: i8, rank: i8) -> Option<Square> {
282    let file = u8::try_from(from.file() as i16 + i16::from(file)).ok()?;
283    let rank = u8::try_from(from.rank() as i16 + i16::from(rank)).ok()?;
284    Some(Square::new(File::from_index(file)?, Rank::from_index(rank)?))
285}
286
287fn wrapping_add(from: Square, file: u8, rank: u8) -> Square {
288    let file = (u16::from(from.file() as u8) + u16::from(file)) % 8;
289    let rank = (u16::from(from.rank() as u8) + u16::from(rank)) % 8;
290    Square::new(File::panicky_from_index(file as u8), Rank::panicky_from_index(rank as u8))
291}