aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--planetwars-localdev/README.md1
-rw-r--r--planetwars-localdev/src/lib.rs14
-rw-r--r--planetwars-localdev/src/web/mod.rs123
3 files changed, 138 insertions, 0 deletions
diff --git a/planetwars-localdev/README.md b/planetwars-localdev/README.md
index 3263508..72572d2 100644
--- a/planetwars-localdev/README.md
+++ b/planetwars-localdev/README.md
@@ -7,3 +7,4 @@ Tools for developping planetwars bots locally.
1. Initialize your project directory: `pwcli init-project my_project`
2. Enter your fresh project: `cd my_project`
3. Run an example match: `pwcli run-match hex simplebot simplebot`
+4. View your match in the web UI: `pwcli serve`
diff --git a/planetwars-localdev/src/lib.rs b/planetwars-localdev/src/lib.rs
index 562e9a6..c64fb55 100644
--- a/planetwars-localdev/src/lib.rs
+++ b/planetwars-localdev/src/lib.rs
@@ -26,6 +26,8 @@ enum Commands {
InitProject(InitProjectCommand),
/// Run a match
RunMatch(RunMatchCommand),
+ /// Host local webserver
+ Serve(ServeCommand),
}
#[derive(Parser)]
@@ -42,6 +44,9 @@ struct InitProjectCommand {
path: String,
}
+#[derive(Parser)]
+struct ServeCommand;
+
#[derive(Serialize, Deserialize, Debug)]
struct ProjectConfig {
bots: HashMap<String, BotConfig>,
@@ -58,6 +63,7 @@ pub async fn run() {
let res = match matches.command {
Commands::RunMatch(command) => run_match(command).await,
Commands::InitProject(command) => init_project(command),
+ Commands::Serve(_) => run_webserver().await,
};
if let Err(err) = res {
eprintln!("{}", err);
@@ -139,3 +145,11 @@ fn init_project(command: InitProjectCommand) -> io::Result<()> {
Ok(())
}
+
+mod web;
+async fn run_webserver() -> io::Result<()> {
+ let project_dir = env::current_dir().unwrap();
+
+ web::run(project_dir).await;
+ Ok(())
+}
diff --git a/planetwars-localdev/src/web/mod.rs b/planetwars-localdev/src/web/mod.rs
new file mode 100644
index 0000000..a5d0989
--- /dev/null
+++ b/planetwars-localdev/src/web/mod.rs
@@ -0,0 +1,123 @@
+use axum::{
+ body::{boxed, Full},
+ extract::{Extension, Path},
+ handler::Handler,
+ http::{header, StatusCode, Uri},
+ response::{IntoResponse, Response},
+ routing::{get, Router},
+ AddExtensionLayer, Json,
+};
+use mime_guess;
+use rust_embed::RustEmbed;
+use serde::{Deserialize, Serialize};
+use std::{
+ net::SocketAddr,
+ path::{self, PathBuf},
+ sync::Arc,
+};
+
+struct State {
+ project_root: PathBuf,
+}
+
+impl State {
+ fn new(project_root: PathBuf) -> Self {
+ Self { project_root }
+ }
+}
+
+pub async fn run(project_root: PathBuf) {
+ let shared_state = Arc::new(State::new(project_root));
+
+ // build our application with a route
+ let app = Router::new()
+ .route("/", get(index_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();
+}
+
+#[derive(Serialize, Deserialize)]
+struct Match {
+ name: String,
+}
+
+async fn list_matches(Extension(state): Extension<Arc<State>>) -> Json<Vec<Match>> {
+ let matches = state
+ .project_root
+ .join("matches")
+ .read_dir()
+ .unwrap()
+ .filter_map(|entry| {
+ let entry = entry.unwrap();
+ extract_match_name(entry).map(|name| Match { name })
+ })
+ .collect::<Vec<_>>();
+ Json(matches)
+}
+
+// extracts 'filename' if the entry matches'$filename.log'.
+fn extract_match_name(entry: std::fs::DirEntry) -> Option<String> {
+ let file_name = entry.file_name();
+ let path = path::Path::new(&file_name);
+ if path.extension() == Some("log".as_ref()) {
+ path.file_stem()
+ .and_then(|name| name.to_str())
+ .map(|name| name.to_string())
+ } else {
+ None
+ }
+}
+
+async fn get_match(Extension(state): Extension<Arc<State>>, Path(id): Path<String>) -> String {
+ let mut match_path = state.project_root.join("matches").join(id);
+ match_path.set_extension("log");
+ std::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(),
+ }
+ }
+}