Skip to content

Commit 56358a2

Browse files
committed
Implement step by step solution
1 parent 6efb147 commit 56358a2

File tree

11 files changed

+134
-49
lines changed

11 files changed

+134
-49
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
"build": "react-scripts build",
2323
"test": "react-scripts test",
2424
"eject": "react-scripts eject",
25+
"build:wasm:dev": "cd wasm && wasm-pack build --target web --out-dir pkg --dev",
2526
"build:wasm": "cd wasm && wasm-pack build --target web --out-dir pkg --release -- --features wasm_alloc"
2627
},
2728
"eslintConfig": {

src/App.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ button {
1616
border: solid 2px black;
1717
}
1818

19+
button[disabled] {
20+
background-color: #2f5e9b;
21+
}
22+
1923
button:hover {
2024
cursor: pointer;
2125
}
@@ -46,6 +50,7 @@ hr {
4650
flex-direction: column;
4751
gap: 0.3rem;
4852
margin: 0.7rem 0;
53+
min-width: 8rem;
4954
}
5055

5156
.sudoku-controller {

src/SudokuBoard.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import React from "react";
22
import {NumberCell} from "./NumberCell";
33
import {ColRowCell} from "./ColRowCell";
4-
import {Board} from "./board";
4+
import {Board, Highlights} from "./board";
55
import {CellBase} from "./CellBase";
6-
import {Highlights} from "./SudokuController";
76

87
type Props = Highlights & {
98
board: Board;
109
setBoard: (board: Board) => void;
10+
readonly: boolean;
1111
}
1212

1313
export const SudokuBoard: React.FC<Props> = (props) => {
@@ -61,7 +61,7 @@ export const SudokuBoard: React.FC<Props> = (props) => {
6161
<CellBase highlighted={props.highlightRow === r || props.highlightCol === c ||
6262
(props.highlightBlock !== null && props.highlightBlock[0] === blockRow && props.highlightBlock[1] === blockCol)}>
6363
<NumberCell index={2 + index++} num={board.get(r, c)}
64-
setNum={(value) => updateBoard(r, c, value)}></NumberCell>
64+
setNum={(value) => props.readonly || updateBoard(r, c, value)}></NumberCell>
6565
</CellBase>
6666
</td>
6767
);

src/SudokuController.tsx

Lines changed: 55 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,34 @@
11
import React, {useState} from "react";
22
import {SudokuBoard} from "./SudokuBoard";
3-
import {Board} from "./board";
3+
import {Board, Highlights} from "./board";
44
import init, {find_errors, random_board, solve} from "wasm";
5+
import {AllNull} from "./util";
56

67
type Props = {
78
setLog: (log: string) => void;
89
}
910

10-
export type Highlights = {
11-
highlightRow: number | null;
12-
highlightCol: number | null;
13-
highlightBlock: [number, number] | null;
11+
type State = (Solution | AllNull<Solution>) & Highlights & {
12+
board: Board;
1413
}
1514

16-
type State = Highlights & {
17-
board: Board;
15+
type Solution = {
16+
steps: Step[];
17+
currentStep: number;
18+
}
19+
20+
type Step = Highlights & {
21+
message: string;
22+
literal: string;
1823
}
1924

20-
function defaultState(board: Board) {
25+
function defaultState(board: Board): State {
2126
return {
2227
highlightBlock: null,
2328
highlightCol: null,
2429
highlightRow: null,
30+
steps: null,
31+
currentStep: null,
2532
board
2633
};
2734
}
@@ -63,23 +70,30 @@ export const SudokuController: React.FC<Props> = (props) => {
6370
})
6471
}
6572

66-
function solveBoard(board: Board) {
67-
let start = Date.now();
68-
73+
function solveBoard(board: Board, recordSteps: number) {
6974
init().then(() => {
70-
let result = JSON.parse(solve(board.toLiteral(), board.blockSize, 0));
75+
let result: Step[] | null = JSON.parse(solve(board.toLiteral(), board.blockSize, recordSteps));
7176
if (result === null) {
7277
props.setLog("Couldn't find solution");
7378
} else {
74-
let solution = result.solution;
75-
props.setLog(`Found solution in ${Date.now() - start}ms`);
76-
let solved = Board.fromLiteral(solution, board.blockSize);
7779
console.log("res", result);
78-
setState(s => ({...s, board: solved}));
80+
setState(s => ({...s, currentStep: 0, steps: result!}));
81+
changeCurrentStep(0, result);
7982
}
8083
})
8184
}
8285

86+
function hideSolution() {
87+
setState(s => ({...s, currentStep: null, steps: null}));
88+
}
89+
90+
function changeCurrentStep(index: number, steps: Step[]) {
91+
if (index < 0 || index >= steps.length) return;
92+
93+
props.setLog(steps[index].message);
94+
setState(s => ({...s, currentStep: index, steps}));
95+
}
96+
8397
function clear() {
8498
setState(s => defaultState(Board.default(s.board.blockSize)));
8599
}
@@ -90,15 +104,20 @@ export const SudokuController: React.FC<Props> = (props) => {
90104
let blockSize = state!.board.blockSize;
91105
let result = random_board(coverage, blockSize);
92106
let board = Board.fromLiteral(result, blockSize);
107+
hideSolution();
93108
setState(s => ({...s, board}));
94-
props.setLog(`Generated random board in ${Date.now() - start}ms`)
109+
props.setLog(`Generated random board in ${Date.now() - start}ms`);
95110
})
96111
}
97112

113+
let focus = state.steps !== null ? state.steps[state.currentStep] : state;
98114
return (
99115
<div className={"sudoku-controller"}>
100-
<SudokuBoard board={state.board} setBoard={changeBoard} highlightRow={state.highlightRow}
101-
highlightCol={state.highlightCol} highlightBlock={state.highlightBlock}></SudokuBoard>
116+
<SudokuBoard
117+
board={state.steps !== null ? Board.fromLiteral(state.steps[state.currentStep].literal, state.board.blockSize) : state.board}
118+
setBoard={changeBoard} highlightRow={focus.highlightRow}
119+
highlightCol={focus.highlightCol} highlightBlock={focus.highlightBlock}
120+
readonly={state.steps !== null}></SudokuBoard>
102121
<div className={"buttons"}>
103122
<select defaultValue={3}
104123
onChange={(e) => setState({
@@ -110,7 +129,6 @@ export const SudokuController: React.FC<Props> = (props) => {
110129
<option value={4}>16x16</option>
111130
</select>
112131
<button onClick={() => check(state.board, true)}>Check</button>
113-
<button onClick={() => solveBoard(state.board)}>Solve</button>
114132
<button onClick={clear}>Clear</button>
115133
<div>
116134
<hr/>
@@ -119,6 +137,23 @@ export const SudokuController: React.FC<Props> = (props) => {
119137
<button onClick={() => randomBoard(0.50)}>Random 50%</button>
120138
<button onClick={() => randomBoard(0.25)}>Random 25%</button>
121139
<button onClick={() => randomBoard(0.1)}>Random 10%</button>
140+
<div>
141+
<hr/>
142+
</div>
143+
144+
{state.steps === null ?
145+
<>
146+
<button onClick={() => solveBoard(state.board, 0)}>Solve</button>
147+
<button onClick={() => solveBoard(state.board, 200)}>Solve step-by-step</button>
148+
</>
149+
:
150+
<>
151+
<button onClick={hideSolution}>Hide solution</button>
152+
<button disabled={state.currentStep <= 0} onClick={() => changeCurrentStep(state.currentStep! - 1, state.steps!)}>Prev step</button>
153+
<button disabled={state.currentStep >= state.steps!.length - 1} onClick={() => changeCurrentStep(state.currentStep! + 1, state.steps!)}>Next step</button>
154+
</>
155+
}
156+
122157
</div>
123158
</div>
124159
)

src/board.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,10 @@ export class Board {
4747
public toLiteral() {
4848
return this.cells.reduce((acc, value) => acc + (value ?? "_") + " ", "");
4949
}
50+
}
51+
52+
export type Highlights = {
53+
highlightRow: number | null;
54+
highlightCol: number | null;
55+
highlightBlock: [number, number] | null;
5056
}

src/util.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export type AllNull<T> = {
2+
[P in keyof T]: null;
3+
}

wasm/Cargo.lock

Lines changed: 13 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

wasm/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ getrandom = { version = "0.2.7", features = ["js"] }
1414
rand = "0.8.5"
1515
wee_alloc = "0.4.5"
1616
json = "0.12.4"
17+
instant = { version = "0.1.12", features = [ "wasm-bindgen" ] }
1718

1819
[dev-dependencies]
1920
criterion = "0.3.6"

wasm/src/lib.rs

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ mod number_options;
55
mod util;
66
mod solve_report;
77

8+
use instant::Instant;
89
use json::{array, JsonValue, object};
910
use rand::Rng;
1011
use wasm_bindgen::prelude::*;
@@ -33,29 +34,37 @@ pub fn solve(board_literal: &str, block_size: usize, record_steps: usize) -> Str
3334
fn solve_with_size<const SIZE: usize, const BLOCK_SIZE: usize>(board_literal: &str, record_steps: usize) -> String {
3435
let board = SudokuBoard::<SIZE, BLOCK_SIZE>::from_literal(board_literal);
3536
let mut solver = SudokuSolver::new(record_steps);
37+
let start = Instant::now();
3638
let result = solver.solve(&board);
39+
let elapsed = start.elapsed().as_micros();
40+
3741
if result.is_none() {
3842
return JsonValue::Null.dump();
3943
}
4044

41-
let solution = result.unwrap().to_literal();
4245
let mut steps = JsonValue::new_array();
4346

4447
for step in solver.steps {
45-
let array: Vec<Option<u8>> = step.numbers.iter().flatten().map(|o| o.clone()).collect();
4648
steps.push(object! {
4749
message: step.message,
4850
highlightRow: step.highlight_row,
4951
highlightCol: step.highlight_col,
5052
highlightBlock: step.highlight_block.map(|[a, b]| array![a, b]),
51-
numbers: array,
53+
literal: step.literal,
5254
}).expect("Invalid Json object");
5355
}
5456

55-
return object! {
56-
solution: solution,
57-
steps: steps
58-
}.dump();
57+
let solution = result.unwrap().to_literal();
58+
let solution_message = format!("Found solution in {elapsed}μs");
59+
steps.push(object! {
60+
message: solution_message,
61+
highlightRow: JsonValue::Null,
62+
highlightCol: JsonValue::Null,
63+
highlightBlock: JsonValue::Null,
64+
literal: solution
65+
}).expect("Invalid Json object");
66+
67+
steps.dump()
5968
}
6069

6170
#[wasm_bindgen]

wasm/src/solve_report.rs

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
use crate::util::Array2D;
2-
31
pub struct ReportStep<const SIZE: usize, const BLOCK_SIZE: usize> {
42
pub message: String,
53
pub highlight_row: Option<u8>,
64
pub highlight_col: Option<u8>,
75
pub highlight_block: Option<[u8; 2]>,
8-
pub numbers: Array2D<Option<u8>, SIZE>
6+
pub literal: String
97
}

0 commit comments

Comments
 (0)