/* 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 . */ use crate::{ pathfinding::{find_path_to_neighbour, Path}, BotAlgo, BotInput, }; use hurrycurry_client_lib::Game; use hurrycurry_protocol::{ glam::IVec2, ItemIndex, Message, PlayerID, Recipe, RecipeIndex, TileIndex, }; use log::{debug, warn}; #[derive(Default)] pub struct Simple { path: Option<(Path, IVec2, f32)>, cooldown: f32, } 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 = Result; 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(); } 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, 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 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 Context<'_, S> { pub fn is_hand_item(&self, item: ItemIndex) -> bool { self.game .players .get(&self.me) .is_some_and(|p| p.items[0].as_ref().is_some_and(|i| i.kind == item)) } pub fn is_hand_occupied(&self) -> bool { self.game .players .get(&self.me) .map(|p| p.items[0].is_some()) .unwrap_or(false) } /// Returns (requested_item, table_pos, remaining_time) pub fn find_demands(&self) -> Vec<(ItemIndex, IVec2, f32)> { self.game .players .iter() .filter_map(|(_, pl)| match &pl.communicate_persist { Some((Message::Item(item), timeout)) => { let pos = pl.movement.position.as_ivec2(); [IVec2::X, IVec2::Y, -IVec2::X, -IVec2::Y] .into_iter() .find(|off| { self.game.tiles.get(&(pos + *off)).is_some_and(|t| { self.game .data .tile_placeable_items .get(&t.kind) .is_none_or(|placable| placable.contains(item)) }) }) .map(|off| pos + off) .map(|pos| (*item, pos, timeout.remaining)) } _ => None, }) .collect() } pub fn find_recipe_with_output(&self, item: ItemIndex) -> Option { self.game .data .recipes .iter() .enumerate() .find(|(_, r)| r.outputs().contains(&item)) .map(|(i, _)| RecipeIndex(i)) } pub fn find_item_on_map(&self, item: ItemIndex) -> Option { self.game .tiles .iter() .find(|(_, t)| t.item.as_ref().is_some_and(|t| t.kind == item)) .map(|(p, _)| *p) } pub fn find_tile(&self, tile: TileIndex) -> Option { self.game .tiles .iter() .find(|(_, t)| t.kind == tile) .map(|(p, _)| *p) } pub fn find_occupied_table_or_floor(&self) -> Option { self.game .tiles .iter() .find(|(_, t)| { t.item.is_some() && matches!( self.game.data.tile_names[t.kind.0].as_str(), "table" | "floor" ) }) .map(|(p, _)| *p) } pub fn find_empty_interactable_tile_by_name(&self, name: &str) -> Option { self.game .tiles .iter() .find(|(_, t)| { !self.game.data.tile_placeable_items.contains_key(&t.kind) && t.item.is_none() && self.game.data.tile_names[t.kind.0] == name }) .map(|(p, _)| *p) } pub fn is_tile_occupied(&self, pos: IVec2) -> bool { self.game .tiles .get(&pos) .map(|t| t.item.is_some()) .unwrap_or(true) } } impl Context<'_, S> { pub fn find_empty_interactable_tile(&self) -> Option { 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_placeable_items.contains_key(&t.kind)) // TODO filter by placable item .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.) } pub fn assert_tile_is_clear(&mut self, pos: IVec2) -> LogicRes { if self.is_tile_occupied(pos) { self.clear_tile(pos)?; } Ok(()) } 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() { self.interact_with(pos, 0.)?; warn!("no path to empty space "); Err(()) } else { warn!("no empty space left"); 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 { debug!("aquire placed item {:?}", self.game.data.item_names[item.0]); if let Some(pos) = self.find_item_on_map(item) { return Ok(pos); } self.aquire_item(item)?; self.dispose_hand()?; Err(()) } 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(()); } if let Some(pos) = self.find_item_on_map(item) { self.assert_hand_is_clear()?; self.interact_with(pos, 0.)?; return Ok(()); } if let Some(recipe) = self.find_recipe_with_output(item) { let r = &self.game.data.recipes[recipe.0]; match r { Recipe::Instant { tile: Some(tile), inputs: [None, None], .. } => { if let Some(pos) = self.find_tile(*tile) { self.assert_tile_is_clear(pos)?; self.assert_hand_is_clear()?; self.interact_with(pos, 0.)?; } } Recipe::Instant { tile: None, inputs: [Some(a), Some(b)], .. } => { let apos = self.aquire_placed_item(*a)?; self.aquire_item(*b)?; self.interact_with(apos, 0.)?; } Recipe::Instant { tile: None, inputs: [Some(input), None], .. } => { self.aquire_item(*input)?; if let Some(pos) = self.find_empty_interactable_tile() { self.interact_with(pos, 0.)?; } else { warn!("no empty space left") } } Recipe::Active { tile: Some(tile), input, speed, .. } => { if let Some(pos) = self.find_tile(*tile) { self.assert_tile_is_clear(pos)?; self.aquire_item(*input)?; self.interact_with(pos, 1. / speed + 0.2)?; } } Recipe::Passive { tile: Some(tile), input, .. } => { if let Some(pos) = self.find_tile(*tile) { 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); return Err(()); // waiting for it to finish } else { self.assert_tile_is_clear(pos)?; } } self.aquire_item(*input)?; self.interact_with(pos, 0.)?; } } Recipe::Passive { tile: None, input, .. } => { self.aquire_placed_item(*input)?; debug!("waiting for passive to finish"); self.state.cooldown(0.5); return Err(()); } _ => warn!("recipe too hard {r:?}"), } } warn!( "stuck at making item {:?}", self.game.data.item_names[item.0] ); Err(()) } pub fn update(&mut self) -> LogicRes { if let Some((item, table, _)) = self.find_demands().pop() { if self.game.data.item_name(item) == "unknown-order" { self.interact_with(table, 0.)?; } else { self.assert_tile_is_clear(table)?; self.aquire_item(item)?; self.interact_with(table, 0.)?; } } Ok(()) } }