~deciduously/fcalc

5ba32ecfb93e39f3b11e2abaa2ba6d5c7c848544 — Ben Lovy a month ago 229270f
Recipe lookup
6 files changed, 255 insertions(+), 9 deletions(-)

M src/bin/fcalc.rs
M src/error.rs
M src/item.rs
M src/lib.rs
M src/recipe.rs
A src/recipes.toml
M src/bin/fcalc.rs => src/bin/fcalc.rs +4 -0
@@ 2,6 2,7 @@

#![cfg(feature = "bin")]

use fcalc::{ITEMS, RECIPES};
use lazy_static::lazy_static;
use std::env::var;



@@ 11,4 12,7 @@ lazy_static! {

fn main() {
    println!("fcalc {} - Factorio Calculator", *VERSION);
    let product = ITEMS.lookup("Iron Plate").unwrap().as_product().unwrap();
    let recipe = RECIPES.lookup(&product).unwrap();
    println!("To get {}:\n{}", product, recipe)
}

M src/error.rs => src/error.rs +4 -0
@@ 7,6 7,10 @@ use thiserror::Error;
pub enum FcalcError {
    #[error("Could not read input data file")]
    FileError(#[from] io::Error),
    #[error("Bad product type")]
    ProductTypeError,
    #[error("Bad building type")]
    InvalidBuildingType
}

/// Export convenient Result type carrying this error.

M src/item.rs => src/item.rs +66 -8
@@ 1,27 1,69 @@
//! An item is something the player can hold.  Some are raw, some are craftable.

use crate::{
    recipe::{Recipe, RECIPES},
    FcalcError, Result,
};
use lazy_static::lazy_static;
use serde::Deserialize;
use std::fmt;

lazy_static! {
    pub static ref ITEMS: Items = Items::new();
}

// /// All items, raw or otherwise, implement this trait.
// // TODO is this even necessary?
// pub trait Item: Display {}
#[derive(Clone, Debug, Deserialize)]
pub enum Item {
    Raw(RawMaterial),
    Product(Product),
}

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),
        }
    }

    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(Debug, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
pub struct RawMaterial {
    name: String,
}

#[derive(Debug, Deserialize)]
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 {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{}", self.name)
    }
}

/// The top-level Items struct, containing all supported items read from TOML.
#[derive(Debug, Deserialize)]
pub struct Items {


@@ 33,6 75,23 @@ impl Items {
    fn new() -> Self {
        Self::default()
    }

    /// 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 p in &self.products {
            if p.name == name {
                return Some(Item::Product(p.clone()));
            }
        }

        None
    }
}

impl Default for Items {


@@ 48,8 107,7 @@ mod test {
    use pretty_assertions::assert_eq;
    #[test]
    fn it_loads_all_items() {
        let items = Items::new();
        assert_eq!(items.raw_materials.len(), 2);
        assert_eq!(items.products.len(), 1);
        assert_eq!(ITEMS.raw_materials.len(), 2);
        assert_eq!(ITEMS.products.len(), 1)
    }
}

M src/lib.rs => src/lib.rs +2 -0
@@ 9,3 9,5 @@ mod item;
mod recipe;

pub use error::{FcalcError, Result};
pub use item::ITEMS;
pub use recipe::RECIPES;
\ No newline at end of file

M src/recipe.rs => src/recipe.rs +142 -1
@@ 1,3 1,144 @@
//! A recipe tracks the cost to produce an item.

use crate::{item::*, FcalcError};
use lazy_static::lazy_static;
use serde::Deserialize;
use std::{collections::HashMap, fmt, str::FromStr};

lazy_static! {
    pub static ref RECIPES: Recipes = Recipes::new();
}

/// A formula for building a product
#[derive(Clone, Debug, Deserialize)]
pub struct RawRecipe {
    inputs: Vec<String>,
    output: String,
    time: f64,
    building_type: String,
}

/// A formula for building a product
#[derive(Clone, Debug, Deserialize)]
pub struct Recipe {
    pub inputs: Vec<Item>,
    pub output: Product,
    time: f64,
    building_type: BuildingType,
}

impl From<RawRecipe> for Recipe {
    fn from(raw: RawRecipe) -> Self {
        let inputs = raw
            .inputs
            .iter()
            .map(|el| ITEMS.lookup(el).expect("Could not find input product"))
            .collect();
        let output = ITEMS
            .lookup(&raw.output)
            .expect("Could not find output product.")
            .as_product()
            .expect("Output product was a raw material");
        Self {
            inputs,
            output,
            time: raw.time,
            building_type: BuildingType::from_str(&raw.building_type).unwrap(),
        }
    }
}

impl fmt::Display for Recipe {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "Inputs: {:?}, Time: {}, Building Type: {:?}",
            self.inputs, self.time, self.building_type
        )
    }
}

/// 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 {
    building_type: BuildingType,
    name: String,
    crafting_speed: f64,
}

/// Top-level to read recipes.toml
#[derive(Debug, Deserialize)]
pub struct RawRecipes {
    buildings: Vec<Building>,
    recipes: Vec<RawRecipe>,
}

impl RawRecipes {
    fn new() -> Self {
        Self::default()
    }
}

impl Default for RawRecipes {
    fn default() -> Self {
        let input = include_str!("recipes.toml");
        toml::from_str(input).expect("Could not read recipes input file.")
    }
}

/// Recipe lookup
#[derive(Debug)]
pub struct Recipe;
pub struct Recipes {
    recipes: HashMap<Product, 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);
        }
        Self { recipes }
    }
}

impl Recipes {
    fn new() -> Self {
        let raw = RawRecipes::new();
        Self::from(raw)
    }

    pub fn lookup(&self, product: &Product) -> Option<&Recipe> {
        self.recipes.get(product)
    }
}

#[cfg(test)]
mod test {
    use super::*;
    use pretty_assertions::assert_eq;
    #[test]
    fn it_loads_all_recipes() {
        assert_eq!(RECIPES.recipes.len(), 1);
    }
}

A src/recipes.toml => src/recipes.toml +37 -0
@@ 0,0 1,37 @@
# All supported recipes

[[buildings]]
name = "Stone Furnace"
building_type = "Furnace"
crafting_speed = 1

[[buildings]]
name = "Steel Furnace"
building_type = "Furnace"
crafting_speed = 2

[[buildings]]
name = "Electric Furnace"
building_type = "Furnace"
crafting_speed = 2

[[buildings]]
name = "Assembling Machine 1"
building_type = "Assembler"
crafting_speed = 0.5

[[buildings]]
name = "Assembling Machine 2"
building_type = "Assembler"
crafting_speed = 0.75

[[buildings]]
name = "Assembling Machine 3"
building_type = "Assembler"
crafting_speed = 1.25

[[recipes]]
inputs = ["Iron Ore"]
output = "Iron Plate"
time = 3.2
building_type = "Furnace"
\ No newline at end of file