aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-07-13 23:27:18 +0200
committermetamuffin <metamuffin@disroot.org>2024-07-13 23:27:18 +0200
commit0c77e0938de43a970e03c6dcef019c87745f0ee4 (patch)
tree76212988078d18edd59330061eeecb38b6ab7fc5
parentcd3a3989901d877426cbd64622b93367eaf81d4a (diff)
downloadhurrycurry-0c77e0938de43a970e03c6dcef019c87745f0ee4.tar
hurrycurry-0c77e0938de43a970e03c6dcef019c87745f0ee4.tar.bz2
hurrycurry-0c77e0938de43a970e03c6dcef019c87745f0ee4.tar.zst
automatically generate demands from map and recipes. added icecream and sushi.
-rw-r--r--data/recipes/default.ts38
-rw-r--r--server/src/bin/graph.rs2
-rw-r--r--server/src/data/demands.rs94
-rw-r--r--server/src/data/mod.rs (renamed from server/src/data.rs)49
-rw-r--r--server/src/state.rs14
5 files changed, 167 insertions, 30 deletions
diff --git a/data/recipes/default.ts b/data/recipes/default.ts
index 24122b5d..33b433a6 100644
--- a/data/recipes/default.ts
+++ b/data/recipes/default.ts
@@ -22,13 +22,25 @@ export interface Recipe {
tile?: string,
inputs: (string | null)[],
outputs: (string | null)[],
- action: "instant" | "passive" | "active" | "demand"
+ action: "instant" | "passive" | "active" | "demand" | "demand"
duration?: number
revert_duration?: number,
warn?: boolean,
points?: number,
}
+function trash_output(ifull: string) {
+ const [i, ic] = get_container(ifull)
+ if (i == "plate") return ifull
+ if (i == "glass") return ifull
+ if (i == "pot") return ifull
+ if (i == "foodprocessor") return ifull
+ if (i == "dirty") return ifull
+ if (ic == "glass") return "glass"
+ if (ic == "plate") return "dirty-plate"
+ return null
+}
+
export const all_items = new Set<string>()
export function auto_trash() {
for (const ifull of all_items) {
@@ -57,6 +69,15 @@ export function out(r: Recipe) {
console.log(`- ${JSON.stringify(r).replaceAll("\"active\"", "!active").replaceAll("\"passive\"", "!passive").replaceAll("\"instant\"", "!instant")}`);
}
+export function edible(item: string) {
+ let i = item
+ if (!item.endsWith("-plate") && !item.endsWith("-glass")) {
+ i += "-plate"
+ out({ action: "instant", inputs: [item], outputs: [i] })
+ }
+ out({ action: "demand", inputs: [i], outputs: [trash_output(i)], duration: 10 })
+}
+
export function cut(from: string, to?: string, to2?: string) {
out({ action: "active", duration: 2, tile: "cuttingboard", inputs: [from], outputs: [to ?? ("sliced-" + from), to2 ?? null] })
}
@@ -120,6 +141,7 @@ export function combine(container: string, ...inputs: string[]) {
}
}
+
if (import.meta.main) {
out({ action: "active", duration: 2, tile: "sink", inputs: ["dirty-plate"], outputs: ["plate"] })
@@ -145,6 +167,12 @@ if (import.meta.main) {
combine("plate", "steak-pot", "sliced-tomato", "bread-slice")
+ edible("steak-plate")
+ edible("bread-slice-steak-plate")
+ edible("bread-slice-sliced-tomato-plate")
+ edible("bread-slice-sliced-tomato-steak-plate")
+ out({ action: "demand", inputs: ["bread"], outputs: [], duration: 0 })
+
crate("rice")
crate("fish")
crate("coconut")
@@ -155,6 +183,10 @@ if (import.meta.main) {
cook("rice")
out({ action: "instant", inputs: ["sliced-fish", "cooked-rice-pot"], outputs: ["nigiri", "pot"] })
+ out({ action: "instant", inputs: ["plate", "cooked-rice-pot"], outputs: ["cooked-rice-plate", "pot"] })
+ edible("cooked-rice-plate")
+ edible("nigiri")
+
// coconut milk and strawberry puree
process("strawberry", "strawberry-puree")
process("coconut", "milk")
@@ -166,11 +198,15 @@ if (import.meta.main) {
// icecream
out({ action: "passive", inputs: ["strawberrymilk-foodprocessor"], outputs: ["strawberry-icecream-foodprocessor"], tile: "freezer", duration: 20 })
out({ action: "instant", inputs: ["strawberry-icecream-foodprocessor", "plate"], outputs: ["foodprocessor", "strawberry-icecream-plate"] })
+ edible("strawberry-icecream-plate")
// drinks
out({ action: "instant", inputs: ["glass"], outputs: ["water-glass"], tile: "sink" })
out({ action: "instant", inputs: ["glass", "milk-foodprocessor"], outputs: ["milk-glass", "foodprocessor"] })
out({ action: "instant", inputs: ["glass", "strawberrymilk-foodprocessor"], outputs: ["strawberrymilk-glass", "foodprocessor"] })
+ edible("water-glass")
+ edible("strawberrymilk-glass")
+
auto_trash()
}
diff --git a/server/src/bin/graph.rs b/server/src/bin/graph.rs
index 888119aa..49ad4716 100644
--- a/server/src/bin/graph.rs
+++ b/server/src/bin/graph.rs
@@ -33,7 +33,7 @@ async fn main() -> Result<()> {
.nth(1)
.ok_or(anyhow!("first arg should be recipe set name"))?;
- let data = index.generate(format!("lobby-default-{rn}")).await?;
+ let data = index.generate(format!("sushibar-{rn}")).await?;
for i in 0..data.item_names.len() {
println!("i{i} [label=\"{}\"]", data.item_name(ItemIndex(i)))
diff --git a/server/src/data/demands.rs b/server/src/data/demands.rs
new file mode 100644
index 00000000..2501e225
--- /dev/null
+++ b/server/src/data/demands.rs
@@ -0,0 +1,94 @@
+/*
+ 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 <https://www.gnu.org/licenses/>.
+
+*/
+use super::Demand;
+use crate::interaction::Recipe;
+use hurrycurry_protocol::{ItemIndex, TileIndex};
+use std::collections::{HashMap, HashSet};
+
+pub fn generate_demands(
+ tiles: HashSet<TileIndex>,
+ items: HashSet<ItemIndex>,
+ raw_demands: &[(ItemIndex, Option<ItemIndex>, f32)],
+ recipes: &[Recipe],
+) -> Vec<Demand> {
+ let recipes = recipes
+ .iter()
+ .filter(|r| r.tile().map(|t| tiles.contains(&t)).unwrap_or(true))
+ .collect::<Vec<_>>();
+
+ let mut producable = HashMap::new();
+
+ for i in &items {
+ producable.insert(*i, 0.0);
+ }
+
+ loop {
+ let prod_count = producable.len();
+
+ for r in &recipes {
+ let output_count = r.outputs().iter().filter(|o| !items.contains(&o)).count();
+ let Some(ingred_cost) = r
+ .inputs()
+ .iter()
+ .map(|i| producable.get(i).copied())
+ .reduce(|a, b| {
+ if let (Some(a), Some(b)) = (a, b) {
+ Some(a + b)
+ } else {
+ None
+ }
+ })
+ .unwrap_or(Some(0.))
+ else {
+ continue;
+ };
+
+ let base_cost = match r {
+ Recipe::Passive { duration, .. } => 2. + duration * 0.1,
+ Recipe::Active { duration, .. } => 2. + duration,
+ Recipe::Instant { .. } => 1.,
+ };
+
+ let output_cost = (ingred_cost + base_cost) / output_count as f32;
+ for o in r.outputs() {
+ let cost = producable.entry(o).or_insert(f32::INFINITY);
+ *cost = cost.min(output_cost);
+ }
+ }
+
+ if prod_count == producable.len() {
+ break;
+ }
+ }
+
+ raw_demands
+ .iter()
+ .filter_map(|(i, o, d)| {
+ if let Some(cost) = producable.get(i) {
+ Some(Demand {
+ from: *i,
+ to: *o,
+ duration: *d,
+ points: *cost as i64,
+ })
+ } else {
+ None
+ }
+ })
+ .collect()
+}
diff --git a/server/src/data.rs b/server/src/data/mod.rs
index 24e8e232..6df60535 100644
--- a/server/src/data.rs
+++ b/server/src/data/mod.rs
@@ -21,6 +21,7 @@ use crate::{
interaction::Recipe,
};
use anyhow::{anyhow, bail, Result};
+use demands::generate_demands;
use hurrycurry_protocol::{
glam::{IVec2, Vec2},
DemandIndex, ItemIndex, MapMetadata, RecipeIndex, TileIndex,
@@ -35,6 +36,8 @@ use std::{
};
use tokio::fs::read_to_string;
+pub mod demands;
+
#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)]
#[serde(rename_all = "snake_case")]
pub enum Action {
@@ -43,6 +46,7 @@ pub enum Action {
Passive,
Active,
Instant,
+ Demand,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
@@ -159,30 +163,23 @@ impl DataIndex {
}
pub async fn generate(&self, spec: String) -> Result<Gamedata> {
- let (map, rest) = spec.split_once("-").unwrap_or((spec.as_str(), "default"));
- let (demands, recipes) = rest.split_once("-").unwrap_or((rest, "default"));
+ let (map, recipes) = spec.split_once("-").unwrap_or((spec.as_str(), "default"));
let map_in = serde_yaml::from_str(&self.read_map(map).await?)?;
- let demands_in = serde_yaml::from_str(&self.read_demands(demands).await?)?;
let recipes_in = serde_yaml::from_str(&self.read_recipes(recipes).await?)?;
- let mut gd = Gamedata::build(spec, map_in, demands_in, recipes_in)?;
+ let mut gd = Gamedata::build(spec, map_in, recipes_in)?;
gd.map = self.maps.clone();
Ok(gd)
}
}
impl Gamedata {
- pub fn build(
- spec: String,
- map_in: InitialMap,
- demands_in: Vec<DemandDecl>,
- recipes_in: Vec<RecipeDecl>,
- ) -> Result<Self> {
+ pub fn build(spec: String, map_in: InitialMap, recipes_in: Vec<RecipeDecl>) -> Result<Self> {
let reg = ItemTileRegistry::default();
let mut recipes = Vec::new();
- let mut demands = Vec::new();
let mut entities = Vec::new();
+ let mut raw_demands = Vec::new();
for mut r in recipes_in {
let r2 = r.clone();
@@ -217,24 +214,32 @@ impl Gamedata {
outputs: [outputs.next(), outputs.next()],
});
}
+ Action::Demand => raw_demands.push((
+ inputs.next().ok_or(anyhow!("demand needs inputs"))?,
+ outputs.next(),
+ r.duration.unwrap_or(10.),
+ )),
}
assert_eq!(inputs.next(), None, "{r2:?} inputs left over");
assert_eq!(outputs.next(), None, "{r2:?} outputs left over");
assert_eq!(r.points, None, "points specified where not possible")
}
- for d in demands_in {
- demands.push(Demand {
- from: reg.register_item(d.from),
- to: d.to.map(|to| reg.register_item(to)),
- duration: d.duration,
- points: d.points,
- })
- }
+ // TODO
+ // for d in demands_in {
+ // demands.push(Demand {
+ // from: reg.register_item(d.from),
+ // to: d.to.map(|to| reg.register_item(to)),
+ // duration: d.duration,
+ // points: d.points,
+ // })
+ // }
let mut chef_spawn = Vec2::new(0., 0.);
let mut customer_spawn = Vec2::new(0., 0.);
let mut initial_map = HashMap::new();
+ let mut tiles_used = HashSet::new();
+ let mut items_used = HashSet::new();
for (y, line) in map_in.map.iter().enumerate() {
for (x, tile) in line.trim().chars().enumerate() {
let pos = IVec2::new(x as i32, y as i32);
@@ -255,6 +260,10 @@ impl Gamedata {
let itemname = map_in.items.get(&tile).cloned();
let tile = reg.register_tile(tilename);
let item = itemname.map(|i| reg.register_item(i));
+ tiles_used.insert(tile);
+ if let Some(i) = item {
+ items_used.insert(i);
+ };
initial_map.insert(pos, (tile, item));
}
}
@@ -267,6 +276,8 @@ impl Gamedata {
.try_collect::<Vec<_>>()?,
);
+ let demands = generate_demands(tiles_used, items_used, &raw_demands, &recipes);
+
let item_names = reg.items.into_inner().unwrap();
let tile_names = reg.tiles.into_inner().unwrap();
let tile_collide = tile_names
diff --git a/server/src/state.rs b/server/src/state.rs
index e9cb1722..215f9a1b 100644
--- a/server/src/state.rs
+++ b/server/src/state.rs
@@ -76,7 +76,7 @@ impl State {
index.reload()?;
let mut game = Game::new();
- game.load(index.generate("lobby-none-none".to_string()).await?, None);
+ game.load(index.generate("lobby-none".to_string()).await?, None);
Ok(Self { game, index, tx })
}
@@ -88,10 +88,8 @@ impl State {
text: format!("Game finished. You reached {} points.", self.game.points),
})
.ok();
- self.game.load(
- self.index.generate("lobby-none-none".to_string()).await?,
- None,
- );
+ self.game
+ .load(self.index.generate("lobby-none".to_string()).await?, None);
}
while let Some(p) = self.game.packet_out() {
if matches!(p, PacketC::UpdateMap { .. } | PacketC::Position { .. }) {
@@ -156,10 +154,8 @@ impl State {
),
})
.ok();
- self.game.load(
- self.index.generate("lobby-none-none".to_string()).await?,
- None,
- );
+ self.game
+ .load(self.index.generate("lobby-none".to_string()).await?, None);
}
Command::Reload => {
if self.game.count_chefs() > 1 {