From fc991e417d0b8ebc87557b9c0817c1f9ed6e7a1b Mon Sep 17 00:00:00 2001 From: Martin Hafskjold Thoresen Date: Sat, 4 Jan 2025 22:16:29 +0100 Subject: More work --- src/main.rs | 194 ++++++++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 169 insertions(+), 25 deletions(-) (limited to 'src/main.rs') diff --git a/src/main.rs b/src/main.rs index bb3e9b3..a4224d7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -47,6 +47,9 @@ fn handle_panic(err: Box) -> Response { (StatusCode::INTERNAL_SERVER_ERROR).into_response() } +#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug, PartialOrd, Ord, Deserialize)] +struct UserId(u64); + #[derive(Deserialize, Debug, Clone)] struct NoteDatum { // Epoch time in ms @@ -60,15 +63,12 @@ struct Notes { notes: Vec, } -#[derive(Deserialize)] -struct NotesForm { - notes: String, -} - struct Round { author: UserId, notes: Notes, guesses: HashMap, + /// [true] means the judge has accepted it. Missing means they haven't done anything + correct: HashMap, } struct Game { @@ -118,15 +118,15 @@ impl Game { self.submissions.insert(uid, notes); - self.message_screen(uid, self.screen_submitted()) - .context("message submit screen") - .unwrap(); - if self.active_users.len() == self.submissions.len() { info!("All users have submitted"); let users = self.submissions.keys().cloned(); self.next_rounds = users.collect(); self.new_round(); + } else { + self.message_screen(uid, self.screen_submitted()) + .context("message submit screen") + .unwrap(); } } @@ -144,11 +144,49 @@ impl Game { .unwrap() .clone(), guesses: HashMap::new(), + correct: HashMap::new(), }); - self.broadcast_screen(self.screen_round()) - .context("broadcast screen round") + for &u in &self.active_users { + if u != uid { + self.message_screen(u, self.screen_round()) + .context("message screen round") + .unwrap(); + } + } + self.message_screen(uid, self.screen_judge()) + .context("message screen judge") + .unwrap(); + } + + fn user_guess(&mut self, uid: UserId, guess: String) { + let r = self.rounds.last_mut().context("No current round").unwrap(); + if r.guesses.contains_key(&uid) { + warn!("User tried to guess again"); + return; + } + r.guesses.insert(uid, guess.clone()); + + if let Some(s) = self.senders.get(&r.author) { + s.send( + html! { + li id=(format!("guess-{}", uid.0)) + hx-swap-oob="outerHTML" { + (self.guess_li(uid)) + } + } + .into_string(), + ) + .context("Send screen to author") .unwrap(); + } else { + warn!("Missing sender for author"); + } + } + + fn mark_guess(&mut self, uid: UserId, mark: bool) { + let r = self.rounds.last_mut().context("No round").unwrap(); + r.correct.insert(uid, mark); } fn screen_submitted(&self) -> Markup { @@ -191,16 +229,68 @@ impl Game { } } + /// Players guess the melody fn screen_round(&self) -> Markup { let i = self.rounds.len(); let n = self.rounds.len() + self.next_rounds.len(); let Some(r) = self.rounds.last() else { return html! { h1 { "No more rounds" }}; }; + let gid = self.id; html! { h1 { (format!("Round {i}/{n}"))} p { "Author was " (r.author.0)} + form #form-guess { + input placeholder="Your guess..." name="guess" {}; + input type="submit" hx-post=(format!("/game/{gid}/guess")) hx-target="#form-guess" {}; + } + } + } + + /// Return the `
  • ` for the guess of `uid` so that its status is marked correctly + fn guess_li(&self, uid: UserId) -> Markup { + let r = self.rounds.last().unwrap(); + let Some(g) = r.guesses.get(&uid) else { + return html! { + li id=(format!("guess-{}", uid.0)) style="display: none;" { } + }; + }; + let gid = self.id; + let curr = r.correct.get(&uid).cloned(); + let accept = match curr { + None => "none", + Some(false) => "false", + Some(true) => "true", + }; + let post = format!( + "/game/{gid}/mark/{}/{}", + uid.0, + !curr.unwrap_or(false) // None should mark as True when clicked + ); + + html! { + li id=(format!("guess-{}", uid.0)) + data-accept=(accept) + hx-post=(post) + hx-swap="outerHTML" + { (g) } + } + } + + /// Author of the melody marks correct answers + fn screen_judge(&self) -> Markup { + let mut v = self.active_users.iter().collect::>(); + v.sort(); + + html! { + h1 { "Mark correct answers" } + ul #guesslist { + @for &u in v { + (self.guess_li(u)) + } + } + button { "Done" } } } @@ -353,9 +443,17 @@ async fn get_game( g.screen_compose() } } else { - // TODO - warn!("Wrong screen"); - g.screen_lobby() + if let Some(r) = g.rounds.last() { + if r.author == uid { + g.screen_judge() + } else { + g.screen_round() + } + } else { + // TODO + warn!("Missing screen, default to lobby"); + g.screen_lobby() + } } }; @@ -380,6 +478,10 @@ async fn get_game( }) } +#[derive(Deserialize)] +struct NotesForm { + notes: String, +} async fn submit_game( Path(gid): Path, State(st): State, @@ -403,6 +505,57 @@ async fn submit_game( StatusCode::OK.into_response() } +#[derive(Deserialize)] +struct GuessTune { + guess: String, +} +async fn guess_tune( + Path(gid): Path, + State(st): State, + Extension(uid): Extension, + Form(form): Form, +) -> Response { + let glock = { + if let Some(g) = st.lock().await.games.get(&gid) { + g.clone() + } else { + return (StatusCode::NOT_FOUND).into_response(); + } + }; + glock.lock().await.user_guess(uid, form.guess.clone()); + + html! { + span { + (format!(r#"You guessed "{}" "#, form.guess)) + } + } + .into_string() + .into_response() +} + +async fn mark_guess( + Path((gid, guess_id, mark)): Path<(u64, UserId, bool)>, + State(st): State, + Extension(uid): Extension, +) -> Response { + let glock = { + if let Some(g) = st.lock().await.games.get(&gid) { + g.clone() + } else { + return (StatusCode::NOT_FOUND).into_response(); + } + }; + + let mut g = glock.lock().await; + let allowed = g.rounds.last().map(|r| r.author == uid).unwrap_or(false); + if !allowed { + return StatusCode::UNAUTHORIZED.into_response(); + } + g.mark_guess(guess_id, mark); + + g.guess_li(guess_id).into_string().into_response() +} + async fn game_ws( ws: WebSocketUpgrade, Path(gid): Path, @@ -435,14 +588,6 @@ async fn handle_socket(mut socket: WebSocket, uid: UserId, game: Arc } let mut single_rx = { let mut g = game.lock().await; - // g.broadcast_tx - // .send( - // html! { - // div #messages hx-swap-oob="beforeend" { p { "Someone joined" } } - // } - // .into_string(), - // ) - // .unwrap(); let (tx, rx) = broadcast::channel(16); g.senders.insert(uid, tx); rx @@ -482,9 +627,6 @@ async fn handle_socket(mut socket: WebSocket, uid: UserId, game: Arc } } -#[derive(Hash, Eq, PartialEq, Copy, Clone, Debug)] -struct UserId(u64); - async fn identity_middleware(jar: CookieJar, mut req: Request, next: Next) -> Response { const UID: &'static str = "uid"; @@ -532,7 +674,9 @@ async fn main() -> Result<()> { .route("/game", post(create_game)) .route("/game/{gid}", get(get_game)) .route("/game/{gid}/start", post(start_game)) + .route("/game/{gid}/guess", post(guess_tune)) .route("/game/{gid}/submit", post(submit_game)) + .route("/game/{gid}/mark/{uid}/{status}", post(mark_guess)) .route("/game/{gid}/ws", get(game_ws)) .nest_service("/static", ServeDir::new("static")) .layer(CatchPanicLayer::custom(handle_panic)) -- cgit v1.2.3