nonogrid 0.7.3

Efficient nonogram solver
Documentation
use std::{
    fmt::Display,
    fs,
    io::{self, stdin, Read},
};

use self::{
    block::{binary::BinaryBlock, multicolor::ColoredBlock, Block},
    board::Board,
    cli::Params,
    parser::{BoardParser, NetworkReader, ParseError, PuzzleScheme},
    render::{Renderer, ShellRenderer},
    solver::{
        line::{DynamicColor, DynamicSolver},
        probing::FullProbe1,
    },
    utils::rc::MutRc,
};

mod block;
mod board;
mod cache;
mod parser;
mod render;
mod solver;
mod utils;

#[cfg(feature = "clap")]
mod cli {
    use std::str::FromStr;

    use clap::{
        crate_authors, crate_description, crate_name, crate_version, value_t, App, Arg, ArgMatches,
    };

    use super::{fs, read_stdin, ParseError, SearchOptions, Source};

    pub(super) struct Params<'a> {
        matches: ArgMatches<'a>,
    }

    impl Params<'_> {
        pub(super) fn new() -> Self {
            let matches = App::new(crate_name!())
                .version(crate_version!())
                .about(crate_description!())
                .author(crate_authors!())
                .arg(
                    Arg::with_name("INPUT")
                        .help("The nonogram file or puzzle ID to solve. When no input is present, read from the stdin.")
                        .index(1)
                )
                .arg(
                    Arg::with_name("webpbn").help("Solve puzzle from http://webpbn.com with specified ID")
                        .short("w").long("webpbn").requires("INPUT")
                )
                .arg(
                    Arg::with_name("nonograms-org").help("Solve puzzle from http://www.nonograms.org/ with specified ID")
                        .short("o").long("nonograms-org").requires("INPUT").conflicts_with("webpbn")
                )
                .args_from_usage(
                    "-m, --max-solutions=[THRESHOLD] 'Stop searching after finding enough solutions'
             -t, --timeout=[SECONDS] 'Stop searching after specified timeout in seconds'
             -d, --max-depth=[DEPTH] 'Stop searching after reaching specified search depth'",
                )
                .get_matches();

            Self { matches }
        }

        pub(super) fn get_content(&self) -> Result<(Source, String), ParseError> {
            let input_id = self.matches.value_of("INPUT");

            if self.matches.is_present("webpbn") {
                return Ok((
                    Source::WebPbn,
                    input_id
                        .expect("INPUT should be present in --webpbn mode")
                        .to_string(),
                ));
            }

            if self.matches.is_present("nonograms-org") {
                return Ok((
                    Source::NonogramsOrg,
                    input_id
                        .expect("INPUT should be present in --nonograms-org mode")
                        .to_string(),
                ));
            }

            let content = if let Some(input_file) = input_id {
                let raw = fs::read(input_file)?;
                // ignore non-unicode symbols
                String::from_utf8_lossy(&raw).into()
            } else {
                read_stdin()?
            };

            Ok((Source::LocalFile, content))
        }

        pub(super) fn get_search_options(&self) -> SearchOptions {
            (
                self.parse_arg("max-solutions"),
                self.parse_arg("timeout"),
                self.parse_arg("max-depth"),
            )
        }

        fn parse_arg<T>(&self, name: &str) -> Option<T>
        where
            T: FromStr,
        {
            let matches = &self.matches;
            if matches.is_present(name) {
                let value = value_t!(matches, name, T).unwrap_or_else(|e| e.exit());
                return Some(value);
            }

            None
        }
    }
}

#[cfg(not(feature = "clap"))]
mod cli {
    use std::env;

    use super::{fs, read_stdin, ParseError, SearchOptions, Source};

    pub(super) struct Params {
        file_name: Option<String>,
    }

    impl Params {
        pub(super) fn new() -> Self {
            let file_name = env::args().nth(1);
            Self { file_name }
        }

        pub(super) fn get_content(&self) -> Result<(Source, String), ParseError> {
            let content = if let Some(input_file) = &self.file_name {
                fs::read_to_string(input_file)?
            } else {
                read_stdin()?
            };
            Ok((Source::LocalFile, content))
        }

