Refactor the board game

main
kirbylife 2023-09-29 14:08:23 -06:00
parent 8387179bd0
commit d521d69eac
15 changed files with 2033 additions and 130 deletions

1310
Cargo.lock generated 100644

File diff suppressed because it is too large Load Diff

View File

@ -13,4 +13,5 @@ log = "0.4.20"
rand = { version = "0.8.5", default-features = false, features = ["alloc"] }
wasm-bindgen = "0.2.87"
wasm-logger = "0.2.0"
web-sys = "0.3.64"
yew = { version = "0.20.0", features = ["csr"] }

View File

@ -3,7 +3,9 @@
<head>
<meta charset="UTF-8">
<title>Yew App</title>
<link rel="stylesheet" href="https://maxst.icons8.com/vue-static/landings/line-awesome/line-awesome/1.3.0/css/line-awesome.min.css">
<link data-trunk rel="css" href="static/css/styles.css" />
<link data-trunk rel="css" href="static/css/normalize.css" />
</head>
<body>
<h1>Hola mundo</h1>

View File

@ -1,18 +1,26 @@
use crate::consts::{AttemptField, Status};
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct AttemptRowProps {
pub answer: AttrValue,
pub text: AttrValue,
pub attempt: Vec<AttemptField>,
}
#[function_component]
pub fn AttemptRow(props: &AttemptRowProps) -> Html {
let AttemptRowProps { answer, text } = props;
let AttemptRowProps { attempt } = props;
html! {
{ for answer.chars().zip(text.chars()).map(|(ans, att)| html! { <p class={
if ans == att { "correct" } else if answer.contains(att) { "almost" } else { "missed" }
} >{ att }</p> }) }
{ for attempt.iter().map(|att| html_nested! {
<p
class={
match att.status {
Status::Found => "correct",
Status::Almost => "almost",
Status::NotFound => "missed",
}
}
>{ att.char_field }</p>
})}
}
}

View File

@ -0,0 +1,37 @@
use yew::prelude::*;
use crate::components::{EmptyRow, CurrentRow, AttemptRow};
use crate::consts::{Attempts, MAX_ATTEMPTS};
#[derive(Properties, PartialEq)]
pub struct BoardProps {
pub attempts: Attempts,
pub attempt_index: usize,
pub current_attempt: AttrValue,
pub onkeypress: Callback<KeyboardEvent>,
}
#[function_component]
pub fn Board(props: &BoardProps) -> Html {
let BoardProps {
attempts,
attempt_index,
current_attempt,
onkeypress
} = props;
html! {
<div class="board" onkeyup={onkeypress} tabindex="0">
{
for (0..MAX_ATTEMPTS).map(|i| {
if i < *attempt_index {
html! { <AttemptRow attempt={ attempts.fields[i].clone() } /> }
} else if i == *attempt_index {
html! { <CurrentRow text={ current_attempt } /> }
} else {
html! { <EmptyRow /> }
}
})
}
</div>
}
}

View File

@ -0,0 +1,31 @@
use crate::consts::{Key, KeyboardKeyType, VirtualKey};
use yew::prelude::*;
#[derive(Properties, PartialEq)]
pub struct KeyboardProps {
pub onclick: Callback<Key>,
pub keyboard: UseStateHandle<Vec<VirtualKey>>,
}
#[function_component]
pub fn Keyboard(props: &KeyboardProps) -> Html {
let handle_click = |k: Key| {
let onclick = props.onclick.clone();
Callback::from(move |_| {
onclick.emit(k);
}).clone()
};
html! {
<div class="keyboard"> {
for (props.keyboard).iter().map(|vk: &VirtualKey| {
match vk.key {
KeyboardKeyType::CharKey(c) => html! { <button onclick={ handle_click(Key::CharKey(c)) }> { c } </button> },
KeyboardKeyType::Backspace => html! { <button onclick={ handle_click(Key::Backspace) } class="key-big"><i class="las la-undo"></i></button> },
KeyboardKeyType::Enter => html! { <button onclick={ handle_click(Key::Enter) } class="key-big"> { "Enviar" } </button> }
}
})
} </div>
}
}

View File

@ -1,9 +1,13 @@
pub use crate::components::attempt_row::AttemptRow;
pub use crate::components::current_row::CurrentRow;
pub use crate::components::empty_row::EmptyRow;
pub use crate::components::keyboard::Keyboard;
pub use crate::components::result_board::ResultBoard;
pub use crate::components::board::Board;
mod board;
mod attempt_row;
mod current_row;
mod empty_row;
mod keyboard;
mod result_board;

View File

