aboutsummaryrefslogtreecommitdiff
path: root/server/bot
diff options
context:
space:
mode:
authormetamuffin <metamuffin@disroot.org>2024-08-11 13:35:15 +0200
committermetamuffin <metamuffin@disroot.org>2024-08-11 13:35:15 +0200
commit52d99b16534631e293a23ddbc18c4ea70b71392f (patch)
treef596887a976540ab553e69105ab192cbbb2dd753 /server/bot
parent63d5a3ff37d1e3972d34ccc9e1d26c3b4bc05efb (diff)
downloadhurrycurry-52d99b16534631e293a23ddbc18c4ea70b71392f.tar
hurrycurry-52d99b16534631e293a23ddbc18c4ea70b71392f.tar.bz2
hurrycurry-52d99b16534631e293a23ddbc18c4ea70b71392f.tar.zst
add recipes back to protocol
Diffstat (limited to 'server/bot')
-rw-r--r--server/bot/Cargo.toml1
-rw-r--r--server/bot/src/main.rs134
-rw-r--r--server/bot/src/pathfinding.rs96
3 files changed, 224 insertions, 7 deletions
diff --git a/server/bot/Cargo.toml b/server/bot/Cargo.toml
index cd0ac383..bd8cbe36 100644
--- a/server/bot/Cargo.toml
+++ b/server/bot/Cargo.toml
@@ -8,3 +8,4 @@ hurrycurry-client-lib = { path = "../client-lib", features = ["tokio-network"] }
hurrycurry-protocol = { path = "../protocol" }
log = "0.4.22"
anyhow = "1.0.86"
+env_logger = "0.11.5"
diff --git a/server/bot/src/main.rs b/server/bot/src/main.rs
index 18c4617a..864141b0 100644
--- a/server/bot/src/main.rs
+++ b/server/bot/src/main.rs
@@ -15,13 +15,21 @@
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
+#![feature(isqrt)]
+pub mod pathfinding;
use anyhow::Result;
use hurrycurry_client_lib::{network::sync::Network, Game};
-use hurrycurry_protocol::{glam::Vec2, PacketC, PacketS, PlayerID};
+use hurrycurry_protocol::{
+ glam::{IVec2, Vec2},
+ ItemIndex, Message, PacketC, PacketS, PlayerID, RecipeIndex,
+};
+use log::{info, warn};
+use pathfinding::{find_path, Path};
use std::{thread::sleep, time::Duration};
fn main() -> Result<()> {
+ env_logger::init_from_env("LOG");
let mut network = Network::connect("ws://127.0.0.1")?;
let mut game = Game::default();
@@ -46,11 +54,19 @@ fn main() -> Result<()> {
game.apply_packet(packet);
}
- for b in &bots {
+ for b in &mut bots {
+ let (dir, boost, interact) = b.tick(&game);
+ if interact.is_some() != b.interacting {
+ b.interacting = interact.is_some();
+ network.queue_out.push_back(PacketS::Interact {
+ player: b.id,
+ pos: interact,
+ })
+ }
network.queue_out.push_back(PacketS::Movement {
player: b.id,
- dir: Vec2::ONE,
- boost: true,
+ dir,
+ boost,
pos: None,
});
}
@@ -60,14 +76,118 @@ fn main() -> Result<()> {
}
pub struct Bot {
+ pub interacting: bool,
+
id: PlayerID,
+ want: Option<ItemIndex>,
+ take: Option<IVec2>,
+ put: Option<IVec2>,
+ path: Option<Path>,
}
impl Bot {
pub fn new(id: PlayerID) -> Self {
- Self { id }
+ Self {
+ id,
+ want: None,
+ path: None,
+ take: None,
+ put: None,
+ interacting: false,
+ }
}
- pub fn tick(&self, game: &Game) {
- if let Some(player) = game.players.get(&self.id) {}
+ pub fn tick(&mut self, game: &Game) -> (Vec2, bool, Option<IVec2>) {
+ if let Some(player) = game.players.get(&self.id) {
+ let pos = player.movement.position;
+
+ if let Some(path) = &mut self.path {
+ let dir = path.next_direction(pos);
+ if path.is_done() {
+ self.path = None;
+ }
+ return (dir, false, None);
+ }
+ if let Some(interact) = self.take.take() {
+ return (Vec2::ZERO, false, Some(interact));
+ }
+ if let Some(item) = &player.item {
+ if Some(item.kind) == self.want {
+ if let Some(interact) = self.put.take() {
+ return (Vec2::ZERO, false, Some(interact));
+ }
+ }
+ }
+
+ if let Some(item) = self.want {
+ if let Some((path, target)) = find_item_on_map(game, pos.as_ivec2(), item) {
+ info!("target={target}");
+ info!("path found");
+ self.path = Some(path);
+ self.take = Some(target);
+ } else if let Some(recipe) = find_item_as_recipe_output(game, item) {
+ info!("recipe={recipe:?}");
+ self.want = game.data.recipes[recipe.0].outputs().first().copied();
+ info!("want={:?}", self.want)
+ } else {
+ warn!("stuck");
+ }
+ } else {
+ if let Some((item, dest)) = select_demand(game) {
+ info!("want={item:?}");
+ self.want = Some(item);
+ self.put = Some(dest);
+ }
+ }
+ }
+ (Vec2::ZERO, false, None)
}
}
+
+fn find_item_as_recipe_output(game: &Game, item: ItemIndex) -> Option<RecipeIndex> {
+ game.data
+ .recipes
+ .iter()
+ .enumerate()
+ .find(|(_, r)| r.inputs().contains(&item))
+ .map(|r| RecipeIndex(r.0))
+}
+
+fn find_item_on_map(game: &Game, player: IVec2, item: ItemIndex) -> Option<(Path, IVec2)> {
+ game.tiles.iter().find_map(|(pos, tile)| {
+ if let Some(i) = &tile.item {
+ if i.kind == item {
+ for xo in -1..=1 {
+ for yo in -1..=1 {
+ let t = *pos + IVec2::new(xo, yo);
+ if let Some(path) = find_path(&game.walkable, player, t) {
+ return Some((path, *pos));
+ }
+ }
+ }
+ }
+ }
+ None
+ })
+}
+
+fn select_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();
+ for xo in -1..=1 {
+ for yo in -1..=1 {
+ let t = pos + IVec2::new(xo, yo);
+ if let Some(tile) = game.tiles.get(&t) {
+ if game.data.tile_interact[tile.kind.0] {
+ return Some((*item, t));
+ }
+ }
+ }
+ }
+ None
+ }
+ _ => None,
+ })
+}
diff --git a/server/bot/src/pathfinding.rs b/server/bot/src/pathfinding.rs
new file mode 100644
index 00000000..87ccf391
--- /dev/null
+++ b/server/bot/src/pathfinding.rs
@@ -0,0 +1,96 @@
+/*
+ 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 hurrycurry_protocol::glam::{IVec2, Vec2};
+use log::trace;
+use std::{
+ cmp::Ordering,
+ collections::{BinaryHeap, HashMap, HashSet},
+};
+
+#[derive(Debug, Clone)]
+pub struct Path(Vec<Vec2>);
+
+impl Path {
+ pub fn next_direction(&mut self, position: Vec2) -> Vec2 {
+ if let Some(next) = self.0.last().copied() {
+ trace!("next {next}");
+ if next.distance(position) < if self.0.len() == 1 { 0.1 } else { 0.6 } {
+ self.0.pop();
+ }
+ (next - position).normalize_or_zero() * 0.5
+ } else {
+ Vec2::ZERO
+ }
+ }
+ pub fn is_done(&self) -> bool {
+ self.0.is_empty()
+ }
+}
+
+pub fn find_path(walkable: &HashSet<IVec2>, from: IVec2, to: IVec2) -> Option<Path> {
+ #[derive(Debug, PartialEq, Eq)]
+ struct Open(i32, IVec2, IVec2, i32);
+ impl PartialOrd for Open {
+ fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
+ Some(self.0.cmp(&other.0))
+ }
+ }
+ impl Ord for Open {
+ fn cmp(&self, other: &Self) -> Ordering {
+ self.0.cmp(&other.0)
+ }
+ }
+
+ let mut visited = HashMap::new();
+ let mut open = BinaryHeap::new();
+ open.push(Open(1, from, from, 0));
+
+ loop {
+ let Open(_, pos, f, distance) = open.pop()?;
+ if visited.contains_key(&pos) {
+ continue;
+ }
+ visited.insert(pos, f);
+ if pos == to {
+ break;
+ }
+ for dir in [IVec2::NEG_X, IVec2::NEG_Y, IVec2::X, IVec2::Y] {
+ let next = pos + dir;
+ if walkable.contains(&next) {
+ open.push(Open(
+ -(distance + next.distance_squared(to).isqrt()),
+ next,
+ pos,
+ distance + 1,
+ ));
+ }
+ }
+ }
+
+ let mut path = Vec::new();
+ let mut c = to;
+ loop {
+ path.push(c.as_vec2() + 0.5);
+ let cn = visited[&c];
+ if cn == c {
+ break;
+ }
+ c = cn
+ }
+ Some(Path(path))
+}