From f356a3692829db4ddba43cc5d8dcc8e031187420 Mon Sep 17 00:00:00 2001 From: "Dylan R. Johnston" Date: Fri, 30 Aug 2024 17:18:55 +0800 Subject: [PATCH] Refactor game_state into directory to remove game_id --- .vscode/settings.json | 3 + app/src/adapters/game_state/durable_object.rs | 23 ++- app/src/adapters/game_state/file.rs | 76 ++++---- app/src/adapters/game_state/in_memory.rs | 70 ------- app/src/adapters/game_state/mod.rs | 2 - app/src/bin/server.rs | 4 +- app/src/extractors.rs | 20 +- app/src/handlers/on_connect.rs | 35 ++-- app/src/handlers/register_command.rs | 20 +- app/src/ports/game_state.rs | 16 +- app/src/router.rs | 10 +- app/src/screens/player/pregame.rs | 181 +++++++++--------- assets/pkg/style.css | 13 +- game/src/plugins/scenes/race.rs | 28 ++- shared/src/models/commands.rs | 2 +- shared/src/models/projections.rs | 4 + 16 files changed, 236 insertions(+), 271 deletions(-) delete mode 100644 app/src/adapters/game_state/in_memory.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index 34f1128..6adf332 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -42,4 +42,7 @@ "CARGO_TARGET_DIR": "target/rust-analyzer" }, "rust-analyzer.check.extraArgs": ["--target-dir=target/rust-analyzer"] + // "rust-analyzer.cargo.target": "wasm32-unknown-unknown" + // "rust-analyzer.cargo.noDefaultFeatures": true, + // "rust-analyzer.cargo.features": ["app/ssr", "app/wasm"] } diff --git a/app/src/adapters/game_state/durable_object.rs b/app/src/adapters/game_state/durable_object.rs index 65393e8..31b3d9c 100644 --- a/app/src/adapters/game_state/durable_object.rs +++ b/app/src/adapters/game_state/durable_object.rs @@ -10,7 +10,7 @@ use worker::{durable_object, Env, State, Storage, WebSocketPair}; use crate::adapters::event_log::durable_object::DurableObjectKeyValue; use crate::ports::event_log::EventLog; -use crate::ports::game_state::GameState; +use crate::ports::game_state::{GameDirectory, GameState}; use crate::router::into_game_router; #[derive(Clone)] @@ -44,19 +44,17 @@ impl DurableObject for Game { pub async fn alarm(&mut self) -> worker::Result { let events = self.events.vector().await.map_err(|err| err.to_string())?; - // TODO: Remove this, GameState should be produced by a factory that takes GameID instead; - let _game_id = GameID::random(); let (events, alarm) = run_processors(&events).map_err(|err| err.to_string())?; for event in events { - self.push_event(_game_id, event) + self.push_event(event) .await .map_err(|err| err.to_string())?; } if let Some(Alarm(duration)) = alarm { - self.set_alarm(_game_id, duration) + self.set_alarm(duration) .await .map_err(|err| err.to_string())?; } @@ -67,11 +65,11 @@ impl DurableObject for Game { // DurableObject Game State Can Ignore the GameID parameter as there is one per game impl GameState for Game { - async fn events(&self, _: GameID) -> anyhow::Result> { + async fn events(&self) -> anyhow::Result> { SendFuture::new(async { Ok(self.events.vector().await?) }).await } - async fn push_event(&self, _: GameID, event: Event) -> anyhow::Result<()> { + async fn push_event(&self, event: Event) -> anyhow::Result<()> { SendFuture::new(async { self.events.push(event.clone()).await; @@ -84,7 +82,7 @@ impl GameState for Game { .await } - fn accept_web_socket(&self, _: GameID) -> anyhow::Result { + fn accept_web_socket(&self) -> anyhow::Result { let pair = WebSocketPair::new()?; self.state.accept_web_socket(&pair.server); @@ -93,7 +91,6 @@ impl GameState for Game { fn set_alarm( &self, - _: GameID, duration: std::time::Duration, ) -> std::pin::Pin> + Send + '_>> { Box::pin(async move { @@ -112,3 +109,11 @@ impl GameState for Game { }) } } + +impl GameDirectory for Game { + type GameState = Game; + + async fn get(&self, _: GameID) -> Self::GameState { + self.clone() + } +} diff --git a/app/src/adapters/game_state/file.rs b/app/src/adapters/game_state/file.rs index 80a2cd4..d7ad26b 100644 --- a/app/src/adapters/game_state/file.rs +++ b/app/src/adapters/game_state/file.rs @@ -8,16 +8,24 @@ use tracing::instrument; use crate::{ adapters::event_log::file::FileEventLog, - ports::{event_log::EventLog, game_state::GameState}, + ports::{ + event_log::EventLog, + game_state::{GameDirectory, GameState}, + }, }; -struct Game { +struct InnerGame { events: FileEventLog, sockets: Vec, alarm: Option>, } -impl Game { +#[derive(Clone)] +pub struct Game { + inner: Arc>, +} + +impl InnerGame { async fn push_event(&mut self, event: Event) -> Result<()> { self.events.push(event.clone()).await?; @@ -43,49 +51,48 @@ impl Game { impl Game { fn from_game_id(game_id: GameID) -> Self { Self { - events: FileEventLog::from_game_id(game_id), - sockets: vec![], - alarm: None, + inner: Arc::new(Mutex::new(InnerGame { + events: FileEventLog::from_game_id(game_id), + sockets: vec![], + alarm: None, + })), } } } #[derive(Clone, Default)] -pub struct FileGameState { +pub struct FileGameDirectory { inner: Arc>>, } -impl GameState for FileGameState { - async fn events(&self, game_id: GameID) -> Result> { +impl GameDirectory for FileGameDirectory { + type GameState = Game; + + async fn get(&self, game_id: GameID) -> Self::GameState { self.inner .lock() .await .entry(game_id) .or_insert_with(|| Game::from_game_id(game_id)) - .events - .vector() - .await + .clone() + } +} + +impl GameState for Game { + async fn events(&self) -> Result> { + self.inner.lock().await.events.vector().await } - async fn push_event(&self, game_id: GameID, event: Event) -> Result<()> { + async fn push_event(&self, event: Event) -> Result<()> { let mut lock_guard = self.inner.lock().await; - let game = lock_guard - .entry(game_id) - .or_insert_with(|| Game::from_game_id(game_id)); - game.push_event(event).await?; + lock_guard.push_event(event).await?; Ok(()) } - async fn accept_web_socket(&self, game_id: GameID, ws: WebSocket) -> Result<()> { - self.inner - .lock() - .await - .entry(game_id) - .or_insert_with(|| Game::from_game_id(game_id)) - .sockets - .push(ws); + async fn accept_web_socket(&self, ws: WebSocket) -> Result<()> { + self.inner.lock().await.sockets.push(ws); Ok(()) } @@ -93,15 +100,10 @@ impl GameState for FileGameState { #[instrument(skip(self))] fn set_alarm( &self, - game_id: GameID, duration: Duration, ) -> Pin> + Send + '_>> { Box::pin(async move { - let mut lock_guard = self.inner.lock().await; - - let game = lock_guard - .entry(game_id) - .or_insert_with(|| Game::from_game_id(game_id)); + let mut game = self.inner.lock().await; if let Some(handle) = game.alarm.take() { handle.abort(); @@ -121,11 +123,7 @@ impl GameState for FileGameState { return; }; - let mut lock_guard = this.lock().await; - - let game = lock_guard - .entry(game_id) - .or_insert_with(|| Game::from_game_id(game_id)); + let mut game = this.lock().await; let (new_events, alarm) = run_processors(&game.events.vector().await?)?; @@ -136,12 +134,10 @@ impl GameState for FileGameState { } game.alarm = None; - drop(lock_guard); + drop(game); if let Some(alarm) = alarm { - FileGameState { inner: this } - .set_alarm(game_id, alarm.0) - .await?; + Game { inner: this }.set_alarm(alarm.0).await?; } }; diff --git a/app/src/adapters/game_state/in_memory.rs b/app/src/adapters/game_state/in_memory.rs deleted file mode 100644 index bf3a744..0000000 --- a/app/src/adapters/game_state/in_memory.rs +++ /dev/null @@ -1,70 +0,0 @@ -use std::{collections::HashMap, pin::Pin, sync::Arc}; -use tokio::sync::{Mutex, RwLock}; - -use anyhow::Result; -use axum::extract::ws::WebSocket; -use shared::models::{events::Event, game_id::GameID}; - -use crate::{ - adapters::event_log::in_memory::InMemoryKV, - ports::{event_log::EventLog, game_state::GameState}, -}; - -#[derive(Clone, Default)] -struct Game { - events: InMemoryKV, - sockets: Arc>>, -} - -#[derive(Clone, Default)] -pub struct InMemoryGameState { - inner: Arc>>, -} - -impl GameState for InMemoryGameState { - async fn events(&self, game_id: GameID) -> Result> { - self.inner - .read() - .await - .get(&game_id) - .cloned() - .unwrap_or_default() - .events - .vector() - .await - } - - async fn push_event(&self, game_id: GameID, event: Event) -> Result<()> { - let mut lock_guard = self.inner.write().await; - let game = lock_guard.entry(game_id).or_default(); - - game.events.push(event.clone()).await?; - - for socket in game.sockets.lock().await.iter_mut() { - let message = serde_json::to_string(&event)?; - - if let Err(err) = socket.send(message.into()).await { - tracing::error!(?err, "error forwarding event to client sockets") - } - } - - Ok(()) - } - - async fn accept_web_socket(&self, game_id: GameID, ws: WebSocket) -> Result<()> { - let mut lock_guard = self.inner.write().await; - let game = lock_guard.entry(game_id).or_default(); - - game.sockets.lock().await.push(ws); - - Ok(()) - } - - fn set_alarm( - &self, - _: GameID, - _: std::time::Duration, - ) -> Pin> + Send + '_>> { - unimplemented!() - } -} diff --git a/app/src/adapters/game_state/mod.rs b/app/src/adapters/game_state/mod.rs index 4653b62..69de0ad 100644 --- a/app/src/adapters/game_state/mod.rs +++ b/app/src/adapters/game_state/mod.rs @@ -2,5 +2,3 @@ pub mod durable_object; #[cfg(not(target_arch = "wasm32"))] pub mod file; -#[cfg(not(target_arch = "wasm32"))] -pub mod in_memory; diff --git a/app/src/bin/server.rs b/app/src/bin/server.rs index b999cf5..baafe77 100644 --- a/app/src/bin/server.rs +++ b/app/src/bin/server.rs @@ -1,7 +1,7 @@ use std::net::SocketAddr; use app::{ - adapters::game_state::file::FileGameState, + adapters::game_state::file::FileGameDirectory, router::{into_game_router, into_outer_router}, service::axum_router::AxumGameService, }; @@ -13,7 +13,7 @@ pub async fn main() { tracing_subscriber::fmt().pretty().init(); let app = into_outer_router(AxumGameService { - router: into_game_router(FileGameState::default()), + router: into_game_router(FileGameDirectory::default()), }) .layer( TraceLayer::new_for_http().make_span_with(DefaultMakeSpan::default().include_headers(true)), diff --git a/app/src/extractors.rs b/app/src/extractors.rs index 03919de..2ae686b 100644 --- a/app/src/extractors.rs +++ b/app/src/extractors.rs @@ -12,7 +12,10 @@ use serde::Deserialize; use tracing::instrument; use uuid::Uuid; -use crate::service::InternalServerError; +use crate::{ + ports::game_state::{GameDirectory, GameState}, + service::InternalServerError, +}; #[derive(Debug, Copy, Clone)] pub struct SessionID(pub Uuid); @@ -54,3 +57,18 @@ impl FromRequestParts for GameCode { Ok(GameCode { code: game_id }) } } + +#[derive(Clone)] +pub struct Game(pub ::GameState); + +#[async_trait] +impl FromRequestParts for Game { + type Rejection = InternalServerError; + + #[instrument(skip_all, err, fields(uri = ?parts.uri))] + async fn from_request_parts(parts: &mut Parts, state: &G) -> Result { + let GameCode { code } = parts.extract::().await?; + + Ok(Game(state.get(code).await)) + } +} diff --git a/app/src/handlers/on_connect.rs b/app/src/handlers/on_connect.rs index 9ad801f..36b1bdf 100644 --- a/app/src/handlers/on_connect.rs +++ b/app/src/handlers/on_connect.rs @@ -1,23 +1,22 @@ #[cfg(target_arch = "wasm32")] mod wasm { - use axum::{extract::State, response::Response}; - use tracing::instrument; + use axum::response::Response; use crate::{ - extractors::{GameCode, SessionID}, - ports::game_state::GameState, + extractors::{Game, GameCode, SessionID}, + ports::game_state::{GameDirectory, GameState}, service::InternalServerError, }; // #[instrument(skip_all, err)] - pub async fn on_connect( - State(game_state): State, + pub async fn on_connect( + Game(game_state): Game, SessionID(_session_id): SessionID, GameCode { code }: GameCode, ) -> Result { - let pair = game_state.accept_web_socket(code)?; + let pair = game_state.accept_web_socket()?; - for event in game_state.events(code).await?.into_iter() { + for event in game_state.events().await?.into_iter() { pair.server.send(&event)?; } @@ -35,29 +34,29 @@ pub use wasm::on_connect; #[cfg(not(target_arch = "wasm32"))] mod native { - use axum::{ - extract::State, - response::{IntoResponse, Response}, - }; + use axum::response::{IntoResponse, Response}; use tracing::instrument; - use crate::{extractors::GameCode, ports::game_state::GameState, service::InternalServerError}; + use crate::{ + extractors::Game, + ports::game_state::{GameDirectory, GameState}, + service::InternalServerError, + }; #[instrument(skip_all, err)] - pub async fn on_connect( + pub async fn on_connect( ws: axum::extract::WebSocketUpgrade, - State(game_state): State, - GameCode { code }: GameCode, + Game(game_state): Game, ) -> Result { Ok(ws .on_upgrade(move |mut ws| async move { let result: anyhow::Result<()> = try { - for event in game_state.events(code).await?.into_iter() { + for event in game_state.events().await?.into_iter() { let message = serde_json::to_string(&event)?; ws.send(message.into()).await?; } - game_state.accept_web_socket(code, ws).await?; + game_state.accept_web_socket(ws).await?; }; if let Err(err) = result { diff --git a/app/src/handlers/register_command.rs b/app/src/handlers/register_command.rs index 0654093..c6f5565 100644 --- a/app/src/handlers/register_command.rs +++ b/app/src/handlers/register_command.rs @@ -1,12 +1,11 @@ use std::any::type_name; use crate::{ - extractors::{GameCode, SessionID}, - ports::game_state::GameState, + extractors::{Game, SessionID}, + ports::game_state::{GameDirectory, GameState}, service::InternalServerError, }; use axum::{ - extract::State, response::{IntoResponse, Response}, routing::post, Json, @@ -21,34 +20,33 @@ pub trait RegisterCommandExt { } #[tracing::instrument(skip_all, fields(session_id, input, command = type_name::()), err)] -pub async fn command_handler( +pub async fn command_handler( SessionID(session_id): SessionID, - State(game): State, - GameCode { code }: GameCode, + Game(game): Game, Json(input): Json, ) -> Result { - let mut events = game.events(code).await?; + let mut events = game.events().await?; let new_events = C::handle(session_id, &events, input)?; for event in new_events { - game.push_event(code, event.clone()).await?; + game.push_event(event.clone()).await?; events.push_back(event); } let (new_events, alarm) = run_processors(&events)?; for event in new_events { - game.push_event(code, event).await?; + game.push_event(event).await?; } if let Some(alarm) = alarm { - game.set_alarm(code, alarm.0).await?; + game.set_alarm(alarm.0).await?; } Ok(().into_response()) } -impl RegisterCommandExt for axum::Router { +impl RegisterCommandExt for axum::Router { fn register_command_handler(self) -> Self { self.route(&C::url(":code"), post(command_handler::)) } diff --git a/app/src/ports/game_state.rs b/app/src/ports/game_state.rs index 18119fd..2137612 100644 --- a/app/src/ports/game_state.rs +++ b/app/src/ports/game_state.rs @@ -4,22 +4,26 @@ use anyhow::Result; use im::Vector; use shared::models::{events::Event, game_id::GameID}; -pub trait GameState: Clone + Send + Sync + 'static { - fn events(&self, game_id: GameID) -> impl Future>> + Send; - fn push_event(&self, game_id: GameID, event: Event) -> impl Future> + Send; +pub trait GameState: Clone + Send + 'static { + fn events(&self) -> impl Future>> + Send; + fn push_event(&self, event: Event) -> impl Future> + Send; fn set_alarm( &self, - game_id: GameID, duration: Duration, ) -> Pin> + Send + '_>>; #[cfg(target_arch = "wasm32")] - fn accept_web_socket(&self, game_id: GameID) -> anyhow::Result; + fn accept_web_socket(&self) -> anyhow::Result; #[cfg(not(target_arch = "wasm32"))] fn accept_web_socket( &self, - game_id: GameID, ws: axum::extract::ws::WebSocket, ) -> impl Future> + Send; } + +pub trait GameDirectory: Clone + Send + Sync + 'static { + type GameState: GameState; + + fn get(&self, game_id: GameID) -> impl Future + Send; +} diff --git a/app/src/router.rs b/app/src/router.rs index ee2f2a2..93ff985 100644 --- a/app/src/router.rs +++ b/app/src/router.rs @@ -1,10 +1,6 @@ use std::{net::SocketAddr, str::FromStr}; -use axum::{ - extract::Request, - middleware::Next, - routing::{any, get, post}, -}; +use axum::routing::{any, get, post}; use leptos::{leptos_config, LeptosOptions}; use leptos_axum::{generate_route_list, LeptosRoutes}; use shared::models::commands; @@ -16,7 +12,7 @@ use crate::{ on_connect::on_connect, register_command::RegisterCommandExt, }, middleware::session_middleware, - ports::game_state::GameState, + ports::game_state::GameDirectory, service::GameService, }; @@ -47,7 +43,7 @@ pub fn into_outer_router(game_service: S) -> axum::Router { router.with_state(leptos_options) } -pub fn into_game_router(game: G) -> axum::Router { +pub fn into_game_router(game: G) -> axum::Router { axum::Router::new() .route( "/api/object/game/by_code/:code/connect", diff --git a/app/src/screens/player/pregame.rs b/app/src/screens/player/pregame.rs index ccd763d..4f646f8 100644 --- a/app/src/screens/player/pregame.rs +++ b/app/src/screens/player/pregame.rs @@ -171,66 +171,68 @@ pub fn pre_game() -> impl IntoView { Signal::derive(move || projections::already_played_card_this_round(&events(), player_id)); view! { -
- //
"Profile Image"
-
-

