~deciduously/fcalc

ab5f3ffdbc8c819466fedac28c8317b6af8bd799 — Ben Lovy 3 months ago 5ba32ec master
ItemQuantity
7 files changed, 174 insertions(+), 103 deletions(-)

M src/bin/fcalc.rs
M src/error.rs
M src/item.rs
M src/items.toml
M src/lib.rs
M src/recipe.rs
M src/recipes.toml
M src/bin/fcalc.rs => src/bin/fcalc.rs +2 -2
@@ 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)
}

M src/error.rs => src/error.rs +5 -3
@@ 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.

M src/item.rs => src/item.rs +16 -57
@@ 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<Product> {
        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<RawMaterial> {
        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<RawMaterial>,
    products: Vec<Product>,
    items: Vec<Item>,
}

impl Items {


@@ 78,18 45,11 @@ impl Items {

    /// Find an item by name
    pub fn lookup(&self, name: &str) -> Option<Item> {
        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);
    }
}

M src/items.toml => src/items.toml +7 -4
@@ 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

M src/lib.rs => src/lib.rs +1 -1
@@ 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;

M src/recipe.rs => src/recipe.rs +141 -34
@@ 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<Self, Self::Err> {
        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<Self, Self::Err> {
        let split = s.split(' ').collect::<Vec<&str>>();
        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::<u32>().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<Item>,
    pub output: Product,
    pub inputs: Vec<ItemQuantity>,
    pub output: ItemQuantity,
    time: f64,
    building_type: BuildingType,
}


@@ 32,50 97,43 @@ impl From<RawRecipe> 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<ItemQuantity> 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<Self, Self::Err> {
        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<Product, Recipe>,
    recipes: HashMap<Item, Recipe>,
}

impl From<RawRecipes> 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() {
    //     
    // }
}

M src/recipes.toml => src/recipes.toml +2 -2
@@ 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