sgf_render/render/
text.rs1use 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}