        #[allow(clippy::unused_self)]
        pub(super) const fn get_search_options(&self) -> SearchOptions {
            (None, None, None)
        }
    }
}

fn read_stdin() -> Result<String, io::Error> {
    log::warn!("Reading from stdin...");
    let mut buffer = String::new();
    let _ = stdin().read_to_string(&mut buffer)?;
    Ok(buffer)
}

fn main() -> Result<(), ParseError> {
    #[cfg(feature = "env_logger")]
    env_logger::init();

    let params = Params::new();
    let search_options = params.get_search_options();
    let (source, content) = params.get_content()?;

    match source {
        Source::LocalFile => run(
            &parser::DetectedParser::with_content(&content)?,
            search_options,
        ),
        Source::WebPbn => run(&parser::WebPbn::read_remote(&content)?, search_options),
        Source::NonogramsOrg => run(
            &parser::NonogramsOrg::read_remote(&content)?,
            search_options,
        ),
    };
    Ok(())
}

fn run<P>(board_parser: &P, search_options: SearchOptions)
where
    P: BoardParser,
{
    match board_parser.infer_scheme() {
        PuzzleScheme::BlackAndWhite => {
            run_with_block::<BinaryBlock, _>(board_parser, search_options)
        }
        PuzzleScheme::MultiColor => run_with_block::<ColoredBlock, _>(board_parser, search_options),
    }
}

fn run_with_block<B, P>(board_parser: &P, search_options: SearchOptions)
where
    B: 'static + Block + Display,
    B::Color: DynamicColor + Display,
    P: BoardParser,
{
    let board = {
        let mut board = board_parser.parse::<B>();
        board.reduce_colors();
        board
    };
    let board = MutRc::new(board);
    let r = ShellRenderer::with_board(MutRc::clone(&board));

    #[cfg(not(feature = "sat"))]
    {
        let backtracking = solver::run::<_, DynamicSolver<_>, FullProbe1<_>>(
            MutRc::clone(&board),
            search_options.0,
            search_options.1,
            search_options.2,
        )
        .unwrap();
        println!("{}", r.render());

        if let Some(backtracking) = backtracking {
            let solutions = backtracking.solutions;
            if !solutions.is_empty() && (!board.read().is_solved_full() || solutions.len() > 1) {
                println!("Backtracking found {} solutions:", solutions.len());
                for (i, solution) in solutions.into_iter().enumerate() {
                    if i > 0 {
                        let diff = board.read().diff(&solution);
                        assert!(!diff.is_empty());
                        println!("Diff with previous solution: {:?}", diff);
                    }
                    Board::restore_with_callback(&board, solution);
                    println!("{}-th solution:", i + 1);
                    println!("{}", r.render_simple());
                }
            }

            if log::log_enabled!(log::Level::Warn) {
                let search_tree = backtracking.search_tree.read();
                if !search_tree.is_empty() {
                    println!("Searching progress: {:?}", search_tree);
                }
            }
        }
    }

    #[cfg(feature = "sat")]
    {
        let sat_solutions = solver::run::<_, DynamicSolver<_>, FullProbe1<_>>(
            MutRc::clone(&board),
            search_options.0,
        )
        .unwrap();
        println!("{}", r.render());

        if let Some(solutions) = sat_solutions {
            let mut found = false;
            for (i, solution) in solutions.enumerate() {
                if i > 0 {
                    let diff = board.read().diff(&solution);
                    assert!(!diff.is_empty());
                    println!("Diff with previous solution: {:?}", diff);
                }

                Board::restore_with_callback(&board, solution);
                log::warn!("{}-th solution found!", i + 1);
                println!("{}-th solution:", i + 1);
                println!("{}", r.render_simple());
                found = true;
            }
            assert!(found, "Puzzle is unsatisfied");
        }
    }
}

type SearchOptions = (Option<usize>, Option<u32>, Option<usize>);

enum Source {
    LocalFile,
    #[allow(dead_code)]
    WebPbn,
    #[allow(dead_code)]
    NonogramsOrg,
}