Compare commits

...

2 Commits

Author SHA1 Message Date
46f4e00e50 start over. Move randomly in a valid direction 2024-09-30 19:34:09 +02:00
d1a128e9df add documentation 2024-09-30 19:32:17 +02:00
2 changed files with 66 additions and 98 deletions

View File

@ -14,99 +14,71 @@ use log::info;
use rand::seq::SliceRandom; use rand::seq::SliceRandom;
use serde_json::{json, Value}; use serde_json::{json, Value};
use crate::{Battlesnake, Board, Coord, Direction, Game, Move}; use crate::{Battlesnake, Board, Direction, Game, Move};
const MAX_HEALTH: i32 = 100;
impl Battlesnake { impl Battlesnake {
/// Check if moving the snake in `direction` is safe. fn possible_moves_without_heads<'a>(
#[must_use] &'a self,
pub fn is_direction_safe(&self, direction: Direction, game: &Game, board: &Board) -> bool { game: &'a Game,
if self.is_direction_death(direction, game, board) { board: &'a Board,
return false; ) -> impl Iterator<Item = Direction> + 'a {
} enum_iterator::all::<Direction>()
.filter(|direction| {
let target = self.head.move_to(direction); // filter out directions that would go outside the field
let target = self.head.move_to(*direction);
// check if a bigger snake could move to this square (0..board.width).contains(&target.x) && (0..board.height).contains(&target.y)
if board })
.snakes .filter(|direction| {
.iter() let target = self.head.move_to(*direction);
.filter(|snake| snake.id != self.id && self.length <= snake.length) // don't collide with any snake
.flat_map(|snake| enum_iterator::all().map(|dir| snake.head.move_to(dir))) !board
.any(|coord| target == coord)
{
return false;
}
// check for hazards
if board.hazards.iter().any(|&coord| target == coord) {
return false;
}
true
}
#[must_use]
pub fn is_direction_death(&self, direction: Direction, game: &Game, board: &Board) -> bool {
let target = self.head.move_to(direction);
// check if target is out of bounds
if !((0..board.width).contains(&target.x) && (0..board.height).contains(&target.y)) {
return true;
}
// check if target is inside a snake
if board
.snakes .snakes
.iter() .iter()
.filter(|snake| { .filter(|snake| {
!(game.ruleset.settings.squad.allow_body_collisions && self.squad == snake.squad) // filter out snakes that are in our squad if body collisions are allowed
!(game.ruleset.settings.squad.allow_body_collisions
&& self.squad == snake.squad
&& self.id != snake.id)
})
.flat_map(|snake| {
// get all coordinates of the snake body. The tail of the body can be ignored
// if the snake hasn't just eaten, as it will move out of the way
let has_eaten = snake.health == MAX_HEALTH;
snake.body[..snake.body.len() - usize::from(!has_eaten)].iter()
})
.any(|&coord| coord == target)
}) })
.flat_map(|snake| snake.body.iter())
.any(|&coord| target == coord)
{
return true;
}
false
} }
#[must_use] #[must_use]
pub fn health_cost(&self, tile: Coord, game: &Game, board: &Board) -> i32 { pub fn possible_moves(&self, game: &Game, board: &Board) -> Vec<Direction> {
// we can't leave the board self.possible_moves_without_heads(game, board)
if !((0..board.width).contains(&tile.x) && (0..board.height).contains(&tile.y)) { .filter(|direction| {
return self.health; // don't go into spots where a bigger snake must go
} let target = self.head.move_to(*direction);
!board
// we can't move into other snakes
if board
.snakes .snakes
.iter() .iter()
.filter(|snake| { .filter(|snake| {
!(game.ruleset.settings.squad.allow_body_collisions && self.squad == snake.squad) // The other snake must be bigger than we are
snake.length > self.length
}) })
.flat_map(|snake| snake.body.iter()) .filter_map(|snake| {
.any(|&coord| tile == coord) // get all snakes movement options
{ let moves = snake
return self.health; .possible_moves_without_heads(game, board)
.collect::<Vec<_>>();
// only snakes that have a single option
match moves[..] {
[direction] => Some(snake.head.move_to(direction)),
_ => None,
} }
})
// every step costs us one health point .any(|coord| coord == target)
let mut cost = 1; })
.collect()
// hazards increase the health loss
if board.hazards.iter().any(|&hazard| tile == hazard) {
cost += game.ruleset.settings.hazard_damage_per_turn;
}
// check for bigger snakes that can move into this square
for snake in board
.snakes
.iter()
.filter(|snake| snake.id != self.id && self.length <= snake.length)
{
let moves = enum_iterator::all().map(|dir| snake.head.move_to(dir));
}
cost
} }
} }
@ -139,19 +111,13 @@ pub fn end(_game: &Game, _turn: i32, _board: &Board, _you: &Battlesnake) {
// Valid moves are "up", "down", "left", or "right" // Valid moves are "up", "down", "left", or "right"
// See https://docs.battlesnake.com/api/example-move for available data // See https://docs.battlesnake.com/api/example-move for available data
pub fn get_move(game: &Game, turn: i32, board: &Board, you: &Battlesnake) -> Option<Move> { pub fn get_move(game: &Game, turn: i32, board: &Board, you: &Battlesnake) -> Option<Move> {
let moves = enum_iterator::all() let moves = you.possible_moves(game, board);
.filter(|&direction| !you.is_direction_death(direction, game, board)) if moves.is_empty() {
.collect::<Vec<_>>(); return None;
let safe_moves = moves }
.iter()
.copied()
.filter(|&direction| you.is_direction_safe(direction, game, board))
.collect::<Vec<_>>();
// Choose a random move from the safe ones // Choose a random move from the safe ones
let chosen = safe_moves let chosen = moves.choose(&mut rand::thread_rng())?;
.choose(&mut rand::thread_rng())
.or_else(|| moves.choose(&mut rand::thread_rng()))?;
info!("MOVE {}: {:?}", turn, chosen); info!("MOVE {}: {:?}", turn, chosen);
Some(Move { Some(Move {

View File

@ -30,7 +30,9 @@ pub enum Direction {
#[derive(Debug, Deserialize, Serialize)] #[derive(Debug, Deserialize, Serialize)]
pub struct Move { pub struct Move {
/// In which direction the snake should move
r#move: Direction, r#move: Direction,
/// Say something to the other snakes
#[serde(default, skip_serializing_if = "is_default")] #[serde(default, skip_serializing_if = "is_default")]
shout: Option<String>, shout: Option<String>,
} }