Initial commit; parse game actions
Signed-off-by: Maxime “pep” Buquet <pep@bouah.net>
This commit is contained in:
commit
f51c9fe6da
7 changed files with 525 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
/target
|
12
Cargo.toml
Normal file
12
Cargo.toml
Normal 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
30
src/args.rs
Normal 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
63
src/error.rs
Normal 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
61
src/main.rs
Normal 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
129
src/parser.rs
Normal 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
229
src/types.rs
Normal 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),
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue