commit f51c9fe6da47c5cf8f7ce638a19b1fc9290be95b Author: Maxime “pep” Buquet Date: Sun Aug 7 15:36:35 2022 +0200 Initial commit; parse game actions Signed-off-by: Maxime “pep” Buquet diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ea8c4bf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/target diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..540caa0 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "hanabi-repl" +version = "0.1.0" +edition = "2021" +license = "AGPL-3.0-or-later" +authors = ["Maxime “pep” Buquet "] +description = "Keep track of your Hanabi plays" + +[dependencies] +clap = { version = "3.2.16", features = ["derive"] } +nom = "7.1.1" +rustyline = "10.0.0" diff --git a/src/args.rs b/src/args.rs new file mode 100644 index 0000000..74bea13 --- /dev/null +++ b/src/args.rs @@ -0,0 +1,30 @@ +// Copyright (C) 2022 Maxime “pep” Buquet +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +// for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::types::Variant; +use clap::Parser; +use std::default::Default; + +#[derive(Parser, Debug)] +#[clap(author, version, about, long_about = None)] +pub struct Args { + /// Number of players + #[clap(short, long, value_parser = clap::value_parser!(u8).range(2..=5), default_value_t = 2u8)] + pub players: u8, + + /// Game variant. Unused + #[clap(arg_enum, short, long, default_value_t = Default::default())] + pub variant: Variant, +} diff --git a/src/error.rs b/src/error.rs new file mode 100644 index 0000000..077599e --- /dev/null +++ b/src/error.rs @@ -0,0 +1,63 @@ +// Copyright (C) 2022 Maxime “pep” Buquet +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +// for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use nom; +use rustyline::error::ReadlineError; +use std::error::Error as StdError; +use std::fmt; + +#[derive(Debug)] +pub enum Error { + ParseColorError(String), + ParseDigitError(String), + NomError(nom::Err>), + ReadlineError(ReadlineError), +} + +impl StdError for Error {} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Error::ParseDigitError(err) => write!(f, "Parse digit error: {}", err), + Error::ParseColorError(err) => write!(f, "Parse color error: {}", err), + Error::NomError(err) => write!(f, "Nom error: {}", err), + Error::ReadlineError(err) => write!(f, "Readline error: {}", err), + } + } +} + +impl<'a> From>> for Error { + fn from(err: nom::Err>) -> Error { + let foo = match err { + nom::Err::Incomplete(needed) => nom::Err::Incomplete(needed), + nom::Err::Error(error) => nom::Err::Error(nom::error::Error::new( + String::from(error.input), + error.code, + )), + nom::Err::Failure(error) => nom::Err::Failure(nom::error::Error::new( + String::from(error.input), + error.code, + )), + }; + Error::NomError(foo) + } +} + +impl From for Error { + fn from(err: ReadlineError) -> Error { + Error::ReadlineError(err) + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..f18c1f9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,61 @@ +// Copyright (C) 2022 Maxime “pep” Buquet +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +// for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +mod args; +mod error; +mod parser; +mod types; + +use crate::args::Args; +use crate::error::Error; +use crate::parser::parse_line; +use crate::types::Action; + +use clap::Parser; +use rustyline::{error::ReadlineError, Editor}; + +fn main() -> Result<(), Error> { + let args = Args::parse(); + println!( + "Hanabi - Players: {} - Variants: {}", + args.players, args.variant + ); + + + let mut rl = Editor::<()>::new()?; + + loop { + let readline = rl.readline(">> "); + match readline { + Ok(line) => { + rl.add_history_entry(line.as_str()); + if let Ok(action) = parse_line(line.as_str()) { + println!("Action: {:?}", action); + } else { + println!("Invalid command: {}", line); + } + } + Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => { + println!("Quitting."); + break; + } + Err(err) => { + println!("Error: {:?}", err); + break; + } + } + } + Ok(()) +} diff --git a/src/parser.rs b/src/parser.rs new file mode 100644 index 0000000..6292797 --- /dev/null +++ b/src/parser.rs @@ -0,0 +1,129 @@ +// Copyright (C) 2022 Maxime “pep” Buquet +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +// for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::error::Error; +use crate::types::{Action, Color, ColorDigit, Digit}; +use nom::{ + branch::alt, + bytes::complete::tag, + character::complete::{digit1, space1}, + error::{Error as NomError, ErrorKind}, + multi::many0, + IResult, +}; +use std::str::FromStr; + +fn parse_color(i: &str) -> IResult<&str, ColorDigit> { + let (i, color) = alt(( + tag("blue"), + tag("green"), + tag("purple"), + tag("red"), + tag("white"), + tag("yellow"), + ))(i)?; + let color = Color::from_str(color).unwrap(); + Ok((i, color.into())) +} + +fn parse_digit(i: &str) -> IResult<&str, ColorDigit> { + let (i, digit) = alt(( + tag("1"), + tag("one"), + tag("2"), + tag("two"), + tag("3"), + tag("three"), + tag("4"), + tag("four"), + tag("5"), + tag("five"), + ))(i)?; + let digit = Digit::from_str(digit).unwrap(); + Ok((i, digit.into())) +} + +fn parse_play(i: &str) -> IResult<&str, Action> { + let (i, _) = alt((tag("play"), tag("p")))(i)?; + let (i, _) = space1(i)?; + let (i, slot) = digit1(i)?; + + let slot = slot + .parse::() + .map_err(|_| nom::Err::Error(NomError::new("", ErrorKind::IsA)))?; + + if slot < 1 || slot > 6 { + return Err(nom::Err::Error(NomError::new("", ErrorKind::IsA))); + } + + Ok((i, Action::PlayCard(slot))) +} + +fn parse_slot(i: &str) -> IResult<&str, u8> { + let (i, slot) = digit1(i)?; + let slot = slot + .parse::() + .map_err(|_| nom::Err::Error(NomError::new("", ErrorKind::IsA)))?; + + if slot < 1 || slot > 6 { + return Err(nom::Err::Error(NomError::new("", ErrorKind::IsA))); + } + + Ok((i, slot)) +} + +fn parse_slot1(i: &str) -> IResult<&str, u8> { + let (i, _) = space1(i)?; + let (i, slot) = parse_slot(i)?; + + Ok((i, slot)) +} + +fn parse_slots1(i: &str) -> IResult<&str, Vec> { + let (i, slot1) = parse_slot(i)?; + let (i, slots2) = many0(parse_slot1)(i)?; + + let mut slots = vec![slot1]; + slots.extend(slots2); + + Ok((i, slots)) +} + +fn parse_drop(i: &str) -> IResult<&str, Action> { + let (i, _) = alt((tag("drop"), tag("d")))(i)?; + let (i, _) = space1(i)?; + let (i, slot) = parse_slot(i)?; + + Ok((i, Action::DropCard(slot))) +} + +fn parse_hint(i: &str) -> IResult<&str, Action> { + let (i, _) = alt((tag("hint"), tag("h")))(i)?; + let (i, _) = space1(i)?; + let (i, colordigit) = alt((parse_color, parse_digit))(i)?; + let (i, _) = space1(i)?; + let (i, slots) = parse_slots1(i)?; + + Ok((i, Action::from((colordigit, slots)))) +} + +fn parse_action(input: &str) -> IResult<&str, Action> { + let (i, action) = alt((parse_play, parse_drop, parse_hint))(input)?; + Ok((i, action)) +} + +pub fn parse_line(input: &str) -> Result { + Ok(parse_action(input)?.1) +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..7f021ba --- /dev/null +++ b/src/types.rs @@ -0,0 +1,229 @@ +// Copyright (C) 2022 Maxime “pep” Buquet +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU Affero General Public License as published by the +// Free Software Foundation, either version 3 of the License, or (at your +// option) any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License +// for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use std::fmt; +use std::str::FromStr; + +use crate::error::Error; + +use clap::ArgEnum; + +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord, ArgEnum)] +pub enum Variant { + #[default] + Default, + Multicolor, +} + +impl fmt::Display for Variant { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + match self { + Variant::Default => "default", + Variant::Multicolor => "multicolor", + } + ) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Digit { + One, + Two, + Three, + Four, + Five, +} + +impl FromStr for Digit { + type Err = Error; + + fn from_str(s: &str) -> Result { + let s = s.trim().to_lowercase(); + Ok(match s.as_str() { + "1" | "one" => Digit::One, + "2" | "two" => Digit::Two, + "3" | "three" => Digit::Three, + "4" | "four" => Digit::Four, + "5" | "five" => Digit::Five, + _ => return Err(Error::ParseDigitError(s.clone())), + }) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Color { + Blue, + Green, + Purple, + Red, + White, + Yellow, +} + +impl FromStr for Color { + type Err = Error; + + fn from_str(s: &str) -> Result { + let s = s.trim().to_lowercase(); + Ok(match s.as_str() { + "blue" => Color::Blue, + "green" => Color::Green, + "purple" => Color::Purple, + "red" => Color::Red, + "white" => Color::White, + "yellow" => Color::Yellow, + _ => return Err(Error::ParseColorError(s.clone())), + }) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ColorDigit { + Color(Color), + Digit(Digit), +} + +impl From for ColorDigit { + fn from(c: Color) -> Self { + ColorDigit::Color(c) + } +} + +impl From for ColorDigit { + fn from(d: Digit) -> Self { + ColorDigit::Digit(d) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Card { + pub digit: Digit, + pub color: Option, +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Slot(u8); + +impl Deref for Slot { + type Target = u8; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From for Slot { + fn from(s: u8) -> Slot { + Slot(s) + } +} + +impl fmt::Display for Slot { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, " {}", *self) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub struct Slots(Vec); + +impl Deref for Slots { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl From> for Slots { + fn from(s: Vec) -> Slots { + Slots(s.into_iter().map(Slot).collect()) + } +} + +impl From> for Slots { + fn from(s: Vec) -> Slots { + Slots(s) + } +} + +impl fmt::Display for Slots { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.len() > 0 { + write!(f, "{}", self)?; + } + + if self.len() > 1 { + for slot in self.iter() { + write!(f, " {}", slot)?; + } + } + + Ok(()) + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum GameAction { + PlayCard(Slot), + DropCard(Slot), + ColorHint(Slots, Color), + DigitHint(Slots, Digit), +} + +impl From<(ColorDigit, Slots)> for GameAction { + fn from((cd, slots): (ColorDigit, Slots)) -> Self { + match cd { + ColorDigit::Color(c) => GameAction::ColorHint(slots, c), + ColorDigit::Digit(d) => GameAction::DigitHint(slots, d), + } + } +} + +impl fmt::Display for GameAction { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + GameAction::PlayCard(c) => write!(f, "play {}", c), + GameAction::DropCard(c) => write!(f, "drop {}", c), + GameAction::ColorHint(slots, c) => write!(f, "hint {} {}", c, slots), + GameAction::DigitHint(slots, d) => write!(f, "hint {} {}", d, slots), + } + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum ReplAction { + CancelLast, +} + +#[derive(Clone, Debug, PartialEq)] +pub enum Action { + PlayCard(Slot), + DropCard(Slot), + ColorHint(Vec, Color), + DigitHint(Vec, Digit), +} + +impl From<(ColorDigit, Vec)> for Action { + fn from((cd, slots): (ColorDigit, Vec)) -> Self { + match cd { + ColorDigit::Color(c) => Action::ColorHint(slots, c), + ColorDigit::Digit(d) => Action::DigitHint(slots, d), + } + } +}