Skip to content

Commit

Permalink
feat: undo (#5)
Browse files Browse the repository at this point in the history
* refactor: type alias SudokuState for [u8; 81]

* fix: ui multiple conflicting cells

* feat: undo button

* chore: find_changed_cell tests and doc

* refactor: conflicting logic

* fix: New button remembering old filled cell style

* ui: new buttons

* fix: new screenshot

* format: fix formatting

* fix: new screenshot
  • Loading branch information
storopoli committed Jan 23, 2024
1 parent 070d10d commit c654c1e
Show file tree
Hide file tree
Showing 6 changed files with 233 additions and 18 deletions.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "sudoku-dioxus"
version = "0.2.3"
version = "0.3.0"
edition = "2021"
authors = ["Jose Storopoli <[email protected]>"]
description = "Sudoku PWA with Dioxus"
Expand Down
8 changes: 6 additions & 2 deletions assets/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,15 @@ button.icon {
}

button.delete {
background-image: url('data:image/svg+xml;base64,PHN2ZyBkYXRhLXNsb3Q9Imljb24iIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2U9IiMzYjU5YTkiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+CiAgPHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJtOS43NSA5Ljc1IDQuNSA0LjVtMC00LjUtNC41IDQuNU0yMSAxMmE5IDkgMCAxIDEtMTggMCA5IDkgMCAwIDEgMTggMFoiPjwvcGF0aD4KPC9zdmc+');
background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlPSIjM2I1OWE5IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgYXJpYS1oaWRkZW49InRydWUiPjxwYXRoIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0iTTEyIDkuNzUgMTQuMjUgMTJtMCAwIDIuMjUgMi4yNU0xNC4yNSAxMmwyLjI1LTIuMjVNMTQuMjUgMTIgMTIgMTQuMjVtLTIuNTggNC45Mi02LjM3NC02LjM3NWExLjEyNSAxLjEyNSAwIDAgMSAwLTEuNTlMOS40MiA0LjgzYy4yMS0uMjExLjQ5Ny0uMzMgLjc5NS0uMzNIMTkuNWEyLjI1IDIuMjUgMCAwIDEgMi4yNSAyLjI1djEwLjVhMi4yNSAyLjI1IDAgMCAxLTIuMjUgMi4yNWgtOS4yODRjLS4yOTggMC0uNTg1LS4xMTktLjc5NS0uMzNaIj48L3BhdGg+PC9zdmc+');
}

button.new {
background-image: url("data:image/svg+xml;base64,CjxzdmcgZGF0YS1zbG90PSJpY29uIiBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlPSIjM2I1OWE5IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgYXJpYS1oaWRkZW49InRydWUiPgogIDxwYXRoIHN0cm9rZS1saW5lY2FwPSJyb3VuZCIgc3Ryb2tlLWxpbmVqb2luPSJyb3VuZCIgZD0iTTEyIDl2Nm0zLTNIOW0xMiAwYTkgOSAwIDEgMS0xOCAwIDkgOSAwIDAgMSAxOCAwWiI+PC9wYXRoPgo8L3N2Zz4K");
background-image: url('data:image/svg+xml;base64,PHN2ZyBkYXRhLXNsb3Q9Imljb24iIGZpbGw9Im5vbmUiIHN0cm9rZS13aWR0aD0iMS41IiBzdHJva2U9IiMzYjU5YTkiIHZpZXdCb3g9IjAgMCAyNCAyNCIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIiBhcmlhLWhpZGRlbj0idHJ1ZSI+PHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNMTkuNSAxNC4yNXYtMi42MjVhMy4zNzUgMy4zNzUgMCAwIDAtMy4zNzUtMy4zNzVoLTEuNUExLjEyNSAxLjEyNSAwIDAgMSAxMy41IDcuMTI1di0xLjVhMy4zNzUgMy4zNzUgMCAwIDAtMy4zNzUtMy4zNzVINy41bTMuNzUgOXY2bTMtM0g5bTEuNS0xMkg1LjYyNWMtLjYyMSAwLTEuMTI1LjUwNC0xLjEyNSAxLjEyNXYxNy4yNWMwIC42MjEuNTA0IDEuMTI1IDEuMTI1IDEuMTI1aDEyLjc1Yy42MjEgMCAxLjEyNS0uNTA0IDEuMTI1LTEuMTI1VjExLjI1YTkgOSAwIDAgMC05LTlaIj48L3BhdGg+PC9zdmc+');
}

button.undo {
background-image: url('data:image/svg+xml;base64,PHN2ZyBmaWxsPSJub25lIiBzdHJva2Utd2lkdGg9IjEuNSIgc3Ryb2tlPSIjM2I1OWE5IiB2aWV3Qm94PSIwIDAgMjQgMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PHBhdGggc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiBkPSJNOSAxNSAzIDltMCAwIDYtNiBNMyA5aDEyYTYgNiAwIDAgMSAwIDEyaC0zIj48L3BhdGg+PC9zdmc+');
}

div.github {
Expand Down
Binary file modified screenshot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
11 changes: 10 additions & 1 deletion src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@

use dioxus::prelude::*;

use crate::components::board::{InitialSudokuPuzzle, SudokuBoard, SudokuPuzzle};
use crate::components::board::{InitialSudokuPuzzle, SudokuBoard, SudokuPuzzle, SudokuPuzzleMoves};

/// Represents a Sudoku state with the values, as `u8`, of the 81 cells in a
/// Sodoku game
pub type SudokuState = [u8; 81];

/// This function sets up the main environment for
/// the Sudoku game in a web browser, initializes the necessary state,
Expand All @@ -23,12 +27,17 @@ use crate::components::board::{InitialSudokuPuzzle, SudokuBoard, SudokuPuzzle};
/// The app will panic if fails to get initial Sudoku puzzle shared state.
#[must_use]
pub fn App(cx: Scope) -> Element {
// unpack initial puzzle
use_shared_state_provider(cx, InitialSudokuPuzzle::new);

// set current sudoku and cache of user moves
let initial_sudoku = use_shared_state::<InitialSudokuPuzzle>(cx)
.expect("failed to get initial sudoku puzzle shared state")
.read()
.0;
use_shared_state_provider(cx, || SudokuPuzzle(initial_sudoku));
use_shared_state_provider(cx, || SudokuPuzzleMoves(vec![initial_sudoku]));

cx.render(rsx!(
h1 {
class: "input",
Expand Down
103 changes: 94 additions & 9 deletions src/components/board.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,11 @@

use dioxus::prelude::*;

use crate::app::SudokuState;
use crate::components::cell::Cell;
use crate::utils::{create_sudoku, get_class, get_conflicting_cells};
use crate::utils::{
create_sudoku, find_changed_cell, get_all_conflicting_cells, get_class, get_related_cells,
};

/// Shared State for clicked [`Cell`]
///
Expand All @@ -35,7 +38,7 @@ pub struct Mutable(pub bool);
/// [`Cell`]s are related if they share the same row, column, or sub-grid in
/// a Sudoku board.
///
/// See also: [`get_related_cells`](crate::utils::get_related_cells).
/// See also: [`get_related_cells`].
pub struct Related(pub Vec<u8>);

/// Shared State for clicked [`Cell`]'s conficts
Expand All @@ -46,12 +49,12 @@ pub struct Related(pub Vec<u8>);
/// [`Cell`]s are in conflict if they share the same row, column, or sub-grid in
/// a Sudoku board and have the same value.
///
/// See also: [`get_related_cells`](crate::utils::get_related_cells)
/// and [`get_conflicting_cells`].
/// See also: [`get_related_cells`]
/// and [`get_conflicting_cells`](crate::utils::get_conflicting_cells).
pub struct Conflicting(pub Vec<u8>);

/// Shared State for the initial [`SudokuBoard`] puzzle
pub struct InitialSudokuPuzzle(pub [u8; 81]);
pub struct InitialSudokuPuzzle(pub SudokuState);
impl InitialSudokuPuzzle {
#[must_use]
pub fn new() -> Self {
Expand All @@ -65,7 +68,10 @@ impl Default for InitialSudokuPuzzle {
}

/// Shared State for the current [`SudokuBoard`] puzzle
pub struct SudokuPuzzle(pub [u8; 81]);
pub struct SudokuPuzzle(pub SudokuState);

/// Shared State for the all the [`SudokuState`] across user moves
pub struct SudokuPuzzleMoves(pub Vec<SudokuState>);

/// Component Props for [`NumberButton`]
///
Expand All @@ -91,6 +97,8 @@ fn NumberButton(cx: Scope<NumberButtonProps>) -> Element {
};

// Unpack shared states
let moves = use_shared_state::<SudokuPuzzleMoves>(cx)
.expect("failed to get sudoku puzzle shared state");
let sudoku =
use_shared_state::<SudokuPuzzle>(cx).expect("failed to get sudoku puzzle shared state");
let conflicting =
Expand All @@ -112,7 +120,12 @@ fn NumberButton(cx: Scope<NumberButtonProps>) -> Element {
if mutable {
// chaging the clicked cell value to the button number
sudoku.write().0[clicked as usize] = number;
conflicting.write().0 = get_conflicting_cells(&sudoku.read().0, clicked);
let current_sudoku = sudoku.read().0;
moves.write().0.push(current_sudoku);

// conflicting logic
let new_conflicting = get_all_conflicting_cells(&current_sudoku);
conflicting.write().0 = new_conflicting;
}
},
"{number}"
Expand All @@ -129,6 +142,8 @@ fn NewButton(cx: Scope) -> Element {
// Unpack shared states
let initial_sudoku = use_shared_state::<InitialSudokuPuzzle>(cx)
.expect("failed to get initial sudoku puzzle shared state");
let moves = use_shared_state::<SudokuPuzzleMoves>(cx)
.expect("failed to get sudoku puzzle shared state");
let sudoku =
use_shared_state::<SudokuPuzzle>(cx).expect("failed to get sudoku puzzle shared state");
let clicked = use_shared_state::<Clicked>(cx).expect("failed to get clicked cell shared state");
Expand All @@ -144,6 +159,7 @@ fn NewButton(cx: Scope) -> Element {
onclick: move |_| {
// resetting the board with a new puzzle
initial_sudoku.write().0 = create_sudoku();
moves.write().0 = vec![initial_sudoku.read().0];
sudoku.write().0 = initial_sudoku.read().0;
// resetting the clicked cell
clicked.write().0 = 90;
Expand All @@ -157,6 +173,67 @@ fn NewButton(cx: Scope) -> Element {
}))
}

/// Component to render an undo button
///
/// This component renders a "Undo" button.
/// When activated, all current state is dropped and the board is drawn with a
/// fresh new puzzle for the user.
fn UndoButton(cx: Scope) -> Element {
// Unpack shared states
let initial_sudoku = use_shared_state::<InitialSudokuPuzzle>(cx)
.expect("failed to get initial sudoku puzzle shared state")
.read()
.0;
let moves = use_shared_state::<SudokuPuzzleMoves>(cx)
.expect("failed to get sudoku puzzle shared state");
let current_sudoku = *moves
.read()
.0
.last()
.expect("failed to get the current sudoku state");
let sudoku =
use_shared_state::<SudokuPuzzle>(cx).expect("failed to get sudoku puzzle shared state");
let clicked = use_shared_state::<Clicked>(cx).expect("failed to get clicked cell shared state");
let related =
use_shared_state::<Related>(cx).expect("failed to get related cells shared state");
let conflicting =
use_shared_state::<Conflicting>(cx).expect("failed to get conflicting cells shared state");

cx.render(rsx!(button {
class: "input icon undo",
onclick: move |_| {
if current_sudoku == initial_sudoku {
conflicting.notify_consumers();
} else {
// pop the last element of moves
let last_state = moves
.write()
.0
.pop()
.expect("cannot pop the last element of the sudoku moves shared state");

let new_sudoku = *moves
.read()
.0
.last()
.expect("failed to get sudoku moves shared state");
// resetting the board with a new puzzle
sudoku.write().0 = new_sudoku;

// update clicked, related
let last_clicked = find_changed_cell(&last_state, &new_sudoku)
.expect("cannot find changed index between the two previous state");
clicked.write().0 = last_clicked;
related.write().0 = get_related_cells(last_clicked);

// conflicting logic
let new_conflicting = get_all_conflicting_cells(&new_sudoku);
conflicting.write().0 = new_conflicting;
}
}
}))
}

/// Component to render a Sudoku board.
///
/// This component renders a Sudoku board which can be either randomly generated.
Expand Down Expand Up @@ -184,10 +261,15 @@ pub fn SudokuBoard(cx: Scope) -> Element {
.expect("failed to get initial sudoku puzzle shared state")
.read()
.0;
let sudoku = use_shared_state::<SudokuPuzzle>(cx)
.expect("failed to get sudoku puzzle shared state")
let moves = &use_shared_state::<SudokuPuzzleMoves>(cx)
.expect("failed to get sudoku moves shared state")
.read()
.0;

let sudoku = moves
.last()
.expect("failed to get the last element of the sudoku moves shared state");

let clicked = use_shared_state::<Clicked>(cx);

cx.render(rsx!(div {
Expand Down Expand Up @@ -217,6 +299,9 @@ pub fn SudokuBoard(cx: Scope) -> Element {
number: 0,
}

// Render UndoButton
UndoButton{}

// Render NewButton
NewButton{}
}))
Expand Down
Loading

0 comments on commit c654c1e

Please sign in to comment.