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)?;
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,
}