delete old planetwars-cli code

This commit is contained in:
Ilion Beyst 2022-07-21 19:19:40 +02:00
parent 31f8271db6
commit c6293d8e32
16 changed files with 0 additions and 658 deletions

View file

@ -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"

View file

@ -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!

View file

@ -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
}
]
}

View file

@ -1,6 +0,0 @@
[paths]
maps_dir = "maps"
matches_dir = "matches"
[bots.simplebot]
path = "bots/simplebot"

View file

@ -1,2 +0,0 @@
name = "simplebot"
run_command = "python3 simplebot.py"

View file

@ -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
})

View file

@ -1,6 +0,0 @@
use planetwars_cli;
#[tokio::main]
async fn main() {
planetwars_cli::run().await
}

View file

@ -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(())
}
}

View file

@ -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(())
}
}

View file

@ -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),
}

View file

@ -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<String>,
}
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, &timestamp));
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(())
}
}

View file

@ -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(())
}
}

View file

@ -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);
}
}

View file

@ -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<Arc<State>>) -> Json<Vec<MatchData>> {
let mut matches = state
.workspace
.matches_dir()
.read_dir()
.unwrap()
.filter_map(|entry| {
let entry = entry.unwrap();
get_match_data(&entry).ok()
})
.collect::<Vec<_>>();
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<MatchData> {
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<String> {
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<MatchMeta> {
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<Arc<State>>, Path(id): Path<String>) -> 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::<Uri>().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<T>(pub T);
impl<T> IntoResponse for StaticFile<T>
where
T: Into<String>,
{
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(),
}
}
}

View file

@ -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<Self> {
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<String>,
}
impl BotConfig {
// TODO: these commands should not be here
pub fn get_run_argv(&self) -> Vec<String> {
// 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<Vec<String>> {
// 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.")
})
}
}

View file

@ -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<String, BotEntry>,
}
#[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<Workspace> {
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> {
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<WorkspaceBot> {
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))
}
}