Initial commit; parse game actions

Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
Maxime “pep” Buquet 2022-08-07 15:36:35 +02:00
commit f51c9fe6da
7 changed files with 525 additions and 0 deletions

1
.gitignore vendored Normal file
View file

@ -0,0 +1 @@
/target

12
Cargo.toml Normal file
View file

@ -0,0 +1,12 @@
[package]
name = "hanabi-repl"
version = "0.1.0"
edition = "2021"
license = "AGPL-3.0-or-later"
authors = ["Maxime “pep” Buquet <pep@bouah.net>"]
description = "Keep track of your Hanabi plays"
[dependencies]
clap = { version = "3.2.16", features = ["derive"] }
nom = "7.1.1"
rustyline = "10.0.0"

30
src/args.rs Normal file
View file

@ -0,0 +1,30 @@
// Copyright (C) 2022 Maxime “pep” Buquet <pep@bouah.net>
//
// 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 <https://www.gnu.org/licenses/>.
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,
}

63
src/error.rs Normal file
View file

@ -0,0 +1,63 @@
// Copyright (C) 2022 Maxime “pep” Buquet <pep@bouah.net>
//
// 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 <https://www.gnu.org/licenses/>.
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<nom::error::Error<String>>),
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<nom::Err<nom::error::Error<&'a str>>> for Error {
fn from(err: nom::Err<nom::error::Error<&'a str>>) -> 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<ReadlineError> for Error {
fn from(err: ReadlineError) -> Error {
Error::ReadlineError(err)
}
}

61
src/main.rs Normal file
View file

@ -0,0 +1,61 @@
// Copyright (C) 2022 Maxime “pep” Buquet <pep@bouah.net>
//
// 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 <https://www.gnu.org/licenses/>.
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(())
}

129
src/parser.rs Normal file
View file

@ -0,0 +1,129 @@
// Copyright (C) 2022 Maxime “pep” Buquet <pep@bouah.net>
//
// 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 <https://www.gnu.org/licenses/>.
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::<u8>()
.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::<u8>()
.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<u8>> {
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<Action, Error> {
Ok(parse_action(input)?.1)
}

229
src/types.rs Normal file
View file

@ -0,0 +1,229 @@
// Copyright (C) 2022 Maxime “pep” Buquet <pep@bouah.net>
//
// 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 <https://www.gnu.org/licenses/>.
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<Digit, Error> {
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<Color, Error> {
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<Color> for ColorDigit {
fn from(c: Color) -> Self {
ColorDigit::Color(c)
}
}
impl From<Digit> for ColorDigit {
fn from(d: Digit) -> Self {
ColorDigit::Digit(d)
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct Card {
pub digit: Digit,
pub color: Option<Color>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Slot(u8);
impl Deref for Slot {
type Target = u8;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<u8> 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<Slot>);
impl Deref for Slots {
type Target = Vec<Slot>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl From<Vec<u8>> for Slots {
fn from(s: Vec<u8>) -> Slots {
Slots(s.into_iter().map(Slot).collect())
}
}
impl From<Vec<Slot>> for Slots {
fn from(s: Vec<Slot>) -> 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<Slot>, Color),
DigitHint(Vec<Slot>, Digit),
}
impl From<(ColorDigit, Vec<Slot>)> for Action {
fn from((cd, slots): (ColorDigit, Vec<Slot>)) -> Self {
match cd {
ColorDigit::Color(c) => Action::ColorHint(slots, c),
ColorDigit::Digit(d) => Action::DigitHint(slots, d),
}
}
}