diff --git a/crates/ascend/src/models.rs b/crates/ascend/src/models.rs index 3879ce4..8212f4b 100644 --- a/crates/ascend/src/models.rs +++ b/crates/ascend/src/models.rs @@ -42,13 +42,13 @@ pub mod v4 { pub problems: BTreeSet, } - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Problem { pub pattern: Pattern, pub method: v2::Method, } - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct Pattern { pub pattern: BTreeMap, } @@ -160,7 +160,7 @@ pub mod v2 { #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)] pub struct ProblemUid(pub uuid::Uuid); - #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Display, Copy)] + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Display, Copy, Hash)] pub enum Method { #[display("Feet follow hands")] FeetFollowHands, diff --git a/crates/ascend/src/models/semantics.rs b/crates/ascend/src/models/semantics.rs index 75c47ef..0c7b040 100644 --- a/crates/ascend/src/models/semantics.rs +++ b/crates/ascend/src/models/semantics.rs @@ -2,6 +2,22 @@ use super::*; use chrono::DateTime; use chrono::Utc; use std::collections::BTreeMap; +use std::collections::HashSet; + +impl Problem { + /// Returns all possible transformations for the pattern. Not for the method. + #[must_use] + pub fn transformations(&self, wall_dimensions: WallDimensions) -> HashSet { + self.pattern + .transformations(wall_dimensions) + .into_iter() + .map(|pattern| Self { + pattern, + method: self.method, + }) + .collect() + } +} impl Pattern { #[must_use] @@ -88,6 +104,23 @@ impl Pattern { pattern } + + /// Returns all possible transformations for the pattern + #[must_use] + pub fn transformations(&self, wall_dimensions: WallDimensions) -> HashSet { + let mut transformations = HashSet::new(); + + let pattern = self.canonicalize(); + for mut pat in [pattern.mirror(), pattern] { + transformations.insert(pat.clone()); + while let Some(p) = pat.shift_right(wall_dimensions, 1) { + transformations.insert(p.clone()); + pat = p; + } + } + + transformations + } } impl UserInteraction { diff --git a/crates/ascend/src/pages/wall.rs b/crates/ascend/src/pages/wall.rs index 358496f..144b5c3 100644 --- a/crates/ascend/src/pages/wall.rs +++ b/crates/ascend/src/pages/wall.rs @@ -13,6 +13,7 @@ use leptos::prelude::*; use leptos_router::params::Params; use std::collections::BTreeMap; use std::collections::BTreeSet; +use std::collections::HashSet; use std::ops::Deref; #[derive(Params, PartialEq, Clone)] @@ -60,7 +61,7 @@ struct Context { wall: Signal, user_interactions: Signal>, problem: Signal>, - filtered_problems: Signal>, + filtered_problem_transformations: Signal>>, user_interaction: Signal>, todays_attempt: Signal>, latest_attempt: Signal>, @@ -70,6 +71,9 @@ struct Context { cb_next_problem: Callback<()>, cb_set_problem: Callback, cb_upsert_todays_attempt: Callback, + + #[expect(dead_code)] + filtered_problems: Signal>, } #[component] @@ -93,7 +97,12 @@ fn Controller( // Derive signals let user_interaction = signals::user_interaction(user_interactions.into(), problem.into()); + let problem_transformations = signals::problem_transformations(wall); + + // TODO: still used? let filtered_problems = signals::filtered_problems(wall, filter_holds.into()); + + let filtered_problem_transformations = signals::filtered_problem_transformations(problem_transformations.into(), filter_holds.into()); let todays_attempt = signals::todays_attempt(user_interaction); let latest_attempt = signals::latest_attempt(user_interaction); @@ -111,14 +120,23 @@ fn Controller( // Callback: Set next problem to a random problem let cb_set_random_problem: Callback<()> = Callback::new(move |_| { // TODO: remove current problem from population - let population = filtered_problems.read(); + let population = filtered_problem_transformations.read(); let population = population.deref(); use rand::seq::IteratorRandom; let mut rng = rand::rng(); - let problem = population.iter().choose(&mut rng); - set_problem.set(problem.cloned()); + // Pick pattern + let Some(problem_set) = population.iter().choose(&mut rng) else { + return; + }; + + // Pick problem out of pattern transformations + let Some(problem) = problem_set.iter().choose(&mut rng) else { + return; + }; + + set_problem.set(Some(problem.clone())); }); // Callback: On click hold, Add/Remove hold position to problem filter @@ -161,6 +179,7 @@ fn Controller( todays_attempt, filter_holds: filter_holds.into(), filtered_problems: filtered_problems.into(), + filtered_problem_transformations: filtered_problem_transformations.into(), user_interactions: user_interactions.into(), }); @@ -315,9 +334,11 @@ fn Filter() -> impl IntoView { } } + let problems_count = ctx.filtered_problem_transformations.read().iter().map(|set| set.len()).sum::(); + let problems_counter = { let name = view! {

{"Problems:"}

}; - let value = view! {

{ctx.filtered_problems.read().len()}

}; + let value = view! {

{problems_count}

}; view! {
{name} {value} @@ -336,13 +357,15 @@ fn Filter() -> impl IntoView { let mut interaction_counters = InteractionCounters::default(); let interaction_counters_view = { let user_ints = ctx.user_interactions.read(); - for problem in ctx.filtered_problems.read().iter() { - if let Some(user_int) = user_ints.get(problem) { - match user_int.best_attempt().map(|da| da.attempt) { - Some(models::Attempt::Flash) => interaction_counters.flash += 1, - Some(models::Attempt::Send) => interaction_counters.send += 1, - Some(models::Attempt::Attempt) => interaction_counters.attempt += 1, - None => {} + for problem_set in ctx.filtered_problem_transformations.read().iter() { + for problem in problem_set { + if let Some(user_int) = user_ints.get(problem) { + match user_int.best_attempt().map(|da| da.attempt) { + Some(models::Attempt::Flash) => interaction_counters.flash += 1, + Some(models::Attempt::Send) => interaction_counters.send += 1, + Some(models::Attempt::Attempt) => interaction_counters.attempt += 1, + None => {} + } } } } @@ -575,6 +598,7 @@ mod signals { use leptos::prelude::*; use std::collections::BTreeMap; use std::collections::BTreeSet; + use std::collections::HashSet; pub fn latest_attempt(user_interaction: Signal>) -> Signal> { Signal::derive(move || user_interaction.read().as_ref().and_then(models::UserInteraction::latest_attempt)) @@ -600,6 +624,17 @@ mod signals { }) } + /// Maps each problem to a set of problems comprising all transformation of the problem pattern. + pub(crate) fn problem_transformations(wall: Signal) -> Memo>> { + Memo::new(move |_prev_val| { + let wall = wall.read(); + wall.problems + .iter() + .map(|problem| problem.transformations(wall.wall_dimensions)) + .collect() + }) + } + pub(crate) fn filtered_problems( wall: Signal, filter_holds: Signal>, @@ -616,6 +651,28 @@ mod signals { }) } + pub(crate) fn filtered_problem_transformations( + problem_transformations: Signal>>, + filter_holds: Signal>, + ) -> Memo>> { + Memo::new(move |_prev_val| { + let filter_holds = filter_holds.read(); + let problem_transformations = problem_transformations.read(); + + problem_transformations + .iter() + .map(|problem_set| { + problem_set + .iter() + .filter(|problem| filter_holds.iter().all(|hold_pos| problem.pattern.pattern.contains_key(hold_pos))) + .map(|problem| problem.clone()) + .collect::>() + }) + .filter(|set| !set.is_empty()) + .collect() + }) + } + pub(crate) fn hold_role(problem: Signal>, hold_position: models::HoldPosition) -> Signal> { Signal::derive(move || problem.get().and_then(|p| p.pattern.pattern.get(&hold_position).copied())) }