Skip to main content

document_graphics/chess/
position.rs

1use super::*;
2use cozy_chess::{File, Move, Rank};
3
4#[derive(Clone, Debug)]
5pub struct Position {
6    node: NodeId,
7    source: Arc<[u8]>,
8    board: Board,
9    /// Actual source/destination squares (castling points at the king's destination).
10    pub last_move: Option<(Square, Square)>,
11}
12impl Position {
13    pub fn node(&self) -> NodeId {
14        self.node
15    }
16    pub fn piece(&self, square: Square) -> Option<(Color, Piece)> {
17        Some((self.board.color_on(square)?, self.board.piece_on(square)?))
18    }
19    pub fn side_to_move(&self) -> Color {
20        self.board.side_to_move()
21    }
22    pub fn fullmove_number(&self) -> u16 {
23        self.board.fullmove_number()
24    }
25    pub fn in_check(&self) -> bool {
26        !self.board.checkers().is_empty()
27    }
28    pub fn fen(&self) -> String {
29        self.board.to_string()
30    }
31    pub fn advance(
32        &self,
33        doc: &Collection,
34        node: NodeId,
35        cancel: &AtomicBool,
36    ) -> Result<Self, Error> {
37        check(cancel)?;
38        if !Arc::ptr_eq(&self.source, &doc.source)
39            || doc.nodes.get(node).and_then(|n| n.parent) != Some(self.node)
40        {
41            return Err(invalid(
42                0,
43                "Position can only advance to a child in the same collection",
44            ));
45        }
46        let mut result = self.clone();
47        result.apply(doc, node)?;
48        check(cancel)?;
49        Ok(result)
50    }
51    fn apply(&mut self, doc: &Collection, node: NodeId) -> Result<(), Error> {
52        let offset = doc.nodes[node].san.start;
53        let san = doc
54            .san(node)
55            .ok_or_else(|| invalid(offset, "Invalid SAN move"))?;
56        let mv = san_move(&self.board, san)
57            .ok_or_else(|| invalid(offset, "Illegal or ambiguous chess move"))?;
58        let castle = self.board.color_on(mv.to) == Some(self.board.side_to_move());
59        self.last_move = Some((
60            mv.from,
61            if castle {
62                Square::new(
63                    if mv.to.file() > mv.from.file() {
64                        File::G
65                    } else {
66                        File::C
67                    },
68                    mv.from.rank(),
69                )
70            } else {
71                mv.to
72            },
73        ));
74        self.board
75            .try_play(mv)
76            .map_err(|_| invalid(offset, "Illegal chess move"))?;
77        self.node = node;
78        Ok(())
79    }
80}
81pub(super) fn at(doc: &Collection, node: NodeId, cancel: &AtomicBool) -> Result<Position, Error> {
82    check(cancel)?;
83    let entry = doc
84        .nodes
85        .get(node)
86        .ok_or_else(|| invalid(0, "Unknown chess node"))?;
87    let game = &doc.games[entry.game];
88    let mut position = Position {
89        node: game.root,
90        source: doc.source.clone(),
91        board: game.start.clone(),
92        last_move: None,
93    };
94    let mut ancestors = Vec::new();
95    let mut id = node;
96    while let Some(parent) = doc.nodes[id].parent {
97        check(cancel)?;
98        ancestors.push(id);
99        id = parent;
100    }
101    for id in ancestors.into_iter().rev() {
102        check(cancel)?;
103        position.apply(doc, id)?;
104    }
105    Ok(position)
106}
107
108/// Resolve SAN against legal moves, including pinned-piece disambiguation,
109/// promotions, en passant and castling. At most one standard board's moves are visited.
110fn san_move(board: &Board, san: &str) -> Option<Move> {
111    if !san.is_ascii() || san.len() > 32 {
112        return None;
113    }
114    let san = san.trim_end_matches(['+', '#', '!', '?']);
115    let castle = match san {
116        "O-O" | "0-0" => Some(true),
117        "O-O-O" | "0-0-0" => Some(false),
118        _ => None,
119    };
120    let mut s = san;
121    let piece = if castle.is_some() {
122        Piece::King
123    } else {
124        match s.as_bytes().first()? {
125            b'K' => {
126                s = &s[1..];
127                Piece::King
128            }
129            b'Q' => {
130                s = &s[1..];
131                Piece::Queen
132            }
133            b'R' => {
134                s = &s[1..];
135                Piece::Rook
136            }
137            b'B' => {
138                s = &s[1..];
139                Piece::Bishop
140            }
141            b'N' => {
142                s = &s[1..];
143                Piece::Knight
144            }
145            _ => Piece::Pawn,
146        }
147    };
148    let mut promotion = None;
149    let mut destination = None;
150    let mut file = None;
151    let mut rank = None;
152    let mut capture = false;
153    if castle.is_none() {
154        if s.len() >= 2 && s.as_bytes()[s.len() - 2] == b'=' {
155            promotion = Some(match s.as_bytes()[s.len() - 1] {
156                b'Q' => Piece::Queen,
157                b'R' => Piece::Rook,
158                b'B' => Piece::Bishop,
159                b'N' => Piece::Knight,
160                _ => return None,
161            });
162            s = &s[..s.len() - 2];
163        }
164        if s.len() < 2 {
165            return None;
166        }
167        destination = Some(s[s.len() - 2..].parse::<Square>().ok()?);
168        s = &s[..s.len() - 2];
169        if let Some(prefix) = s.strip_suffix('x') {
170            capture = true;
171            s = prefix;
172        }
173        if let Some(b'a'..=b'h') = s.as_bytes().first() {
174            file = Some(s[..1].parse::<File>().ok()?);
175            s = &s[1..];
176        }
177        if let Some(b'1'..=b'8') = s.as_bytes().first() {
178            rank = Some(s[..1].parse::<Rank>().ok()?);
179            s = &s[1..];
180        }
181        if !s.is_empty() || (piece == Piece::Pawn && (rank.is_some() || capture != file.is_some()))
182        {
183            return None;
184        }
185    }
186    let mut selected = None;
187    let mut ambiguous = false;
188    board.generate_moves(|moves| {
189        for mv in moves {
190            if board.piece_on(mv.from) != Some(piece) {
191                continue;
192            }
193            let is_castle = board.color_on(mv.to) == Some(board.side_to_move());
194            if let Some(short) = castle {
195                if !is_castle || (mv.to.file() > mv.from.file()) != short {
196                    continue;
197                }
198            } else {
199                if is_castle || Some(mv.to) != destination || mv.promotion != promotion {
200                    continue;
201                }
202                if file.is_some_and(|f| mv.from.file() != f)
203                    || rank.is_some_and(|r| mv.from.rank() != r)
204                {
205                    continue;
206                }
207                let is_capture = board.color_on(mv.to) == Some(!board.side_to_move())
208                    || (piece == Piece::Pawn && mv.from.file() != mv.to.file());
209                if capture != is_capture {
210                    continue;
211                }
212            }
213            if selected.is_some() {
214                ambiguous = true;
215                return true;
216            }
217            selected = Some(mv);
218        }
219        false
220    });
221    if ambiguous { None } else { selected }
222}