summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock39
-rw-r--r--Cargo.toml4
-rw-r--r--src/main.rs116
3 files changed, 145 insertions, 14 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 641787f..6f61113 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -24,6 +24,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
[[package]]
+name = "async-trait"
+version = "0.1.83"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "721cae7de5c34fbb2acd27e21e6d2cf7b886dce0c27388d46c4e6c47ea4318dd"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -35,7 +46,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
dependencies = [
- "axum-core",
+ "axum-core 0.5.0",
"base64",
"bytes",
"form_urlencoded",
@@ -68,6 +79,26 @@ dependencies = [
[[package]]
name = "axum-core"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199"
+dependencies = [
+ "async-trait",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "rustversion",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "axum-core"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
@@ -93,7 +124,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "460fc6f625a1f7705c6cf62d0d070794e94668988b1c38111baeec177c715f7b"
dependencies = [
"axum",
- "axum-core",
+ "axum-core 0.5.0",
"bytes",
"futures-util",
"http",
@@ -433,6 +464,8 @@ version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa"
dependencies = [
+ "axum-core 0.4.5",
+ "http",
"itoa",
"maud_macros",
]
@@ -503,6 +536,8 @@ dependencies = [
"cookie",
"maud",
"rand",
+ "serde",
+ "serde_json",
"tokio",
"tokio-tungstenite",
"tower-http",
diff --git a/Cargo.toml b/Cargo.toml
index 4b0600f..e6c281c 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,8 +10,10 @@ axum-extra = "0.10.0"
axum-macros = "0.5.0"
bytes = "1.9.0"
cookie = { version = "0.18.1", features = ["percent-encode"] }
-maud = "0.26.0"
+maud = { version = "0.26.0", features = ["axum"] }
rand = "0.8.5"
+serde = { version = "1.0.217", features = ["derive"] }
+serde_json = "1.0.134"
tokio = { version = "1.42.0", features = [
"macros",
"rt-multi-thread",
diff --git a/src/main.rs b/src/main.rs
index 0cc864f..e164981 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashMap, net::SocketAddr, sync::Arc};
+use std::{collections::HashMap, net::SocketAddr, sync::Arc, time::Duration};
use anyhow::{Context, Result};
use axum::{
@@ -10,10 +10,11 @@ use axum::{
http::StatusCode,
response::{IntoResponse, Response},
routing::{get, post},
- Router,
+ Form, Router,
};
use maud::{html, Markup, PreEscaped, DOCTYPE};
use rand::random;
+use serde::Deserialize;
use tokio::sync::{
broadcast::{self, Receiver, Sender},
Mutex,
@@ -23,7 +24,7 @@ use tower_http::{
services::ServeDir,
trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer},
};
-use tracing::{error, info, trace, Level};
+use tracing::{error, info, trace, warn, Level};
use tracing_subscriber::FmtSubscriber;
/// If a handler panics we run this function and return the [Response].
@@ -40,11 +41,31 @@ fn handle_panic(err: Box<dyn std::any::Any + Send + 'static>) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR).into_response()
}
+#[derive(Deserialize)]
+struct NoteDatum {
+ // Epoch time in ms
+ time: u64,
+ // Fraction up/down
+ y: f64,
+}
+
+#[derive(Deserialize)]
+struct Notes {
+ notes: Vec<NoteDatum>,
+}
+
+#[derive(Deserialize)]
+struct NotesForm {
+ notes: String,
+}
+
struct Game {
id: u64,
is_started: bool,
+ submitted: Option<Notes>,
+
/// Sender to broadcast things
broadcast_tx: Sender<String>,
/// Broadcast receiver to send to WebSocket
@@ -52,24 +73,66 @@ struct Game {
}
impl Game {
+ /// Return the appropriate HTML for the game state.
fn get_html(&self) -> Markup {
let gid = self.id;
- if !self.is_started {
+
+ if self.submitted.is_some() {
return html! {
- h1 { "Game" }
- p { "Current game id is " (gid)}
- div #messages {};
- button hx-post=(format!("/game/{gid}/start")) { "Start" }
+ h1 { "Submitted!" }
};
- } else {
+ }
+ if self.is_started {
return html! {
+ script { ({PreEscaped("document.notes = []; ")})}
h1 { "Playing Game" }
p { "Were playing here" }
+
+ canvas
+ #canvas
+ height=(256) width=(256)
+ style="position:relative; background: cyan;"
+ {};
+ script { (PreEscaped(r#"document.getElementById("canvas").onclick = (e) => {
+ const notes = JSON.parse(e.target.dataset.notes ?? "[]");
+ notes.push({ time: Date.now(), y: e.layerY / e.target.height});
+ e.target.dataset.notes = JSON.stringify(notes);
+ }"#))}
+
+ button hx-post=(format!("/game/{gid}/submit"))
+ hx-vals=r#"js:{ "notes": document.getElementById("canvas").dataset.notes }"#
+ { "Submit" }
};
}
+
+ html! {
+ h1 { "Game" }
+ p { "Current game id is " (gid)}
+ div #messages {};
+ button hx-post=(format!("/game/{gid}/start")) { "Start" }
+ }
}
}
+async fn submit_game_answer(lock: Arc<Mutex<Game>>, notes: Notes) {
+ lock.lock().await.submitted = Some(notes);
+
+ tokio::spawn(async move {
+ let _ = tokio::time::sleep(Duration::from_secs(2)).await;
+ let mut g = lock.lock().await;
+ g.is_started = false;
+ g.submitted = None;
+ g.broadcast_tx.send(
+ html! {
+ section #content {
+ (g.get_html())
+ }
+ }
+ .into_string(),
+ )
+ });
+}
+
struct ServerState {
games: HashMap<u64, Arc<Mutex<Game>>>,
}
@@ -80,6 +143,7 @@ impl ServerState {
let g = Game {
id: gid,
is_started: false,
+ submitted: None,
broadcast_rx: rx,
broadcast_tx: tx,
};
@@ -134,7 +198,7 @@ fn html_response(html: PreEscaped<String>) -> Response {
.expect("Failed to set body")
}
-async fn post_game(State(st): State<Server>) -> Response {
+async fn create_game(State(st): State<Server>) -> Response {
let gid = random::<u64>();
let _ = {
@@ -201,6 +265,35 @@ async fn get_game(Path(gid): Path<u64>, State(st): State<Server>) -> Response {
})
}
+async fn submit_game(
+ Path(gid): Path<u64>,
+ State(st): State<Server>,
+ Form(form): Form<NotesForm>,
+) -> Response {
+ let glock = {
+ if let Some(g) = st.lock().await.games.get(&gid) {
+ g.clone()
+ } else {
+ return (StatusCode::NOT_FOUND).into_response();
+ }
+ };
+
+ let Ok(notes) = serde_json::from_str(&form.notes) else {
+ warn!(notes = form.notes);
+ return (StatusCode::UNPROCESSABLE_ENTITY).into_response();
+ };
+
+ submit_game_answer(glock.clone(), Notes { notes }).await;
+
+ let g = glock.lock().await;
+ let html = g.get_html();
+ g.broadcast_tx
+ .send(html! { section #content { (html) } }.into_string())
+ .expect("failed to send");
+
+ StatusCode::OK.into_response()
+}
+
async fn game_ws(ws: WebSocketUpgrade, Path(gid): Path<u64>, State(st): State<Server>) -> Response {
let game = {
let s = st.lock().await;
@@ -262,9 +355,10 @@ async fn main() -> Result<()> {
let app = Router::new()
.route("/", get(route_index))
- .route("/game", post(post_game))
+ .route("/game", post(create_game))
.route("/game/{gid}", get(get_game))
.route("/game/{gid}/start", post(start_game))
+ .route("/game/{gid}/submit", post(submit_game))
.route("/game/{gid}/ws", get(game_ws))
.nest_service("/static", ServeDir::new("static"))
.layer(CatchPanicLayer::custom(handle_panic))