From a8114230b8f884245d9f2d30652d99cc53b32604 Mon Sep 17 00:00:00 2001 From: itsscb Date: Mon, 9 Sep 2024 14:39:19 +0200 Subject: [PATCH] chore: removes old pages feat: adds word struct --- Cargo.lock | 1 + frontend/Cargo.toml | 1 + frontend/src/input.rs | 79 ---- frontend/src/lib.rs | 135 ------- frontend/src/pages/game.rs | 122 ------- frontend/src/pages/home.rs | 639 +-------------------------------- frontend/src/pages/home_old.rs | 639 +++++++++++++++++++++++++++++++++ frontend/src/pages/mod.rs | 2 - src/lib.rs | 2 +- src/model/charstatus.rs | 37 +- src/model/game.rs | 31 +- 11 files changed, 698 insertions(+), 990 deletions(-) delete mode 100644 frontend/src/input.rs delete mode 100644 frontend/src/pages/game.rs create mode 100644 frontend/src/pages/home_old.rs diff --git a/Cargo.lock b/Cargo.lock index f793fea..dd999a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2856,6 +2856,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "wordl", "yew", "yew-router", "yewdux", diff --git a/frontend/Cargo.toml b/frontend/Cargo.toml index af71099..8fccf21 100644 --- a/frontend/Cargo.toml +++ b/frontend/Cargo.toml @@ -20,4 +20,5 @@ gloo-net = "0.6.0" serde = { version = "1.0.209", features = ["derive"] } serde_json = "1.0.127" rand = "0.8.5" +wordl = {path = "../"} diff --git a/frontend/src/input.rs b/frontend/src/input.rs deleted file mode 100644 index b6b8406..0000000 --- a/frontend/src/input.rs +++ /dev/null @@ -1,79 +0,0 @@ -use web_sys::HtmlInputElement; -use yew::prelude::*; - -#[derive(Properties, PartialEq)] -pub struct InputStringProps { - pub value: String, -} - -pub enum Msg { - CharInput(usize, String), -} - -pub struct InputString { - value: String, - nodes: Vec, - focused_index: usize, -} - -impl Component for InputString { - type Message = Msg; - type Properties = InputStringProps; - - fn create(ctx: &Context) -> Self { - let value = ctx.props().value.clone(); - let nodes = vec![NodeRef::default(); value.len()]; - Self { - value, - nodes, - focused_index: 0, - } - } - - fn update(&mut self, _ctx: &Context, msg: Self::Message) -> bool { - match msg { - Msg::CharInput(index, new_char) => { - let mut new_value = self.value.clone(); - new_value.replace_range(index..index + 1, &new_char); - self.value = new_value; - - if index < self.value.len() - 1 { - self.focused_index = index + 1; - if let Some(next_node) = self.nodes.get(self.focused_index) { - if let Some(input) = next_node.cast::() { - input.focus().unwrap(); - } - } - } - true - } - } - } - - fn view(&self, ctx: &Context) -> Html { - let chars = self.value.chars().enumerate().map(|(index, char)| { - let on_input = ctx.link().callback(move |input: InputEvent| { - let new_char = input.data(); - Msg::CharInput(index, new_char.unwrap()) - }); - - html! { - - } - }); - - html! { -
- { for chars } -
- } - } -} diff --git a/frontend/src/lib.rs b/frontend/src/lib.rs index 7ba53f4..ae0b06d 100644 --- a/frontend/src/lib.rs +++ b/frontend/src/lib.rs @@ -1,137 +1,2 @@ pub mod pages; pub mod router; -// pub mod storage; - -// mod input; - -use std::collections::HashMap; - -use serde::{Deserialize, Serialize}; - -#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] -enum CharStatus { - NotContained(T), - Contained(T), - Match(T), - Unknown, -} - -fn compare_strings(s1: &str, s2: &str) -> Vec> { - let mut result: Vec> = Vec::with_capacity(s1.len()); - result.resize_with(s1.len(), || CharStatus::Unknown); - - let mut s1_char_count: HashMap = HashMap::new(); - let mut s2_char_count: HashMap = HashMap::new(); - - for c in s1.chars() { - *s1_char_count.entry(c).or_insert(0) += 1; - } - - for ((c1, c2), res) in s1.chars().zip(s2.chars()).zip(result.iter_mut()) { - if c1 == c2 { - *res = CharStatus::Match(c2.to_string()); - *s2_char_count.entry(c2).or_insert(0) += 1; - } else { - *res = CharStatus::Unknown; - } - } - - for (res, c2) in result.iter_mut().zip(s2.chars()) { - if res == &CharStatus::Unknown { - let c1_count = s1_char_count.get(&c2).unwrap_or(&0); - let c2_count = s2_char_count.get(&c2).unwrap_or(&0); - - if *c1_count > 0 && c1_count > c2_count { - *res = CharStatus::Contained(c2.to_string()); - *s2_char_count.entry(c2).or_insert(0) += 1; - } else { - *res = CharStatus::NotContained(c2.to_string()); - } - } - } - - result -} - -#[cfg(test)] -mod test { - - use super::*; - #[test] - fn test_compare_strings() { - let source = "HALLO"; - - let want = vec![ - CharStatus::NotContained("0".to_owned()), - CharStatus::NotContained("0".to_owned()), - CharStatus::NotContained("0".to_owned()), - CharStatus::NotContained("0".to_owned()), - CharStatus::NotContained("0".to_owned()), - ]; - let input = "00000"; - - let got = compare_strings(source, input); - assert_eq!(want, got); - let source = "HALLO"; - - let want = vec![ - CharStatus::NotContained("L".to_owned()), - CharStatus::NotContained("L".to_owned()), - CharStatus::Match("L".to_owned()), - CharStatus::Match("L".to_owned()), - CharStatus::NotContained("L".to_owned()), - ]; - let input = "LLLLL"; - - let got = compare_strings(source, input); - assert_eq!(want, got); - - let want = vec![ - CharStatus::Match("H".to_owned()), - CharStatus::Match("A".to_owned()), - CharStatus::Match("L".to_owned()), - CharStatus::Match("L".to_owned()), - CharStatus::Match("O".to_owned()), - ]; - let input = "HALLO"; - - let got = compare_strings(source, input); - assert_eq!(want, got); - - let want = vec![ - CharStatus::Match("H".to_owned()), - CharStatus::NotContained("L".to_owned()), - CharStatus::Match("L".to_owned()), - CharStatus::Match("L".to_owned()), - CharStatus::Match("O".to_owned()), - ]; - let input = "HLLLO"; - - let got = compare_strings(source, input); - assert_eq!(want, got); - - let want = vec![ - CharStatus::Match("H".to_owned()), - CharStatus::Contained("L".to_owned()), - CharStatus::Match("L".to_owned()), - CharStatus::NotContained("I".to_owned()), - CharStatus::NotContained("L".to_owned()), - ]; - let input = "HLLIL"; - - let got = compare_strings(source, input); - assert_eq!(want, got); - - let want = vec![ - CharStatus::Contained("L".to_owned()), - CharStatus::NotContained("L".to_owned()), - CharStatus::Match("L".to_owned()), - CharStatus::Contained("A".to_owned()), - CharStatus::Match("O".to_owned()), - ]; - let input = "LLLAO"; - - let got = compare_strings(source, input); - assert_eq!(want, got); - } -} diff --git a/frontend/src/pages/game.rs b/frontend/src/pages/game.rs deleted file mode 100644 index c79d288..0000000 --- a/frontend/src/pages/game.rs +++ /dev/null @@ -1,122 +0,0 @@ -// use rand::seq::SliceRandom; -use serde::{Deserialize, Serialize}; - -use crate::CharStatus; - -const MAX_TRIES: usize = 5; - -// #[derive(Debug, Serialize, Deserialize, Clone)] -// struct Games(Vec); - -// impl Games { -// pub const fn new() -> Self { -// Self(Vec::new()) -// } - -// pub fn new_game(&mut self, word: String) { -// let game = Game::new(); -// self.0.push(game); -// } - -// pub fn current_game(&self) -> Option<&Game> { -// self.0.last() -// } -// } - -#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] -pub struct WordList { - words: Vec, -} - -impl WordList { - pub const fn new() -> Self { - Self { words: Vec::new() } - } - pub fn from_json(s: &str) -> Self { - serde_json::from_str(s).map_or(Self::new(), |w| w) - } - // pub fn to_json(&self) -> String { - // serde_json::to_string_pretty(self).map_or(String::new(), |w| w) - // } - // pub fn get_word(&self) -> String { - // let mut rng = rand::thread_rng(); - // self.words - // .choose(&mut rng) - // .map_or_else(String::new, |w| (*w).to_string()) - // } -} - -#[derive(Debug, Serialize, Deserialize, Clone)] -pub struct Game { - pub word: Option, - pub submitted_words: Vec>>, - tries: usize, - status: Status, -} - -impl Game { - pub const fn new() -> Self { - Self { - word: None, - tries: 0, - submitted_words: Vec::new(), - status: Status::New, - } - } - - pub fn start(&mut self, word: String) { - if self.word.is_none() && self.status == Status::New { - self.status = Status::InProgress; - self.word = Some(word); - } - } - - pub fn submit_answer(&mut self, answer: &[String]) { - if let Some(ref word) = self.word { - let res = crate::compare_strings(word, &answer.join("")); - self.submitted_words.push(res); - self.tries += 1; - self.status = self.current_status(); - } - } - - pub fn current_status(&self) -> Status { - self.word.as_ref().map_or(Status::New, |_| { - let word_count = self.submitted_words.len(); - match self.tries { - 0 => Status::New, - 1..MAX_TRIES => self - .submitted_words - .last() - .map_or(Status::InProgress, |words| { - if words.iter().all(|v| matches!(v, CharStatus::Match(_))) { - Status::Win(word_count) - } else { - Status::InProgress - } - }), - _ => self - .submitted_words - .last() - .map_or(Status::Lose(word_count), |words| { - if words.iter().all(|v| matches!(v, CharStatus::Match(_))) { - Status::Win(word_count) - } else { - Status::Lose(word_count) - } - }), - } - }) - } -} - -type Tries = usize; - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] -#[allow(clippy::module_name_repetitions)] -pub enum Status { - New, - Win(Tries), - Lose(Tries), - InProgress, -} diff --git a/frontend/src/pages/home.rs b/frontend/src/pages/home.rs index 098b474..414c1e4 100644 --- a/frontend/src/pages/home.rs +++ b/frontend/src/pages/home.rs @@ -1,639 +1,10 @@ -use gloo_net::http::Request; -use web_sys::wasm_bindgen::convert::OptionIntoWasmAbi; -use web_sys::wasm_bindgen::JsCast; -use web_sys::HtmlElement; -use yew::prelude::*; -use yew::{classes, function_component, Callback, Html}; - -use crate::pages::game::{Game, Status}; -use crate::CharStatus; - -use super::game::WordList; - -static NEW_WORD_URI: &str = "https://wordl.shuttleapp.rs/word"; -static WORDS_URI: &str = "https://wordl.shuttleapp.rs/public/wordlist.json"; -static MAX_TRIES: usize = 5; - -fn set_focus(index: usize) { - let prefix = match index { - 0 => "", - _ => "-", - }; - if let Some(w) = web_sys::window() { - if let Some(d) = w.document() { - if let Some(n) = d - .query_selector(&format!("[tabindex='{prefix}{index}']")) - .ok() - .flatten() - { - if let Some(e) = n.dyn_ref::() { - e.focus().ok(); - } - } - } - } -} - -fn string_to_html(input: &[CharStatus]) -> Html { - let classes = classes!( - "bg-gray-700", - "w-16", - "h-16", - "text-center", - "py-4", - "font-bold", - "text-lg", - "mb-4", - ); - html! ( -
    - { - input.iter().map(|e|{ - let mut classes = classes.clone(); - - let text = match e { - CharStatus::Match(s) => { - classes.push("bg-green-400"); - s - }, - CharStatus::Contained(s) => { - classes.push("bg-yellow-400"); - s - }, - CharStatus::NotContained(s) => { - classes.push("bg-gray-900"); - classes.push("border-white"); - classes.push("border-2"); - s - } - CharStatus::Unknown => { - "" - }, - }; - html!{ -
  • - - {text} - -
  • - }}).collect::() - } -
