sgf-render 3.3.1

CLI to generate diagrams of Go games from SGF game records
Documentation
use super::options::BoardSide;
use super::{board_label_text, RenderOptions};

use crate::errors::{GobanError, UsageError};
use crate::goban::StoneColor;
use crate::Goban;

pub fn render(goban: &Goban, options: &RenderOptions) -> Result<String, GobanError> {
    let (x_range, y_range) = options.goban_range.get_ranges(goban, options)?;
    let width = x_range.end - x_range.start;
    let height = y_range.end - y_range.start;
    if !options.label_sides.is_empty() && width > 25 || height > 99 {
        return Err(GobanError::UnlabellableRange);
    }
    let mut lines: Vec<String> = vec![];
    let label_padding = if options.label_sides.contains(BoardSide::West) {
        "   "
    } else {
        ""
    };
    if options.label_sides.contains(BoardSide::North) {
        let line: String = x_range.clone().map(board_label_text).collect();
        lines.push(format!("{label_padding}{line}"));
    }
    for y in y_range {
        let mut line = x_range
            .clone()
            .map(|x| options.tileset.char_at(goban, x, y))
            .collect();
        if options.label_sides.contains(BoardSide::West) {
            line = format!("{: >2} {}", y + 1, line);
        }
        if options.label_sides.contains(BoardSide::East) {
            line.push_str(&format!(" {}", y + 1));
        }
        lines.push(line);
    }
    if options.label_sides.contains(BoardSide::South) {
        let line: String = x_range.clone().map(board_label_text).collect();
        lines.push(format!("{label_padding}{line}"));
    }
    Ok(lines.join("\n"))
}

#[derive(Debug, Clone)]
pub struct TileSet {
    tiles: [char; 11],
}

impl TileSet {
    fn char_at(&self, goban: &Goban, x: u8, y: u8) -> char {
        let max_x = goban.size().0 - 1;
        let max_y = goban.size().1 - 1;
        match goban.stone_color(x, y) {
            Some(StoneColor::White) => self.tiles[0],
            Some(StoneColor::Black) => self.tiles[1],
            None => match (x, y) {
                (0, 0) => self.tiles[2],
                (x, 0) if x == max_x => self.tiles[3],
                (0, y) if y == max_y => self.tiles[4],
                (x, y) if x == max_x && y == max_y => self.tiles[5],
                (_, 0) => self.tiles[6],
                (0, _) => self.tiles[7],
                (_, y) if y == max_y => self.tiles[8],
                (x, _) if x == max_x => self.tiles[9],
                (_, _) => self.tiles[10],
            },
        }
    }
}

impl std::str::FromStr for TileSet {
    type Err = UsageError;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use std::convert::TryInto;

        let tiles: [char; 11] = s
            .chars()
            .collect::<Vec<_>>()
            .try_into()
            .map_err(|_| UsageError::InvalidTileSet)?;
        Ok(TileSet { tiles })
    }
}

impl Default for TileSet {
    fn default() -> Self {
        "●○┏┓┗┛┯┠┷┨┼".parse().unwrap()
    }
}

#[cfg(test)]
mod tests {
    use std::path::PathBuf;

    use crate::render::GobanRange;
    use crate::{Goban, RenderOptions};

    use super::render;

    fn build_diagram(sgf_dir: &str, options: &RenderOptions) -> String {
        let d: PathBuf = [
            env!("CARGO_MANIFEST_DIR"),
            "tests",
            "data",
            sgf_dir,
            "input.sgf",
        ]
        .iter()
        .collect();
        let sgf = std::fs::read_to_string(d).unwrap();
        let goban = Goban::from_sgf(&sgf, &options.node_description, true).unwrap();
        render(&goban, &options).unwrap()
    }

