feat: sample from transformations

This commit is contained in:
Asger Juul Brunshøj 2025-04-03 23:12:42 +02:00
parent bd8b0fecf1
commit b37386b9e8
3 changed files with 105 additions and 15 deletions

View File

@ -42,13 +42,13 @@ pub mod v4 {
pub problems: BTreeSet<Problem>, pub problems: BTreeSet<Problem>,
} }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Problem { pub struct Problem {
pub pattern: Pattern, pub pattern: Pattern,
pub method: v2::Method, 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 struct Pattern {
pub pattern: BTreeMap<v1::HoldPosition, v1::HoldRole>, pub pattern: BTreeMap<v1::HoldPosition, v1::HoldRole>,
} }
@ -160,7 +160,7 @@ pub mod v2 {
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)] #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash, derive_more::FromStr, derive_more::Display)]
pub struct ProblemUid(pub uuid::Uuid); 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 { pub enum Method {
#[display("Feet follow hands")] #[display("Feet follow hands")]
FeetFollowHands, FeetFollowHands,

View File

@ -2,6 +2,22 @@ use super::*;
use chrono::DateTime; use chrono::DateTime;
use chrono::Utc; use chrono::Utc;
use std::collections::BTreeMap; 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> {
self.pattern
.transformations(wall_dimensions)
.into_iter()
.map(|pattern| Self {
pattern,
method: self.method,
})
.collect()
}
}
impl Pattern { impl Pattern {
#[must_use] #[must_use]
@ -88,6 +104,23 @@ impl Pattern {
pattern pattern
} }
/// Returns all possible transformations for the pattern
#[must_use]
pub fn transformations(&self, wall_dimensions: WallDimensions) -> HashSet<Self> {
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 { impl UserInteraction {

View File

@ -13,6 +13,7 @@ use leptos::prelude::*;
use leptos_router::params::Params; use leptos_router::params::Params;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::collections::HashSet;
use std::ops::Deref; use std::ops::Deref;
#[derive(Params, PartialEq, Clone)] #[derive(Params, PartialEq, Clone)]
@ -60,7 +61,7 @@ struct Context {
wall: Signal<models::Wall>, wall: Signal<models::Wall>,
user_interactions: Signal<BTreeMap<models::Problem, models::UserInteraction>>, user_interactions: Signal<BTreeMap<models::Problem, models::UserInteraction>>,
problem: Signal<Option<models::Problem>>, problem: Signal<Option<models::Problem>>,
filtered_problems: Signal<BTreeSet<models::Problem>>, filtered_problem_transformations: Signal<Vec<HashSet<models::Problem>>>,
user_interaction: Signal<Option<models::UserInteraction>>, user_interaction: Signal<Option<models::UserInteraction>>,
todays_attempt: Signal<Option<models::Attempt>>, todays_attempt: Signal<Option<models::Attempt>>,
latest_attempt: Signal<Option<models::DatedAttempt>>, latest_attempt: Signal<Option<models::DatedAttempt>>,
@ -70,6 +71,9 @@ struct Context {
cb_next_problem: Callback<()>, cb_next_problem: Callback<()>,
cb_set_problem: Callback<models::Problem>, cb_set_problem: Callback<models::Problem>,
cb_upsert_todays_attempt: Callback<server_functions::UpsertTodaysAttempt>, cb_upsert_todays_attempt: Callback<server_functions::UpsertTodaysAttempt>,
#[expect(dead_code)]
filtered_problems: Signal<BTreeSet<models::Problem>>,
} }
#[component] #[component]
@ -93,7 +97,12 @@ fn Controller(
// Derive signals // Derive signals
let user_interaction = signals::user_interaction(user_interactions.into(), problem.into()); 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_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 todays_attempt = signals::todays_attempt(user_interaction);
let latest_attempt = signals::latest_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 // Callback: Set next problem to a random problem
let cb_set_random_problem: Callback<()> = Callback::new(move |_| { let cb_set_random_problem: Callback<()> = Callback::new(move |_| {
// TODO: remove current problem from population // TODO: remove current problem from population
let population = filtered_problems.read(); let population = filtered_problem_transformations.read();
let population = population.deref(); let population = population.deref();
use rand::seq::IteratorRandom; use rand::seq::IteratorRandom;
let mut rng = rand::rng(); 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 // Callback: On click hold, Add/Remove hold position to problem filter
@ -161,6 +179,7 @@ fn Controller(
todays_attempt, todays_attempt,
filter_holds: filter_holds.into(), filter_holds: filter_holds.into(),
filtered_problems: filtered_problems.into(), filtered_problems: filtered_problems.into(),
filtered_problem_transformations: filtered_problem_transformations.into(),
user_interactions: user_interactions.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::<usize>();
let problems_counter = { let problems_counter = {
let name = view! { <p class="mr-4 font-light text-right text-orange-300">{"Problems:"}</p> }; let name = view! { <p class="mr-4 font-light text-right text-orange-300">{"Problems:"}</p> };
let value = view! { <p class="text-white">{ctx.filtered_problems.read().len()}</p> }; let value = view! { <p class="text-white">{problems_count}</p> };
view! { view! {
<div class="grid grid-rows-none gap-x-0.5 gap-y-1 grid-cols-[auto_1fr]"> <div class="grid grid-rows-none gap-x-0.5 gap-y-1 grid-cols-[auto_1fr]">
{name} {value} {name} {value}
@ -336,13 +357,15 @@ fn Filter() -> impl IntoView {
let mut interaction_counters = InteractionCounters::default(); let mut interaction_counters = InteractionCounters::default();
let interaction_counters_view = { let interaction_counters_view = {
let user_ints = ctx.user_interactions.read(); let user_ints = ctx.user_interactions.read();
for problem in ctx.filtered_problems.read().iter() { for problem_set in ctx.filtered_problem_transformations.read().iter() {
if let Some(user_int) = user_ints.get(problem) { for problem in problem_set {
match user_int.best_attempt().map(|da| da.attempt) { if let Some(user_int) = user_ints.get(problem) {
Some(models::Attempt::Flash) => interaction_counters.flash += 1, match user_int.best_attempt().map(|da| da.attempt) {
Some(models::Attempt::Send) => interaction_counters.send += 1, Some(models::Attempt::Flash) => interaction_counters.flash += 1,
Some(models::Attempt::Attempt) => interaction_counters.attempt += 1, Some(models::Attempt::Send) => interaction_counters.send += 1,
None => {} Some(models::Attempt::Attempt) => interaction_counters.attempt += 1,
None => {}
}
} }
} }
} }
@ -575,6 +598,7 @@ mod signals {
use leptos::prelude::*; use leptos::prelude::*;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use std::collections::BTreeSet; use std::collections::BTreeSet;
use std::collections::HashSet;
pub fn latest_attempt(user_interaction: Signal<Option<models::UserInteraction>>) -> Signal<Option<models::DatedAttempt>> { pub fn latest_attempt(user_interaction: Signal<Option<models::UserInteraction>>) -> Signal<Option<models::DatedAttempt>> {
Signal::derive(move || user_interaction.read().as_ref().and_then(models::UserInteraction::latest_attempt)) 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<models::Wall>) -> Memo<Vec<HashSet<models::Problem>>> {
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( pub(crate) fn filtered_problems(
wall: Signal<models::Wall>, wall: Signal<models::Wall>,
filter_holds: Signal<BTreeSet<models::HoldPosition>>, filter_holds: Signal<BTreeSet<models::HoldPosition>>,
@ -616,6 +651,28 @@ mod signals {
}) })
} }
pub(crate) fn filtered_problem_transformations(
problem_transformations: Signal<Vec<HashSet<models::Problem>>>,
filter_holds: Signal<BTreeSet<models::HoldPosition>>,
) -> Memo<Vec<HashSet<models::Problem>>> {
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::<HashSet<models::Problem>>()
})
.filter(|set| !set.is_empty())
.collect()
})
}
pub(crate) fn hold_role(problem: Signal<Option<models::Problem>>, hold_position: models::HoldPosition) -> Signal<Option<models::HoldRole>> { pub(crate) fn hold_role(problem: Signal<Option<models::Problem>>, hold_position: models::HoldPosition) -> Signal<Option<models::HoldRole>> {
Signal::derive(move || problem.get().and_then(|p| p.pattern.pattern.get(&hold_position).copied())) Signal::derive(move || problem.get().and_then(|p| p.pattern.pattern.get(&hold_position).copied()))
} }