diff options
Diffstat (limited to 'server')
-rw-r--r-- | server/bot/Cargo.toml | 1 | ||||
-rw-r--r-- | server/bot/src/algos/mod.rs | 29 | ||||
-rw-r--r-- | server/bot/src/algos/simple.rs | 136 | ||||
-rw-r--r-- | server/bot/src/algos/test.rs | 17 | ||||
-rw-r--r-- | server/bot/src/algos/waiter.rs | 112 | ||||
-rw-r--r-- | server/bot/src/lib.rs | 19 | ||||
-rw-r--r-- | server/bot/src/main.rs | 47 |
7 files changed, 286 insertions, 75 deletions
diff --git a/server/bot/Cargo.toml b/server/bot/Cargo.toml index 2afc1e59..ee9706d8 100644 --- a/server/bot/Cargo.toml +++ b/server/bot/Cargo.toml @@ -11,3 +11,4 @@ anyhow = "1.0.86" env_logger = "0.11.5" rustls = { version = "0.23.12", features = ["ring"] } clap = { version = "4.5.15", features = ["derive"] } +rand = "0.9.0-alpha.2" diff --git a/server/bot/src/algos/mod.rs b/server/bot/src/algos/mod.rs index 920230e5..7b165da4 100644 --- a/server/bot/src/algos/mod.rs +++ b/server/bot/src/algos/mod.rs @@ -1,5 +1,30 @@ -mod simple; -pub use simple::Simple; +/* + 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/>. +*/ +mod simple; mod test; +mod waiter; + +pub use simple::Simple; pub use test::Test; +pub use waiter::Waiter; + +pub const ALGO_CONSTRUCTORS: &'static [(&'static str, fn() -> Box<dyn crate::BotAlgo>)] = &[ + ("test", || Box::new(Test::default())), + ("simple", || Box::new(Simple::default())), + ("waiter", || Box::new(Waiter::default())), +]; diff --git a/server/bot/src/algos/simple.rs b/server/bot/src/algos/simple.rs index afa5764f..22ed50bd 100644 --- a/server/bot/src/algos/simple.rs +++ b/server/bot/src/algos/simple.rs @@ -1,3 +1,20 @@ +/* + 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 crate::{ pathfinding::{find_path_to_neighbour, Path}, BotAlgo, BotInput, @@ -14,11 +31,12 @@ pub struct Simple { cooldown: f32, } -struct SimpleContext<'a> { - game: &'a Game, - me: PlayerID, - own_position: IVec2, - state: &'a mut Simple, +pub struct Context<'a, State> { + pub game: &'a Game, + pub me: PlayerID, + pub own_position: IVec2, + pub state: &'a mut State, + pub recursion_abort: usize, } type LogicRes<Out = ()> = Result<Out, ()>; @@ -53,11 +71,12 @@ impl BotAlgo for Simple { }; } - SimpleContext { + Context { game, own_position: pos.as_ivec2(), me, state: self, + recursion_abort: 0, } .update() .ok(); @@ -67,7 +86,24 @@ impl BotAlgo for Simple { } } -impl SimpleContext<'_> { +pub trait State { + fn cooldown(&mut self, duration: f32); + fn queue_segment(&mut self, path: Path, tile: IVec2, duration: f32); + fn get_empty_tile_priority(&self) -> &'static [&'static str]; +} +impl State for Simple { + fn cooldown(&mut self, duration: f32) { + self.cooldown = duration; + } + fn queue_segment(&mut self, path: Path, tile: IVec2, duration: f32) { + self.path = Some((path, tile, duration)); + } + fn get_empty_tile_priority(&self) -> &'static [&'static str] { + &["counter", "counter-window"] + } +} + +impl<S> Context<'_, S> { pub fn is_hand_item(&self, item: ItemIndex) -> bool { self.game .players @@ -81,7 +117,7 @@ impl SimpleContext<'_> { .map(|p| p.item.is_some()) .unwrap_or(false) } - fn find_demand(&self) -> Option<(ItemIndex, IVec2)> { + pub fn find_demand(&self) -> Option<(ItemIndex, IVec2)> { self.game .players .iter() @@ -102,7 +138,7 @@ impl SimpleContext<'_> { _ => None, }) } - fn find_recipe_with_output(&self, item: ItemIndex) -> Option<RecipeIndex> { + pub fn find_recipe_with_output(&self, item: ItemIndex) -> Option<RecipeIndex> { self.game .data .recipes @@ -111,70 +147,84 @@ impl SimpleContext<'_> { .find(|(_, r)| r.outputs().contains(&item)) .map(|(i, _)| RecipeIndex(i)) } - fn find_item_on_map(&self, item: ItemIndex) -> Option<IVec2> { + pub fn find_item_on_map(&self, item: ItemIndex) -> Option<IVec2> { self.game .tiles .iter() .find(|(_, t)| t.item.as_ref().map_or(false, |t| t.kind == item)) .map(|(p, _)| *p) } - fn find_tile(&self, tile: TileIndex) -> Option<IVec2> { + pub fn find_tile(&self, tile: TileIndex) -> Option<IVec2> { self.game .tiles .iter() .find(|(_, t)| t.kind == tile) .map(|(p, _)| *p) } - fn find_empty_interactable_tile_by_name(&self, name: &str) -> Option<IVec2> { + pub fn find_occupied_table_or_floor(&self) -> Option<IVec2> { self.game .tiles .iter() .find(|(_, t)| { - self.game.data.tile_interact[t.kind.0] - && t.item.is_none() - && self.game.data.tile_names[t.kind.0] == name + t.item.is_some() + && matches!( + self.game.data.tile_names[t.kind.0].as_str(), + "table" | "floor" + ) }) .map(|(p, _)| *p) } - fn find_empty_interactable_tile(&self) -> Option<IVec2> { - if let Some(t) = self.find_empty_interactable_tile_by_name("counter") { - return Some(t); - } - if let Some(t) = self.find_empty_interactable_tile_by_name("counter-window") { - return Some(t); - } - warn!("all counters filled up"); + pub fn find_empty_interactable_tile_by_name(&self, name: &str) -> Option<IVec2> { self.game .tiles .iter() - .find(|(_, t)| self.game.data.tile_interact[t.kind.0] && t.item.is_none()) + .find(|(_, t)| { + self.game.data.tile_interact[t.kind.0] + && t.item.is_none() + && self.game.data.tile_names[t.kind.0] == name + }) .map(|(p, _)| *p) } - fn is_tile_occupied(&self, pos: IVec2) -> bool { + + pub fn is_tile_occupied(&self, pos: IVec2) -> bool { self.game .tiles .get(&pos) .map(|t| t.item.is_some()) .unwrap_or(true) } - fn clear_tile(&mut self, pos: IVec2) -> LogicRes { +} +impl<S: State> Context<'_, S> { + pub fn find_empty_interactable_tile(&self) -> Option<IVec2> { + for p in self.state.get_empty_tile_priority() { + if let Some(t) = self.find_empty_interactable_tile_by_name(p) { + return Some(t); + } + } + warn!("all counters filled up"); + self.game + .tiles + .iter() + .find(|(_, t)| self.game.data.tile_interact[t.kind.0] && t.item.is_none()) + .map(|(p, _)| *p) + } + pub fn clear_tile(&mut self, pos: IVec2) -> LogicRes { debug!("clear tile {pos}"); self.assert_hand_is_clear()?; self.interact_with(pos, 0.) } - fn assert_tile_is_clear(&mut self, pos: IVec2) -> LogicRes { + pub fn assert_tile_is_clear(&mut self, pos: IVec2) -> LogicRes { if self.is_tile_occupied(pos) { self.clear_tile(pos)?; } Ok(()) } - fn assert_hand_is_clear(&mut self) -> LogicRes { + pub fn assert_hand_is_clear(&mut self) -> LogicRes { if self.is_hand_occupied() { self.dispose_hand()?; } Ok(()) } - pub fn dispose_hand(&mut self) -> LogicRes { debug!("dispose hand"); if let Some(pos) = self.find_empty_interactable_tile() { @@ -186,6 +236,17 @@ impl SimpleContext<'_> { Err(()) } } + pub fn interact_with(&mut self, tile: IVec2, duration: f32) -> LogicRes { + if let Some(path) = find_path_to_neighbour(&self.game.walkable, self.own_position, tile) { + self.state.queue_segment(path, tile, duration); + Err(()) + } else { + Ok(()) + } + } +} + +impl Context<'_, Simple> { pub fn aquire_placed_item(&mut self, item: ItemIndex) -> LogicRes<IVec2> { debug!("aquire placed item {:?}", self.game.data.item_names[item.0]); if let Some(pos) = self.find_item_on_map(item) { @@ -197,6 +258,11 @@ impl SimpleContext<'_> { } pub fn aquire_item(&mut self, item: ItemIndex) -> LogicRes { debug!("aquire item {:?}", self.game.data.item_names[item.0]); + self.recursion_abort += 1; + if self.recursion_abort > 32 { + warn!("too much recursion"); + return Err(()); + } if self.is_hand_item(item) { return Ok(()); } @@ -261,7 +327,7 @@ impl SimpleContext<'_> { if let Some(item) = &self.game.tiles.get(&pos).unwrap().item { if item.kind == *input { debug!("waiting for passive to finish at {pos}"); - self.state.cooldown = 0.5; + self.state.cooldown(0.5); return Err(()); // waiting for it to finish // TODO check progress } else { @@ -277,7 +343,7 @@ impl SimpleContext<'_> { } => { self.aquire_placed_item(*input)?; debug!("waiting for passive to finish"); - self.state.cooldown = 0.5; + self.state.cooldown(0.5); return Err(()); } _ => warn!("recipe too hard {r:?}"), @@ -289,14 +355,6 @@ impl SimpleContext<'_> { ); Err(()) } - pub fn interact_with(&mut self, tile: IVec2, duration: f32) -> LogicRes { - if let Some(path) = find_path_to_neighbour(&self.game.walkable, self.own_position, tile) { - self.state.path = Some((path, tile, duration)); - Err(()) - } else { - Ok(()) - } - } pub fn update(&mut self) -> LogicRes { if let Some((item, table)) = self.find_demand() { self.assert_tile_is_clear(table)?; diff --git a/server/bot/src/algos/test.rs b/server/bot/src/algos/test.rs index d17b079d..d56aa00b 100644 --- a/server/bot/src/algos/test.rs +++ b/server/bot/src/algos/test.rs @@ -1,3 +1,20 @@ +/* + 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 crate::{ pathfinding::{find_path_to_neighbour, Path}, BotAlgo, BotInput, diff --git a/server/bot/src/algos/waiter.rs b/server/bot/src/algos/waiter.rs new file mode 100644 index 00000000..7a25108b --- /dev/null +++ b/server/bot/src/algos/waiter.rs @@ -0,0 +1,112 @@ +/* + 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::simple::State; +use crate::{algos::simple::Context, pathfinding::Path, BotAlgo, BotInput}; +use hurrycurry_client_lib::Game; +use hurrycurry_protocol::{glam::IVec2, ItemIndex, PlayerID}; +use log::debug; + +#[derive(Default)] +pub struct Waiter { + path: Option<(Path, IVec2, f32)>, + cooldown: f32, +} + +type LogicRes<Out = ()> = Result<Out, ()>; + +impl BotAlgo for Waiter { + fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput { + let Some(player) = game.players.get(&me) else { + return BotInput::default(); + }; + let pos = player.movement.position; + + if self.cooldown > 0. { + self.cooldown -= dt; + return BotInput::default(); + } + + if let Some((path, target, down)) = &mut self.path { + let direction = path.next_direction(pos); + let arrived = path.is_done(); + let target = *target; + if arrived { + *down -= dt; + if *down < 0. { + self.path = None; + self.cooldown = 0.2; + } + } + return BotInput { + direction, + boost: false, + interact: if arrived { Some(target) } else { None }, + }; + } + Context { + game, + own_position: pos.as_ivec2(), + me, + state: self, + recursion_abort: 0, + } + .update() + .ok(); + + BotInput::default() + } +} + +impl State for Waiter { + fn cooldown(&mut self, dur: f32) { + self.cooldown += dur + } + fn queue_segment(&mut self, path: Path, tile: IVec2, duration: f32) { + self.path = Some((path, tile, duration)); + } + fn get_empty_tile_priority(&self) -> &'static [&'static str] { + &["counter-window", "counter"] + } +} + +impl Context<'_, Waiter> { + fn aquire_item(&mut self, item: ItemIndex) -> LogicRes<bool> { + debug!("aquire item {:?}", self.game.data.item_names[item.0]); + if self.is_hand_item(item) { + return Ok(true); + } + if let Some(pos) = self.find_item_on_map(item) { + self.assert_hand_is_clear()?; + self.interact_with(pos, 0.)?; + return Ok(true); + } + Ok(false) + } + fn update(&mut self) -> LogicRes { + if let Some(pos) = self.find_occupied_table_or_floor() { + self.assert_tile_is_clear(pos)?; + } + if let Some((item, table)) = self.find_demand() { + if self.aquire_item(item)? { + self.interact_with(table, 0.)?; + } + } + self.assert_hand_is_clear()?; + Ok(()) + } +} diff --git a/server/bot/src/lib.rs b/server/bot/src/lib.rs new file mode 100644 index 00000000..e8f05fd6 --- /dev/null +++ b/server/bot/src/lib.rs @@ -0,0 +1,19 @@ +#![feature(isqrt)] +pub mod algos; +pub mod pathfinding; + +use hurrycurry_client_lib::Game; +use hurrycurry_protocol::{ + glam::{IVec2, Vec2}, + PlayerID, +}; + +#[derive(Default, Clone, Copy)] +pub struct BotInput { + pub direction: Vec2, + pub boost: bool, + pub interact: Option<IVec2>, +} +pub trait BotAlgo { + fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput; +} diff --git a/server/bot/src/main.rs b/server/bot/src/main.rs index 08d17d19..9e3fbee1 100644 --- a/server/bot/src/main.rs +++ b/server/bot/src/main.rs @@ -15,36 +15,26 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -#![feature(isqrt)] -pub mod algos; -pub mod pathfinding; - use anyhow::Result; +use bot::{algos::ALGO_CONSTRUCTORS, BotAlgo, BotInput}; use clap::Parser; use hurrycurry_client_lib::{network::sync::Network, Game}; -use hurrycurry_protocol::{ - glam::{IVec2, Vec2}, - PacketC, PacketS, PlayerID, -}; +use hurrycurry_protocol::{PacketC, PacketS, PlayerID}; use log::warn; use std::{thread::sleep, time::Duration}; -#[derive(Default, Clone, Copy)] -pub struct BotInput { - direction: Vec2, - boost: bool, - interact: Option<IVec2>, -} -pub trait BotAlgo { - fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput; -} - #[derive(Parser)] struct Args { algo: String, address: String, } +pub struct BotDriver { + pub interacting: bool, + id: PlayerID, + state: Box<dyn BotAlgo>, +} + fn main() -> Result<()> { env_logger::init_from_env("LOG"); rustls::crypto::ring::default_provider() @@ -53,15 +43,6 @@ fn main() -> Result<()> { let args = Args::parse(); - let algo = args.algo.to_owned(); - let init_algo = move || -> Box<dyn BotAlgo> { - match algo.as_str() { - "test" => Box::new(algos::Test::default()), - "simple" => Box::new(algos::Simple::default()), - _ => panic!("unknown algo {algo:?}"), - } - }; - let mut network = Network::connect(&args.address)?; let mut game = Game::default(); @@ -83,7 +64,11 @@ fn main() -> Result<()> { PacketC::Joined { id } => bots.push(BotDriver { id: *id, interacting: false, - state: init_algo(), + state: ALGO_CONSTRUCTORS + .iter() + .find(|(n, _)| n == &args.algo) + .map(|(_, c)| c()) + .expect(&format!("unknown algo {:?}", args.algo)), }), PacketC::Error { message } => { warn!("server error message: {message}"); @@ -117,9 +102,3 @@ fn main() -> Result<()> { sleep(Duration::from_secs_f32(dt)); } } - -pub struct BotDriver { - pub interacting: bool, - id: PlayerID, - state: Box<dyn BotAlgo>, -} |