diff --git a/Cargo.lock b/Cargo.lock index d2ec154..46dd1dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -327,6 +327,7 @@ dependencies = [ "anyhow", "clap", "gomori", + "itertools", "rand", "serde", "serde_json", diff --git a/judge/Cargo.toml b/judge/Cargo.toml index a3ddc39..b8696a2 100644 --- a/judge/Cargo.toml +++ b/judge/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" anyhow = "1.0.86" clap = { version = "4.5.13", features = ["derive"] } gomori = { path = "../gomori" } +itertools = "0.13.0" rand = "0.8.5" serde = "1.0.203" serde_json = "1.0.118" diff --git a/judge/src/main.rs b/judge/src/main.rs index f988f8a..1676611 100644 --- a/judge/src/main.rs +++ b/judge/src/main.rs @@ -1,7 +1,9 @@ +use std::collections::HashMap; use std::path::PathBuf; use clap::Parser; -use judge::{play_game, GameResult, Player, Recorder}; +use itertools::Itertools; +use judge::{play_game, GameResult, Player, PlayerConfig, Recorder}; use rand::rngs::StdRng; use rand::SeedableRng; use tracing::{debug, info}; @@ -11,11 +13,9 @@ use tracing_subscriber::util::SubscriberInitExt; #[derive(Parser)] struct Args { - /// Path to the config JSON file for player 1 - player_1_config: PathBuf, - - /// Path to the config JSON file for player 2 - player_2_config: PathBuf, + /// Path to the config JSON files of players + #[clap(num_args(2..), value_delimiter = ' ')] + player_configs: Vec, /// How many games to play #[arg(short, long, default_value_t = 100)] @@ -38,40 +38,33 @@ struct Args { log_level: LevelFilter, } -fn main() -> anyhow::Result<()> { - let args = Args::parse(); - - initialize_logging(args.log_level); - - let mut player_1 = Player::new(&args.player_1_config)?; - let mut player_2 = Player::new(&args.player_2_config)?; +#[derive(Default)] +struct MatchScore { + wins: [usize; 2], + illegal_moves: [usize; 2], + ties: usize, +} +fn play_matchup( + player_1: &mut Player, + player_2: &mut Player, + num_games: usize, + rng: &mut StdRng, + stop_on_illegal_move: bool, + recorder: &mut Option, +) -> anyhow::Result { let player_names = [player_1.name.clone(), player_2.name.clone()]; + let mut match_score = MatchScore::default(); - let mut wins = [0, 0]; - let mut illegal_moves = [0, 0]; - let mut ties = 0; - - let mut recorder = if let Some(dir_path) = args.record_games_to_directory { - Some(Recorder::new(dir_path)?) - } else { - None - }; - - // Get a random seed - let seed = args.seed.unwrap_or_else(rand::random); - info!(seed); - let mut rng = StdRng::seed_from_u64(seed); - - for game_idx in 0..args.num_games { - match play_game(&mut rng, &mut player_1, &mut player_2, &mut recorder)? { + for game_idx in 0..num_games { + match play_game(rng, player_1, player_2, recorder)? { GameResult::WonByPlayer { player_idx } => { debug!(winner = player_names[player_idx], game_idx); - wins[player_idx] += 1; + match_score.wins[player_idx] += 1; } GameResult::Tie => { debug!(game_idx, "Tie"); - ties += 1; + match_score.ties += 1; } GameResult::IllegalMoveByPlayer { player_idx, err } => { info!( @@ -84,30 +77,129 @@ fn main() -> anyhow::Result<()> { err_dyn = src_err; } info!("{}", err_dyn); - if args.stop_on_illegal_move { + if stop_on_illegal_move { break; } else { - wins[1 - player_idx] += 1; - illegal_moves[player_idx] += 1; + match_score.wins[1 - player_idx] += 1; + match_score.illegal_moves[player_idx] += 1; } } } } - let paren_1 = if illegal_moves[1] > 0 { - format!(" ({} through illegal moves by player 2)", illegal_moves[1]) + let paren_1 = if match_score.illegal_moves[1] > 0 { + format!( + " ({} through illegal moves by player 2)", + match_score.illegal_moves[1] + ) } else { String::new() }; - let paren_2 = if illegal_moves[0] > 0 { - format!(" ({} through illegal moves by player 1)", illegal_moves[0]) + let paren_2 = if match_score.illegal_moves[0] > 0 { + format!( + " ({} through illegal moves by player 1)", + match_score.illegal_moves[0] + ) } else { String::new() }; eprintln!( "End result:\n- {} wins by {}{}\n- {} wins by {}{}\n- {} ties", - wins[0], &player_1.name, paren_1, wins[1], player_2.name, paren_2, ties + match_score.wins[0], + &player_1.name, + paren_1, + match_score.wins[1], + player_2.name, + paren_2, + match_score.ties ); + + Ok(match_score) +} + +// prints an upper triangular matrix of the results of the tournament +fn print_tournament_results( + player_configs: &[PlayerConfig], + match_results: &HashMap<(usize, usize), Option>, +) { + println!("\nTournament results (p1 win %, p2 win %, tie %):\n"); + print!(" {:19} |", "p1 ↓ p2 →"); + for j in (0..player_configs.len()).rev() { + print!(" {:19} |", player_configs[j].nick); + } + println!(); + for i in 0..player_configs.len() { + for _ in 0..player_configs.len() - i + 1 { + print!("---------------------|"); + } + println!(); + print!(" {:19} |", player_configs[i].nick); + for j in (0..player_configs.len()).rev() { + if i >= j { + print!(" "); + } else if let Some(Some(score)) = match_results.get(&(i, j)) { + let num_games = score.wins[0] + score.wins[1] + score.ties; + let win_1_percentage = score.wins[0] as f32 / num_games as f32 * 100.0; + let win_2_percentage = score.wins[1] as f32 / num_games as f32 * 100.0; + let tie_percentage = score.ties as f32 / num_games as f32 * 100.0; + print!( + "{:5.1}% {:5.1}% {:5.1}% |", + win_1_percentage, win_2_percentage, tie_percentage + ); + } else { + print!(" {:19} |", "N/A"); + } + } + println!(); + } + println!("---------------------|"); +} + +fn main() -> anyhow::Result<()> { + let args = Args::parse(); + + initialize_logging(args.log_level); + + // Get a random seed + let seed = args.seed.unwrap_or_else(rand::random); + info!(seed); + let mut rng = StdRng::seed_from_u64(seed); + + let mut recorder = if let Some(dir_path) = args.record_games_to_directory { + Some(Recorder::new(dir_path)?) + } else { + None + }; + + let player_configs = args + .player_configs + .iter() + .map(|path| PlayerConfig::load(path)) + .collect::, anyhow::Error>>()?; + + let matchups: Vec<(usize, usize)> = (0..player_configs.len()).tuple_combinations().collect(); + + let mut match_results: HashMap<(usize, usize), Option> = HashMap::new(); + for (i1, i2) in matchups { + let mut player_1 = Player::from_config(&player_configs[i1])?; + let mut player_2 = Player::from_config(&player_configs[i2])?; + + let match_score = play_matchup( + &mut player_1, + &mut player_2, + args.num_games, + &mut rng, + args.stop_on_illegal_move, + &mut recorder, + )?; + + match_results.insert((i1, i2), Some(match_score)); + } + + if player_configs.len() > 2 { + print_tournament_results(&player_configs, &match_results); + } + Ok(()) } diff --git a/judge/src/player.rs b/judge/src/player.rs index d6a22dd..abcd457 100644 --- a/judge/src/player.rs +++ b/judge/src/player.rs @@ -49,6 +49,10 @@ pub struct PlayerWithGameState<'a> { impl Player { pub fn new(path: &Path) -> anyhow::Result { let config = PlayerConfig::load(path)?; + Self::from_config(&config) + } + + pub fn from_config(config: &PlayerConfig) -> anyhow::Result { let child_proc = Command::new(&config.cmd[0]) .args(&config.cmd[1..]) .stdin(Stdio::piped()) @@ -58,7 +62,7 @@ impl Player { info!(cmd = ?config.cmd, "Spawned child process"); Ok(Self { - name: config.nick, + name: config.nick.clone(), stdin: child_proc.stdin.expect("Could not access stdin"), stdout: BufReader::new(child_proc.stdout.expect("Could not access stdout")), buf: String::new(),