Skip to main content

chess/position/
play.rs

1use core::{fmt, str};
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(
16    Clone, Copy, Debug, DeserializeFromStr, Eq, Ord, PartialEq, PartialOrd, SerializeDisplay,
17)]
18/// Move that a player can make.
19pub struct Move {
20    pub kind: Kind,
21    /// redundant when given the board
22    pub role: Role,
23    pub from: Square,
24    pub to: Square,
25    /// redundant when given the board
26    pub capture: Option<Role>,
27}
28
29/// What kind of move is it?
30#[repr(u8)]
31#[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)]
32pub enum Kind {
33    #[default]
34    Normal,
35    Special(Special),
36}
37
38#[repr(u8)]
39#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
40pub enum Special {
41    EnPassant,
42    Castle(File),
43    Promote(Role),
44}
45
46impl Kind {
47    #[inline]
48    pub const fn normal() -> Self {
49        Kind::Normal
50    }
51
52    #[inline]
53    pub const fn special(special: Special) -> Self {
54        Kind::Special(special)
55    }
56
57    #[inline]
58    pub const fn en_passant() -> Self {
59        Kind::Special(Special::EnPassant)
60    }
61
62    #[inline]
63    pub const fn castle(file: File) -> Self {
64        Kind::Special(Special::Castle(file))
65    }
66
67    #[inline]
68    pub const fn promote(role: Role) -> Self {
69        Kind::Special(Special::Promote(role))
70    }
71
72    #[inline]
73    pub const fn is_normal(self) -> bool {
74        matches!(self, Kind::Normal)
75    }
76
77    #[inline]
78    pub const fn is_special(self) -> bool {
79        matches!(self, Kind::Normal)
80    }
81
82    #[inline]
83    pub const fn specials(self) -> Option<Special> {
84        if let Kind::Special(special) = self { Some(special) } else { None }
85    }
86
87    #[inline]
88    pub const fn is_en_passant(self) -> bool {
89        if let Kind::Special(special) = self { special.is_en_passant() } else { false }
90    }
91
92    #[inline]
93    pub const fn is_castle(self) -> bool {
94        if let Kind::Special(special) = self { special.is_castle() } else { false }
95    }
96
97    #[inline]
98    pub const fn castle_rook_file(self) -> Option<File> {
99        if let Kind::Special(special) = self { special.castle_rook_file() } else { None }
100    }
101
102    #[inline]
103    pub const fn is_promote(self) -> bool {
104        if let Kind::Special(special) = self { special.is_promote() } else { false }
105    }
106
107    #[inline]
108    pub const fn promotes(self) -> Option<Role> {
109        if let Kind::Special(special) = self { special.promotes() } else { None }
110    }
111
112    #[inline]
113    pub const fn is_promote_role(self, role: Role) -> bool {
114        if let Kind::Special(special) = self { special.is_promote_role(role) } else { false }
115    }
116}
117
118// Construct a normal move, or use the special constructors castle, en_passant, or promote.
119//
120// If a piece is captured, optionally set its role.
121
122/// Constructors
123impl Move {
124    #[inline]
125    pub const fn normal(role: Role, from: Square, to: Square) -> Move {
126        Move::capture(role, from, to, None)
127    }
128
129    #[inline]
130    pub const fn capture(role: Role, from: Square, to: Square, capture: Option<Role>) -> Move {
131        Move { kind: Kind::Normal, role, from, to, capture }
132    }
133
134    #[inline]
135    pub const fn chess_castle(player: Player, side: Side) -> Move {
136        Move::castle(player, Square::new(E, player.backrank()), side.chess_rook())
137    }
138
139    #[inline]
140    pub const fn castle(player: Player, from: Square, file: File) -> Move {
141        let side = Side::of_rook(from, file);
142        Move {
143            kind: Kind::Special(Special::Castle(file)),
144            role: King,
145            from,
146            to: player.castle_king_to(side),
147            capture: None,
148        }
149    }
150
151    #[inline]
152    pub const fn en_passant(from: Square, to: Square) -> Move {
153        Move { role: Pawn, from, to, capture: Some(Pawn), kind: Kind::Special(Special::EnPassant) }
154    }
155
156    #[inline]
157    pub const fn promote(from: Square, to: Square, role: Role) -> Move {
158        Move::promote_capture(from, to, role, None)
159    }
160
161    #[inline]
162    pub const fn promote_capture(
163        from: Square,
164        to: Square,
165        role: Role,
166        capture: Option<Role>,
167    ) -> Move {
168        Move { role: Pawn, from, to, capture, kind: Kind::Special(Special::Promote(role)) }
169    }
170
171    pub fn pawn(player: Player, from: Square, to: Square, capture: Option<Role>) -> Vec<Move> {
172        let mut plays = Vec::new();
173
174        if to.rank() as u8 == player.promotion_rank() as u8 {
175            for role in [Queen, Rook, Bishop, Knight] {
176                plays.push(Move::promote_capture(from, to, role, capture));
177            }
178        } else {
179            plays.push(Move::capture(Pawn, from, to, capture));
180        }
181
182        plays
183    }
184}
185
186/// Builders
187impl Move {
188    #[inline]
189    pub const fn capturing(mut self, role: Role) -> Move {
190        self.capture = Some(role);
191        self
192    }
193}
194
195/// Accessors inherited from [`Kind`]
196impl Move {
197    #[inline]
198    pub const fn is_normal(self) -> bool {
199        self.kind.is_normal()
200    }
201
202    #[inline]
203    pub const fn is_special(self) -> bool {
204        self.kind.is_special()
205    }
206
207    #[inline]
208    pub const fn specials(self) -> Option<Special> {
209        self.kind.specials()
210    }
211}
212
213/// Accessors inherited from [`Special`]
214impl Move {
215    #[inline]
216    pub const fn is_en_passant(self) -> bool {
217        self.kind.is_en_passant()
218    }
219
220    #[inline]
221    pub const fn is_castle(self) -> bool {
222        self.kind.is_castle()
223    }
224
225    #[inline]
226    pub const fn castle_rook_file(self) -> Option<File> {
227        self.kind.castle_rook_file()
228    }
229
230    #[inline]
231    pub const fn is_castle_side(self, side: Side) -> bool {
232        match self.castle_side() {
233            Some(this) => this.eq(side),
234            None => false,
235        }
236    }
237
238    #[inline]
239    pub const fn castle_side(self) -> Option<Side> {
240        match self.castle_rook_file() {
241            Some(file) => Some(Side::of_rook(self.from, file)),
242            None => None,
243        }
244    }
245
246    #[inline]
247    pub const fn is_promote(self) -> bool {
248        self.kind.is_promote()
249    }
250
251    #[inline]
252    pub const fn promotes(self) -> Option<Role> {
253        self.kind.promotes()
254    }
255
256    #[inline]
257    pub const fn is_promote_role(self, role: Role) -> bool {
258        self.kind.is_promote_role(role)
259    }
260}
261
262/// Accessors inherited from `capture`
263impl Move {
264    #[inline]
265    pub const fn is_capture(self) -> bool {
266        self.capture.is_some()
267    }
268
269    #[inline]
270    pub const fn captures(self) -> Option<Role> {
271        self.capture
272    }
273
274    #[inline]
275    pub const fn is_capture_role(self, role: Role) -> bool {
276        if let Some(this) = self.capture { this as u8 == role as u8 } else { false }
277    }
278}
279
280/// Compact encoding
281impl Move {
282    #[inline]
283    pub const fn code(self) -> u16 {
284        (self.to as u16)
285            | ((self.from as u16) << 6)
286            | (self.promotion_code() << 12)
287            | (self.kind_code() << 14)
288    }
289
290    #[inline]
291    const fn promotion_code(self) -> u16 {
292        match self.promotes() {
293            Some(Knight) => 0,
294            Some(Bishop) => 1,
295            Some(Rook) => 2,
296            Some(Queen) => 3,
297            _ => 0,
298        }
299    }
300
301    #[inline]
302    const fn kind_code(self) -> u16 {
303        match self.specials() {
304            Some(Special::Promote(_)) => 1,
305            Some(Special::EnPassant) => 2,
306            Some(Special::Castle(_)) => 3,
307            None => 0,
308        }
309    }
310}
311
312/// Uses the reversible move representation.
313impl fmt::Display for Move {
314    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
315        repr::display(self, f)
316    }
317}
318
319impl str::FromStr for Move {
320    type Err = repr::Error;
321
322    fn from_str(s: &str) -> Result<Self, Self::Err> {
323        repr::from_str(s)
324    }
325}
326
327impl Special {
328    #[inline]
329    pub const fn en_passant() -> Self {
330        Special::EnPassant
331    }
332
333    #[inline]
334    pub const fn castle(file: File) -> Self {
335        Special::Castle(file)
336    }
337
338    #[inline]
339    pub const fn promote(role: Role) -> Self {
340        Special::Promote(role)
341    }
342
343    #[inline]
344    pub const fn is_en_passant(self) -> bool {
345        matches!(self, Special::EnPassant)
346    }
347
348    #[inline]
349    pub const fn is_castle(self) -> bool {
350        matches!(self, Special::Castle(_))
351    }
352
353    #[inline]
354    pub const fn castle_rook_file(self) -> Option<File> {
355        if let Special::Castle(file) = self { Some(file) } else { None }
356    }
357
358    #[inline]
359    pub const fn is_promote(self) -> bool {
360        matches!(self, Special::Promote(_))
361    }
362
363    #[inline]
364    pub const fn promotes(self) -> Option<Role> {
365        if let Special::Promote(role) = self { Some(role) } else { None }
366    }
367
368    #[inline]
369    pub const fn is_promote_role(self, role: Role) -> bool {
370        if let Special::Promote(this) = self { this as u8 == role as u8 } else { false }
371    }
372}
373
374pub mod algebraic {
375    // Note: This is FIDE notation, even though "O-O" and "O-O-O" is prettier
376    pub mod fide {
377        pub const SHORT_CASTLE: &str = "0-0";
378        pub const LONG_CASTLE: &str = "0-0-0";
379    }
380    // Note: PGN uses vowel O instead of number 0
381    pub mod pgn {
382        pub const SHORT_CASTLE: &str = "O-O";
383        pub const LONG_CASTLE: &str = "O-O-O";
384    }
385
386    pub const SHORT_CASTLE: &str = pgn::SHORT_CASTLE;
387    pub const LONG_CASTLE: &str = pgn::LONG_CASTLE;
388    pub const MOVE: char = '-';
389    pub const CAPTURE: char = 'x';
390}
391
392/// A reversible adaptation of long algebraic notation, independent of a board position.
393///
394/// Castling uses the king's move and the rook's original file because `O-O` and
395/// `O-O-O` cannot recover both without the position.
396pub mod repr {
397    use crate::{
398        formats::{StrInput as Input, prelude::*},
399        square::Rank,
400    };
401
402    use super::*;
403
404    pub fn display(play: &Move, f: &mut fmt::Formatter<'_>) -> fmt::Result {
405        if play.role != Pawn {
406            write!(f, "{}", play.role.upper())?;
407        }
408        write!(f, "{}", play.from)?;
409        match play.capture {
410            Some(Pawn) => write!(f, "x{}", play.to)?,
411            Some(role) => write!(f, "x{}{}", role.upper(), play.to)?,
412            None => write!(f, "-{}", play.to)?,
413        }
414        match play.kind {
415            Kind::Normal => Ok(()),
416            Kind::Special(Special::EnPassant) => write!(f, ".ep"),
417            Kind::Special(Special::Castle(file)) => write!(f, "/{file}"),
418            Kind::Special(Special::Promote(role)) => write!(f, "={}", role.upper()),
419        }
420    }
421
422    pub fn from_str(s: &str) -> Result<Move, Error> {
423        let mut input = s.trim();
424        let play = play(&mut input).map_err(|_| Error)?;
425        input.is_empty().then_some(play).ok_or(Error)
426    }
427
428    #[derive(Debug, thiserror::Error)]
429    #[error("invalid move representation")]
430    pub struct Error;
431
432    pub fn play(input: &mut Input<'_>) -> ModalResult<Move> {
433        let play = seq! {Move {
434            role: role,
435            from: square,
436            capture: capture,
437            to: square,
438            kind: kind,
439        }}
440        .parse_next(input)?;
441
442        // Consistency checks that are not strictly needed - our Move
443        // type has no validity protection.
444        // chess::Move enters a chess::Game via the validity check in
445        // PositionMut::insert_at_untracked, which is the validity boundary.
446        let invalid_castle = play.is_castle() && (play.capture.is_some() || play.role != King);
447        let invalid_en_passant =
448            play.is_en_passant() && (play.role != Pawn || play.capture != Some(Pawn));
449        if invalid_castle || invalid_en_passant {
450            return Err(backtrack());
451        }
452
453        Ok(play)
454    }
455
456    fn capture(input: &mut Input<'_>) -> ModalResult<Option<Role>> {
457        alt(('-'.value(None), preceded('x', role).map(Some))).parse_next(input)
458    }
459
460    fn role(input: &mut Input<'_>) -> ModalResult<Role> {
461        opt(explicit_role).map(|role| role.unwrap_or(Pawn)).parse_next(input)
462    }
463
464    fn explicit_role(input: &mut Input<'_>) -> ModalResult<Role> {
465        one_of(|c| "NBRQK".contains(c)).map(Role::panicky_from_char).parse_next(input)
466    }
467
468    fn kind(input: &mut Input<'_>) -> ModalResult<Kind> {
469        opt(special).map(|special| special.map_or(Kind::Normal, Kind::Special)).parse_next(input)
470    }
471
472    fn special(input: &mut Input<'_>) -> ModalResult<Special> {
473        alt((
474            preceded('/', file).map(Special::castle),
475            ".ep".value(Special::en_passant()),
476            preceded('=', promotion).map(Special::promote),
477        ))
478        .parse_next(input)
479    }
480
481    fn promotion(input: &mut Input<'_>) -> ModalResult<Role> {
482        one_of(|c| "NBRQ".contains(c)).map(Role::panicky_from_char).parse_next(input)
483    }
484
485    fn square(input: &mut Input<'_>) -> ModalResult<Square> {
486        (file, rank).map(|(file, rank)| Square::new(file, rank)).parse_next(input)
487    }
488
489    fn file(input: &mut Input<'_>) -> ModalResult<File> {
490        one_of(|c| "abcdefgh".contains(c)).map(File::panicky_from_char).parse_next(input)
491    }
492
493    fn rank(input: &mut Input<'_>) -> ModalResult<Rank> {
494        one_of(|c| "12345678".contains(c)).map(Rank::panicky_from_char).parse_next(input)
495    }
496}
497
498#[test]
499fn display_play() {
500    use Square::*;
501
502    let mut play = Move::promote(A2, H7, Queen);
503
504    assert_eq!(play.to_string(), "a2-h7=Q");
505    assert_eq!(play.to_string().parse::<Move>().unwrap(), play);
506    assert_eq!(play.uci_chess().to_string(), "a2h7q");
507
508    play.role = King;
509    assert_eq!(play.to_string(), "Ka2-h7=Q");
510    assert_eq!(play.to_string().parse::<Move>().unwrap(), play);
511    assert_eq!(play.uci_chess().to_string(), "a2h7q");
512
513    play = play.capturing(Bishop);
514    assert_eq!(play.to_string(), "Ka2xBh7=Q");
515    assert_eq!(play.to_string().parse::<Move>().unwrap(), play);
516    assert_eq!(play.uci_chess().to_string(), "a2h7q");
517
518    let play = Move::chess_castle(White, Side::King);
519    assert_eq!(play.to_string(), "Ke1-g1/h");
520    assert_eq!(play.to_string().parse::<Move>().unwrap(), play);
521    assert_eq!(play.uci_chess().to_string(), "e1g1");
522
523    let play = Move::chess_castle(Black, Side::Queen);
524    assert_eq!(play.to_string(), "Ke8-c8/a");
525    assert_eq!(play.to_string().parse::<Move>().unwrap(), play);
526    assert_eq!(play.uci_chess().to_string(), "e8c8");
527
528    let play = Move::castle(White, G1, F);
529    assert_eq!(play.to_string(), "Kg1-c1/f");
530    assert_eq!(play.to_string().parse::<Move>().unwrap(), play);
531    assert_eq!(play.uci_chess().to_string(), "g1c1");
532    assert_eq!(play.uci_960().to_string(), "g1f1");
533
534    let play = Move::en_passant(E5, D6);
535    assert_eq!(play.capture, Some(Pawn));
536    assert!(play.is_capture());
537    assert_eq!(play.to_string(), "e5xd6.ep");
538    assert_eq!(play.to_string().parse::<Move>().unwrap(), play);
539
540    let play = Move::capture(Pawn, E5, D6, Some(Pawn));
541    assert_eq!(play.to_string(), "e5xd6");
542    assert_eq!(play.to_string().parse::<Move>().unwrap(), play);
543}