Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Accept multiple items in buy and use commands #84

Merged
merged 7 commits into from
Aug 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
* Initial stats are randomized 50af983
* Use GitHub actions instead of travis for CI and release building #80
* Change xp gained based on enemy class category #83
* Accept multiple items in buy and use commands #84

### Fixed
* Find chest quest not rewarded when finding a tombstone c0d62aa
Expand Down
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,5 @@ typetag = "0.1"
dunce = "1.0.1"
once_cell = "1.7.2"
serde_json = "1.0.64"
serde_yaml = "0.8"
serde_yaml = "0.8"
anyhow = "1.0"
149 changes: 57 additions & 92 deletions src/command.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use crate::character;
use crate::game;
use crate::game::Game;
use crate::item;
use crate::location::Location;
use crate::log;
use anyhow::{bail, Result};

use clap::Clap;

Expand Down Expand Up @@ -41,11 +41,11 @@ pub enum Command {
/// Buys an item from the shop.
/// If name is omitted lists the items available for sale.
#[clap(alias = "b", display_order = 2)]
Buy { item: Option<String> },
Buy { items: Vec<String> },

/// Uses an item from the inventory.
#[clap(alias = "u", display_order = 3)]
Use { item: Option<String> },
Use { items: Vec<String> },

/// Prints the quest todo list.
#[clap(alias = "t", display_order = 4)]
Expand Down Expand Up @@ -78,132 +78,97 @@ pub enum Command {
},
}

pub fn run(cmd: Option<Command>, game: &mut Game) -> i32 {
let mut exit_code = 0;

pub fn run(cmd: Option<Command>, game: &mut Game) -> Result<()> {
match cmd.unwrap_or(Command::Stat) {
Command::Stat => log::status(&game),
Command::ChangeDir {
destination,
run,
bribe,
force,
} => {
exit_code = change_dir(game, &destination, run, bribe, force);
}
Command::Inspect => {
game.inspect();
}
Command::Class { name } => class(game, &name),
Command::Battle { run, bribe } => {
exit_code = battle(game, run, bribe);
}
} => change_dir(game, &destination, run, bribe, force)?,
Command::Inspect => game.inspect(),
Command::Class { name } => class(game, &name)?,
Command::Battle { run, bribe } => battle(game, run, bribe)?,
Command::PrintWorkDir => println!("{}", game.location.path_string()),
Command::Reset { .. } => game.reset(),
Command::Buy { item } => exit_code = shop(game, &item),
Command::Use { item } => exit_code = use_item(game, &item),
Command::Buy { items } => shop(game, &items)?,
Command::Use { items } => use_item(game, &items)?,
Command::Todo => {
let (todo, done) = game.quests.list(&game);
log::quest_list(&todo, &done);
}
}
};

exit_code
Ok(())
}

