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
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 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
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");
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
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");
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
// 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 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(
board,
&token_map,
&game_info.token_mapping,
turn,
game.ruleset.settings.food_spawn_chance,
game.ruleset.settings.minimum_food,
);
let my_token = token_map[&you.id];
let mut tree = Node::default();
for _ in 0..300 {
while start.elapsed() < game_info.calculation_time.get() {
let mut board = board.clone();
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)]
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))
.map(|(direction, _)| *direction)?;
info!("DIRECTION {}: {:?}", turn, chosen);
info!(
"DIRECTION {turn}: {chosen:?} after {}ms ({})",
start.elapsed().as_millis(),
you.name,
);
Some(Action {
r#move: chosen,
shout: None,
@ -186,28 +246,23 @@ impl Node {
let actions = possible_actions
.iter()
.map(|(token, actions)| {
.filter_map(|(token, actions)| {
let statistics = self.child_statistics.entry(*token).or_default();
let selected = actions
.iter()
.copied()
.max_by_key(|direction| {
let statistics = statistics.entry(*direction).or_default();
if statistics.played == 0 {
return OrderedFloat(f64::INFINITY);
}
#[allow(clippy::cast_precision_loss)]
let exploitation = statistics.won as f64 / statistics.played as f64;
#[allow(clippy::cast_precision_loss)]
let exploration = f64::consts::SQRT_2
* f64::sqrt(
f64::ln(self.statistic.played as f64)
/ statistics.played as f64,
);
OrderedFloat(exploitation + exploration)
})
.unwrap_or_default();
(*token, selected)
let selected = actions.iter().copied().max_by_key(|direction| {
let statistics = statistics.entry(*direction).or_default();
if statistics.played == 0 {
return OrderedFloat(f64::INFINITY);
}
#[allow(clippy::cast_precision_loss)]
let exploitation = statistics.won as f64 / statistics.played as f64;
#[allow(clippy::cast_precision_loss)]
let exploration = f64::consts::SQRT_2
* f64::sqrt(
f64::ln(self.statistic.played as f64) / statistics.played as f64,
);
OrderedFloat(exploitation + exploration)
})?;
Some((*token, selected))
})
.collect();

View File

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

View File

@ -92,7 +92,7 @@ impl Board {
pub fn simulate_actions(&mut self, actions: &BTreeMap<SnakeToken, Direction>) {
// move 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
@ -170,7 +170,7 @@ impl Board {
.iter()
.choose(&mut rand::thread_rng())
.copied()
.unwrap_or_default(),
.unwrap_or(Direction::Up),
)
})
.collect();