sgf_render/render/
text.rs

1use super::options::BoardSide;
2use super::{board_label_text, RenderOptions};
3
4use crate::errors::{GobanError, UsageError};
5use crate::goban::StoneColor;
6use crate::Goban;
7
8pub fn render(goban: &Goban, options: &RenderOptions) -> Result<String, GobanError> {
9    let (x_range, y_range) = options.goban_range.get_ranges(goban, options)?;
10    let width = x_range.end - x_range.start;
11    let height = y_range.end - y_range.start;
12    if !options.label_sides.is_empty() && width > 25 || height > 99 {
13        return Err(GobanError::UnlabellableRange);
14    }
15    let mut lines: Vec<String> = vec![];
16    let label_padding = if options.label_sides.contains(BoardSide::West) {
17        "   "
18    } else {
19        ""
20    };
21    if options.label_sides.contains(BoardSide::North) {
22        let line: String = x_range.clone().map(board_label_text).collect();
23        lines.push(format!("{label_padding}{line}"));
24    }
25    for y in y_range {
26        let mut line = x_range
27            .clone()
28            .map(|x| options.tileset.char_at(goban, x, y))
29            .collect();
30        if options.label_sides.contains(BoardSide::West) {
31            line = format!("{: >2} {}", y + 1, line);
32        }
33        if options.label_sides.contains(BoardSide::East) {
34            line.push_str(&format!(" {}", y + 1));
35        }
36        lines.push(line);
37    }
38    if options.label_sides.contains(BoardSide::South) {
39        let line: String = x_range.clone().map(board_label_text).collect();
40        lines.push(format!("{label_padding}{line}"));
41    }
42    Ok(lines.join("\n"))
43}
44
45#[derive(Debug, Clone)]
46pub struct TileSet {
47    tiles: [char; 11],
48}
49
50impl TileSet {
51    fn char_at(&self, goban: &Goban, x: u8, y: u8) -> char {
52        let max_x = goban.size().0 - 1;
53        let max_y = goban.size().1 - 1;
54        match goban.stone_color(x, y) {
55            Some(StoneColor::White) => self.tiles[0],
56            Some(StoneColor::Black) => self.tiles[1],
57            None => match (x, y) {
58                (0, 0) => self.tiles[2],
59                (x, 0) if x == max_x => self.tiles[3],
60                (0, y) if y == max_y => self.tiles[4],
61                (x, y) if x == max_x && y == max_y => self.tiles[5],
62                (_, 0) => self.tiles[6],
63                (0, _) => self.tiles[7],
64                (_, y) if y == max_y => self.tiles[8],
65                (x, _) if x == max_x => self.tiles[9],
66                (_, _) => self.tiles[10],
67            },
68        }
69    }
70}
71
72impl std::str::FromStr for TileSet {
73    type Err = UsageError;
74
75    fn from_str(s: &str) -> Result<Self, Self::Err> {
76        use std::convert::TryInto;
77
78        let tiles: [char; 11] = s
79            .chars()
80            .collect::<Vec<_>>()
81            .try_into()
82            .map_err(|_| UsageError::InvalidTileSet)?;
83        Ok(TileSet { tiles })
84    }
85}
86
87impl Default for TileSet {
88    fn default() -> Self {
89        "●○┏┓┗┛┯┠┷┨┼".parse().unwrap()
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use std::path::PathBuf;
96
97    use crate::render::GobanRange;
98    use crate::{Goban, RenderOptions};
99
100    use super::render;
101
102    fn build_diagram(sgf_dir: &str, options: &RenderOptions) -> String {
103        let d: PathBuf = [
104            env!("CARGO_MANIFEST_DIR"),
105            "tests",
106            "data",
107            sgf_dir,
108            "input.sgf",
109        ]
110        .iter()
111        .collect();
112        let sgf = std::fs::read_to_string(d).unwrap();
113        let goban = Goban::from_sgf(&sgf, &options.node_description, true).unwrap();
114        render(&goban, &options).unwrap()
115    }
116
117    #[test]
118    fn full_board() {
119        let options = RenderOptions::default();
120        let diagram = build_diagram("last_move", &options);
121        let expected = "\
122┏┯┯┯┯┯┯┯┯┯┯○●●●●┯┯┓
123┠┼┼┼┼┼┼┼┼┼┼○○○●○●┼┨
124┠┼┼┼○○●○○○┼┼○●●○●●┨
125○○○○○●○○●┼○┼┼○●○○○●
126○●●●○●●●┼┼┼┼○┼┼┼○●┨
127●┼●○┼┼┼┼○┼┼┼●○┼●●┼●
128┠●●○┼┼○○┼●○○○●●┼┼●┨
129┠●○○○○●○┼○●●●○┼┼┼┼┨
130┠●○●┼○●○○○○○●●●●●┼┨
131┠┼●●●┼●○●●●●○○○○○●┨
132┠┼┼●○┼●●○┼┼○┼┼┼┼┼●┨
133┠┼●┼●○┼○┼○○┼┼┼┼┼○●┨
134┠●●●○○┼○○┼●○┼┼○┼○●┨
135┠●○○○●●●●●●○┼┼○●●┼┨
136○●●○●┼●○○○●●○┼┼○●┼┨
137┠○○●●●┼●●○○○○○○┼●┼┨
138┠○┼○┼●●●○┼┼●●○●●┼┼┨
139┠┼○○●┼┼●○┼○●┼●┼┼┼┼┨
140┗┷┷┷┷┷┷┷┷┷○┷●┷┷┷┷┷┛";
141        assert_eq!(diagram, expected);
142    }
143
144    #[test]
145    fn labels() {
146        let mut options = RenderOptions::default();
147        options.label_sides = "nw".parse().unwrap();
148        let diagram = build_diagram("last_move", &options);
149        let expected = "   ABCDEFGHJKLMNOPQRST
150 1 ┏┯┯┯┯┯┯┯┯┯┯○●●●●┯┯┓
151 2 ┠┼┼┼┼┼┼┼┼┼┼○○○●○●┼┨
152 3 ┠┼┼┼○○●○○○┼┼○●●○●●┨
153 4 ○○○○○●○○●┼○┼┼○●○○○●
154 5 ○●●●○●●●┼┼┼┼○┼┼┼○●┨
155 6 ●┼●○┼┼┼┼○┼┼┼●○┼●●┼●
156 7 ┠●●○┼┼○○┼●○○○●●┼┼●┨
157 8 ┠●○○○○●○┼○●●●○┼┼┼┼┨
158 9 ┠●○●┼○●○○○○○●●●●●┼┨
15910 ┠┼●●●┼●○●●●●○○○○○●┨
16011 ┠┼┼●○┼●●○┼┼○┼┼┼┼┼●┨
16112 ┠┼●┼●○┼○┼○○┼┼┼┼┼○●┨
16213 ┠●●●○○┼○○┼●○┼┼○┼○●┨
16314 ┠●○○○●●●●●●○┼┼○●●┼┨
16415 ○●●○●┼●○○○●●○┼┼○●┼┨
16516 ┠○○●●●┼●●○○○○○○┼●┼┨
16617 ┠○┼○┼●●●○┼┼●●○●●┼┼┨
16718 ┠┼○○●┼┼●○┼○●┼●┼┼┼┼┨
16819 ┗┷┷┷┷┷┷┷┷┷○┷●┷┷┷┷┷┛";
169        assert_eq!(diagram, expected);
170    }
171
172    #[test]
173    fn range() {
174        let mut options = RenderOptions::default();
175        options.goban_range = GobanRange::Ranged(1..7, 0..5);
176        let diagram = build_diagram("prob45", &options);
177        let expected = "\
178┯○○●●┯
179○┼○○●┼
180┼○●●●┼
181○○●┼┼┼
182●●┼┼┼┼";
183        assert_eq!(diagram, expected);
184    }
185
186    #[test]
187    fn range_with_labels() {
188        let mut options = RenderOptions::default();
189        options.label_sides = "nwes".parse().unwrap();
190        options.goban_range = GobanRange::Ranged(1..7, 0..5);
191        let diagram = build_diagram("prob45", &options);
192        println!("{}", diagram);
193        let expected = "   BCDEFG
194 1 ┯○○●●┯ 1
195 2 ○┼○○●┼ 2
196 3 ┼○●●●┼ 3
197 4 ○○●┼┼┼ 4
198 5 ●●┼┼┼┼ 5
199   BCDEFG";
200        assert_eq!(diagram, expected);
201    }
202
203    #[test]
204    fn shrink_wrap() {
205        let mut options = RenderOptions::default();
206        options.goban_range = GobanRange::ShrinkWrap;
207        let diagram = build_diagram("prob45", &options);
208        let expected = "\
209┏┯○○●●┯
210○○┼○○●┼
211●┼○●●●┼
212●○○●┼┼┼
213┠●●┼┼┼┼
214┠┼┼┼┼┼┼";
215        assert_eq!(diagram, expected);
216    }
217
218    #[test]
219    fn tileset() {
220        let mut options = RenderOptions::default();
221        options.goban_range = GobanRange::ShrinkWrap;
222        options.tileset = "OX++++-|-|.".parse().unwrap();
223        let diagram = build_diagram("prob45", &options);
224        let expected = "\
225+-XXOO-
226XX.XXO.
227O.XOOO.
228OXXO...
229|OO....
230|......";
231        println!("{}", diagram);
232        assert_eq!(diagram, expected);
233    }
234}