add simulation to get the best move
This commit is contained in:
		@@ -10,11 +10,12 @@
 | 
			
		||||
// To get you started we've included code to prevent your Battlesnake from moving backwards.
 | 
			
		||||
// For more info see docs.battlesnake.com
 | 
			
		||||
 | 
			
		||||
use std::{cmp::Ordering, time::Instant};
 | 
			
		||||
 | 
			
		||||
use log::info;
 | 
			
		||||
use rand::seq::SliceRandom;
 | 
			
		||||
use serde_json::{json, Value};
 | 
			
		||||
 | 
			
		||||
use crate::{Action, Battlesnake, Board, Direction, Game, MAX_HEALTH};
 | 
			
		||||
use crate::{simulation, Action, Battlesnake, Board, Direction, Game, MAX_HEALTH};
 | 
			
		||||
 | 
			
		||||
impl Battlesnake {
 | 
			
		||||
    fn possible_actions_without_heads<'a>(
 | 
			
		||||
@@ -109,17 +110,72 @@ pub fn end(_game: &Game, _turn: i32, _board: &Board, _you: &Battlesnake) {
 | 
			
		||||
// Valid moves are "up", "down", "left", or "right"
 | 
			
		||||
// 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> {
 | 
			
		||||
    let actions = you.possible_actions(game, board);
 | 
			
		||||
    if actions.is_empty() {
 | 
			
		||||
        return None;
 | 
			
		||||
    }
 | 
			
		||||
    let id_map = board
 | 
			
		||||
        .snakes
 | 
			
		||||
        .iter()
 | 
			
		||||
        .enumerate()
 | 
			
		||||
        .map(|(i, snake)| (snake.id.clone(), u8::try_from(i).unwrap()))
 | 
			
		||||
        .collect();
 | 
			
		||||
    let board = simulation::Board::from_game_board(board, &id_map, turn);
 | 
			
		||||
 | 
			
		||||
    // Choose a random move from the safe ones
 | 
			
		||||
    let chosen = actions.choose(&mut rand::thread_rng())?;
 | 
			
		||||
    let my_id = id_map[&you.id];
 | 
			
		||||
    let my_index = board.snake_index(my_id)?;
 | 
			
		||||
 | 
			
		||||
    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);
 | 
			
		||||
    Some(Action {
 | 
			
		||||
        r#move: *chosen,
 | 
			
		||||
        r#move: chosen,
 | 
			
		||||
        shout: None,
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ use serde_json::Value;
 | 
			
		||||
use std::env;
 | 
			
		||||
 | 
			
		||||
mod logic;
 | 
			
		||||
mod simulation;
 | 
			
		||||
 | 
			
		||||
const MAX_HEALTH: i32 = 100;
 | 
			
		||||
 | 
			
		||||
@@ -119,7 +120,7 @@ pub struct RulesetSquad {
 | 
			
		||||
    shared_length: bool,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Serialize, Debug)]
 | 
			
		||||
#[derive(Deserialize, Serialize, Debug, Clone)]
 | 
			
		||||
pub struct Board {
 | 
			
		||||
    /// The number of rows in the y-axis of the game board.
 | 
			
		||||
    height: i32,
 | 
			
		||||
@@ -134,7 +135,7 @@ pub struct Board {
 | 
			
		||||
    hazards: Vec<Coord>,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[derive(Deserialize, Serialize, Debug)]
 | 
			
		||||
#[derive(Deserialize, Serialize, Debug, Clone)]
 | 
			
		||||
pub struct Battlesnake {
 | 
			
		||||
    /// Unique identifier for this Battlesnake in the context of the current Game
 | 
			
		||||
    id: String,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										257
									
								
								battlesnake/src/simulation.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										257
									
								
								battlesnake/src/simulation.rs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,257 @@
 | 
			
		||||
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")
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
		Reference in New Issue
	
	Block a user