Compare commits
19 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
77e78c8031 | ||
|
7ebcbcff41 | ||
|
ab8b28feba | ||
|
f08cd376a2 | ||
|
8d1139b2e6 | ||
|
dea989194c | ||
|
4a3aa2cddd | ||
|
cd18662fed | ||
|
367e12fe7b | ||
|
a24e32249b | ||
|
f1c1a02877 | ||
|
dfe51421ab | ||
|
a9d1fd2c62 | ||
|
70a27f1089 | ||
|
dafabec309 | ||
|
ec3b55b2f3 | ||
|
ce5dead2cb | ||
|
b9cc26856c | ||
|
b5c1058b3b |
34
reviews/BordedDev/notes.md
Normal file
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
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 {
|
||||
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)
|
||||
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
|
||||
}
|
||||
|
||||
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() {
|
||||
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
|
||||
const me = this
|
||||
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
|
||||
_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) {
|
||||
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;
|
||||
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)]
|
||||
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;
|
||||
}
|
||||
.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;
|
||||
}`
|
||||
|
||||
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');
|
||||
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
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
|
||||
}
|
||||
|
||||
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 }
|
||||
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) {
|
||||
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()) {
|
||||
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()
|
||||
},
|
||||
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
73
reviews/BordedDev/sudoku.rewrite.css
Normal file
@ -0,0 +1,73 @@
|
||||
:host {
|
||||
display: inline-block;
|
||||
--border-radius: 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;
|
||||
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
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) {
|
||||
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 = []
|
||||
/**
|
||||
*
|
||||
* @type {SudokuState}
|
||||
*/
|
||||
#activeState = new Array(SUDOKU_GRID_SIZE)
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {SudokuState}
|
||||
* @readonly
|
||||
*/
|
||||
get grid() {
|
||||
const gridValue = [...this.#activeState]
|
||||
Object.freeze(gridValue)
|
||||
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) {
|
||||
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)]
|
||||
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) {
|
||||
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) {
|
||||
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"]
|
||||
/**
|
||||
* @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")
|
||||
}
|
||||
|
||||
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)
|
||||
|
||||
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
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
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
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>
|
Loading…
Reference in New Issue
Block a user