/*
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();
}