- ) -} - -#[allow(clippy::too_many_arguments)] -fn fetch_new_word( - word: &UseStateHandle, - loading: &UseStateHandle, - submitted_words: &UseStateHandle>>>, - input_values: &UseStateHandle>, - game_over: &UseStateHandle, - length: &UseStateHandle, - node_refs: &UseStateHandle>, - result: &UseStateHandle, -) { - let loading = loading.clone(); - let submitted_words = submitted_words.clone(); - let input_values = input_values.clone(); - let game_over = game_over.clone(); - let length = length.clone(); - let node_refs = node_refs.clone(); - let result = result.clone(); - let word = word.clone(); - - wasm_bindgen_futures::spawn_local(async move { - loading.set(true); - let res = Request::get(NEW_WORD_URI).send().await; - if let Ok(r) = res { - if let Ok(w) = r.text().await { - length.set(w.len()); - node_refs.set(vec![NodeRef::default(); w.len()]); - input_values.set(vec![String::new(); w.len()]); - word.set(w.to_uppercase()); - submitted_words.set(Vec::with_capacity(MAX_TRIES)); - game_over.set(false); - result.set(Status::New); - loading.set(false); - } - } - }); -} - -#[allow(dead_code)] -fn fetch_words(state: &UseStateHandle) { - let state = state.clone(); - wasm_bindgen_futures::spawn_local(async move { - let res = Request::get(WORDS_URI).send().await; - if let Ok(r) = res { - if let Ok(w) = r.text().await { - state.set(WordList::from_json(&w)); - } - } - }); -} - -fn new_game(game: &UseStateHandle) { - let game = game.clone(); - wasm_bindgen_futures::spawn_local(async move { - let res = Request::get(NEW_WORD_URI).send().await; - if let Ok(r) = res { - if let Ok(w) = r.text().await { - let mut g = (*game).clone(); - g.start(w); - game.set(g); - } - } - }); -} +use wordl::Game; +use yew::{function_component, html, use_state, Html, UseStateHandle}; #[function_component] pub fn Home() -> Html { - let game: UseStateHandle = use_state(Game::new); - let word: UseStateHandle = use_state(String::new); - let loading: UseStateHandle = use_state(|| true); - let curr_index: UseStateHandle = use_state(|| 0usize); - - let length = use_state(|| 0usize); - let submitted_words: UseStateHandle>>> = - use_state(|| std::vec::Vec::with_capacity(MAX_TRIES)); - - let node_refs = use_state(|| vec![NodeRef::default(); 10]); - let input_values: UseStateHandle> = use_state(|| vec![String::new(); *length]); - let game_over = use_state(|| false); - - let result = use_state(|| Status::New); - - { - let game = game.clone(); - let handle = word.clone(); - let loading = loading.clone(); - - let submitted_words = submitted_words.clone(); - let input_values = input_values.clone(); - let game_over = game_over.clone(); - let length = length.clone(); - let node_refs = node_refs.clone(); - let result = result.clone(); - - use_effect_with((), move |()| { - new_game(&game); - fetch_new_word( - &handle, - &loading, - &submitted_words, - &input_values, - &game_over, - &length, - &node_refs, - &result, - ); - }); + let game: UseStateHandle = use_state(Game::default); + html! { + game.get_submitted_words().iter().map(|c| html!{

