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}