Sudoku review #1
34
reviews/BordedDev/notes.md
Normal file
@ -0,0 +1,34 @@
|
||||
It doesn’t render correctly on firefox but does in edge, it’s a rectangle, not a square.
|
||||
Since the scaling is based on the font size, using `em` + `line-height: 1` allows it to scale correctly by adding it to
|
||||
`.sudoku-field`.
|
||||
Fixing it by wrapping the numbers and adding `height: 100%` fixes the individual cells, but the whole table is still a
|
||||
rectangle but causes the cells to overlap (inside out size issues was excuse).
|
||||
|
||||
There is a bunch of unused code
|
||||
|
||||
Autosolver is broken, I had to move the function around
|
||||
|
||||
|
||||
Switched .forEach to for loop for performance (recommended practice, but honestly it’s not a big deal)
|
||||
|
||||
Convert the keyboard key lookup to a map, mostly stylistic but I imagine it’s also easier to edit.
|
||||
|
||||
Not sure why there is a col and row class that manages cells if the layout is flat
|
||||
|
||||
Getters, setter, properties, functions and constructors are all mixed together, it's hard to read
|
||||
|
||||
A custom event manager is used, EventTarget is built into the browser not sure why it’s not used
|
||||
|
||||
Usage of `_` instead of `#` for private variables
|
||||
|
||||
It's not recommended to setup the elements in the constructor https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements#implementing_a_custom_element
|
||||
|
||||
You can use `||=` for assigning to an undefined variable (if it's undefined)
|
||||
|
||||
Borders are on all grid elements, causing a double border on the edges
|
||||
|
||||
I'd recommend running Biome.js or ESlint + Prettier to clean up the code
|
||||
(if you're using ML you can even make these strict enough about ordering and naming conventions)
|
||||
|
||||
When you create an object where the key's and value's name are the same, you can use the shorthand `{ value }` instead of `{ value: value }`
|
||||
|
||||
Border radius doesn't propagate to the children, so the cells are still square and will show if it has a background
|
925
reviews/BordedDev/sudoku.js
Normal file
@ -0,0 +1,925 @@
|
||||
function randInt(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function cssRuleExists(match) {
|
||||
for (let i = 0; i < document.styleSheets.length; i++) {
|
||||
let styleSheet = document.styleSheets[i]
|
||||
let rules = styleSheet.cssRules
|
||||
for (let j = 0; j < rules.length; j++) {
|
||||
let rule = rules[j]
|
||||
if (rule.selectorText && rule.selectorText == match) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
class EventHandler {
|
||||
BordedDev
commented
A pretty standard, pretty robust way of handling events. I'd lean to the built-in EventTarget until the need for something better is needed. A pretty standard, pretty robust way of handling events. I'd lean to the built-in [EventTarget](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget) until the need for something better is needed.
|
||||
constructor() {
|
||||
this.events = {};
|
||||
this.eventCount = 0;
|
||||
this.suppresEvents = false;
|
||||
this.debugEvents = false;
|
||||
}
|
||||
|
||||
on(event, listener) {
|
||||
if (!this.events[event]) {
|
||||
this.events[event] = {name: name, listeners: [], callCount: 0};
|
||||
}
|
||||
this.events[event].listeners.push(listener);
|
||||
}
|
||||
|
||||
off(event, listenerToRemove) {
|
||||
if (!this.events[event]) return;
|
||||
this.events[event].listeners = this.events[event].listeners.filter(listener => listener !== listenerToRemove);
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (!this.events[event]) return [];
|
||||
if (this.suppresEvents) return [];
|
||||
this.eventCount++;
|
||||
const returnValue = this.events[event].listeners.map(listener => {
|
||||
var returnValue = listener(data)
|
||||
BordedDev
commented
var :( var :(
|
||||
if (returnValue == undefined)
|
||||
return null
|
||||
return returnValue
|
||||
});
|
||||
this.events[event].callCount++;
|
||||
if (this.debugEvents) {
|
||||
console.debug('debugEvent', {
|
||||
event: event,
|
||||
arg: data,
|
||||
callCount: this.events[event].callCount,
|
||||
number: this.eventCount,
|
||||
returnValues: returnValue
|
||||
})
|
||||
}
|
||||
return returnValue
|
||||
}
|
||||
|
||||
suppres(fn) {
|
||||
const originallySuppressed = this.suppresEvents
|
||||
this.suppresEvents = true
|
||||
fn(this)
|
||||
this.suppresEvents = originallySuppressed
|
||||
return originallySuppressed
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class Col extends EventHandler {
|
||||
id = 0
|
||||
values = []
|
||||
initial = false
|
||||
_marked = false
|
||||
row = null
|
||||
index = 0
|
||||
valid = true
|
||||
_selected = false
|
||||
_value = 0
|
||||
|
||||
_isValidXy() {
|
||||
if (!this._value)
|
||||
return true;
|
||||
return this.row.puzzle.fields.filter(field => {
|
||||
return (
|
||||
field.index == this.index && field.value == this._value
|
||||
||
|
||||
field.row.index == this.row.index && field.value == this._value)
|
||||
|
||||
}).filter(field => field != this).length == 0
|
||||
BordedDev
commented
A double filter, it's inefficient (since both will loop through the array) but for the size it doesn't really matter A double filter, it's inefficient (since both will loop through the array) but for the size it doesn't really matter
|
||||
}
|
||||
|
||||
mark() {
|
||||
this.marked = true
|
||||
}
|
||||
|
||||
unmark() {
|
||||
this.marked = false
|
||||
}
|
||||
|
||||
select() {
|
||||
this.selected = true
|
||||
}
|
||||
|
||||
unselect() {
|
||||
this.selected = false
|
||||
}
|
||||
|
||||
_isValidBox() {
|
||||
if (!this._value)
|
||||
return true;
|
||||
let startRow = this.row.index - this.row.index % (this.row.puzzle.size / 3);
|
||||
let startCol = this.index - this.index % (this.row.puzzle.size / 3);
|
||||
for (let i = 0; i < this.row.puzzle.size / 3; i++) {
|
||||
for (let j = 0; j < this.row.puzzle.size / 3; j++) {
|
||||
const field = this.row.puzzle.get(i + startRow, j + startCol);
|
||||
if (field != this && field.value == this._value) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
validate() {
|
||||
if (!this.row.puzzle.initalized) {
|
||||
return this.valid;
|
||||
}
|
||||
if (this.initial) {
|
||||
this.valid = true
|
||||
return this.valid
|
||||
}
|
||||
if (!this.value && !this.valid) {
|
||||
this.valid = true
|
||||
this.emit('update', this)
|
||||
return this.valid
|
||||
}
|
||||
let oldValue = this.valid
|
||||
this.valid = this._isValidXy() && this._isValidBox();
|
||||
if (oldValue != this.valid) {
|
||||
this.emit('update', this)
|
||||
}
|
||||
return this.valid
|
||||
}
|
||||
|
||||
set value(val) {
|
||||
if (this.initial)
|
||||
return;
|
||||
const digit = Number(val)
|
||||
const validDigit = digit >= 0 && digit <= 9;
|
||||
let update = validDigit && digit != this.value
|
||||
if (update) {
|
||||
this._value = Number(digit)
|
||||
this.validate()
|
||||
this.emit('update', this);
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value
|
||||
}
|
||||
|
||||
get selected() {
|
||||
return this._selected
|
||||
}
|
||||
|
||||
set selected(val) {
|
||||
if (val != this._selected) {
|
||||
this._selected = val
|
||||
if (this.row.puzzle.initalized)
|
||||
this.emit('update', this);
|
||||
}
|
||||
}
|
||||
|
||||
get marked() {
|
||||
return this._marked
|
||||
}
|
||||
|
||||
set marked(val) {
|
||||
if (val != this._marked) {
|
||||
this._marked = val
|
||||
if (this.row.puzzle.initalized) {
|
||||
this.emit('update', this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(row) {
|
||||
super()
|
||||
this.row = row
|
||||
this.index = this.row.cols.length
|
||||
this.id = this.row.puzzle.rows.length * this.row.puzzle.size + this.index;
|
||||
this.initial = false
|
||||
this.selected = false
|
||||
this._value = 0;
|
||||
this.marked = false
|
||||
this.valid = true
|
||||
}
|
||||
|
||||
update() {
|
||||
this.emit('update', this)
|
||||
}
|
||||
|
||||
toggleSelected() {
|
||||
this.selected = !this.selected
|
||||
}
|
||||
|
||||
toggleMarked() {
|
||||
this.marked = !this.marked
|
||||
}
|
||||
|
||||
get data() {
|
||||
return {
|
||||
values: this.values,
|
||||
value: this.value,
|
||||
index: this.index,
|
||||
id: this.id,
|
||||
row: this.row.index,
|
||||
col: this.index,
|
||||
valid: this.valid,
|
||||
initial: this.initial,
|
||||
selected: this.selected,
|
||||
marked: this.marked
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
BordedDev
commented
Seeing someone implement a custom Seeing someone implement a custom `toString` is always nice
|
||||
return String(this.value)
|
||||
}
|
||||
|
||||
toText() {
|
||||
return this.toString().replace("0", " ");
|
||||
}
|
||||
}
|
||||
|
||||
class Row extends EventHandler {
|
||||
BordedDev
commented
The Row/Col classes are confusing in the current situation, since Row isn't used for more than a bag holder/event forwarding The Row/Col classes are confusing in the current situation, since Row isn't used for more than a bag holder/event forwarding
|
||||
cols = []
|
||||
puzzle = null
|
||||
index = 0
|
||||
initialized = false
|
||||
|
||||
constructor(puzzle) {
|
||||
super()
|
||||
this.puzzle = puzzle
|
||||
this.cols = []
|
||||
this.index = this.puzzle.rows.length
|
||||
const me = this
|
||||
BordedDev
commented
A lot of these A lot of these `me` proxies aren't necessary with the arrow functions you're using, they're removed in the modified version
|
||||
this.initialized = false
|
||||
for (let i = 0; i < puzzle.size; i++) {
|
||||
const col = new Col(this);
|
||||
this.cols.push(col);
|
||||
col.on('update', (field) => {
|
||||
me.emit('update', field)
|
||||
})
|
||||
}
|
||||
this.initialized = true
|
||||
}
|
||||
|
||||
get data() {
|
||||
return {
|
||||
cols: this.cols.map(col => col.data),
|
||||
index: this.index
|
||||
}
|
||||
}
|
||||
|
||||
toText() {
|
||||
let result = ''
|
||||
for (let col of this.cols) {
|
||||
result += col.toText();
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.toText().replaceAll(" ", "0");
|
||||
}
|
||||
}
|
||||
|
||||
class Puzzle extends EventHandler {
|
||||
rows = []
|
||||
size = 0
|
||||
hash = 0
|
||||
states = []
|
||||
parsing = false
|
||||
_initialized = false
|
||||
initalized = false
|
||||
BordedDev
commented
Why are there 2 initialized properties? Why are there 2 initialized properties?
retoor
commented
Even worse, one is wrongly written :P Even worse, one is wrongly written :P
|
||||
_fields = null
|
||||
|
||||
constructor(arg) {
|
||||
super()
|
||||
this.debugEvents = true;
|
||||
this.initalized = false
|
||||
this.rows = []
|
||||
if (isNaN(arg)) {
|
||||
BordedDev
commented
Nice usage of the coercion, even if my IDE complains because there is coercion Nice usage of the coercion, even if my IDE complains because there is coercion
|
||||
// load session
|
||||
} else {
|
||||
this.size = Number(arg)
|
||||
}
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
const row = new Row(this);
|
||||
this.rows.push(row);
|
||||
row.on('update', (field) => {
|
||||
this.onFieldUpdate(field)
|
||||
})
|
||||
}
|
||||
this._initialized = true
|
||||
this.initalized = true
|
||||
this.commitState()
|
||||
}
|
||||
|
||||
validate() {
|
||||
return this.valid;
|
||||
}
|
||||
|
||||
_onEventHandler() {
|
||||
this.eventCount++;
|
||||
}
|
||||
|
||||
makeInvalid() {
|
||||
if (!app.valid) {
|
||||
let invalid = this.invalid;
|
||||
return invalid[invalid.length - 1];
|
||||
}
|
||||
this.rows.forEach(row => {
|
||||
row.cols.forEach(col => {
|
||||
if (col.value) {
|
||||
let modify = null;
|
||||
if (col.index == this.size) {
|
||||
modify = this.get(row.index, col.index - 2);
|
||||
} else {
|
||||
modify = this.get(row.index, col.index + 1);
|
||||
}
|
||||
modify.value = col.value
|
||||
// last one is invalid
|
||||
return modify.index > col.index ? modify : col;
|
||||
}
|
||||
col.valid = false
|
||||
})
|
||||
})
|
||||
this.get(0, 0).value = 1;
|
||||
this.get(0, 1).value = 1;
|
||||
return this.get(0, 1);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this._initialized = false
|
||||
this.initalized == false;
|
||||
BordedDev
commented
Accidental comparison 😅 Accidental comparison 😅
retoor
commented
Oh fuck, that can maybe be the reason something is not working or why even the second variable is implemented. Because I really don't know what the reason was. Oh fuck, that can maybe be the reason something is not working or why even the second variable is implemented. Because I really don't know what the reason was.
|
||||
this.parsing = true
|
||||
this.fields.forEach(field => {
|
||||
field.initial = false
|
||||
field.selected = false
|
||||
field.marked = false
|
||||
field.value = 0
|
||||
})
|
||||
this.hash = 0
|
||||
this.states = []
|
||||
this.parsing = false
|
||||
this.initalized = true
|
||||
this._initialized = true
|
||||
this.commitState()
|
||||
}
|
||||
|
||||
get valid() {
|
||||
return this.invalid.length == 0
|
||||
}
|
||||
|
||||
get invalid() {
|
||||
this.emit('validating', this)
|
||||
const result = this.fields.filter(field => !field.validate())
|
||||
this.emit('validated', this)
|
||||
return result
|
||||
}
|
||||
|
||||
get selected() {
|
||||
return this.fields.filter(field => field.selected)
|
||||
}
|
||||
|
||||
get marked() {
|
||||
return this.fields.filter(field => field.marked)
|
||||
}
|
||||
|
||||
loadString(content) {
|
||||
this.emit('parsing', this)
|
||||
this.reset()
|
||||
this.parsing = true
|
||||
this.initalized = false;
|
||||
this._initialized = false;
|
||||
|
||||
const regex = /\d/g;
|
||||
const matches = [...content.matchAll(regex)]
|
||||
BordedDev
commented
Regex strikes again 🏃💨. Technically inefficient, practically I like it. BTW it's intentionally not an array it returns so that you can efficiently stream the results through the iterator (that's how I understand the below)
Regex strikes again 🏃💨. Technically inefficient, practically I like it.
BTW it's intentionally not an array it returns so that you can efficiently stream the results through the iterator (that's how I understand the below)
>With matchAll() available, you can avoid the while loop and exec with g. Instead, you get an iterator to use with the more convenient for...of, array spreading, or Array.from() constructs:
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/matchAll
|
||||
let index = 0;
|
||||
const max = this.size * this.size;
|
||||
matches.forEach(match => {
|
||||
const digit = Number(match[0]);
|
||||
let field = this.fields[index]
|
||||
field.value = digit;
|
||||
field.initial = digit != 0
|
||||
index++;
|
||||
});
|
||||
this._initialized = true;
|
||||
this.parsing = false
|
||||
this.deselect();
|
||||
this.initalized = true;
|
||||
this.suppres(() => {
|
||||
this.fields.forEach((field) => {
|
||||
field.update()
|
||||
})
|
||||
})
|
||||
this.commitState()
|
||||
this.emit('parsed', this)
|
||||
this.emit('update', this)
|
||||
}
|
||||
|
||||
get state() {
|
||||
return this.getData(true)
|
||||
}
|
||||
|
||||
get previousState() {
|
||||
if (this.states.length == 0)
|
||||
return null;
|
||||
return this.states.at(this.states.length - 1)
|
||||
}
|
||||
|
||||
get stateChanged() {
|
||||
if (!this._initialized)
|
||||
return false
|
||||
return !this.previousState || this.state != this.previousState
|
||||
}
|
||||
|
||||
commitState() {
|
||||
if (!this.initalized)
|
||||
return false;
|
||||
this.hash = this._generateHash()
|
||||
if (this.stateChanged) {
|
||||
this.states.push(this.state)
|
||||
this.emit('commitState', this)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
onFieldUpdate(field) {
|
||||
if (!this.initalized)
|
||||
return false;
|
||||
if (!this._initialized)
|
||||
return;
|
||||
this.validate();
|
||||
this.commitState();
|
||||
this.emit('update', this)
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this.getData(true)
|
||||
}
|
||||
|
||||
popState() {
|
||||
let prevState = this.previousState
|
||||
if (!prevState) {
|
||||
this.deselect()
|
||||
return null
|
||||
}
|
||||
while (prevState && prevState.hash == this.state.hash)
|
||||
prevState = this.states.pop()
|
||||
if (!prevState) {
|
||||
this.deselect()
|
||||
return null
|
||||
}
|
||||
this.applyState(prevState)
|
||||
this.emit('popState', this)
|
||||
return prevState
|
||||
}
|
||||
|
||||
applyState(newState) {
|
||||
|
||||
this._initialized = false
|
||||
newState.fields.forEach(stateField => {
|
||||
let field = this.get(stateField.row, stateField.col)
|
||||
field.selected = stateField.selected
|
||||
field.values = stateField.values
|
||||
field.value = stateField.value
|
||||
field.initial = stateField.initial
|
||||
field.validate()
|
||||
})
|
||||
this._initialized = true
|
||||
this.emit('stateApplied', this)
|
||||
this.emit('update', this)
|
||||
}
|
||||
|
||||
getData(withHash = false) {
|
||||
let result = {
|
||||
fields: this.fields.map(field => field.data),
|
||||
size: this.size,
|
||||
valid: this.valid
|
||||
}
|
||||
if (withHash) {
|
||||
result['hash'] = this._generateHash()
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
get(row, col) {
|
||||
if (!this.initalized)
|
||||
return null;
|
||||
if (!this.rows.length)
|
||||
return null;
|
||||
if (!this.rows[row])
|
||||
return null;
|
||||
return this.rows[row].cols[col];
|
||||
}
|
||||
|
||||
get fields() {
|
||||
if (this._fields == null) {
|
||||
this._fields = []
|
||||
for (let row of this.rows) {
|
||||
for (let col of row.cols) {
|
||||
this._fields.push(col)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._fields
|
||||
}
|
||||
|
||||
_generateHash() {
|
||||
var result = 0;
|
||||
JSON.stringify(this.getData(false)).split('').map(char => {
|
||||
return char.charCodeAt(0) - '0'.charCodeAt(0)
|
||||
}).forEach(num => {
|
||||
result += 26
|
||||
result = result + num
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
get text() {
|
||||
let result = ''
|
||||
for (let row of this.rows) {
|
||||
result += row.toText() + "\n"
|
||||
}
|
||||
result = result.slice(0, result.length - 1)
|
||||
return result
|
||||
}
|
||||
|
||||
get initialFields() {
|
||||
return this.fields.filter(field => field.initial)
|
||||
}
|
||||
|
||||
get json() {
|
||||
return JSON.stringify(this.data)
|
||||
}
|
||||
|
||||
get zeroedText() {
|
||||
return this.text.replaceAll(" ", "0")
|
||||
}
|
||||
|
||||
get string() {
|
||||
return this.toString()
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.text.replaceAll("\n", "").replaceAll(" ", "0")
|
||||
}
|
||||
|
||||
get humanFormat() {
|
||||
return ' ' + this.text.replaceAll(" ", "0").split("").join(" ")
|
||||
}
|
||||
|
||||
getRandomField() {
|
||||
const emptyFields = this.empty;
|
||||
return emptyFields[randInt(0, emptyFields.length - 1)]
|
||||
}
|
||||
|
||||
update(callback) {
|
||||
this.commitState()
|
||||
this.intalized = false
|
||||
callback(this);
|
||||
this.intalized = true
|
||||
this.validate()
|
||||
this.commitState()
|
||||
this.intalized = false
|
||||
if (this.valid)
|
||||
this.deselect()
|
||||
this.intalized = true
|
||||
this.emit('update', this)
|
||||
}
|
||||
|
||||
get empty() {
|
||||
return this.fields.filter(field => field.value == 0)
|
||||
}
|
||||
|
||||
getRandomEmptyField() {
|
||||
let field = this.getRandomField()
|
||||
if (!field)
|
||||
return null
|
||||
return field
|
||||
}
|
||||
|
||||
deselect() {
|
||||
this.fields.forEach(field => field.selected = false)
|
||||
}
|
||||
|
||||
generate() {
|
||||
this.reset()
|
||||
this.initalized = false
|
||||
for (let i = 0; i < 17; i++) {
|
||||
this.fillRandomField()
|
||||
}
|
||||
this.deselect()
|
||||
this.initalized = true
|
||||
this.commitState()
|
||||
this.emit('update', this)
|
||||
}
|
||||
|
||||
fillRandomField() {
|
||||
let field = this.getRandomEmptyField()
|
||||
if (!field)
|
||||
return
|
||||
this.deselect()
|
||||
field.selected = true
|
||||
let number = 0
|
||||
number++;
|
||||
|
||||
while (number <= 9) {
|
||||
field.value = randInt(1, 9)
|
||||
field.update()
|
||||
if (this.validate()) {
|
||||
field.initial = true
|
||||
return field
|
||||
}
|
||||
number++;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
class PuzzleManager {
|
||||
constructor(size) {
|
||||
this.size = size
|
||||
this.puzzles = []
|
||||
this._activePuzzle = null
|
||||
}
|
||||
|
||||
addPuzzle(puzzle) {
|
||||
this.puzzles.push(puzzle)
|
||||
}
|
||||
|
||||
get active() {
|
||||
return this.activePuzzle
|
||||
}
|
||||
|
||||
set activePuzzle(puzzle) {
|
||||
this._activePuzzle = puzzle
|
||||
}
|
||||
|
||||
get activePuzzle() {
|
||||
return this._activePuzzle
|
||||
}
|
||||
}
|
||||
|
||||
const puzzleManager = new PuzzleManager(9)
|
||||
|
||||
class Sudoku extends HTMLElement {
|
||||
styleSheet = `
|
||||
.sudoku {
|
||||
font-size: 13px;
|
||||
color:#222;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(9, 1fr);
|
||||
grid-template-rows: auto;
|
||||
gap: 0px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
background-color: #e5e5e5;
|
||||
border-radius: 5px;
|
||||
aspect-ratio: 1/1;
|
||||
BordedDev
commented
Unfortunately, this doesn't work with dynamic inside out sizing/nested aspect-ration on firefox. Luckily, you can use Unfortunately, this doesn't work with dynamic inside out sizing/nested aspect-ration on firefox. Luckily, you can use `em` since the size is based on font size
retoor
commented
So they're not nice squares on firefox? Upgrade to chrome! So they're not nice squares on firefox? Upgrade to chrome!
BordedDev
commented
Never! Google can claw my faux-privacy from my cold dead hands (also ublock). I've been considering using chromium/vavaldi so I can get functional HDR (right now I use edge when I need chrome) Never! Google can claw my faux-privacy from my cold dead hands (also ublock). I've been considering using chromium/vavaldi so I can get functional HDR (right now I use edge when I need chrome)
|
||||
}
|
||||
.sudoku-field-initial {
|
||||
BordedDev
commented
You can combine styles as well, I find it to be a bit easier to work with on average since you can toggle and combine them when it's needed much easier.
You can combine styles as well, I find it to be a bit easier to work with on average since you can toggle and combine them when it's needed much easier.
Also CSS now supports nested styling rules:
```css
.sudoku-field {
&.initial {
color: #777;
}
&.selected {
background-color: lightgreen;
}
&.marked {
background-color: blue;
}
&.invalid {
color: red;
}
}
```
retoor
commented
Ah facks. Thanks. Yh, my css skills are very outdated. Ah facks. Thanks. Yh, my css skills are very outdated.
BordedDev
commented
Still better than my junior. I was very surprised, learning about the CSS upgrade ~this~ last year Still better than my junior. I was very surprised, learning about the CSS upgrade ~this~ last year
|
||||
color: #777;
|
||||
}
|
||||
.sudoku-field-selected {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
.soduku-field-marked {
|
||||
background-color: blue;
|
||||
}
|
||||
.sudoku-field-invalid {
|
||||
color: red;
|
||||
}
|
||||
.sudoku-field {
|
||||
border: 1px solid #ccc;
|
||||
BordedDev
commented
Not sure if the double border was intentional, I assume it wasn't. The way I solved it was making the border on the bottom and right, you can use the nth selector to disable it on the edges if need be Not sure if the double border was intentional, I assume it wasn't.
The way I solved it was making the border on the bottom and right, you can use the nth selector to disable it on the edges if need be
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
aspect-ratio: 1/1;
|
||||
}`
|
||||
|
||||
set fieldSize(val) {
|
||||
this._fieldSize = val ? Number(val) : null
|
||||
this.fieldElements.forEach(field => {
|
||||
field.style.fontSize = this._fieldSize ? this._fieldSize.toString() + 'px' : ''
|
||||
})
|
||||
}
|
||||
|
||||
get fieldSize() {
|
||||
return this._fieldSize
|
||||
}
|
||||
|
||||
get eventCount() {
|
||||
return this.puzzle.eventCount
|
||||
}
|
||||
|
||||
get puzzleContent() {
|
||||
return this.puzzle.humanFormat
|
||||
}
|
||||
|
||||
set puzzleContent(val) {
|
||||
if (val == "generate") {
|
||||
this.puzzle.generate()
|
||||
} else if (val) {
|
||||
this.puzzle.loadString(val)
|
||||
} else {
|
||||
this.puzzle.reset()
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.puzzleContent = this.getAttribute('puzzle') ? this.getAttribute('puzzle') : null
|
||||
this._fieldSize = null
|
||||
this.fieldSize = this.getAttribute('size') ? this.getAttribute('size') : null
|
||||
this.readOnly = !!this.getAttribute('read-only')
|
||||
this.attachShadow({mode: 'open'});
|
||||
this.shadowRoot.appendChild(this.styleElement)
|
||||
this.shadowRoot.appendChild(this.puzzleDiv)
|
||||
}
|
||||
|
||||
toString() {
|
||||
return this.puzzleContent
|
||||
}
|
||||
|
||||
set active(val) {
|
||||
this._active = val
|
||||
if (this._active)
|
||||
this.manager.activePuzzle = this
|
||||
}
|
||||
|
||||
get active() {
|
||||
return this._active
|
||||
}
|
||||
|
||||
set readOnly(val) {
|
||||
this._readOnly = !!val
|
||||
}
|
||||
|
||||
get readOnly() {
|
||||
return this._readOnly
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this._readOnly = false;
|
||||
this._active = false
|
||||
this.fieldElements = []
|
||||
this.puzzle = new Puzzle(9)
|
||||
this.fields = []
|
||||
this.styleElement = document.createElement('style');
|
||||
BordedDev
commented
From what I've read, it's not recommended to create elements in the constructor and instead should be done in the From what I've read, it's not recommended to create elements in the constructor and instead should be done in the `connectedCallback`
retoor
commented
I'm not sure about that. Will look it up, but you kinda want to have the component ready right when it get 'connected'? I also use that event somewhere I guess. I'm not sure about that. Will look it up, but you kinda want to have the component ready right when it get 'connected'? I also use that event somewhere I guess.
BordedDev
commented
It's here: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements
That's what the connected event is for. They break that "rule" in the example on the same page, and yeah, it gets called through the It's here: https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_custom_elements
> In the class constructor, you can set up initial state and default values, register event listeners and perhaps create a shadow root. At this point, you should not inspect the element's attributes or children, or add new attributes or children. See Requirements for custom element constructors and reactions for the complete set of requirements.
That's what the connected event is for. They break that "rule" in the example on the same page, and yeah, it gets called through the `_bind` function. I think the comment was originally meant to be on the `_bind` call in the constructor
|
||||
this.styleElement.textContent = this.styleSheet
|
||||
this.puzzleDiv = document.createElement('div')
|
||||
this.puzzleDiv.classList.add('sudoku');
|
||||
this._bind()
|
||||
this.manager.addPuzzle(this)
|
||||
}
|
||||
|
||||
get manager() {
|
||||
return puzzleManager
|
||||
}
|
||||
|
||||
_bind() {
|
||||
this._bindFields()
|
||||
this._bindEvents()
|
||||
this._sync()
|
||||
}
|
||||
|
||||
_bindFields() {
|
||||
const me = this
|
||||
this.puzzle.rows.forEach((row) => {
|
||||
row.cols.forEach((field) => {
|
||||
const fieldElement = document.createElement('div');
|
||||
fieldElement.classList.add('sudoku-field');
|
||||
fieldElement.field = field
|
||||
field.on('update', (field) => {
|
||||
me._sync()
|
||||
})
|
||||
fieldElement.addEventListener('click', (e) => {
|
||||
if (!me.readOnly)
|
||||
field.toggleSelected()
|
||||
})
|
||||
fieldElement.addEventListener('contextmenu', (e) => {
|
||||
e.preventDefault()
|
||||
field.row.puzzle.update(() => {
|
||||
field.selected = false
|
||||
field.value = 0
|
||||
})
|
||||
})
|
||||
this.fields.push(field)
|
||||
this.fieldElements.push(fieldElement)
|
||||
this.puzzleDiv.appendChild(fieldElement);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_bindEvents() {
|
||||
const me = this
|
||||
this.puzzle.on('update', () => {
|
||||
me._sync()
|
||||
});
|
||||
this.puzzleDiv.addEventListener('mouseenter', (e) => {
|
||||
me.active = true
|
||||
})
|
||||
this.puzzleDiv.addEventListener('mouseexit', (e) => {
|
||||
me.active = false
|
||||
})
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (me.readOnly)
|
||||
return
|
||||
if (!puzzleManager.active)
|
||||
return
|
||||
const puzzle = puzzleManager.active.puzzle
|
||||
if (e.key == 'u') {
|
||||
puzzle.popState();
|
||||
} else if (e.key == 'd') {
|
||||
puzzle.update((target) => {
|
||||
puzzle.selected.forEach(field => {
|
||||
field.value = 0
|
||||
});
|
||||
})
|
||||
} else if (e.key == 'a') {
|
||||
puzzle.autoSolve()
|
||||
} else if (e.key == 'r') {
|
||||
puzzle.fillRandomField();
|
||||
} else if (!isNaN(e.key)) {
|
||||
puzzle.update((target) => {
|
||||
puzzle.selected.forEach(field => {
|
||||
field.value = Number(e.key)
|
||||
})
|
||||
});
|
||||
} else if (e.key == 'm') {
|
||||
let fields = [];
|
||||
puzzle.update((target) => {
|
||||
target.selected.forEach(field => {
|
||||
field.selected = false;
|
||||
fields.push(field)
|
||||
});
|
||||
});
|
||||
puzzle.update((target) => {
|
||||
fields.forEach((field) => {
|
||||
field.toggleMarked();
|
||||
})
|
||||
});
|
||||
puzzle.emit('update', puzzle);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
autoSolve() {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (this.fillRandomField()) {
|
||||
if (this.empty.length)
|
||||
return this.autoSolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
get(row, col) {
|
||||
return this.puzzle.get(row, col)
|
||||
}
|
||||
|
||||
_syncField(fieldElement) {
|
||||
const field = fieldElement.field
|
||||
fieldElement.classList.remove('sudoku-field-selected', 'sudoku-field-empty', 'sudoku-field-invalid', 'sudoku-field-initial', 'sudoku-field-marked')
|
||||
console.info('Removed marked class');
|
||||
fieldElement.innerHTML = field.value ? field.value.toString() : ' '
|
||||
|
||||
if (field.selected) {
|
||||
fieldElement.classList.add('sudoku-field-selected')
|
||||
window.selected = field.field
|
||||
}
|
||||
if (!field.valid) {
|
||||
fieldElement.classList.add('sudoku-field-invalid')
|
||||
}
|
||||
if (!field.value) {
|
||||
fieldElement.classList.add('sudoku-field-empty')
|
||||
}
|
||||
if (field.initial) {
|
||||
fieldElement.classList.add('sudoku-field-initial')
|
||||
}
|
||||
if (field.marked) {
|
||||
fieldElement.classList.add('sudoku-field-marked')
|
||||
console.info("added marked lcass")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
_sync() {
|
||||
this.fieldElements.forEach(fieldElement => {
|
||||
this._syncField(fieldElement);
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
customElements.define("my-sudoku", Sudoku);
|
||||
|
||||
function generateIdByPosition(element) {
|
||||
const parent = element.parentNode;
|
||||
const index = Array.prototype.indexOf.call(parent.children, element);
|
||||
const generatedId = `${element.tagName.toLowerCase()}-${index}`;
|
||||
element.id = generatedId.replace('div-', 'session-key-');
|
||||
return element.id;
|
||||
}
|
||||
|
854
reviews/BordedDev/sudoku.modified.js
Normal file
@ -0,0 +1,854 @@
|
||||
function randInt(min, max) {
|
||||
min = Math.ceil(min)
|
||||
max = Math.floor(max)
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min
|
||||
}
|
||||
|
||||
BordedDev
commented
I ran Biome.js over this, and I'm a I ran Biome.js over this, and I'm a `;` hater (my default config), that's why they're not here
|
||||
class EventHandler {
|
||||
constructor() {
|
||||
this.events = {}
|
||||
this.eventCount = 0
|
||||
this.suppresEvents = false
|
||||
this.debugEvents = false
|
||||
}
|
||||
|
||||
on(event, listener) {
|
||||
this.events[event] ??= { name: name, listeners: [], callCount: 0 }
|
||||
BordedDev
commented
Simplified the empty event handler assignment. It was meant to be Simplified the empty event handler assignment. It was meant to be `??=` not `||=` both will work in this case because of you how JS works though
|
||||
this.events[event].listeners.push(listener)
|
||||
}
|
||||
|
||||
off(event, listenerToRemove) {
|
||||
if (!this.events[event]) return
|
||||
this.events[event].listeners = this.events[event].listeners.filter(
|
||||
(listener) => listener !== listenerToRemove,
|
||||
)
|
||||
}
|
||||
|
||||
emit(event, data) {
|
||||
if (!this.events[event]) return []
|
||||
if (this.suppresEvents) return []
|
||||
this.eventCount++
|
||||
const returnValue = this.events[event].listeners.map((listener) => {
|
||||
return listener(data) ?? null
|
||||
})
|
||||
this.events[event].callCount++
|
||||
if (this.debugEvents) {
|
||||
console.debug("debugEvent", {
|
||||
event: event,
|
||||
arg: data,
|
||||
callCount: this.events[event].callCount,
|
||||
number: this.eventCount,
|
||||
returnValues: returnValue,
|
||||
})
|
||||
}
|
||||
return returnValue
|
||||
}
|
||||
|
||||
suppres(fn) {
|
||||
const originallySuppressed = this.suppresEvents
|
||||
this.suppresEvents = true
|
||||
fn(this)
|
||||
this.suppresEvents = originallySuppressed
|
||||
return originallySuppressed
|
||||
}
|
||||
}
|
||||
|
||||
class Col extends EventHandler {
|
||||
id = 0
|
||||
values = []
|
||||
initial = false
|
||||
_marked = false
|
||||
row = null
|
||||
index = 0
|
||||
valid = true
|
||||
_selected = false
|
||||
_value = 0
|
||||
|
||||
_isValidXy() {
|
||||
if (!this._value) return true
|
||||
return (
|
||||
this.row.puzzle.fields
|
||||
.filter((field) => {
|
||||
return (
|
||||
(field.index == this.index && field.value == this._value) ||
|
||||
(field.row.index == this.row.index && field.value == this._value)
|
||||
)
|
||||
})
|
||||
.filter((field) => field != this).length == 0
|
||||
)
|
||||
}
|
||||
|
||||
mark() {
|
||||
this.marked = true
|
||||
}
|
||||
|
||||
unmark() {
|
||||
this.marked = false
|
||||
}
|
||||
|
||||
select() {
|
||||
this.selected = true
|
||||
}
|
||||
|
||||
unselect() {
|
||||
this.selected = false
|
||||
}
|
||||
|
||||
_isValidBox() {
|
||||
if (!this._value) return true
|
||||
const startRow =
|
||||
this.row.index - (this.row.index % (this.row.puzzle.size / 3))
|
||||
const startCol = this.index - (this.index % (this.row.puzzle.size / 3))
|
||||
|
||||
for (let i = 0; i < this.row.puzzle.size / 3; i++) {
|
||||
for (let j = 0; j < this.row.puzzle.size / 3; j++) {
|
||||
const field = this.row.puzzle.get(i + startRow, j + startCol)
|
||||
if (field != this && field.value == this._value) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
validate() {
|
||||
if (!this.row.puzzle.initalized) {
|
||||
return this.valid
|
||||
}
|
||||
if (this.initial) {
|
||||
this.valid = true
|
||||
return this.valid
|
||||
}
|
||||
if (!this.value && !this.valid) {
|
||||
this.valid = true
|
||||
this.emit("update", this)
|
||||
return this.valid
|
||||
}
|
||||
const oldValue = this.valid
|
||||
this.valid = this._isValidXy() && this._isValidBox()
|
||||
if (oldValue != this.valid) {
|
||||
this.emit("update", this)
|
||||
}
|
||||
return this.valid
|
||||
}
|
||||
|
||||
set value(val) {
|
||||
if (this.initial) return
|
||||
const digit = Number(val)
|
||||
const validDigit = digit >= 0 && digit <= 9
|
||||
const update = validDigit && digit != this.value
|
||||
if (update) {
|
||||
this._value = Number(digit)
|
||||
this.validate()
|
||||
this.emit("update", this)
|
||||
}
|
||||
}
|
||||
|
||||
get value() {
|
||||
return this._value
|
||||
}
|
||||
|
||||
get selected() {
|
||||
return this._selected
|
||||
}
|
||||
|
||||
set selected(val) {
|
||||
if (val != this._selected) {
|
||||
this._selected = val
|
||||
if (this.row.puzzle.initalized) this.emit("update", this)
|
||||
}
|
||||
}
|
||||
|
||||
get marked() {
|
||||
return this._marked
|
||||
}
|
||||
|
||||
set marked(val) {
|
||||
if (val != this._marked) {
|
||||
this._marked = val
|
||||
if (this.row.puzzle.initalized) {
|
||||
this.emit("update", this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
constructor(row) {
|
||||
super()
|
||||
this.row = row
|
||||
this.index = this.row.cols.length
|
||||
this.id = this.row.puzzle.rows.length * this.row.puzzle.size + this.index
|
||||
this.initial = false
|
||||
this.selected = false
|
||||
this._value = 0
|
||||
this.marked = false
|
||||
this.valid = true
|
||||
}
|
||||
|
||||
update() {
|
||||
this.emit("update", this)
|
||||
}
|
||||
|
||||
toggleSelected() {
|
||||
this.selected = !this.selected
|
||||
}
|
||||
|
||||
toggleMarked() {
|
||||
this.marked = !this.marked
|
||||
}
|
||||
|
||||
get data() {
|
||||
return {
|
||||
values: this.values,
|
||||
value: this.value,
|
||||
index: this.index,
|
||||
id: this.id,
|
||||
row: this.row.index,
|
||||
col: this.index,
|
||||
valid: this.valid,
|
||||
initial: this.initial,
|
||||
selected: this.selected,
|
||||
marked: this.marked,
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
return String(this.value)
|
||||
}
|
||||
|
||||
toText() {
|
||||
return this.toString().replace("0", " ")
|
||||
}
|
||||
}
|
||||
class Row extends EventHandler {
|
||||
cols = []
|
||||
puzzle = null
|
||||
index = 0
|
||||
initialized = false
|
||||
constructor(puzzle) {
|
||||
super()
|
||||
this.puzzle = puzzle
|
||||
this.cols = []
|
||||
this.index = this.puzzle.rows.length
|
||||
this.initialized = false
|
||||
for (let i = 0; i < puzzle.size; i++) {
|
||||
const col = new Col(this)
|
||||
this.cols.push(col)
|
||||
col.on("update", (field) => {
|
||||
this.emit("update", field)
|
||||
})
|
||||
}
|
||||
this.initialized = true
|
||||
}
|
||||
get data() {
|
||||
return {
|
||||
cols: this.cols.map((col) => col.data),
|
||||
index: this.index,
|
||||
}
|
||||
}
|
||||
toText() {
|
||||
let result = ""
|
||||
for (const col of this.cols) {
|
||||
result += col.toText()
|
||||
}
|
||||
return result
|
||||
}
|
||||
toString() {
|
||||
return this.toText().replaceAll(" ", "0")
|
||||
}
|
||||
}
|
||||
|
||||
class Puzzle extends EventHandler {
|
||||
rows = []
|
||||
size = 0
|
||||
hash = 0
|
||||
states = []
|
||||
parsing = false
|
||||
_initialized = false
|
||||
initalized = false
|
||||
_fields = null
|
||||
constructor(arg) {
|
||||
super()
|
||||
this.debugEvents = true
|
||||
this.initalized = false
|
||||
this.rows = []
|
||||
if (isNaN(arg)) {
|
||||
// load session
|
||||
} else {
|
||||
this.size = Number(arg)
|
||||
}
|
||||
for (let i = 0; i < this.size; i++) {
|
||||
const row = new Row(this)
|
||||
this.rows.push(row)
|
||||
row.on("update", (field) => {
|
||||
this.onFieldUpdate(field)
|
||||
})
|
||||
}
|
||||
this._initialized = true
|
||||
this.initalized = true
|
||||
this.commitState()
|
||||
}
|
||||
validate() {
|
||||
return this.valid
|
||||
}
|
||||
_onEventHandler() {
|
||||
this.eventCount++
|
||||
}
|
||||
makeInvalid() {
|
||||
if (!app.valid) {
|
||||
const invalid = this.invalid
|
||||
return invalid[invalid.length - 1]
|
||||
}
|
||||
|
||||
for (const row of this.rows) {
|
||||
BordedDev
commented
Switched Switched `.forEach` to `for of` loop for performance (recommended practice, but honestly, it’s not a big deal - I'd even call it a nitpick)
|
||||
for (const col of row.cols) {
|
||||
if (col.value) {
|
||||
let modify = null
|
||||
if (col.index == this.size) {
|
||||
modify = this.get(row.index, col.index - 2)
|
||||
} else {
|
||||
modify = this.get(row.index, col.index + 1)
|
||||
}
|
||||
modify.value = col.value
|
||||
// last one is invalid
|
||||
return modify.index > col.index ? modify : col
|
||||
}
|
||||
col.valid = false
|
||||
}
|
||||
}
|
||||
|
||||
this.get(0, 0).value = 1
|
||||
this.get(0, 1).value = 1
|
||||
return this.get(0, 1)
|
||||
}
|
||||
reset() {
|
||||
this._initialized = false
|
||||
this.initalized = false
|
||||
this.parsing = true
|
||||
|
||||
for (const field of this.fields) {
|
||||
field.initial = false
|
||||
field.selected = false
|
||||
field.marked = false
|
||||
field.value = 0
|
||||
}
|
||||
this.hash = 0
|
||||
this.states = []
|
||||
this.parsing = false
|
||||
this.initalized = true
|
||||
this._initialized = true
|
||||
this.commitState()
|
||||
}
|
||||
get valid() {
|
||||
return this.invalid.length == 0
|
||||
}
|
||||
get invalid() {
|
||||
this.emit("validating", this)
|
||||
const result = this.fields.filter((field) => !field.validate())
|
||||
this.emit("validated", this)
|
||||
return result
|
||||
}
|
||||
get selected() {
|
||||
return this.fields.filter((field) => field.selected)
|
||||
}
|
||||
get marked() {
|
||||
return this.fields.filter((field) => field.marked)
|
||||
}
|
||||
loadString(content) {
|
||||
this.emit("parsing", this)
|
||||
this.reset()
|
||||
this.parsing = true
|
||||
this.initalized = false
|
||||
this._initialized = false
|
||||
|
||||
const regex = /\d/g
|
||||
const matches = [...content.matchAll(regex)]
|
||||
const max = this.size * this.size
|
||||
|
||||
for (const [index, match] of matches.entries()) {
|
||||
BordedDev
commented
Collapsed the index to come from the Collapsed the index to come from the `matches` array, since I noted the 1-to-1 relation
|
||||
const digit = Number(match[0])
|
||||
const field = this.fields[index]
|
||||
field.value = digit
|
||||
field.initial = digit != 0
|
||||
}
|
||||
|
||||
this._initialized = true
|
||||
this.parsing = false
|
||||
this.deselect()
|
||||
this.initalized = true
|
||||
this.suppres(() => {
|
||||
for (const field of this.fields) {
|
||||
field.validate()
|
||||
}
|
||||
})
|
||||
this.commitState()
|
||||
this.emit("parsed", this)
|
||||
this.emit("update", this)
|
||||
}
|
||||
get state() {
|
||||
return this.getData(true)
|
||||
}
|
||||
get previousState() {
|
||||
if (this.states.length == 0) return null
|
||||
return this.states.at(this.states.length - 1)
|
||||
}
|
||||
get stateChanged() {
|
||||
if (!this._initialized) return false
|
||||
return !this.previousState || this.state != this.previousState
|
||||
}
|
||||
|
||||
commitState() {
|
||||
if (!this.initalized) return false
|
||||
this.hash = this._generateHash()
|
||||
if (this.stateChanged) {
|
||||
this.states.push(this.state)
|
||||
this.emit("commitState", this)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
onFieldUpdate(field) {
|
||||
if (!this.initalized) return false
|
||||
if (!this._initialized) return
|
||||
this.validate()
|
||||
this.commitState()
|
||||
this.emit("update", this)
|
||||
}
|
||||
|
||||
get data() {
|
||||
return this.getData(true)
|
||||
}
|
||||
|
||||
popState() {
|
||||
let prevState = this.previousState
|
||||
if (!prevState) {
|
||||
this.deselect()
|
||||
return null
|
||||
}
|
||||
while (prevState && prevState.hash == this.state.hash)
|
||||
prevState = this.states.pop()
|
||||
if (!prevState) {
|
||||
this.deselect()
|
||||
return null
|
||||
}
|
||||
this.applyState(prevState)
|
||||
this.emit("popState", this)
|
||||
return prevState
|
||||
}
|
||||
applyState(newState) {
|
||||
this._initialized = false
|
||||
|
||||
for (const stateField of newState.fields) {
|
||||
const field = this.get(stateField.row, stateField.col)
|
||||
field.selected = stateField.selected
|
||||
field.values = stateField.values
|
||||
field.value = stateField.value
|
||||
field.initial = stateField.initial
|
||||
field.validate()
|
||||
}
|
||||
this._initialized = true
|
||||
this.emit("stateApplied", this)
|
||||
this.emit("update", this)
|
||||
}
|
||||
getData(withHash = false) {
|
||||
const result = {
|
||||
fields: this.fields.map((field) => field.data),
|
||||
size: this.size,
|
||||
valid: this.valid,
|
||||
}
|
||||
if (withHash) {
|
||||
result.hash = this._generateHash()
|
||||
}
|
||||
return result
|
||||
}
|
||||
get(row, col) {
|
||||
if (!this.initalized) return null
|
||||
if (!this.rows.length) return null
|
||||
if (!this.rows[row]) return null
|
||||
return this.rows[row].cols[col]
|
||||
}
|
||||
get fields() {
|
||||
if (this._fields == null) {
|
||||
this._fields = []
|
||||
for (const row of this.rows) {
|
||||
for (const col of row.cols) {
|
||||
this._fields.push(col)
|
||||
}
|
||||
}
|
||||
}
|
||||
return this._fields
|
||||
}
|
||||
_generateHash() {
|
||||
return JSON.stringify(this.getData(false))
|
||||
.split("")
|
||||
.map((char) => {
|
||||
return char.charCodeAt(0) - "0".charCodeAt(0)
|
||||
})
|
||||
.reduce((result, num) => {
|
||||
return result + num + 26
|
||||
}, 0)
|
||||
}
|
||||
get text() {
|
||||
let result = ""
|
||||
for (const row of this.rows) {
|
||||
result += `${row.toText()}\n`
|
||||
}
|
||||
result = result.slice(0, result.length - 1)
|
||||
return result
|
||||
}
|
||||
get initialFields() {
|
||||
return this.fields.filter((field) => field.initial)
|
||||
}
|
||||
get json() {
|
||||
return JSON.stringify(this.data)
|
||||
}
|
||||
get zeroedText() {
|
||||
return this.text.replaceAll(" ", "0")
|
||||
}
|
||||
get string() {
|
||||
return this.toString()
|
||||
}
|
||||
toString() {
|
||||
return this.text.replaceAll("\n", "").replaceAll(" ", "0")
|
||||
}
|
||||
get humanFormat() {
|
||||
return ` ${this.text.replaceAll(" ", "0").split("").join(" ")}`
|
||||
}
|
||||
getRandomField() {
|
||||
const emptyFields = this.empty
|
||||
return emptyFields[randInt(0, emptyFields.length - 1)]
|
||||
}
|
||||
update(callback) {
|
||||
this.commitState()
|
||||
this.intalized = false
|
||||
callback(this)
|
||||
this.intalized = true
|
||||
this.validate()
|
||||
this.commitState()
|
||||
this.intalized = false
|
||||
if (this.valid) this.deselect()
|
||||
this.intalized = true
|
||||
this.emit("update", this)
|
||||
}
|
||||
get empty() {
|
||||
return this.fields.filter((field) => field.value == 0)
|
||||
}
|
||||
getRandomEmptyField() {
|
||||
const field = this.getRandomField()
|
||||
if (!field) return null
|
||||
return field
|
||||
}
|
||||
deselect() {
|
||||
for (const field of this.fields) {
|
||||
field.selected = false
|
||||
}
|
||||
}
|
||||
generate() {
|
||||
this.reset()
|
||||
this.initalized = false
|
||||
for (let i = 0; i < 17; i++) {
|
||||
this.fillRandomField()
|
||||
}
|
||||
this.deselect()
|
||||
this.initalized = true
|
||||
this.commitState()
|
||||
this.emit("update", this)
|
||||
}
|
||||
fillRandomField() {
|
||||
const field = this.getRandomEmptyField()
|
||||
if (!field) return
|
||||
this.deselect()
|
||||
field.selected = true
|
||||
let number = 0
|
||||
number++
|
||||
|
||||
while (number <= 9) {
|
||||
field.value = randInt(1, 9)
|
||||
field.update()
|
||||
if (this.validate()) {
|
||||
field.initial = true
|
||||
return field
|
||||
}
|
||||
number++
|
||||
}
|
||||
return false
|
||||
}
|
||||
autoSolve() {
|
||||
window.requestAnimationFrame(() => {
|
||||
if (this.fillRandomField()) {
|
||||
if (this.empty.length) return this.autoSolve()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class PuzzleManager {
|
||||
constructor(size) {
|
||||
this.size = size
|
||||
this.puzzles = []
|
||||
this._activePuzzle = null
|
||||
}
|
||||
|
||||
addPuzzle(puzzle) {
|
||||
this.puzzles.push(puzzle)
|
||||
}
|
||||
get active() {
|
||||
return this.activePuzzle
|
||||
}
|
||||
set activePuzzle(puzzle) {
|
||||
this._activePuzzle = puzzle
|
||||
}
|
||||
get activePuzzle() {
|
||||
return this._activePuzzle
|
||||
}
|
||||
}
|
||||
|
||||
const puzzleManager = new PuzzleManager(9)
|
||||
|
||||
class Sudoku extends HTMLElement {
|
||||
styleSheet = `
|
||||
.sudoku {
|
||||
font-size: 13px;
|
||||
color:#222;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(9, 1fr);
|
||||
grid-template-rows: auto;
|
||||
gap: 0px;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
background-color: #e5e5e5;
|
||||
border-radius: 5px;
|
||||
aspect-ratio: 1/1;
|
||||
}
|
||||
.sudoku-field-initial {
|
||||
color: #777;
|
||||
}
|
||||
.sudoku-field-selected {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
.soduku-field-marked {
|
||||
background-color: blue;
|
||||
}
|
||||
.sudoku-field-invalid {
|
||||
color: red;
|
||||
}
|
||||
.sudoku-field {
|
||||
border: 1px solid #ccc;
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
aspect-ratio: 1/1;
|
||||
width: 1em;
|
||||
line-height: 1;
|
||||
}`
|
||||
set fieldSize(val) {
|
||||
this._fieldSize = val ? Number(val) : null
|
||||
|
||||
for (const field of this.fieldElements) {
|
||||
field.style.fontSize = this._fieldSize
|
||||
? `${this._fieldSize.toString()}px`
|
||||
: ""
|
||||
}
|
||||
}
|
||||
get fieldSize() {
|
||||
return this._fieldSize
|
||||
}
|
||||
get eventCount() {
|
||||
return this.puzzle.eventCount
|
||||
}
|
||||
get puzzleContent() {
|
||||
return this.puzzle.humanFormat
|
||||
}
|
||||
set puzzleContent(val) {
|
||||
if (val == "generate") {
|
||||
this.puzzle.generate()
|
||||
} else if (val) {
|
||||
this.puzzle.loadString(val)
|
||||
} else {
|
||||
this.puzzle.reset()
|
||||
}
|
||||
}
|
||||
connectedCallback() {
|
||||
this.puzzleContent = this.getAttribute("puzzle")
|
||||
? this.getAttribute("puzzle")
|
||||
: null
|
||||
this._fieldSize = null
|
||||
this.fieldSize = this.getAttribute("size")
|
||||
? this.getAttribute("size")
|
||||
: null
|
||||
this.readOnly = !!this.getAttribute("read-only")
|
||||
this.attachShadow({ mode: "open" })
|
||||
this.shadowRoot.appendChild(this.styleElement)
|
||||
this.shadowRoot.appendChild(this.puzzleDiv)
|
||||
}
|
||||
toString() {
|
||||
return this.puzzleContent
|
||||
}
|
||||
set active(val) {
|
||||
this._active = val
|
||||
if (this._active) this.manager.activePuzzle = this
|
||||
}
|
||||
get active() {
|
||||
return this._active
|
||||
}
|
||||
set readOnly(val) {
|
||||
this._readOnly = !!val
|
||||
}
|
||||
get readOnly() {
|
||||
return this._readOnly
|
||||
}
|
||||
constructor() {
|
||||
super()
|
||||
this._readOnly = false
|
||||
this._active = false
|
||||
this.fieldElements = []
|
||||
this.puzzle = new Puzzle(9)
|
||||
this.fields = []
|
||||
this.styleElement = document.createElement("style")
|
||||
this.styleElement.textContent = this.styleSheet
|
||||
this.puzzleDiv = document.createElement("div")
|
||||
this.puzzleDiv.classList.add("sudoku")
|
||||
this._bind()
|
||||
this.manager.addPuzzle(this)
|
||||
}
|
||||
get manager() {
|
||||
return puzzleManager
|
||||
}
|
||||
_bind() {
|
||||
this._bindFields()
|
||||
this._bindEvents()
|
||||
this._sync()
|
||||
}
|
||||
_bindFields() {
|
||||
for (const row of this.puzzle.rows) {
|
||||
for (const field of row.cols) {
|
||||
const fieldElement = document.createElement("div")
|
||||
fieldElement.classList.add("sudoku-field")
|
||||
fieldElement.field = field
|
||||
field.on("update", (field) => {
|
||||
this._sync()
|
||||
})
|
||||
fieldElement.addEventListener("click", (e) => {
|
||||
if (!this.readOnly) field.toggleSelected()
|
||||
})
|
||||
fieldElement.addEventListener("contextmenu", (e) => {
|
||||
e.preventDefault()
|
||||
field.row.puzzle.update(() => {
|
||||
field.selected = false
|
||||
field.value = 0
|
||||
})
|
||||
})
|
||||
this.fields.push(field)
|
||||
this.fieldElements.push(fieldElement)
|
||||
this.puzzleDiv.appendChild(fieldElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
_bindEvents() {
|
||||
this.puzzle.on("update", () => {
|
||||
this._sync()
|
||||
})
|
||||
this.puzzleDiv.addEventListener("mouseenter", (e) => {
|
||||
this.active = true
|
||||
})
|
||||
this.puzzleDiv.addEventListener("mouseexit", (e) => {
|
||||
this.active = false
|
||||
})
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (this.readOnly) return
|
||||
if (!puzzleManager.active) return
|
||||
const puzzle = puzzleManager.active.puzzle
|
||||
|
||||
if (!isNaN(e.key)) {
|
||||
puzzle.update((target) => {
|
||||
for (const field of puzzle.selected) {
|
||||
field.value = Number(e.key)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
const keyLookup = {
|
||||
u() {
|
||||
puzzle.popState()
|
||||
BordedDev
commented
This is mostly personal preference, since an "industry standard" way would be key => action enum => (action transform =>) action function. I find this a bit easier to read. If I'd bring in a library for the matching I'd have used This is mostly personal preference, since an "industry standard" way would be key => action enum => (action transform =>) action function. I find this a bit easier to read. If I'd bring in a library for the matching I'd have used `cond` from lodash https://lodash.com/docs/4.17.15#cond
|
||||
},
|
||||
d() {
|
||||
puzzle.update((target) => {
|
||||
for (const field of puzzle.selected) {
|
||||
field.value = 0
|
||||
}
|
||||
})
|
||||
},
|
||||
a() {
|
||||
puzzle.autoSolve()
|
||||
},
|
||||
r() {
|
||||
puzzle.fillRandomField()
|
||||
},
|
||||
m() {
|
||||
const fields = []
|
||||
puzzle.update((target) => {
|
||||
for (const field of target.selected) {
|
||||
field.selected = false
|
||||
fields.push(field)
|
||||
}
|
||||
})
|
||||
puzzle.update((target) => {
|
||||
for (const field of fields) {
|
||||
field.toggleMarked()
|
||||
}
|
||||
})
|
||||
puzzle.emit("update", puzzle)
|
||||
},
|
||||
}
|
||||
keyLookup[e.key]?.()
|
||||
}
|
||||
})
|
||||
}
|
||||
get(row, col) {
|
||||
return this.puzzle.get(row, col)
|
||||
}
|
||||
_syncField(fieldElement) {
|
||||
const field = fieldElement.field
|
||||
fieldElement.classList.remove(
|
||||
"sudoku-field-selected",
|
||||
"sudoku-field-empty",
|
||||
"sudoku-field-invalid",
|
||||
"sudoku-field-initial",
|
||||
"sudoku-field-marked",
|
||||
)
|
||||
console.info("Removed marked class")
|
||||
fieldElement.innerHTML = field.value ? field.value.toString() : " "
|
||||
|
||||
if (field.selected) {
|
||||
fieldElement.classList.add("sudoku-field-selected")
|
||||
window.selected = field.field
|
||||
}
|
||||
if (!field.valid) {
|
||||
fieldElement.classList.add("sudoku-field-invalid")
|
||||
}
|
||||
if (!field.value) {
|
||||
fieldElement.classList.add("sudoku-field-empty")
|
||||
}
|
||||
if (field.initial) {
|
||||
fieldElement.classList.add("sudoku-field-initial")
|
||||
}
|
||||
if (field.marked) {
|
||||
fieldElement.classList.add("sudoku-field-marked")
|
||||
console.info("added marked lcass")
|
||||
}
|
||||
}
|
||||
_sync() {
|
||||
for (const fieldElement of this.fieldElements) {
|
||||
this._syncField(fieldElement)
|
||||
}
|
||||
}
|
||||
}
|
||||
customElements.define("my-sudoku", Sudoku)
|
||||
|
||||
function generateIdByPosition(element) {
|
||||
const parent = element.parentNode
|
||||
const index = Array.prototype.indexOf.call(parent.children, element)
|
||||
const generatedId = `${element.tagName.toLowerCase()}-${index}`
|
||||
element.id = generatedId.replace("div-", "session-key-")
|
||||
return element.id
|
||||
}
|
73
reviews/BordedDev/sudoku.rewrite.css
Normal file
@ -0,0 +1,73 @@
|
||||
:host {
|
||||
display: inline-block;
|
||||
--border-radius: 5px;
|
||||
BordedDev
commented
I'd normally use a @property for this but it wasn't working the initial value so I did this instead
I'd normally use a @property for this but it wasn't working the initial value so I did this instead
https://developer.mozilla.org/en-US/docs/Web/CSS/@property
```css
@property --border-radius {
syntax: "<length>";
inherits: true;
initial-value: 5px;
}
```
|
||||
}
|
||||
|
||||
.sudoku {
|
||||
user-select: none;
|
||||
border: 1px solid #ccc;
|
||||
font-size: 13px;
|
||||
color: #222;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(9, 1fr);
|
||||
grid-auto-rows: auto;
|
||||
aspect-ratio: 1 / 1;
|
||||
background-color: #e5e5e5;
|
||||
border-radius: var(--border-radius);
|
||||
overflow: hidden;
|
||||
|
||||
.sudoku-field {
|
||||
aspect-ratio: 1 / 1;
|
||||
width: 1em;
|
||||
line-height: 1;
|
||||
BordedDev
commented
This is to fix the vertical alignment since it had a value of 1.5 for me This is to fix the vertical alignment since it had a value of 1.5 for me
|
||||
text-align: center;
|
||||
padding: 2px;
|
||||
border: 1px solid #ccc;
|
||||
border-left: none;
|
||||
border-top: none;
|
||||
|
||||
/* Disable borders at the edges */
|
||||
&:nth-child(9n) {
|
||||
border-right: none;
|
||||
}
|
||||
&:nth-last-child(-n + 9) {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* Cross bars */
|
||||
&:nth-child(3n):not(:nth-child(9n)) {
|
||||
border-right-color: black;
|
||||
}
|
||||
&:nth-child(n + 19):nth-child(-n + 27),
|
||||
&:nth-child(n + 46):nth-child(-n + 54) {
|
||||
border-bottom-color: black;
|
||||
}
|
||||
|
||||
/* Match the corners with parent, otherwise they'll render on top */
|
||||
&:nth-child(1) {
|
||||
border-top-left-radius: var(--border-radius);
|
||||
}
|
||||
&:nth-child(9) {
|
||||
border-top-right-radius: var(--border-radius);
|
||||
}
|
||||
&:nth-last-child(1) {
|
||||
border-bottom-right-radius: var(--border-radius);
|
||||
}
|
||||
&:nth-last-child(9) {
|
||||
border-bottom-left-radius: var(--border-radius);
|
||||
}
|
||||
|
||||
&.initial {
|
||||
color: #777;
|
||||
}
|
||||
&.selected {
|
||||
background-color: lightgreen;
|
||||
}
|
||||
&.marked {
|
||||
background-color: blue;
|
||||
}
|
||||
&.invalid {
|
||||
color: red;
|
||||
}
|
||||
}
|
||||
}
|
484
reviews/BordedDev/sudoku.rewrite.js
Normal file
@ -0,0 +1,484 @@
|
||||
const PARSE_RANGE_START = "0"
|
||||
const PARSE_RANGE_END = "9"
|
||||
|
||||
const VALID_RANGE_START = "1"
|
||||
const VALID_RANGE_END = "9"
|
||||
|
||||
const SUDOKU_GRID_SIZE = 9 * 9
|
||||
|
||||
const SUDOKU_VALUES = new Set([1, 2, 3, 4, 5, 6, 7, 8, 9])
|
||||
|
||||
/**
|
||||
* @param min {number}
|
||||
* @param max {number}
|
||||
* @returns {number}
|
||||
*/
|
||||
function randInt(min, max) {
|
||||
BordedDev
commented
Type hints are here since I can see them in my IDE and make things slightly easier to work with, hence why I've put them everywhere but with no actual docs Type hints are here since I can see them in my IDE and make things slightly easier to work with, hence why I've put them everywhere but with no actual docs
|
||||
const minCeil = Math.ceil(min)
|
||||
const maxFloor = Math.floor(max)
|
||||
return Math.trunc(Math.random() * (maxFloor - minCeil + 1)) + minCeil
|
||||
}
|
||||
|
||||
class SudokuPuzzle extends EventTarget {
|
||||
/**
|
||||
* undefined is an empty cell, null is a cell that was cleared
|
||||
* @typedef {(number|null|undefined)[]} SudokuState
|
||||
*/
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {SudokuState[]}
|
||||
*/
|
||||
#state = []
|
||||
BordedDev
commented
This contains a stack of states, essentially the initial state + all steps This contains a stack of states, essentially the initial state + all steps
|
||||
/**
|
||||
retoor
commented
Ooooh, magic numbers :P Did i have that too? Ooooh, magic numbers :P Did i have that too?
BordedDev
commented
Dang it I missed it, it's meant to be Dang it I missed it, it's meant to be `SUDOKU_GRID_SIZE`. You did with the `new Puzzle(9)`
|
||||
*
|
||||
* @type {SudokuState}
|
||||
*/
|
||||
#activeState = new Array(SUDOKU_GRID_SIZE)
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {SudokuState}
|
||||
* @readonly
|
||||
*/
|
||||
get grid() {
|
||||
const gridValue = [...this.#activeState]
|
||||
Object.freeze(gridValue)
|
||||
BordedDev
commented
This mainly to enforce that the underlying grid isn't modifiable, technically we return a new array anyway, so there is little point. This mainly to enforce that the underlying grid isn't modifiable, technically we return a new array anyway, so there is little point. `#activeState` is also always replaced so we can freeze it when it's created, but this felt more flexible
|
||||
return gridValue
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param start {string} Either "generate" for a random sudoku or numbers to prefill into the grid (0 is empty)
|
||||
*/
|
||||
constructor(start) {
|
||||
super()
|
||||
|
||||
if (start === "generate") {
|
||||
this.applyState(this.#generateRandomState())
|
||||
} else if (start) {
|
||||
this.applyState(SudokuPuzzle.#stateFromString(start))
|
||||
} else {
|
||||
this.applyState(Array.from({ length: SUDOKU_GRID_SIZE }))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param serializedState {string}
|
||||
* @returns {SudokuState}
|
||||
*/
|
||||
static #stateFromString(serializedState) {
|
||||
let cellIndex = 0
|
||||
|
||||
const newState = Array.from({ length: SUDOKU_GRID_SIZE })
|
||||
|
||||
for (const char of serializedState) {
|
||||
if (PARSE_RANGE_START <= char && char <= PARSE_RANGE_END) {
|
||||
if (VALID_RANGE_START <= char && char <= PARSE_RANGE_END) {
|
||||
BordedDev
commented
Since 0 is an invalid value for the Since 0 is an invalid value for the `SudokuState` this allows for them to be filtered out while still increasing the number placement
|
||||
newState[cellIndex] = Number.parseInt(char, 10)
|
||||
}
|
||||
cellIndex++
|
||||
}
|
||||
|
||||
if (cellIndex >= newState.length) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param stateToSample {SudokuState}
|
||||
* @returns {undefined|number}
|
||||
*/
|
||||
#sampleRandomEmptyField(stateToSample = this.#activeState) {
|
||||
const iters = [...stateToSample.entries().filter(([, v]) => v == null)]
|
||||
BordedDev
commented
Recent learned that the variables are optional in destruction, had them as Recent learned that the variables are optional in destruction, had them as `_` before.
|
||||
if (iters.length > 0) {
|
||||
return iters[randInt(0, iters.length - 1)][0]
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param states {...SudokuState[]}
|
||||
*/
|
||||
static collapseStates(...states) {
|
||||
return Array.from({ length: SUDOKU_GRID_SIZE }, (_, i) => {
|
||||
for (let stateIndex = states.length - 1; stateIndex >= 0; stateIndex--) {
|
||||
if (states[stateIndex][i] !== undefined) {
|
||||
return states[stateIndex][i]
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param slot {number}
|
||||
* @param state {SudokuState}
|
||||
* @return {Set<number>}
|
||||
*/
|
||||
#boxGetValues(slot, state = this.#activeState) {
|
||||
const containedValues = new Set()
|
||||
const left = Math.trunc(Math.trunc(slot % 9) / 3) * 3
|
||||
const top = Math.trunc(Math.trunc(slot / 9) / 3) * (9 * 3)
|
||||
|
||||
for (let y = 0; y <= 27; y += 9) {
|
||||
BordedDev
commented
I'm being a little cheeky here, making the multiplication an addition instead. I can see an argument that this should be a 1 to 3 loop with multiplication, I liked the simplicity of I'm being a little cheeky here, making the multiplication an addition instead. I can see an argument that this should be a 1 to 3 loop with multiplication, I liked the simplicity of `positionIndex` this way
|
||||
for (let x = 0; x < 3; x++) {
|
||||
const positionIndex = top + y + left + x
|
||||
|
||||
if (positionIndex !== slot) {
|
||||
containedValues.add(state[positionIndex])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return containedValues
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param slot {number}
|
||||
* @param state {SudokuState}
|
||||
* @return {Set<number>}
|
||||
*/
|
||||
#crossGetValues(slot, state = this.#activeState) {
|
||||
const containedValues = new Set()
|
||||
const left = slot % 9
|
||||
|
||||
for (let leftIndex = slot - left; leftIndex < slot; leftIndex++) {
|
||||
if (state[leftIndex] != null) {
|
||||
containedValues.add(state[leftIndex])
|
||||
}
|
||||
}
|
||||
for (let rightIndex = slot - left + 9; rightIndex > slot; rightIndex--) {
|
||||
if (state[rightIndex] != null) {
|
||||
containedValues.add(state[rightIndex])
|
||||
}
|
||||
}
|
||||
for (let topIndex = left; topIndex < slot; topIndex += 9) {
|
||||
if (state[topIndex] != null) {
|
||||
containedValues.add(state[topIndex])
|
||||
}
|
||||
}
|
||||
|
||||
for (
|
||||
let bottomIndex = SUDOKU_GRID_SIZE - (9 - left);
|
||||
bottomIndex > slot;
|
||||
bottomIndex -= 9
|
||||
) {
|
||||
if (state[bottomIndex] != null) {
|
||||
containedValues.add(state[bottomIndex])
|
||||
}
|
||||
}
|
||||
|
||||
return containedValues
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param slot {number}
|
||||
* @param state {SudokuState}
|
||||
* @return {Set<number>}
|
||||
*/
|
||||
getValues(slot, state = this.#activeState) {
|
||||
BordedDev
commented
These return a These return a `Set` to simplify/combine the logic for automatically resolving and checking if a number is valid. In theory, the check would be faster since it can check early
|
||||
return this.#boxGetValues(slot, state).union(
|
||||
this.#crossGetValues(slot, state),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param value {number}
|
||||
* @param slot {number}
|
||||
* @param state {SudokuState}
|
||||
* @return boolean
|
||||
*/
|
||||
isValueValidForSlot(value, slot, state = this.#activeState) {
|
||||
return !this.getValues(slot, state).has(value)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param newState {SudokuState}
|
||||
*/
|
||||
fillRandomFieldInState(newState = Array.from({ length: SUDOKU_GRID_SIZE })) {
|
||||
const inProgressState = SudokuPuzzle.collapseStates(
|
||||
this.#activeState,
|
||||
newState,
|
||||
)
|
||||
|
||||
const cell = this.#sampleRandomEmptyField(inProgressState)
|
||||
|
||||
if (cell === undefined) return newState
|
||||
|
||||
const availableValues = [
|
||||
...SUDOKU_VALUES.difference(this.getValues(cell, inProgressState)),
|
||||
]
|
||||
|
||||
newState[cell] = availableValues[randInt(0, availableValues.length - 1)]
|
||||
return newState
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {SudokuState}
|
||||
*/
|
||||
#generateRandomState() {
|
||||
const newState = Array.from({ length: SUDOKU_GRID_SIZE })
|
||||
for (let i = 0; i < 17; i++) {
|
||||
this.fillRandomFieldInState(newState)
|
||||
}
|
||||
|
||||
return newState
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {SudokuState}
|
||||
*/
|
||||
get baseState() {
|
||||
return this.#state[0]
|
||||
}
|
||||
|
||||
popState() {
|
||||
if (this.#state.length <= 1) return
|
||||
|
||||
this.#state.pop()
|
||||
this.#activeState = SudokuPuzzle.collapseStates(...this.#state)
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("state-changed", { detail: { value: this } }),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param newState {SudokuState}
|
||||
*/
|
||||
applyState(newState) {
|
||||
this.#state.push(newState)
|
||||
this.#activeState = SudokuPuzzle.collapseStates(...this.#state)
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("state-changed", { detail: { value: this } }),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class SudokuHost extends HTMLElement {
|
||||
static observedAttributes = ["size", "puzzle", "readonly"]
|
||||
BordedDev
commented
These were necessary for me to have These were necessary for me to have `attributeChangedCallback` work
|
||||
/**
|
||||
* @type {?ShadowRoot}
|
||||
*/
|
||||
#root
|
||||
/**
|
||||
* @type {?HTMLLinkElement}
|
||||
*/
|
||||
#styling
|
||||
/**
|
||||
* @type {?HTMLElement}
|
||||
*/
|
||||
#container
|
||||
/**
|
||||
* @type {HTMLElement[]}
|
||||
*/
|
||||
#cellGrid
|
||||
|
||||
/**
|
||||
* @type {SudokuPuzzle}
|
||||
*/
|
||||
#activePuzzle
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Map<number, HTMLElement>}
|
||||
*/
|
||||
#selectedCells = new Map()
|
||||
|
||||
/**
|
||||
*
|
||||
* @type {Map<string, (event: KeyboardEvent) => void>}
|
||||
*/
|
||||
#keyProcessors = new Map()
|
||||
|
||||
get isActive() {
|
||||
return this.matches(":hover")
|
||||
BordedDev
commented
This was more convenient for me than the mouseenter/mouseexit events This was more convenient for me than the mouseenter/mouseexit events
|
||||
}
|
||||
|
||||
constructor() {
|
||||
super()
|
||||
|
||||
this.#generateKeyProcessors()
|
||||
}
|
||||
|
||||
syncCellsToSudoku() {
|
||||
const puzzleGrid = this.#activePuzzle.grid
|
||||
const baseGrid = this.#activePuzzle.baseState
|
||||
for (let i = 0; i < SUDOKU_GRID_SIZE; i++) {
|
||||
this.#cellGrid[i].innerHTML = puzzleGrid[i] ?? ""
|
||||
|
||||
if (baseGrid[i] != null) {
|
||||
this.#cellGrid[i].classList.add("initial")
|
||||
} else if (puzzleGrid[i] != null) {
|
||||
if (this.#activePuzzle.isValueValidForSlot(puzzleGrid[i], i)) {
|
||||
this.#cellGrid[i].classList.remove("invalid")
|
||||
} else {
|
||||
this.#cellGrid[i].classList.add("invalid")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#generateKeyProcessors() {
|
||||
const cellValueProcessor = (event) => {
|
||||
if (this.#selectedCells.size === 0) return
|
||||
|
||||
const cellValue = Number.parseInt(event.key, 10)
|
||||
|
||||
const newState = Array.from({ length: SUDOKU_GRID_SIZE })
|
||||
|
||||
const baseState = this.#activePuzzle.baseState
|
||||
|
||||
for (const [index] of this.#selectedCells) {
|
||||
if (baseState[index] == null) {
|
||||
newState[index] = cellValue
|
||||
}
|
||||
}
|
||||
|
||||
this.#activePuzzle.applyState(newState)
|
||||
}
|
||||
|
||||
for (let i = 1; i <= 9; i++) {
|
||||
this.#keyProcessors.set(i.toString(), cellValueProcessor)
|
||||
}
|
||||
|
||||
this.#keyProcessors.set("d", () => {
|
||||
const newState = Array.from({ length: SUDOKU_GRID_SIZE })
|
||||
|
||||
for (const [index] of this.#selectedCells) {
|
||||
newState[index] = null
|
||||
}
|
||||
|
||||
this.#activePuzzle.applyState(newState)
|
||||
})
|
||||
|
||||
this.#keyProcessors.set("a", () => {
|
||||
const generator = () => {
|
||||
const newState = this.#activePuzzle.fillRandomFieldInState()
|
||||
|
||||
if (newState.some((v) => v != null)) {
|
||||
this.#activePuzzle.applyState(newState)
|
||||
|
||||
if (
|
||||
this.#activePuzzle.grid.includes(null) ||
|
||||
this.#activePuzzle.grid.includes(undefined)
|
||||
) {
|
||||
requestAnimationFrame(generator)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(generator)
|
||||
})
|
||||
|
||||
this.#keyProcessors.set("u", () => {
|
||||
this.#activePuzzle.popState()
|
||||
})
|
||||
|
||||
this.#keyProcessors.set("r", () => {
|
||||
this.#activePuzzle.applyState(this.#activePuzzle.fillRandomFieldInState())
|
||||
})
|
||||
|
||||
this.#keyProcessors.set("m", () => {
|
||||
for (const [, cell] of this.#selectedCells) {
|
||||
cell.classList.toggle("marked")
|
||||
cell.classList.remove("selected")
|
||||
}
|
||||
|
||||
this.#selectedCells.clear()
|
||||
})
|
||||
}
|
||||
|
||||
#selectCell(i, cell) {
|
||||
if (this.hasAttribute("readonly") || !this.isActive) return
|
||||
this.#selectedCells.set(i, cell)
|
||||
cell.classList.add("selected")
|
||||
}
|
||||
|
||||
#deselectCell(i, cell) {
|
||||
if (this.hasAttribute("readonly") || !this.isActive) return
|
||||
this.#selectedCells.delete(i)
|
||||
cell.classList.remove("selected")
|
||||
}
|
||||
|
||||
#keyHandler(event) {
|
||||
if (this.hasAttribute("readonly") || !this.isActive) return
|
||||
const key = event.key
|
||||
this.#keyProcessors.get(key)?.(event)
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
this.#root = this.attachShadow({ mode: "open" })
|
||||
|
||||
this.#styling = document.createElement("link")
|
||||
this.#styling.rel = "stylesheet"
|
||||
this.#styling.href = "sudoku.rewrite.css"
|
||||
this.#root.appendChild(this.#styling)
|
||||
BordedDev
commented
This was more convenient for me than replacing the outer HTML with a This was more convenient for me than replacing the outer HTML with a `<style>...</style>` string to get syntax highlighting
|
||||
|
||||
this.#container = document.createElement("div")
|
||||
this.#container.classList.add("sudoku")
|
||||
this.#root.appendChild(this.#container)
|
||||
|
||||
if (this.hasAttribute("size")) {
|
||||
this.#container.style.fontSize = `${this.getAttribute("size")}px`
|
||||
}
|
||||
|
||||
this.#cellGrid = Array.from({ length: SUDOKU_GRID_SIZE }, (_, i) => {
|
||||
const cell = document.createElement("div")
|
||||
|
||||
cell.classList.add("sudoku-field")
|
||||
cell.innerHTML = " "
|
||||
|
||||
cell.addEventListener("click", () => {
|
||||
this.#selectCell(i, cell)
|
||||
})
|
||||
|
||||
cell.addEventListener("contextmenu", (event) => {
|
||||
event.preventDefault()
|
||||
this.#deselectCell(i, cell)
|
||||
})
|
||||
|
||||
this.#container.appendChild(cell)
|
||||
|
||||
return cell
|
||||
})
|
||||
|
||||
document.addEventListener("keydown", this.#keyHandler.bind(this))
|
||||
|
||||
this.#activePuzzle = new SudokuPuzzle(this.getAttribute("puzzle"))
|
||||
this.syncCellsToSudoku()
|
||||
this.#activePuzzle.addEventListener(
|
||||
"state-changed",
|
||||
this.syncCellsToSudoku.bind(this),
|
||||
)
|
||||
}
|
||||
|
||||
attributeChangedCallback(name, oldValue, newValue) {
|
||||
if (name === "size" && this.#container) {
|
||||
this.#container.style.fontSize = `${newValue}px`
|
||||
}
|
||||
if (name === "readonly") {
|
||||
if (this.#container) {
|
||||
this.#container.classList.toggle("readonly", newValue !== null)
|
||||
}
|
||||
|
||||
if (newValue) {
|
||||
this.#selectedCells.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("my-sudoku", SudokuHost)
|
83
reviews/BordedDev/sudoku2.html
Normal file
@ -0,0 +1,83 @@
|
||||
<html>
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<script type="text/javascript" src="sudoku.js"></script>
|
||||
<style>
|
||||
body {
|
||||
|
||||
background-color: #222;
|
||||
}
|
||||
.sudoku {
|
||||
float: left;
|
||||
color: hwb(0 20% 80%);
|
||||
opacity: 0.90
|
||||
}
|
||||
.sudoku:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sudoku-dark {
|
||||
background-color: #222 !important;
|
||||
color: #ffffff;
|
||||
opacity: 0.8
|
||||
}
|
||||
.sudoku-dark > .sudoku-field {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.sudoku-dark>.sudoku-field-selected {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.sudoku-dark>.grid-item {
|
||||
border: none;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre style="opacity: 0.8; color: #fff;margin:0;padding:10;text-align: left">
|
||||
Refresh the page for different theme. The widgets are in two colors and multiple sizes available.
|
||||
|
||||
These keyboard shortcuts are available for active puzzle widget:
|
||||
- `d` for deleting last filled field.
|
||||
- `a` for auto resolving.
|
||||
- `u` for unlimited undo's.
|
||||
- `r` for a tip.
|
||||
|
||||
Developer notes:
|
||||
- Widget size is defined by font size on the .soduku css class or as html
|
||||
attribute to the component.
|
||||
- You can use an existing puzzle by setting the puzzle attribute on a component.
|
||||
It must contain at least 81 digits to be valid.
|
||||
</pre>
|
||||
<my-sudoku size="17" class="sudoku sudoku-dark" puzzle="
|
||||
A nice generated puzzle.
|
||||
It will parse, even with this
|
||||
nonsense above it. Cool huh?
|
||||
0 0 8 0 9 0 0 0 0
|
||||
0 9 0 0 0 0 0 0 7
|
||||
0 0 0 0 2 0 0 0 0
|
||||
0 0 3 0 0 0 0 0 0
|
||||
5 0 0 0 8 0 0 0 0
|
||||
0 0 6 0 0 0 0 0 0
|
||||
0 0 0 0 0 3 9 0 0
|
||||
9 1 0 0 0 0 0 0 0
|
||||
0 0 0 0 1 0 0 0 0
|
||||
">
|
||||
</my-sudoku>
|
||||
<my-sudoku size="8" class="sudoku" puzzle="generate">
|
||||
Small generated puzzle
|
||||
</my-sudoku>
|
||||
<my-sudoku class="sudoku" puzzle="generate">
|
||||
Default generated puzzle
|
||||
</my-sudoku>
|
||||
<my-sudoku class="sudoku">
|
||||
Default empty puzzle
|
||||
</my-sudoku>
|
||||
</body>
|
||||
|
||||
</html>
|
96
reviews/BordedDev/sudoku2.modified.html
Normal file
@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sudoku</title>
|
||||
<script type="text/javascript" src="sudoku.modified.js" async defer></script>
|
||||
<style>
|
||||
body {
|
||||
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.sudoku {
|
||||
float: left;
|
||||
color: hwb(0 20% 80%);
|
||||
opacity: 0.90
|
||||
}
|
||||
|
||||
.sudoku:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sudoku-dark {
|
||||
background-color: #222 !important;
|
||||
color: #ffffff;
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
.sudoku-dark > .sudoku-field {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.sudoku-dark > .sudoku-field-selected {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.sudoku-dark > .grid-item {
|
||||
border: none;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
opacity: 0.8;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
text-align: left
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre class="instructions">
|
||||
Refresh the page for different theme. The widgets are in two colors and multiple sizes available.
|
||||
|
||||
These keyboard shortcuts are available for active puzzle widget:
|
||||
- `d` for deleting last filled field.
|
||||
- `a` for auto resolving.
|
||||
- `u` for unlimited undo's.
|
||||
- `r` for a tip.
|
||||
|
||||
Developer notes:
|
||||
- Widget size is defined by font size on the .soduku css class or as html
|
||||
attribute to the component.
|
||||
- You can use an existing puzzle by setting the puzzle attribute on a component.
|
||||
It must contain at least 81 digits to be valid.
|
||||
</pre>
|
||||
<my-sudoku size="17" class="sudoku sudoku-dark" puzzle="
|
||||
A nice generated puzzle.
|
||||
It will parse, even with this
|
||||
nonsense above it. Cool huh?
|
||||
0 0 8 0 9 0 0 0 0
|
||||
0 9 0 0 0 0 0 0 7
|
||||
0 0 0 0 2 0 0 0 0
|
||||
0 0 3 0 0 0 0 0 0
|
||||
5 0 0 0 8 0 0 0 0
|
||||
0 0 6 0 0 0 0 0 0
|
||||
0 0 0 0 0 3 9 0 0
|
||||
9 1 0 0 0 0 0 0 0
|
||||
0 0 0 0 1 0 0 0 0
|
||||
">
|
||||
</my-sudoku>
|
||||
<my-sudoku size="8" class="sudoku" puzzle="generate">
|
||||
Small generated puzzle
|
||||
</my-sudoku>
|
||||
<my-sudoku class="sudoku" puzzle="generate">
|
||||
Default generated puzzle
|
||||
</my-sudoku>
|
||||
<my-sudoku class="sudoku">
|
||||
Default empty puzzle
|
||||
</my-sudoku>
|
||||
</body>
|
||||
|
||||
</html>
|
96
reviews/BordedDev/sudoku2.rewrite.html
Normal file
@ -0,0 +1,96 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Sudoku</title>
|
||||
<script type="text/javascript" src="sudoku.rewrite.js" async defer></script>
|
||||
<style>
|
||||
body {
|
||||
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.sudoku {
|
||||
float: left;
|
||||
color: hwb(0 20% 80%);
|
||||
opacity: 0.90
|
||||
}
|
||||
|
||||
.sudoku:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.sudoku-dark {
|
||||
background-color: #222 !important;
|
||||
color: #ffffff;
|
||||
opacity: 0.8
|
||||
}
|
||||
|
||||
.sudoku-dark > .sudoku-field {
|
||||
color: #333333;
|
||||
}
|
||||
|
||||
.sudoku-dark > .sudoku-field-selected {
|
||||
color: black;
|
||||
}
|
||||
|
||||
.sudoku-dark > .grid-item {
|
||||
border: none;
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
.instructions {
|
||||
opacity: 0.8;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
padding: 10px;
|
||||
text-align: left
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<pre class="instructions">
|
||||
Refresh the page for different theme. The widgets are in two colors and multiple sizes available.
|
||||
|
||||
These keyboard shortcuts are available for active puzzle widget:
|
||||
- `d` for deleting last filled field.
|
||||
- `a` for auto resolving.
|
||||
- `u` for unlimited undo's.
|
||||
- `r` for a tip.
|
||||
|
||||
Developer notes:
|
||||
- Widget size is defined by font size on the .soduku css class or as html
|
||||
attribute to the component.
|
||||
- You can use an existing puzzle by setting the puzzle attribute on a component.
|
||||
It must contain at least 81 digits to be valid.
|
||||
</pre>
|
||||
<my-sudoku size="17" class="sudoku sudoku-dark" puzzle="
|
||||
A nice generated puzzle.
|
||||
It will parse, even with this
|
||||
nonsense above it. Cool huh?
|
||||
0 0 8 0 9 0 0 0 0
|
||||
0 9 0 0 0 0 0 0 7
|
||||
0 0 0 0 2 0 0 0 0
|
||||
0 0 3 0 0 0 0 0 0
|
||||
5 0 0 0 8 0 0 0 0
|
||||
0 0 6 0 0 0 0 0 0
|
||||
0 0 0 0 0 3 9 0 0
|
||||
9 1 0 0 0 0 0 0 0
|
||||
0 0 0 0 1 0 0 0 0
|
||||
">
|
||||
</my-sudoku>
|
||||
<my-sudoku size="8" class="sudoku" puzzle="generate">
|
||||
Small generated puzzle
|
||||
</my-sudoku>
|
||||
<my-sudoku class="sudoku" puzzle="generate">
|
||||
Default generated puzzle
|
||||
</my-sudoku>
|
||||
<my-sudoku class="sudoku">
|
||||
Default empty puzzle
|
||||
</my-sudoku>
|
||||
</body>
|
||||
|
||||
</html>
|
Ah, sad. I didn't know status of the code. But if it's working again, i will put the sudoku on my site.