othello/src/elo.rs

297 lines
9.2 KiB
Rust

use crate::{
agent::Agent,
complexagent::ComplexAgent,
game_inner::GameInner,
logic::{ChildrenEvalMethod, FutureMoveConfig},
repr::{Board, Piece, Winner},
};
use indicatif::{ParallelProgressIterator, ProgressBar, ProgressDrawTarget, ProgressStyle};
use rand::seq::SliceRandom;
use rayon::iter::{IntoParallelIterator, ParallelIterator};
use skillratings::{
elo::{elo, EloConfig, EloRating},
Outcomes, Rating,
};
use std::num::NonZero;
type AgentMaker = Box<dyn Fn(Piece) -> Box<dyn Agent>>;
#[allow(dead_code)]
pub fn run() {
const FMV_BASE: FutureMoveConfig = FutureMoveConfig {
max_depth: 20,
min_arena_depth: 14,
top_k_children: 2,
up_to_minus: 10,
max_arena_size: usize::MAX,
do_prune: false,
print: false,
children_eval_method: ChildrenEvalMethod::AverageDivDepth,
};
let configs = [6]
.into_iter()
.map(move |d| FutureMoveConfig {
max_depth: d,
..FMV_BASE
})
.flat_map(move |prev_c| {
// create children which enable, and disable pruning
[true, 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
[1, 2, 3]
.map(move |top_k_children| FutureMoveConfig {
top_k_children,
..prev_c
})
.to_vec()
})
.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
[1, 2, 3]
.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
[prev_c.max_depth, 1, 2, 3]
.into_iter()
.filter(|&x| x <= prev_c.max_depth)
.map(move |up_to_minus| FutureMoveConfig {
up_to_minus,
..prev_c
})
.collect()
});
let 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();
let mut arena = PlayerArena::new(vec);
arena.prop_arena(100);
println!("{}", arena);
}
pub struct PlayerArena {
/// Name, Creator Function, Elo
players: Vec<(String, AgentMaker, EloRating)>,
}
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([EloRating::new()].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::create_agents(&self.players[i].1, &self.players[j].1),
)
})
.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();
let term = term.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()
.progress_with({
let a = ProgressBar::new(num as u64).with_style(
ProgressStyle::with_template(
"[{elapsed_precise}] {pos:>7}/{len:7} ETA: {eta}",
)
.expect("invalid ProgressStyle"),
);
a.set_draw_target(ProgressDrawTarget::term(term, 5));
a
})
.progress_with_style(
ProgressStyle::with_template(
"[{elapsed_precise}] {pos:>7}/{len:7} ETA: {eta}",
)
.expect("invalid ProgressStyle"),
)
.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;
while let Ok((i, j, o)) = receiver.recv() {
self.process_outcome(i, j, &o);
received_num += 1;
term.clear_last_lines(self.players.len())
.expect("unable to clear prev lines");
term.write_str(format!("{}", self).as_str())
.expect("unable to write leaderboard");
// 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) {
self.play(
&(0..self.players.len())
.flat_map(|i| {
(0..self.players.len())
.map(move |j| (i, j))
.filter(|(i, j)| i != j)
.collect::<Vec<_>>()
})
.collect::<Vec<_>>()
.repeat(n),
);
}
fn process_outcome(&mut self, player1: usize, player2: usize, outcome: &Outcomes) {
let (np1, np2) = elo(
&self.players[player1].2,
&self.players[player2].2,
outcome,
&EloConfig::new(),
);
self.players[player1].2 = np1;
self.players[player2].2 = np2;
}
fn create_agents(
player_1_fn: &AgentMaker,
player_2_fn: &AgentMaker,
) -> (Box<dyn Agent>, Box<dyn Agent>) {
(player_1_fn(Piece::Black), player_2_fn(Piece::White))
}
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(3..=7)),
)
.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"),
}
}
}