diff --git a/Cargo.toml b/Cargo.toml index 454e455c9a5..5180b602f93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "examples/counter", "examples/dyn_create_destroy_apps", "examples/file_upload", + "examples/function_memory_game", "examples/function_todomvc", "examples/futures", "examples/game_of_life", diff --git a/examples/function_memory_game/Cargo.toml b/examples/function_memory_game/Cargo.toml new file mode 100644 index 00000000000..cbcc4c83714 --- /dev/null +++ b/examples/function_memory_game/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "function_memory_game" +version = "0.1.0" +authors = ["Howard.Zuo "] +edition = "2018" +license = "MIT OR Apache-2.0" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +strum = "0.23" +strum_macros = "0.23" +gloo = "0.4" +nanoid = "0.4" +rand = "0.8" +getrandom = { version = "0.2", features = ["js"] } +yew = { path = "../../packages/yew" } + +[dependencies.web-sys] +version = "0.3" +features = [ + "HtmlInputElement", +] diff --git a/examples/function_memory_game/README.md b/examples/function_memory_game/README.md new file mode 100644 index 00000000000..1ee70893618 --- /dev/null +++ b/examples/function_memory_game/README.md @@ -0,0 +1,15 @@ +# Memory Game Example + +[![Demo](https://img.shields.io/website?label=demo&url=https%3A%2F%2Fexamples.yew.rs%2Ffunction_memory_game)](https://examples.yew.rs/function_memory_game) + +This is an implementation of [Memory Game](https://github.com/bradlygreen/Memory-Game) for Yew using function components and hooks. + +## Concepts + +- Uses [`function_components`](https://yew.rs/docs/next/concepts/function-components) +- Uses [`gloo::storage`](https://docs.rs/gloo-storage/0.2.0/gloo_storage/index.html) to persist the state +- Uses [`gloo::timers`](https://docs.rs/gloo-timers/0.2.2/gloo_timers/index.html) to schedule asynchronous callback + +## Note + +Images are authorized by [@bradlygreen](https://github.com/bradlygreen), see [authorization-issue](https://github.com/bradlygreen/Memory-Game/issues/6) \ No newline at end of file diff --git a/examples/function_memory_game/index.html b/examples/function_memory_game/index.html new file mode 100644 index 00000000000..4db6cd60bf1 --- /dev/null +++ b/examples/function_memory_game/index.html @@ -0,0 +1,15 @@ + + + + + + + Yew • Function Memory Game + + + + + + + + diff --git a/examples/function_memory_game/public/8-ball.png b/examples/function_memory_game/public/8-ball.png new file mode 100644 index 00000000000..bb0edfd1596 Binary files /dev/null and b/examples/function_memory_game/public/8-ball.png differ diff --git a/examples/function_memory_game/public/back.png b/examples/function_memory_game/public/back.png new file mode 100644 index 00000000000..0ee6dff8755 Binary files /dev/null and b/examples/function_memory_game/public/back.png differ diff --git a/examples/function_memory_game/public/baked-potato.png b/examples/function_memory_game/public/baked-potato.png new file mode 100644 index 00000000000..9f3bc12f936 Binary files /dev/null and b/examples/function_memory_game/public/baked-potato.png differ diff --git a/examples/function_memory_game/public/dinosaur.png b/examples/function_memory_game/public/dinosaur.png new file mode 100644 index 00000000000..878def3e482 Binary files /dev/null and b/examples/function_memory_game/public/dinosaur.png differ diff --git a/examples/function_memory_game/public/favicon.ico b/examples/function_memory_game/public/favicon.ico new file mode 100644 index 00000000000..16eb2a0ac5b Binary files /dev/null and b/examples/function_memory_game/public/favicon.ico differ diff --git a/examples/function_memory_game/public/kronos.png b/examples/function_memory_game/public/kronos.png new file mode 100644 index 00000000000..6ec6e74d106 Binary files /dev/null and b/examples/function_memory_game/public/kronos.png differ diff --git a/examples/function_memory_game/public/rocket.png b/examples/function_memory_game/public/rocket.png new file mode 100644 index 00000000000..b20e848b3c2 Binary files /dev/null and b/examples/function_memory_game/public/rocket.png differ diff --git a/examples/function_memory_game/public/skinny-unicorn.png b/examples/function_memory_game/public/skinny-unicorn.png new file mode 100644 index 00000000000..d207e1c3415 Binary files /dev/null and b/examples/function_memory_game/public/skinny-unicorn.png differ diff --git a/examples/function_memory_game/public/that-guy.png b/examples/function_memory_game/public/that-guy.png new file mode 100644 index 00000000000..b8336f2d332 Binary files /dev/null and b/examples/function_memory_game/public/that-guy.png differ diff --git a/examples/function_memory_game/public/zeppelin.png b/examples/function_memory_game/public/zeppelin.png new file mode 100644 index 00000000000..e8abe79f7ea Binary files /dev/null and b/examples/function_memory_game/public/zeppelin.png differ diff --git a/examples/function_memory_game/scss/chess_board.scss b/examples/function_memory_game/scss/chess_board.scss new file mode 100644 index 00000000000..0350ec9c28e --- /dev/null +++ b/examples/function_memory_game/scss/chess_board.scss @@ -0,0 +1,28 @@ +.chess-board { + margin-top: 20px; + width: 100%; + background-color: #fff; + height: 530px; + border-radius: 4px; + padding: 10px 5px; + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + align-content: space-around; + .chess-board-card-container:nth-child(4n) { + margin-right: 0px; + } +} + +@media screen and (max-width: 450px) { + .chess-board { + height: 480px; + padding: 10px 0px; + } +} +@media screen and (max-width: 370px) { + .chess-board { + height: 450px; + } +} diff --git a/examples/function_memory_game/scss/chess_board_card.scss b/examples/function_memory_game/scss/chess_board_card.scss new file mode 100644 index 00000000000..f467d2903ea --- /dev/null +++ b/examples/function_memory_game/scss/chess_board_card.scss @@ -0,0 +1,61 @@ +.chess-board-card-container { + width: 100px; + height: 121px; + margin-right: 3px; + cursor: pointer; + position: relative; + perspective: 800px; + + .card { + width: 100%; + height: 100%; + transition: transform 1s; + transform-style: preserve-3d; + } + + .card.flipped { + transform: rotateY(180deg); + } + + .card img { + display: block; + height: 100%; + width: 100%; + position: absolute; + backface-visibility: hidden; + } + + .card .back { + background: blue; + transform: rotateY(0deg); + } + + .card .front { + background: blue; + transform: rotateY(180deg); + } +} + +@media screen and (max-width: 450px) { + .chess-board-card-container { + width: 92px; + height: 111px; + margin-right: 1px; + } +} + +@media screen and (max-width: 395px) { + .chess-board-card-container { + width: 85px; + height: 102px; + margin-right: 1px; + } +} + +@media screen and (max-width: 360px) { + .chess-board-card-container { + width: 70px; + height: 84px; + margin-right: 1px; + } +} diff --git a/examples/function_memory_game/scss/game_progress.scss b/examples/function_memory_game/scss/game_progress.scss new file mode 100644 index 00000000000..f3e4d146820 --- /dev/null +++ b/examples/function_memory_game/scss/game_progress.scss @@ -0,0 +1,48 @@ +.game-progress { + width: 120px; + height: 100px; + padding: 10px; + background-color: #bbada0; + border-radius: 5px; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + color: #eae0d1; + + span { + font-size: 19px; + font-weight: bold; + display: block; + width: 100%; + text-align: center; + } + + h2 { + color: #fff; + } +} + +@media screen and (max-width: 450px) { + .game-progress { + width: 105px; + span { + font-size: 17px; + } + } +} + +@media screen and (max-width: 380px) { + .game-progress { + width: 95px; + } +} + +@media screen and (max-width: 360px) { + .game-progress { + width: 90px; + span { + font-size: 15px; + } + } +} diff --git a/examples/function_memory_game/scss/game_status_board.scss b/examples/function_memory_game/scss/game_status_board.scss new file mode 100644 index 00000000000..f0de14cbd2c --- /dev/null +++ b/examples/function_memory_game/scss/game_status_board.scss @@ -0,0 +1,24 @@ +.game-status-container { + position: relative; + margin-top: 10px; + width: 100%; + height: 20px; + line-height: 20px; + text-align: center; + font-size: 18px; + font-weight: bold; + button { + border: none; + cursor: pointer; + background: transparent; + color: #5979ac; + font-size: 17px; + font-weight: bold; + } + .sec-past { + position: absolute; + right: 10px; + font-size: 15px; + font-weight: normal; + } +} diff --git a/examples/function_memory_game/scss/index.scss b/examples/function_memory_game/scss/index.scss new file mode 100644 index 00000000000..cbd4758408c --- /dev/null +++ b/examples/function_memory_game/scss/index.scss @@ -0,0 +1,45 @@ +* { + box-sizing: border-box; + padding: 0; + margin: 0; +} + +html, +body { + height: 100vh; + width: 100vw; + margin: 0; + padding: 0; +} + +body { + display: flex; + justify-content: center; + align-items: center; +} + +.game-panel { + width: 450px; + height: 670px; + border: 4px solid #bdbdbd; + border-radius: 2px; + background-color: #faf8ef; + padding: 10px; + display: flex; + flex-direction: column; +} + +@media screen and (max-width: 450px) { + .game-panel { + width: 100%; + height: 100%; + justify-content: space-around; + } +} + +@import './score_board.scss'; +@import './score_board_best_score.scss'; +@import './game_progress'; +@import './chess_board.scss'; +@import './chess_board_card.scss'; +@import './game_status_board.scss'; diff --git a/examples/function_memory_game/scss/score_board.scss b/examples/function_memory_game/scss/score_board.scss new file mode 100644 index 00000000000..b302fa21111 --- /dev/null +++ b/examples/function_memory_game/scss/score_board.scss @@ -0,0 +1,46 @@ +.score-board { + width: 100%; + height: 100px; + display: flex; + justify-content: space-between; + align-items: center; + .logo { + width: 160px; + height: 100px; + line-height: 90px; + padding: 5px; + border-radius: 5px; + background-color: #5979ac; + color: #fff; + text-align: center; + } + a { + text-decoration: none; + color: #fff; + } +} + +// score board -> logo + +@media screen and (max-width: 450px) { + .score-board .logo { + width: 150px; + } +} + +@media screen and (max-width: 380px) { + .score-board .logo { + width: 140px; + } +} + +@media screen and (max-width: 360px) { + .score-board { + .logo { + width: 110px; + } + a { + font-size: 18px; + } + } +} diff --git a/examples/function_memory_game/scss/score_board_best_score.scss b/examples/function_memory_game/scss/score_board_best_score.scss new file mode 100644 index 00000000000..108305f462d --- /dev/null +++ b/examples/function_memory_game/scss/score_board_best_score.scss @@ -0,0 +1,46 @@ +.best-score { + width: 120px; + height: 100px; + padding: 10px; + background-color: #bbada0; + border-radius: 5px; + display: flex; + flex-direction: column; + justify-content: space-between; + align-items: center; + color: #eae0d1; + span { + font-size: 19px; + font-weight: bold; + display: block; + width: 100%; + text-align: center; + } + + h2 { + color: #fff; + } +} + +@media screen and (max-width: 450px) { + .best-score { + width: 105px; + span { + font-size: 17px; + } + } +} +@media screen and (max-width: 380px) { + .best-score { + width: 95px; + } +} + +@media screen and (max-width: 360px) { + .best-score { + width: 90px; + span { + font-size: 15px; + } + } +} diff --git a/examples/function_memory_game/src/components.rs b/examples/function_memory_game/src/components.rs new file mode 100644 index 00000000000..660a916d3d4 --- /dev/null +++ b/examples/function_memory_game/src/components.rs @@ -0,0 +1,8 @@ +pub mod app; +pub mod chessboard; +pub mod chessboard_card; +pub mod game_status_board; +pub mod score_board; +pub mod score_board_best_score; +pub mod score_board_logo; +pub mod score_board_progress; diff --git a/examples/function_memory_game/src/components/app.rs b/examples/function_memory_game/src/components/app.rs new file mode 100644 index 00000000000..fae00a39fba --- /dev/null +++ b/examples/function_memory_game/src/components/app.rs @@ -0,0 +1,74 @@ +use gloo::timers::callback::{Interval, Timeout}; +use std::{cell::RefCell, rc::Rc}; +use yew::prelude::*; +use yew::{function_component, html}; + +use crate::components::{ + chessboard::Chessboard, game_status_board::GameStatusBoard, score_board::ScoreBoard, +}; + +use crate::constant::Status; +use crate::state::{Action, State}; + +#[function_component] +pub fn App() -> Html { + let state = use_reducer(State::reset); + let sec_past = use_state(|| 0_u32); + let sec_past_timer: Rc>> = use_mut_ref(|| None); + let flip_back_timer: Rc>> = use_mut_ref(|| None); + let sec_past_time = *sec_past; + + use_effect_with_deps( + move |state| { + // game reset + if state.status == Status::Ready { + sec_past.set(0); + } + // game start + else if *sec_past == 0 && state.last_card.is_some() { + let sec_past = sec_past.clone(); + let mut sec = *sec_past; + *sec_past_timer.borrow_mut() = Some(Interval::new(1000, move || { + sec += 1; + sec_past.set(sec); + })); + } + // game over + else if state.status == Status::Passed { + *sec_past_timer.borrow_mut() = None; + *flip_back_timer.borrow_mut() = None; + state.dispatch(Action::TrySaveBestScore(*sec_past)); + } + // match failed + else if state.rollback_cards.is_some() { + let cloned_state = state.clone(); + let cloned_rollback_cards = state.rollback_cards.clone().unwrap(); + *flip_back_timer.borrow_mut() = Some(Timeout::new(1000, move || { + cloned_state.dispatch(Action::RollbackCards(cloned_rollback_cards)); + })); + } + || () + }, + state.clone(), + ); + + let on_reset = { + let state = state.clone(); + Callback::from(move |_| state.dispatch(Action::GameReset)) + }; + + let on_flip = { + let state = state.clone(); + Callback::from(move |card| { + state.dispatch(Action::FlipCard(card)); + }) + }; + + html! { +
+ + + +
+ } +} diff --git a/examples/function_memory_game/src/components/chessboard.rs b/examples/function_memory_game/src/components/chessboard.rs new file mode 100644 index 00000000000..22688337b5e --- /dev/null +++ b/examples/function_memory_game/src/components/chessboard.rs @@ -0,0 +1,23 @@ +use yew::prelude::*; +use yew::{function_component, html, Properties}; + +use crate::components::chessboard_card::ChessboardCard; +use crate::state::{Card, RawCard}; + +#[derive(Properties, Clone, PartialEq)] +pub struct Props { + pub cards: Vec, + pub on_flip: Callback, +} +#[function_component] +pub fn Chessboard(props: &Props) -> Html { + html! { +
+ { for props.cards.iter().map(|card| + html! { + + } + ) } +
+ } +} diff --git a/examples/function_memory_game/src/components/chessboard_card.rs b/examples/function_memory_game/src/components/chessboard_card.rs new file mode 100644 index 00000000000..a3c2ce43b80 --- /dev/null +++ b/examples/function_memory_game/src/components/chessboard_card.rs @@ -0,0 +1,51 @@ +use web_sys::MouseEvent; +use yew::prelude::*; +use yew::{function_component, html, Html, Properties}; + +use crate::constant::CardName; +use crate::state::{Card, RawCard}; + +#[derive(Properties, Clone, PartialEq)] +pub struct Props { + pub card: Card, + pub on_flip: Callback, +} + +#[function_component] +pub fn ChessboardCard(props: &Props) -> Html { + let Props { card, on_flip } = props.clone(); + let Card { flipped, name, id } = card; + + let get_link_by_cardname = { + match name { + CardName::EightBall => "public/8-ball.png", + CardName::Kronos => "public/kronos.png", + CardName::BakedPotato => "public/baked-potato.png", + CardName::Dinosaur => "public/dinosaur.png", + CardName::Rocket => "public/rocket.png", + CardName::SkinnyUnicorn => "public/skinny-unicorn.png", + CardName::ThatGuy => "public/that-guy.png", + CardName::Zeppelin => "public/zeppelin.png", + } + .to_string() + }; + + let onclick = move |e: MouseEvent| { + e.stop_propagation(); + (!flipped).then(|| { + on_flip.emit(RawCard { + id: id.clone(), + name, + }) + }); + }; + + html! { +
+
+ card + card +
+
+ } +} diff --git a/examples/function_memory_game/src/components/game_status_board.rs b/examples/function_memory_game/src/components/game_status_board.rs new file mode 100644 index 00000000000..19cd2813f14 --- /dev/null +++ b/examples/function_memory_game/src/components/game_status_board.rs @@ -0,0 +1,39 @@ +use crate::constant::Status; +use yew::prelude::*; +use yew::{function_component, html, Properties}; + +#[derive(Properties, Clone, PartialEq)] +pub struct Props { + pub status: Status, + pub sec_past: u32, + pub on_reset: Callback<()>, +} + +#[function_component] +pub fn GameStatusBoard(props: &Props) -> Html { + let get_content = { + let onclick = props.on_reset.reform(move |e: MouseEvent| { + e.stop_propagation(); + e.prevent_default(); + }); + + match props.status { + Status::Ready => html! { + {"Ready"} + }, + Status::Playing => html! { + {"Playing"} + }, + Status::Passed => html! { + + }, + } + }; + + html! { +
+ {get_content} + { props.sec_past}{" s"} +
+ } +} diff --git a/examples/function_memory_game/src/components/score_board.rs b/examples/function_memory_game/src/components/score_board.rs new file mode 100644 index 00000000000..e3c7efee0f2 --- /dev/null +++ b/examples/function_memory_game/src/components/score_board.rs @@ -0,0 +1,26 @@ +use yew::{function_component, html, Html, Properties}; + +use crate::components::{ + score_board_best_score::BestScore, score_board_logo::Logo, score_board_progress::GameProgress, +}; + +#[derive(PartialEq, Properties, Clone)] +pub struct Props { + pub unresolved_card_pairs: u8, + pub best_score: u32, +} + +#[function_component] +pub fn ScoreBoard(props: &Props) -> Html { + let Props { + best_score, + unresolved_card_pairs, + } = props.clone(); + html! { +
+ + + +
+ } +} diff --git a/examples/function_memory_game/src/components/score_board_best_score.rs b/examples/function_memory_game/src/components/score_board_best_score.rs new file mode 100644 index 00000000000..3af394a93a8 --- /dev/null +++ b/examples/function_memory_game/src/components/score_board_best_score.rs @@ -0,0 +1,16 @@ +use yew::{function_component, html, Html, Properties}; + +#[derive(PartialEq, Properties, Clone)] +pub struct Props { + pub best_score: u32, +} + +#[function_component] +pub fn BestScore(props: &Props) -> Html { + html! { +
+ {"Highest Record"} +

