From 6aed6574046bcc3ab0dfac15229afa607a90196e Mon Sep 17 00:00:00 2001 From: ioni <104294646+IongIer@users.noreply.github.com> Date: Tue, 11 Jun 2024 10:34:29 +0300 Subject: [PATCH] Uses slices and batchs signal updates when using the gamestate signal --- apis/src/common/mod.rs | 4 + apis/src/common/move_info.rs | 41 ++++++ apis/src/common/rating_change_info.rs | 20 +++ apis/src/components/atoms/gc_button.rs | 36 ++--- apis/src/components/atoms/hex.rs | 9 +- apis/src/components/atoms/history_button.rs | 19 ++- apis/src/components/atoms/piece.rs | 3 +- apis/src/components/atoms/status_indicator.rs | 2 +- apis/src/components/atoms/title.rs | 18 +-- apis/src/components/layouts/base_layout.rs | 58 +++++---- apis/src/components/molecules/board_pieces.rs | 15 ++- .../components/molecules/control_buttons.rs | 5 +- apis/src/components/molecules/game_info.rs | 29 ++++- apis/src/components/molecules/game_row.rs | 6 +- apis/src/components/molecules/live_timer.rs | 24 ++-- .../components/molecules/rating_and_change.rs | 43 +++--- .../components/molecules/user_with_rating.rs | 26 +--- apis/src/components/organisms/board.rs | 24 ++-- .../src/components/organisms/display_timer.rs | 26 ++-- apis/src/components/organisms/history.rs | 39 +++--- apis/src/components/organisms/reserve.rs | 49 ++++--- apis/src/components/organisms/side_board.rs | 11 +- apis/src/pages/play.rs | 9 +- apis/src/providers/game_state.rs | 123 ++++++++++-------- apis/src/providers/games.rs | 6 +- apis/src/providers/navigation_controller.rs | 20 +-- apis/src/providers/websocket/context.rs | 2 +- .../providers/websocket/response_handler.rs | 38 +++--- apis/src/responses/game.rs | 9 ++ 29 files changed, 416 insertions(+), 298 deletions(-) create mode 100644 apis/src/common/move_info.rs create mode 100644 apis/src/common/rating_change_info.rs diff --git a/apis/src/common/mod.rs b/apis/src/common/mod.rs index da34700d..ccf2aa89 100644 --- a/apis/src/common/mod.rs +++ b/apis/src/common/mod.rs @@ -5,7 +5,9 @@ mod game_action; mod game_reaction; mod hex; mod hex_stack; +mod move_info; mod piece_type; +mod rating_change_info; mod server_message; mod server_result; mod svg_pos; @@ -14,7 +16,9 @@ pub use client_message::ClientRequest; pub use game_action::GameAction; pub use game_reaction::GameReaction; pub use hex_stack::HexStack; +pub use move_info::MoveInfo; pub use piece_type::PieceType; +pub use rating_change_info::RatingChangeInfo; pub use svg_pos::SvgPos; pub use challenge_action::ChallengeAction; diff --git a/apis/src/common/move_info.rs b/apis/src/common/move_info.rs new file mode 100644 index 00000000..e0f44a2c --- /dev/null +++ b/apis/src/common/move_info.rs @@ -0,0 +1,41 @@ +use hive_lib::{Piece, Position}; + +#[derive(Debug, Clone, PartialEq)] +pub struct MoveInfo { + // the piece (either from reserve or board) that has been clicked last + pub active: Option, + // the position of the board piece that has been clicked last + pub current_position: Option, + // possible destinations of selected piece + pub target_positions: Vec, + // the position of the target that got clicked last + pub target_position: Option, + // the position of the reserve piece that got clicked last + pub reserve_position: Option, +} + +impl Default for MoveInfo { + fn default() -> Self { + Self::new() + } +} + +impl MoveInfo { + pub fn new() -> Self { + Self { + active: None, + current_position: None, + target_positions: vec![], + target_position: None, + reserve_position: None, + } + } + + pub fn reset(&mut self) { + self.target_positions.clear(); + self.active = None; + self.target_position = None; + self.current_position = None; + self.reserve_position = None; + } +} diff --git a/apis/src/common/rating_change_info.rs b/apis/src/common/rating_change_info.rs new file mode 100644 index 00000000..c14e6739 --- /dev/null +++ b/apis/src/common/rating_change_info.rs @@ -0,0 +1,20 @@ +use crate::responses::GameResponse; + +#[derive(Clone, PartialEq)] +pub struct RatingChangeInfo { + pub white_rating_change: i64, + pub white_rating: u64, + pub black_rating_change: i64, + pub black_rating: u64, +} + +impl RatingChangeInfo { + pub fn from_game_response(gr: &GameResponse) -> Self { + RatingChangeInfo { + white_rating_change: gr.white_rating_change.unwrap_or_default() as i64, + white_rating: gr.white_rating.unwrap_or_default() as u64, + black_rating_change: gr.black_rating_change.unwrap_or_default() as i64, + black_rating: gr.black_rating.unwrap_or_default() as u64, + } + } +} diff --git a/apis/src/components/atoms/gc_button.rs b/apis/src/components/atoms/gc_button.rs index 1f977b75..041da171 100644 --- a/apis/src/components/atoms/gc_button.rs +++ b/apis/src/components/atoms/gc_button.rs @@ -78,27 +78,31 @@ pub fn ConfirmButton( (interval().pause)(); } }); + + let pending_slice = + create_read_slice(game_state().signal, |gs| gs.game_control_pending.clone()); + let cancel = move |_| is_clicked.update(|v| *v = false); - let pending = - move |game_control: GameControl| match (game_state().signal)().game_control_pending { - Some(GameControl::DrawOffer(gc_color)) => { - if color == gc_color && matches!(game_control, GameControl::DrawOffer(_)) { - return true; - } - false + let pending = move |game_control: GameControl| match pending_slice() { + Some(GameControl::DrawOffer(gc_color)) => { + if color == gc_color && matches!(game_control, GameControl::DrawOffer(_)) { + return true; } - Some(GameControl::TakebackRequest(gc_color)) => { - if color == gc_color && matches!(game_control, GameControl::TakebackRequest(_)) { - return true; - } - false + false + } + Some(GameControl::TakebackRequest(gc_color)) => { + if color == gc_color && matches!(game_control, GameControl::TakebackRequest(_)) { + return true; } - _ => false, - }; + false + } + _ => false, + }; + + let turn = create_read_slice(game_state().signal, |gs| gs.state.turn as i32); let disabled = move || { - let turn = (game_state().signal)().state.turn as i32; - if game_control().allowed_on_turn(turn) { + if game_control().allowed_on_turn(turn()) { !matches!(game_control(), GameControl::Resign(_)) && (pending(GameControl::DrawOffer(color)) || pending(GameControl::TakebackRequest(color))) diff --git a/apis/src/components/atoms/hex.rs b/apis/src/components/atoms/hex.rs index c09303e4..10f0f712 100644 --- a/apis/src/components/atoms/hex.rs +++ b/apis/src/components/atoms/hex.rs @@ -26,7 +26,14 @@ pub fn Hex(hex: Hex) -> impl IntoView { match hex.kind { HexType::Active(_) => { - if game_state.signal.get_untracked().target_position.is_none() || hex.level == 0 { + if game_state + .signal + .get_untracked() + .move_info + .target_position + .is_none() + || hex.level == 0 + { view! { } } else { view! { } diff --git a/apis/src/components/atoms/history_button.rs b/apis/src/components/atoms/history_button.rs index d648ff58..00a74162 100644 --- a/apis/src/components/atoms/history_button.rs +++ b/apis/src/components/atoms/history_button.rs @@ -17,6 +17,9 @@ pub fn HistoryButton( #[prop(optional)] post_action: Option>, #[prop(optional)] node_ref: Option>, ) -> impl IntoView { + let game_state_signal = expect_context::(); + let is_last_turn = game_state_signal.is_last_turn_as_signal(); + let is_first_turn = game_state_signal.is_first_turn_as_signal(); let cloned_action = action.clone(); let nav_buttons_style = "flex place-items-center justify-center hover:bg-pillbug-teal transform transition-transform duration-300 active:scale-95 m-1 h-7 rounded-md border-cyan-500 dark:border-button-twilight border-2 drop-shadow-lg disabled:opacity-25 disabled:cursor-not-allowed disabled:hover:bg-transparent"; let icon = match action { @@ -25,17 +28,13 @@ pub fn HistoryButton( HistoryNavigation::Next => icondata::AiStepForwardFilled, HistoryNavigation::Previous => icondata::AiStepBackwardFilled, }; - let is_disabled = move || { - let game_state_signal = expect_context::(); - let game_state = game_state_signal.signal.get(); - match cloned_action { - HistoryNavigation::Last | HistoryNavigation::MobileLast | HistoryNavigation::Next => { - game_state.is_last_turn() - } - HistoryNavigation::Previous | HistoryNavigation::First => { - game_state.history_turn.is_none() || game_state.history_turn == Some(0) - } + + let is_disabled = move || match cloned_action { + HistoryNavigation::Last | HistoryNavigation::MobileLast | HistoryNavigation::Next => { + is_last_turn() } + + HistoryNavigation::Previous | HistoryNavigation::First => is_first_turn(), }; let debounced_action = debounce(std::time::Duration::from_millis(10), move |_| { send_action(&action); diff --git a/apis/src/components/atoms/piece.rs b/apis/src/components/atoms/piece.rs index 8fcb7a89..b7127d89 100644 --- a/apis/src/components/atoms/piece.rs +++ b/apis/src/components/atoms/piece.rs @@ -134,8 +134,9 @@ pub fn Piece( .top_piece(position) .unwrap_or(piece); + let active_piece = create_read_slice(game_state_signal.signal, |gs| gs.move_info.active); let show_ds = move || { - if let Some(active) = game_state_signal.signal.get().active { + if let Some(active) = active_piece() { if active == piece { return "#no_ds"; } diff --git a/apis/src/components/atoms/status_indicator.rs b/apis/src/components/atoms/status_indicator.rs index 318f66c4..d185ad82 100644 --- a/apis/src/components/atoms/status_indicator.rs +++ b/apis/src/components/atoms/status_indicator.rs @@ -30,7 +30,7 @@ pub fn StatusIndicator(username: String) -> impl IntoView { let icon_color = move || { if user_is_player() { - if user_has_ws() { + if batch(user_has_ws) { "fill-grasshopper-green" } else { "fill-ladybug-red" diff --git a/apis/src/components/atoms/title.rs b/apis/src/components/atoms/title.rs index 0dba4363..269b2486 100644 --- a/apis/src/components/atoms/title.rs +++ b/apis/src/components/atoms/title.rs @@ -7,14 +7,16 @@ pub fn Title() -> impl IntoView { let games = expect_context::(); let focused = expect_context::(); let title_text = move || { - let len = games.own.get().next_untimed.len() - + games.own.get().next_realtime.len() - + games.own.get().next_correspondence.len(); - if !focused.signal.get().focused && len > 0 { - format!("({}) HiveGame.com", len) - } else { - String::from("HiveGame.com") - } + batch(move || { + let len = games.own.get().next_untimed.len() + + games.own.get().next_realtime.len() + + games.own.get().next_correspondence.len(); + if !focused.signal.get().focused && len > 0 { + format!("({}) HiveGame.com", len) + } else { + String::from("HiveGame.com") + } + }) }; view! { } diff --git a/apis/src/components/layouts/base_layout.rs b/apis/src/components/layouts/base_layout.rs index fbd6bd12..825e144c 100644 --- a/apis/src/components/layouts/base_layout.rs +++ b/apis/src/components/layouts/base_layout.rs @@ -142,8 +142,6 @@ pub fn BaseLayout(children: ChildrenFn) -> impl IntoView { }; }); - let api = ApiRequests::new(); - let focused = use_window_focus(); let _ = watch( focused, @@ -161,35 +159,43 @@ pub fn BaseLayout(children: ChildrenFn) -> impl IntoView { let counter = RwSignal::new(0_u64); let retry_at = RwSignal::new(2_u64); + let Pausable { .. } = use_interval_fn( move || { - api.ping(); - counter.update(|c| *c += 1); - match ws_ready() { - ConnectionReadyState::Closed => { - if retry_at.get() == counter.get() { - //log!("Reconnecting due to ReadyState"); + batch({ + let ws = ws.clone(); + let api = ApiRequests::new(); + move || { + api.ping(); + counter.update(|c| *c += 1); + match ws_ready() { + ConnectionReadyState::Closed => { + if retry_at.get() == counter.get() { + //log!("Reconnecting due to ReadyState"); + ws.open(); + counter.update(|c| *c = 0); + retry_at.update(|r| *r *= 2); + } + } + ConnectionReadyState::Open => { + counter.update(|c| *c = 0); + retry_at.update(|r| *r = 2); + } + _ => {} + } + if Utc::now() + .signed_duration_since(ping.signal.get_untracked().last_update) + .num_seconds() + >= 5 + && retry_at.get() == counter.get() + { + //log!("Reconnecting due to ping duration"); ws.open(); counter.update(|c| *c = 0); - counter.update(|r| *r *= 2); - } + retry_at.update(|r| *r *= 2); + }; } - ConnectionReadyState::Open => { - counter.update(|c| *c = 0); - } - _ => {} - } - if Utc::now() - .signed_duration_since(ping.signal.get_untracked().last_update) - .num_seconds() - >= 5 - && retry_at.get() == counter.get() - { - //log!("Reconnecting due to ping duration"); - ws.open(); - counter.update(|c| *c = 0); - counter.update(|r| *r *= 2); - }; + }) }, 1000, ); diff --git a/apis/src/components/molecules/board_pieces.rs b/apis/src/components/molecules/board_pieces.rs index 3619dc09..8d9bf488 100644 --- a/apis/src/components/molecules/board_pieces.rs +++ b/apis/src/components/molecules/board_pieces.rs @@ -13,17 +13,24 @@ pub fn BoardPieces() -> impl IntoView { let board = move || { let mut board = Vec::new(); let game_state = (game_state_signal.signal)(); - let targets = game_state.target_positions; + let targets = game_state.move_info.target_positions; let last_move = game_state.state.board.last_move; - let active_piece = (game_state.active, game_state.target_position); - let from_to_position = (game_state.current_position, game_state.target_position); + let active_piece = ( + game_state.move_info.active, + game_state.move_info.target_position, + ); + let from_to_position = ( + game_state.move_info.current_position, + game_state.move_info.target_position, + ); + // TODO: Find a better solution instead of the nested loop here for r in 0..32 { for q in 0..32 { let position = Position::new(q, r); // start this empty and only add let bug_stack = game_state.state.board.board.get(position).clone(); let mut hs = HexStack::new(&bug_stack, position); - if game_state.active.is_none() { + if game_state.move_info.active.is_none() { if let (_, Some(to)) = last_move { if to == position { hs.add_last_move(Direction::To); diff --git a/apis/src/components/molecules/control_buttons.rs b/apis/src/components/molecules/control_buttons.rs index d90fbea3..a4c4a786 100644 --- a/apis/src/components/molecules/control_buttons.rs +++ b/apis/src/components/molecules/control_buttons.rs @@ -29,14 +29,15 @@ pub fn ControlButtons() -> impl IntoView { unreachable!() } }; + let pending = create_read_slice(game_state.signal, |gs| gs.game_control_pending.clone()); - let pending_draw = move || match (game_state.signal)().game_control_pending { + let pending_draw = move || match pending() { Some(GameControl::DrawOffer(gc_color)) => gc_color.opposite_color() == color(), _ => false, }; - let pending_takeback = move || match (game_state.signal)().game_control_pending { + let pending_takeback = move || match pending() { Some(GameControl::TakebackRequest(gc_color)) => gc_color.opposite_color() == color(), _ => false, diff --git a/apis/src/components/molecules/game_info.rs b/apis/src/components/molecules/game_info.rs index c4fb4a7c..c71115f8 100644 --- a/apis/src/components/molecules/game_info.rs +++ b/apis/src/components/molecules/game_info.rs @@ -1,20 +1,37 @@ use leptos::*; +use shared_types::TimeMode; use crate::{components::molecules::time_row::TimeRow, providers::game_state::GameStateSignal}; +#[derive(Clone, PartialEq)] +struct TimeInfo { + time_mode: TimeMode, + time_base: Option, + time_increment: Option, + rated: bool, +} + #[component] pub fn GameInfo(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoView { let game_state = expect_context::(); + let game_info = create_read_slice(game_state.signal, |gs| { + gs.game_response.as_ref().map(|gr| TimeInfo { + time_mode: gr.time_mode.clone(), + time_base: gr.time_base, + time_increment: gr.time_increment, + rated: gr.rated, + }) + }); move || { - if let Some(gr) = game_state.signal.get().game_response { - let rated = format!("• {}", if gr.rated { "Rated" } else { "Casual" }); + if let Some(gi) = game_info() { + let rated = format!("• {}", if gi.rated { "Rated" } else { "Casual" }); view! {
-
+
{rated} diff --git a/apis/src/components/molecules/game_row.rs b/apis/src/components/molecules/game_row.rs index 6c813662..4c94e49f 100644 --- a/apis/src/components/molecules/game_row.rs +++ b/apis/src/components/molecules/game_row.rs @@ -1,4 +1,5 @@ use crate::{ + common::RatingChangeInfo, components::{ atoms::{ download_pgn::DownloadPgn, profile_link::ProfileLink, status_indicator::StatusIndicator, @@ -85,6 +86,7 @@ pub fn GameRow(game: StoredValue) -> impl IntoView { Conclusion::Repetition => String::from(" 3 move repetition"), Conclusion::Unknown => String::new(), }; + let ratings = store_value(RatingChangeInfo::from_game_response(&game())); view! {
@@ -115,7 +117,7 @@ pub fn GameRow(game: StoredValue) -> impl IntoView {

- +
@@ -130,7 +132,7 @@ pub fn GameRow(game: StoredValue) -> impl IntoView {

- + diff --git a/apis/src/components/molecules/live_timer.rs b/apis/src/components/molecules/live_timer.rs index 0c9e1259..63fe1e3f 100644 --- a/apis/src/components/molecules/live_timer.rs +++ b/apis/src/components/molecules/live_timer.rs @@ -80,17 +80,19 @@ pub fn LiveTimer(side: Color) -> impl IntoView { is_active, } = use_interval_fn_with_options( move || { - ticks.update(|t| *t += 1); - time_left.update(|t| { - if ticks.get_untracked() > 10 { - ticks.update(|t| *t = 0); - *t = match side { - Color::Black => black_time(), - Color::White => white_time(), - }; - } else { - *t = t.checked_sub(tick_rate).unwrap_or(Duration::from_millis(0)); - } + batch(move || { + ticks.update(|t| *t += 1); + time_left.update(|t| { + if ticks.get_untracked() > 10 { + ticks.update(|t| *t = 0); + *t = match side { + Color::Black => black_time(), + Color::White => white_time(), + }; + } else { + *t = t.checked_sub(tick_rate).unwrap_or(Duration::from_millis(0)); + } + }) }) }, 100, diff --git a/apis/src/components/molecules/rating_and_change.rs b/apis/src/components/molecules/rating_and_change.rs index ab0587fa..662975c4 100644 --- a/apis/src/components/molecules/rating_and_change.rs +++ b/apis/src/components/molecules/rating_and_change.rs @@ -1,5 +1,5 @@ +use crate::common::RatingChangeInfo; use crate::providers::game_state::GameStateSignal; -use crate::responses::GameResponse; use hive_lib::Color; use leptos::*; use std::cmp::Ordering; @@ -7,30 +7,13 @@ use std::cmp::Ordering; #[component] pub fn RatingAndChange( #[prop(optional)] extend_tw_classes: &'static str, - game: GameResponse, + ratings: RatingChangeInfo, side: Color, ) -> impl IntoView { let (rating_change, rating) = match side { - Color::White => { - if game.rated { - ( - game.white_rating_change.unwrap_or_default() as i64, - game.white_rating.unwrap_or_default() as u64, - ) - } else { - (0_i64, game.white_rating.unwrap_or_default() as u64) - } - } - Color::Black => { - if game.rated { - ( - game.black_rating_change.unwrap_or_default() as i64, - game.black_rating.unwrap_or_default() as u64, - ) - } else { - (0, game.black_rating.unwrap_or_default() as u64) - } - } + Color::White => (ratings.white_rating_change, ratings.white_rating), + + Color::Black => (ratings.black_rating_change, ratings.black_rating), }; let (sign, style) = match rating_change.cmp(&0_i64) { Ordering::Equal => ("+", "text-pillbug-teal"), @@ -50,13 +33,21 @@ pub fn RatingAndChangeDynamic( side: Color, ) -> impl IntoView { let game_state = expect_context::(); - let game = move || game_state.signal.get().game_response; + let ratings = create_read_slice(game_state.signal, |gs| { + gs.game_response + .as_ref() + .map(RatingChangeInfo::from_game_response) + }); view! { {move || { - game() - .map(|game| { + ratings() + .map(|ratings| { view! { - + } }) }} diff --git a/apis/src/components/molecules/user_with_rating.rs b/apis/src/components/molecules/user_with_rating.rs index 18f06cd5..16cffd5e 100644 --- a/apis/src/components/molecules/user_with_rating.rs +++ b/apis/src/components/molecules/user_with_rating.rs @@ -17,31 +17,19 @@ pub fn UserWithRating( #[prop(optional)] is_tall: Signal, ) -> impl IntoView { let game_state = expect_context::(); + let game_response = create_read_slice(game_state.signal, |gs| gs.game_response.clone()); let player = move || match side { - Color::White => game_state - .signal - .get() - .game_response - .map(|g| g.white_player), - Color::Black => game_state - .signal - .get() - .game_response - .map(|g| g.black_player), + Color::White => game_response().map(|g| g.white_player), + Color::Black => game_response().map(|g| g.black_player), }; let speed = move || { - game_state - .signal - .get_untracked() - .game_response - .map(|resp| match resp.speed { - GameSpeed::Untimed => GameSpeed::Correspondence, - _ => resp.speed, - }) + game_response().map(|resp| match resp.speed { + GameSpeed::Untimed => GameSpeed::Correspondence, + _ => resp.speed, + }) }; let username = move || player().map_or(String::new(), |p| p.username); let patreon = move || player().map_or(false, |p| p.patreon); - // TODO: Display proper rating for game use let rating = move || match (player(), speed()) { (Some(player), Some(speed)) => { view! { } diff --git a/apis/src/components/organisms/board.rs b/apis/src/components/organisms/board.rs index 46b7ad5f..9a733c1b 100644 --- a/apis/src/components/organisms/board.rs +++ b/apis/src/components/organisms/board.rs @@ -58,7 +58,7 @@ pub fn Board( #[prop(optional)] extend_tw_classes: &'static str, #[prop(optional)] overwrite_tw_classes: &'static str, ) -> impl IntoView { - let mut game_state_signal = expect_context::(); + let mut game_state = expect_context::(); let target_stack = expect_context::().0; let is_panning = RwSignal::new(false); let has_zoomed = RwSignal::new(false); @@ -69,12 +69,15 @@ pub fn Board( let div_ref = NodeRef::::new(); let zoom_in_limit = 150.0; let zoom_out_limit = 2500.0; - let history_style = move || match (game_state_signal.signal)().view { + let last_turn = game_state.is_last_turn_as_signal(); + let board_view = create_read_slice(game_state.signal, |gs| gs.view.clone()); + let game_status = create_read_slice(game_state.signal, |gs| gs.state.game_status.clone()); + let history_style = move || match board_view() { View::Game => "", - View::History => match (game_state_signal.signal)().state.game_status { + View::History => match game_status() { GameStatus::Finished(_) => "", _ => { - if (game_state_signal.signal)().is_last_turn() { + if last_turn() { "" } else { "sepia-[.75]" @@ -102,7 +105,7 @@ pub fn Board( }; let current_center = move || { - game_state_signal + game_state .signal .get_untracked() .state @@ -111,7 +114,7 @@ pub fn Board( }; let update_once = create_effect(move |_| { - if game_state_signal.loaded.get() { + if game_state.loaded.get() { let div = div_ref.get_untracked().expect("it exists"); let rect = div.get_bounding_client_rect(); let svg_pos = SvgPos::center_for_level(current_center(), 0); @@ -126,7 +129,7 @@ pub fn Board( }; }); create_effect(move |_| { - if game_state_signal.loaded.get() { + if game_state.loaded.get() { update_once.dispose(); } }); @@ -337,16 +340,13 @@ pub fn Board( class=move || format!("touch-none duration-300 {}", history_style()) ref=viewbox_ref xmlns="http://www.w3.org/2000/svg" - on:click=move |_| { game_state_signal.reset() } + on:click=move |_| { game_state.reset() } > } diff --git a/apis/src/components/organisms/display_timer.rs b/apis/src/components/organisms/display_timer.rs index 4b55472f..5cb3ebf3 100644 --- a/apis/src/components/organisms/display_timer.rs +++ b/apis/src/components/organisms/display_timer.rs @@ -24,12 +24,9 @@ pub fn DisplayTimer(placement: Placement, vertical: bool) -> impl IntoView { Some(Ok(Some(user))) => Some(user), _ => None, }; - let player_is_black = create_memo(move |_| { - user().map_or(false, |user| { - let game_state = game_state.signal.get(); - Some(user.id) == game_state.black_id - }) - }); + let black_id = create_read_slice(game_state.signal, |gs| gs.black_id); + let player_is_black = + create_memo(move |_| user().map_or(false, |user| Some(user.id) == black_id())); let side = move || match (player_is_black(), placement) { (true, Placement::Top) => Color::White, (true, Placement::Bottom) => Color::Black, @@ -55,13 +52,16 @@ pub fn DisplayTimer(placement: Placement, vertical: bool) -> impl IntoView { true => ("flex grow justify-end items-center", "w-14 h-14 grow-0 duration-300",""), }; let timer = expect_context::(); - let active_side = create_memo(move |_| match timer.signal.get().finished { - true => "bg-stone-200 dark:bg-reserve-twilight", - false => { - if (side() == Color::White) == (timer.signal.get().turn % 2 == 0) { - "bg-grasshopper-green" - } else { - "bg-stone-200 dark:bg-reserve-twilight" + let active_side = create_memo(move |_| { + let timer = timer.signal.get(); + match timer.finished { + true => "bg-stone-200 dark:bg-reserve-twilight", + false => { + if (side() == Color::White) == (timer.turn % 2 == 0) { + "bg-grasshopper-green" + } else { + "bg-stone-200 dark:bg-reserve-twilight" + } } } }); diff --git a/apis/src/components/organisms/history.rs b/apis/src/components/organisms/history.rs index 56e54e82..fd578ac5 100644 --- a/apis/src/components/organisms/history.rs +++ b/apis/src/components/organisms/history.rs @@ -16,7 +16,7 @@ pub fn HistoryMove( repetition: bool, parent_div: NodeRef, ) -> impl IntoView { - let mut game_state_signal = expect_context::(); + let mut game_state = expect_context::(); let div_ref = create_node_ref::(); div_ref.on_load(move |_| { let _ = div_ref @@ -28,11 +28,12 @@ pub fn HistoryMove( }); }); let onclick = move |_| { - game_state_signal.show_history_turn(turn); + game_state.show_history_turn(turn); }; + let history_turn = create_read_slice(game_state.signal, |gs| gs.history_turn); let get_class = move || { let mut class = "col-span-2 ml-3 h-auto max-h-6 leading-6 transition-transform duration-300 transform hover:bg-pillbug-teal active:scale-95"; - if let Some(history_turn) = (game_state_signal.signal)().history_turn { + if let Some(history_turn) = history_turn() { if turn == history_turn { class = "col-span-2 ml-3 h-auto max-h-6 leading-6 transition-transform duration-300 transform hover:bg-pillbug-teal bg-orange-twilight active:scale-95" } @@ -53,10 +54,13 @@ pub fn HistoryMove( #[component] pub fn History(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoView { - let game_state_signal = expect_context::(); + let game_state = expect_context::(); + let state = create_read_slice(game_state.signal, |gs| gs.state.clone()); + let repetitions = create_read_slice(game_state.signal, |gs| { + gs.game_response.as_ref().map(|gr| gr.repetitions.clone()) + }); let history_moves = move || { - (game_state_signal.signal)() - .state + state() .history .moves .into_iter() @@ -66,20 +70,13 @@ pub fn History(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoVi }; let parent = create_node_ref::(); - let is_finished = move || { - matches!( - (game_state_signal.signal)().state.game_status, - GameStatus::Finished(_) - ) - }; - - let game_result = move || match (game_state_signal.signal)().state.game_status { + let game_result = move || match state().game_status { GameStatus::Finished(result) => result.to_string(), _ => "".to_string(), }; - let conclusion = move || { - if let Some(game) = (game_state_signal.signal)().game_response { + let conclusion = create_read_slice(game_state.signal, |gs| { + if let Some(game) = &gs.game_response { match game.conclusion { Conclusion::Board => String::from("Finished on board"), Conclusion::Draw => String::from("Draw agreed"), @@ -91,7 +88,7 @@ pub fn History(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoVi } else { String::from("No data") } - }; + }); let window = use_window(); let active = Signal::derive(move || { @@ -120,7 +117,7 @@ pub fn History(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoVi let if_last_go_to_end = Callback::new(move |()| { focus(()); - let gamestate = (game_state_signal.signal)(); + let gamestate = game_state.signal.get_untracked(); { if let Some(turn) = gamestate.history_turn { if turn == gamestate.state.turn - 1 { @@ -131,8 +128,8 @@ pub fn History(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoVi }); let is_repetition = move |turn: usize| { - if let Some(game) = (game_state_signal.signal)().game_response { - game.repetitions.contains(&turn) + if let Some(repetitions) = repetitions() { + repetitions.contains(&turn) } else { false } @@ -194,7 +191,7 @@ pub fn History(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoVi /> - +
{game_result}
{conclusion}
diff --git a/apis/src/components/organisms/reserve.rs b/apis/src/components/organisms/reserve.rs index 70a69d43..ce3d9af7 100644 --- a/apis/src/components/organisms/reserve.rs +++ b/apis/src/components/organisms/reserve.rs @@ -45,32 +45,43 @@ pub fn Reserve( alignment: Alignment, #[prop(optional)] extend_tw_classes: &'static str, ) -> impl IntoView { - let game_state_signal = expect_context::(); + let game_state = expect_context::(); let (viewbox_str, viewbox_styles) = match alignment { Alignment::SingleRow => ("-40 -55 450 100", "inline max-h-[inherit] h-full w-fit"), Alignment::DoubleRow => ("-32 -55 250 180", "p-1"), }; + // For some reason getting a slice of the whole state is a problem and leads to wasm oob errors, because of that the other slices are less useful + let board_view = create_read_slice(game_state.signal, |gs| gs.view.clone()); + let move_info = create_read_slice(game_state.signal, |gs| gs.move_info.clone()); + let history_turn = create_read_slice(game_state.signal, |gs| gs.history_turn); + let last_turn = game_state.is_last_turn_as_signal(); let stacked_pieces = move || { - let game_state = (game_state_signal.signal)(); - let reserve = match game_state.view { + let board_view = board_view(); + let move_info = move_info(); + let history_turn = history_turn(); + let game_state = game_state.signal.get(); + let reserve = match board_view { View::Game => game_state .state .board .reserve(color(), game_state.state.game_type), View::History => { let mut history = History::new(); - if let Some(turn) = game_state.history_turn { + if let Some(turn) = history_turn { history.moves = game_state.state.history.moves[0..=turn].into(); } - let state = State::new_from_history(&history).expect("Got state from history"); - state.board.reserve(color(), game_state.state.game_type) + let history_state = + State::new_from_history(&history).expect("Got state from history"); + history_state + .board + .reserve(color(), game_state.state.game_type) } }; let mut clicked_position = None; if color() == game_state.state.turn_color { - clicked_position = game_state.reserve_position; + clicked_position = move_info.reserve_position; } let mut seen = -1; let mut res = Vec::new(); @@ -87,20 +98,16 @@ pub fn Reserve( let stack_height = piece_strings.len() - 1; for (i, piece_str) in piece_strings.iter().rev().enumerate() { let piece = Piece::from_str(piece_str).expect("Parsed piece"); - let piece_type = if piece_active( - &game_state.state, - &game_state.view, - &piece, - game_state.is_last_turn(), - ) { - if i == stack_height { - PieceType::Reserve + let piece_type = + if piece_active(&game_state.state, &board_view, &piece, last_turn()) { + if i == stack_height { + PieceType::Reserve + } else { + PieceType::Nope + } } else { - PieceType::Nope - } - } else { - PieceType::Inactive - }; + PieceType::Inactive + }; hs.hexes.push(Hex { kind: HexType::Tile(piece, piece_type), position, @@ -109,7 +116,7 @@ pub fn Reserve( } if let Some(click) = clicked_position { if click == position { - if game_state.target_position.is_some() { + if move_info.target_position.is_some() { hs.add_active(true); } else { hs.add_active(false); diff --git a/apis/src/components/organisms/side_board.rs b/apis/src/components/organisms/side_board.rs index 85704e8b..54a43fe4 100644 --- a/apis/src/components/organisms/side_board.rs +++ b/apis/src/components/organisms/side_board.rs @@ -28,7 +28,7 @@ pub fn SideboardTabs( #[prop(optional)] analysis: bool, #[prop(optional)] extend_tw_classes: &'static str, ) -> impl IntoView { - let mut game_state_signal = expect_context::(); + let mut game_state = expect_context::(); let chat = expect_context::(); let tab_view = RwSignal::new(SideboardTabView::Reserve); let auth_context = expect_context::(); @@ -36,6 +36,7 @@ pub fn SideboardTabs( Some(Ok(Some(user))) => Some(user), _ => None, }; + let white_and_black = create_read_slice(game_state.signal, |gs| (gs.white_id, gs.black_id)); // On navigation switch to reserve create_effect(move |_| { let location = use_location(); @@ -44,8 +45,8 @@ pub fn SideboardTabs( }); let show_buttons = move || { user().map_or(false, |user| { - let game_state = game_state_signal.signal.get(); - Some(user.id) == game_state.black_id || Some(user.id) == game_state.white_id + let (white_id, black_id) = white_and_black(); + Some(user.id) == black_id || Some(user.id) == white_id }) && !analysis }; @@ -97,7 +98,7 @@ pub fn SideboardTabs( on:click=move |_| { batch(move || { - game_state_signal.view_game(); + game_state.view_game(); if tab_view() == SideboardTabView::Chat { chat.seen_messages(); } @@ -119,7 +120,7 @@ pub fn SideboardTabs( on:click=move |_| { batch(move || { - game_state_signal.view_history(); + game_state.view_history(); if tab_view() == SideboardTabView::Chat { chat.seen_messages(); } diff --git a/apis/src/pages/play.rs b/apis/src/pages/play.rs index b0eef5be..e1a3dcb2 100644 --- a/apis/src/pages/play.rs +++ b/apis/src/pages/play.rs @@ -31,16 +31,17 @@ pub fn Play(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoView Some(Ok(Some(user))) => Some(user), _ => None, }; + let white_and_black = create_read_slice(game_state.signal, |gs| (gs.white_id, gs.black_id)); let show_buttons = move || { user().map_or(false, |user| { - let game_state = game_state.signal.get(); - Some(user.id) == game_state.black_id || Some(user.id) == game_state.white_id + let (white_id, black_id) = white_and_black(); + Some(user.id) == black_id || Some(user.id) == white_id }) }; let player_is_black = create_memo(move |_| { user().map_or(false, |user| { - let game_state = game_state.signal.get(); - Some(user.id) == game_state.black_id + let black_id = white_and_black().1; + Some(user.id) == black_id }) }); let parent_container_style = move || { diff --git a/apis/src/providers/game_state.rs b/apis/src/providers/game_state.rs index 58f76f7b..995fa2fc 100644 --- a/apis/src/providers/game_state.rs +++ b/apis/src/providers/game_state.rs @@ -1,3 +1,4 @@ +use crate::common::MoveInfo; use crate::providers::api_requests::ApiRequests; use crate::responses::GameResponse; use hive_lib::{Color, GameControl, GameStatus, GameType, Piece, Position, State, Turn}; @@ -35,11 +36,7 @@ impl GameStateSignal { s.state = state; s.black_id = None; s.white_id = None; - s.target_positions = vec![]; - s.active = None; - s.target_position = None; - s.current_position = None; - s.reserve_position = None; + s.move_info.reset(); s.history_turn = None; s.view = View::Game; s.game_control_pending = None; @@ -55,8 +52,19 @@ impl GameStateSignal { }); } + // No longer access the whole signal when getting user_color pub fn user_color(&self, user_id: Uuid) -> Option { - self.signal.get().user_color(user_id) + let ids = create_read_slice(self.signal, |gamestate| { + (gamestate.white_id, gamestate.black_id) + }); + let (white_id, black_id) = ids(); + if Some(user_id) == black_id { + return Some(Color::Black); + } + if Some(user_id) == white_id { + return Some(Color::White); + } + None } pub fn undo_move(&mut self) { @@ -97,18 +105,20 @@ impl GameStateSignal { } pub fn set_state(&mut self, state: State, black_id: Uuid, white_id: Uuid) { - self.reset(); - let turn = if state.turn != 0 { - Some(state.turn - 1) - } else { - None - }; - self.signal.update(|s| { - s.history_turn = turn; - s.state = state; - s.black_id = Some(black_id); - s.white_id = Some(white_id); - }) + batch(move || { + self.reset(); + let turn = if state.turn != 0 { + Some(state.turn - 1) + } else { + None + }; + self.signal.update(|s| { + s.history_turn = turn; + s.state = state; + s.black_id = Some(black_id); + s.white_id = Some(white_id); + }) + }); } pub fn set_game_id(&mut self, game_id: String) { @@ -118,12 +128,12 @@ impl GameStateSignal { pub fn play_turn(&mut self, piece: Piece, position: Position) { self.signal.update(|s| { s.play_turn(piece, position); - s.reset() + s.move_info.reset() }) } pub fn reset(&mut self) { - self.signal.update(|s| s.reset()) + self.signal.update(|s| s.move_info.reset()) } pub fn move_active(&mut self) { @@ -201,6 +211,22 @@ impl GameStateSignal { }); Memo::new(move |_| game_status_finished() || game_response_finished()) } + + pub fn is_last_turn_as_signal(&self) -> Signal { + create_read_slice(self.signal, |gs| { + if gs.state.turn == 0 { + true + } else { + gs.history_turn == Some(gs.state.turn - 1) + } + }) + } + + pub fn is_first_turn_as_signal(&self) -> Signal { + create_read_slice(self.signal, |gs| { + gs.history_turn.is_none() || gs.history_turn == Some(0) + }) + } } #[derive(Clone, Debug, PartialEq, Eq)] @@ -217,16 +243,7 @@ pub struct GameState { pub state: State, pub black_id: Option, pub white_id: Option, - // possible destinations of selected piece - pub target_positions: Vec, - // the piece (either from reserve or board) that has been clicked last - pub active: Option, - // the position of the piece that has been clicked last - pub current_position: Option, - // the position of the target that got clicked last - pub target_position: Option, - // the position of the reserve piece that got clicked last - pub reserve_position: Option, + pub move_info: MoveInfo, // the turn we want to display the history at pub history_turn: Option, // show history or reserve @@ -251,11 +268,7 @@ impl GameState { state, black_id: None, white_id: None, - target_positions: vec![], - active: None, - target_position: None, - current_position: None, - reserve_position: None, + move_info: MoveInfo::new(), history_turn: None, view: View::Game, game_control_pending: None, @@ -263,6 +276,7 @@ impl GameState { } } + // Still needed because send_game_control uses it, maybe this should be moved out of the gamestate? pub fn user_color(&self, user_id: Uuid) -> Option { if Some(user_id) == self.black_id { return Some(Color::Black); @@ -284,15 +298,7 @@ impl GameState { } pub fn set_target(&mut self, position: Position) { - self.target_position = Some(position); - } - - pub fn reset(&mut self) { - self.target_positions.clear(); - self.active = None; - self.target_position = None; - self.current_position = None; - self.reserve_position = None; + self.move_info.target_position = Some(position); } pub fn send_game_control(&mut self, game_control: GameControl, user_id: Uuid) { @@ -331,17 +337,19 @@ impl GameState { pub fn move_active(&mut self) { //log!("Moved active!"); - if let (Some(active), Some(position)) = (self.active, self.target_position) { + if let (Some(active), Some(position)) = + (self.move_info.active, self.move_info.target_position) + { if let Err(e) = self.state.play_turn_from_position(active, position) { log!("Could not play turn: {} {} {}", active, position, e); } else if let Some(ref game_id) = self.game_id { let turn = Turn::Move(active, position); ApiRequests::new().turn(game_id.to_owned(), turn); - self.reset(); + self.move_info.reset(); self.history_turn = Some(self.state.turn - 1); } else { log!("We should be in analysis"); - self.reset(); + self.move_info.reset(); self.history_turn = Some(self.state.turn - 1); } } @@ -349,30 +357,30 @@ impl GameState { // TODO refactor to not take a position, the position and piece are in self already pub fn show_moves(&mut self, piece: Piece, position: Position) { - if let Some(already) = self.active { + if let Some(already) = self.move_info.active { if piece == already { - self.reset(); + self.move_info.reset(); return; } } - self.reset(); - self.current_position = Some(position); + self.move_info.reset(); + self.move_info.current_position = Some(position); let moves = self.state.board.moves(self.state.turn_color); if let Some(positions) = moves.get(&(piece, position)) { - positions.clone_into(&mut self.target_positions); - self.active = Some(piece); + positions.clone_into(&mut self.move_info.target_positions); + self.move_info.active = Some(piece); } } pub fn show_spawns(&mut self, piece: Piece, position: Position) { - self.reset(); - self.target_positions = self + self.move_info.reset(); + self.move_info.target_positions = self .state .board .spawnable_positions(self.state.turn_color) .collect::>(); - self.active = Some(piece); - self.reserve_position = Some(position); + self.move_info.active = Some(piece); + self.move_info.reserve_position = Some(position); } pub fn show_history_turn(&mut self, turn: usize) { @@ -386,6 +394,7 @@ impl GameState { } } + //TODO: is this still useful for play and analysis where gamestate is untracked for the callback? pub fn is_last_turn(&self) -> bool { if self.state.turn == 0 { return true; diff --git a/apis/src/providers/games.rs b/apis/src/providers/games.rs index 995e8cd3..0230ce3a 100644 --- a/apis/src/providers/games.rs +++ b/apis/src/providers/games.rs @@ -51,8 +51,7 @@ impl GamesSignal { } } } - } - if let Some(game) = s.realtime.get(&nanoid) { + } else if let Some(game) = s.realtime.get(&nanoid) { if game.current_player_id == user.id { if let Some(gp) = s .next_realtime @@ -71,8 +70,7 @@ impl GamesSignal { } } } - } - if let Some(game) = s.correspondence.get(&nanoid) { + } else if let Some(game) = s.correspondence.get(&nanoid) { if game.current_player_id == user.id { if let Some(gp) = s .next_correspondence diff --git a/apis/src/providers/navigation_controller.rs b/apis/src/providers/navigation_controller.rs index eac073f3..a76b6d3e 100644 --- a/apis/src/providers/navigation_controller.rs +++ b/apis/src/providers/navigation_controller.rs @@ -28,15 +28,17 @@ impl NavigationControllerSignal { } pub fn update_nanoid(&mut self, nanoid: Option) { - self.signal.update(|s| nanoid.clone_into(&mut s.nanoid)); - let api = ApiRequests::new(); - if let Some(game_id) = nanoid { - let mut game_state = expect_context::(); - let chat = expect_context::(); - game_state.set_game_id(game_id.to_owned()); - api.join(game_id.to_owned()); - chat.typed_message.update(|s| s.clear()); - } + batch(move || { + self.signal.update(|s| nanoid.clone_into(&mut s.nanoid)); + let api = ApiRequests::new(); + if let Some(game_id) = nanoid { + let mut game_state = expect_context::(); + let chat = expect_context::(); + game_state.set_game_id(game_id.to_owned()); + api.join(game_id.to_owned()); + chat.typed_message.update(|s| s.clear()); + } + }); } } diff --git a/apis/src/providers/websocket/context.rs b/apis/src/providers/websocket/context.rs index 40b33da5..a9952350 100644 --- a/apis/src/providers/websocket/context.rs +++ b/apis/src/providers/websocket/context.rs @@ -61,7 +61,7 @@ impl WebsocketContext { } fn on_message_callback(m: String) { - handle_response(m) + handle_response(m); } fn fix_wss(url: &str) -> String { diff --git a/apis/src/providers/websocket/response_handler.rs b/apis/src/providers/websocket/response_handler.rs index db04308d..f584e320 100644 --- a/apis/src/providers/websocket/response_handler.rs +++ b/apis/src/providers/websocket/response_handler.rs @@ -13,23 +13,25 @@ use super::{ }; pub fn handle_response(m: String) { - let _game_state = expect_context::(); - let _games = expect_context::(); - match serde_json::from_str::(&m) { - Ok(result) => match result { - ServerResult::Ok(message) => match *message { - Pong { ping_sent, .. } => handle_ping(ping_sent), - UserStatus(user_update) => handle_user_status(user_update), - Game(game_update) => handle_game(*game_update), - Challenge(challenge) => handle_challenge(challenge), - Chat(message) => handle_chat(message), - UserSearch(results) => handle_user_search(results), - todo => { - log!("Got {todo:?} which is currently still unimplemented"); - } // GameRequiresAction, UserStatusChange, ... + batch(move || { + let _game_state = expect_context::(); + let _games = expect_context::(); + match serde_json::from_str::(&m) { + Ok(result) => match result { + ServerResult::Ok(message) => match *message { + Pong { ping_sent, .. } => handle_ping(ping_sent), + UserStatus(user_update) => handle_user_status(user_update), + Game(game_update) => handle_game(*game_update), + Challenge(challenge) => handle_challenge(challenge), + Chat(message) => handle_chat(message), + UserSearch(results) => handle_user_search(results), + todo => { + log!("Got {todo:?} which is currently still unimplemented"); + } // GameRequiresAction, UserStatusChange, ... + }, + ServerResult::Err(e) => log!("Got error from server: {e}"), }, - ServerResult::Err(e) => log!("Got error from server: {e}"), - }, - Err(e) => log!("Can't parse: {m}, error is: {e}"), - } + Err(e) => log!("Can't parse: {m}, error is: {e}"), + } + }); } diff --git a/apis/src/responses/game.rs b/apis/src/responses/game.rs index 96a79165..3d06a16f 100644 --- a/apis/src/responses/game.rs +++ b/apis/src/responses/game.rs @@ -44,6 +44,15 @@ pub struct GameResponse { pub repetitions: Vec, } +impl PartialEq for GameResponse { + fn eq(&self, other: &Self) -> bool { + self.game_id == other.game_id + && self.turn == other.turn + && self.finished == other.finished + && self.last_interaction == other.last_interaction + } +} + impl GameResponse { pub fn white_rating(&self) -> u64 { self.white_player.rating_for_speed(&self.speed)