Skip to content

Commit 31428e2

Browse files
committed
Improve documentation
1 parent 3712e29 commit 31428e2

12 files changed

+101
-45
lines changed

README.md

+16
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,22 @@ step-by-step.
1010
* Algorithm made in Rust to solve puzzles in milliseconds
1111
* Step-by-step visualization of the solution with explanations
1212

13+
## How the [algorithm](https://github.com/cau777/sudoku_solver/blob/master/wasm/src/sudoku_solver.rs) works
14+
It was inspired by some real-world Sudoku solving techniques, and aims to minimize guesses. The algorithm has a recursive
15+
idea, but is actually implemented iteratively using a Stack, It also uses bitwise operations
16+
whenever possible to improve performance massively.
17+
1) Load the board from a string representation
18+
2) Search for cell whose value can be inferred. This is done in 2 ways:
19+
* Sole candidate: when a cell can only contain one number, because all the other ones are already taken in the row/column/block.
20+
* Unique candidate: when, in a row/column/block, a number can only be put in one cell. Because every number must appear
21+
once in every row/column/block, if only one cell can fit a determined number, it's definitely there.
22+
3) If the step 2 had success, complete that cell and do it again.
23+
4) Check if the board is complete, if so, return.
24+
5) Now, only guesses remain, so we have to find the cell with the least number of candidates.
25+
6) Guess a number on that cell and execute step 2 in the modified board.
26+
27+
This [website](https://www.conceptispuzzles.com/index.aspx?uri=puzzle/sudoku/techniques) explains some of the logic.
28+
1329
## Screenshots
1430
* ![Empty board](https://github.com/cau777/sudoku_solver/blob/master/screenshots/empty_board.png)
1531
* ![Solution step](https://github.com/cau777/sudoku_solver/blob/master/screenshots/solution_step.png)

src/App.css

+5
Original file line numberDiff line numberDiff line change
@@ -127,4 +127,9 @@ hr {
127127
border: none;
128128
outline: solid #4b9fd5 2px;
129129
background-color: #1F78B4;
130+
}
131+
132+
.subtitle {
133+
text-align: center;
134+
font-weight: 500;
130135
}

src/App.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ function App() {
1212
let {t} = useTranslation();
1313

1414
useEffect(() => {
15+
// Translate the window title when the translation is loaded
1516
document.title = t("title");
1617
}, [t])
1718

src/Message.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// All the observations that the Wasm code can make about a cell
12
export type Message =
23
{ t: "found", ms: number } |
34
{ t: "tried", num: number, row: number, col: number } |

src/SudokuBoard.tsx

+2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export const SudokuBoard: React.FC<Props> = (props) => {
2727
];
2828

2929
for (let s = 0; s < size; s++) {
30+
// Column number indicator
3031
firstRow.push(
3132
<td key={s} className={" block-row-start " + (s % blockSize === 0 ? " block-col-start " : "")}>
3233
<CellBase highlighted={s === props.highlightCol}>
@@ -43,6 +44,7 @@ export const SudokuBoard: React.FC<Props> = (props) => {
4344
let blockRow = Math.floor(r / blockSize);
4445

4546
let cells = [
47+
// Row number indicator
4648
<td key={"row nums " + r} className={" block-col-start " +
4749
(r % blockSize === 0 ? " block-row-start " : "")}>
4850
<CellBase highlighted={props.highlightRow === r}>

src/SudokuController.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,14 @@ export const SudokuController: React.FC<Props> = (props) => {
9696

9797
function changeCurrentStep(index: number, steps: Step[]) {
9898
if (index < 0 || index >= steps.length) return;
99+
// Translates the message using its key and other values as params
99100
let message = t(steps[index].message.t, {...steps[index].message});
100101

101102
if (steps.length !== 1)
103+
// If there are multiple messages, display the step number and then the message
102104
props.setLog(t("step", {num: index+1, message}));
103105
else
106+
// If there's only one message, just display it
104107
props.setLog(message);
105108
setState(s => ({...s, currentStep: index, steps}));
106109
}
@@ -141,13 +144,15 @@ export const SudokuController: React.FC<Props> = (props) => {
141144
<div>
142145
<hr/>
143146
</div>
147+
<div className={"subtitle"}>{t("generate")}</div>
144148
<button onClick={() => randomBoard(0.75)}>{t("generateRandomButton", {perc: 75})}</button>
145149
<button onClick={() => randomBoard(0.50)}>{t("generateRandomButton", {perc: 50})}</button>
146150
<button onClick={() => randomBoard(0.25)}>{t("generateRandomButton", {perc: 25})}</button>
147151
<button onClick={() => randomBoard(0.1)}>{t("generateRandomButton", {perc: 10})}</button>
148152
<div>
149153
<hr/>
150154
</div>
155+
<div className={"subtitle"}>{t("solve")}</div>
151156

152157
{state.steps === null ?
153158
<>

src/board.ts

+5-9
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,3 @@
1-
// export type Board = (number | null)[];
2-
//
3-
// export function defaultBoard(size: number) {
4-
// return new Array(size).fill(null);
5-
// }
6-
//
7-
// export function setCell(board: Board, row: number, col: number, size: number, value: number | undefined) {
8-
// board[row + "-" + col] =
9-
// }
101

112
export class Board {
123
readonly size: number;
@@ -16,10 +7,13 @@ export class Board {
167
this.size = blockSize * blockSize;
178
}
189

10+
// Create an empty board
1911
public static default(blockSize: number) {
2012
return new Board(blockSize, new Array(blockSize * blockSize * blockSize * blockSize).fill(null))
2113
}
2214

15+
// A literal a continuous string representation of the board, in the format "1 2 3 _ _ 6 7 8 _"
16+
// that can contain new lines
2317
public static fromLiteral(literal: string, blockSize: number) {
2418
let array = literal
2519
.replace("\n", " ")
@@ -40,10 +34,12 @@ export class Board {
4034
return this;
4135
}
4236

37+
// Deep copy of the object
4338
public copy() {
4439
return new Board(this.blockSize, [...this.cells]);
4540
}
4641

42+
// Converts the board to the format "1 2 3 _ _ 6 7 8 _"
4743
public toLiteral() {
4844
return this.cells.reduce((acc, value) => acc + (value ?? "_") + " ", "");
4945
}

src/i18n.ts

+13-7
Original file line numberDiff line numberDiff line change
@@ -2,21 +2,23 @@ import i18next from "i18next";
22
import {initReactI18next} from "react-i18next";
33
import LanguageDetector from "i18next-browser-languagedetector";
44

5+
// Update the html lang attribute
56
i18next.on("languageChanged", (lng) => document.documentElement.setAttribute("lang", lng));
67

78
i18next
89
.use(initReactI18next)
9-
.use(LanguageDetector)
10+
.use(LanguageDetector) // Uses the browser's language
1011
.init({
11-
debug: true,
12-
fallbackLng: "en",
12+
debug: process.env.NODE_ENV !== "production",
13+
fallbackLng: "en", // Defaults to English
1314
interpolation: {
14-
escapeValue: false
15+
escapeValue: false, // React automatically escapes values such as "<"
1516
},
17+
// All the translations are contained in this file for simplicity
1618
resources: {
1719
en: {
1820
translation: {
19-
"title": "Sudoku Solver",
21+
title: "Sudoku Solver",
2022
wrongSolutionRow: "Your solution is wrong. See row {{row}}",
2123
wrongSolutionCol: "Your solution is wrong. See column {{col}}",
2224
wrongSolutionBlock: "Your solution is wrong. See block {{blockRow}},{{blockCol}}",
@@ -35,13 +37,15 @@ i18next
3537
nextStepButton: "Next step",
3638
next10StepsButton: "Next 10 steps",
3739
step: "Step {{num}}: {{message}}",
38-
found: "Found solution in {{ms}}µ",
40+
found: "Found solution in {{ms}}µs",
3941
tried: "Tried number {{num}} in {{row}},{{col}}",
4042
gaveUp: "Gave up",
4143
canContainOnly: "Cell {{row}},{{col}} can only contain number {{num}}",
4244
numberOnlyFitsInRow: "Number {{num}} can only be placed in one cell in row {{row}}",
4345
numberOnlyFitsInCol: "Number {{num}} can only be placed in one cell in col {{col}}",
4446
numberOnlyFitsInBlock: "Number {{num}} can only be placed in one cell in block {{row}},{{col}}",
47+
generate: "Generate board",
48+
solve: "Solution",
4549
}
4650
},
4751
"pt-BR": {
@@ -65,13 +69,15 @@ i18next
6569
nextStepButton: "Avançar passo",
6670
next10StepsButton: "Avançar 10 passos",
6771
step: "Passo {{num}}: {{message}}",
68-
found: "Solução encontrada em {{ms}}µ",
72+
found: "Solução encontrada em {{ms}}µs",
6973
tried: "Tentar número {{num}} em {{row}},{{col}}",
7074
gaveUp: "Desistir",
7175
canContainOnly: "Casa {{row}},{{col}} apenas pode conter o número {{num}}",
7276
numberOnlyFitsInRow: "O número {{num}} apenas pode ser colocado em uma casa na linha {{row}}",
7377
numberOnlyFitsInCol: "O número {{num}} apenas pode ser colocado em uma casa na coluna {{col}}",
7478
numberOnlyFitsInBlock: "O número {{num}} apenas pode ser colocado em uma casa no bloco {{row}},{{col}}",
79+
generate: "Gerar tabuleiro",
80+
solve: "Solução",
7581
}
7682
}
7783
}

wasm/src/lib.rs

+2-2
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ fn solve_with_size<const SIZE: usize, const BLOCK_SIZE: usize>(board_literal: &s
4747

4848
for step in solver.steps {
4949
steps.push(object! {
50-
message: step.message.to_object(),
50+
message: step.message.into_object(),
5151
highlightRow: step.highlight_row,
5252
highlightCol: step.highlight_col,
5353
highlightBlock: step.highlight_block.map(|[a, b]| array![a, b]),
@@ -57,7 +57,7 @@ fn solve_with_size<const SIZE: usize, const BLOCK_SIZE: usize>(board_literal: &s
5757

5858
let solution = result.unwrap().to_literal();
5959
steps.push(object! {
60-
message: Message::Found(elapsed as u64).to_object(),
60+
message: Message::Found(elapsed as u64).into_object(),
6161
highlightRow: JsonValue::Null,
6262
highlightCol: JsonValue::Null,
6363
highlightBlock: JsonValue::Null,

wasm/src/solve_report.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ pub enum Message {
1111
}
1212

1313
impl Message {
14-
pub fn to_object(self) -> JsonValue {
14+
pub fn into_object(self) -> JsonValue {
1515
use Message::*;
1616
match self {
1717
Found(ms) => object! {

wasm/src/sudoku_board.rs

+19-17
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ use crate::util::Array2D;
55

66
pub type DefaultBoard = SudokuBoard<9, 3>;
77

8+
/// Struct that keeps track of the numbers in the board and also what values are already used
9+
/// in each row/column/block
810
#[derive(Clone, Eq, PartialEq)]
911
pub struct SudokuBoard<const SIZE: usize, const BLOCK_SIZE: usize> {
1012
pub numbers: Array2D<Option<u8>, SIZE>,
@@ -31,16 +33,14 @@ impl<const SIZE: usize, const BLOCK_SIZE: usize> SudokuBoard<SIZE, BLOCK_SIZE> {
3133

3234
pub fn set_number(&mut self, value: Option<u8>, row: usize, col: usize) {
3335
let prev = self.numbers[row][col];
34-
if prev.is_some() {
35-
let val = prev.unwrap();
36+
if let Some(val) = prev {
3637
self.rows[row].remove_number(val);
3738
self.cols[col].remove_number(val);
3839
self.blocks[row / BLOCK_SIZE][col / BLOCK_SIZE].remove_number(val);
3940
self.numbers[row][col] = None;
4041
}
4142

42-
if value.is_some() {
43-
let val = value.unwrap();
43+
if let Some(val) = value {
4444
self.rows[row].add_number(val);
4545
self.cols[col].add_number(val);
4646
self.blocks[row / BLOCK_SIZE][col / BLOCK_SIZE].add_number(val);
@@ -60,9 +60,9 @@ impl<const SIZE: usize, const BLOCK_SIZE: usize> SudokuBoard<SIZE, BLOCK_SIZE> {
6060
let mut board = SudokuBoard::new();
6161

6262
literal
63-
.replace('\n', &" ")
64-
.split(" ")
65-
.filter(|o| o.len() != 0)
63+
.replace('\n', " ")
64+
.split(' ')
65+
.filter(|o| !o.is_empty())
6666
.enumerate()
6767
.for_each(|(index, o)| {
6868
board.set_number(u8::from_str(o).ok(), index / SIZE, index % SIZE)
@@ -71,6 +71,8 @@ impl<const SIZE: usize, const BLOCK_SIZE: usize> SudokuBoard<SIZE, BLOCK_SIZE> {
7171
board
7272
}
7373

74+
/// Return a more compact representation of the board, in the format "1 2 3 _ _ 6 7 8 _"
75+
/// without newlines
7476
pub fn to_literal(&self) -> String {
7577
let mut result = String::new();
7678
for row in &self.numbers {
@@ -91,10 +93,10 @@ impl<const SIZE: usize, const BLOCK_SIZE: usize> SudokuBoard<SIZE, BLOCK_SIZE> {
9193
let mut board = SudokuBoard::new();
9294

9395
for (index, number) in literal
94-
.replace('\n', &" ")
95-
.split(" ")
96-
.filter(|o| o.len() != 0)
97-
.map(|o| u8::from_str(o))
96+
.replace('\n', " ")
97+
.split(' ')
98+
.filter(|o| !o.is_empty())
99+
.map(u8::from_str)
98100
.enumerate()
99101
.filter(|(_, o)| o.is_ok())
100102
.map(|(index, o)| (index, o.unwrap())) {
@@ -132,14 +134,19 @@ impl<const SIZE: usize, const BLOCK_SIZE: usize> SudokuBoard<SIZE, BLOCK_SIZE> {
132134
true
133135
}
134136

137+
/// Returns a readable representation of the board
135138
pub fn board_to_string(&self) -> String {
136139
let mut result = String::new();
137140
result += " ---------------------\n";
138141

139142
for row in 0..SIZE {
140143
result += &format!("{} | ", row);
141144
for col in 0..SIZE {
142-
result += &(self.numbers[row][col].map(|x| x.to_string()).unwrap_or("_".to_owned()).to_string() + " ");
145+
match self.numbers[row][col] {
146+
Some(x) => result += &x.to_string(),
147+
None => result += "_",
148+
}
149+
result += " ";
143150
}
144151
result += "|\n";
145152
}
@@ -160,11 +167,6 @@ impl<const SIZE: usize, const BLOCK_SIZE: usize> Debug for SudokuBoard<SIZE, BLO
160167
mod tests {
161168
use crate::sudoku_board::{DefaultBoard};
162169

163-
#[test]
164-
fn util() {
165-
assert!(true);
166-
}
167-
168170
#[test]
169171
fn empty_board() {
170172
let board = DefaultBoard::new();

0 commit comments

Comments
 (0)