    #[test]
    fn full_board() {
        let options = RenderOptions::default();
        let diagram = build_diagram("last_move", &options);
        let expected = "\
┏┯┯┯┯┯┯┯┯┯┯○●●●●┯┯┓
┠┼┼┼┼┼┼┼┼┼┼○○○●○●┼┨
┠┼┼┼○○●○○○┼┼○●●○●●┨
○○○○○●○○●┼○┼┼○●○○○●
○●●●○●●●┼┼┼┼○┼┼┼○●┨
●┼●○┼┼┼┼○┼┼┼●○┼●●┼●
┠●●○┼┼○○┼●○○○●●┼┼●┨
┠●○○○○●○┼○●●●○┼┼┼┼┨
┠●○●┼○●○○○○○●●●●●┼┨
┠┼●●●┼●○●●●●○○○○○●┨
┠┼┼●○┼●●○┼┼○┼┼┼┼┼●┨
┠┼●┼●○┼○┼○○┼┼┼┼┼○●┨
┠●●●○○┼○○┼●○┼┼○┼○●┨
┠●○○○●●●●●●○┼┼○●●┼┨
○●●○●┼●○○○●●○┼┼○●┼┨
┠○○●●●┼●●○○○○○○┼●┼┨
┠○┼○┼●●●○┼┼●●○●●┼┼┨
┠┼○○●┼┼●○┼○●┼●┼┼┼┼┨
┗┷┷┷┷┷┷┷┷┷○┷●┷┷┷┷┷┛";
        assert_eq!(diagram, expected);
    }

    #[test]
    fn labels() {
        let mut options = RenderOptions::default();
        options.label_sides = "nw".parse().unwrap();
        let diagram = build_diagram("last_move", &options);
        let expected = "   ABCDEFGHJKLMNOPQRST
 1 ┏┯┯┯┯┯┯┯┯┯┯○●●●●┯┯┓
 2 ┠┼┼┼┼┼┼┼┼┼┼○○○●○●┼┨
 3 ┠┼┼┼○○●○○○┼┼○●●○●●┨
 4 ○○○○○●○○●┼○┼┼○●○○○●
 5 ○●●●○●●●┼┼┼┼○┼┼┼○●┨
 6 ●┼●○┼┼┼┼○┼┼┼●○┼●●┼●
 7 ┠●●○┼┼○○┼●○○○●●┼┼●┨
 8 ┠●○○○○●○┼○●●●○┼┼┼┼┨
 9 ┠●○●┼○●○○○○○●●●●●┼┨
10 ┠┼●●●┼●○●●●●○○○○○●┨
11 ┠┼┼●○┼●●○┼┼○┼┼┼┼┼●┨
12 ┠┼●┼●○┼○┼○○┼┼┼┼┼○●┨
13 ┠●●●○○┼○○┼●○┼┼○┼○●┨
14 ┠●○○○●●●●●●○┼┼○●●┼┨
15 ○●●○●┼●○○○●●○┼┼○●┼┨
16 ┠○○●●●┼●●○○○○○○┼●┼┨
17 ┠○┼○┼●●●○┼┼●●○●●┼┼┨
18 ┠┼○○●┼┼●○┼○●┼●┼┼┼┼┨
19 ┗┷┷┷┷┷┷┷┷┷○┷●┷┷┷┷┷┛";
        assert_eq!(diagram, expected);
    }

    #[test]
    fn range() {
        let mut options = RenderOptions::default();
        options.goban_range = GobanRange::Ranged(1..7, 0..5);
        let diagram = build_diagram("prob45", &options);
        let expected = "\
┯○○●●┯
○┼○○●┼
┼○●●●┼
○○●┼┼┼
●●┼┼┼┼";
        assert_eq!(diagram, expected);
    }

    #[test]
    fn range_with_labels() {
        let mut options = RenderOptions::default();
        options.label_sides = "nwes".parse().unwrap();
        options.goban_range = GobanRange::Ranged(1..7, 0..5);
        let diagram = build_diagram("prob45", &options);
        println!("{}", diagram);
        let expected = "   BCDEFG
 1 ┯○○●●┯ 1
 2 ○┼○○●┼ 2
 3 ┼○●●●┼ 3
 4 ○○●┼┼┼ 4
 5 ●●┼┼┼┼ 5
   BCDEFG";
        assert_eq!(diagram, expected);
    }

    #[test]
    fn shrink_wrap() {
        let mut options = RenderOptions::default();
        options.goban_range = GobanRange::ShrinkWrap;
        let diagram = build_diagram("prob45", &options);
        let expected = "\
┏┯○○●●┯
○○┼○○●┼
●┼○●●●┼
●○○●┼┼┼
┠●●┼┼┼┼
┠┼┼┼┼┼┼";
        assert_eq!(diagram, expected);
    }

    #[test]
    fn tileset() {
        let mut options = RenderOptions::default();
        options.goban_range = GobanRange::ShrinkWrap;
        options.tileset = "OX++++-|-|.".parse().unwrap();
        let diagram = build_diagram("prob45", &options);
        let expected = "\
+-XXOO-
XX.XXO.
O.XOOO.
OXXO...
|OO....
|......";
        println!("{}", diagram);
        assert_eq!(diagram, expected);
    }
}