use super::types::*;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::time::Duration;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Recipe {
pub id: RecipeId,
pub name: String,
pub category: CategoryId,
pub ingredients: Vec<Ingredient>,
pub results: Vec<SynthesisResult>,
pub base_success_rate: f32,
pub synthesis_duration: Duration,
pub prerequisites: Vec<RecipeId>,
pub discovery_difficulty: f32,
pub is_hidden: bool,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct RecipeRegistry {
recipes: HashMap<RecipeId, Recipe>,
categories: HashMap<CategoryId, Vec<RecipeId>>,
dependencies: HashMap<RecipeId, Vec<RecipeId>>,
}
impl RecipeRegistry {
pub fn new() -> Self {
Self::default()
}
pub fn add_recipe(&mut self, recipe: Recipe) {
let id = recipe.id.clone();
let category = recipe.category.clone();
self.categories
.entry(category)
.or_default()
.push(id.clone());
if !recipe.prerequisites.is_empty() {
self.dependencies
.insert(id.clone(), recipe.prerequisites.clone());
}
self.recipes.insert(id, recipe);
}
pub fn get(&self, id: &RecipeId) -> Option<&Recipe> {
self.recipes.get(id)
}
pub fn get_by_category(&self, category: &CategoryId) -> Vec<&Recipe> {
self.categories
.get(category)
.map(|ids| ids.iter().filter_map(|id| self.recipes.get(id)).collect())
.unwrap_or_default()
}
pub fn has_circular_dependency(&self, recipe_id: &RecipeId) -> bool {
let mut visited = HashSet::new();
let mut stack = vec![recipe_id.clone()];
while let Some(current) = stack.pop() {
if !visited.insert(current.clone()) {
return true;
}
if let Some(deps) = self.dependencies.get(¤t) {
stack.extend(deps.iter().cloned());
}
}
false
}
pub fn all_recipes(&self) -> impl Iterator<Item = (&RecipeId, &Recipe)> {
self.recipes.iter()
}
pub fn recipe_count(&self) -> usize {
self.recipes.len()
}
pub fn categories(&self) -> impl Iterator<Item = &CategoryId> {
self.categories.keys()
}
pub fn remove_recipe(&mut self, id: &RecipeId) -> Option<Recipe> {
if let Some(recipe) = self.recipes.remove(id) {
if let Some(cat_recipes) = self.categories.get_mut(&recipe.category) {
cat_recipes.retain(|rid| rid != id);
}
self.dependencies.remove(id);
Some(recipe)
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn create_test_recipe(id: &str, category: &str, prerequisites: Vec<String>) -> Recipe {
Recipe {
id: id.to_string(),
name: format!("Recipe {}", id),
category: CategoryId(category.to_string()),
ingredients: vec![],
results: vec![],
base_success_rate: 0.8,
synthesis_duration: Duration::from_secs(10),
prerequisites,
discovery_difficulty: 0.5,
is_hidden: false,
}
}
#[test]
fn test_registry_new() {
let registry = RecipeRegistry::new();
assert_eq!(registry.recipe_count(), 0);
}
#[test]
fn test_add_and_get_recipe() {
let mut registry = RecipeRegistry::new();
let recipe = create_test_recipe("sword", "weapon", vec![]);
registry.add_recipe(recipe.clone());
assert_eq!(registry.recipe_count(), 1);
assert!(registry.get(&"sword".to_string()).is_some());
assert_eq!(
registry.get(&"sword".to_string()).unwrap().name,
"Recipe sword"
);
}
#[test]
fn test_get_by_category() {
let mut registry = RecipeRegistry::new();
registry.add_recipe(create_test_recipe("sword", "weapon", vec![]));
registry.add_recipe(create_test_recipe("bow", "weapon", vec![]));
registry.add_recipe(create_test_recipe("potion", "consumable", vec![]));
let weapons = registry.get_by_category(&CategoryId("weapon".to_string()));
assert_eq!(weapons.len(), 2);
let consumables = registry.get_by_category(&CategoryId("consumable".to_string()));
assert_eq!(consumables.len(), 1);
let empty = registry.get_by_category(&CategoryId("nonexistent".to_string()));
assert_eq!(empty.len(), 0);
}
#[test]
fn test_circular_dependency_detection() {
let mut registry = RecipeRegistry::new();
registry.add_recipe(create_test_recipe("recipe_c", "magic", vec![]));
registry.add_recipe(create_test_recipe(
"recipe_b",
"magic",
vec!["recipe_c".to_string()],
));
registry.add_recipe(create_test_recipe(
"recipe_a",
"magic",
vec!["recipe_b".to_string()],
));
assert!(!registry.has_circular_dependency(&"recipe_a".to_string()));
assert!(!registry.has_circular_dependency(&"recipe_b".to_string()));
assert!(!registry.has_circular_dependency(&"recipe_c".to_string()));
}
#[test]
fn test_circular_dependency_cycle() {
let mut registry = RecipeRegistry::new();
registry.add_recipe(create_test_recipe(
"recipe_a",
"magic",
vec!["recipe_b".to_string()],
));
registry.add_recipe(create_test_recipe(
"recipe_b",
"magic",
vec!["recipe_a".to_string()],
));
assert!(registry.has_circular_dependency(&"recipe_a".to_string()));
assert!(registry.has_circular_dependency(&"recipe_b".to_string()));
}
#[test]
fn test_all_recipes() {
let mut registry = RecipeRegistry::new();
registry.add_recipe(create_test_recipe("sword", "weapon", vec![]));
registry.add_recipe(create_test_recipe("bow", "weapon", vec![]));
registry.add_recipe(create_test_recipe("potion", "consumable", vec![]));
let all: Vec<_> = registry.all_recipes().collect();
assert_eq!(all.len(), 3);
}
#[test]
fn test_remove_recipe() {
let mut registry = RecipeRegistry::new();
registry.add_recipe(create_test_recipe("sword", "weapon", vec![]));
assert_eq!(registry.recipe_count(), 1);
let removed = registry.remove_recipe(&"sword".to_string());
assert!(removed.is_some());
assert_eq!(registry.recipe_count(), 0);
let weapons = registry.get_by_category(&CategoryId("weapon".to_string()));
assert_eq!(weapons.len(), 0);
}
#[test]
fn test_categories() {
let mut registry = RecipeRegistry::new();
registry.add_recipe(create_test_recipe("sword", "weapon", vec![]));
registry.add_recipe(create_test_recipe("potion", "consumable", vec![]));
let cats: Vec<_> = registry.categories().collect();
assert_eq!(cats.len(), 2);
}
#[test]
fn test_recipe_serialization() {
let recipe = create_test_recipe(
"test",
"magic",
vec!["prereq1".to_string(), "prereq2".to_string()],
);
let json = serde_json::to_string(&recipe).unwrap();
let deserialized: Recipe = serde_json::from_str(&json).unwrap();
assert_eq!(recipe.id, deserialized.id);
assert_eq!(recipe.prerequisites.len(), deserialized.prerequisites.len());
}
}