Skip to main content

chess/position/
play.rs

1use core::fmt;
2
3use crate::{Player, Role, Square, square::File};
4
5use super::Side;
6
7use File::*;
8use Role::*;
9
10#[cfg(test)]
11use Player::*;
12
13// Since `move` is a keyword, can use `play: Move` as a synonym.
14// At least this is the most useful suggestion by Google AI Overview ;)
15#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
16/// Move that a player can make.
17pub struct Move {
18    pub kind: Kind,
19    /// redundant when given the board
20    pub role: Role,
21    pub from: Square,
22    pub to: Square,
23    /// redundant when given the board
24    pub capture: Option<Role>,
25}
26
27/// What kind of move is it?
28#[repr(u8)]
29#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
30pub enum Kind {
31    #[default]
32    Normal,
33    Special(Special),
34}
35
36#[repr(u8)]
37#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
38pub enum Special {
39    EnPassant,
40    Castle(File),
41    Promote(Role),
42}
43
44impl Kind {
45    #[inline]
46    pub const fn normal() -> Self {
47        Kind::Normal
48    }
49
50    #[inline]
51    pub const fn special(special: Special) -> Self {
52        Kind::Special(special)
53    }
54
55    #[inline]
56    pub const fn en_passant() -> Self {
57        Kind::Special(Special::EnPassant)
58    }
59
60    #[inline]
61    pub const fn castle(file: File) -> Self {
62        Kind::Special(Special::Castle(file))
63    }
64
65    #[inline]
66    pub const fn promote(role: Role) -> Self {
67        Kind::Special(Special::Promote(role))
68    }
69
70    #[inline]
71    pub const fn is_normal(self) -> bool {
72        matches!(self, Kind::Normal)
73    }
74
75    #[inline]
76    pub const fn is_special(self) -> bool {
77        matches!(self, Kind::Normal)
78    }
79
80    #[inline]
81    pub const fn specials(self) -> Option<Special> {
82        if let Kind::Special(special) = self { Some(special) } else { None }
83    }
84
85    #[inline]
86    pub const fn is_en_passant(self) -> bool {
87        if let Kind::Special(special) = self { special.is_en_passant() } else { false }
88    }
89
90    #[inline]
91    pub const fn is_castle(self) -> bool {
92        if let Kind::Special(special) = self { special.is_castle() } else { false }
93    }
94
95    #[inline]
96    pub const fn castle_rook_file(self) -> Option<File> {
97        if let Kind::Special(special) = self { special.castle_rook_file() } else { None }
98    }
99
100    #[inline]
101    pub const fn is_promote(self) -> bool {
102        if let Kind::Special(special) = self { special.is_promote() } else { false }
103    }
104
105    #[inline]
106    pub const fn promotes(self) -> Option<Role> {
107        if let Kind::Special(special) = self { special.promotes() } else { None }
108    }
109
110    #[inline]
111    pub const fn is_promote_role(self, role: Role) -> bool {
112        if let Kind::Special(special) = self { special.is_promote_role(role) } else { false }
113    }
114}
115
116// Construct a normal move, or use the special constructors castle, en_passant, or promote.
117//
118// If a piece is captured, optionally set its role.
119
120/// Constructors
121impl Move {
122    #[inline]
123    pub const fn normal(role: Role, from: Square, to: Square) -> Move {
124        Move::capture(role, from, to, None)
125    }
126
127    #[inline]
128    pub const fn capture(role: Role, from: Square, to: Square, capture: Option<Role>) -> Move {
129        Move { kind: Kind::Normal, role, from, to, capture }
130    }
131
132    #[inline]
133    pub const fn chess_castle(player: Player, side: Side) -> Move {
134        Move::castle(player, Square::new(E, player.backrank()), side.chess_rook())
135    }
136
137    #[inline]
138    pub const fn castle(player: Player, from: Square, file: File) -> Move {
139        let side = Side::of_rook(from, file);
140        Move {
141            kind: Kind::Special(Special::Castle(file)),
142            role: King,
143            from,
144            to: player.castle_king_to(side),
145            capture: None,
146        }
147    }
148
149    #[inline]
150    pub const fn en_passant(from: Square, to: Square) -> Move {
151        Move { role: Pawn, from, to, capture: None, kind: Kind::Special(Special::EnPassant) }
152    }
153
154    #[inline]
155    pub const fn promote(from: Square, to: Square, role: Role) -> Move {
156        Move::promote_capture(from, to, role, None)
157    }
158
159    #[inline]
160    pub const fn promote_capture(
161        from: Square,
162        to: Square,
163        role: Role,
164        capture: Option<Role>,
165    ) -> Move {
166        Move { role: Pawn, from, to, capture, kind: Kind::Special(Special::Promote(role)) }
167    }
168
169    pub fn pawn(player: Player, from: Square, to: Square, capture: Option<Role>) -> Vec<Move> {
170        let mut moves = Vec::new();
171
172        if to.rank() as u8 == player.promotion_rank() as u8 {
173            for role in [Queen, Rook, Bishop, Knight] {
174                moves.push(Move::promote_capture(from, to, role, capture));
175            }
176        } else {
177            moves.push(Move::capture(Pawn, from, to, capture));
178        }
179
180        moves
181    }
182}
183
184/// Builders
185impl Move {
186    #[inline]
187    pub const fn capturing(mut self, role: Role) -> Move {
188        self.capture = Some(role);
189        self
190    }
191}
192
193/// Accessors inherited from [`Kind`]
194impl Move {
195    #[inline]
196    pub const fn is_normal(self) -> bool {
197        self.kind.is_normal()
198    }
199
200    #[inline]
201    pub const fn is_special(self) -> bool {
202        self.kind.is_special()
203    }
204
205    #[inline]
206    pub const fn specials(self) -> Option<Special> {
207        self.kind.specials()
208    }
209}
210
211/// Accessors inherited from [`Special`]
212impl Move {
213    #[inline]
214    pub const fn is_en_passant(self) -> bool {
215        self.kind.is_en_passant()
216    }
217
218    #[inline]
219    pub const fn is_castle(self) -> bool {
220        self.kind.is_castle()
221    }
222
223    #[inline]
224    pub const fn castle_rook_file(self) -> Option<File> {
225        self.kind.castle_rook_file()
226    }
227
228    #[inline]
229    pub const fn is_castle_side(self, side: Side) -> bool {
230        match self.castle_side() {
231            Some(this) => this.eq(side),
232            None => false,
233        }
234    }
235
236    #[inline]
237    pub const fn castle_side(self) -> Option<Side> {
238        match self.castle_rook_file() {
239            Some(file) => Some(Side::of_rook(self.from, file)),
240            None => None,
241        }
242    }
243
244    #[inline]
245    pub const fn is_promote(self) -> bool {
246        self.kind.is_promote()
247    }
248
249    #[inline]
250    pub const fn promotes(self) -> Option<Role> {
251        self.kind.promotes()
252    }
253
254    #[inline]
255    pub const fn is_promote_role(self, role: Role) -> bool {
256        self.kind.is_promote_role(role)
257    }
258}
259
260/// Accessors inherited from `capture`
261impl Move {
262    #[inline]
263    pub const fn is_capture(self) -> bool {
264        self.capture.is_some()
265    }
266
267    #[inline]
268    pub const fn captures(self) -> Option<Role> {
269        self.capture
270    }
271
272    #[inline]
273    pub const fn is_capture_role(self, role: Role) -> bool {
274        if let Some(this) = self.capture { this as u8 == role as u8 } else { false }
275    }
276}
277
278/// Compact encoding
279impl Move {
280    #[inline]
281    pub const fn code(self) -> u16 {
282        (self.to as u16)
283            | ((self.from as u16) << 6)
284            | (self.promotion_code() << 12)
285            | (self.kind_code() << 14)
286    }
287
288    #[inline]
289    const fn promotion_code(self) -> u16 {
290        match self.promotes() {
291            Some(Knight) => 0,
292            Some(Bishop) => 1,
293            Some(Rook) => 2,
294            Some(Queen) => 3,
295            _ => 0,
296        }
297    }
298
299    #[inline]
300    const fn kind_code(self) -> u16 {
301        match self.specials() {
302            Some(Special::Promote(_)) => 1,
303            Some(Special::EnPassant) => 2,
304            Some(Special::Castle(_)) => 3,
305            None => 0,
306        }
307    }
308
309    fn long_algebraic(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        use algebraic::*;
311        use fmt::Write as _;
312
313        let Move { role, from, to, capture, kind } = *self;
314
315        if let Some(side) = self.castle_side() {
316            f.write_str(if side == Side::King { SHORT_CASTLE } else { LONG_CASTLE })
317        } else {
318            if role != Pawn {
319                f.write_char(role.upper())?;
320            }
321            let does = if capture.is_some() { CAPTURE } else { MOVE };
322            write!(f, "{}{}{}", from, does, to)?;
323
324            if let Some(promoted) = kind.promotes() {
325                write!(f, "={}", promoted.upper())?;
326            }
327            Ok(())
328        }
329    }
330}
331
332/// Uses long algebraic notation.
333impl fmt::Display for Move {
334    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
335        self.long_algebraic(f)
336    }
337}
338
339impl Special {
340    #[inline]
341    pub const fn en_passant() -> Self {
342        Special::EnPassant
343    }
344
345    #[inline]
346    pub const fn castle(file: File) -> Self {
347        Special::Castle(file)
348    }
349
350    #[inline]
351    pub const fn promote(role: Role) -> Self {
352        Special::Promote(role)
353    }
354
355    #[inline]
356    pub const fn is_en_passant(self) -> bool {
357        matches!(self, Special::EnPassant)
358    }
359
360    #[inline]
361    pub const fn is_castle(self) -> bool {
362        matches!(self, Special::Castle(_))
363    }
364
365    #[inline]
366    pub const fn castle_rook_file(self) -> Option<File> {
367        if let Special::Castle(file) = self { Some(file) } else { None }
368    }
369
370    #[inline]
371    pub const fn is_promote(self) -> bool {
372        matches!(self, Special::Promote(_))
373    }
374
375    #[inline]
376    pub const fn promotes(self) -> Option<Role> {
377        if let Special::Promote(role) = self { Some(role) } else { None }
378    }
379
380    #[inline]
381    pub const fn is_promote_role(self, role: Role) -> bool {
382        if let Special::Promote(this) = self { this as u8 == role as u8 } else { false }
383    }
384}
385
386pub mod algebraic {
387    // Note: This is FIDE notation, even though "O-O" and "O-O-O" is prettier
388    pub mod fide {
389        pub const SHORT_CASTLE: &str = "0-0";
390        pub const LONG_CASTLE: &str = "0-0-0";
391    }
392    // Note: PGN uses vowel O instead of number 0
393    pub mod pgn {
394        pub const SHORT_CASTLE: &str = "O-O";
395        pub const LONG_CASTLE: &str = "O-O-O";
396    }
397
398    pub const SHORT_CASTLE: &str = pgn::SHORT_CASTLE;
399    pub const LONG_CASTLE: &str = pgn::LONG_CASTLE;
400    pub const MOVE: char = '-';
401    pub const CAPTURE: char = 'x';
402}
403
404#[test]
405fn display_move() {
406    use Square::*;
407
408    let mut play = Move::promote(A2, H7, Queen);
409
410    assert_eq!(play.to_string(), "a2-h7=Q");
411    assert_eq!(play.uci_chess().to_string(), "a2h7q");
412
413    play.role = King;
414    assert_eq!(play.to_string(), "Ka2-h7=Q");
415    assert_eq!(play.uci_chess().to_string(), "a2h7q");
416
417    play = play.capturing(Bishop);
418    assert_eq!(play.to_string(), "Ka2xh7=Q");
419    assert_eq!(play.uci_chess().to_string(), "a2h7q");
420
421    let play = Move::chess_castle(White, Side::King);
422    assert_eq!(play.to_string(), algebraic::SHORT_CASTLE);
423    assert_eq!(play.uci_chess().to_string(), "e1g1");
424
425    let play = Move::chess_castle(Black, Side::Queen);
426    assert_eq!(play.to_string(), algebraic::LONG_CASTLE);
427    assert_eq!(play.uci_chess().to_string(), "e8c8");
428
429    let play = Move::castle(White, G1, F);
430    assert_eq!(play.to_string(), algebraic::LONG_CASTLE);
431    assert_eq!(play.uci_chess().to_string(), "g1c1");
432    assert_eq!(play.uci_960().to_string(), "g1f1");
433}