{format!("{c:?}")}

}).collect::() } - - let game_over_check = { - let word = word.clone(); - let submitted_words = submitted_words.clone(); - let input_values = input_values.clone(); - let game_over = game_over.clone(); - let length = length.clone(); - let result = result.clone(); - - Callback::from(move |_| { - if submitted_words.iter().count() >= *length - 1 - || crate::compare_strings(&word, &input_values.join("")) - .iter() - .all(|v| matches!(v, CharStatus::Match(_))) - { - if crate::compare_strings(&word, &input_values.join("")) - .iter() - .all(|v| matches!(v, CharStatus::Match(_))) - { - result.set(Status::Win(submitted_words.iter().count())); - } else { - result.set(Status::Lose(MAX_TRIES)); - } - game_over.set(true); - } - }) - }; - - let on_disabled = { - let curr_index = curr_index.clone(); - let input_values = input_values.clone(); - - Callback::from(move |_e: MouseEvent| { - let index = input_values - .iter() - .enumerate() - .find(|(_, v)| v.is_empty()) - .map_or(0, |(i, _)| i); - set_focus(index); - curr_index.set(index); - }) - }; - - let on_submit = { - let game = game.clone(); - - let input_values = input_values.clone(); - let submitted_words = submitted_words.clone(); - let game_over = game_over.clone(); - let length = length.clone(); - let word = word.clone(); - let node_refs = node_refs.clone(); - let loading = loading.clone(); - let result = result.clone(); - let curr_index = curr_index.clone(); - - Callback::from(move |_e: MouseEvent| { - if *game_over { - curr_index.set(0); - let input_values = input_values.clone(); - let submitted_words = submitted_words.clone(); - let game_over = game_over.clone(); - let length = length.clone(); - let word = word.clone(); - let loading = loading.clone(); - let node_refs = node_refs.clone(); - let result = result.clone(); - fetch_new_word( - &word, - &loading, - &submitted_words, - &input_values, - &game_over, - &length, - &node_refs, - &result, - ); - return; - } - let values: Vec<_> = input_values.iter().cloned().collect(); - if !values.iter().all(|v| !v.is_empty()) { - return; - } - let mut g = (*game).clone(); - g.submit_answer(&input_values); - game.set(g); - - let mut new_items = (*submitted_words).clone(); - new_items.push(crate::compare_strings(&word, &values.join(""))); - submitted_words.set(new_items); - input_values.set(vec![String::new(); word.len()]); - set_focus(0); - curr_index.set(0); - game_over_check.emit(MouseEvent::none()); - }) - }; - let on_enter = { - let on_submit = on_submit.clone(); - let curr_index = curr_index.clone(); - let node_refs = node_refs.clone(); - let input_values = input_values.clone(); - let length = length.clone(); - - Callback::from(move |e: KeyboardEvent| match e.key().as_ref() { - "Enter" => { - if let Ok(m) = MouseEvent::new("click") { - on_submit.emit(m); - } - } - "Backspace" => { - e.prevent_default(); - - let mut index = *curr_index; - let mut values = (*input_values).clone(); - - if index >= *length { - curr_index.set(*length - 1); - index = *length - 1; - } - - if node_refs[index] - .cast::() - .is_some() - && index > 0 - { - values[index] = String::new(); - input_values.set(values); - let index = index - 1; - curr_index.set(index); - set_focus(index); - } - } - _ => {} - }) - }; - - let on_input = { - let curr_index = curr_index.clone(); - let length = length.clone(); - let input_values = input_values.clone(); - - Callback::from(move |e: InputEvent| { - if let Some(value) = e.data() { - let value = value.to_uppercase(); - let index = *curr_index; - let mut values = (*input_values).clone(); - - if index >= *length { - values[index - 1] = value; - input_values.set(values); - } else if value.len() < values[index].len() && index > 0 && index <= *length { - values[index] = String::new(); - input_values.set(values); - let new_index = index - 1; - curr_index.set(new_index); - set_focus(new_index); - } else if value.len() == 1 && value.chars().all(char::is_alphabetic) { - values[index] = value; - input_values.set(values); - if index < *length { - let new_index = index + 1; - curr_index.set(new_index); - set_focus(new_index); - } - } else { - values[index] = String::new(); - input_values.set(values); - } - } - }) - }; - - let view = { - move || { - html! { -
- // { - // match game.current_status() { - // Status::New => html!{ - // <> - // - // - // - //

{"Loading..."}

- // - // }, - // Status::Win(tries) => html!{ - //

{format!("WIN: {tries}")}

- // }, - // Status::Lose(tries) => html!{ - //

{format!("LOSE: {tries}")}

- // }, - // Status::InProgress => html!{ - //
- //

{"IN PROGRESS"}

- //

{&game.word}

- //
- // }, - // } - // } - if *loading { - - - -

{"Loading..."}

- } else { - -
- -
- { for submitted_words.iter().map(|e| {string_to_html(e)})} -
-
-
- { - if *game_over { - - let (text, color) = match *result { - Status::Win(_) => { - ("FOUND", "bg-green-600") - }, - Status::Lose(_) => { - ("WANTED", "bg-red-600") - }, - _ => { - ("NEW", "bg-gray-600") - }, - }; - html! ( -
-

{ - text - }

-
    - { - word.chars().map(|e|{ - - let text = e; - html!{ -
  • - - {text} - -
  • - }}).collect::() - } -
-
- ) - } - else if !*game_over { - node_refs.iter().enumerate().map(|(index, node_ref)| { - let on_focus = { - let curr_index = curr_index.clone(); - - Callback::from(move |e: FocusEvent| { - let target = e.target_unchecked_into::(); - if let Some(index) = target.get_attribute("tabindex") { - if let Ok(i) = index.replace('-', "").parse::() { - curr_index.set(i); - } - } - - }) - }; - let prefix = match index { - 0 => String::new(), - _ => "-".to_owned(), - }; - html! { - - } - }).collect::() - } else { - html!(
) - } - } -
-
- { - if *loading { - html!{<>} - } else { - html!{ -
- -
- } - } - } - -
- } -
- } - } - }; - - view() } diff --git a/frontend/src/pages/home_old.rs b/frontend/src/pages/home_old.rs new file mode 100644 index 0000000..098b474 --- /dev/null +++ b/frontend/src/pages/home_old.rs @@ -0,0 +1,639 @@ +use gloo_net::http::Request; +use web_sys::wasm_bindgen::convert::OptionIntoWasmAbi; +use web_sys::wasm_bindgen::JsCast; +use web_sys::HtmlElement; +use yew::prelude::*; +use yew::{classes, function_component, Callback, Html}; + +use crate::pages::game::{Game, Status}; +use crate::CharStatus; + +use super::game::WordList; + +static NEW_WORD_URI: &str = "https://wordl.shuttleapp.rs/word"; +static WORDS_URI: &str = "https://wordl.shuttleapp.rs/public/wordlist.json"; +static MAX_TRIES: usize = 5; + +fn set_focus(index: usize) { + let prefix = match index { + 0 => "", + _ => "-", + }; + if let Some(w) = web_sys::window() { + if let Some(d) = w.document() { + if let Some(n) = d + .query_selector(&format!("[tabindex='{prefix}{index}']")) + .ok() + .flatten() + { + if let Some(e) = n.dyn_ref::() { + e.focus().ok(); + } + } + } + } +} + +fn string_to_html(input: &[CharStatus]) -> Html { + let classes = classes!( + "bg-gray-700", + "w-16", + "h-16", + "text-center", + "py-4", + "font-bold", + "text-lg", + "mb-4", + ); + html! ( +
    + { + input.iter().map(|e|{ + let mut classes = classes.clone(); + + let text = match e { + CharStatus::Match(s) => { + classes.push("bg-green-400"); + s + }, + CharStatus::Contained(s) => { + classes.push("bg-yellow-400"); + s + }, + CharStatus::NotContained(s) => { + classes.push("bg-gray-900"); + classes.push("border-white"); + classes.push("border-2"); + s + } + CharStatus::Unknown => { + "" + }, + }; + html!{ +
  • + + {text} + +
  • + }}).collect::() + } +
+ ) +} + +#[allow(clippy::too_many_arguments)] +fn fetch_new_word( + word: &UseStateHandle, + loading: &UseStateHandle, + submitted_words: &UseStateHandle>>>, + input_values: &UseStateHandle>, + game_over: &UseStateHandle, + length: &UseStateHandle, + node_refs: &UseStateHandle>, + result: &UseStateHandle, +) { + let loading = loading.clone(); + let submitted_words = submitted_words.clone(); + let input_values = input_values.clone(); + let game_over = game_over.clone(); + let length = length.clone(); + let node_refs = node_refs.clone(); + let result = result.clone(); + let word = word.clone(); + + wasm_bindgen_futures::spawn_local(async move { + loading.set(true); + let res = Request::get(NEW_WORD_URI).send().await; + if let Ok(r) = res { + if let Ok(w) = r.text().await { + length.set(w.len()); + node_refs.set(vec![NodeRef::default(); w.len()]); + input_values.set(vec![String::new(); w.len()]); + word.set(w.to_uppercase()); + submitted_words.set(Vec::with_capacity(MAX_TRIES)); + game_over.set(false); + result.set(Status::New); + loading.set(false); + } + } + }); +} + +#[allow(dead_code)] +fn fetch_words(state: &UseStateHandle) { + let state = state.clone(); + wasm_bindgen_futures::spawn_local(async move { + let res = Request::get(WORDS_URI).send().await; + if let Ok(r) = res { + if let Ok(w) = r.text().await { + state.set(WordList::from_json(&w)); + } + } + }); +} + +fn new_game(game: &UseStateHandle) { + let game = game.clone(); + wasm_bindgen_futures::spawn_local(async move { + let res = Request::get(NEW_WORD_URI).send().await; + if let Ok(r) = res { + if let Ok(w) = r.text().await { + let mut g = (*game).clone(); + g.start(w); + game.set(g); + } + } + }); +} + +#[function_component] +pub fn Home() -> Html { + let game: UseStateHandle = use_state(Game::new); + let word: UseStateHandle = use_state(String::new); + let loading: UseStateHandle = use_state(|| true); + let curr_index: UseStateHandle = use_state(|| 0usize); + + let length = use_state(|| 0usize); + let submitted_words: UseStateHandle>>> = + use_state(|| std::vec::Vec::with_capacity(MAX_TRIES)); + + let node_refs = use_state(|| vec![NodeRef::default(); 10]); + let input_values: UseStateHandle> = use_state(|| vec![String::new(); *length]); + let game_over = use_state(|| false); + + let result = use_state(|| Status::New); + + { + let game = game.clone(); + let handle = word.clone(); + let loading = loading.clone(); + + let submitted_words = submitted_words.clone(); + let input_values = input_values.clone(); + let game_over = game_over.clone(); + let length = length.clone(); + let node_refs = node_refs.clone(); + let result = result.clone(); + + use_effect_with((), move |()| { + new_game(&game); + fetch_new_word( + &handle, + &loading, + &submitted_words, + &input_values, + &game_over, + &length, + &node_refs, + &result, + ); + }); + } + + let game_over_check = { + let word = word.clone(); + let submitted_words = submitted_words.clone(); + let input_values = input_values.clone(); + let game_over = game_over.clone(); + let length = length.clone(); + let result = result.clone(); + + Callback::from(move |_| { + if submitted_words.iter().count() >= *length - 1 + || crate::compare_strings(&word, &input_values.join("")) + .iter() + .all(|v| matches!(v, CharStatus::Match(_))) + { + if crate::compare_strings(&word, &input_values.join("")) + .iter() + .all(|v| matches!(v, CharStatus::Match(_))) + { + result.set(Status::Win(submitted_words.iter().count())); + } else { + result.set(Status::Lose(MAX_TRIES)); + } + game_over.set(true); + } + }) + }; + + let on_disabled = { + let curr_index = curr_index.clone(); + let input_values = input_values.clone(); + + Callback::from(move |_e: MouseEvent| { + let index = input_values + .iter() + .enumerate() + .find(|(_, v)| v.is_empty()) + .map_or(0, |(i, _)| i); + set_focus(index); + curr_index.set(index); + }) + }; + + let on_submit = { + let game = game.clone(); + + let input_values = input_values.clone(); + let submitted_words = submitted_words.clone(); + let game_over = game_over.clone(); + let length = length.clone(); + let word = word.clone(); + let node_refs = node_refs.clone(); + let loading = loading.clone(); + let result = result.clone(); + let curr_index = curr_index.clone(); + + Callback::from(move |_e: MouseEvent| { + if *game_over { + curr_index.set(0); + let input_values = input_values.clone(); + let submitted_words = submitted_words.clone(); + let game_over = game_over.clone(); + let length = length.clone(); + let word = word.clone(); + let loading = loading.clone(); + let node_refs = node_refs.clone(); + let result = result.clone(); + fetch_new_word( + &word, + &loading, + &submitted_words, + &input_values, + &game_over, + &length, + &node_refs, + &result, + ); + return; + } + let values: Vec<_> = input_values.iter().cloned().collect(); + if !values.iter().all(|v| !v.is_empty()) { + return; + } + let mut g = (*game).clone(); + g.submit_answer(&input_values); + game.set(g); + + let mut new_items = (*submitted_words).clone(); + new_items.push(crate::compare_strings(&word, &values.join(""))); + submitted_words.set(new_items); + input_values.set(vec![String::new(); word.len()]); + set_focus(0); + curr_index.set(0); + game_over_check.emit(MouseEvent::none()); + }) + }; + let on_enter = { + let on_submit = on_submit.clone(); + let curr_index = curr_index.clone(); + let node_refs = node_refs.clone(); + let input_values = input_values.clone(); + let length = length.clone(); + + Callback::from(move |e: KeyboardEvent| match e.key().as_ref() { + "Enter" => { + if let Ok(m) = MouseEvent::new("click") { + on_submit.emit(m); + } + } + "Backspace" => { + e.prevent_default(); + + let mut index = *curr_index; + let mut values = (*input_values).clone(); + + if index >= *length { + curr_index.set(*length - 1); + index = *length - 1; + } + + if node_refs[index] + .cast::() + .is_some() + && index > 0 + { + values[index] = String::new(); + input_values.set(values); + let index = index - 1; + curr_index.set(index); + set_focus(index); + } + } + _ => {} + }) + }; + + let on_input = { + let curr_index = curr_index.clone(); + let length = length.clone(); + let input_values = input_values.clone(); + + Callback::from(move |e: InputEvent| { + if let Some(value) = e.data() { + let value = value.to_uppercase(); + let index = *curr_index; + let mut values = (*input_values).clone(); + + if index >= *length { + values[index - 1] = value; + input_values.set(values); + } else if value.len() < values[index].len() && index > 0 && index <= *length { + values[index] = String::new(); + input_values.set(values); + let new_index = index - 1; + curr_index.set(new_index); + set_focus(new_index); + } else if value.len() == 1 && value.chars().all(char::is_alphabetic) { + values[index] = value; + input_values.set(values); + if index < *length { + let new_index = index + 1; + curr_index.set(new_index); + set_focus(new_index); + } + } else { + values[index] = String::new(); + input_values.set(values); + } + } + }) + }; + + let view = { + move || { + html! { +
+ // { + // match game.current_status() { + // Status::New => html!{ + // <> + // + // + // + //

