variable simulation time

This commit is contained in:
Max Känner 2024-10-03 03:30:16 +02:00
parent c2d242453e
commit 23be6f1a18
3 changed files with 91 additions and 48 deletions

View File

@ -11,9 +11,14 @@
// For more info see docs.battlesnake.com // For more info see docs.battlesnake.com
use core::f64; use core::f64;
use std::collections::BTreeMap; use std::{
cell::Cell,
collections::{BTreeMap, HashMap},
sync::{Arc, LazyLock, Mutex},
time::{Duration, Instant},
};
use log::info; use log::{error, info};
use ordered_float::OrderedFloat; use ordered_float::OrderedFloat;
use serde_json::{json, Value}; use serde_json::{json, Value};
@ -101,41 +106,92 @@ pub fn info() -> Value {
}) })
} }
#[derive(Debug, PartialEq, Eq, Clone)]
struct GameInfo {
calculation_time: Cell<Duration>,
token_mapping: Arc<BTreeMap<String, SnakeToken>>,
my_token: SnakeToken,
}
static GAME_INFOS: LazyLock<Mutex<HashMap<(String, String), GameInfo>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
// start is called when your Battlesnake begins a game // start is called when your Battlesnake begins a game
pub fn start(_game: &Game, _turn: i32, _board: &Board, _you: &Battlesnake) { pub fn start(game: &Game, _turn: i32, board: &Board, you: &Battlesnake) {
info!("GAME START"); info!("GAME START");
let token_mapping = Arc::new(SnakeToken::from_board(board));
let my_token = token_mapping[&you.id];
let Ok(mut game_infos) = GAME_INFOS.lock() else {
error!("unable to lock game infos");
return;
};
game_infos.insert(
(game.id.clone(), you.id.clone()),
GameInfo {
calculation_time: Cell::new(Duration::from_millis(50)),
token_mapping,
my_token,
},
);
} }
// end is called when your Battlesnake finishes a game // end is called when your Battlesnake finishes a game
pub fn end(_game: &Game, _turn: i32, _board: &Board, _you: &Battlesnake) { pub fn end(game: &Game, _turn: i32, _board: &Board, you: &Battlesnake) {
info!("GAME OVER"); info!("GAME OVER");
let Ok(mut game_infos) = GAME_INFOS.lock() else {
error!("unable to lock game infos");
return;
};
game_infos.remove(&(game.id.clone(), you.id.clone()));
} }
// move is called on every turn and returns your next move // move is called on every turn and returns your next move
// 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 token_map = SnakeToken::from_board(board); let start = Instant::now();
let game_info = GAME_INFOS
.lock()
.ok()
.and_then(|guard| guard.get(&(game.id.clone(), you.id.clone())).cloned())
.unwrap_or_else(|| {
let token_mapping = Arc::new(SnakeToken::from_board(board));
let my_token = token_mapping[&you.id];
GameInfo {
calculation_time: Cell::new(Duration::from_millis(50)),
token_mapping,
my_token,
}
});
// do some latency compensation
game_info.calculation_time.set(
game_info.calculation_time.get()
+ Duration::from_millis(
u64::from(
game.timeout * 3 / 4 - you.latency.parse().unwrap_or(game.timeout * 3 / 4),
) / 3,
),
);
let board = simulation::Board::from_game_board( let board = simulation::Board::from_game_board(
board, board,
&token_map, &game_info.token_mapping,
turn, turn,
game.ruleset.settings.food_spawn_chance, game.ruleset.settings.food_spawn_chance,
game.ruleset.settings.minimum_food, game.ruleset.settings.minimum_food,
); );
let my_token = token_map[&you.id];
let mut tree = Node::default(); let mut tree = Node::default();
for _ in 0..300 { while start.elapsed() < game_info.calculation_time.get() {
let mut board = board.clone(); let mut board = board.clone();
tree.monte_carlo_step(&mut board); tree.monte_carlo_step(&mut board);
} }
let actions = tree.child_statistics.entry(my_token).or_default(); let actions = tree.child_statistics.entry(game_info.my_token).or_default();
info!("actions: {actions:?}"); info!("actions {}: {actions:?}", you.name);
#[allow(clippy::cast_precision_loss)] #[allow(clippy::cast_precision_loss)]
let chosen = actions let chosen = actions
@ -143,7 +199,11 @@ pub fn get_move(game: &Game, turn: i32, board: &Board, you: &Battlesnake) -> Opt
.max_by_key(|(_, stat)| OrderedFloat(stat.won as f64 / stat.played as f64)) .max_by_key(|(_, stat)| OrderedFloat(stat.won as f64 / stat.played as f64))
.map(|(direction, _)| *direction)?; .map(|(direction, _)| *direction)?;
info!("DIRECTION {}: {:?}", turn, chosen); info!(
"DIRECTION {turn}: {chosen:?} after {}ms ({})",
start.elapsed().as_millis(),
you.name,
);
Some(Action { Some(Action {
r#move: chosen, r#move: chosen,
shout: None, shout: None,
@ -186,28 +246,23 @@ impl Node {
let actions = possible_actions let actions = possible_actions
.iter() .iter()
.map(|(token, actions)| { .filter_map(|(token, actions)| {
let statistics = self.child_statistics.entry(*token).or_default(); let statistics = self.child_statistics.entry(*token).or_default();
let selected = actions let selected = actions.iter().copied().max_by_key(|direction| {
.iter() let statistics = statistics.entry(*direction).or_default();
.copied() if statistics.played == 0 {
.max_by_key(|direction| { return OrderedFloat(f64::INFINITY);
let statistics = statistics.entry(*direction).or_default(); }
if statistics.played == 0 { #[allow(clippy::cast_precision_loss)]
return OrderedFloat(f64::INFINITY); let exploitation = statistics.won as f64 / statistics.played as f64;
} #[allow(clippy::cast_precision_loss)]
#[allow(clippy::cast_precision_loss)] let exploration = f64::consts::SQRT_2
let exploitation = statistics.won as f64 / statistics.played as f64; * f64::sqrt(
#[allow(clippy::cast_precision_loss)] f64::ln(self.statistic.played as f64) / statistics.played as f64,
let exploration = f64::consts::SQRT_2 );
* f64::sqrt( OrderedFloat(exploitation + exploration)
f64::ln(self.statistic.played as f64) })?;
/ statistics.played as f64, Some((*token, selected))
);
OrderedFloat(exploitation + exploration)
})
.unwrap_or_default();
(*token, selected)
}) })
.collect(); .collect();

View File

@ -19,23 +19,11 @@ const MAX_HEALTH: i32 = 100;
// See https://docs.battlesnake.com/api // See https://docs.battlesnake.com/api
#[derive( #[derive(
Debug, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Deserialize, Serialize, Sequence,
PartialEq,
Eq,
PartialOrd,
Ord,
Hash,
Clone,
Copy,
Deserialize,
Serialize,
Sequence,
Default,
)] )]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum Direction { pub enum Direction {
/// Move left (-x) /// Move left (-x)
#[default]
Left, Left,
/// Move up (+y) /// Move up (+y)
Up, Up,

View File

@ -92,7 +92,7 @@ impl Board {
pub fn simulate_actions(&mut self, actions: &BTreeMap<SnakeToken, Direction>) { pub fn simulate_actions(&mut self, actions: &BTreeMap<SnakeToken, Direction>) {
// move snakes // move snakes
for (token, snake) in &mut self.snakes { for (token, snake) in &mut self.snakes {
snake.perform_action(actions.get(token).copied().unwrap_or_default()); snake.perform_action(actions.get(token).copied().unwrap_or(Direction::Up));
} }
// feed snakes // feed snakes
@ -170,7 +170,7 @@ impl Board {
.iter() .iter()
.choose(&mut rand::thread_rng()) .choose(&mut rand::thread_rng())
.copied() .copied()
.unwrap_or_default(), .unwrap_or(Direction::Up),
) )
}) })
.collect(); .collect();