Compare commits
No commits in common. "30c20b3f5416aa4981ffb0f4c46e496197dc5e14" and "7eb6eff65abdb3dd15fc342b4768837af35aa678" have entirely different histories.
30c20b3f54
...
7eb6eff65a
@ -10,12 +10,11 @@
|
|||||||
// To get you started we've included code to prevent your Battlesnake from moving backwards.
|
// To get you started we've included code to prevent your Battlesnake from moving backwards.
|
||||||
// For more info see docs.battlesnake.com
|
// For more info see docs.battlesnake.com
|
||||||
|
|
||||||
use std::{cmp::Ordering, time::Instant};
|
|
||||||
|
|
||||||
use log::info;
|
use log::info;
|
||||||
|
use rand::seq::SliceRandom;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
|
||||||
use crate::{simulation, Action, Battlesnake, Board, Direction, Game, MAX_HEALTH};
|
use crate::{Action, Battlesnake, Board, Direction, Game, MAX_HEALTH};
|
||||||
|
|
||||||
impl Battlesnake {
|
impl Battlesnake {
|
||||||
fn possible_actions_without_heads<'a>(
|
fn possible_actions_without_heads<'a>(
|
||||||
@ -110,72 +109,17 @@ 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<Action> {
|
pub fn get_move(game: &Game, turn: i32, board: &Board, you: &Battlesnake) -> Option<Action> {
|
||||||
let id_map = board
|
let actions = you.possible_actions(game, board);
|
||||||
.snakes
|
if actions.is_empty() {
|
||||||
.iter()
|
return None;
|
||||||
.enumerate()
|
}
|
||||||
.map(|(i, snake)| (snake.id.clone(), u8::try_from(i).unwrap()))
|
|
||||||
.collect();
|
|
||||||
let board = simulation::Board::from_game_board(board, &id_map, turn);
|
|
||||||
|
|
||||||
let my_id = id_map[&you.id];
|
// Choose a random move from the safe ones
|
||||||
let my_index = board.snake_index(my_id)?;
|
let chosen = actions.choose(&mut rand::thread_rng())?;
|
||||||
|
|
||||||
let possible_actions = board.possible_actions();
|
|
||||||
|
|
||||||
let my_actions = &possible_actions[my_index];
|
|
||||||
|
|
||||||
let actions = my_actions
|
|
||||||
.iter()
|
|
||||||
.map(|direction| {
|
|
||||||
let mut actions = vec![None; possible_actions.len()];
|
|
||||||
actions[my_index] = Some(*direction);
|
|
||||||
let mut wins = 0;
|
|
||||||
let mut total_turns = 0;
|
|
||||||
let start = Instant::now();
|
|
||||||
for _ in 0..100 {
|
|
||||||
let mut board = board.clone();
|
|
||||||
board.simulate_with_initial_until(&actions[..], |board| {
|
|
||||||
!board.is_alive(my_id)
|
|
||||||
|| (game.ruleset.name != "solo" && board.alive_snakes() <= 1)
|
|
||||||
});
|
|
||||||
if board.is_alive(my_id) {
|
|
||||||
// we survived
|
|
||||||
wins += 2;
|
|
||||||
} else if board.alive_snakes() == 0 {
|
|
||||||
// no snake is alive. This is a draw
|
|
||||||
wins += 1;
|
|
||||||
} else {
|
|
||||||
// we lost
|
|
||||||
wins += 0;
|
|
||||||
}
|
|
||||||
total_turns += board.turn();
|
|
||||||
}
|
|
||||||
let end = Instant::now();
|
|
||||||
info!(
|
|
||||||
"Simulation for {direction:?} took {}s",
|
|
||||||
(end - start).as_secs_f32()
|
|
||||||
);
|
|
||||||
|
|
||||||
(direction, wins, total_turns)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
info!("actions: {actions:?}");
|
|
||||||
|
|
||||||
let (&chosen, _, _) =
|
|
||||||
actions
|
|
||||||
.into_iter()
|
|
||||||
.max_by(
|
|
||||||
|(_, score1, turns1), (_, score2, turns2)| match score1.cmp(score2) {
|
|
||||||
Ordering::Equal => turns1.cmp(turns2),
|
|
||||||
order => order,
|
|
||||||
},
|
|
||||||
)?;
|
|
||||||
|
|
||||||
info!("DIRECTION {}: {:?}", turn, chosen);
|
info!("DIRECTION {}: {:?}", turn, chosen);
|
||||||
Some(Action {
|
Some(Action {
|
||||||
r#move: chosen,
|
r#move: *chosen,
|
||||||
shout: None,
|
shout: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -11,7 +11,6 @@ use serde_json::Value;
|
|||||||
use std::env;
|
use std::env;
|
||||||
|
|
||||||
mod logic;
|
mod logic;
|
||||||
mod simulation;
|
|
||||||
|
|
||||||
const MAX_HEALTH: i32 = 100;
|
const MAX_HEALTH: i32 = 100;
|
||||||
|
|
||||||
@ -120,7 +119,7 @@ pub struct RulesetSquad {
|
|||||||
shared_length: bool,
|
shared_length: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
pub struct Board {
|
pub struct Board {
|
||||||
/// The number of rows in the y-axis of the game board.
|
/// The number of rows in the y-axis of the game board.
|
||||||
height: i32,
|
height: i32,
|
||||||
@ -135,7 +134,7 @@ pub struct Board {
|
|||||||
hazards: Vec<Coord>,
|
hazards: Vec<Coord>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
#[derive(Deserialize, Serialize, Debug)]
|
||||||
pub struct Battlesnake {
|
pub struct Battlesnake {
|
||||||
/// Unique identifier for this Battlesnake in the context of the current Game
|
/// Unique identifier for this Battlesnake in the context of the current Game
|
||||||
id: String,
|
id: String,
|
||||||
|
@ -1,257 +0,0 @@
|
|||||||
use std::collections::{BTreeSet, HashMap, VecDeque};
|
|
||||||
|
|
||||||
use rand::seq::SliceRandom;
|
|
||||||
|
|
||||||
use crate::{Coord, Direction};
|
|
||||||
|
|
||||||
const MAX_HEALTH: u8 = crate::MAX_HEALTH as u8;
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct Board {
|
|
||||||
turn: i32,
|
|
||||||
/// Height of the board
|
|
||||||
height: i32,
|
|
||||||
/// Width of the board
|
|
||||||
width: i32,
|
|
||||||
/// Food on the board
|
|
||||||
food: BTreeSet<Coord>,
|
|
||||||
/// Alive snakes
|
|
||||||
snakes: Vec<Battlesnake>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Board {
|
|
||||||
pub fn from_game_board(board: &crate::Board, id_map: &HashMap<String, u8>, turn: i32) -> Self {
|
|
||||||
let width = board.width;
|
|
||||||
debug_assert!(width > 0);
|
|
||||||
let height = board.height;
|
|
||||||
debug_assert!(height > 0);
|
|
||||||
let food = board.food.iter().copied().collect();
|
|
||||||
let snakes = board
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.map(|snake| {
|
|
||||||
let id = id_map[&snake.id];
|
|
||||||
Battlesnake::from_game_snake(snake, id)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Self {
|
|
||||||
turn,
|
|
||||||
height,
|
|
||||||
width,
|
|
||||||
food,
|
|
||||||
snakes,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub const fn turn(&self) -> i32 {
|
|
||||||
self.turn
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn snake_index(&self, id: u8) -> Option<usize> {
|
|
||||||
self.snakes
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.find(|(_, snake)| snake.id == id)
|
|
||||||
.map(|(i, _)| i)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn is_alive(&self, id: u8) -> bool {
|
|
||||||
self.snakes.iter().any(|snake| snake.id == id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn alive_snakes(&self) -> usize {
|
|
||||||
self.snakes.len()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_actions(&mut self, actions: &[Direction]) {
|
|
||||||
debug_assert_eq!(self.snakes.len(), actions.len());
|
|
||||||
|
|
||||||
// move snakes
|
|
||||||
for (snake, direction) in self.snakes.iter_mut().zip(actions.iter()) {
|
|
||||||
snake.perform_action(*direction);
|
|
||||||
}
|
|
||||||
|
|
||||||
// feed snakes
|
|
||||||
for snake in &mut self.snakes {
|
|
||||||
let head = snake.head();
|
|
||||||
if self.food.remove(head) {
|
|
||||||
snake.health = MAX_HEALTH;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// kill snakes
|
|
||||||
let alive_ids = self
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.filter(|snake| {
|
|
||||||
// snake must have enough health
|
|
||||||
snake.health != 0
|
|
||||||
})
|
|
||||||
.map(|snake| (snake.id, snake.body.len(), *snake.head()))
|
|
||||||
.filter(|(_, _, head)| {
|
|
||||||
// head in bounds
|
|
||||||
(0..self.width).contains(&head.x) && (0..self.height).contains(&head.y)
|
|
||||||
})
|
|
||||||
.filter(|(_, _, head)| {
|
|
||||||
// body collision
|
|
||||||
!self
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.flat_map(|snake2| snake2.body.iter().skip(1))
|
|
||||||
.any(|body| body == head)
|
|
||||||
})
|
|
||||||
.filter(|(id, len, head)| {
|
|
||||||
// head to head collision
|
|
||||||
!self
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.filter(|snake2| snake2.id != *id && snake2.body.len() >= *len)
|
|
||||||
.any(|snake2| snake2.head() == head)
|
|
||||||
})
|
|
||||||
.map(|(id, _, _)| id)
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
self.snakes.retain(|snake| alive_ids.contains(&snake.id));
|
|
||||||
|
|
||||||
self.turn += 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn simulate_with_initial_until(
|
|
||||||
&mut self,
|
|
||||||
actions: &[Option<Direction>],
|
|
||||||
exit: impl Fn(&Self) -> bool,
|
|
||||||
) {
|
|
||||||
debug_assert_eq!(actions.len(), self.snakes.len());
|
|
||||||
let possible_actions = self.possible_actions();
|
|
||||||
let actions = actions
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, direction)| {
|
|
||||||
direction.unwrap_or_else(|| {
|
|
||||||
possible_actions[i]
|
|
||||||
.choose(&mut rand::thread_rng())
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(Direction::Up)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
self.simulate_actions(&actions);
|
|
||||||
while !exit(self) {
|
|
||||||
let actions = self
|
|
||||||
.possible_actions()
|
|
||||||
.iter()
|
|
||||||
.map(|actions| {
|
|
||||||
actions
|
|
||||||
.choose(&mut rand::thread_rng())
|
|
||||||
.copied()
|
|
||||||
.unwrap_or(Direction::Up)
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
self.simulate_actions(&actions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn possible_actions(&self) -> Vec<Vec<Direction>> {
|
|
||||||
let possible_actions = self
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.map(|snake| {
|
|
||||||
enum_iterator::all::<Direction>()
|
|
||||||
.map(|direction| (direction, snake.head().move_to(direction)))
|
|
||||||
.filter(|(_, target)| {
|
|
||||||
// don't move out of bounds
|
|
||||||
(0..self.width).contains(&target.x) && (0..self.height).contains(&target.y)
|
|
||||||
})
|
|
||||||
.filter(|(_, target)| {
|
|
||||||
// don't collide with other snakes
|
|
||||||
!self
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.flat_map(|snake| {
|
|
||||||
let has_eaten = snake.health == MAX_HEALTH;
|
|
||||||
snake
|
|
||||||
.body
|
|
||||||
.iter()
|
|
||||||
.take(snake.body.len() - usize::from(!has_eaten))
|
|
||||||
})
|
|
||||||
.any(|coord| coord == target)
|
|
||||||
})
|
|
||||||
.map(|(direction, _)| direction)
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
|
|
||||||
// don't move into bigger snakes heads with only one movement option
|
|
||||||
possible_actions
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, actions)| {
|
|
||||||
let snake = &self.snakes[i];
|
|
||||||
let length = snake.body.len();
|
|
||||||
let head = snake.head();
|
|
||||||
actions
|
|
||||||
.iter()
|
|
||||||
.copied()
|
|
||||||
.filter(|direction| {
|
|
||||||
let target = head.move_to(*direction);
|
|
||||||
!self
|
|
||||||
.snakes
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter(|(_, snake)| {
|
|
||||||
// only snakes that are longer
|
|
||||||
snake.body.len() > length
|
|
||||||
})
|
|
||||||
.filter_map(|(i, snake)| match &possible_actions[i][..] {
|
|
||||||
// only snakes that have a single action option
|
|
||||||
[direction] => Some(snake.head().move_to(*direction)),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
.any(|coord| coord == target)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct Battlesnake {
|
|
||||||
/// Id of the snake. Unique inside a game
|
|
||||||
id: u8,
|
|
||||||
/// health points
|
|
||||||
health: u8,
|
|
||||||
/// Body of the snake. The head is the first element in the queue
|
|
||||||
body: VecDeque<Coord>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Battlesnake {
|
|
||||||
pub fn from_game_snake(snake: &crate::Battlesnake, id: u8) -> Self {
|
|
||||||
let body: VecDeque<_> = snake.body.iter().copied().collect();
|
|
||||||
debug_assert_eq!(body.len(), usize::try_from(snake.length).unwrap());
|
|
||||||
debug_assert!(snake.health <= crate::MAX_HEALTH);
|
|
||||||
let health = u8::try_from(snake.health).expect("max health is 100");
|
|
||||||
Self { id, health, body }
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn perform_action(&mut self, direction: Direction) {
|
|
||||||
debug_assert!(!self.body.is_empty());
|
|
||||||
// move the head along
|
|
||||||
self.body.push_front(self.head().move_to(direction));
|
|
||||||
|
|
||||||
// move tail
|
|
||||||
if self.health != MAX_HEALTH {
|
|
||||||
// only move the tail if we didn't eat
|
|
||||||
self.body.pop_back();
|
|
||||||
}
|
|
||||||
|
|
||||||
// decrease helth
|
|
||||||
self.health = self.health.saturating_sub(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn head(&self) -> &Coord {
|
|
||||||
debug_assert!(!self.body.is_empty());
|
|
||||||
self.body.front().expect("not empty")
|
|
||||||
}
|
|
||||||
}
|
|
@ -120,7 +120,6 @@ fn vs_production() -> Result<(), DynError> {
|
|||||||
"https://snake.mkaenner.de",
|
"https://snake.mkaenner.de",
|
||||||
"-g",
|
"-g",
|
||||||
"duel",
|
"duel",
|
||||||
"--browser",
|
|
||||||
])
|
])
|
||||||
.output();
|
.output();
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user