diff --git a/planetwars-cli/Cargo.toml b/planetwars-cli/Cargo.toml deleted file mode 100644 index 972a02b..0000000 --- a/planetwars-cli/Cargo.toml +++ /dev/null @@ -1,25 +0,0 @@ -[package] -name = "planetwars-cli" -version = "0.1.0" -edition = "2021" - -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html -[[bin]] -name = "pwcli" - -[dependencies] -futures-core = "0.3" -futures = "0.3" -tokio = { version = "1", features = ["full"] } -rand = "0.6" -serde = { version = "1.0", features = ["derive"] } -serde_json = "1.0" -toml = "0.5" -clap = { version = "3.0.0-rc.8", features = ["derive"] } -chrono = { version = "0.4", features = ["serde"] } -shlex = "1.1" -planetwars-matchrunner = { path = "../planetwars-matchrunner" } - -rust-embed = "6.3.0" -axum = { version = "0.4", features = ["ws"] } -mime_guess = "2" \ No newline at end of file diff --git a/planetwars-cli/README.md b/planetwars-cli/README.md deleted file mode 100644 index dbf8d26..0000000 --- a/planetwars-cli/README.md +++ /dev/null @@ -1,57 +0,0 @@ -# planetwars-cli -ATTENTION: this package is currently out-of-date. - -Note: this project is under active development. All file and configuration formats will take some time to stabilize, so be prepared for breakage when you upgrade to a new version. -## Building - -The cli comes with a local webserver for visualizing matches. -Therefore, you'll have to build the web application first, so that it can be embedded in the binary. - -You will need: -- rust -- wasm-pack -- npm - -First, build the frontend: -```bash -cd web/pw-frontend -npm install -npm run build-wasm -npm run build -``` - -Then build the backend: -```bash -cd planetwars-cli -cargo build --bin pwcli --release -``` - -You can install the binary by running -```bash -cargo install --path . -``` - -## Getting started -First, initialize your workspace: -```bash -pwcli init my-planetwars-workspace -``` -This creates all required files and directories for your planetwars workspace: -- `pw_workspace.toml`: workspace configuration -- `maps/`: for storing maps -- `matches/`: match logs will be written here -- `bots/simplebot/` an example bot to get started - -All subsequent commands should be run from the root directory of your workspace. - -Try playing an example match: -```bash -pwcli run-match hex simplebot simplebot -``` - -You can now watch a visualization of the match in the web interface: -```bash -pwcli serve -``` - -You can now try writing your own bot by copying the `simplebot` example. Don't forget to add it in your workspace configuration! diff --git a/planetwars-cli/assets/hex.json b/planetwars-cli/assets/hex.json deleted file mode 100644 index 5ef4f31..0000000 --- a/planetwars-cli/assets/hex.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "planets": [ - { - "name": "protos", - "x": -6, - "y": 0, - "owner": 1, - "ship_count": 6 - }, - { - "name": "duteros", - "x": -3, - "y": 5, - "ship_count": 6 - }, - { - "name": "tritos", - "x": 3, - "y": 5, - "ship_count": 6 - }, - { - "name": "tetartos", - "x": 6, - "y": 0, - "owner": 2, - "ship_count": 6 - }, - { - "name": "pemptos", - "x": 3, - "y": -5, - "ship_count": 6 - }, - { - "name": "extos", - "x": -3, - "y": -5, - "ship_count": 6 - } - ] -} - diff --git a/planetwars-cli/assets/pw_workspace.toml b/planetwars-cli/assets/pw_workspace.toml deleted file mode 100644 index d82840f..0000000 --- a/planetwars-cli/assets/pw_workspace.toml +++ /dev/null @@ -1,6 +0,0 @@ -[paths] -maps_dir = "maps" -matches_dir = "matches" - -[bots.simplebot] -path = "bots/simplebot" diff --git a/planetwars-cli/assets/simplebot/botconfig.toml b/planetwars-cli/assets/simplebot/botconfig.toml deleted file mode 100644 index b3a4163..0000000 --- a/planetwars-cli/assets/simplebot/botconfig.toml +++ /dev/null @@ -1,2 +0,0 @@ -name = "simplebot" -run_command = "python3 simplebot.py" \ No newline at end of file diff --git a/planetwars-cli/assets/simplebot/simplebot.py b/planetwars-cli/assets/simplebot/simplebot.py deleted file mode 100644 index b2a6b8f..0000000 --- a/planetwars-cli/assets/simplebot/simplebot.py +++ /dev/null @@ -1,33 +0,0 @@ -import sys, json - -def move(command): - """ print a command record to stdout """ - moves = [] - if command is not None: - moves.append(command) - - print(json.dumps({ 'moves': moves })) - # flush the buffer, so that the gameserver can receive the json-encoded line. - sys.stdout.flush() - - -for line in sys.stdin: - state = json.loads(line) - # you are always player 1. - my_planets = [p for p in state['planets'] if p['owner'] == 1] - other_planets = [p for p in state['planets'] if p['owner'] != 1] - - if not my_planets or not other_planets: - # we don't own any planets, so we can't make any moves. - move(None) - else: - # find my planet that has the most ships - planet = max(my_planets, key=lambda p: p['ship_count']) - # find enemy planet that has the least ships - destination = min(other_planets, key=lambda p: p['ship_count']) - # attack! - move({ - 'origin': planet['name'], - 'destination': destination['name'], - 'ship_count': planet['ship_count'] - 1 - }) diff --git a/planetwars-cli/src/bin/pwcli.rs b/planetwars-cli/src/bin/pwcli.rs deleted file mode 100644 index 438d3bc..0000000 --- a/planetwars-cli/src/bin/pwcli.rs +++ /dev/null @@ -1,6 +0,0 @@ -use planetwars_cli; - -#[tokio::main] -async fn main() { - planetwars_cli::run().await -} diff --git a/planetwars-cli/src/commands/build.rs b/planetwars-cli/src/commands/build.rs deleted file mode 100644 index 1df0bb6..0000000 --- a/planetwars-cli/src/commands/build.rs +++ /dev/null @@ -1,27 +0,0 @@ -use clap::Parser; -use std::io; -use tokio::process; - -use crate::workspace::Workspace; - -#[derive(Parser)] -pub struct BuildCommand { - /// Name of the bot to build - bot: String, -} - -impl BuildCommand { - pub async fn run(self) -> io::Result<()> { - let workspace = Workspace::open_current_dir()?; - let bot = workspace.get_bot(&self.bot)?; - if let Some(argv) = bot.config.get_build_argv() { - process::Command::new(&argv[0]) - .args(&argv[1..]) - .current_dir(&bot.path) - .spawn()? - .wait() - .await?; - } - Ok(()) - } -} diff --git a/planetwars-cli/src/commands/init.rs b/planetwars-cli/src/commands/init.rs deleted file mode 100644 index c95626b..0000000 --- a/planetwars-cli/src/commands/init.rs +++ /dev/null @@ -1,38 +0,0 @@ -use std::path::PathBuf; - -use clap::Parser; -use futures::io; - -#[derive(Parser)] -pub struct InitCommand { - /// workspace root directory - path: String, -} - -macro_rules! copy_asset { - ($path:expr, $file_name:literal) => { - ::std::fs::write( - $path.join($file_name), - include_bytes!(concat!("../../assets/", $file_name)), - )?; - }; -} - -impl InitCommand { - pub async fn run(self) -> io::Result<()> { - let path = PathBuf::from(&self.path); - - // create directories - std::fs::create_dir_all(&path)?; - std::fs::create_dir(path.join("maps"))?; - std::fs::create_dir(path.join("matches"))?; - std::fs::create_dir_all(path.join("bots/simplebot"))?; - - // create files - copy_asset!(path, "pw_workspace.toml"); - copy_asset!(path.join("maps"), "hex.json"); - copy_asset!(path.join("bots/"), "simplebot/botconfig.toml"); - copy_asset!(path.join("bots/"), "simplebot/simplebot.py"); - Ok(()) - } -} diff --git a/planetwars-cli/src/commands/mod.rs b/planetwars-cli/src/commands/mod.rs deleted file mode 100644 index 52fed5c..0000000 --- a/planetwars-cli/src/commands/mod.rs +++ /dev/null @@ -1,40 +0,0 @@ -mod build; -mod init; -mod run_match; -mod serve; - -use clap::{Parser, Subcommand}; -use std::io; - -#[derive(Parser)] -#[clap(name = "pwcli")] -#[clap(author, version, about)] -pub struct Cli { - #[clap(subcommand)] - command: Command, -} - -impl Cli { - pub async fn run() -> io::Result<()> { - let cli = Self::parse(); - - match cli.command { - Command::Init(command) => command.run().await, - Command::RunMatch(command) => command.run().await, - Command::Serve(command) => command.run().await, - Command::Build(command) => command.run().await, - } - } -} - -#[derive(Subcommand)] -enum Command { - /// Initialize a new workspace - Init(init::InitCommand), - /// Run a match - RunMatch(run_match::RunMatchCommand), - /// Host local webserver - Serve(serve::ServeCommand), - /// Run build command for a bot - Build(build::BuildCommand), -} diff --git a/planetwars-cli/src/commands/run_match.rs b/planetwars-cli/src/commands/run_match.rs deleted file mode 100644 index 03868ae..0000000 --- a/planetwars-cli/src/commands/run_match.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::io; - -use clap::Parser; -use planetwars_matchrunner::{run_match, MatchConfig, MatchPlayer}; - -use crate::workspace::Workspace; -#[derive(Parser)] -pub struct RunMatchCommand { - /// map name - map: String, - /// bot names - bots: Vec, -} - -impl RunMatchCommand { - pub async fn run(self) -> io::Result<()> { - let workspace = Workspace::open_current_dir()?; - - let map_path = workspace.map_path(&self.map); - let timestamp = chrono::Local::now().format("%Y-%m-%d-%H-%M-%S"); - let log_path = workspace.match_path(&format!("{}-{}", &self.map, ×tamp)); - - let mut players = Vec::new(); - for bot_name in &self.bots { - let bot = workspace.get_bot(&bot_name)?; - players.push(MatchPlayer { - name: bot_name.clone(), - path: bot.path.clone(), - argv: bot.config.get_run_argv(), - }); - } - - let match_config = MatchConfig { - map_name: self.map, - map_path, - log_path: log_path.clone(), - players, - }; - - run_match(match_config).await; - println!("match completed successfully"); - // TODO: maybe print the match result as well? - - let relative_path = match log_path.strip_prefix(&workspace.root_path) { - Ok(path) => path.to_str().unwrap(), - Err(_) => log_path.to_str().unwrap(), - }; - println!("wrote match log to {}", relative_path); - Ok(()) - } -} diff --git a/planetwars-cli/src/commands/serve.rs b/planetwars-cli/src/commands/serve.rs deleted file mode 100644 index aa8d149..0000000 --- a/planetwars-cli/src/commands/serve.rs +++ /dev/null @@ -1,17 +0,0 @@ -use std::io; - -use clap::Parser; - -use crate::web; -use crate::workspace::Workspace; - -#[derive(Parser)] -pub struct ServeCommand; - -impl ServeCommand { - pub async fn run(self) -> io::Result<()> { - let workspace = Workspace::open_current_dir()?; - web::run(workspace).await; - Ok(()) - } -} diff --git a/planetwars-cli/src/lib.rs b/planetwars-cli/src/lib.rs deleted file mode 100644 index f67b67f..0000000 --- a/planetwars-cli/src/lib.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod commands; -mod web; -mod workspace; - -pub async fn run() { - let res = commands::Cli::run().await; - if let Err(err) = res { - eprintln!("{}", err); - std::process::exit(1); - } -} diff --git a/planetwars-cli/src/web/mod.rs b/planetwars-cli/src/web/mod.rs deleted file mode 100644 index f66b0c6..0000000 --- a/planetwars-cli/src/web/mod.rs +++ /dev/null @@ -1,175 +0,0 @@ -use axum::{ - body::{boxed, Full}, - extract::{ws::WebSocket, Extension, Path, WebSocketUpgrade}, - handler::Handler, - http::{header, StatusCode, Uri}, - response::{IntoResponse, Response}, - routing::{get, Router}, - AddExtensionLayer, Json, -}; -use mime_guess; -use planetwars_matchrunner::MatchMeta; -use rust_embed::RustEmbed; -use serde::{Deserialize, Serialize}; -use std::{ - fs, - io::{self, BufRead}, - net::SocketAddr, - path, - sync::Arc, -}; - -use crate::workspace::Workspace; - -struct State { - workspace: Workspace, -} - -impl State { - fn new(workspace: Workspace) -> Self { - Self { workspace } - } -} - -pub async fn run(workspace: Workspace) { - let shared_state = Arc::new(State::new(workspace)); - - // build our application with a route - let app = Router::new() - .route("/", get(index_handler)) - .route("/ws", get(ws_handler)) - .route("/api/matches", get(list_matches)) - .route("/api/matches/:match_id", get(get_match)) - .fallback(static_handler.into_service()) - .layer(AddExtensionLayer::new(shared_state)); - - // run it - let addr = SocketAddr::from(([127, 0, 0, 1], 5000)); - println!("serving at http://{}", addr); - axum::Server::bind(&addr) - .serve(app.into_make_service()) - .await - .unwrap(); -} - -async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse { - ws.on_upgrade(handle_socket) -} - -async fn handle_socket(mut socket: WebSocket) { - while let Some(msg) = socket.recv().await { - let msg = if let Ok(msg) = msg { - msg - } else { - // client disconnected - return; - }; - - if socket.send(msg).await.is_err() { - // client disconnected - return; - } - } -} - -#[derive(Serialize, Deserialize)] -struct MatchData { - name: String, - #[serde(flatten)] - meta: MatchMeta, -} - -async fn list_matches(Extension(state): Extension>) -> Json> { - let mut matches = state - .workspace - .matches_dir() - .read_dir() - .unwrap() - .filter_map(|entry| { - let entry = entry.unwrap(); - get_match_data(&entry).ok() - }) - .collect::>(); - matches.sort_by(|a, b| { - let a = a.meta.timestamp; - let b = b.meta.timestamp; - a.cmp(&b).reverse() - }); - Json(matches) -} - -// extracts 'filename' if the entry matches'$filename.log'. -fn get_match_data(entry: &fs::DirEntry) -> io::Result { - let file_name = entry.file_name(); - let path = path::Path::new(&file_name); - - let name = get_match_name(&path) - .ok_or_else(|| io::Error::new(io::ErrorKind::InvalidData, "invalid match name"))?; - - let meta = read_match_meta(&entry.path())?; - - Ok(MatchData { name, meta }) -} - -fn get_match_name(path: &path::Path) -> Option { - if path.extension() != Some("log".as_ref()) { - return None; - } - - path.file_stem() - .and_then(|name| name.to_str()) - .map(|name| name.to_string()) -} - -fn read_match_meta(path: &path::Path) -> io::Result { - let file = fs::File::open(path)?; - let mut reader = io::BufReader::new(file); - let mut line = String::new(); - reader.read_line(&mut line)?; - let meta: MatchMeta = serde_json::from_str(&line)?; - Ok(meta) -} - -async fn get_match(Extension(state): Extension>, Path(id): Path) -> String { - let mut match_path = state.workspace.matches_dir().join(id); - match_path.set_extension("log"); - fs::read_to_string(match_path).unwrap() -} - -async fn index_handler() -> impl IntoResponse { - static_handler("/index.html".parse::().unwrap()).await -} - -// static_handler is a handler that serves static files from the -async fn static_handler(uri: Uri) -> impl IntoResponse { - let path = uri.path().trim_start_matches('/').to_string(); - StaticFile(path) -} - -#[derive(RustEmbed)] -#[folder = "../web/pw-frontend/dist/"] -struct Asset; -pub struct StaticFile(pub T); - -impl IntoResponse for StaticFile -where - T: Into, -{ - fn into_response(self) -> Response { - let path = self.0.into(); - match Asset::get(path.as_str()) { - Some(content) => { - let body = boxed(Full::from(content.data)); - let mime = mime_guess::from_path(path).first_or_octet_stream(); - Response::builder() - .header(header::CONTENT_TYPE, mime.as_ref()) - .body(body) - .unwrap() - } - None => Response::builder() - .status(StatusCode::NOT_FOUND) - .body(boxed(Full::from("404"))) - .unwrap(), - } - } -} diff --git a/planetwars-cli/src/workspace/bot.rs b/planetwars-cli/src/workspace/bot.rs deleted file mode 100644 index a0ecb90..0000000 --- a/planetwars-cli/src/workspace/bot.rs +++ /dev/null @@ -1,50 +0,0 @@ -use shlex; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; - -use serde::{Deserialize, Serialize}; - -const BOT_CONFIG_FILENAME: &str = "botconfig.toml"; - -pub struct WorkspaceBot { - pub path: PathBuf, - pub config: BotConfig, -} - -impl WorkspaceBot { - pub fn open(path: &Path) -> io::Result { - let config_path = path.join(BOT_CONFIG_FILENAME); - let config_str = fs::read_to_string(config_path)?; - let bot_config: BotConfig = toml::from_str(&config_str)?; - - Ok(WorkspaceBot { - path: path.to_path_buf(), - config: bot_config, - }) - } -} - -#[derive(Serialize, Deserialize)] -pub struct BotConfig { - pub name: String, - pub run_command: String, - pub build_command: Option, -} - -impl BotConfig { - // TODO: these commands should not be here - pub fn get_run_argv(&self) -> Vec { - // TODO: proper error handling - shlex::split(&self.run_command) - .expect("Failed to parse bot run command. Check for unterminated quotes.") - } - - pub fn get_build_argv(&self) -> Option> { - // TODO: proper error handling - self.build_command.as_ref().map(|cmd| { - shlex::split(cmd) - .expect("Failed to parse bot build command. Check for unterminated quotes.") - }) - } -} diff --git a/planetwars-cli/src/workspace/mod.rs b/planetwars-cli/src/workspace/mod.rs deleted file mode 100644 index 5a1a4ae..0000000 --- a/planetwars-cli/src/workspace/mod.rs +++ /dev/null @@ -1,77 +0,0 @@ -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::env; -use std::fs; -use std::io; -use std::path::{Path, PathBuf}; - -use self::bot::WorkspaceBot; - -const WORKSPACE_CONFIG_FILENAME: &str = "pw_workspace.toml"; - -pub mod bot; - -pub struct Workspace { - pub root_path: PathBuf, - pub config: WorkspaceConfig, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct WorkspaceConfig { - paths: WorkspacePaths, - bots: HashMap, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct WorkspacePaths { - maps_dir: PathBuf, - matches_dir: PathBuf, -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct BotEntry { - path: PathBuf, -} - -impl Workspace { - pub fn open(root_path: &Path) -> io::Result { - let config_path = root_path.join(WORKSPACE_CONFIG_FILENAME); - let config_str = fs::read_to_string(config_path)?; - let workspace_config: WorkspaceConfig = toml::from_str(&config_str)?; - - Ok(Workspace { - root_path: root_path.to_path_buf(), - config: workspace_config, - }) - } - - pub fn open_current_dir() -> io::Result { - Workspace::open(&env::current_dir()?) - } - - pub fn maps_dir(&self) -> PathBuf { - self.root_path.join(&self.config.paths.maps_dir) - } - - pub fn map_path(&self, map_name: &str) -> PathBuf { - self.maps_dir().join(format!("{}.json", map_name)) - } - - pub fn matches_dir(&self) -> PathBuf { - self.root_path.join(&self.config.paths.matches_dir) - } - - pub fn match_path(&self, match_name: &str) -> PathBuf { - self.matches_dir().join(format!("{}.log", match_name)) - } - - pub fn get_bot(&self, bot_name: &str) -> io::Result { - let bot_entry = self.config.bots.get(bot_name).ok_or_else(|| { - io::Error::new( - io::ErrorKind::NotFound, - format!("no such bot: {}", bot_name), - ) - })?; - WorkspaceBot::open(&self.root_path.join(&bot_entry.path)) - } -}