/// Attempt to move the hero to the supplied location, possibly engaging
/// in combat along the way.
fn change_dir(game: &mut Game, dest: &str, run: bool, bribe: bool, force: bool) -> i32 {
if let Ok(dest) = Location::from(&dest) {
if force {
game.location = dest;
} else if let Err(character::Dead) = game.go_to(&dest, run, bribe) {
game.reset();
return 1;
}
} else {
println!("No such file or directory");
return 1;
fn change_dir(game: &mut Game, dest: &str, run: bool, bribe: bool, force: bool) -> Result<()> {
let dest = Location::from(&dest)?;
if force {
game.location = dest;
} else if let Err(character::Dead) = game.go_to(&dest, run, bribe) {
game.reset();
bail!("");
}
0
Ok(())
}

/// Potentially run a battle at the current location, independently from
/// the hero's movement.
fn battle(game: &mut Game, run: bool, bribe: bool) -> i32 {
let mut exit_code = 0;
fn battle(game: &mut Game, run: bool, bribe: bool) -> Result<()> {
if let Some(mut enemy) = game.maybe_spawn_enemy() {
if let Err(character::Dead) = game.maybe_battle(&mut enemy, run, bribe) {
game.reset();
exit_code = 1;
bail!("");
}
}
exit_code
Ok(())
}

/// Set the class for the player character
fn class(game: &mut Game, class_name: &Option<String>) {
fn class(game: &mut Game, class_name: &Option<String>) -> Result<()> {
if let Some(class_name) = class_name {
let class_name = sanitize(class_name);
match game.change_class(&class_name) {
Err(game::ClassChangeError::NotAtHome) => {
println!("Class change is only allowed at home.");
}
Err(game::ClassChangeError::NotFound) => {
println!("Unknown class name.")
}
Ok(()) => {}
}
game.change_class(&class_name)
} else {
let player_classes: Vec<String> =
character::class::Class::names(character::class::Category::Player)
.iter()
.cloned()
.collect();
println!("Options: {}", player_classes.join(", "));
Ok(())
}
}

/// Buy an item from the shop or list the available items if no item name is provided.
/// Shopping is only allowed when the player is at the home directory.
fn shop(game: &mut Game, item_name: &Option<String>) -> i32 {
if game.location.is_home() {
if let Some(item_name) = item_name {
fn shop(game: &mut Game, items: &[String]) -> Result<()> {
if items.is_empty() {
item::shop::list(game)
} else {
for item_name in items {
let item_name = sanitize(item_name);
match item::shop::buy(game, &item_name) {
Err(item::shop::Error::NotEnoughGold) => {
println!("Not enough gold.");
1
}
Err(item::shop::Error::ItemNotAvailable) => {
println!("Item not available.");
1
}
Ok(()) => 0,
}
} else {
item::shop::list(game);
0
item::shop::buy(game, &item_name)?
}
} else {
// FIXME this rule shouldn't be enforced here
println!("Shop is only allowed at home.");
1
Ok(())
}
}

/// Use an item from the inventory or list the inventory contents if no item name is provided.
fn use_item(game: &mut Game, item_name: &Option<String>) -> i32 {
if let Some(item_name) = item_name {
let item_name = sanitize(item_name);
if let Err(game::ItemNotFound) = game.use_item(&item_name) {
println!("Item not found.");
return 1;
}
} else {
fn use_item(game: &mut Game, items: &[String]) -> Result<()> {
if items.is_empty() {
println!("{}", log::format_inventory(&game));
} else {
for item_name in items {
let item_name = sanitize(item_name);
game.use_item(&item_name)?
}
}
0
Ok(())
}

/// Return a clean version of an item/equipment name, including aliases
Expand Down Expand Up @@ -241,7 +206,7 @@ mod tests {

let result = run(Some(cmd), &mut game);

assert_eq!(0, result);
assert!(result.is_ok());
assert!(game.player.xp > 0);
assert!(game.gold > 0);
}
Expand All @@ -266,7 +231,7 @@ mod tests {

let result = run(Some(cmd), &mut game);

assert_eq!(1, result);
assert!(result.is_err());
// game reset
assert_eq!(game.player.max_hp, game.player.current_hp);
assert_eq!(0, game.gold);
Expand All @@ -288,7 +253,7 @@ mod tests {
};

let result = run(Some(cmd), &mut game);
assert_eq!(0, result);
assert!(result.is_ok());
assert!(!game.location.is_home());

game.player.current_hp = 1;
Expand All @@ -302,7 +267,7 @@ mod tests {
};

let result = run(Some(cmd), &mut game);
assert_eq!(0, result);
assert!(result.is_ok());
assert!(game.location.is_home());
assert_eq!(game.player.max_hp, game.player.current_hp);
}
Expand All @@ -326,7 +291,7 @@ mod tests {
game.player.current_hp = 1;

game.gold = 100;
run(Some(cmd), &mut game);
assert!(run(Some(cmd), &mut game).is_err());

assert_eq!(0, game.gold);
assert!(!game.tombstones.is_empty());
Expand All @@ -338,12 +303,12 @@ mod tests {
bribe: false,
force: true,
};
run(Some(cmd), &mut game);
run(Some(cmd), &mut game).unwrap();

// inspect to pick up lost gold
let cmd = Command::Inspect;
let result = run(Some(cmd), &mut game);
assert_eq!(0, result);
assert!(result.is_ok());
assert!(game.tombstones.is_empty());

// includes +200g for visit tombstone quest
Expand All @@ -357,29 +322,29 @@ mod tests {

// not buy if not enough money
let cmd = Command::Buy {
item: Some(String::from("potion")),
items: vec![String::from("potion")],
};
let result = run(Some(cmd), &mut game);
assert_eq!(1, result);
assert!(result.is_err());
assert!(game.inventory().is_empty());

// buy potion
game.gold = 200;
let cmd = Command::Buy {
item: Some(String::from("potion")),
items: vec![String::from("potion")],
};
let result = run(Some(cmd), &mut game);
assert_eq!(0, result);
assert!(result.is_ok());
assert!(!game.inventory().is_empty());
assert_eq!(0, game.gold);

// use potion
game.player.current_hp -= 1;
let cmd = Command::Use {
item: Some(String::from("potion")),
items: vec![String::from("potion")],
};
let result = run(Some(cmd), &mut game);
assert_eq!(0, result);
assert!(result.is_ok());
assert!(game.inventory().is_empty());
assert_eq!(game.player.max_hp, game.player.current_hp);

Expand All @@ -390,14 +355,14 @@ mod tests {
bribe: false,
force: true,
};
run(Some(cmd), &mut game);
run(Some(cmd), &mut game).unwrap();

game.gold = 200;
let cmd = Command::Buy {
item: Some(String::from("potion")),
items: vec![String::from("potion")],
};
let result = run(Some(cmd), &mut game);
assert_eq!(1, result);
assert!(result.is_err());
assert!(game.inventory().is_empty());
}
}
Loading