diff options
| author | metamuffin <metamuffin@disroot.org> | 2025-10-20 20:11:02 +0200 |
|---|---|---|
| committer | metamuffin <metamuffin@disroot.org> | 2025-10-20 20:11:02 +0200 |
| commit | a52785f4869a09e05417f97aff1c0d5b19587463 (patch) | |
| tree | 9d288a969a6da19ddb2848ac18a22f9d3c1879b7 /server/bot | |
| parent | f8d95d074c36ec35eee8def73b8d9f2b83c922cb (diff) | |
| download | hurrycurry-a52785f4869a09e05417f97aff1c0d5b19587463.tar hurrycurry-a52785f4869a09e05417f97aff1c0d5b19587463.tar.bz2 hurrycurry-a52785f4869a09e05417f97aff1c0d5b19587463.tar.zst | |
Refactor bot input to packet based
Diffstat (limited to 'server/bot')
| -rw-r--r-- | server/bot/Cargo.toml | 3 | ||||
| -rw-r--r-- | server/bot/src/algos/customer.rs | 278 | ||||
| -rw-r--r-- | server/bot/src/algos/dishwasher.rs | 63 | ||||
| -rw-r--r-- | server/bot/src/algos/frank.rs | 75 | ||||
| -rw-r--r-- | server/bot/src/algos/mod.rs | 18 | ||||
| -rw-r--r-- | server/bot/src/algos/simple.rs | 76 | ||||
| -rw-r--r-- | server/bot/src/algos/test.rs | 67 | ||||
| -rw-r--r-- | server/bot/src/algos/waiter.rs | 59 | ||||
| -rw-r--r-- | server/bot/src/lib.rs | 38 | ||||
| -rw-r--r-- | server/bot/src/main.rs | 33 | ||||
| -rw-r--r-- | server/bot/src/pathfinding.rs | 4 | ||||
| -rw-r--r-- | server/bot/src/step.rs | 121 |
12 files changed, 397 insertions, 438 deletions
diff --git a/server/bot/Cargo.toml b/server/bot/Cargo.toml index ad63f709..8a841843 100644 --- a/server/bot/Cargo.toml +++ b/server/bot/Cargo.toml @@ -11,3 +11,6 @@ anyhow = "1.0.99" env_logger = "0.11.8" rustls = { version = "0.23.31", features = ["ring"] } clap = { version = "4.5.47", features = ["derive"] } + +[features] +debug_events = [] diff --git a/server/bot/src/algos/customer.rs b/server/bot/src/algos/customer.rs index 5a6f71f7..6ae67f69 100644 --- a/server/bot/src/algos/customer.rs +++ b/server/bot/src/algos/customer.rs @@ -1,5 +1,3 @@ -use std::random::random; - /* Hurry Curry! - a game about cooking Copyright (C) 2025 Hurry Curry! Contributors @@ -18,7 +16,7 @@ use std::random::random; */ use crate::{ - BotAlgo, BotInput, + BotAlgo, PacketSink, pathfinding::{Path, find_path}, random_float, }; @@ -28,10 +26,11 @@ use hurrycurry_protocol::{ glam::{IVec2, Vec2}, }; use log::debug; +use std::random::random; -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] pub struct Customer { - config: CustomerConfig, + me: PlayerID, state: CustomerState, } @@ -74,34 +73,31 @@ enum CustomerState { Exiting { path: Path, }, + Exited, } impl Customer { - pub fn new(config: CustomerConfig) -> Self { + pub fn new(me: PlayerID) -> Self { Customer { - config, + me, state: CustomerState::default(), } } } impl BotAlgo for Customer { - fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput { - let Some(playerdata) = game.players.get(&me) else { - return BotInput::default(); - }; - let pos = playerdata.movement.position; - self.state.tick(me, &self.config, pos, game, dt) + fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) { + self.state.tick(&mut out, self.me, game, dt); + } + fn is_finished(&self) -> bool { + matches!(self.state, CustomerState::Exited) } } impl CustomerState { - fn tick( - &mut self, - me: PlayerID, - config: &CustomerConfig, - pos: Vec2, - game: &Game, - dt: f32, - ) -> BotInput { + fn tick(&mut self, out: &mut PacketSink, me: PlayerID, game: &Game, dt: f32) { + let Some(playerdata) = game.players.get(&me) else { + return; + }; + let pos = playerdata.movement.position; match self { CustomerState::New => { if !game.data.demands.is_empty() { @@ -123,7 +119,6 @@ impl CustomerState { }; } } - BotInput::default() } CustomerState::Entering { path, @@ -158,22 +153,19 @@ impl CustomerState { check: 0, pinned: false, }; - let message_item = if config.unknown_order { + let message_item = if !game.data.flags.disable_unknown_orders { game.data .get_item_by_name("unknown-order") .unwrap_or(requested_item) } else { requested_item }; - BotInput { - extra: vec![PacketS::Communicate { - message: Some(Message::Item(message_item)), - timeout: Some(timeout), - player: me, - pin: Some(false), - }], - ..Default::default() - } + out.push(PacketS::Communicate { + message: Some(Message::Item(message_item)), + timeout: Some(timeout), + player: me, + pin: Some(false), + }); } else if check && path.remaining_segments() < 5 && game.players.iter().any(|(id, p)| { @@ -183,17 +175,17 @@ impl CustomerState { }) { *self = CustomerState::New; - BotInput::default() } else if path.is_stuck() { if let Some(path) = find_path(game, pos.as_ivec2(), *origin) { *self = CustomerState::Exiting { path }; } - BotInput::default() } else { - BotInput { - direction: path.next_direction(pos, dt) * 0.6, - ..Default::default() - } + out.push(PacketS::Movement { + player: me, + dir: path.next_direction(pos, dt) * 0.6, + boost: false, + pos: None, + }); } } CustomerState::Waiting { @@ -210,34 +202,30 @@ impl CustomerState { if *timeout <= 0. { let path = find_path(game, pos.as_ivec2(), *origin).expect("no path to exit"); debug!("{me:?} -> exiting"); - *self = CustomerState::Exiting { path }; - return BotInput { - extra: vec![ - PacketS::Communicate { - message: None, - timeout: Some(0.), - player: me, - pin: Some(false), - }, - PacketS::ApplyScore(Score { - points: -1, - demands_failed: 1, - ..Default::default() - }), - PacketS::Effect { - name: "angry".to_string(), - player: me, - }, - ], + out.push(PacketS::Communicate { + message: None, + timeout: Some(0.), + player: me, + pin: Some(false), + }); + out.push(PacketS::ApplyScore(Score { + points: -1, + demands_failed: 1, ..Default::default() - }; + })); + out.push(PacketS::Effect { + name: "angry".to_string(), + player: me, + }); + *self = CustomerState::Exiting { path }; + return; } else if *check > 10 { let demand_data = &game.data.demands[demand.0]; *check = 0; if !*pinned { let mut pin = false; - if config.unknown_order { + if !game.data.flags.disable_unknown_orders { game.players_spatial_index.query(pos, 3., |pid, _| { if game .players @@ -252,15 +240,12 @@ impl CustomerState { } if pin { *pinned = true; - return BotInput { - extra: vec![PacketS::Communicate { - player: me, - message: Some(Message::Item(demand_data.input)), - timeout: Some(*timeout), - pin: Some(true), - }], - ..Default::default() - }; + out.push(PacketS::Communicate { + player: me, + message: Some(Message::Item(demand_data.input)), + timeout: Some(*timeout), + pin: Some(true), + }); } } @@ -287,6 +272,37 @@ impl CustomerState { if let Some(pos) = demand_pos { debug!("{me:?} -> eating"); let points = game.data.demands[demand.0].points; + out.push(PacketS::Communicate { + message: None, + timeout: Some(0.), + player: me, + pin: Some(false), + }); + out.push(PacketS::Communicate { + message: None, + timeout: Some(0.), + player: me, + pin: Some(true), + }); + out.push(PacketS::Effect { + name: "satisfied".to_string(), + player: me, + }); + out.push(PacketS::Interact { + target: Some(ItemLocation::Tile(pos)), + player: me, + hand: Hand(0), + }); + out.push(PacketS::ApplyScore(Score { + demands_completed: 1, + points, + ..Default::default() + })); + out.push(PacketS::Interact { + target: None, + player: me, + hand: Hand(0), + }); *self = CustomerState::Eating { demand: *demand, table: pos, @@ -294,48 +310,15 @@ impl CustomerState { chair: *chair, origin: *origin, }; - return BotInput { - extra: vec![ - PacketS::Communicate { - message: None, - timeout: Some(0.), - player: me, - pin: Some(false), - }, - PacketS::Communicate { - message: None, - timeout: Some(0.), - player: me, - pin: Some(true), - }, - PacketS::Effect { - name: "satisfied".to_string(), - player: me, - }, - PacketS::Interact { - target: Some(ItemLocation::Tile(pos)), - player: me, - hand: Hand(0), - }, - PacketS::ApplyScore(Score { - demands_completed: 1, - points, - ..Default::default() - }), - PacketS::Interact { - target: None, - player: me, - hand: Hand(0), - }, - ], - ..Default::default() - }; + return; } } - BotInput { - direction: dir_input(pos, *chair, *facing), - ..Default::default() - } + out.push(PacketS::Movement { + player: me, + dir: dir_input(pos, *chair, *facing), + boost: false, + pos: None, + }); } CustomerState::Eating { @@ -354,19 +337,19 @@ impl CustomerState { origin: *origin, cooldown: 0.5, }; - return BotInput { - extra: vec![PacketS::ReplaceHand { - player: me, - item: demand.output, - hand: Hand(0), - }], - ..Default::default() - }; - } - BotInput { - direction: dir_input(pos, *chair, (*table - *chair).as_vec2()), - ..Default::default() + out.push(PacketS::ReplaceHand { + player: me, + item: demand.output, + hand: Hand(0), + }); + return; } + out.push(PacketS::Movement { + player: me, + dir: dir_input(pos, *chair, (*table - *chair).as_vec2()), + boost: false, + pos: None, + }); } CustomerState::Finishing { table, @@ -383,49 +366,42 @@ impl CustomerState { if let Some(path) = find_path(game, pos.as_ivec2(), *origin) { *self = CustomerState::Exiting { path }; } - BotInput::default() } else { - let direction = (table.as_vec2() + 0.5) - pos; if *cooldown < 0. { *cooldown += 1.; - BotInput { - extra: vec![ - PacketS::Interact { - player: me, - target: Some(ItemLocation::Tile(*table)), - hand: Hand(0), - }, - PacketS::Interact { - player: me, - target: None, - hand: Hand(0), - }, - ], - direction, - ..Default::default() - } - } else { - BotInput { - direction, - ..Default::default() - } + out.push(PacketS::Interact { + player: me, + target: Some(ItemLocation::Tile(*table)), + hand: Hand(0), + }); + out.push(PacketS::Interact { + player: me, + target: None, + hand: Hand(0), + }); } + out.push(PacketS::Movement { + player: me, + dir: (table.as_vec2() + 0.5) - pos, + boost: false, + pos: None, + }); } } CustomerState::Exiting { path } => { if path.is_done() || path.is_stuck() { debug!("{me:?} -> leave"); - BotInput { - leave: true, - ..Default::default() - } + *self = CustomerState::Exited } else { - BotInput { - direction: path.next_direction(pos, dt) * 0.6, - ..Default::default() - } + out.push(PacketS::Movement { + player: me, + dir: path.next_direction(pos, dt) * 0.6, + boost: false, + pos: None, + }); } } + CustomerState::Exited => (), } } } diff --git a/server/bot/src/algos/dishwasher.rs b/server/bot/src/algos/dishwasher.rs index 5dbf954f..563e8c54 100644 --- a/server/bot/src/algos/dishwasher.rs +++ b/server/bot/src/algos/dishwasher.rs @@ -16,76 +16,53 @@ */ use super::simple::State; -use crate::{BotAlgo, BotInput, algos::simple::Context, pathfinding::Path}; +use crate::{BotAlgo, PacketSink, algos::simple::Context, step::StepState}; use hurrycurry_game_core::Game; -use hurrycurry_protocol::{ItemIndex, PlayerID, glam::IVec2}; +use hurrycurry_protocol::{ItemIndex, PlayerID}; -#[derive(Default)] pub struct DishWasher { - path: Option<(Path, IVec2, f32)>, - cooldown: f32, + me: PlayerID, + step: StepState, dirty_plate: Option<ItemIndex>, } type LogicRes<Out = ()> = Result<Out, ()>; +impl DishWasher { + pub fn new(me: PlayerID) -> Self { + Self { + me, + step: StepState::new_idle(me), + dirty_plate: None, + } + } +} impl BotAlgo for DishWasher { - fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput { + fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) { if self.dirty_plate.is_none() { self.dirty_plate = game.data.get_item_by_name("dirty-plate"); if self.dirty_plate.is_none() { - return BotInput::default(); + return; } } - - 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 self.step.is_busy() { + return self.step.tick(&mut out, game, dt); } - if let Some((path, target, down)) = &mut self.path { - let direction = path.next_direction(pos, dt); - 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 }, - ..Default::default() - }; - } Context { game, - own_position: pos.as_ivec2(), - me, + me: self.me, state: self, recursion_abort: 0, } .update() .ok(); - - BotInput::default() } } impl State for DishWasher { - 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 queue_step(&mut self, step: StepState) { + self.step = step; } fn get_empty_tile_priority(&self) -> &'static [&'static str] { &["counter-window", "counter"] diff --git a/server/bot/src/algos/frank.rs b/server/bot/src/algos/frank.rs index 489417a6..eeee3b61 100644 --- a/server/bot/src/algos/frank.rs +++ b/server/bot/src/algos/frank.rs @@ -1,5 +1,3 @@ -use std::random::random; - /* Hurry Curry! - a game about cooking Copyright (C) 2025 Hurry Curry! Contributors @@ -18,14 +16,16 @@ use std::random::random; */ use crate::{ - BotAlgo, BotInput, + BotAlgo, PacketSink, pathfinding::{Path, find_path_to_neighbour}, }; use hurrycurry_game_core::Game; use hurrycurry_protocol::{Message, PacketS, PlayerClass, PlayerID, glam::Vec2}; +use std::random::random; -#[derive(Default)] pub struct Frank { + me: PlayerID, + sleep: f32, idle: f32, idle_dir: f32, @@ -34,23 +34,38 @@ pub struct Frank { path: Option<Path>, } +impl Frank { + pub fn new(me: PlayerID) -> Self { + Self { + me, + idle: 0., + idle_dir: 0., + path: None, + sleep: 0., + target: None, + } + } +} + impl BotAlgo for Frank { - fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput { - let Some(player) = game.players.get(&me) else { - return BotInput::default(); + fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) { + let Some(player) = game.players.get(&self.me) else { + return; }; if self.sleep > 0. { self.sleep -= dt; - return BotInput::default(); + return; } if self.idle > 0. { self.idle -= dt; self.idle_dir += self.idle.sin() * dt * (self.idle / 10.).fract() * 10.; - return BotInput { - direction: Vec2::from_angle(self.idle_dir), - ..Default::default() - }; + out.push(PacketS::Movement { + player: self.me, + dir: Vec2::from_angle(self.idle_dir), + boost: false, + pos: None, + }); } let pos = player.movement.position; @@ -68,26 +83,25 @@ impl BotAlgo for Frank { if pos.distance(tpos) < 2. { self.sleep = 8.; self.idle = 15.; - return BotInput { - extra: vec![PacketS::Communicate { - player: me, - message: Some(Message::Translation { - id: format!("s.bot.frank.line.{}", random::<u32>(..) % 8), - params: vec![Message::Text(player.name.clone())], - }), - timeout: Some(3.), - pin: Some(false), - }], - ..Default::default() - }; + out.push(PacketS::Communicate { + player: self.me, + message: Some(Message::Translation { + id: format!("s.bot.frank.line.{}", random::<u32>(..) % 8), + params: vec![Message::Text(player.name.clone())], + }), + timeout: Some(3.), + pin: Some(false), + }); + return; } } else { - return BotInput { - direction, + out.push(PacketS::Movement { + player: self.me, + dir: direction, boost: false, - interact: None, - ..Default::default() - }; + pos: None, + }); + return; } } else if let Some(path) = find_path_to_neighbour(game, pos.as_ivec2(), tpos.as_ivec2()) @@ -97,10 +111,9 @@ impl BotAlgo for Frank { } else { self.target = None; } - } else if let Some(target) = find_chef(game, me) { + } else if let Some(target) = find_chef(game, self.me) { self.target = Some(target); } - BotInput::default() } } diff --git a/server/bot/src/algos/mod.rs b/server/bot/src/algos/mod.rs index ad6b1586..178ecec2 100644 --- a/server/bot/src/algos/mod.rs +++ b/server/bot/src/algos/mod.rs @@ -19,22 +19,22 @@ mod customer; mod dishwasher; mod frank; mod simple; -mod test; mod waiter; pub use customer::{Customer, CustomerConfig}; pub use dishwasher::DishWasher; pub use frank::Frank; +use hurrycurry_protocol::PlayerID; pub use simple::Simple; -pub use test::Test; pub use waiter::Waiter; +pub type BotAlgoConstructor = fn(PlayerID) -> crate::DynBotAlgo; + #[allow(clippy::type_complexity)] -pub const ALGO_CONSTRUCTORS: &[(&str, fn() -> crate::DynBotAlgo)] = &[ - ("test", || Box::new(Test::default())), - ("simple", || Box::new(Simple::default())), - ("waiter", || Box::new(Waiter::default())), - ("dishwasher", || Box::new(DishWasher::default())), - ("customer", || Box::new(Customer::default())), - ("frank", || Box::new(Frank::default())), +pub const ALGO_CONSTRUCTORS: &[(&str, BotAlgoConstructor)] = &[ + ("simple", |p| Box::new(Simple::new(p))), + ("waiter", |p| Box::new(Waiter::new(p))), + ("dishwasher", |p| Box::new(DishWasher::new(p))), + ("customer", |p| Box::new(Customer::new(p))), + ("frank", |p| Box::new(Frank::new(p))), ]; diff --git a/server/bot/src/algos/simple.rs b/server/bot/src/algos/simple.rs index 9c17cd82..050c2081 100644 --- a/server/bot/src/algos/simple.rs +++ b/server/bot/src/algos/simple.rs @@ -15,89 +15,59 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use crate::{ - BotAlgo, BotInput, - pathfinding::{Path, find_path_to_neighbour}, -}; +use crate::{BotAlgo, PacketSink, step::StepState}; use hurrycurry_game_core::Game; use hurrycurry_protocol::{ - ItemIndex, Message, PlayerID, Recipe, RecipeIndex, TileIndex, glam::IVec2, + Hand, ItemIndex, Message, PlayerID, Recipe, RecipeIndex, TileIndex, glam::IVec2, }; use log::{debug, warn}; -#[derive(Default)] pub struct Simple { - path: Option<(Path, IVec2, f32)>, - cooldown: f32, + step: StepState, + me: PlayerID, } 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, ()>; -impl BotAlgo for Simple { - 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(); +impl Simple { + pub fn new(me: PlayerID) -> Self { + Self { + me, + step: StepState::new_idle(me), } - - if let Some((path, target, down)) = &mut self.path { - let direction = path.next_direction(pos, dt); - 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 }, - ..Default::default() - }; + } +} +impl BotAlgo for Simple { + fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) { + if self.step.is_busy() { + return self.step.tick(&mut out, game, dt); } Context { game, - own_position: pos.as_ivec2(), - me, + me: self.me, state: self, recursion_abort: 0, } .update() .ok(); - debug!("target={:?}", self.path.as_ref().map(|a| a.1)); - - BotInput::default() } } pub trait State { - fn cooldown(&mut self, duration: f32); - fn queue_segment(&mut self, path: Path, tile: IVec2, duration: f32); + fn queue_step(&mut self, step: StepState); 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 queue_step(&mut self, step: StepState) { + self.step = step; } fn get_empty_tile_priority(&self) -> &'static [&'static str] { &["counter", "counter-window"] @@ -243,8 +213,8 @@ impl<S: State> Context<'_, S> { } } pub fn interact_with(&mut self, tile: IVec2, duration: f32) -> LogicRes { - if let Some(path) = find_path_to_neighbour(self.game, self.own_position, tile) { - self.state.queue_segment(path, tile, duration); + if let Some(step) = StepState::new_segment(self.game, self.me, Hand(0), tile, duration) { + self.state.queue_step(step); Err(()) } else { Ok(()) @@ -333,7 +303,7 @@ impl Context<'_, Simple> { 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.queue_step(StepState::new_wait(self.me, 0.5)); return Err(()); // waiting for it to finish } else { self.assert_tile_is_clear(pos)?; @@ -348,7 +318,7 @@ impl Context<'_, Simple> { } => { self.aquire_placed_item(*input)?; debug!("waiting for passive to finish"); - self.state.cooldown(0.5); + self.state.queue_step(StepState::new_wait(self.me, 0.5)); return Err(()); } _ => warn!("recipe too hard {r:?}"), diff --git a/server/bot/src/algos/test.rs b/server/bot/src/algos/test.rs deleted file mode 100644 index 387dd285..00000000 --- a/server/bot/src/algos/test.rs +++ /dev/null @@ -1,67 +0,0 @@ -/* - Hurry Curry! - a game about cooking - Copyright (C) 2025 Hurry Curry! Contributors - - 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::{ - BotAlgo, BotInput, - pathfinding::{Path, find_path_to_neighbour}, -}; -use hurrycurry_game_core::Game; -use hurrycurry_protocol::{ItemIndex, Message, PlayerID, glam::IVec2}; -use log::info; - -#[derive(Default)] -pub struct Test { - path: Option<Path>, -} - -impl BotAlgo for Test { - 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 let Some(path) = &mut self.path { - let direction = path.next_direction(pos, dt); - return BotInput { - direction, - boost: false, - interact: None, - ..Default::default() - }; - } else if let Some((item, near)) = find_demand(game) { - info!("demand {item:?} near {near}"); - if let Some(path) = find_path_to_neighbour(game, pos.as_ivec2(), near) { - self.path = Some(path); - } - } - BotInput::default() - } -} - -fn find_demand(game: &Game) -> Option<(ItemIndex, IVec2)> { - game.players - .iter() - .find_map(|(_, pl)| match &pl.communicate_persist { - Some((Message::Item(item), _)) => { - let pos = pl.movement.position.as_ivec2(); - let t = pos; - Some((*item, t)) - } - _ => None, - }) -} diff --git a/server/bot/src/algos/waiter.rs b/server/bot/src/algos/waiter.rs index 9273eb42..b1400ed2 100644 --- a/server/bot/src/algos/waiter.rs +++ b/server/bot/src/algos/waiter.rs @@ -16,69 +16,46 @@ */ use super::simple::State; -use crate::{BotAlgo, BotInput, algos::simple::Context, pathfinding::Path}; +use crate::{BotAlgo, PacketSink, algos::simple::Context, step::StepState}; use hurrycurry_game_core::Game; -use hurrycurry_protocol::{ItemIndex, PlayerID, glam::IVec2}; +use hurrycurry_protocol::{ItemIndex, PlayerID}; use log::debug; -#[derive(Default)] pub struct Waiter { - path: Option<(Path, IVec2, f32)>, - cooldown: f32, + step: StepState, + me: PlayerID, } type LogicRes<Out = ()> = Result<Out, ()>; +impl Waiter { + pub fn new(me: PlayerID) -> Self { + Self { + step: StepState::new_idle(me), + me, + } + } +} 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(); + fn tick(&mut self, mut out: PacketSink, game: &Game, dt: f32) { + if self.step.is_busy() { + return self.step.tick(&mut out, game, dt); } - if let Some((path, target, down)) = &mut self.path { - let direction = path.next_direction(pos, dt); - 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 }, - ..Default::default() - }; - } Context { game, - own_position: pos.as_ivec2(), - me, + me: self.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 queue_step(&mut self, step: StepState) { + self.step = step; } fn get_empty_tile_priority(&self) -> &'static [&'static str] { &["counter-window", "counter"] diff --git a/server/bot/src/lib.rs b/server/bot/src/lib.rs index a3889021..337d18e8 100644 --- a/server/bot/src/lib.rs +++ b/server/bot/src/lib.rs @@ -18,31 +18,37 @@ #![feature(random)] pub mod algos; pub mod pathfinding; +pub mod step; use hurrycurry_game_core::Game; -use hurrycurry_protocol::{ - PacketS, PlayerID, - glam::{IVec2, Vec2}, -}; -use std::random::random; +use hurrycurry_protocol::PacketS; +use std::{collections::VecDeque, random::random}; -#[derive(Default, Clone)] -pub struct BotInput { - pub direction: Vec2, - pub boost: bool, - pub interact: Option<IVec2>, - pub leave: bool, - pub extra: Vec<PacketS>, +pub struct PacketSink<'a> { + buf: &'a mut VecDeque<PacketS>, +} +impl<'a> PacketSink<'a> { + pub fn new(buf: &'a mut VecDeque<PacketS>) -> Self { + Self { buf } + } + pub fn push(&mut self, p: PacketS) { + self.buf.push_back(p); + } } pub type DynBotAlgo = Box<dyn BotAlgo + Send + Sync + 'static>; pub trait BotAlgo { - fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput; + fn tick(&mut self, out: PacketSink, game: &Game, dt: f32); + fn is_finished(&self) -> bool { + false + } } - impl<T: BotAlgo + ?Sized> BotAlgo for Box<T> { - fn tick(&mut self, me: PlayerID, game: &Game, dt: f32) -> BotInput { - (**self).tick(me, game, dt) + fn tick(&mut self, out: PacketSink, game: &Game, dt: f32) { + (**self).tick(out, game, dt) + } + fn is_finished(&self) -> bool { + (**self).is_finished() } } diff --git a/server/bot/src/main.rs b/server/bot/src/main.rs index 2fbf274f..fff25d63 100644 --- a/server/bot/src/main.rs +++ b/server/bot/src/main.rs @@ -17,9 +17,9 @@ */ use anyhow::Result; use clap::Parser; -use hurrycurry_bot::{BotAlgo, BotInput, algos::ALGO_CONSTRUCTORS}; +use hurrycurry_bot::{BotAlgo, PacketSink, algos::ALGO_CONSTRUCTORS}; use hurrycurry_game_core::{Game, network::sync::Network}; -use hurrycurry_protocol::{Character, Hand, ItemLocation, PacketC, PacketS, PlayerClass, PlayerID}; +use hurrycurry_protocol::{Character, PacketC, PacketS, PlayerClass, PlayerID}; use log::warn; use std::{thread::sleep, time::Duration}; @@ -87,7 +87,7 @@ fn main() -> Result<()> { state: ALGO_CONSTRUCTORS .iter() .find(|(n, _)| n == &args.algo) - .map(|(_, c)| c()) + .map(|(_, c)| c(*id)) .unwrap_or_else(|| panic!("unknown algo {:?}", args.algo)), }), PacketC::ServerMessage { @@ -102,34 +102,13 @@ fn main() -> Result<()> { } bots.retain_mut(|b| { - let BotInput { - direction, - boost, - interact, - leave, - extra, - } = b.state.tick(b.id, &game, dt); + b.state + .tick(PacketSink::new(&mut network.queue_out), &game, dt); - if leave { + if b.state.is_finished() { network.queue_out.push_back(PacketS::Leave { player: b.id }); return false; } - - if interact.is_some() != b.interacting { - b.interacting = interact.is_some(); - network.queue_out.push_back(PacketS::Interact { - player: b.id, - target: interact.map(ItemLocation::Tile), - hand: Hand(0), - }) - } - network.queue_out.push_back(PacketS::Movement { - player: b.id, - dir: direction, - boost, - pos: None, - }); - network.queue_out.extend(extra); true }); diff --git a/server/bot/src/pathfinding.rs b/server/bot/src/pathfinding.rs index e17ca80c..ec098495 100644 --- a/server/bot/src/pathfinding.rs +++ b/server/bot/src/pathfinding.rs @@ -31,6 +31,10 @@ pub struct Path { } impl Path { + pub const EMPTY: Path = Path { + seg_time: 0., + segments: Vec::new(), + }; pub fn next_direction(&mut self, position: Vec2, dt: f32) -> Vec2 { if let Some(next) = self.segments.last().copied() { trace!("next {next}"); diff --git a/server/bot/src/step.rs b/server/bot/src/step.rs new file mode 100644 index 00000000..001935ac --- /dev/null +++ b/server/bot/src/step.rs @@ -0,0 +1,121 @@ +/* + Hurry Curry! - a game about cooking + Copyright (C) 2025 Hurry Curry! Contributors + + 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::{ + PacketSink, + pathfinding::{Path, find_path_to_neighbour}, +}; +use hurrycurry_game_core::Game; +use hurrycurry_protocol::{Hand, ItemIndex, ItemLocation, PacketS, PlayerID, glam::IVec2}; + +pub struct StepState { + path: Path, + interact_position: IVec2, + destination_item: Option<ItemIndex>, + hand: Hand, + wait_timer: f32, + interact_start_pending: bool, + interact_stop_pending: bool, + me: PlayerID, +} + +impl StepState { + pub fn new_idle(me: PlayerID) -> Self { + Self::new_wait(me, 0.) + } + pub fn new_wait(me: PlayerID, timer: f32) -> Self { + Self { + destination_item: None, + interact_position: IVec2::ZERO, + hand: Hand(0), + interact_start_pending: false, + interact_stop_pending: false, + me, + path: Path::EMPTY, + wait_timer: timer, + } + } + pub fn new_segment( + game: &Game, + me: PlayerID, + hand: Hand, + pos: IVec2, + interact: f32, + ) -> Option<Self> { + let own_pos = game.players.get(&me)?.movement.position.as_ivec2(); + let path = find_path_to_neighbour(game, own_pos, pos)?; + Some(Self { + me, + path, + hand, + wait_timer: interact, + interact_stop_pending: true, + interact_start_pending: true, + interact_position: pos, + destination_item: game + .tiles + .get(&pos) + .and_then(|t| t.item.as_ref().map(|i| i.kind)), + }) + } + + pub fn is_busy(&self) -> bool { + self.wait_timer >= 0. || self.interact_stop_pending + } + + pub fn tick(&mut self, out: &mut PacketSink, game: &Game, dt: f32) { + if self.path.is_done() { + if self.interact_start_pending { + self.interact_start_pending = false; + out.push(PacketS::Interact { + player: self.me, + hand: self.hand, + target: Some(ItemLocation::Tile(self.interact_position)), + }); + } else { + if self.wait_timer < 0. { + if self.interact_stop_pending { + self.interact_stop_pending = false; + out.push(PacketS::Interact { + player: self.me, + hand: self.hand, + target: None, + }); + } + } else { + self.wait_timer -= dt; + } + } + } + + let position = game + .players + .get(&self.me) + .as_ref() + .map(|p| p.movement.position) + .unwrap_or_default(); + + let dir = self.path.next_direction(position, dt); + out.push(PacketS::Movement { + player: self.me, + dir, + boost: false, + pos: None, + }); + } +} |