aboutsummaryrefslogtreecommitdiff
path: root/server/bot
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2025-10-20 20:11:02 +0200
committermetamuffin <metamuffin@disroot.org>2025-10-20 20:11:02 +0200
commita52785f4869a09e05417f97aff1c0d5b19587463 (patch)
tree9d288a969a6da19ddb2848ac18a22f9d3c1879b7 /server/bot
parentf8d95d074c36ec35eee8def73b8d9f2b83c922cb (diff)
downloadhurrycurry-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.toml3
-rw-r--r--server/bot/src/algos/customer.rs278
-rw-r--r--server/bot/src/algos/dishwasher.rs63
-rw-r--r--server/bot/src/algos/frank.rs75
-rw-r--r--server/bot/src/algos/mod.rs18
-rw-r--r--server/bot/src/algos/simple.rs76
-rw-r--r--server/bot/src/algos/test.rs67
-rw-r--r--server/bot/src/algos/waiter.rs59
-rw-r--r--server/bot/src/lib.rs38
-rw-r--r--server/bot/src/main.rs33
-rw-r--r--server/bot/src/pathfinding.rs4
-rw-r--r--server/bot/src/step.rs121
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,
+ });
+ }
+}