Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

1 change: 1 addition & 0 deletions judge/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
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