diff options
-rw-r--r-- | Cargo.lock | 1 | ||||
-rw-r--r-- | server/bot/Cargo.toml | 1 | ||||
-rw-r--r-- | server/bot/src/main.rs | 134 | ||||
-rw-r--r-- | server/bot/src/pathfinding.rs | 96 | ||||
-rw-r--r-- | server/protocol/src/lib.rs | 82 | ||||
-rw-r--r-- | server/src/bin/graph.rs | 4 | ||||
-rw-r--r-- | server/src/data.rs | 7 | ||||
-rw-r--r-- | server/src/entity/customers/demands.rs | 3 | ||||
-rw-r--r-- | server/src/entity/mod.rs | 4 | ||||
-rw-r--r-- | server/src/game.rs | 1 | ||||
-rw-r--r-- | server/src/interaction.rs | 83 |
11 files changed, 316 insertions, 100 deletions
@@ -281,6 +281,7 @@ name = "bot" version = "0.1.0" dependencies = [ "anyhow", + "env_logger", "hurrycurry-client-lib", "hurrycurry-protocol", "log", 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)) +} diff --git a/server/protocol/src/lib.rs b/server/protocol/src/lib.rs index 8f8e9784..2c165a92 100644 --- a/server/protocol/src/lib.rs +++ b/server/protocol/src/lib.rs @@ -79,6 +79,7 @@ pub struct ClientGamedata { pub tile_interact: Vec<bool>, pub map_names: HashSet<String>, // for compat with game jam version pub maps: HashMap<String, MapMetadata>, + pub recipes: Vec<Recipe>, } #[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode)] @@ -231,6 +232,87 @@ pub struct Score { pub instant_recipes: usize, } +#[derive(Debug, Clone, Serialize, Encode, Decode, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum Recipe { + Passive { + duration: f32, + revert_duration: Option<f32>, + tile: Option<TileIndex>, + input: ItemIndex, + output: Option<ItemIndex>, + warn: bool, + }, + Active { + duration: f32, + tile: Option<TileIndex>, + input: ItemIndex, + outputs: [Option<ItemIndex>; 2], + }, + Instant { + tile: Option<TileIndex>, + inputs: [Option<ItemIndex>; 2], + outputs: [Option<ItemIndex>; 2], + points: i64, + }, +} + +impl Recipe { + pub fn tile(&self) -> Option<TileIndex> { + match self { + Recipe::Passive { tile, .. } => *tile, + Recipe::Active { tile, .. } => *tile, + Recipe::Instant { tile, .. } => *tile, + } + } + pub fn duration(&self) -> Option<f32> { + match self { + Recipe::Passive { duration, .. } => Some(*duration), + Recipe::Active { duration, .. } => Some(*duration), + _ => None, + } + } + pub fn revert_duration(&self) -> Option<f32> { + match self { + Recipe::Passive { + revert_duration, .. + } => *revert_duration, + _ => None, + } + } + pub fn warn(&self) -> bool { + match self { + Recipe::Passive { warn, .. } => *warn, + _ => false, + } + } + pub fn inputs(&self) -> Vec<ItemIndex> { + match self { + Recipe::Passive { input, .. } => vec![*input], + Recipe::Active { input, .. } => vec![*input], + Recipe::Instant { inputs, .. } => inputs.iter().flat_map(|e| e.to_owned()).collect(), + } + } + pub fn outputs(&self) -> Vec<ItemIndex> { + match self { + Recipe::Passive { output, .. } => output.iter().copied().collect(), + Recipe::Active { outputs, .. } => outputs.iter().flat_map(|e| e.to_owned()).collect(), + Recipe::Instant { outputs, .. } => outputs.iter().flat_map(|e| e.to_owned()).collect(), + } + } + pub fn supports_tile(&self, tile: Option<TileIndex>) -> bool { + if let Some(tile_constraint) = self.tile() { + if let Some(tile) = tile { + tile == tile_constraint + } else { + false + } + } else { + true + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize, Encode, Decode, Copy, PartialEq, Eq, Hash)] #[serde(rename_all = "snake_case")] pub enum ItemLocation { diff --git a/server/src/bin/graph.rs b/server/src/bin/graph.rs index 58cc1763..03a59e37 100644 --- a/server/src/bin/graph.rs +++ b/server/src/bin/graph.rs @@ -16,8 +16,8 @@ */ use anyhow::{anyhow, Result}; -use hurrycurry_protocol::{ItemIndex, RecipeIndex}; -use hurrycurry_server::{data::DataIndex, interaction::Recipe}; +use hurrycurry_protocol::{ItemIndex, Recipe, RecipeIndex}; +use hurrycurry_server::data::DataIndex; #[tokio::main] async fn main() -> Result<()> { diff --git a/server/src/data.rs b/server/src/data.rs index 522df916..99cbaf9f 100644 --- a/server/src/data.rs +++ b/server/src/data.rs @@ -16,14 +16,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. */ -use crate::{ - entity::{construct_entity, Entity, EntityDecl}, - interaction::Recipe, -}; +use crate::entity::{construct_entity, Entity, EntityDecl}; use anyhow::{anyhow, bail, Result}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, - ItemIndex, MapMetadata, RecipeIndex, TileIndex, + ItemIndex, MapMetadata, Recipe, RecipeIndex, TileIndex, }; use serde::{Deserialize, Serialize}; use std::{ diff --git a/server/src/entity/customers/demands.rs b/server/src/entity/customers/demands.rs index 33557b50..176ca232 100644 --- a/server/src/entity/customers/demands.rs +++ b/server/src/entity/customers/demands.rs @@ -16,8 +16,7 @@ */ use super::Demand; -use crate::interaction::Recipe; -use hurrycurry_protocol::{ItemIndex, TileIndex}; +use hurrycurry_protocol::{ItemIndex, Recipe, TileIndex}; use std::collections::{HashMap, HashSet}; pub fn generate_demands( diff --git a/server/src/entity/mod.rs b/server/src/entity/mod.rs index 81061bb5..94718dd1 100644 --- a/server/src/entity/mod.rs +++ b/server/src/entity/mod.rs @@ -21,14 +21,14 @@ pub mod environment_effect; pub mod item_portal; pub mod player_portal; -use crate::{data::ItemTileRegistry, game::Game, interaction::Recipe}; +use crate::{data::ItemTileRegistry, game::Game}; use anyhow::{anyhow, Result}; use conveyor::Conveyor; use customers::{demands::generate_demands, Customers}; use environment_effect::{EnvironmentController, EnvironmentEffect, EnvironmentEffectController}; use hurrycurry_protocol::{ glam::{IVec2, Vec2}, - ItemIndex, PacketC, TileIndex, + ItemIndex, PacketC, Recipe, TileIndex, }; use item_portal::ItemPortal; use player_portal::PlayerPortal; diff --git a/server/src/game.rs b/server/src/game.rs index 5af9658e..39cd61dc 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -199,6 +199,7 @@ impl Game { let mut out = Vec::new(); out.push(PacketC::Data { data: ClientGamedata { + recipes: self.data.recipes.clone(), item_names: self.data.item_names.clone(), tile_names: self.data.tile_names.clone(), tile_collide: self.data.tile_collide.clone(), diff --git a/server/src/interaction.rs b/server/src/interaction.rs index 71125ac4..4630b536 100644 --- a/server/src/interaction.rs +++ b/server/src/interaction.rs @@ -19,89 +19,8 @@ use crate::{ data::Gamedata, game::{Involvement, Item}, }; -use hurrycurry_protocol::{ItemIndex, Score, TileIndex}; +use hurrycurry_protocol::{Recipe, Score, TileIndex}; use log::info; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub enum Recipe { - Passive { - duration: f32, - revert_duration: Option<f32>, - tile: Option<TileIndex>, - input: ItemIndex, - output: Option<ItemIndex>, - warn: bool, - }, - Active { - duration: f32, - tile: Option<TileIndex>, - input: ItemIndex, - outputs: [Option<ItemIndex>; 2], - }, - Instant { - tile: Option<TileIndex>, - inputs: [Option<ItemIndex>; 2], - outputs: [Option<ItemIndex>; 2], - points: i64, - }, -} - -impl Recipe { - pub fn tile(&self) -> Option<TileIndex> { - match self { - Recipe::Passive { tile, .. } => *tile, - Recipe::Active { tile, .. } => *tile, - Recipe::Instant { tile, .. } => *tile, - } - } - pub fn duration(&self) -> Option<f32> { - match self { - Recipe::Passive { duration, .. } => Some(*duration), - Recipe::Active { duration, .. } => Some(*duration), - _ => None, - } - } - pub fn revert_duration(&self) -> Option<f32> { - match self { - Recipe::Passive { - revert_duration, .. - } => *revert_duration, - _ => None, - } - } - pub fn warn(&self) -> bool { - match self { - Recipe::Passive { warn, .. } => *warn, - _ => false, - } - } - pub fn inputs(&self) -> Vec<ItemIndex> { - match self { - Recipe::Passive { input, .. } => vec![*input], - Recipe::Active { input, .. } => vec![*input], - Recipe::Instant { inputs, .. } => inputs.iter().flat_map(|e| e.to_owned()).collect(), - } - } - pub fn outputs(&self) -> Vec<ItemIndex> { - match self { - Recipe::Passive { output, .. } => output.iter().copied().collect(), - Recipe::Active { outputs, .. } => outputs.iter().flat_map(|e| e.to_owned()).collect(), - Recipe::Instant { outputs, .. } => outputs.iter().flat_map(|e| e.to_owned()).collect(), - } - } - pub fn supports_tile(&self, tile: Option<TileIndex>) -> bool { - if let Some(tile_constraint) = self.tile() { - if let Some(tile) = tile { - tile == tile_constraint - } else { - false - } - } else { - true - } - } -} pub enum InteractEffect { Put, |