summaryrefslogtreecommitdiff
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/bot/Cargo.toml1
-rw-r--r--server/bot/src/algos/mod.rs29
-rw-r--r--server/bot/src/algos/simple.rs136
-rw-r--r--server/bot/src/algos/test.rs17
-rw-r--r--server/bot/src/algos/waiter.rs112
-rw-r--r--server/bot/src/lib.rs19
-rw-r--r--server/bot/src/main.rs47
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>,
-}