aboutsummaryrefslogtreecommitdiff
path: root/server
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
parent63d5a3ff37d1e3972d34ccc9e1d26c3b4bc05efb (diff)
downloadhurrycurry-52d99b16534631e293a23ddbc18c4ea70b71392f.tar
hurrycurry-52d99b16534631e293a23ddbc18c4ea70b71392f.tar.bz2
hurrycurry-52d99b16534631e293a23ddbc18c4ea70b71392f.tar.zst
add recipes back to protocol
Diffstat (limited to 'server')
-rw-r--r--server/bot/Cargo.toml1
-rw-r--r--server/bot/src/main.rs134
-rw-r--r--server/bot/src/pathfinding.rs96
-rw-r--r--server/protocol/src/lib.rs82
-rw-r--r--server/src/bin/graph.rs4
-rw-r--r--server/src/data.rs7
-rw-r--r--server/src/entity/customers/demands.rs3
-rw-r--r--server/src/entity/mod.rs4
-rw-r--r--server/src/game.rs1
-rw-r--r--server/src/interaction.rs83
10 files changed, 315 insertions, 100 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))
+}
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,