{ props.best_score }

+
+ } +} diff --git a/examples/function_memory_game/src/components/score_board_logo.rs b/examples/function_memory_game/src/components/score_board_logo.rs new file mode 100644 index 00000000000..c18158bc803 --- /dev/null +++ b/examples/function_memory_game/src/components/score_board_logo.rs @@ -0,0 +1,10 @@ +use yew::{function_component, html, Html}; + +#[function_component] +pub fn Logo() -> Html { + html! { +

+ {"Memory"} +

+ } +} diff --git a/examples/function_memory_game/src/components/score_board_progress.rs b/examples/function_memory_game/src/components/score_board_progress.rs new file mode 100644 index 00000000000..13f9bc97848 --- /dev/null +++ b/examples/function_memory_game/src/components/score_board_progress.rs @@ -0,0 +1,16 @@ +use yew::{function_component, html, Html, Properties}; + +#[derive(PartialEq, Properties, Clone)] +pub struct Props { + pub unresolved_card_pairs: u8, +} + +#[function_component] +pub fn GameProgress(props: &Props) -> Html { + html! { +
+ {"Cards not Matched"} +

{ props.unresolved_card_pairs }

+
+ } +} diff --git a/examples/function_memory_game/src/constant.rs b/examples/function_memory_game/src/constant.rs new file mode 100644 index 00000000000..0de39262e4a --- /dev/null +++ b/examples/function_memory_game/src/constant.rs @@ -0,0 +1,42 @@ +use serde::{Deserialize, Serialize}; +use strum_macros::{Display, EnumIter}; + +pub const KEY_BEST_SCORE: &str = "memory.game.best.score"; + +#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Serialize, Deserialize)] +pub enum CardName { + EightBall, + Kronos, + BakedPotato, + Dinosaur, + Rocket, + SkinnyUnicorn, + ThatGuy, + Zeppelin, +} + +#[derive(Clone, Copy, Debug, EnumIter, Display, PartialEq, Serialize, Deserialize)] +pub enum Status { + Ready, + Playing, + Passed, +} + +pub const RAW_CARDS: [CardName; 16] = [ + CardName::EightBall, + CardName::Kronos, + CardName::BakedPotato, + CardName::Dinosaur, + CardName::Rocket, + CardName::SkinnyUnicorn, + CardName::ThatGuy, + CardName::Zeppelin, + CardName::EightBall, + CardName::Kronos, + CardName::BakedPotato, + CardName::Dinosaur, + CardName::Rocket, + CardName::SkinnyUnicorn, + CardName::ThatGuy, + CardName::Zeppelin, +]; diff --git a/examples/function_memory_game/src/helper.rs b/examples/function_memory_game/src/helper.rs new file mode 100644 index 00000000000..c74796dc053 --- /dev/null +++ b/examples/function_memory_game/src/helper.rs @@ -0,0 +1,21 @@ +use nanoid::nanoid; +use rand::seq::SliceRandom; +use rand::thread_rng; + +use crate::constant::RAW_CARDS; +use crate::state::Card; + +pub fn shuffle_cards() -> Vec { + let mut raw_cards = RAW_CARDS; + + raw_cards.shuffle(&mut thread_rng()); + + raw_cards + .iter() + .map(|&p| Card { + id: nanoid!(), + flipped: false, + name: p, + }) + .collect() +} diff --git a/examples/function_memory_game/src/main.rs b/examples/function_memory_game/src/main.rs new file mode 100644 index 00000000000..995fd2787be --- /dev/null +++ b/examples/function_memory_game/src/main.rs @@ -0,0 +1,10 @@ +mod components; +mod constant; +mod helper; +mod state; + +use crate::components::app::App; + +fn main() { + yew::start_app::(); +} diff --git a/examples/function_memory_game/src/state.rs b/examples/function_memory_game/src/state.rs new file mode 100644 index 00000000000..6d3115ad76f --- /dev/null +++ b/examples/function_memory_game/src/state.rs @@ -0,0 +1,147 @@ +use gloo::storage::{LocalStorage, Storage}; +use serde::{Deserialize, Serialize}; +use std::rc::Rc; +use yew::prelude::*; + +use crate::constant::{CardName, Status, KEY_BEST_SCORE}; +use crate::helper::shuffle_cards; + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct RawCard { + pub id: String, + pub name: CardName, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)] +pub struct Card { + pub id: String, + pub flipped: bool, + pub name: CardName, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct State { + pub unresolved_card_pairs: u8, + pub best_score: u32, + pub status: Status, + pub cards: Vec, + pub last_card: Option, + pub rollback_cards: Option<[RawCard; 2]>, +} + +impl PartialEq for &mut Card { + fn eq(&self, other: &RawCard) -> bool { + self.id == other.id && self.name == other.name + } +} + +pub enum Action { + FlipCard(RawCard), + RollbackCards([RawCard; 2]), + TrySaveBestScore(u32), + GameReset, +} + +impl Reducible for State { + type Action = Action; + + fn reduce(self: Rc, action: Self::Action) -> Rc { + match action { + Action::FlipCard(card) => { + let status = if self.status == Status::Ready { + Status::Playing + } else { + self.status + }; + + let mut cards = self.cards.clone(); + cards.iter_mut().filter(|c| c.eq(&card)).for_each(|c| { + c.flipped = !c.flipped; + }); + + let last_card = self.last_card.clone(); + + match last_card { + None => State { + unresolved_card_pairs: self.unresolved_card_pairs, + best_score: self.best_score, + status, + cards: cards.clone(), + last_card: Some(card), + rollback_cards: None, + }, + Some(last_card) => { + let mut unresolved_card_pairs = self.unresolved_card_pairs; + let mut status = self.status; + let mut rollback_cards = self.rollback_cards.clone(); + if card.id.ne(&last_card.id) && card.name.eq(&last_card.name) { + unresolved_card_pairs = self.unresolved_card_pairs - 1; + status = if unresolved_card_pairs == 0 { + Status::Passed + } else { + self.status + }; + } else { + rollback_cards = Some([last_card, card]); + } + + State { + unresolved_card_pairs, + best_score: self.best_score, + status, + cards: cards.clone(), + last_card: None, + rollback_cards, + } + } + } + .into() + } + Action::RollbackCards(rollback_cards) => { + let mut cards = self.cards.clone(); + + cards + .iter_mut() + .filter(|c| { + rollback_cards.contains( + &(RawCard { + id: c.id.clone(), + name: c.name, + }), + ) + }) + .for_each(|c| { + c.flipped = !c.flipped; + }); + + State { + unresolved_card_pairs: self.unresolved_card_pairs, + best_score: self.best_score, + status: self.status, + cards, + last_card: self.last_card.clone(), + rollback_cards: None, + } + .into() + } + Action::TrySaveBestScore(sec_past) => { + (self.best_score > sec_past).then(|| LocalStorage::set(KEY_BEST_SCORE, sec_past)); + self + } + Action::GameReset => State::reset().into(), + } + } +} + +impl State { + pub fn reset() -> State { + State { + unresolved_card_pairs: 8, + best_score: LocalStorage::get(KEY_BEST_SCORE).unwrap_or(9999), + status: Status::Ready, + cards: shuffle_cards(), + last_card: None, + rollback_cards: None, + } + } +}