@ -1,33 +1,31 @@
use crate::consts::GameResult;
use yew::prelude::*;
use crate::consts::{Attempts, Status};
#[derive(Properties, PartialEq)]
pub struct ResultBoardProps {
pub answer: AttrValue,
pub attempts: Vec<String>,
pub attempts: Attempts,
}
#[function_component]
pub fn ResultBoard(props: &ResultBoardProps) -> Html {
let ResultBoardProps { answer, attempts } = props;
let answer_chars = answer.chars();
let mut result = String::default();
let ResultBoardProps {
attempts,
} = props;
html! {
<div class="result-board"> {
for attempts.iter().map(|attempt| {
html! {
<> {
for answer.chars().zip(attempt.chars()).map(| (ans, att)| {
if ans == att {
html! { <span> { "🟩" } </span> }
} else if answer.contains(att) {
html! { <span>{ "🟨" }</span> }
} else {
html! { <span>{ "" }</span> }
for attempts.fields.iter().map(|attempt| {
html_nested! {
<p class={ "result-board" }> {
for attempt.iter().map(| att| {
match att.status {
Status::Found => html_nested! { <> { "🟩" } </> },
Status::Almost => html_nested! { <span>{ "🟨" }</span> },
Status::NotFound => html_nested! { <span>{ "" }</span> }
}
})
} </>
} </p>
}
})
} </div>

View File

@ -1,10 +1,66 @@
pub const MAX_ATTEMPTS: usize = 6;
use yew::html::IntoPropValue;
pub enum Key {
pub const MAX_ATTEMPTS: usize = 6;
pub const WORD_LEN: usize = 5;
#[derive(Clone, PartialEq)]
pub enum KeyboardKeyType {
Enter,
// 13
Backspace,
CharKey(char),
}
#[derive(Clone, PartialEq)]
pub enum Status {
NotFound,
Found,
Almost,
}
#[derive(Clone, PartialEq)]
pub struct AttemptField {
pub char_field: char,
pub status: Status,
}
#[derive(Clone, PartialEq)]
pub struct Attempts {
pub fields: Vec<Vec<AttemptField>>,
}
impl Attempts {
pub fn new() -> Self {
Attempts {
fields: vec![]
}
}
}
#[derive(Clone, PartialEq)]
pub struct VirtualKey {
pub key: KeyboardKeyType,
pub status: Option<Status>,
}
impl VirtualKey {
pub fn from_charkey(charkey: char) -> Self {
VirtualKey {
key: KeyboardKeyType::CharKey(charkey),
status: None,
}
}
pub fn from_key(key: KeyboardKeyType) -> Self {
VirtualKey { key, status: None }
}
}
#[derive(Copy, Clone)]
pub enum Key {
// 13
Enter,
// 8
Backspace,
CharKey(char),
Ignored,
}
@ -21,7 +77,8 @@ impl From<u32> for Key {
}
}
pub enum Result {
#[derive(PartialEq, Copy, Clone)]
pub enum GameResult {
Win,
Fail,
}

4
src/hooks/mod.rs 100644
View File

@ -0,0 +1,4 @@
pub use use_board::use_board;
pub use use_board::UseBoardHandle;
mod use_board;

View File

@ -0,0 +1,114 @@
use crate::consts::{AttemptField, Attempts, GameResult, Key, VirtualKey, MAX_ATTEMPTS};
use crate::services::words::{get_word_of_the_day, WORDS};
use crate::utils::{evaluate_status, new_empty_virtual_keyboard};
use std::cell::RefCell;
use std::rc::Rc;
use yew::prelude::*;
type AttemptRow = Attempts;
#[derive(PartialEq, Clone)]
pub struct UseBoardHandle {
pub current_attempt: UseStateHandle<String>,
pub attempts: UseStateHandle<AttemptRow>,
pub virtual_keyboard: UseStateHandle<Vec<VirtualKey>>,
pub result: UseStateHandle<Option<GameResult>>,
pub attempt_index: Rc<RefCell<usize>>,
answer: Rc<String>,
}
impl UseBoardHandle {
pub fn send_key(&self, k: Key) {
let current_attempt_len = self.current_attempt.chars().count();
if self.result.is_some() {
return;
}
match k {
Key::CharKey(c) => {
if current_attempt_len >= 5 {
return;
}
let mut new_attempt = (*self.current_attempt).clone();
new_attempt.push(c);
self.current_attempt.set(new_attempt);
}
Key::Backspace => {
if current_attempt_len == 0 {
return;
}
let mut new_attempt = (*self.current_attempt).clone();
// This unwrap is safe because the potential failura it's already covered
new_attempt.pop().unwrap();
self.current_attempt.set(new_attempt);
}
Key::Enter => {
if current_attempt_len < 5 {
return;
}
if !WORDS.contains(&*self.current_attempt) {
return;
}
let mut new_attempts = (*self.attempts).clone();
let new_attempt_row = (*self.current_attempt)
.clone()
.chars()
.zip((*self.answer).chars())
.map(|(att, ans)| {
let status = evaluate_status(ans, att, (*self.answer).clone());
AttemptField {
char_field: att,
status,
}
}).collect();
new_attempts.fields.push(new_attempt_row);
self.attempts.set(new_attempts);
*self.attempt_index.borrow_mut() += 1;
if (*self.current_attempt) == (*self.answer) {
self.result.set(Some(GameResult::Win));
} else if *self.attempt_index.borrow() == MAX_ATTEMPTS {
self.result.set(Some(GameResult::Fail));
}
self.current_attempt.set(String::default());
}
_ => {}
}
}
}
#[hook]
pub fn use_board() -> UseBoardHandle {
let current_attempt = use_state(|| "".to_string());
let attempts: UseStateHandle<Attempts> = use_state(|| Attempts::new());
let attempt_index = use_mut_ref(|| 0usize);
// let answer = use_memo(|_| get_word_of_the_day(), None::<()>);
let answer = use_memo(|_| "ABEJA".to_owned(), None::<()>);
let virtual_keyboard = use_state(|| new_empty_virtual_keyboard().into());
let result = use_state(|| None::<GameResult>);
let send_key = {
let current_attempt = current_attempt.clone();
let current_attempt_len = current_attempt.chars().count();
let attempts = attempts.clone();
let answer = (*answer).clone();
let attempt_index = attempt_index.clone();
let result = result.clone();
move |k: Key| {}
};
UseBoardHandle {
current_attempt,
attempts,
attempt_index,
virtual_keyboard,
result,
answer,
}
}

View File

@ -1,132 +1,62 @@
use yew::prelude::*;
use crate::components::AttemptRow;
use crate::components::{AttemptRow, Board};
use crate::components::CurrentRow;
use crate::components::EmptyRow;
use crate::components::Keyboard;
use crate::components::ResultBoard;
use crate::consts::Key;
use crate::consts::Result;
use crate::consts::MAX_ATTEMPTS;
use crate::services::words::{get_all_words, get_word_of_the_day};
use chrono::Datelike;
use crate::consts::{Key, MAX_ATTEMPTS};
use crate::hooks::use_board;
use gloo::console;
mod components;
mod consts;
mod hooks;
mod services;
mod utils;
#[function_component]
fn App() -> Html {
let answer = use_state(|| get_word_of_the_day());
let all_the_words = use_memo(|_| get_all_words(), 0);
let attempts: UseStateHandle<Vec<String>> = use_state(|| vec![String::default(); MAX_ATTEMPTS]);
let attempt_index = use_state(|| 0usize);
let current_attempt = use_state(|| String::new());
let result = use_state(|| None::<Result>);
let board = use_board();
let onkeypress = {
console::log!(chrono::Utc::now().year());
let current = current_attempt.clone();
let answer = answer.clone();
let answer_str = answer.clone().to_string();
let current_str = current.clone().to_string();
let attempts = attempts.clone();
let result = result.clone();
let index = attempt_index.clone();
move |event: KeyboardEvent| {
console::log!(&event);
if result.is_some() {
return {};
}
let board = board.clone();
Callback::from(move |event: KeyboardEvent| {
let key_code = event.key_code();
board.send_key(Key::from(key_code));
console::log!(&event);
})
};
match Key::from(key_code) {
Key::CharKey(c) => {
if current.chars().count() >= 5 {
return ();
}
let onclick = {
let board = board.clone();
let mut new_attempt = current_str.clone();
new_attempt.push(c);
console::log!(&new_attempt);
current.set(new_attempt);
}
Key::Backspace => {
if current.is_empty() {
return ();
}
let mut new_attempt = current_str.clone();
// This unwrap is safe because the potential failura it's already covered
// by the upper if
new_attempt.pop().unwrap();
console::log!(&new_attempt);
current.set(new_attempt);
}
Key::Enter => {
if current.chars().count() != 5 {
return;
}
if !all_the_words.contains(&current) {
return;
}
let mut new_attempts = attempts.clone().to_vec();
new_attempts[*index] = current_str.clone();
attempts.set(new_attempts);
current.set(String::default());
index.set(*index + 1);
if &current_str == &answer_str {
result.set(Some(Result::Win));
} else if *index == MAX_ATTEMPTS {
result.set(Some(Result::Fail));
} else {
}
}
_ => {}
}
}
Callback::from(move |key| {
board.send_key(key);
})
};
html! {
<>
<h1>{ "Worsdle" }</h1>
<div class="board" onkeyup={onkeypress} tabindex="0">
{
for attempts.clone().iter().enumerate().map(|(i, attempt)| {
let current = current_attempt.clone().to_string();
let attempt = attempt.clone();
if i < *attempt_index {
let answer = answer.clone().to_string();
html! { <AttemptRow text={attempt} answer={answer} /> }
} else if i == *attempt_index {
html! { <CurrentRow text={ current } /> }
} else {
html! { <EmptyRow /> }
}
})
}
<Board
attempts={ (*board.attempts).clone() }
attempt_index={ *board.attempt_index.borrow() }
current_attempt={ (*board.current_attempt).clone() }
onkeypress={ onkeypress }
/>
<div>
<span> {
match (*board.result).clone() {
Some(res) => html! { <ResultBoard attempts={(*board.attempts).clone()} /> },
_ => html! { <Keyboard
keyboard={ (board.virtual_keyboard).clone() }
onclick={ onclick } /> }
}
} </span>
</div>
<div class={ format!("result {}", if result.is_some() { "" } else { "hidden" }) } >
<span> {
match *result {
Some(Result::Win) => "Ganaste!!",
Some(Result::Fail) => "Perdiste :c",
_ => "No deberías ver esto"
}
} </span>
< ResultBoard answer={ answer.to_string().clone() } attempts={(*attempts).clone()} />
<p> { "Vuelve mañana para otro desafío :)" } </p>
</div>
</>
}
}

45
src/utils.rs 100644
View File

@ -0,0 +1,45 @@
use crate::consts::{KeyboardKeyType, Status, VirtualKey};
pub fn new_empty_virtual_keyboard() -> [VirtualKey; 29] {
[
VirtualKey::from_charkey('Q'),
VirtualKey::from_charkey('W'),
VirtualKey::from_charkey('E'),
VirtualKey::from_charkey('R'),
VirtualKey::from_charkey('T'),
VirtualKey::from_charkey('Y'),
VirtualKey::from_charkey('U'),
VirtualKey::from_charkey('I'),
VirtualKey::from_charkey('O'),
VirtualKey::from_charkey('P'),
VirtualKey::from_charkey('A'),
VirtualKey::from_charkey('S'),
VirtualKey::from_charkey('D'),
VirtualKey::from_charkey('F'),
VirtualKey::from_charkey('G'),
VirtualKey::from_charkey('H'),
VirtualKey::from_charkey('J'),
VirtualKey::from_charkey('K'),
VirtualKey::from_charkey('L'),
VirtualKey::from_charkey('Ñ'),
VirtualKey::from_key(KeyboardKeyType::Backspace),
VirtualKey::from_charkey('Z'),
VirtualKey::from_charkey('X'),
VirtualKey::from_charkey('C'),
VirtualKey::from_charkey('V'),
VirtualKey::from_charkey('B'),
VirtualKey::from_charkey('N'),
VirtualKey::from_charkey('M'),
VirtualKey::from_key(KeyboardKeyType::Enter),
]
}
pub fn evaluate_status(ans: char, attr: char, answer: String) -> Status {
if ans == attr {
Status::Found
} else if answer.contains(attr) {
Status::Almost
} else {
Status::NotFound
}
}

349
static/css/normalize.css vendored 100644
View File

@ -0,0 +1,349 @@
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
/* Document
========================================================================== */
/**
* 1. Correct the line height in all browsers.
* 2. Prevent adjustments of font size after orientation changes in iOS.
*/
html {
line-height: 1.15; /* 1 */
-webkit-text-size-adjust: 100%; /* 2 */
}
/* Sections
========================================================================== */
/**
* Remove the margin in all browsers.
*/
body {
margin: 0;
}
/**
* Render the `main` element consistently in IE.
*/
main {
display: block;
}
/**
* Correct the font size and margin on `h1` elements within `section` and
* `article` contexts in Chrome, Firefox, and Safari.
*/
h1 {
font-size: 2em;
margin: 0.67em 0;
}
/* Grouping content
========================================================================== */
/**
* 1. Add the correct box sizing in Firefox.
* 2. Show the overflow in Edge and IE.
*/
hr {
box-sizing: content-box; /* 1 */
height: 0; /* 1 */
overflow: visible; /* 2 */
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
pre {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/* Text-level semantics
========================================================================== */
/**
* Remove the gray background on active links in IE 10.
*/
a {
background-color: transparent;
}
/**
* 1. Remove the bottom border in Chrome 57-
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
*/
abbr[title] {
border-bottom: none; /* 1 */
text-decoration: underline; /* 2 */
text-decoration: underline dotted; /* 2 */
}
/**
* Add the correct font weight in Chrome, Edge, and Safari.
*/
b,
strong {
font-weight: bolder;
}
/**
* 1. Correct the inheritance and scaling of font size in all browsers.
* 2. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp {
font-family: monospace, monospace; /* 1 */
font-size: 1em; /* 2 */
}
/**
* Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/**
* Prevent `sub` and `sup` elements from affecting the line height in
* all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/* Embedded content
========================================================================== */
/**
* Remove the border on images inside links in IE 10.
*/
img {
border-style: none;
}
/* Forms
========================================================================== */
/**
* 1. Change the font styles in all browsers.
* 2. Remove the margin in Firefox and Safari.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit; /* 1 */
font-size: 100%; /* 1 */
line-height: 1.15; /* 1 */
margin: 0; /* 2 */
}
/**
* Show the overflow in IE.
* 1. Show the overflow in Edge.
*/
button,
input { /* 1 */
overflow: visible;
}
/**
* Remove the inheritance of text transform in Edge, Firefox, and IE.
* 1. Remove the inheritance of text transform in Firefox.
*/
button,
select { /* 1 */
text-transform: none;
}
/**
* Correct the inability to style clickable types in iOS and Safari.
*/
button,
[type="button"],
[type="reset"],
[type="submit"] {
-webkit-appearance: button;
}
/**
* Remove the inner border and padding in Firefox.
*/
button::-moz-focus-inner,
[type="button"]::-moz-focus-inner,
[type="reset"]::-moz-focus-inner,
[type="submit"]::-moz-focus-inner {
border-style: none;
padding: 0;
}
/**
* Restore the focus styles unset by the previous rule.
*/
button:-moz-focusring,
[type="button"]:-moz-focusring,
[type="reset"]:-moz-focusring,
[type="submit"]:-moz-focusring {
outline: 1px dotted ButtonText;
}
/**
* Correct the padding in Firefox.
*/
fieldset {
padding: 0.35em 0.75em 0.625em;
}
/**
* 1. Correct the text wrapping in Edge and IE.
* 2. Correct the color inheritance from `fieldset` elements in IE.
* 3. Remove the padding so developers are not caught out when they zero out
* `fieldset` elements in all browsers.
*/
legend {
box-sizing: border-box; /* 1 */
color: inherit; /* 2 */
display: table; /* 1 */
max-width: 100%; /* 1 */
padding: 0; /* 3 */
white-space: normal; /* 1 */
}
/**
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
*/
progress {
vertical-align: baseline;
}
/**
* Remove the default vertical scrollbar in IE 10+.
*/
textarea {
overflow: auto;
}
/**
* 1. Add the correct box sizing in IE 10.
* 2. Remove the padding in IE 10.
*/
[type="checkbox"],
[type="radio"] {
box-sizing: border-box; /* 1 */
padding: 0; /* 2 */
}
/**
* Correct the cursor style of increment and decrement buttons in Chrome.
*/
[type="number"]::-webkit-inner-spin-button,
[type="number"]::-webkit-outer-spin-button {
height: auto;
}
/**
* 1. Correct the odd appearance in Chrome and Safari.
* 2. Correct the outline style in Safari.
*/
[type="search"] {
-webkit-appearance: textfield; /* 1 */
outline-offset: -2px; /* 2 */
}
/**
* Remove the inner padding in Chrome and Safari on macOS.
*/
[type="search"]::-webkit-search-decoration {
-webkit-appearance: none;
}
/**
* 1. Correct the inability to style clickable types in iOS and Safari.
* 2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button; /* 1 */
font: inherit; /* 2 */
}
/* Interactive
========================================================================== */
/*
* Add the correct display in Edge, IE 10+, and Firefox.
*/
details {
display: block;
}
/*
* Add the correct display in all browsers.
*/
summary {
display: list-item;
}
/* Misc
========================================================================== */
/**
* Add the correct display in IE 10+.
*/
template {
display: none;
}
/**
* Add the correct display in IE 10.
*/
[hidden] {
display: none;
}

View File

@ -82,3 +82,16 @@ body {
width: 20px;
}
.keyboard {
display: grid;
grid-template-columns: repeat(20, 1fr);
}
.keyboard > button {
grid-column: span 2;
}
.keyboard > button.key-big {
grid-column: span 3;
}