use clap::{Parser, Subcommand, ValueEnum};
use knossos::Color;
use knossos::maze::{self, formatters};
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum Algorithm {
AldousBroder,
BinaryTree,
Eller,
GrowingTree,
HuntAndKill,
Kruskal,
Prim,
RecursiveBacktracking,
RecursiveDivision,
Sidewinder,
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum)]
enum AsciiOutputType {
Narrow,
Broad,
}
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Debug, Subcommand)]
enum Commands {
Generate {
#[command(subcommand)]
output: OutputCommands,
#[arg(short = 'A', long, value_enum, default_value_t = Algorithm::RecursiveBacktracking)]
algorithm: Algorithm,
#[arg(short = 'H', long, default_value_t = 10)]
height: usize,
#[arg(short = 'W', long, default_value_t = 10)]
width: usize,
#[arg(long)]
seed: Option<u64>,
#[arg(
long,
default_value_t = maze::Bias::NorthEast,
require_equals = true,
num_args = 0..=1,
default_missing_value = "north-east",
value_enum,
)]
bias: maze::Bias,
#[arg(
long,
default_value_t = maze::Method::Newest,
require_equals = true,
num_args = 0..=1,
default_missing_value = "newest",
value_enum,
)]
growing_method: maze::Method,
},
}
#[derive(Debug, Subcommand)]
enum OutputCommands {
Ascii {
#[arg(short = 'O', long)]
output_path: String,
#[arg(
short = 'T',
long,
value_enum,
default_value_t = AsciiOutputType::Narrow,
require_equals = true,
num_args = 0..=1,
default_missing_value = "narrow",
)]
output_type: AsciiOutputType,
},
GameMap {
#[arg(short = 'O', long)]
output_path: String,
#[arg(long, default_value_t = 3)]
span: usize,
#[arg(long, default_value_t = '.')]
passage: char,
#[arg(long, default_value_t = '#')]
wall: char,
#[arg(long, default_value_t = false)]
with_start_goal: bool,
},
Image {
#[arg(short = 'O', long)]
output_path: String,
#[arg(long = "wall-size", default_value_t = 40)]
wall_size: usize,
#[arg(long = "passage-size", default_value_t = 40)]
passage_size: usize,
#[arg(long, default_value_t = 50)]
margin: usize,
#[arg(long = "passage-color", default_value = "#ffffff", value_parser = hex_to_rgb)]
passage_color: Color,
#[arg(long = "wall-color", default_value = "#000000", value_parser = hex_to_rgb)]
wall_color: Color,
},
}
fn main() -> Result<(), maze::MazeSaveError> {
let args = Cli::parse();
match args.command {
Commands::Generate {
output,
algorithm,
height,
width,
seed,
bias,
growing_method,
} => {
let algorithm: Box<dyn maze::Algorithm> = match algorithm {
Algorithm::AldousBroder => Box::new(maze::AldousBroder),
Algorithm::BinaryTree => Box::new(maze::BinaryTree::new(bias)),
Algorithm::Eller => Box::new(maze::Eller),
Algorithm::GrowingTree => Box::new(maze::GrowingTree::new(growing_method)),
Algorithm::HuntAndKill => Box::new(maze::HuntAndKill::new()),
Algorithm::Kruskal => Box::new(maze::Kruskal),
Algorithm::Prim => Box::new(maze::Prim::new()),
Algorithm::RecursiveBacktracking => Box::new(maze::RecursiveBacktracking),
Algorithm::RecursiveDivision => Box::new(maze::RecursiveDivision),
Algorithm::Sidewinder => Box::new(maze::Sidewinder),
};
let maze = maze::OrthogonalMazeBuilder::new()
.height(height)
.width(width)
.seed(seed)
.algorithm(algorithm)
.build();
let result;
match output {
OutputCommands::Ascii {
output_path,
output_type,
} => {
match output_type {
AsciiOutputType::Narrow => {
result = maze.save(output_path.as_str(), formatters::AsciiNarrow)
}
AsciiOutputType::Broad => {
result = maze.save(output_path.as_str(), formatters::AsciiBroad)
}
};
}
OutputCommands::GameMap {
output_path,
span,
passage,
wall,
with_start_goal,
} => {
result = match with_start_goal {
true => maze.save(
output_path.as_str(),
maze::GameMap::new()
.span(span)
.passage(passage)
.wall(wall)
.with_start_goal(),
),
false => maze.save(
output_path.as_str(),
maze::GameMap::new().span(span).passage(passage).wall(wall),
),
};
}
OutputCommands::Image {
output_path,
wall_size,
passage_size,
margin,
passage_color,
wall_color,
} => {
result = maze.save(
output_path.as_str(),
maze::Image::new()
.wall(wall_size)
.passage(passage_size)
.margin(margin)
.background(passage_color)
.foreground(wall_color),
);
}
};
match result {
Ok(msg) => {
println!("{}", msg);
Ok(())
}
Err(err) => Err(err),
}
}
}
}
fn hex_to_rgb(s: &str) -> Result<Color, ParseHexError> {
let s = if let Some(hex) = s.strip_prefix('#') {
hex
} else {
s
};
if s.len() != 6 {
return Err(ParseHexError::Length(s.to_string()));
}
Ok(Color::RGB(
u8::from_str_radix(&s[..2], 16)?,
u8::from_str_radix(&s[2..4], 16)?,
u8::from_str_radix(&s[4..6], 16)?,
))
}
#[derive(Debug)]
enum ParseHexError {
IntError(std::num::ParseIntError),
Length(String),
}
impl std::error::Error for ParseHexError {}
impl std::fmt::Display for ParseHexError {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
ParseHexError::Length(e) => write!(
f,
"Expected a 6 character color value in hex, but got: {:?}",
e
),
ParseHexError::IntError(ref e) => e.fmt(f),
}
}
}
impl From<std::num::ParseIntError> for ParseHexError {
fn from(err: std::num::ParseIntError) -> ParseHexError {
ParseHexError::IntError(err)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn verify_cli() {
use clap::CommandFactory;
Cli::command().debug_assert()
}
}