Skip to main content

chess/
position.rs

1//! # Chess positions
2//!
3//! Any [`Position`] is determined by:
4//! - the location of pieces on the [`Board`]
5//! - parameters for the next turn (these are not visible from the board itself)
6//!   - the [`Player`] whose turn it is
7//!   - the possible castle [`Sides`]
8//!   - the possible en-passant [`Square`] if any
9//! - counter for "reversible" turns since last "non-reversible" (aka pawn moves + captures) [`Move`], aka "halfmoves"
10//! - counter of full rounds, aka "fullmoves"
11//!
12//! Compared to `shakmaty` our position is a concrete `struct`
13#[cfg(test)]
14extern crate alloc;
15
16use core::num::NonZeroU32;
17
18use crate::{
19    Board, Player, Role, Scharnagl, Square,
20    board::Bitboard,
21    square::File,
22};
23
24use Player::*;
25use Role::*;
26
27mod moves;
28mod play;
29mod rights;
30mod validate;
31
32pub use play::*;
33pub use rights::*;
34
35pub use Kind::Normal;
36pub use Special::{Castle, Promote};
37
38#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
39pub enum Error {
40    #[error("inconsistent board bitboards")]
41    InconsistentBoard,
42    #[error("expected exactly one {0} king")]
43    KingCount(Player),
44    #[error("pawns cannot be on the first or eighth rank")]
45    PawnOnBackrank,
46    #[error("kings cannot be adjacent")]
47    AdjacentKings,
48    #[error("{0} king is attacked")]
49    KingAttacked(Player),
50    #[error("{0} king with castling rights is not on its back rank")]
51    CastleKing(Player),
52    #[error("no {player} rook for {side:?}-side castling on {file}")]
53    CastleRook { player: Player, side: Side, file: File },
54    #[error("{player} castling rook on {file} is not on the {side:?} side")]
55    CastleSide { player: Player, side: Side, file: File },
56    #[error("no piece on {0}")]
57    MissingPiece(Square),
58}
59
60pub type Result<T, E = Error> = core::result::Result<T, E>;
61
62/// Chess position, including the board, turn, rights, and counters.
63///
64/// Size:
65/// - board: 9 * 8 = 72 bytes
66/// - turn: 1 byte (really 1 bit)
67/// - castle rights: 2 bytes (really 2 bits)
68/// - en passant square: 1 bytes (really 65 options)
69/// - reversible move counter: 4 bytes (modeled as u32, really 1 byte would be enough)
70/// - round counter: 4 bytes (value >=1, u16 or even u8 should be enough, who has 65536 rounds in a game of chess?)
71///
72/// Unpacked, this amounts to 88 bytes (Rust 1.91).
73/// Packed it is 84 bytes.
74///
75/// <https://lichess.org/@/revoof/blog/adapting-nnue-pytorchs-binary-position-format-for-lichess/cpeeAMeY>
76/// shows that about 18.7 bytes is enough
77///
78/// Besides size, a goal is also to stick these into SQLite3 (or DuckDB), and make something
79/// similar to [Chess Query Language][cql] (with a less weird syntax...) efficently implementable.
80///
81/// The contents can be split in:
82/// - board
83/// - rights: turn + castle + en passant
84/// - counters
85///
86/// [cql]: https://en.wikipedia.org/wiki/Chess_Query_Language
87#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
88pub struct Position {
89    /// location of the pieces on the board
90    board: Board,
91    /// player to move
92    turn: Player,
93    /// possible castle sides
94    castles: Castles,
95    /// possible en passant square
96    en_passant: Option<EnPassant>,
97    /// ply counter since last capture or pawn move (reversible moves)
98    reversible: u32,
99    /// starts at 1 and increments after every Black move
100    round: NonZeroU32,
101}
102
103/// The raw parts from which a position can be constructed.
104#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
105pub struct Parts {
106    /// location of the pieces on the board
107    pub board: Board,
108    /// player to move
109    pub turn: Player,
110    /// possible castle sides
111    pub castles: Castles,
112    /// possible en passant square
113    pub en_passant: Option<EnPassant>,
114    /// ply counter since last capture or pawn move (reversible moves)
115    pub reversible: u32,
116    /// starts at 1 and increments after every Black move
117    pub round: NonZeroU32,
118}
119
120impl Parts {
121    pub const fn new(board: Board, turn: Player) -> Self {
122        Self {
123            board,
124            turn,
125            castles: Castles::empty(),
126            en_passant: None,
127            reversible: 0,
128            round: NonZeroU32::MIN,
129        }
130    }
131
132    pub const fn empty() -> Self {
133        Self::new(Board::EMPTY, White)
134    }
135
136    pub const fn ply(&self) -> usize {
137        let round = self.round.get() as usize - 1;
138        round * 2 + self.turn.eq(Black) as usize
139    }
140
141}
142
143impl Default for Parts {
144    fn default() -> Self {
145        Self::empty()
146    }
147}
148
149impl Position {
150    pub const fn start() -> Position {
151        Position {
152            board: Board::standard(),
153            turn: White,
154            castles: Castles::chess(),
155            en_passant: None,
156            reversible: 0,
157            round: NonZeroU32::MIN,
158        }
159    }
160
161    pub const fn freestyle(i: Scharnagl) -> Position {
162        // Construct board
163        let board = Board::freestyle(i);
164
165        // Extract castle rights
166        let king = board.king_of(White).unwrap();
167        let mut rooks = board.rooks();
168        let queen_rook = rooks.pop_first().unwrap().file();
169        let king_rook = rooks.first().unwrap().file();
170
171        let mut castles = Castles::empty();
172        castles.set(White, Side::of_rook(king, queen_rook), queen_rook);
173        castles.set(White, Side::of_rook(king, king_rook), king_rook);
174        castles.set(Black, Side::Queen, queen_rook);
175        castles.set(Black, Side::King, king_rook);
176
177        Position {
178            board,
179            turn: White,
180            castles,
181            en_passant: None,
182            reversible: 0,
183            round: NonZeroU32::MIN,
184        }
185    }
186}
187
188impl Position {
189    #[inline]
190    pub const fn board(&self) -> Board {
191        self.board
192    }
193
194    #[inline]
195    pub const fn turn(&self) -> Player {
196        self.turn
197    }
198
199    #[inline]
200    pub const fn castles(&self) -> Castles {
201        self.castles
202    }
203
204    #[inline]
205    pub const fn en_passant(&self) -> Option<EnPassant> {
206        self.en_passant
207    }
208
209    #[inline]
210    pub const fn reversible(&self) -> u32 {
211        self.reversible
212    }
213
214    #[inline]
215    pub const fn round(&self) -> NonZeroU32 {
216        self.round
217    }
218
219    pub const fn checkers(&self) -> Bitboard {
220        match self.board.king_of(self.turn) {
221            Some(king) => self.board.attacks_on(king, self.turn.other(), self.board.occupied()),
222            None => Bitboard::EMPTY,
223        }
224    }
225
226    pub const fn is_check(&self) -> bool {
227        !self.checkers().is_empty()
228    }
229}
230
231impl Position {
232    pub fn capture_moves(&self) -> Vec<Move> {
233        self.legal_moves().into_iter().filter(|m| m.is_capture()).collect()
234    }
235
236    pub fn castle_side_moves(&self, side: Side) -> Vec<Move> {
237        self.legal_moves().into_iter().filter(|m| m.is_castle_side(side)).collect()
238    }
239
240    pub fn castle_moves(&self) -> Vec<Move> {
241        self.legal_moves().into_iter().filter(|m| m.is_castle()).collect()
242    }
243}
244
245impl Position {
246    pub const fn parts(self) -> Parts {
247        Parts {
248            board: self.board,
249            turn: self.turn,
250            castles: self.castles,
251            en_passant: self.en_passant,
252            reversible: self.reversible,
253            round: self.round,
254        }
255    }
256
257    pub fn first_ply(&self) -> usize {
258        let round = self.round.get() as usize - 1;
259        round * 2 + usize::from(self.turn == Black)
260    }
261
262}
263
264impl From<Position> for Parts {
265    fn from(position: Position) -> Self {
266        position.parts()
267    }
268}
269
270impl Position {
271    pub(crate) fn apply_unchecked(mut self, play: Move) -> Position {
272        let player = self.turn;
273        let captured = if play.is_en_passant() {
274            Some(Square::new(play.to.file(), play.from.rank()))
275        } else if play.capture.is_some() {
276            Some(play.to)
277        } else {
278            None
279        };
280
281        if play.role == King {
282            self.castles.clear_player(player);
283        }
284
285        if play.role == Rook {
286            self.clear_castle_rook(player, play.from);
287        }
288
289        if let Some(captured) = captured {
290            self.clear_castle_rook(player.other(), captured);
291        }
292
293        self.board.play_unchecked(player, play);
294
295        let mut en_passant = None;
296        if play.role == Pawn {
297            let from = play.from as u8;
298            let to = play.to as u8;
299            if from.abs_diff(to) == 16 {
300                en_passant = EnPassant::try_from(Square::panicky_from_index((from + to) / 2)).ok();
301            }
302        }
303
304        if play.role == Pawn || play.capture.is_some() || play.is_en_passant() {
305            self.reversible = 0;
306        } else {
307            self.reversible += 1;
308        }
309
310        if player == Black {
311            self.round = self.round.saturating_add(1);
312        }
313
314        self.turn = player.other();
315        self.en_passant = match en_passant {
316            Some(en_passant) => self.board.attacked_en_passant(en_passant, self.turn),
317            None => None,
318        };
319        self
320    }
321
322    fn clear_castle_rook(&mut self, player: Player, square: Square) {
323        if square.rank() != player.backrank() {
324            return;
325        }
326
327        for side in Side::ALL {
328            if self.castles.get(player, side) == Some(square.file()) {
329                self.castles.clear(player, side);
330            }
331        }
332    }
333}
334
335#[test]
336fn all_random() {
337    use alloc::collections::BTreeSet as Set;
338
339    let mut positions = Vec::new();
340    let all = Set::from_iter(1usize..=8);
341    for rook_l in 1usize..=8 {
342        for rook_r in rook_l + 2..=8 {
343            for king in (rook_l + 1)..rook_r {
344                assert!(rook_l < king);
345                assert!(king < rook_r);
346                let it = (1usize..rook_l)
347                    .chain((rook_l + 1)..king)
348                    .chain((king + 1)..rook_r)
349                    .chain((rook_r + 1)..=8);
350                for bishop_b in it.clone().filter(|i| (i & 1) == 1) {
351                    for bishop_w in it.clone().filter(|i| (i & 1) == 0) {
352                        let used = Set::from([rook_l, rook_r, king, bishop_b, bishop_w]);
353                        let remaining = all.difference(&used);
354                        for queen in remaining.clone() {
355                            let mut position = vec!['-'; 8];
356                            position[rook_l - 1] = 'r';
357                            position[rook_r - 1] = 'r';
358                            position[king - 1] = 'k';
359                            position[queen - 1] = 'q';
360                            position[bishop_w - 1] = 'b';
361                            position[bishop_b - 1] = 'b';
362                            for knight in remaining.clone() {
363                                if knight != queen {
364                                    position[knight - 1] = 'n';
365                                }
366                            }
367                            assert!(!position.contains(&'-'));
368                            positions.push(position);
369                        }
370                    }
371                }
372            }
373        }
374    }
375    for position in positions.iter() {
376        println!("{}", position.iter().collect::<String>());
377    }
378    assert_eq!(960, positions.len());
379}