1use std::convert::TryFrom;
2use std::fmt::{Alignment, Debug, Display, Formatter};
3use std::str::FromStr;
4
5use itertools::Itertools;
6
7use crate::board::{Board, Player};
8use crate::games::go::chains::Chains;
9use crate::games::go::tile::Tile;
10use crate::games::go::{GoBoard, Komi, Move, PlacementKind, Rules, State, TileOccupied, GO_MAX_SIZE};
11
12impl Display for Tile {
13 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
14 write!(f, "{}{}", self.x_disp(), self.y() as u32 + 1)
16 }
17}
18
19impl Debug for Tile {
20 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
21 write!(f, "Tile(({}, {}), {})", self.x(), self.y(), self)
22 }
23}
24
25#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
26pub struct InvalidTile;
27
28#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
29pub struct InvalidX;
30
31const TILE_X_NAMES_SINGLE: &[u8] = b"ABCDEFGHJKLMNOPQRSTUVWXYZ";
33
34impl Tile {
35 pub fn x_disp(&self) -> TileX {
36 TileX(self.x())
37 }
38}
39
40#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
41pub struct TileX(pub u8);
42
43impl FromStr for TileX {
44 type Err = InvalidX;
45
46 fn from_str(s: &str) -> Result<Self, Self::Err> {
47 fn byte_index(c: u8) -> Result<usize, InvalidX> {
48 TILE_X_NAMES_SINGLE
49 .iter()
50 .position(|&cand| cand == c.to_ascii_uppercase())
51 .ok_or(InvalidX)
52 }
53
54 let x = match *s.as_bytes() {
55 [c] => byte_index(c)?,
56 [c1, c0] => (1 + byte_index(c1)?) * TILE_X_NAMES_SINGLE.len() + byte_index(c0)?,
57 _ => return Err(InvalidX),
58 };
59
60 if x <= GO_MAX_SIZE as usize {
61 Ok(TileX(x as u8))
62 } else {
63 Err(InvalidX)
64 }
65 }
66}
67
68impl Display for TileX {
69 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
70 let width = f.width().unwrap_or(0);
71
72 let left = match f.align() {
73 Some(Alignment::Left) => true,
74 Some(Alignment::Center | Alignment::Right) | None => false,
75 };
76
77 let x = self.0 as usize;
78 match TILE_X_NAMES_SINGLE.get(x).copied() {
79 Some(b) => match left {
80 true => write!(f, "{:<width$}", b as char, width = width),
81 false => write!(f, "{:>width$}", b as char, width = width),
82 },
83 None => {
84 let b1 = TILE_X_NAMES_SINGLE[(x / TILE_X_NAMES_SINGLE.len()) - 1] as char;
85 let b0 = (TILE_X_NAMES_SINGLE[x % TILE_X_NAMES_SINGLE.len()] as char).to_ascii_lowercase();
86 let pad = width.saturating_sub(2);
87 match left {
88 true => write!(f, "{}{}{:pad$}", b1, b0, "", pad = pad),
89 false => write!(f, "{:pad$}{}{}", "", b1, b0, pad = pad),
90 }
91 }
92 }
93 }
94}
95
96impl FromStr for Tile {
97 type Err = InvalidTile;
98
99 fn from_str(s: &str) -> Result<Self, Self::Err> {
100 check(s.len() >= 2 && s.is_ascii(), InvalidTile)?;
101 let split = s.bytes().take_while(|c| c.is_ascii_alphabetic()).count();
102
103 let x = TileX::from_str(&s[..split]).map_err(|_| InvalidTile)?.0;
104
105 let y_1 = s[split..].parse::<u32>().map_err(|_| InvalidTile)?;
106 check(y_1 > 0, InvalidTile)?;
107 let y = y_1 - 1;
108 check(y <= GO_MAX_SIZE as u32, InvalidTile)?;
109 let y = y as u8;
110
111 Ok(Tile::new(x, y))
112 }
113}
114
115impl GoBoard {
116 fn write_debug(&self, f: &mut Formatter, include_fen: bool) -> std::fmt::Result {
117 let fen = match include_fen {
118 true => format!(", fen={:?}", self.to_fen()),
119 false => String::new(),
120 };
121
122 write!(
123 f,
124 "GoBoard(next={:?}, state={:?}, history_len={}, stones_b={}, stones_w={}, komi={}, rules={:?}{})",
125 go_player_to_symbol(self.next_player()),
126 self.state(),
127 self.history().len(),
128 self.chains().stone_count_from(Player::A),
129 self.chains().stone_count_from(Player::B),
130 self.komi(),
131 self.rules(),
132 fen,
133 )
134 }
135}
136
137impl Debug for GoBoard {
138 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
139 self.write_debug(f, true)
140 }
141}
142
143impl Display for GoBoard {
144 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
146 self.write_debug(f, false)?;
147 writeln!(f)?;
148
149 let size = self.size();
150 let width_x = TileX(size.saturating_sub(1)).to_string().len();
151 let width_y = size.to_string().len();
152
153 for y in (0..size).rev() {
154 write!(f, "{:width$} ", y + 1, width = width_y)?;
155
156 for x in 0..size {
157 let tile = Tile::new(x, y);
158 let player = self.stone_at(tile);
159 let c = match player {
160 None => '.',
161 Some(player) => go_player_to_symbol(player),
162 };
163 write!(f, "{:width$}", c, width = width_x)?;
164 }
165
166 writeln!(f)?;
167 }
168
169 write!(f, "{:width$}", "", width = width_y + 1)?;
170 for x in 0..size {
171 write!(f, "{:<width$}", TileX(x), width = width_x)?;
172 }
173 writeln!(f)?;
174
175 Ok(())
176 }
177}
178
179impl Display for Move {
180 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
181 match self {
182 Move::Pass => write!(f, "PASS"),
183 Move::Place(tile) => write!(f, "{}", tile),
184 }
185 }
186}
187
188#[derive(Default, Debug, Copy, Clone, Eq, PartialEq)]
189pub struct InvalidMove;
190
191impl FromStr for Move {
192 type Err = InvalidMove;
193
194 fn from_str(s: &str) -> Result<Self, Self::Err> {
195 if s == "PASS" || s == "pass" {
196 Ok(Move::Pass)
197 } else {
198 match Tile::from_str(s) {
199 Ok(tile) => Ok(Move::Place(tile)),
200 Err(InvalidTile) => Err(InvalidMove),
201 }
202 }
203 }
204}
205
206pub fn go_player_to_symbol(player: Player) -> char {
207 match player {
208 Player::A => 'b',
209 Player::B => 'w',
210 }
211}
212
213pub fn go_player_from_symbol(symbol: char) -> Option<Player> {
214 match symbol {
215 'b' | 'B' => Some(Player::A),
216 'w' | 'W' => Some(Player::B),
217 _ => None,
218 }
219}
220
221#[derive(Debug, Copy, Clone, Eq, PartialEq)]
222pub struct InvalidKomi;
223
224impl TryFrom<f32> for Komi {
225 type Error = InvalidKomi;
226
227 fn try_from(value: f32) -> Result<Self, Self::Error> {
228 let komi_2_f = value * 2.0;
229 if komi_2_f.fract() == 0.0 {
231 let komi_2 = komi_2_f as i16;
232 if komi_2 as f32 == komi_2_f {
234 Ok(Komi::new(komi_2))
235 } else {
236 Err(InvalidKomi)
237 }
238 } else {
239 Err(InvalidKomi)
240 }
241 }
242}
243
244impl Display for Komi {
245 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
246 write!(f, "{}", self.as_float())
247 }
248}
249
250impl FromStr for Komi {
251 type Err = InvalidKomi;
252
253 fn from_str(s: &str) -> Result<Self, Self::Err> {
254 Komi::try_from(s.parse::<f32>().map_err(|_| InvalidKomi)?)
255 }
256}
257
258#[derive(Debug, Copy, Clone, Eq, PartialEq)]
259pub enum InvalidFen {
260 Syntax,
261 InvalidChar,
262 TooLarge,
263 InvalidShape,
264 HasDeadStones,
265 Komi,
266}
267
268impl GoBoard {
269 pub fn to_fen(&self) -> String {
270 let chains = self.chains().to_fen();
271 let next_player = go_player_to_symbol(self.next_player());
272 let pass_counter = match self.state() {
273 State::Normal => 0,
274 State::Passed => 1,
275 State::Done(_) => 2,
276 };
277 let komi = self.komi().as_float();
278 if komi == 0.0 {
279 format!("{} {} {}", chains, next_player, pass_counter)
280 } else {
281 format!("{} {} {} {}", chains, next_player, pass_counter, komi)
282 }
283 }
284
285 pub fn from_fen(fen: &str, rules: Rules) -> Result<GoBoard, InvalidFen> {
288 let values = fen.split(' ').collect_vec();
289 let (&tiles, &next, &pass, komi) = match values.as_slice() {
290 [tiles, next, pass] => (tiles, next, pass, None),
291 [tiles, next, pass, komi] => (tiles, next, pass, Some(komi)),
292 _ => return Err(InvalidFen::Syntax),
293 };
294
295 let chains = Chains::from_fen(tiles)?;
296
297 let next_player = match next {
298 "b" => Player::A,
299 "w" => Player::B,
300 _ => return Err(InvalidFen::InvalidChar),
301 };
302
303 let komi = match komi {
304 None => Komi::zero(),
305 Some(komi) => Komi::from_str(komi).map_err(|_| InvalidFen::Komi)?,
306 };
307
308 let state = match pass {
309 "0" => State::Normal,
310 "1" => State::Passed,
311 "2" => State::Done(chains.score().to_outcome(komi)),
312 _ => return Err(InvalidFen::InvalidChar),
313 };
314
315 Ok(GoBoard::from_parts(
316 rules,
317 chains,
318 next_player,
319 state,
320 Default::default(),
321 komi,
322 ))
323 }
324}
325
326impl Chains {
327 pub fn to_fen(&self) -> String {
328 let size = self.size();
329 let mut fen = String::new();
330
331 if size == 0 {
332 fen.push('/');
333 } else {
334 for y in (0..size).rev() {
335 for x in 0..size {
336 let tile = Tile::new(x, y).to_flat(size);
337 let player = self.stone_at(tile);
338 let c = match player {
339 None => '.',
340 Some(player) => go_player_to_symbol(player),
341 };
342 fen.push(c);
343 }
344 if y != 0 {
345 fen.push('/');
346 }
347 }
348 }
349
350 fen
351 }
352
353 pub fn from_fen(fen: &str) -> Result<Chains, InvalidFen> {
354 check(fen.chars().all(|c| "/wb.".contains(c)), InvalidFen::InvalidChar)?;
355
356 if fen == "/" {
357 Ok(Chains::new(0))
358 } else {
359 let lines: Vec<&str> = fen.split('/').collect_vec();
360 let size = lines.len();
361
362 check(size <= GO_MAX_SIZE as usize, InvalidFen::TooLarge)?;
363 let size = size as u8;
364
365 let mut chains = Chains::new(size);
366 for (y_rev, line) in lines.iter().enumerate() {
367 let y = size as usize - 1 - y_rev;
368 check(line.len() == size as usize, InvalidFen::InvalidShape)?;
369
370 for (x, value) in line.chars().enumerate() {
371 let tile = Tile::new(x as u8, y as u8).to_flat(size);
372 let value = match value {
373 'b' | 'w' => Some(go_player_from_symbol(value).unwrap()),
374 '.' => None,
375 _ => unreachable!(),
376 };
377
378 if let Some(player) = value {
379 let result = chains.place_stone(tile, player);
380 match result {
381 Ok(sim) => check(sim.kind == PlacementKind::Normal, InvalidFen::HasDeadStones)?,
382 Err(TileOccupied) => unreachable!(),
383 }
384 }
385 }
386 }
387
388 Ok(chains)
389 }
390 }
391}
392
393impl Debug for Chains {
394 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
395 write!(f, "Chains({:?})", self.to_fen())
396 }
397}
398
399impl Display for Chains {
400 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
401 writeln!(f, "Chains {{")?;
402 writeln!(f, " fen: {:?}", self.to_fen())?;
403
404 writeln!(f, " tiles:")?;
405 let size = self.size();
406 for y in (0..size).rev() {
407 write!(f, " {:2} ", y + 1)?;
408 for x in 0..size {
409 let tile = Tile::new(x, y).to_flat(size);
410 match self.tiles()[tile.index() as usize].group_id.to_option() {
411 None => write!(f, " .")?,
412 Some(group) => write!(f, "{:4}", group)?,
413 }
414 }
415 writeln!(f)?;
416 }
417 write!(f, " ")?;
418 for x in 0..size {
419 write!(f, " {}", TileX(x))?;
420 }
421 writeln!(f)?;
422
423 writeln!(f, " groups:")?;
425 for (i, group) in self.groups().enumerate() {
426 writeln!(f, " group {}: {:?}", i, group)?;
427 }
428
429 writeln!(f, "}}")?;
430 Ok(())
431 }
432}
433
434fn check<E>(c: bool, e: E) -> Result<(), E> {
435 match c {
436 true => Ok(()),
437 false => Err(e),
438 }
439}