summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorMartin Hafskjold Thoresen <martin@vind.ai>2025-01-04 17:02:57 +0100
committerMartin Hafskjold Thoresen <martin@vind.ai>2025-01-04 17:02:57 +0100
commite855b6f7315b1f2db6a5c3a5a46fffeeb15251b0 (patch)
treebb8692680314ab50355f1ca843707689b14ffe7c /src/main.rs
parente6da55daa54cdf528af1f3e7617ca124ffee3159 (diff)
downloadmusicgame-e855b6f7315b1f2db6a5c3a5a46fffeeb15251b0.tar.gz
musicgame-e855b6f7315b1f2db6a5c3a5a46fffeeb15251b0.zip
POC async timeout for a thing
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs116
1 files changed, 105 insertions, 11 deletions
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))