board_game/games/go/
io.rs

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        // TODO support padding here?
15        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
31// By convention 'I' is skipped because it can be confused with "1".
32const 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    // TODO re-introduce score and territory once we have optimized implementations for those?
145    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        // ensure komi_2 is an integer
230        if komi_2_f.fract() == 0.0 {
231            let komi_2 = komi_2_f as i16;
232            // ensure komi_2 fits in i16
233            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    /// The fen format:
286    /// `"tiles next pass [komi]"`
287    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        // TODO only print alive groups?
424        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}