From a05a4c2be030892a5d1b29c9ab7a8afbadc50a12 Mon Sep 17 00:00:00 2001 From: PenguinWithATie <166940857+PenguinWithATie@users.noreply.github.com> Date: Tue, 2 Jul 2024 14:32:05 -0600 Subject: [PATCH] Analysis tool (#242) * analysis tool v1 * Styles analysis board, fixes ssr issues (#266) --------- Co-authored-by: IongIer <104294646+IongIer@users.noreply.github.com> --- Cargo.lock | 51 ++++- Cargo.toml | 7 +- apis/Cargo.toml | 3 + apis/src/components/atoms/mod.rs | 1 - apis/src/components/atoms/piece.rs | 9 +- apis/src/components/atoms/target.rs | 21 +- apis/src/components/atoms/undo_button.rs | 20 -- .../components/organisms/analysis/atoms.rs | 208 ++++++++++++++++++ .../components/organisms/analysis/history.rs | 146 ++++++++++++ apis/src/components/organisms/analysis/mod.rs | 15 +- .../organisms/analysis/save_and_load.rs | 162 ++++++++++++++ .../organisms/analysis/side_board.rs | 71 ------ .../src/components/organisms/analysis/tree.rs | 109 +++++++++ .../src/components/organisms/display_timer.rs | 1 + apis/src/components/organisms/dropdowns.rs | 16 ++ apis/src/components/organisms/reserve.rs | 42 +++- apis/src/components/organisms/side_board.rs | 46 +--- apis/src/pages/analysis.rs | 41 ++-- apis/src/pages/play.rs | 21 +- apis/style/main.scss | 19 +- engine/src/history.rs | 32 +++ 21 files changed, 850 insertions(+), 191 deletions(-) delete mode 100644 apis/src/components/atoms/undo_button.rs create mode 100644 apis/src/components/organisms/analysis/atoms.rs create mode 100644 apis/src/components/organisms/analysis/history.rs create mode 100644 apis/src/components/organisms/analysis/save_and_load.rs delete mode 100644 apis/src/components/organisms/analysis/side_board.rs create mode 100644 apis/src/components/organisms/analysis/tree.rs diff --git a/Cargo.lock b/Cargo.lock index 1524ff61..e8aeb99d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -418,6 +418,8 @@ dependencies = [ "actix-web-actors", "anyhow", "argon2", + "base64 0.22.1", + "bincode", "cfg-if", "chrono", "console_error_panic_hook", @@ -449,6 +451,7 @@ dependencies = [ "simple_logger", "thiserror", "tokio", + "tree-ds", "uuid", "wasm-bindgen", "web-sys", @@ -576,6 +579,15 @@ dependencies = [ "tokio", ] +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bitfield-struct" version = "0.6.1" @@ -1897,14 +1909,18 @@ checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388" [[package]] name = "lazy_static" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "leptix_primitives" -version = "0.1.0" -source = "git+https://github.com/leptix/leptix.git#867728677f7372767938f71df901655f2ba5f872" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb1c55c1e0403f008c8f82362e4e247034552d64cf641f51d12a2c46d391bf7" dependencies = [ "derive_more", "itertools", @@ -2863,6 +2879,15 @@ dependencies = [ "futures-core", ] +[[package]] +name = "sequential_gen" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da850bb1f2deffd0d19c1a1fcf445cb747aa401517c96eff7c6d965de792175d" +dependencies = [ + "lazy_static", +] + [[package]] name = "serde" version = "1.0.202" @@ -3132,6 +3157,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "stringprep" version = "0.1.4" @@ -3445,6 +3476,18 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tree-ds" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a67f3e9bee38b6a34b29a98abf5fc5b13aea6a7db36753d260735af7633a8" +dependencies = [ + "lazy_static", + "sequential_gen", + "serde", + "thiserror", +] + [[package]] name = "typed-builder" version = "0.18.2" diff --git a/Cargo.toml b/Cargo.toml index c0abe706..271c4e42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,13 +48,16 @@ diesel_migrations = { version = "2.1", features = ["postgres"]} uuid = { version = "1.7", features = ["v4", "js", "serde"] } nanoid = "0.4" dotenvy = "0.15" -lazy_static = "1.4" +lazy_static = "1.5" rand = "0.8" rand_core = "0.6" cookie = "0.18" skillratings = "0.26" chrono = { version = "0.4", features = ["serde"] } -leptix_primitives = {git ="https://github.com/leptix/leptix.git" } +leptix_primitives = { version = "0.2.0" } +tree-ds = {version = "0.1.4", features = ["serde", "auto_id"] } +bincode = { version = "1.3.3"} +base64 = { version = "0.22.1"} # Defines a size-optimized profile for the WASM bundle in release mode [profile.wasm-release] inherits = "release" diff --git a/apis/Cargo.toml b/apis/Cargo.toml index 456c5439..cbaafb66 100644 --- a/apis/Cargo.toml +++ b/apis/Cargo.toml @@ -50,6 +50,9 @@ uuid = { workspace = true } wasm-bindgen = { workspace = true} web-sys = { workspace = true } leptix_primitives = {workspace = true} +tree-ds = {workspace = true} +bincode = {workspace = true} +base64 = {workspace = true} [features] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] diff --git a/apis/src/components/atoms/mod.rs b/apis/src/components/atoms/mod.rs index f4ac59d9..7ee977b7 100644 --- a/apis/src/components/atoms/mod.rs +++ b/apis/src/components/atoms/mod.rs @@ -20,4 +20,3 @@ pub mod svgs; pub mod target; pub mod title; pub mod toggle_controls; -pub mod undo_button; diff --git a/apis/src/components/atoms/piece.rs b/apis/src/components/atoms/piece.rs index 0187c190..5fc9db8c 100644 --- a/apis/src/components/atoms/piece.rs +++ b/apis/src/components/atoms/piece.rs @@ -1,6 +1,7 @@ use crate::common::{MoveConfirm, TileDesign, TileDots, TileRotation}; use crate::common::{PieceType, SvgPos}; -use crate::pages::{analysis::InAnalysis, play::CurrentConfirm}; +use crate::components::organisms::analysis::AnalysisSignal; +use crate::pages::play::CurrentConfirm; use crate::providers::game_state::GameStateSignal; use crate::providers::Config; use hive_lib::{Bug, Piece, Position}; @@ -64,10 +65,12 @@ pub fn Piece( let mut game_state = expect_context::(); let current_confirm = expect_context::().0; - let in_analysis = use_context::().unwrap_or(InAnalysis(RwSignal::new(false))); + let analysis = use_context::() + .unwrap_or(AnalysisSignal(RwSignal::new(None))) + .0; let onclick = move |evt: MouseEvent| { evt.stop_propagation(); - let in_analysis = in_analysis.0.get_untracked(); + let in_analysis = analysis.get_untracked().is_some(); if in_analysis || game_state.is_move_allowed() { match piece_type { PieceType::Board => { diff --git a/apis/src/components/atoms/target.rs b/apis/src/components/atoms/target.rs index 82b1491e..5d760310 100644 --- a/apis/src/components/atoms/target.rs +++ b/apis/src/components/atoms/target.rs @@ -1,6 +1,7 @@ use crate::common::MoveConfirm; use crate::common::SvgPos; -use crate::pages::{analysis::InAnalysis, play::CurrentConfirm}; +use crate::components::organisms::analysis::AnalysisSignal; +use crate::pages::play::CurrentConfirm; use crate::providers::game_state::GameStateSignal; use hive_lib::Position; use leptos::*; @@ -14,18 +15,30 @@ pub fn Target( let center = move || SvgPos::center_for_level(position, level()); let transform = move || format!("translate({},{})", center().0, center().1); let mut game_state = expect_context::(); - let in_analysis = use_context::().unwrap_or(InAnalysis(RwSignal::new(false))); + let analysis = use_context::() + .unwrap_or(AnalysisSignal(RwSignal::new(None))) + .0; let current_confirm = expect_context::().0; - // Select the target position and make a move if it's the correct mode let onclick = move |_| { - let in_analysis = in_analysis.0.get_untracked(); + let in_analysis = analysis.get().is_some(); if in_analysis || game_state.is_move_allowed() { batch(move || { game_state.set_target(position); if current_confirm() == MoveConfirm::Single || in_analysis { game_state.move_active(); } + analysis.update(|analysis| { + if let Some(analysis) = analysis { + let moves = game_state.signal.get_untracked().state.history.moves; + let last_move = moves.last().unwrap().clone(); + if last_move.0 == "pass" { + //if move is pass, add prev move + analysis.add_node(moves[moves.len() - 2].clone()); + } + analysis.add_node(last_move); + } + }); }); } }; diff --git a/apis/src/components/atoms/undo_button.rs b/apis/src/components/atoms/undo_button.rs deleted file mode 100644 index d45f7630..00000000 --- a/apis/src/components/atoms/undo_button.rs +++ /dev/null @@ -1,20 +0,0 @@ -use crate::providers::game_state::GameStateSignal; -use leptos::*; -use leptos_icons::*; - -#[component] -pub fn UndoButton() -> impl IntoView { - let undo = move |_| { - let mut game_state = expect_context::(); - game_state.undo_move(); - }; - - view! { - - } -} diff --git a/apis/src/components/organisms/analysis/atoms.rs b/apis/src/components/organisms/analysis/atoms.rs new file mode 100644 index 00000000..b3caf7fd --- /dev/null +++ b/apis/src/components/organisms/analysis/atoms.rs @@ -0,0 +1,208 @@ +use super::TreeNode; +use crate::components::organisms::analysis::{AnalysisSignal, ToggleStates}; +use crate::components::organisms::reserve::Alignment; +use crate::components::organisms::reserve::Reserve; +use hive_lib::Color; +use leptix_primitives::components::collapsible::{ + CollapsibleContent, CollapsibleRoot, CollapsibleTrigger, +}; +use leptos::*; +use leptos_icons::Icon; +use tree_ds::prelude::Node; + +#[component] +pub fn UndoButton() -> impl IntoView { + let analysis = expect_context::().0; + let is_disabled = move || { + analysis.get().map_or(true, |analysis| { + analysis + .current_node + .map_or(true, |n| n.get_parent_id().is_none()) + }) + }; + let undo = move |_| { + analysis.update(|a| { + if let Some(a) = a { + if let Some(node) = a.current_node.clone() { + let new_current = node.get_parent_id(); + if let Some(new_current) = new_current { + a.update_node(new_current); + a.tree + .remove_node( + &node.get_node_id(), + tree_ds::prelude::NodeRemovalStrategy::RemoveNodeAndChildren, + ) + .unwrap(); + } else { + a.reset(); + } + } + } + }); + }; + + view! { + + } +} + +#[component] +pub fn ReserveContent(player_color: Memo) -> impl IntoView { + let top_color = Signal::derive(move || player_color().opposite_color()); + let bottom_color = Signal::derive(player_color); + view! { + +
+ +
+ + } +} + +use leptos::leptos_dom::helpers::debounce; +#[derive(Clone)] +pub enum HistoryNavigation { + Next, + Previous, +} + +#[component] +pub fn HistoryButton( + action: HistoryNavigation, + post_action: Option>, + #[prop(optional)] node_ref: Option>, +) -> impl IntoView { + let analysis = expect_context::().0; + 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 { + HistoryNavigation::Next => icondata::AiStepForwardFilled, + HistoryNavigation::Previous => icondata::AiStepBackwardFilled, + }; + + let is_disabled = move || { + analysis.get().map_or(true, |analysis| { + analysis.current_node.map_or(true, |n| match cloned_action { + HistoryNavigation::Next => n.get_children_ids().is_empty(), + HistoryNavigation::Previous => n.get_parent_id().is_none(), + }) + }) + }; + let debounced_action = debounce(std::time::Duration::from_millis(10), move |_| { + let current_node = analysis.get().unwrap().current_node; + let updated_node_id = current_node.and_then(|n| match action { + HistoryNavigation::Next => n.get_children_ids().first().cloned(), + HistoryNavigation::Previous => n.get_parent_id(), + }); + if let Some(updated_node_id) = updated_node_id { + analysis.update(|a| { + if let Some(a) = a { + a.update_node(updated_node_id); + } + }); + } + if let Some(post_action) = post_action { + post_action(()) + } + }); + let _definite_node_ref = node_ref.unwrap_or(create_node_ref::()); + + view! { + + } +} + +#[component] +pub fn HistoryMove(node: Node) -> impl IntoView { + let analysis = expect_context::().0; + let value = node.get_value().unwrap(); + let node_id = node.get_node_id(); + let class = move || { + let mut class = + "w-full transition-transform duration-300 transform hover:bg-pillbug-teal active:scale-95"; + if analysis + .get() + .unwrap() + .current_node + .map_or(false, |n| n.get_node_id() == node_id) + { + class = "w-full transition-transform duration-300 transform hover:bg-pillbug-teal bg-orange-twilight active:scale-95" + } + class + }; + let onclick = move |_| { + analysis.update(|a| { + if let Some(a) = a { + a.update_node(node_id); + } + }); + }; + view! { +
+ {format!("{}. {} {}", value.turn, value.piece, value.position)} +
+ } +} + +#[component] +pub fn CollapsibleMove(node: Node, children: ChildrenFn) -> impl IntoView { + let closed_toggles = expect_context::().0; + let node_id = node.get_node_id(); + let is_open = !closed_toggles.get().contains(&node_id); + let (open, set_open) = create_signal(is_open); + view! { + + +
+ + + + +
+ +
+ } +} diff --git a/apis/src/components/organisms/analysis/history.rs b/apis/src/components/organisms/analysis/history.rs new file mode 100644 index 00000000..cbae58cf --- /dev/null +++ b/apis/src/components/organisms/analysis/history.rs @@ -0,0 +1,146 @@ +use std::collections::HashMap; + +use crate::components::organisms::analysis::atoms::{ + CollapsibleMove, HistoryButton, HistoryMove, HistoryNavigation, +}; +use crate::components::organisms::{ + analysis::{AnalysisSignal, DownloadTree, LoadTree, UndoButton}, + reserve::{Alignment, Reserve}, +}; +use hive_lib::Color; +use leptos::{ev::keydown, *}; +use leptos_use::{use_event_listener, use_window}; +use tree_ds::prelude::*; + +#[component] +pub fn History(#[prop(optional)] mobile: bool) -> impl IntoView { + let analysis = expect_context::().0; + let prev_button = create_node_ref::(); + let next_button = create_node_ref::(); + let window = use_window(); + let active = Signal::derive(move || { + window + .as_ref()? + .document()? + .query_selector(".bg-orange-twilight") + .ok()? + }); + create_effect(move |_| { + _ = use_event_listener(document().body(), keydown, move |evt| { + if evt.key() == "ArrowLeft" { + evt.prevent_default(); + if let Some(prev) = prev_button.get_untracked() { + prev.click() + } + } else if evt.key() == "ArrowRight" { + evt.prevent_default(); + if let Some(next) = next_button.get_untracked() { + next.click() + } + } + }); + }); + + let focus = if mobile { + None + } else { + Some(Callback::new(move |()| { + if let Some(elem) = active.get_untracked() { + elem.scroll_into_view_with_bool(false); + } + })) + }; + let walk_tree = move || { + let tree = analysis.get().unwrap().tree; + let root = tree.get_root_node()?; + //Post order traversal ensures all children are processed before their parents + let node_order = tree + .traverse(TraversalStrategy::PostOrder, &root.get_node_id()) + .ok()?; + let mut content = Fragment::new(vec![]); + let mut branches = HashMap::::new(); + for node_id in node_order { + let node = tree.get_node_by_id(&node_id)?; + let children_ids = node.get_children_ids(); + let siblings_ids = tree.get_sibling_ids(&node_id, true).ok()?; + let parent_deg = siblings_ids.len(); + let not_first_sibling = siblings_ids.first().map_or(true, |s| *s != node_id); + content = if children_ids.len() > 1 { + /* == More than one child == + gather all children but the first (secondary variations) + and place them inside a collapsible + then place the first child (main variation) at the same level as the parent + */ + let inner = store_value( + children_ids + .iter() + .skip(1) + .map(|c| branches.remove(c)) + .collect::>(), + ); + view! { + {inner} + {branches.remove(&children_ids[0])} + } + } else if parent_deg > 2 && not_first_sibling && children_ids.len() == 1 { + /* We make a colapsible for nodes with one child + for aesthetic reasons, to hide its content. + it must be that parent has already a "seccondary variation" + (else a toggle would not be needed) + and this must not be the "main variation" (first child) + */ + let static_cont = store_value(content); + view! { + <> + {static_cont} + + } + } else { + /* All other nodes are placed at the same level as the parent + in a regular HistoryMove node */ + view! { + + {content} + } + }; + branches.insert(node_id, content.clone()); + /* We are start of a new branch so clear the content + to process either the next sibling or tho parent */ + if parent_deg > 1 { + content = Fragment::new(vec![]); + } + } + Some(content) + }; + let viewbox_str = "-32 -40 250 120"; + view! { +
+
+ + + +
+ +
+ + +
+
+
+ + + + +
+
{walk_tree}
+
+ } +} diff --git a/apis/src/components/organisms/analysis/mod.rs b/apis/src/components/organisms/analysis/mod.rs index e56e35f1..172ed485 100644 --- a/apis/src/components/organisms/analysis/mod.rs +++ b/apis/src/components/organisms/analysis/mod.rs @@ -1,2 +1,13 @@ -mod side_board; -pub use side_board::SideboardTabs; +mod atoms; +mod history; +mod save_and_load; +mod tree; +pub use atoms::HistoryButton; +pub use atoms::HistoryNavigation; +pub use atoms::UndoButton; +pub use history::History; +pub use save_and_load::{DownloadTree, LoadTree}; +pub use tree::AnalysisSignal; +pub use tree::AnalysisTree; +pub use tree::ToggleStates; +pub use tree::TreeNode; diff --git a/apis/src/components/organisms/analysis/save_and_load.rs b/apis/src/components/organisms/analysis/save_and_load.rs new file mode 100644 index 00000000..f71d4091 --- /dev/null +++ b/apis/src/components/organisms/analysis/save_and_load.rs @@ -0,0 +1,162 @@ +use core::str; +use hive_lib::History; +use leptos::*; +use wasm_bindgen::{JsCast, JsValue}; +use web_sys::{js_sys::Array, Blob, Url}; + +use super::AnalysisTree; +use crate::{ + components::organisms::analysis::AnalysisSignal, + providers::game_state::{GameState, GameStateSignal}, +}; +use std::path::Path; +use wasm_bindgen::closure::Closure; +#[component] +pub fn DownloadTree(tree: AnalysisTree) -> impl IntoView { + let download = move |_| { + let (blob, filename) = blob_and_filename(tree.clone()); + // Create an object URL for the blob + let url = Url::create_object_url_with_blob(&blob).unwrap(); + // Create a download link + let a = web_sys::window() + .unwrap() + .document() + .unwrap() + .create_element("a") + .unwrap() + .dyn_into::() + .expect("This element is not an HtmlElement"); + a.set_attribute("href", &url).unwrap(); + a.set_attribute("download", &filename).unwrap(); + a.click(); + let _ = Url::revoke_object_url(&url); + }; + + view! { + + } +} + +fn blob_and_filename(tree: AnalysisTree) -> (Blob, String) { + let tree = bincode::serialize(&tree).unwrap(); + let tree = String::from_utf8(tree).unwrap(); + let file = Array::from(&JsValue::from(tree)); + let date = chrono::offset::Local::now() + .format("%d-%b-%Y_%H:%M:%S") + .to_string(); + ( + Blob::new_with_u8_array_sequence(&file).unwrap(), + format!("analysis_{date}.hat"), + ) +} + +#[component] +pub fn LoadTree() -> impl IntoView { + let input_ref = create_node_ref::(); + let analysis = expect_context::().0; + let loaded = RwSignal::new(false); + let div_ref = NodeRef::::new(); + div_ref.on_load(move |_| { + let _ = div_ref + .get_untracked() + .expect("div to be loaded") + .on_mount(move |_| loaded.set(true)); + }); + view! { +
+ + { + let from_hat = Closure::new(move |string: JsValue| { + let bytes = string.as_string().unwrap().as_bytes().to_vec(); + if let Ok(tree) = bincode::deserialize::(&bytes) { + analysis + .update(|a| { + if let Some(a) = a { + a.reset(); + a.tree = tree.tree; + if let Some(current_node) = tree.current_node { + a.update_node(current_node.get_node_id()); + } + } + }); + } else { + logging::log!("Couldn't open analysis file"); + } + }); + let from_pgn = Closure::new(move |string: JsValue| { + let string = string.as_string().unwrap(); + let history = History::from_pgn_str(string); + if let Ok(history) = history { + let state = hive_lib::State::new_from_history(&history) + .expect("Couldn't create game state"); + let mut new_gs = GameState::new(); + new_gs.state = state; + let new_gs_signal = GameStateSignal::new(); + new_gs_signal.signal.update_untracked(|gs| *gs = new_gs); + if let Some(tree) = AnalysisTree::from_state(new_gs_signal) { + analysis + .update(|a| { + if let Some(a) = a { + a.reset(); + a.tree = tree.tree; + if let Some(current_node) = tree.current_node { + a.update_node(current_node.get_node_id()); + } + } + }); + } else { + logging::log!("Couldn't open pgn file"); + } + } + }); + view! { + + } + } + + +
+ } +} diff --git a/apis/src/components/organisms/analysis/side_board.rs b/apis/src/components/organisms/analysis/side_board.rs deleted file mode 100644 index 9a889440..00000000 --- a/apis/src/components/organisms/analysis/side_board.rs +++ /dev/null @@ -1,71 +0,0 @@ -use crate::{ - components::{ - atoms::undo_button::UndoButton, - organisms::{ - history::History, - reserve::{Alignment, Reserve}, - }, - }, - providers::game_state::GameStateSignal, -}; -use hive_lib::Color; -use leptos::*; - -use leptix_primitives::components::tabs::{TabsContent, TabsList, TabsRoot, TabsTrigger}; - -#[component] -pub fn SideboardTabs( - player_is_black: Memo, - #[prop(optional)] extend_tw_classes: &'static str, -) -> impl IntoView { - let top_color = Signal::derive(move || { - if player_is_black() { - Color::White - } else { - Color::Black - } - }); - let bottom_color = Signal::derive(move || top_color().opposite_color()); - let mut game_state = expect_context::(); - let button_class = move || { - "transform transition-transform duration-300 active:scale-95 hover:bg-pillbug-teal data-[state=active]:dark:bg-button-twilight data-[state=active]:bg-slate-400".to_string() - }; - view! { - - - -
- - "Game" - - - "History" - -
-
- - -
- -
- -
- - - -
- } -} diff --git a/apis/src/components/organisms/analysis/tree.rs b/apis/src/components/organisms/analysis/tree.rs new file mode 100644 index 00000000..57b7563b --- /dev/null +++ b/apis/src/components/organisms/analysis/tree.rs @@ -0,0 +1,109 @@ +use crate::providers::game_state::{GameState, GameStateSignal}; +use hive_lib::{GameType, History, State}; +use leptos::*; +use serde::{Deserialize, Serialize}; +use std::{collections::HashSet, vec}; +use tree_ds::prelude::{Node, Tree}; + +#[derive(Clone, Eq, PartialEq, Debug, Serialize, Deserialize)] +pub struct TreeNode { + pub turn: usize, + pub piece: String, + pub position: String, +} + +#[derive(Clone)] +pub struct AnalysisSignal(pub RwSignal>); + +#[derive(Clone)] +pub struct ToggleStates(pub RwSignal>); + +#[derive(Clone, Default, Serialize, Deserialize)] +pub struct AnalysisTree { + pub current_node: Option>, + pub tree: Tree, +} + +impl AnalysisTree { + pub fn from_state(game_state: GameStateSignal) -> Option { + let gs = game_state.signal.get_untracked(); + let mut tree = Tree::new(Some("analysis")); + let mut previous = None; + for (i, (piece, position)) in gs.state.history.moves.iter().enumerate() { + let new_node = Node::new_with_auto_id(Some(TreeNode { + turn: i + 1, + piece: piece.to_string(), + position: position.to_string(), + })); + let new_id = new_node.get_node_id(); + tree.add_node(new_node, previous.as_ref()).ok()?; + previous = Some(new_id); + } + let current_node = previous.and_then(|p| tree.get_node_by_id(&p)); + Some(Self { current_node, tree }) + } + + pub fn update_node(&mut self, node_id: i32) -> Option<()> { + let moves = self + .tree + .get_ancestor_ids(&node_id) + .ok()? + .into_iter() + .rev() + .chain(vec![node_id]) + .map(|a| self.tree.get_node_by_id(&a)?.get_value()) + .map(|a| { + let a = a.unwrap(); + (a.piece, a.position) + }) + .collect::>(); + let state = State::new_from_history(&History { + moves, + game_type: GameType::MLP, + ..History::new() + }) + .ok()?; + + let history_turn = self + .current_node + .as_ref() + .and_then(|n| n.get_value().map(|v| v.turn)); + + expect_context::().signal.update(|gs| { + gs.state = state; + gs.history_turn = history_turn; + gs.move_info.reset(); + }); + self.current_node + .clone_from(&self.tree.get_node_by_id(&node_id)); + Some(()) + } + + pub fn add_node(&mut self, last_move: (String, String)) { + let (piece, position) = last_move; + let turn = self + .current_node + .as_ref() + .map_or(1, |n| 1 + n.get_value().unwrap().turn); + + let new_node = Node::new_with_auto_id(Some(TreeNode { + turn, + piece, + position, + })); + let new_id = new_node.get_node_id(); + let parent_id = self.current_node.as_ref().map(|n| n.get_node_id()); + + self.tree.add_node(new_node, parent_id.as_ref()).unwrap(); + self.current_node = self.tree.get_node_by_id(&new_id); + } + + pub fn reset(&mut self) { + self.current_node = None; + self.tree = Tree::new(Some("analysis")); + let game_state = expect_context::(); + game_state.signal.update(|gs| { + *gs = GameState::new(); + }); + } +} diff --git a/apis/src/components/organisms/display_timer.rs b/apis/src/components/organisms/display_timer.rs index 0d65f238..9f538a5b 100644 --- a/apis/src/components/organisms/display_timer.rs +++ b/apis/src/components/organisms/display_timer.rs @@ -121,6 +121,7 @@ pub fn DisplayTimer(placement: Placement, vertical: bool) -> impl IntoView { } } }} +
diff --git a/apis/src/components/organisms/dropdowns.rs b/apis/src/components/organisms/dropdowns.rs index 1b8ad125..260c4181 100644 --- a/apis/src/components/organisms/dropdowns.rs +++ b/apis/src/components/organisms/dropdowns.rs @@ -84,6 +84,14 @@ pub fn MobileDropdown() -> impl IntoView { FAQ Learn: + + Analysis + impl IntoView { dropdown_style=DROPDOWN_MENU_STYLE content="Learn" > + + Analysis + bool { //viewing history if viewing == &View::History && !is_last_turn { @@ -44,12 +46,19 @@ pub fn Reserve( #[prop(into)] color: MaybeSignal, alignment: Alignment, #[prop(optional)] extend_tw_classes: &'static str, + #[prop(optional)] viewbox_str: Option<&'static str>, ) -> impl IntoView { 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"), + Alignment::DoubleRow => { + if let Some(viewbox_str) = viewbox_str { + (viewbox_str, "") + } else { + ("-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()); @@ -153,3 +162,32 @@ pub fn Reserve( } } + +#[component] +pub fn ReserveContent(player_color: Memo) -> impl IntoView { + let game_state = expect_context::(); + let top_color = Signal::derive(move || player_color().opposite_color()); + let bottom_color = Signal::derive(player_color); + let auth_context = expect_context::(); + let user = move || match (auth_context.user)() { + 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 (white_id, black_id) = white_and_black(); + Some(user.id) == black_id || Some(user.id) == white_id + }) + }; + view! { + +
+ + + + +
+ + } +} diff --git a/apis/src/components/organisms/side_board.rs b/apis/src/components/organisms/side_board.rs index 0f911aef..d4ce24a7 100644 --- a/apis/src/components/organisms/side_board.rs +++ b/apis/src/components/organisms/side_board.rs @@ -1,13 +1,7 @@ +use crate::components::organisms::reserve::ReserveContent; use crate::{ - components::{ - molecules::{analysis_and_download::AnalysisAndDownload, control_buttons::ControlButtons}, - organisms::{ - chat::ChatWindow, - history::History, - reserve::{Alignment, Reserve}, - }, - }, - providers::{chat::Chat, game_state::GameStateSignal, AuthContext}, + components::organisms::{chat::ChatWindow, history::History}, + providers::{chat::Chat, game_state::GameStateSignal}, }; use hive_lib::Color; use leptix_primitives::components::tabs::{TabsContent, TabsList, TabsRoot, TabsTrigger}; @@ -64,33 +58,10 @@ fn TriggerButton(name: TabView, tab: RwSignal) -> impl IntoView { #[component] pub fn SideboardTabs( - player_is_black: Memo, + player_color: Memo, #[prop(optional)] extend_tw_classes: &'static str, ) -> impl IntoView { - let game_state = expect_context::(); let tab = RwSignal::new(TabView::Reserve); - let auth_context = expect_context::(); - let user = move || match (auth_context.user)() { - 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 (white_id, black_id) = white_and_black(); - Some(user.id) == black_id || Some(user.id) == white_id - }) - }; - - let top_color = Signal::derive(move || { - if player_is_black() { - Color::White - } else { - Color::Black - } - }); - let bottom_color = Signal::derive(move || top_color().opposite_color()); view! { - -
- - - - -
- +
diff --git a/apis/src/pages/analysis.rs b/apis/src/pages/analysis.rs index b30dd7fc..c01d2c38 100644 --- a/apis/src/pages/analysis.rs +++ b/apis/src/pages/analysis.rs @@ -1,13 +1,9 @@ use crate::{ common::MoveConfirm, components::{ - atoms::{ - history_button::{HistoryButton, HistoryNavigation}, - undo_button::UndoButton, - }, layouts::base_layout::OrientationSignal, organisms::{ - analysis::SideboardTabs, + analysis::{AnalysisSignal, AnalysisTree, History, ToggleStates, UndoButton}, board::Board, reserve::{Alignment, Reserve}, }, @@ -17,37 +13,31 @@ use crate::{ }; use hive_lib::Color; use leptos::*; - -#[derive(Clone)] -pub struct InAnalysis(pub RwSignal); +use std::collections::HashSet; #[component] pub fn Analysis(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoView { provide_context(TargetStack(RwSignal::new(None))); - provide_context(InAnalysis(RwSignal::new(true))); + provide_context(AnalysisSignal(RwSignal::new(Some( + AnalysisTree::from_state(expect_context::()).unwrap_or_default(), + )))); + provide_context(ToggleStates(RwSignal::new(HashSet::new()))); provide_context(CurrentConfirm(Memo::new(move |_| MoveConfirm::Single))); let is_tall = expect_context::().is_tall; let parent_container_style = move || { if is_tall() { - "flex flex-col" + "flex flex-col h-full" } else { - "grid grid-cols-board-xs sm:grid-cols-board-sm lg:grid-cols-board-lg xxl:grid-cols-board-xxl grid-rows-6 pr-1" + "max-h-[100dvh] min-h-[100dvh] grid grid-cols-board-xs sm:grid-cols-board-sm lg:grid-cols-board-lg xxl:grid-cols-board-xxl grid-rows-6 pr-1" } }; - let player_is_black = create_memo(move |_| false); - let go_to_game = Callback::new(move |()| { - let mut game_state = expect_context::(); - if game_state.signal.get_untracked().is_last_turn() { - game_state.view_game(); - } - }); let bottom_color = Color::Black; let top_color = Color::White; view! {
@@ -56,14 +46,14 @@ pub fn Analysis(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoV fallback=move || { view! { -
- +
+
} } > -
+
@@ -75,14 +65,9 @@ pub fn Analysis(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoV
-
- - - - -
+
} diff --git a/apis/src/pages/play.rs b/apis/src/pages/play.rs index d34dfc1f..0b480bb7 100644 --- a/apis/src/pages/play.rs +++ b/apis/src/pages/play.rs @@ -59,10 +59,13 @@ pub fn Play(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoView Some(user.id) == black_id || Some(user.id) == white_id }) }; - let player_is_black = create_memo(move |_| { - user().map_or(false, |user| { + let player_color = create_memo(move |_| { + user().map_or(Color::White, |user| { let black_id = white_and_black().1; - Some(user.id) == black_id + match Some(user.id) == black_id { + true => Color::Black, + false => Color::White, + } }) }); let parent_container_style = move || { @@ -78,14 +81,8 @@ pub fn Play(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoView game_state.view_game(); } }); - let bottom_color = move || { - if player_is_black() { - Color::Black - } else { - Color::White - } - }; - let top_color = move || bottom_color().opposite_color(); + let bottom_color = player_color; + let top_color = move || player_color().opposite_color(); let controls_signal = expect_context::(); let show_controls = Signal::derive(move || !controls_signal.hidden.get() || game_state.is_finished()()); @@ -105,7 +102,7 @@ pub fn Play(#[prop(optional)] extend_tw_classes: &'static str) -> impl IntoView
- +
} diff --git a/apis/style/main.scss b/apis/style/main.scss index c1241b52..11c28c18 100644 --- a/apis/style/main.scss +++ b/apis/style/main.scss @@ -1 +1,18 @@ -// WARN: Hot reloads don't work without this file using cargo leptos v 0.2.0 \ No newline at end of file +.nested-content { + padding-left: 1em; /* Level 1 padding */ + .nested-content { + padding-left: 1em; /* Level 2 padding */ + .nested-content { + padding-left: 1em; /* Level 3 padding */ + .nested-content { + padding-left: 1em; /* Level 4 padding */ + .nested-content { + padding-left: 1em; /* Level 5 padding */ + .nested-content { + padding-left: 1em; /* Level 6 padding */ + } + } + } + } + } +} diff --git a/engine/src/history.rs b/engine/src/history.rs index 9fb2a526..cf24927b 100644 --- a/engine/src/history.rs +++ b/engine/src/history.rs @@ -208,4 +208,36 @@ impl History { } Ok(history) } + pub fn from_pgn_str(string: String) -> Result { + let mut history = History::new(); + lazy_static! { + static ref HEADER: Regex = Regex::new(r"\[.*").expect("This regex should compile"); + } + lazy_static! { + static ref RESULT: Regex = Regex::new(r"\[Result").expect("This regex should compile"); + } + lazy_static! { + static ref GAME_TYPE_LINE: Regex = + Regex::new(r"\[GameType.*").expect("This regex should compile"); + } + for line in string.lines() { + if line.is_empty() { + continue; + } + let tokens = line.split_whitespace().collect::>(); + if RESULT.is_match(line) { + if let Some(game_result) = tokens.get(1) { + history.parse_game_result(game_result); + } + } + if GAME_TYPE_LINE.is_match(line) { + history.parse_game_type(line)?; + } + if HEADER.is_match(line) { + continue; + } + history.parse_turn(&tokens)?; + } + Ok(history) + } }