summaryrefslogtreecommitdiff
path: root/src/main.rs
diff options
context:
space:
mode:
authorMartin Hafskjold Thoresen <martin@vind.ai>2025-01-04 14:55:12 +0100
committerMartin Hafskjold Thoresen <martin@vind.ai>2025-01-04 14:55:12 +0100
commite6da55daa54cdf528af1f3e7617ca124ffee3159 (patch)
treecce79cb8fbc4b16a49352240e232a038037de1c3 /src/main.rs
parent7a049d3acf49af248700e18568ee18ee9e99fc9c (diff)
downloadmusicgame-e6da55daa54cdf528af1f3e7617ca124ffee3159.tar.gz
musicgame-e6da55daa54cdf528af1f3e7617ca124ffee3159.zip
Skeleton of gameplay loop in place
Diffstat (limited to 'src/main.rs')
-rw-r--r--src/main.rs249
1 files changed, 240 insertions, 9 deletions
diff --git a/src/main.rs b/src/main.rs
index e078a32..0cc864f 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,19 +1,32 @@
-use std::net::SocketAddr;
+use std::{collections::HashMap, net::SocketAddr, sync::Arc};
use anyhow::{Context, Result};
use axum::{
+ body::Body,
+ extract::{
+ ws::{Message, WebSocket},
+ Path, State, WebSocketUpgrade,
+ },
http::StatusCode,
response::{IntoResponse, Response},
- routing::get,
+ routing::{get, post},
Router,
};
+use maud::{html, Markup, PreEscaped, DOCTYPE};
+use rand::random;
+use tokio::sync::{
+ broadcast::{self, Receiver, Sender},
+ Mutex,
+};
use tower_http::{
catch_panic::CatchPanicLayer,
- trace::{self, TraceLayer},
+ services::ServeDir,
+ trace::{DefaultMakeSpan, DefaultOnResponse, TraceLayer},
};
-use tracing::{error, info, Level};
+use tracing::{error, info, trace, Level};
use tracing_subscriber::FmtSubscriber;
+/// If a handler panics we run this function and return the [Response].
fn handle_panic(err: Box<dyn std::any::Any + Send + 'static>) -> Response {
let details = if let Some(s) = err.downcast_ref::<String>() {
s.clone()
@@ -27,6 +40,213 @@ fn handle_panic(err: Box<dyn std::any::Any + Send + 'static>) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR).into_response()
}
+struct Game {
+ id: u64,
+
+ is_started: bool,
+
+ /// Sender to broadcast things
+ broadcast_tx: Sender<String>,
+ /// Broadcast receiver to send to WebSocket
+ broadcast_rx: Receiver<String>,
+}
+
+impl Game {
+ fn get_html(&self) -> Markup {
+ let gid = self.id;
+ if !self.is_started {
+ return html! {
+ h1 { "Game" }
+ p { "Current game id is " (gid)}
+ div #messages {};
+ button hx-post=(format!("/game/{gid}/start")) { "Start" }
+ };
+ } else {
+ return html! {
+ h1 { "Playing Game" }
+ p { "Were playing here" }
+ };
+ }
+ }
+}
+
+struct ServerState {
+ games: HashMap<u64, Arc<Mutex<Game>>>,
+}
+
+impl ServerState {
+ fn new_game(&mut self, gid: u64) {
+ let (tx, rx) = broadcast::channel(16);
+ let g = Game {
+ id: gid,
+ is_started: false,
+ broadcast_rx: rx,
+ broadcast_tx: tx,
+ };
+ self.games.insert(gid, Arc::new(Mutex::new(g)));
+ }
+}
+
+type Server = Arc<Mutex<ServerState>>;
+
+async fn route_index(State(st): State<Server>) -> Response {
+ let game_ids = st.lock().await.games.keys().cloned().collect::<Vec<_>>();
+
+ let game_list = if game_ids.is_empty() {
+ html! { "No current games"}
+ } else {
+ html! {
+ h3 { "Games" }
+ ul {
+ @for id in &game_ids {
+ li { a href=(format!("/game/{id}")) { (id) } }
+ }
+ }
+ }
+ };
+
+ let html = html! {
+ h1 { "Good game "}
+ (game_list)
+
+ button hx-post="/game" { "Create game" }
+ };
+
+ html_response(html! {
+ (DOCTYPE)
+ head {
+ meta charset="utf-8";
+ title { "Good Game" };
+ meta name="viewport" content="width=device-width, initial-scale=1" {};
+ script src="https://unpkg.com/htmx.org@2.0.4" {};
+ link rel="stylesheet" type="text/css" href="static/style.css";
+ }
+ body {
+ (html)
+ }
+ })
+}
+
+fn html_response(html: PreEscaped<String>) -> Response {
+ Response::builder()
+ .status(StatusCode::OK)
+ .body(Body::from(html.into_string()))
+ .expect("Failed to set body")
+}
+
+async fn post_game(State(st): State<Server>) -> Response {
+ let gid = random::<u64>();
+
+ let _ = {
+ let mut s = st.lock().await;
+ s.new_game(gid);
+ };
+
+ Response::builder()
+ .header("hx-redirect", format!("/game/{gid}"))
+ .body(Body::empty())
+ .expect("failed to build response")
+}
+
+async fn start_game(Path(gid): Path<u64>, State(st): State<Server>) -> Response {
+ let lock = {
+ if let Some(g) = st.lock().await.games.get(&gid) {
+ g.clone()
+ } else {
+ return StatusCode::NOT_FOUND.into_response();
+ }
+ };
+
+ let mut g = lock.lock().await;
+
+ g.is_started = true;
+ 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 get_game(Path(gid): Path<u64>, State(st): State<Server>) -> Response {
+ let game = {
+ if let Some(g) = st.lock().await.games.get(&gid) {
+ g.clone()
+ } else {
+ return (StatusCode::NOT_FOUND).into_response();
+ }
+ };
+
+ let html = game.lock().await.get_html();
+
+ html_response(html! {
+ (DOCTYPE)
+ head {
+ meta charset="utf-8";
+ title { "Good Game" };
+ meta name="viewport" content="width=device-width, initial-scale=1" {};
+ script src="https://unpkg.com/htmx.org@2.0.4" {};
+ script src="https://unpkg.com/htmx-ext-ws@2.0.1/ws.js" {};
+
+ link rel="stylesheet" type="text/css" href="/static/style.css";
+ }
+ body hx-ext="ws" ws-connect=(format!("/game/{gid}/ws")) {
+ footer { }
+ section #content {
+ (html)
+ }
+ footer { span { "Good Game" } }
+ }
+ })
+}
+
+async fn game_ws(ws: WebSocketUpgrade, Path(gid): Path<u64>, State(st): State<Server>) -> Response {
+ let game = {
+ let s = st.lock().await;
+ if let Some(g) = s.games.get(&gid) {
+ g.clone()
+ } else {
+ return StatusCode::NOT_FOUND.into_response();
+ }
+ };
+
+ ws.on_upgrade(move |socket| handle_socket(socket, game))
+ .into_response()
+}
+
+async fn handle_socket(mut socket: WebSocket, game: Arc<Mutex<Game>>) {
+ if socket
+ .send(Message::Ping(bytes::Bytes::from("wat")))
+ .await
+ .is_ok()
+ {
+ trace!("Pinged ws");
+ } else {
+ println!("Could not send ping!");
+ return;
+ }
+ {
+ game.lock()
+ .await
+ .broadcast_tx
+ .send(
+ html! {
+ div #messages hx-swap-oob="beforeend" { p { "Someone joined" } }
+ }
+ .into_string(),
+ )
+ .unwrap();
+ }
+
+ let mut rx = { game.lock().await.broadcast_rx.resubscribe() };
+ while let Ok(a) = rx.recv().await {
+ trace!(a, "send ");
+ let res = socket.send(a.into()).await;
+ trace!(?res);
+ }
+}
+
#[tokio::main]
async fn main() -> Result<()> {
let subscriber = FmtSubscriber::builder()
@@ -35,16 +255,27 @@ async fn main() -> Result<()> {
tracing::subscriber::set_global_default(subscriber)
.expect("set global default subscriber failed");
+ let mut server_state = ServerState {
+ games: HashMap::new(),
+ };
+ server_state.new_game(123);
+
let app = Router::new()
- .route("/", get(|| async { "wat" }))
+ .route("/", get(route_index))
+ .route("/game", post(post_game))
+ .route("/game/{gid}", get(get_game))
+ .route("/game/{gid}/start", post(start_game))
+ .route("/game/{gid}/ws", get(game_ws))
+ .nest_service("/static", ServeDir::new("static"))
.layer(CatchPanicLayer::custom(handle_panic))
.layer(
TraceLayer::new_for_http()
- .make_span_with(trace::DefaultMakeSpan::new().level(Level::INFO))
- .on_response(trace::DefaultOnResponse::new().level(Level::INFO)),
- );
+ .make_span_with(DefaultMakeSpan::new().level(Level::INFO))
+ .on_response(DefaultOnResponse::new().level(Level::INFO)),
+ )
+ .with_state(Arc::new(Mutex::new(server_state)));
- let addr = "0.0.0.0:4800";
+ let addr = "192.168.0.106:4800";
info!("Listening on {addr}");
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
axum::serve(