From 0c77e0938de43a970e03c6dcef019c87745f0ee4 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Sat, 13 Jul 2024 23:27:18 +0200 Subject: automatically generate demands from map and recipes. added icecream and sushi. --- server/src/bin/graph.rs | 2 +- server/src/data.rs | 360 ------------------------------------------- server/src/data/demands.rs | 94 ++++++++++++ server/src/data/mod.rs | 371 +++++++++++++++++++++++++++++++++++++++++++++ server/src/state.rs | 14 +- 5 files changed, 471 insertions(+), 370 deletions(-) delete mode 100644 server/src/data.rs create mode 100644 server/src/data/demands.rs create mode 100644 server/src/data/mod.rs (limited to 'server/src') 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.rs b/server/src/data.rs deleted file mode 100644 index 24e8e232..00000000 --- a/server/src/data.rs +++ /dev/null @@ -1,360 +0,0 @@ -/* - Hurry Curry! - a game about cooking - Copyright 2024 metamuffin - Copyright 2024 nokoe - - 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 crate::{ - entity::{construct_entity, Entity, EntityDecl}, - interaction::Recipe, -}; -use anyhow::{anyhow, bail, Result}; -use hurrycurry_protocol::{ - glam::{IVec2, Vec2}, - DemandIndex, ItemIndex, MapMetadata, RecipeIndex, TileIndex, -}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::{HashMap, HashSet}, - fs::File, - path::PathBuf, - str::FromStr, - sync::{Mutex, RwLock}, -}; -use tokio::fs::read_to_string; - -#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] -#[serde(rename_all = "snake_case")] -pub enum Action { - #[default] - Never, - Passive, - Active, - Instant, -} - -#[derive(Debug, Clone, Deserialize, Serialize)] -pub struct RecipeDecl { - #[serde(default)] - tile: Option, - #[serde(default)] - inputs: Vec, - #[serde(default)] - outputs: Vec, - #[serde(default)] - action: Action, - #[serde(default)] - warn: bool, - #[serde(default)] - revert_duration: Option, - #[serde(default)] - duration: Option, - #[serde(default)] - points: Option, -} - -#[derive(Debug, Clone, Deserialize)] -pub struct InitialMap { - map: Vec, - tiles: HashMap, - #[serde(default)] - items: HashMap, - collider: Vec, - walkable: Vec, - chef_spawn: char, - customer_spawn: char, - #[serde(default)] - entities: Vec, - #[serde(default)] - tile_entities: HashMap, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DemandDecl { - from: String, - to: Option, - duration: f32, - points: i64, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Demand { - pub from: ItemIndex, - pub to: Option, - pub duration: f32, - pub points: i64, -} - -#[derive(Debug, Clone, Serialize, Deserialize, Default)] -#[rustfmt::skip] -pub struct Gamedata { - pub spec: String, - pub item_names: Vec, - pub tile_names: Vec, - pub tile_collide: Vec, - pub tile_interact: Vec, - pub map: HashMap, - #[serde(skip)] pub recipes: Vec, - #[serde(skip)] pub demands: Vec, - #[serde(skip)] pub initial_map: HashMap)>, - #[serde(skip)] pub chef_spawn: Vec2, - #[serde(skip)] pub customer_spawn: Vec2, - #[serde(skip)] pub entities: Vec, -} - -#[derive(Debug, Deserialize, Default)] -pub struct DataIndex { - pub maps: HashMap, - pub demands: HashSet, - pub recipes: HashSet, -} - -pub static DATA_DIR: Mutex> = Mutex::new(None); -fn data_dir() -> PathBuf { - DATA_DIR - .lock() - .unwrap() - .to_owned() - .unwrap_or_else(|| PathBuf::from_str("data").unwrap()) -} - -impl DataIndex { - pub fn reload(&mut self) -> Result<()> { - *self = serde_yaml::from_reader(File::open(data_dir().join("index.yaml"))?)?; - Ok(()) - } - - pub async fn read_map(&self, name: &str) -> Result { - if !self.maps.contains_key(name) { - bail!("unknown map: {name:?}"); - } - let path = data_dir().join(format!("maps/{name}.yaml")); - Ok(read_to_string(path).await?) - } - pub async fn read_demands(&self, name: &str) -> Result { - if !self.demands.contains(name) { - bail!("unknown demands: {name:?}"); - } - let path = data_dir().join(format!("demands/{name}.yaml")); - Ok(read_to_string(path).await?) - } - pub async fn read_recipes(&self, name: &str) -> Result { - if !self.recipes.contains(name) { - bail!("unknown recipes: {name:?}"); - } - let path = data_dir().join(format!("recipes/{name}.yaml")); - Ok(read_to_string(path).await?) - } - - pub async fn generate(&self, spec: String) -> Result { - let (map, rest) = spec.split_once("-").unwrap_or((spec.as_str(), "default")); - let (demands, recipes) = rest.split_once("-").unwrap_or((rest, "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)?; - gd.map = self.maps.clone(); - Ok(gd) - } -} - -impl Gamedata { - pub fn build( - spec: String, - map_in: InitialMap, - demands_in: Vec, - recipes_in: Vec, - ) -> Result { - let reg = ItemTileRegistry::default(); - let mut recipes = Vec::new(); - let mut demands = Vec::new(); - let mut entities = Vec::new(); - - for mut r in recipes_in { - let r2 = r.clone(); - let mut inputs = r.inputs.into_iter().map(|i| reg.register_item(i)); - let mut outputs = r.outputs.into_iter().map(|o| reg.register_item(o)); - let tile = r.tile.map(|t| reg.register_tile(t)); - match r.action { - Action::Never => {} - Action::Passive => recipes.push(Recipe::Passive { - duration: r.duration.ok_or(anyhow!("duration for passive missing"))?, - warn: r.warn, - tile, - revert_duration: r.revert_duration, - input: inputs - .next() - .ok_or(anyhow!("passive recipe without input"))?, - output: outputs.next(), - }), - Action::Active => recipes.push(Recipe::Active { - duration: r.duration.ok_or(anyhow!("duration for active missing"))?, - tile, - input: inputs - .next() - .ok_or(anyhow!("active recipe without input"))?, - outputs: [outputs.next(), outputs.next()], - }), - Action::Instant => { - recipes.push(Recipe::Instant { - points: r.points.take().unwrap_or(0), - tile, - inputs: [inputs.next(), inputs.next()], - outputs: [outputs.next(), outputs.next()], - }); - } - } - 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, - }) - } - - let mut chef_spawn = Vec2::new(0., 0.); - let mut customer_spawn = Vec2::new(0., 0.); - let mut initial_map = HashMap::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); - if tile == map_in.chef_spawn { - chef_spawn = pos.as_vec2() + Vec2::splat(0.5); - } - if tile == map_in.customer_spawn { - customer_spawn = pos.as_vec2() + Vec2::splat(0.5); - } - let tilename = map_in - .tiles - .get(&tile) - .ok_or(anyhow!("tile {tile} is undefined"))? - .clone(); - if let Some(ent) = map_in.tile_entities.get(&tile) { - entities.push(construct_entity(Some(pos), ent, ®)?); - } - let itemname = map_in.items.get(&tile).cloned(); - let tile = reg.register_tile(tilename); - let item = itemname.map(|i| reg.register_item(i)); - initial_map.insert(pos, (tile, item)); - } - } - - entities.extend( - map_in - .entities - .iter() - .map(|decl| construct_entity(None, decl, ®)) - .try_collect::>()?, - ); - - let item_names = reg.items.into_inner().unwrap(); - let tile_names = reg.tiles.into_inner().unwrap(); - let tile_collide = tile_names - .iter() - .map(|i| !map_in.walkable.contains(i)) - .collect(); - let tile_interact = tile_names - .iter() - .map(|i| !map_in.collider.contains(i) && !map_in.walkable.contains(i)) - .collect(); - - Ok(Gamedata { - spec, - demands, - tile_collide, - tile_interact, - recipes, - map: HashMap::new(), - initial_map, - item_names, - entities, - tile_names, - chef_spawn, - customer_spawn, - }) - } -} - -#[derive(Default)] -pub struct ItemTileRegistry { - tiles: RwLock>, - items: RwLock>, -} - -impl ItemTileRegistry { - pub fn register_tile(&self, name: String) -> TileIndex { - TileIndex(Self::register(&self.tiles, name)) - } - pub fn register_item(&self, name: String) -> ItemIndex { - ItemIndex(Self::register(&self.items, name)) - } - fn register(db: &RwLock>, name: String) -> usize { - let mut db = db.write().unwrap(); - if let Some(index) = db.iter().position(|e| e == &name) { - index - } else { - let index = db.len(); - db.push(name); - index - } - } -} - -impl Gamedata { - pub fn tile_name(&self, index: TileIndex) -> &String { - &self.tile_names[index.0] - } - pub fn is_tile_colliding(&self, index: TileIndex) -> bool { - self.tile_collide[index.0] - } - pub fn is_tile_interactable(&self, index: TileIndex) -> bool { - self.tile_interact[index.0] - } - pub fn item_name(&self, index: ItemIndex) -> &String { - &self.item_names[index.0] - } - pub fn recipe(&self, index: RecipeIndex) -> &Recipe { - &self.recipes[index.0] - } - pub fn demand(&self, index: DemandIndex) -> &Demand { - &self.demands[index.0] - } - pub fn get_tile_by_name(&self, name: &str) -> Option { - self.tile_names - .iter() - .position(|t| t == name) - .map(TileIndex) - } - pub fn get_item_by_name(&self, name: &str) -> Option { - self.item_names - .iter() - .position(|t| t == name) - .map(ItemIndex) - } - pub fn recipes(&self) -> impl Iterator { - self.recipes - .iter() - .enumerate() - .map(|(i, e)| (RecipeIndex(i), e)) - } -} 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 . + +*/ +use super::Demand; +use crate::interaction::Recipe; +use hurrycurry_protocol::{ItemIndex, TileIndex}; +use std::collections::{HashMap, HashSet}; + +pub fn generate_demands( + tiles: HashSet, + items: HashSet, + raw_demands: &[(ItemIndex, Option, f32)], + recipes: &[Recipe], +) -> Vec { + let recipes = recipes + .iter() + .filter(|r| r.tile().map(|t| tiles.contains(&t)).unwrap_or(true)) + .collect::>(); + + 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/mod.rs b/server/src/data/mod.rs new file mode 100644 index 00000000..6df60535 --- /dev/null +++ b/server/src/data/mod.rs @@ -0,0 +1,371 @@ +/* + Hurry Curry! - a game about cooking + Copyright 2024 metamuffin + Copyright 2024 nokoe + + 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 crate::{ + entity::{construct_entity, Entity, EntityDecl}, + interaction::Recipe, +}; +use anyhow::{anyhow, bail, Result}; +use demands::generate_demands; +use hurrycurry_protocol::{ + glam::{IVec2, Vec2}, + DemandIndex, ItemIndex, MapMetadata, RecipeIndex, TileIndex, +}; +use serde::{Deserialize, Serialize}; +use std::{ + collections::{HashMap, HashSet}, + fs::File, + path::PathBuf, + str::FromStr, + sync::{Mutex, RwLock}, +}; +use tokio::fs::read_to_string; + +pub mod demands; + +#[derive(Debug, Deserialize, Serialize, Clone, Copy, Default)] +#[serde(rename_all = "snake_case")] +pub enum Action { + #[default] + Never, + Passive, + Active, + Instant, + Demand, +} + +#[derive(Debug, Clone, Deserialize, Serialize)] +pub struct RecipeDecl { + #[serde(default)] + tile: Option, + #[serde(default)] + inputs: Vec, + #[serde(default)] + outputs: Vec, + #[serde(default)] + action: Action, + #[serde(default)] + warn: bool, + #[serde(default)] + revert_duration: Option, + #[serde(default)] + duration: Option, + #[serde(default)] + points: Option, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct InitialMap { + map: Vec, + tiles: HashMap, + #[serde(default)] + items: HashMap, + collider: Vec, + walkable: Vec, + chef_spawn: char, + customer_spawn: char, + #[serde(default)] + entities: Vec, + #[serde(default)] + tile_entities: HashMap, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DemandDecl { + from: String, + to: Option, + duration: f32, + points: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Demand { + pub from: ItemIndex, + pub to: Option, + pub duration: f32, + pub points: i64, +} + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[rustfmt::skip] +pub struct Gamedata { + pub spec: String, + pub item_names: Vec, + pub tile_names: Vec, + pub tile_collide: Vec, + pub tile_interact: Vec, + pub map: HashMap, + #[serde(skip)] pub recipes: Vec, + #[serde(skip)] pub demands: Vec, + #[serde(skip)] pub initial_map: HashMap)>, + #[serde(skip)] pub chef_spawn: Vec2, + #[serde(skip)] pub customer_spawn: Vec2, + #[serde(skip)] pub entities: Vec, +} + +#[derive(Debug, Deserialize, Default)] +pub struct DataIndex { + pub maps: HashMap, + pub demands: HashSet, + pub recipes: HashSet, +} + +pub static DATA_DIR: Mutex> = Mutex::new(None); +fn data_dir() -> PathBuf { + DATA_DIR + .lock() + .unwrap() + .to_owned() + .unwrap_or_else(|| PathBuf::from_str("data").unwrap()) +} + +impl DataIndex { + pub fn reload(&mut self) -> Result<()> { + *self = serde_yaml::from_reader(File::open(data_dir().join("index.yaml"))?)?; + Ok(()) + } + + pub async fn read_map(&self, name: &str) -> Result { + if !self.maps.contains_key(name) { + bail!("unknown map: {name:?}"); + } + let path = data_dir().join(format!("maps/{name}.yaml")); + Ok(read_to_string(path).await?) + } + pub async fn read_demands(&self, name: &str) -> Result { + if !self.demands.contains(name) { + bail!("unknown demands: {name:?}"); + } + let path = data_dir().join(format!("demands/{name}.yaml")); + Ok(read_to_string(path).await?) + } + pub async fn read_recipes(&self, name: &str) -> Result { + if !self.recipes.contains(name) { + bail!("unknown recipes: {name:?}"); + } + let path = data_dir().join(format!("recipes/{name}.yaml")); + Ok(read_to_string(path).await?) + } + + pub async fn generate(&self, spec: String) -> Result { + 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 recipes_in = serde_yaml::from_str(&self.read_recipes(recipes).await?)?; + + 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, recipes_in: Vec) -> Result { + let reg = ItemTileRegistry::default(); + let mut recipes = Vec::new(); + let mut entities = Vec::new(); + let mut raw_demands = Vec::new(); + + for mut r in recipes_in { + let r2 = r.clone(); + let mut inputs = r.inputs.into_iter().map(|i| reg.register_item(i)); + let mut outputs = r.outputs.into_iter().map(|o| reg.register_item(o)); + let tile = r.tile.map(|t| reg.register_tile(t)); + match r.action { + Action::Never => {} + Action::Passive => recipes.push(Recipe::Passive { + duration: r.duration.ok_or(anyhow!("duration for passive missing"))?, + warn: r.warn, + tile, + revert_duration: r.revert_duration, + input: inputs + .next() + .ok_or(anyhow!("passive recipe without input"))?, + output: outputs.next(), + }), + Action::Active => recipes.push(Recipe::Active { + duration: r.duration.ok_or(anyhow!("duration for active missing"))?, + tile, + input: inputs + .next() + .ok_or(anyhow!("active recipe without input"))?, + outputs: [outputs.next(), outputs.next()], + }), + Action::Instant => { + recipes.push(Recipe::Instant { + points: r.points.take().unwrap_or(0), + tile, + inputs: [inputs.next(), inputs.next()], + 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") + } + + // 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); + if tile == map_in.chef_spawn { + chef_spawn = pos.as_vec2() + Vec2::splat(0.5); + } + if tile == map_in.customer_spawn { + customer_spawn = pos.as_vec2() + Vec2::splat(0.5); + } + let tilename = map_in + .tiles + .get(&tile) + .ok_or(anyhow!("tile {tile} is undefined"))? + .clone(); + if let Some(ent) = map_in.tile_entities.get(&tile) { + entities.push(construct_entity(Some(pos), ent, ®)?); + } + 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)); + } + } + + entities.extend( + map_in + .entities + .iter() + .map(|decl| construct_entity(None, decl, ®)) + .try_collect::>()?, + ); + + 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 + .iter() + .map(|i| !map_in.walkable.contains(i)) + .collect(); + let tile_interact = tile_names + .iter() + .map(|i| !map_in.collider.contains(i) && !map_in.walkable.contains(i)) + .collect(); + + Ok(Gamedata { + spec, + demands, + tile_collide, + tile_interact, + recipes, + map: HashMap::new(), + initial_map, + item_names, + entities, + tile_names, + chef_spawn, + customer_spawn, + }) + } +} + +#[derive(Default)] +pub struct ItemTileRegistry { + tiles: RwLock>, + items: RwLock>, +} + +impl ItemTileRegistry { + pub fn register_tile(&self, name: String) -> TileIndex { + TileIndex(Self::register(&self.tiles, name)) + } + pub fn register_item(&self, name: String) -> ItemIndex { + ItemIndex(Self::register(&self.items, name)) + } + fn register(db: &RwLock>, name: String) -> usize { + let mut db = db.write().unwrap(); + if let Some(index) = db.iter().position(|e| e == &name) { + index + } else { + let index = db.len(); + db.push(name); + index + } + } +} + +impl Gamedata { + pub fn tile_name(&self, index: TileIndex) -> &String { + &self.tile_names[index.0] + } + pub fn is_tile_colliding(&self, index: TileIndex) -> bool { + self.tile_collide[index.0] + } + pub fn is_tile_interactable(&self, index: TileIndex) -> bool { + self.tile_interact[index.0] + } + pub fn item_name(&self, index: ItemIndex) -> &String { + &self.item_names[index.0] + } + pub fn recipe(&self, index: RecipeIndex) -> &Recipe { + &self.recipes[index.0] + } + pub fn demand(&self, index: DemandIndex) -> &Demand { + &self.demands[index.0] + } + pub fn get_tile_by_name(&self, name: &str) -> Option { + self.tile_names + .iter() + .position(|t| t == name) + .map(TileIndex) + } + pub fn get_item_by_name(&self, name: &str) -> Option { + self.item_names + .iter() + .position(|t| t == name) + .map(ItemIndex) + } + pub fn recipes(&self) -> impl Iterator { + self.recipes + .iter() + .enumerate() + .map(|(i, e)| (RecipeIndex(i), e)) + } +} 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 { -- cgit v1.2.3-70-g09d2