solo mode (not verry good)

This commit is contained in:
Max Känner 2024-10-03 04:20:52 +02:00
parent 23be6f1a18
commit 11ba9b7c94
2 changed files with 99 additions and 2 deletions

View File

@ -186,7 +186,11 @@ pub fn get_move(game: &Game, turn: i32, board: &Board, you: &Battlesnake) -> Opt
while start.elapsed() < game_info.calculation_time.get() { while start.elapsed() < game_info.calculation_time.get() {
let mut board = board.clone(); let mut board = board.clone();
tree.monte_carlo_step(&mut board); if game.ruleset.name == "solo" {
tree.monte_carlo_step_solo(&mut board);
} else {
tree.monte_carlo_step(&mut board);
}
} }
let actions = tree.child_statistics.entry(game_info.my_token).or_default(); let actions = tree.child_statistics.entry(game_info.my_token).or_default();
@ -299,4 +303,88 @@ impl Node {
} }
winner winner
} }
/// Performs one monte carlo simulation step for a solo game
///
/// Returns the lengths before death
fn monte_carlo_step_solo(
&mut self,
board: &mut simulation::Board,
) -> BTreeMap<SnakeToken, usize> {
let lengths = if self.statistic.played == 0 {
// We didn't simulate a game for this node yet. Do that
let mut lengths: BTreeMap<_, _> = board
.snakes()
.filter_map(|snake| Some((snake, board.snake_length(snake)?)))
.collect();
board.simulate_until(|board| {
for snake in board.snakes() {
if let Some(length) = board.snake_length(snake) {
lengths.insert(snake, length);
}
}
board.alive_snakes() == 0
});
lengths
} else {
// select a node to simulate
let possible_actions = board.possible_actions();
let actions = possible_actions
.iter()
.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
/ board.spaces() 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();
board.simulate_actions(&actions);
let lengths = self
.childs
.entry(actions.clone())
.or_default()
.monte_carlo_step_solo(board);
// update child statistics
for (token, action) in &actions {
let entry = self
.child_statistics
.entry(*token)
.or_default()
.entry(*action)
.or_default();
entry.played += 1;
if let Some(length) = lengths.get(&token) {
entry.won += length;
}
}
lengths
};
self.statistic.played += 1;
for (token, length) in &lengths {
self.statistic
.won
.entry(*token)
.and_modify(|won| *won += length)
.or_insert(*length);
}
lengths
}
} }

View File

@ -81,6 +81,11 @@ impl Board {
} }
} }
#[allow(clippy::cast_sign_loss)]
pub const fn spaces(&self) -> usize {
self.height as usize * self.width as usize
}
pub fn alive_snakes(&self) -> usize { pub fn alive_snakes(&self) -> usize {
self.snakes.len() self.snakes.len()
} }
@ -89,6 +94,10 @@ impl Board {
self.snakes.keys().copied() self.snakes.keys().copied()
} }
pub fn snake_length(&self, token: SnakeToken) -> Option<usize> {
self.snakes.get(&token).map(|snake| snake.body.len())
}
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 {
@ -158,7 +167,7 @@ impl Board {
self.turn += 1; self.turn += 1;
} }
pub fn simulate_until(&mut self, exit: impl Fn(&Self) -> bool) { pub fn simulate_until(&mut self, mut exit: impl FnMut(&Self) -> bool) {
while !exit(self) { while !exit(self) {
let actions = self let actions = self
.possible_actions() .possible_actions()