/* Hurry Curry! - a game about cooking Copyright 2024 metamuffin This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, version 3 of the License only. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . */ use serde::{Deserialize, Serialize}; use std::{ collections::BTreeSet, fmt::Display, sync::{LazyLock, Mutex}, }; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Default)] pub struct Item { container: Option>, name: String, container_dispose_tile: Option, } #[derive(Debug, Deserialize, Serialize, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord)] #[serde(rename_all = "snake_case")] pub enum RecipeAction { #[default] Never, Passive, Active, Instant, Demand, } #[rustfmt::skip] #[derive(Debug, Clone, Deserialize,Default, Serialize,PartialEq, Eq, PartialOrd, Ord)] pub struct Recipe { #[serde(default)] tile: Option, #[serde(default)] inputs: Vec, #[serde(default)] outputs: Vec, #[serde(default)] action: RecipeAction, #[serde(default)] warn: bool, #[serde(default)] revert_duration: Option>, #[serde(default)] duration: Option>, #[serde(default)] points: Option, } #[derive(Debug, Clone, Copy, PartialEq, PartialOrd, Serialize, Deserialize)] #[repr(transparent)] #[serde(transparent)] struct OrdAnyway(T); impl Ord for OrdAnyway { fn cmp(&self, other: &Self) -> std::cmp::Ordering { self.partial_cmp(other).unwrap() } } impl Eq for OrdAnyway {} static ALL_ITEMS: Mutex> = Mutex::new(BTreeSet::new()); static ALL_RECIPES: Mutex>> = Mutex::new(BTreeSet::new()); fn out(r: Recipe) { for i in r.inputs.clone() { ALL_ITEMS.lock().unwrap().insert(i.clone()); } for o in r.outputs.clone() { ALL_ITEMS.lock().unwrap().insert(o.clone()); } ALL_RECIPES.lock().unwrap().insert(r); } fn finish() { println!( "{}", serde_yml::to_string( &ALL_RECIPES .lock() .unwrap() .iter() .map(|r| Recipe { action: r.action, inputs: r.inputs.iter().map(|i| i.to_string()).collect(), outputs: r.outputs.iter().map(|o| o.to_string()).collect(), points: r.points, tile: r.tile.clone(), warn: r.warn, duration: r.duration, revert_duration: r.revert_duration, }) .collect::>() ) .unwrap() ); } fn auto_trash() { let all_items = ALL_ITEMS.lock().unwrap().clone(); for i in all_items { if i.container_dispose_tile.is_some() { continue; } if let Some(c) = &i.container { let cdt = c.container_dispose_tile.as_ref().unwrap(); out(Recipe { action: RecipeAction::Instant, inputs: vec![i.clone()], outputs: i.container.clone().into_iter().map(|i| *i).collect(), tile: Some(cdt.to_owned()), ..Default::default() }); } else { out(Recipe { action: RecipeAction::Instant, inputs: vec![i], outputs: vec![], tile: Some("trash".to_owned()), ..Default::default() }); } } } fn auto_burn() { let all_items = ALL_ITEMS.lock().unwrap().clone(); for i in all_items { if i.container_dispose_tile.is_some() { continue; } if i.container.is_some() || i.name == "burned" { continue; } else { out(Recipe { action: RecipeAction::Passive, inputs: vec![i], outputs: vec![Item { name: "burned".to_owned(), ..Default::default() }], duration: Some(OrdAnyway(1.5)), revert_duration: Some(OrdAnyway(1.5)), warn: true, tile: Some("stove".to_owned()), ..Default::default() }) } } } impl Item { fn r#as(mut self, s: &str) -> Item { self.name = s.to_owned(); self } fn tr(&self, container: Option>) -> Item { let o = Item { name: self.name.to_owned(), container, ..Default::default() }; if self.container == o.container { return o; } match (self.container.clone(), o.container.clone()) { (None, None) => (), (Some(old_c), Some(new_c)) => out(Recipe { action: RecipeAction::Instant, inputs: vec![self.clone(), *new_c], outputs: vec![*old_c, o.clone()], ..Default::default() }), (None, Some(new_c)) => out(Recipe { action: RecipeAction::Instant, inputs: vec![*new_c, self.clone()], outputs: vec![o.clone()], ..Default::default() }), (Some(old_c), None) => out(Recipe { action: RecipeAction::Instant, inputs: vec![self.clone()], outputs: vec![o.clone(), *old_c], ..Default::default() }), } return o; } } impl Display for Item { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { if let Some(c) = &self.container { write!(f, "{c}:")?; } write!(f, "{}", self.name) } } static FP: LazyLock> = LazyLock::new(move || { Box::new(Item { container: None, name: "foodprocessor".to_owned(), container_dispose_tile: Some("trash".to_string()), }) }); static POT: LazyLock> = LazyLock::new(move || { Box::new(Item { container: None, name: "pot".to_owned(), container_dispose_tile: Some("trash".to_string()), ..Default::default() }) }); static PAN: LazyLock> = LazyLock::new(move || { Box::new(Item { container: None, name: "pan".to_owned(), container_dispose_tile: Some("trash".to_string()), ..Default::default() }) }); static PL: LazyLock> = { LazyLock::new(move || { Box::new(Item { container: None, name: "plate".to_owned(), container_dispose_tile: Some("trash".to_string()), ..Default::default() }) }) }; static GL: LazyLock> = { LazyLock::new(move || { Box::new(Item { container: None, name: "glass".to_owned(), container_dispose_tile: Some("sink".to_string()), ..Default::default() }) }) }; fn from_crate(s: &str) -> Item { let item = Item { name: s.to_owned(), ..Default::default() }; out(Recipe { action: RecipeAction::Instant, inputs: vec![], outputs: vec![item.clone()], tile: Some(format!("{s}-crate")), points: Some(-1), ..Default::default() }); out(Recipe { action: RecipeAction::Instant, inputs: vec![item.clone()], outputs: vec![], tile: Some(format!("{s}-crate")), points: Some(1), ..Default::default() }); return item; } fn cut(s: &Item) -> Item { cut_two(s, false) } fn cut_two(s: &Item, two: bool) -> Item { let o = Item { name: format!("sliced-{}", s.name), container: s.container.clone(), ..Default::default() }; out(Recipe { action: RecipeAction::Active, inputs: vec![s.clone()], outputs: if two { vec![o.clone(), o.clone()] } else { vec![o.clone()] }, tile: Some("cuttingboard".to_owned()), duration: Some(OrdAnyway(2.)), ..Default::default() }); o } const COOK_D: f32 = 20.; fn cook(s: &Item, d: f32) -> Item { let o = Item { name: format!("cooked-{}", s.name), container: s.container.clone(), ..Default::default() }; out(Recipe { action: RecipeAction::Passive, duration: Some(OrdAnyway(d)), revert_duration: Some(OrdAnyway(d * 2.)), tile: Some("stove".to_owned()), inputs: vec![s.clone()], outputs: vec![o.clone()], ..Default::default() }); out(Recipe { action: RecipeAction::Passive, duration: Some(OrdAnyway(d / 3.)), revert_duration: Some(OrdAnyway(d / 2.)), tile: Some("stove".to_owned()), inputs: vec![o.clone()], outputs: vec![Item { name: "burned".to_owned(), container: Some(POT.clone()), ..Default::default() }], warn: true, ..Default::default() }); return o; } const SEAR_D: f32 = 15.; fn sear(s: &Item, d: f32) -> Item { let s = s.tr(Some(PAN.clone())); let o = Item { name: format!("seared-{}", s.name), container: s.container.clone(), ..Default::default() }; out(Recipe { action: RecipeAction::Passive, duration: Some(OrdAnyway(d)), revert_duration: Some(OrdAnyway(d * 2.)), tile: Some("stove".to_owned()), inputs: vec![s.clone()], outputs: vec![o.clone()], ..Default::default() }); out(Recipe { action: RecipeAction::Passive, duration: Some(OrdAnyway(d / 3.)), revert_duration: Some(OrdAnyway(d / 3.)), tile: Some("stove".to_owned()), inputs: vec![o.clone()], outputs: vec![Item { name: "burned".to_owned(), container: Some(PAN.clone()), ..Default::default() }], warn: true, ..Default::default() }); return o; } const BAKE_D: f32 = 25.; fn bake(s: &Item, d: f32) -> Item { let o = Item { name: format!("sliced-{}", s.name), container: s.container.clone(), ..Default::default() }; out(Recipe { action: RecipeAction::Passive, duration: Some(OrdAnyway(d)), revert_duration: Some(OrdAnyway(d * 2.)), tile: Some("oven".to_owned()), inputs: vec![s.clone()], outputs: vec![o.clone()], ..Default::default() }); out(Recipe { action: RecipeAction::Passive, duration: Some(OrdAnyway(d / 2.)), revert_duration: Some(OrdAnyway(d / 4.)), tile: Some("oven".to_owned()), inputs: vec![o.clone()], outputs: vec![Item { name: "burned".to_owned(), ..Default::default() }], warn: true, ..Default::default() }); return o; } fn freeze(s: &Item) -> Item { let o = Item { name: format!("frozen-{}", s.name), container: s.container.clone(), ..Default::default() }; out(Recipe { action: RecipeAction::Passive, duration: Some(OrdAnyway(25.)), tile: Some("freezer".to_owned()), inputs: vec![s.clone()], outputs: vec![o.clone()], ..Default::default() }); return o; } fn process(s: &Item) -> Item { let o = Item { name: format!("processed-{}", s.name), container: s.container.clone(), ..Default::default() }; out(Recipe { action: RecipeAction::Passive, duration: Some(OrdAnyway(5.)), inputs: vec![s.clone()], outputs: vec![o.clone()], ..Default::default() }); return o; } fn container_add(base: &Item, add: &Item) -> Item { let o = Item { name: "!!!".to_string(), container: base.container.clone(), ..Default::default() }; out(Recipe { action: RecipeAction::Instant, inputs: vec![base.clone(), add.clone()], outputs: if let Some(c) = add.container.clone() { vec![o.clone(), *c] } else { vec![o.clone()] }, ..Default::default() }); return o; } fn combine(c: Item, items: Vec<&Item>) -> Item { if items.len() == 1 { return items[0].tr(Some(Box::new(c.clone()))); } let mut open = BTreeSet::from_iter(items.iter().map(|i| { i.tr(Some(Box::new(c.clone()))); vec![i.name.clone()] })); let mut seen = BTreeSet::new(); let mut result = None; while let Some(cur) = open.pop_first() { for &new_item in &items { if cur.contains(&new_item.name) { continue; } let rkey = format!("{}#{}", cur.join(","), new_item); if seen.contains(&rkey) { continue; } seen.insert(rkey); let mut parts = cur.clone(); parts.push(new_item.name.clone()); parts.sort(); open.insert(parts.clone()); let i = Item { name: cur.join(","), container: Some(Box::new(c.clone())), ..Default::default() }; let o = Item { name: parts.join(","), container: Some(Box::new(c.clone())), ..Default::default() }; if parts.len() == items.len() { result = Some(o.clone()) }; out(Recipe { action: RecipeAction::Instant, inputs: vec![i, new_item.clone()], outputs: if let Some(c) = new_item.container.clone() { vec![o, *c] } else { vec![o] }, ..Default::default() }) } } return result.unwrap(); } fn edible(i: Item) { out(Recipe { action: RecipeAction::Demand, inputs: vec![i], outputs: vec![], // TODO duration: Some(OrdAnyway(10.)), ..Default::default() }) } fn either(a: Item, b: Item) -> Item { if a.name != b.name { panic!("either options are named differently"); } if a.container != b.container { panic!("either options are contained differently"); } return a; } fn sink_fill() -> Item { let o = Item { name: "water".to_owned(), container: Some(GL.clone()), ..Default::default() }; out(Recipe { action: RecipeAction::Active, inputs: vec![*GL.clone()], outputs: vec![o.clone()], tile: Some("sink".to_owned()), duration: Some(OrdAnyway(1.)), ..Default::default() }); return o; } fn main() { out(Recipe { action: RecipeAction::Active, duration: Some(OrdAnyway(2.)), tile: Some("sink".to_owned()), inputs: vec![Item { name: "dirty-plate".to_owned(), ..Default::default() }], outputs: vec![*PL.clone()], ..Default::default() }); let tomato = from_crate("tomato"); let steak = from_crate("steak"); let flour = from_crate("flour"); let leek = from_crate("leek"); let rice = from_crate("rice"); let fish = from_crate("fish"); let coconut = from_crate("coconut"); let strawberry = from_crate("strawberry"); let cheese = from_crate("cheese"); let lettuce = from_crate("lettuce"); // // Buns let dough = process(&flour.tr(Some(FP.clone()))).r#as("dough").tr(None); let bun = bake(&dough, BAKE_D).r#as("bun"); edible(bun.tr(Some(PL.clone()))); // Steak let seared_steak = sear(&steak, SEAR_D); edible(combine(*PL.clone(), vec![&seared_steak, &bun])); edible(combine(*PL.clone(), vec![&seared_steak])); // Salad edible(combine(*PL.clone(), vec![&cut(&tomato), &cut(&lettuce)])); edible(combine(*PL.clone(), vec![&cut(&lettuce)])); // Burger let b_patty = sear(&cut(&steak).r#as("patty"), SEAR_D); edible(combine( *PL.clone(), vec![&cut(&bun), &b_patty, &cut(&cheese)], )); edible(combine( *PL.clone(), vec![&cut(&bun), &b_patty, &cut(&cheese), &cut(&lettuce)], )); edible(combine( *PL.clone(), vec![&cut(&bun), &b_patty, &cut(&tomato), &cut(&lettuce)], )); edible(combine( *PL.clone(), vec![&cut(&bun), &b_patty, &cut(&cheese), &cut(&tomato)], )); edible(combine( *PL.clone(), vec![&cut(&bun), &cut(&cheese), &cut(&lettuce), &cut(&tomato)], )); edible(combine( *PL.clone(), vec![&cut(&bun), &cut(&lettuce), &cut(&tomato)], )); // Soup let tomato_juice = process(&tomato.tr(Some(FP.clone()))).r#as("tomato-juice"); let leek_tj_pot = combine(*POT.clone(), vec![&leek, &tomato_juice]); let tomato_soup_plate = cook(&leek_tj_pot, COOK_D) .r#as("tomato-soup") .tr(Some(PL.clone())); edible(tomato_soup_plate); // Rice and nigiri let nigiri = container_add(&cut(&fish), &cook(&rice.tr(Some(POT.clone())), COOK_D)) .r#as("nigiri") .tr(Some(PL.clone())); edible(nigiri); // coconut milk and strawberry puree let strawberry_puree = process(&strawberry.tr(Some(FP.clone()))).r#as("strawberry-puree"); let milk = process(&coconut.tr(Some(FP.clone()))).r#as("milk"); let strawberry_shake = either( process(&container_add(&milk, &strawberry).r#as("milk,strawberry")) .r#as("strawberry-shake"), process(&container_add(&strawberry_puree, &coconut).r#as("coconut,strawberry-puree")) .r#as("strawberry-shake"), ); // Icecream edible( freeze(&strawberry_shake) .r#as("strawberry-icecream") .tr(Some(PL.clone())), ); // Mochi let rice_flour = process(&rice.tr(Some(FP.clone()))).r#as("rice-flour"); let mochi_dough = cook(&rice_flour.tr(Some(POT.clone())), 5.).r#as("mochi-dough"); let strawberry_mochi = container_add(&strawberry, &mochi_dough).r#as("strawberry-mochi"); edible(strawberry_mochi); // Drinks edible(strawberry_shake.tr(Some(GL.clone()))); edible(tomato_juice.tr(Some(GL.clone()))); edible(sink_fill()); // Curry let curry_with_rice = combine( *PL.clone(), vec![ &cook(&&rice.tr(Some(POT.clone())), COOK_D), &cook(&combine(*POT.clone(), vec![&milk, &tomato, &leek]), COOK_D).r#as("curry"), ], ); edible(curry_with_rice); auto_trash(); auto_burn(); finish(); }