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 Box>; #[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 = (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::>(); // 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::>() .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, player_2: Box) -> 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"), } } }