diff options
-rw-r--r-- | data/recipes/default.ts | 38 | ||||
-rw-r--r-- | server/src/bin/graph.rs | 2 | ||||
-rw-r--r-- | server/src/data/demands.rs | 94 | ||||
-rw-r--r-- | server/src/data/mod.rs (renamed from server/src/data.rs) | 49 | ||||
-rw-r--r-- | server/src/state.rs | 14 |
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 { |