othello/src/elo.rs

299 lines
9.1 KiB
Rust

use crate::{
agent::{Agent, RandomAgent},
complexagent::ComplexAgent,
game_inner::GameInner,
logic::{ChildrenEvalMethod, FutureMoveConfig, FutureMoves},
repr::{Board, Piece, Winner},
};
use indicatif::{ProgressBar, ProgressStyle};
use rand::seq::SliceRandom;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use skillratings::{
glicko2::{glicko2, Glicko2Rating},
Outcomes, Rating,
};
use std::num::NonZero;
type AgentMaker = Box<dyn Fn(Piece) -> Box<dyn Agent>>;
#[allow(dead_code)]
pub fn run() {
let total_memory = 30_000_000_000; // 30 GB
let num_threads = std::thread::available_parallelism()
.map(NonZero::get)
.expect("unable to get number of threads");
let mem_per_thread = total_memory / num_threads;
let fmv_base = FutureMoveConfig {
max_arena_size: mem_per_thread / FutureMoves::ARENA_ENTRY_SIZE,
print: false,
..Default::default()
};
let configs = [2, 4, 6]
.into_iter()
.map(move |d| FutureMoveConfig {
max_depth: d,
..fmv_base
})
.flat_map(move |prev_c| {
// create children which enable, and disable pruning
[false].map(move |do_prune| FutureMoveConfig { do_prune, ..prev_c })
})
// .filter(move |move_c| {
// if move_c.do_prune {
// move_c.max_depth >= 8
// } else {
// move_c.max_depth < 8
// }
// })
// .flat_map(move |prev_c| {
// [
// ChildrenEvalMethod::Average,
// ChildrenEvalMethod::AverageDivDepth,
// ]
// .map(move |children_strat| FutureMoveConfig {
// children_eval_method: children_strat,
// ..prev_c
// })
// });
.flat_map(move |prev_c| {
if !prev_c.do_prune {
// do not bother making configs when pruning is disabled
// as top_k_children does nothing when pruning is skipped
return vec![prev_c];
}
// different values of top_k_children
[2].map(move |top_k_children| FutureMoveConfig {
top_k_children,
..prev_c
})
.to_vec()
})
.flat_map(move |prev_c| {
[ChildrenEvalMethod::MinMax, ChildrenEvalMethod::MinMaxProb].map(move |method| {
FutureMoveConfig {
children_eval_method: method,
..prev_c
}
})
})
.flat_map(move |prev_c| {
if !prev_c.do_prune {
// do not bother making configs when pruning is disabled
return vec![prev_c];
}
// different values to be subtracted from max_depth
// to become min_arena_depth
[2].into_iter()
.filter(|&x| x <= prev_c.max_depth)
.map(move |ad_offset| FutureMoveConfig {
min_arena_depth: prev_c.max_depth - ad_offset,
..prev_c
})
.collect()
})
.flat_map(move |prev_c| {
if !prev_c.do_prune {
// do not bother making configs when pruning is disabled
return vec![prev_c];
}
// different values of up_to_minus
[3].into_iter()
.filter(|&x| x <= prev_c.max_depth)
.map(move |up_to_minus| FutureMoveConfig {
up_to_minus,
..prev_c
})
.collect()
});
let mut vec: Vec<(String, AgentMaker)> = configs
.into_iter()
.map(move |config| -> (String, AgentMaker) {
(
format!("{}", config),
Box::new(move |piece| Box::new(ComplexAgent::new(piece, config))),
)
})
.collect();
if true {
vec.push((
"RandomAgent".to_string(),
Box::new(move |piece| Box::new(RandomAgent::new(piece))),
));
}
let mut arena = PlayerArena::new(vec);
arena.prop_arena(500);
println!("{}", arena);
}
pub struct PlayerArena {
/// Name, Creator Function, Elo
players: Vec<(String, AgentMaker, Glicko2Rating)>,
}
impl std::fmt::Display for PlayerArena {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut players_i: Vec<usize> = (0..self.players.len()).collect();
players_i.sort_by_key(|&i| -(self.players[i].2.rating() * 100.0) as i64);
for i in players_i {
writeln!(
f,
"({:.2}): {}",
self.players[i].2.rating(),
self.players[i].0
)?;
}
Ok(())
}
}
impl PlayerArena {
pub fn new(players: Vec<(String, AgentMaker)>) -> Self {
Self {
players: players
.into_iter()
.zip([Default::default()].into_iter().cycle())
// flatten tuple
.map(|((a, b), c)| (a, b, c))
.collect(),
}
}
fn play(&mut self, pairs: &[(usize, usize)]) {
let mut created_pairs = pairs
.iter()
.map(|&(i, j)| {
(
(i, j),
(
(self.players[i].1)(Piece::Black),
(self.players[j].1)(Piece::White),
),
)
})
.collect::<Vec<_>>();
// shuffle for consistency
created_pairs.shuffle(&mut rand::rng());
let num = created_pairs.len();
let (sender, receiver) = crossbeam_channel::unbounded();
let term = console::Term::stdout();
// Spawn parallel processing in a dedicated thread
let processing_thread = {
let sender = sender.clone();
std::thread::spawn(move || {
rayon::ThreadPoolBuilder::new()
.num_threads(
std::thread::available_parallelism()
.map(NonZero::get)
.expect("unable to get number of threads"),
)
.build_global()
.unwrap();
created_pairs
.into_par_iter()
.map(|((i, j), (p1, p2))| (i, j, Self::play_two_inner(p1, p2)))
.for_each(|(i, j, o)| {
sender.send((i, j, o)).expect("Failed to send result");
});
})
};
// Immediately drop our copy of the sender so the channel closes properly
drop(sender);
// Process results on main thread as they arrive
let mut received_num = 0;
let p = ProgressBar::new(num as u64).with_style(
ProgressStyle::with_template("[{elapsed_precise}] {pos:>7}/{len:7} ETA: {eta}")
.expect("invalid ProgressStyle"),
);
while let Ok((i, j, o)) = receiver.recv() {
self.process_outcome(i, j, &o);
if received_num > 0 {
term.clear_last_lines(self.players.len() + 1)
.expect("unable to clear prev lines");
}
term.write_str(format!("{}", self).as_str())
.expect("unable to write leaderboard");
received_num += 1;
p.inc(1);
println!();
// break if all pairs were recieved
if received_num == num {
break;
}
}
// Ensure parallel thread completes
processing_thread
.join()
.expect("Processing thread panicked");
}
fn prop_arena(&mut self, n: usize) {
let mut games = (0..self.players.len())
.flat_map(|i| {
(0..self.players.len())
.map(move |j| (i, j))
.filter(|(i, j)| i != j)
})
.collect::<Vec<_>>()
.repeat(n);
games.shuffle(&mut rand::rng());
self.play(&games);
}
fn process_outcome(&mut self, player1: usize, player2: usize, outcome: &Outcomes) {
let (np1, np2) = glicko2(
&self.players[player1].2,
&self.players[player2].2,
outcome,
&Default::default(),
);
self.players[player1].2 = np1;
self.players[player2].2 = np2;
}
fn play_two_inner(player_1: Box<dyn Agent>, player_2: Box<dyn Agent>) -> Outcomes {
let result = GameInner::new(
player_1,
player_2,
false,
// Board::random(rand::random_range(4..=15)),
Board::STARTING_POSITION,
)
.expect("unable to create game")
.loop_until_result();
match result {
Winner::Player(piece) => match piece {
Piece::Black => Outcomes::WIN,
Piece::White => Outcomes::LOSS,
},
Winner::Tie => Outcomes::DRAW,
Winner::None => panic!("somehow met None"),
}
}
}