{player_name}

-
- "Funds:" - "💎 " {available_money} - "Debt:" - "💎 " {debt} + +
+ //
"Profile Image"
+
+

{player_name}

+
+ "Funds:" + "💎 " {available_money} + "Debt:" + "💎 " {debt} +
-
-
-
- -
-
- +
+
+ +
+
+ +
+ + +
- - - -
-
- {move || { - let cards = cards(); - let count = cards.len() as i32; - cards - .into_iter() - .enumerate() - .map(|(index, card)| { - let rotation = if count % 2 == 0 { - (index as i32 - count / 2) * 25 + 13 - } else { - (index as i32 - count / 2) * 25 - }; - view! { - - } - }) - .collect::>() - }} +
+ {move || { + let cards = cards(); + let count = cards.len() as i32; + cards + .into_iter() + .enumerate() + .map(|(index, card)| { + let rotation = if count % 2 == 0 { + (index as i32 - count / 2) * 25 + 13 + } else { + (index as i32 - count / 2) * 25 + }; + view! { + + } + }) + .collect::>() + }} +
-
+
- //

"Loan shark"

+

"Loan shark"

"\"I'm a shark, How much do you want to borrow?\""

"Interest Rate: 5.1%/pr"

@@ -358,13 +360,13 @@ fn card_preview( } #[component] -fn card_main(card: Card) -> impl IntoView { +fn card_main(card: Card, on_click: impl FnMut(MouseEvent) + 'static) -> impl IntoView { view! { -
+
+ } } @@ -378,41 +380,46 @@ fn card_modal(close: impl Fn() + Copy + 'static) -> impl IntoView { let scroll = use_scroll(scroll_ref); - let (target_modal, toggle_target_modal) = { - let (read, write) = create_signal(false); - - (read, move || write(!read())) - }; - let card = Signal::derive(move || { let index = (((scroll.x)() as f32 - 125.0) / 250.) as usize; cards().get(index).copied() }); + let (selected_card, set_selected_card) = create_signal::>(None); + view! { -
- - - {move || match card() { - Some(card) if target_modal() => { - view! { }.into_view() - } - _ => ().into_view(), + + {move || { + selected_card() + .map(|card| { + view! { } + }) }} } } diff --git a/assets/pkg/style.css b/assets/pkg/style.css index 067f088..ae37c27 100644 --- a/assets/pkg/style.css +++ b/assets/pkg/style.css @@ -1,6 +1,7 @@ body { display: block; - overflow: hidden; + overflow-x: hidden; + overflow-y: scroll; margin: 0px; background-color: #03193f; font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande", @@ -223,9 +224,8 @@ canvas { } .pre-game-container { - position: absolute; width: 100%; - min-height: 100dvh; + min-height: 100vh; display: flex; flex-direction: column; align-items: center; @@ -409,6 +409,7 @@ h2 { scroll-snap-align: center; font-size: 24px; + color: #eee; } .card-main > img { @@ -520,10 +521,8 @@ h2 { } .loan-shark { - aspect-ratio: 1 / 1; - - flex-grow: 1; - flex-shrink: 1; + width: 250px; + height: 250px; /* -webkit-backdrop-filter: blur(8px); diff --git a/game/src/plugins/scenes/race.rs b/game/src/plugins/scenes/race.rs index 4b2d8d4..79c8215 100644 --- a/game/src/plugins/scenes/race.rs +++ b/game/src/plugins/scenes/race.rs @@ -1,10 +1,10 @@ use bevy::prelude::*; -use shared::models::projections::{self, Jump}; +use shared::models::projections::{self, Jump, RaceResults}; use crate::plugins::{ delayed_command::DelayedCommandExt, event_stream::GameEvents, - monster::{DespawnAllMonsters, MonsterBehaviour, MonsterID, SpawnMonster}, + monster::{DespawnAllMonsters, MonsterBehaviour, MonsterID, MonsterInfo, SpawnMonster}, music::PlayCountdown, }; @@ -152,9 +152,9 @@ fn init_race( let seed = projections::race_seed(&game_events); let monsters = projections::monsters(&game_events, seed); - let (_, jump) = projections::race(&monsters, seed); + let (results, jump) = projections::race(&monsters, seed); - commands.insert_resource(Race(jump)); + commands.insert_resource(Race((results, jump))); race_points .into_iter() @@ -181,7 +181,7 @@ pub struct RaceTimer { } #[derive(Debug, Resource, Deref, DerefMut)] -struct Race(Vec); +struct Race((RaceResults, Vec)); impl Default for RaceTimer { fn default() -> Self { @@ -195,22 +195,30 @@ impl Default for RaceTimer { fn run_race( race: Res, time: Res