{"Loading..."}

+ // + // }, + // Status::Win(tries) => html!{ + //

{format!("WIN: {tries}")}

+ // }, + // Status::Lose(tries) => html!{ + //

{format!("LOSE: {tries}")}

+ // }, + // Status::InProgress => html!{ + //
+ //

{"IN PROGRESS"}

+ //

{&game.word}

+ //
+ // }, + // } + // } + if *loading { + + + +

{"Loading..."}

+ } else { + +
+ +
+ { for submitted_words.iter().map(|e| {string_to_html(e)})} +
+
+
+ { + if *game_over { + + let (text, color) = match *result { + Status::Win(_) => { + ("FOUND", "bg-green-600") + }, + Status::Lose(_) => { + ("WANTED", "bg-red-600") + }, + _ => { + ("NEW", "bg-gray-600") + }, + }; + html! ( +
+

{ + text + }

+
    + { + word.chars().map(|e|{ + + let text = e; + html!{ +
  • + + {text} + +
  • + }}).collect::() + } +
+
+ ) + } + else if !*game_over { + node_refs.iter().enumerate().map(|(index, node_ref)| { + let on_focus = { + let curr_index = curr_index.clone(); + + Callback::from(move |e: FocusEvent| { + let target = e.target_unchecked_into::(); + if let Some(index) = target.get_attribute("tabindex") { + if let Ok(i) = index.replace('-', "").parse::() { + curr_index.set(i); + } + } + + }) + }; + let prefix = match index { + 0 => String::new(), + _ => "-".to_owned(), + }; + html! { + + } + }).collect::() + } else { + html!(
) + } + } +
+
+ { + if *loading { + html!{<>} + } else { + html!{ +
+ +
+ } + } + } + +
+ } +
+ } + } + }; + + view() +} diff --git a/frontend/src/pages/mod.rs b/frontend/src/pages/mod.rs index 111e5e8..84c458e 100644 --- a/frontend/src/pages/mod.rs +++ b/frontend/src/pages/mod.rs @@ -3,5 +3,3 @@ pub use home::Home; mod settings; pub use settings::Settings; - -mod game; diff --git a/src/lib.rs b/src/lib.rs index f5a6d47..f485a35 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,3 @@ mod model; -pub use model::game; +pub use model::game::Game; diff --git a/src/model/charstatus.rs b/src/model/charstatus.rs index 285f378..7bd9294 100644 --- a/src/model/charstatus.rs +++ b/src/model/charstatus.rs @@ -2,6 +2,15 @@ use std::collections::HashMap; use serde::{Deserialize, Serialize}; +#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] +pub struct Word(Vec>); + +impl Word { + pub fn chars(&self) -> Vec> { + self.0.clone() + } +} + #[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)] pub enum CharStatus { NotContained(T), @@ -10,7 +19,7 @@ pub enum CharStatus { Unknown, } -pub(super) fn compare_strings(s1: &str, s2: &str) -> Vec> { +pub(super) fn compare_strings(s1: &str, s2: &str) -> Word { let mut result: Vec> = Vec::with_capacity(s1.len()); result.resize_with(s1.len(), || CharStatus::Unknown); @@ -44,7 +53,7 @@ pub(super) fn compare_strings(s1: &str, s2: &str) -> Vec> { } } - result + Word(result) } #[cfg(test)] @@ -55,74 +64,74 @@ mod test { fn test_compare_strings() { let source = "HALLO"; - let want = vec![ + let want = Word(vec![ CharStatus::NotContained("0".to_owned()), CharStatus::NotContained("0".to_owned()), CharStatus::NotContained("0".to_owned()), CharStatus::NotContained("0".to_owned()), CharStatus::NotContained("0".to_owned()), - ]; + ]); let input = "00000"; let got = compare_strings(source, input); assert_eq!(want, got); let source = "HALLO"; - let want = vec![ + let want = Word(vec![ CharStatus::NotContained("L".to_owned()), CharStatus::NotContained("L".to_owned()), CharStatus::Match("L".to_owned()), CharStatus::Match("L".to_owned()), CharStatus::NotContained("L".to_owned()), - ]; + ]); let input = "LLLLL"; let got = compare_strings(source, input); assert_eq!(want, got); - let want = vec![ + let want = Word(vec![ CharStatus::Match("H".to_owned()), CharStatus::Match("A".to_owned()), CharStatus::Match("L".to_owned()), CharStatus::Match("L".to_owned()), CharStatus::Match("O".to_owned()), - ]; + ]); let input = "HALLO"; let got = compare_strings(source, input); assert_eq!(want, got); - let want = vec![ + let want = Word(vec![ CharStatus::Match("H".to_owned()), CharStatus::NotContained("L".to_owned()), CharStatus::Match("L".to_owned()), CharStatus::Match("L".to_owned()), CharStatus::Match("O".to_owned()), - ]; + ]); let input = "HLLLO"; let got = compare_strings(source, input); assert_eq!(want, got); - let want = vec![ + let want = Word(vec![ CharStatus::Match("H".to_owned()), CharStatus::Contained("L".to_owned()), CharStatus::Match("L".to_owned()), CharStatus::NotContained("I".to_owned()), CharStatus::NotContained("L".to_owned()), - ]; + ]); let input = "HLLIL"; let got = compare_strings(source, input); assert_eq!(want, got); - let want = vec![ + let want = Word(vec![ CharStatus::Contained("L".to_owned()), CharStatus::NotContained("L".to_owned()), CharStatus::Match("L".to_owned()), CharStatus::Contained("A".to_owned()), CharStatus::Match("O".to_owned()), - ]; + ]); let input = "LLLAO"; let got = compare_strings(source, input); diff --git a/src/model/game.rs b/src/model/game.rs index 284fa13..c1f670e 100644 --- a/src/model/game.rs +++ b/src/model/game.rs @@ -1,4 +1,4 @@ -use super::charstatus::{compare_strings, CharStatus}; +use super::charstatus::{compare_strings, CharStatus, Word}; use serde::{Deserialize, Serialize}; type Attempts = usize; @@ -6,7 +6,7 @@ type Attempts = usize; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] pub struct Game { word: Option, - submitted_words: Vec>>, + submitted_words: Vec, max_attempts: Attempts, status: Status, } @@ -47,7 +47,11 @@ impl Game { .submitted_words .last() .map_or(Status::Lose(word_count), |words| { - if words.iter().all(|v| matches!(v, CharStatus::Match(_))) { + if words + .chars() + .iter() + .all(|v| matches!(v, CharStatus::Match(_))) + { Status::Win(word_count) } else if i < self.max_attempts { Status::InProgress @@ -58,6 +62,11 @@ impl Game { } }) } + + #[must_use] + pub fn get_submitted_words(&self) -> Vec { + self.submitted_words.clone() + } } impl Default for Game { @@ -188,6 +197,22 @@ mod test { assert_eq!(got.current_status(), Status::Win(3)); } + #[test] + fn get_submitted_words() { + let word = "hallo"; + let answer = "hello"; + let mut got = Game::default(); + got.start(word); + got.submit_answer(answer); + + let want = vec![compare_strings( + &word.to_uppercase(), + &answer.to_uppercase(), + )]; + + assert_eq!(got.get_submitted_words(), want); + } + fn random_word(len: usize) -> String { let mut rng = rand::thread_rng(); let word: String = iter::repeat(())