Skip to content

Commit

Permalink
Add round-robin tournament feature
Browse files Browse the repository at this point in the history
  • Loading branch information
heinzelotto committed Aug 30, 2024
1 parent 3710be4 commit 676eb4c
Show file tree
Hide file tree
Showing 4 changed files with 140 additions and 42 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion judge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ 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"
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
tracing-subscriber = "0.3.18"
172 changes: 132 additions & 40 deletions judge/src/main.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand All @@ -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<PathBuf>,

/// How many games to play
#[arg(short, long, default_value_t = 100)]
Expand All @@ -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<Recorder>,
) -> anyhow::Result<MatchScore> {
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!(
Expand All @@ -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<MatchScore>>,
) {
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::<Result<Vec<PlayerConfig>, anyhow::Error>>()?;

let matchups: Vec<(usize, usize)> = (0..player_configs.len()).tuple_combinations().collect();

let mut match_results: HashMap<(usize, usize), Option<MatchScore>> = 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(())
}

Expand Down
6 changes: 5 additions & 1 deletion judge/src/player.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,10 @@ pub struct PlayerWithGameState<'a> {
impl Player {
pub fn new(path: &Path) -> anyhow::Result<Self> {
let config = PlayerConfig::load(path)?;
Self::from_config(&config)
}

pub fn from_config(config: &PlayerConfig) -> anyhow::Result<Self> {
let child_proc = Command::new(&config.cmd[0])
.args(&config.cmd[1..])
.stdin(Stdio::piped())
Expand All @@ -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(),
Expand Down

0 comments on commit 676eb4c

Please sign in to comment.