From ab5f3ffdbc8c819466fedac28c8317b6af8bd799 Mon Sep 17 00:00:00 2001 From: Ben Lovy Date: Sun, 3 Jan 2021 17:44:11 -0500 Subject: [PATCH] ItemQuantity --- src/bin/fcalc.rs | 4 +- src/error.rs | 8 ++- src/item.rs | 73 +++++--------------- src/items.toml | 11 +-- src/lib.rs | 2 +- src/recipe.rs | 175 ++++++++++++++++++++++++++++++++++++++--------- src/recipes.toml | 4 +- 7 files changed, 174 insertions(+), 103 deletions(-) diff --git a/src/bin/fcalc.rs b/src/bin/fcalc.rs index 4b4b2d6..7414169 100644 --- a/src/bin/fcalc.rs +++ b/src/bin/fcalc.rs @@ -12,7 +12,7 @@ lazy_static! { fn main() { println!("fcalc {} - Factorio Calculator", *VERSION); - let product = ITEMS.lookup("Iron Plate").unwrap().as_product().unwrap(); + let product = ITEMS.lookup("Iron Plate").unwrap(); let recipe = RECIPES.lookup(&product).unwrap(); - println!("To get {}:\n{}", product, recipe) + println!("{}", recipe) } diff --git a/src/error.rs b/src/error.rs index 2b31426..ea89344 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,12 +5,14 @@ use thiserror::Error; #[derive(Debug, Error)] pub enum FcalcError { - #[error("Could not read input data file")] - FileError(#[from] io::Error), + #[error("Input data error")] + IOError(#[from] io::Error), #[error("Bad product type")] ProductTypeError, #[error("Bad building type")] - InvalidBuildingType + InvalidBuildingType, + #[error("Invalid item quanitty string")] + ItemQuantityError, } /// Export convenient Result type carrying this error. diff --git a/src/item.rs b/src/item.rs index 239dfc0..f0b01d2 100644 --- a/src/item.rs +++ b/src/item.rs @@ -1,9 +1,6 @@ //! An item is something the player can hold. Some are raw, some are craftable. -use crate::{ - recipe::{Recipe, RECIPES}, - FcalcError, Result, -}; +use crate::recipe::{Recipe, RECIPES}; use lazy_static::lazy_static; use serde::Deserialize; use std::fmt; @@ -12,53 +9,24 @@ lazy_static! { pub static ref ITEMS: Items = Items::new(); } -#[derive(Clone, Debug, Deserialize)] -pub enum Item { - Raw(RawMaterial), - Product(Product), +/// An item in a recipe +#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)] +pub struct Item { + pub name: String, + pub raw: bool, } impl Item { pub fn recipe(&self) -> Option<&Recipe> { - match self { - Item::Raw(_) => None, - Item::Product(p) => RECIPES.lookup(&p), - } - } - - pub fn as_product(&self) -> Result { - match self { - Item::Product(p) => Ok(p.clone()), - _ => Err(FcalcError::ProductTypeError), + if !self.raw { + RECIPES.lookup(self) + } else { + None } } - - pub fn as_raw(&self) -> Result { - match self { - Item::Raw(r) => Ok(r.clone()), - _ => Err(FcalcError::ProductTypeError), - } - } -} - -/// A base-level raw material, not craftable. -#[derive(Clone, Debug, Deserialize)] -pub struct RawMaterial { - name: String, } -impl fmt::Display for RawMaterial { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.name) - } -} - -#[derive(Clone, Debug, Deserialize, PartialEq, Eq, Hash)] -pub struct Product { - name: String, -} - -impl fmt::Display for Product { +impl fmt::Display for Item { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { write!(f, "{}", self.name) } @@ -67,8 +35,7 @@ impl fmt::Display for Product { /// The top-level Items struct, containing all supported items read from TOML. #[derive(Debug, Deserialize)] pub struct Items { - raw_materials: Vec, - products: Vec, + items: Vec, } impl Items { @@ -78,18 +45,11 @@ impl Items { /// Find an item by name pub fn lookup(&self, name: &str) -> Option { - for r in &self.raw_materials { - if r.name == name { - return Some(Item::Raw(r.clone())); + for i in &self.items { + if i.name == name { + return Some(i.clone()); } } - - for p in &self.products { - if p.name == name { - return Some(Item::Product(p.clone())); - } - } - None } } @@ -107,7 +67,6 @@ mod test { use pretty_assertions::assert_eq; #[test] fn it_loads_all_items() { - assert_eq!(ITEMS.raw_materials.len(), 2); - assert_eq!(ITEMS.products.len(), 1) + assert_eq!(ITEMS.items.len(), 3); } } diff --git a/src/items.toml b/src/items.toml index 1b1811d..c9c51ab 100644 --- a/src/items.toml +++ b/src/items.toml @@ -1,10 +1,13 @@ # All the items supported -[[raw_materials]] +[[items]] name = "Coal" +raw = true -[[raw_materials]] +[[items]] name = "Iron Ore" +raw = true -[[products]] -name = "Iron Plate" \ No newline at end of file +[[items]] +name = "Iron Plate" +raw = false \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 66ec9f8..ccb2b2b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -10,4 +10,4 @@ mod recipe; pub use error::{FcalcError, Result}; pub use item::ITEMS; -pub use recipe::RECIPES; \ No newline at end of file +pub use recipe::RECIPES; diff --git a/src/recipe.rs b/src/recipe.rs index a40513e..82f0150 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -9,6 +9,30 @@ lazy_static! { pub static ref RECIPES: Recipes = Recipes::new(); } +/// Each recipe is made in a type of building +#[derive(Clone, Copy, Debug, PartialEq, Deserialize)] +pub enum BuildingType { + Assembler, + Furnace, +} + +impl FromStr for BuildingType { + type Err = FcalcError; + fn from_str(s: &str) -> Result { + match s { + "Furnace" => Ok(BuildingType::Furnace), + "Assembler" => Ok(BuildingType::Assembler), + _ => Err(FcalcError::InvalidBuildingType), + } + } +} + +impl fmt::Display for BuildingType { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + /// A formula for building a product #[derive(Clone, Debug, Deserialize)] pub struct RawRecipe { @@ -18,11 +42,52 @@ pub struct RawRecipe { building_type: String, } +/// An item with a quantity +#[derive(Clone, Debug, PartialEq)] +pub struct ItemQuantity { + item: Item, + quantity: u32, +} + +impl ItemQuantity { + fn new(item: Item, quantity: u32) -> Self { + Self { item, quantity } + } +} + +impl fmt::Display for ItemQuantity { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let plural = if self.quantity > 1 { "s" } else { "" }; + write!(f, "{} {}{}", self.quantity, self.item, plural) + } +} + +impl FromStr for ItemQuantity { + type Err = FcalcError; + fn from_str(s: &str) -> Result { + let split = s.split(' ').collect::>(); + if split.len() < 2 { + return Err(FcalcError::ItemQuantityError); + } + // Put the rest back together + let item_str = split[1..].iter().fold(String::new(), |mut acc, el| { + acc.push_str(el); + acc.push(' '); + acc + }); + // Lop off last space + let item_str = &item_str[0..item_str.len() - 1]; + let item = ITEMS.lookup(&item_str).expect("Unknown item"); + let quantity = split[0].parse::().expect("u32"); + Ok(Self::new(item, quantity)) + } +} + /// A formula for building a product -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, PartialEq)] pub struct Recipe { - pub inputs: Vec, - pub output: Product, + pub inputs: Vec, + pub output: ItemQuantity, time: f64, building_type: BuildingType, } @@ -32,50 +97,43 @@ impl From for Recipe { let inputs = raw .inputs .iter() - .map(|el| ITEMS.lookup(el).expect("Could not find input product")) + .map(|el| ItemQuantity::from_str(el).unwrap()) .collect(); - let output = ITEMS - .lookup(&raw.output) - .expect("Could not find output product.") - .as_product() - .expect("Output product was a raw material"); + let output = ItemQuantity::from_str(&raw.output).unwrap(); + let building_type = BuildingType::from_str(&raw.building_type).unwrap(); Self { inputs, output, time: raw.time, - building_type: BuildingType::from_str(&raw.building_type).unwrap(), + building_type, } } } +/// This impl is used to produce new recipes. +/// The passed ItemQuantity is the target output. +impl From for Recipe { + fn from(iq: ItemQuantity) -> Self { + unimplemented!() + } +} + impl fmt::Display for Recipe { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut inputs = String::new(); + for input in &self.inputs { + inputs.push_str(&input.to_string()); + inputs.push_str(", "); + } + let inputs = &inputs[0..inputs.len() - 2]; write!( f, - "Inputs: {:?}, Time: {}, Building Type: {:?}", - self.inputs, self.time, self.building_type + "{} => {} | {}, {} seconds", + inputs, self.output, self.building_type, self.time ) } } -/// Each recipe is made in a type of building -#[derive(Clone, Copy, Debug, Deserialize)] -pub enum BuildingType { - Assembler, - Furnace, -} - -impl FromStr for BuildingType { - type Err = FcalcError; - fn from_str(s: &str) -> Result { - match s { - "Furnace" => Ok(BuildingType::Furnace), - "Assembler" => Ok(BuildingType::Assembler), - _ => Err(FcalcError::InvalidBuildingType), - } - } -} - /// Each building type has several tiers #[derive(Debug, Deserialize)] pub struct Building { @@ -107,16 +165,15 @@ impl Default for RawRecipes { /// Recipe lookup #[derive(Debug)] pub struct Recipes { - recipes: HashMap, + recipes: HashMap, } impl From for Recipes { fn from(raw: RawRecipes) -> Self { let mut recipes = HashMap::new(); for raw in &raw.recipes { - // TODO convert all Strings to Items in input and output let recipe = Recipe::from(raw.clone()); - recipes.insert(recipe.output.clone(), recipe); + recipes.insert(recipe.output.item.clone(), recipe); } Self { recipes } } @@ -128,7 +185,7 @@ impl Recipes { Self::from(raw) } - pub fn lookup(&self, product: &Product) -> Option<&Recipe> { + pub fn lookup(&self, product: &Item) -> Option<&Recipe> { self.recipes.get(product) } } @@ -141,4 +198,54 @@ mod test { fn it_loads_all_recipes() { assert_eq!(RECIPES.recipes.len(), 1); } + #[test] + fn itemquantity_from_str() { + assert_eq!(ItemQuantity::from_str("1 Iron Ore").unwrap(), ItemQuantity { + item: Item { + name: "Iron Ore".to_string(), + raw: true + }, + quantity: 1 + }); + assert_eq!(ItemQuantity::from_str("35 Iron Plate").unwrap(), ItemQuantity { + item: Item { + name: "Iron Plate".to_string(), + raw: false + }, + quantity: 35 + }); + } + #[test] + fn it_retrieves_defined_simple_recipe() { + let product = ITEMS.lookup("Iron Plate").unwrap(); + let recipe = RECIPES.lookup(&product).unwrap(); + let expected = Recipe { + inputs: vec![ItemQuantity::from_str("1 Iron Ore").unwrap()], + output: ItemQuantity::from_str("1 Iron Plate").unwrap(), + time: 3.2, + building_type: BuildingType::Furnace + }; + assert_eq!(recipe, &expected); + } + #[test] + fn it_multiplies_simple_recipe() { + let product = ITEMS.lookup("Iron Plate").unwrap(); + let target = ItemQuantity::from_str("3 Iron Plate").unwrap(); + let recipe = Recipe::from(target); + let expected = Recipe { + inputs: vec![ItemQuantity::from_str("3 Iron Ore").unwrap()], + output: ItemQuantity::from_str("3 Iron Plate").unwrap(), + time: 3.2, + building_type: BuildingType::Furnace + }; + assert_eq!(recipe, expected); + } + // #[test] + // fn it_expands_defined_compound_recipe() { + // + // } + // #[test] + // fn it_expands_custom_compound_recipe() { + // + // } } diff --git a/src/recipes.toml b/src/recipes.toml index 99666af..c9c83ec 100644 --- a/src/recipes.toml +++ b/src/recipes.toml @@ -31,7 +31,7 @@ building_type = "Assembler" crafting_speed = 1.25 [[recipes]] -inputs = ["Iron Ore"] -output = "Iron Plate" +inputs = ["1 Iron Ore"] +output = "1 Iron Plate" time = 3.2 building_type = "Furnace" \ No newline at end of file -- 2.30.1