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 Box>; #[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 = (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::>(); // 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::>() }) .collect::>() .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, Box) { (player_1_fn(Piece::Black), player_2_fn(Piece::White)) } fn play_two_inner(player_1: Box, player_2: Box) -> 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"), } } }