document_graphics/chess/
position.rs1use 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 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
108fn 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}