use std::{ cmp::Ordering, env, net::TcpStream, path::{Path, PathBuf}, process::{Child, Command, Stdio}, sync::{Mutex, atomic::AtomicUsize}, thread::sleep, time::Duration, }; use rayon::iter::{IntoParallelIterator, ParallelIterator}; use rich_progress_bar::{Colors, RichProgressBar}; type DynError = Box; fn main() { if let Err(e) = try_main() { eprintln!("{e}"); std::process::exit(-1) } } fn try_main() -> Result<(), DynError> { let task = env::args().nth(1); match task.as_deref() { Some("local") => local()?, Some("local2") => local2()?, Some("local4") => local4()?, Some("constrictor4") => constrictor4()?, Some("vs_production" | "vs_prod") => vs_production()?, Some("regression") => regression()?, Some("docker") => docker()?, _ => print_help(), } Ok(()) } fn print_help() { eprintln!( "Tasks: local runs the snake on a local game local2 runs the snake twice on a local game local4 runs the snake on a local standard game constrictor4 runs the snake on a local constrictor game vs_production | vs_prod runs the snake against the active server regression runs the snake against the active server multiple times docker package docker image and upload to docker.mkaenner.de/snake " ) } fn local() -> Result<(), DynError> { let mut snake = run_snake(8000, None)?; let game = Command::new("./battlesnake-cli") .current_dir(project_root()) .args([ "play", "-W", "11", "-H", "11", "--name", "local test", "--url", "http://localhost:8000", "-g", "solo", "--browser", ]) .status(); game.and(snake.kill())?; Ok(()) } fn local2() -> Result<(), DynError> { let mut snake = run_snake(8000, None)?; let game = Command::new("./battlesnake-cli") .current_dir(project_root()) .args([ "play", "-W", "11", "-H", "11", "--name", "local test1", "--url", "http://localhost:8000", "--name", "local test2", "--url", "http://localhost:8000", "-g", "duel", "--browser", ]) .status(); game.and(snake.kill())?; Ok(()) } fn local4() -> Result<(), DynError> { let mut snake = run_snake(8000, None)?; let game = Command::new("./battlesnake-cli") .current_dir(project_root()) .args([ "play", "-W", "11", "-H", "11", "--name", "local test1", "--url", "http://localhost:8000", "--name", "local test2", "--url", "http://localhost:8000", "--name", "local test3", "--url", "http://localhost:8000", "--name", "local test4", "--url", "http://localhost:8000", "-g", "standard", "--browser", ]) .status(); game.and(snake.kill())?; Ok(()) } fn constrictor4() -> Result<(), DynError> { let mut snake = run_snake(8000, None)?; let game = Command::new("./battlesnake-cli") .current_dir(project_root()) .args([ "play", "-W", "11", "-H", "11", "--name", "local test1", "--url", "http://localhost:8000", "--name", "local test2", "--url", "http://localhost:8000", "--name", "local test3", "--url", "http://localhost:8000", "--name", "local test4", "--url", "http://localhost:8000", "-g", "constrictor", "--browser", ]) .status(); game.and(snake.kill())?; Ok(()) } fn vs_production() -> Result<(), DynError> { let mut snake = run_snake(8000, None)?; let game = Command::new("./battlesnake-cli") .current_dir(project_root()) .args([ "play", "-W", "11", "-H", "11", "--name", "local", "--url", "http://localhost:8000", "--name", "production", "--url", "https://snake.mkaenner.de", "-g", "duel", "--browser", ]) .output(); let game = snake.kill().and(game)?; if !game.status.success() { eprintln!("game output: {}", String::from_utf8(game.stderr)?); eprintln!("game status: {}", game.status); Err("failed to play the game".into()) } else { const WIN_PHRASE: &[u8] = b"local was the winner.\n"; let won = &game.stderr[(game.stderr.len() - WIN_PHRASE.len())..game.stderr.len()] == WIN_PHRASE; if won { println!("The local snake has won"); } else { println!("The production snake has won"); } Ok(()) } } fn regression() -> Result<(), DynError> { let mut snake = run_local_docker(53511)?; let mut prod = match run_production(64576) { Ok(prod) => prod, err => { snake.kill()?; err?; unreachable!() } }; sleep(Duration::from_secs(1)); let res = try_regression(); snake.kill().and(prod.kill())?; let (won, draw, loose) = res?; let games = won + draw + loose; println!( "\nThe local snake has won {won}/{games} games ({}%)", (won + games / 200) * 100 / games ); println!( "The local snake has drawn {draw}/{games} games ({}%)", (draw + games / 200) * 100 / games ); println!( "The local snake has lost {loose}/{games} games ({}%)", (loose + games / 200) * 100 / games ); match won.cmp(&loose) { Ordering::Less => println!("The local nake is worse than the production"), Ordering::Equal => println!("The local snake is equivalent to the production"), Ordering::Greater => println!("The local snake is better than the production"), } Ok(()) } fn try_regression() -> Result<(usize, usize, usize), DynError> { const GAMES: usize = 100; // limit the parallelism rayon::ThreadPoolBuilder::new() .num_threads(std::thread::available_parallelism()?.get() / 4) .build_global() .unwrap(); let mut progress = RichProgressBar::new(); progress .set_progress_character('=') .set_color(Colors::BrightCyan) .set_total(GAMES as u64); let progress = Mutex::new(&mut progress); let stats = (0..GAMES) .into_par_iter() .flat_map(|_| { let game = Command::new("./battlesnake-cli") .current_dir(project_root()) .args([ "play", "--width", "11", "--height", "11", "--timeout", "100", "--name", "local", "--url", "http://localhost:53511", "--name", "production", "--url", "http://localhost:64576", "-g", "duel", ]) .output() .ok()?; let res = if !game.status.success() { eprintln!("game output: {}", String::from_utf8(game.stderr).ok()?); eprintln!("game status: {}", game.status); None } else { const WIN_PHRASE: &[u8] = b"local was the winner.\n"; const LOOSE_PHRASE: &[u8] = b"production was the winner.\n"; if &game.stderr[(game.stderr.len() - WIN_PHRASE.len())..game.stderr.len()] == WIN_PHRASE { Some((1, 0, 0)) } else if &game.stderr[(game.stderr.len() - LOOSE_PHRASE.len())..game.stderr.len()] == LOOSE_PHRASE { Some((0, 0, 1)) } else { Some((0, 1, 0)) } }; if let Ok(mut progress) = progress.lock() { let _ = progress.inc(); } res }) .reduce( || (0, 0, 0), |(sum_win, sum_draw, sum_loose), (win, draw, loose)| { (sum_win + win, sum_draw + draw, sum_loose + loose) }, ); Ok(stats) } fn run_snake(port: u16, log: Option<&str>) -> Result { let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); let mut snake = Command::new(cargo); snake .current_dir(project_root()) .env("PORT", port.to_string()); if let Some(log) = log { snake.env("RUST_LOG", log); } let mut snake = snake .args( ["run", "--bin", "battlesnake"] .map(str::to_string) .into_iter() .chain(env::args().skip(2)), ) .spawn()?; loop { if let Some(status) = snake.try_wait()? { Err(format!("{status}"))?; } if TcpStream::connect(("127.0.0.1", port)).is_ok() { break Ok(snake); } sleep(Duration::from_secs(1)); } } fn run_local_docker(port: u16) -> Result { if !Command::new("docker") .current_dir(project_root()) .args(["build", ".", "-t", "localhost/local_snake:latest"]) .status()? .success() { Err("Build of docker image failed")?; } let mut snake = Command::new("docker") .args([ "run", "--name", "battlesnake-regression-local", "--replace", "--env", "RUST_LOG=error", "--env", format!("PORT={}", port).as_str(), "--network=host", "--rm", "local_snake", ]) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .stdin(Stdio::inherit()) .spawn()?; loop { if let Some(status) = snake.try_wait()? { Err(format!("{status}"))?; } if TcpStream::connect(("127.0.0.1", port)).is_ok() { break Ok(snake); } sleep(Duration::from_secs(1)); } } fn run_production(port: u16) -> Result { let mut snake = Command::new("docker") .args([ "run", "--name", "battlesnake-regression-production", "--replace", "--env", "RUST_LOG=error", "--env", format!("PORT={}", port).as_str(), "--network=host", "--rm", "docker.mkaenner.de/snake:latest", ]) .stdout(Stdio::inherit()) .stderr(Stdio::inherit()) .stdin(Stdio::inherit()) .spawn()?; loop { if let Some(status) = snake.try_wait()? { Err(format!("{status}"))?; } if TcpStream::connect(("127.0.0.1", port)).is_ok() { break Ok(snake); } sleep(Duration::from_secs(1)); } } fn docker() -> Result<(), DynError> { if !Command::new("docker") .current_dir(project_root()) .args(["build", ".", "-t", "docker.mkaenner.de/snake"]) .status()? .success() { Err("Build of docker image failed")?; } Command::new("docker") .args(["push", "docker.mkaenner.de/snake"]) .status()?; Ok(()) } fn project_root() -> PathBuf { Path::new(&env!("CARGO_MANIFEST_DIR")) .ancestors() .nth(1) .unwrap() .to_path_buf() }