From b5c1058b3b34d5005ba0f528f1280dd60448aeba Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Tue, 14 Jan 2025 09:49:41 +0100 Subject: [PATCH 01/19] Initial comments + rewrite + modifications --- reviews/BordedDev/notes.md | 27 + reviews/BordedDev/sudoku.js | 925 ++++++++++++++++++++++++ reviews/BordedDev/sudoku.modified.js | 858 ++++++++++++++++++++++ reviews/BordedDev/sudoku.rewrite.js | 460 ++++++++++++ reviews/BordedDev/sudoku2.html | 83 +++ reviews/BordedDev/sudoku2.modified.html | 96 +++ reviews/BordedDev/sudoku2.rewrite.html | 96 +++ 7 files changed, 2545 insertions(+) create mode 100644 reviews/BordedDev/notes.md create mode 100644 reviews/BordedDev/sudoku.js create mode 100644 reviews/BordedDev/sudoku.modified.js create mode 100644 reviews/BordedDev/sudoku.rewrite.js create mode 100644 reviews/BordedDev/sudoku2.html create mode 100644 reviews/BordedDev/sudoku2.modified.html create mode 100644 reviews/BordedDev/sudoku2.rewrite.html diff --git a/reviews/BordedDev/notes.md b/reviews/BordedDev/notes.md new file mode 100644 index 0000000..846954c --- /dev/null +++ b/reviews/BordedDev/notes.md @@ -0,0 +1,27 @@ +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. + +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) + +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 \ No newline at end of file diff --git a/reviews/BordedDev/sudoku.js b/reviews/BordedDev/sudoku.js new file mode 100644 index 0000000..f3433d2 --- /dev/null +++ b/reviews/BordedDev/sudoku.js @@ -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; +} + diff --git a/reviews/BordedDev/sudoku.modified.js b/reviews/BordedDev/sudoku.modified.js new file mode 100644 index 0000000..73ae5a6 --- /dev/null +++ b/reviews/BordedDev/sudoku.modified.js @@ -0,0 +1,858 @@ +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) => { + const 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 + 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") + const subFieldElement = document.createElement("div") + fieldElement.classList.add("sudoku-field") + fieldElement.field = field + fieldElement.appendChild(subFieldElement) + 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 +} diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js new file mode 100644 index 0000000..a10e17a --- /dev/null +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -0,0 +1,460 @@ +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 { + /** + * + * @type {(number|null)[][]} + */ + #state = [] + /** + * + * @type {(number|null|undefined)[]} + */ + #activeState = new Array(9 * 9) + + 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 {(?string)[]} + */ + 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 {(number|null|undefined)[]} + * @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 {...(number|null|undefined)[][]} + */ + 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 {(number|null|undefined)[]} + * @return {Set} + */ + #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 {(number|null|undefined)[]} + * @return {Set} + */ + #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 {(number|null|undefined)[]} + * @return {Set} + */ + getValues(slot, state = this.#activeState) { + return this.#boxGetValues(slot, state).union( + this.#crossGetValues(slot, state), + ) + } + + /** + * + * @param value {number} + * @param slot {number} + * @param state {(number|null|undefined)[]} + * @return boolean + */ + isValueValidForSlot(value, slot, state = this.#activeState) { + return !this.#boxGetValues(slot, state) + .union(this.#crossGetValues(slot, state)) + .has(value) + } + + /** + * + * @param newState {(number|null|undefined)[]} + */ + 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 + } + + #generateRandomState() { + const newState = Array.from({ length: SUDOKU_GRID_SIZE }) + for (let i = 0; i < 17; i++) { + this.fillRandomFieldInState(newState) + } + + return newState + } + + 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 } }), + ) + } + + 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} + */ + #selectedCells = new Map() + + /** + * + * @type {Map void>} + */ + #keyProcessors = new Map() + + get isActive() { + return this.matches(":hover") + } + + constructor() { + super() + + 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() + }) + } + + 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.add("invalid") + } + } + } + } + + #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), + ) + console.log(this.#activePuzzle) + } + + 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) diff --git a/reviews/BordedDev/sudoku2.html b/reviews/BordedDev/sudoku2.html new file mode 100644 index 0000000..ffadfdc --- /dev/null +++ b/reviews/BordedDev/sudoku2.html @@ -0,0 +1,83 @@ + + + + + + + + + + +
+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.
+
+ + + + Small generated puzzle + + + Default generated puzzle + + + Default empty puzzle + + + + \ No newline at end of file diff --git a/reviews/BordedDev/sudoku2.modified.html b/reviews/BordedDev/sudoku2.modified.html new file mode 100644 index 0000000..157ea44 --- /dev/null +++ b/reviews/BordedDev/sudoku2.modified.html @@ -0,0 +1,96 @@ + + + + + + + Sudoku + + + + + +
+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.
+
+ + + + Small generated puzzle + + + Default generated puzzle + + + Default empty puzzle + + + + \ No newline at end of file diff --git a/reviews/BordedDev/sudoku2.rewrite.html b/reviews/BordedDev/sudoku2.rewrite.html new file mode 100644 index 0000000..1f080b3 --- /dev/null +++ b/reviews/BordedDev/sudoku2.rewrite.html @@ -0,0 +1,96 @@ + + + + + + + Sudoku + + + + + +
+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.
+
+ + + + Small generated puzzle + + + Default generated puzzle + + + Default empty puzzle + + + + \ No newline at end of file -- 2.45.2 From b9cc26856cab1f520408d2afeb7f0098d7e9c18f Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Tue, 14 Jan 2025 11:14:07 +0100 Subject: [PATCH 02/19] Added a forgotten file --- reviews/BordedDev/sudoku.rewrite.css | 65 ++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 reviews/BordedDev/sudoku.rewrite.css diff --git a/reviews/BordedDev/sudoku.rewrite.css b/reviews/BordedDev/sudoku.rewrite.css new file mode 100644 index 0000000..4ab1051 --- /dev/null +++ b/reviews/BordedDev/sudoku.rewrite.css @@ -0,0 +1,65 @@ +: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; + + /* 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; + } + } +} -- 2.45.2 From ce5dead2cbec37087842b4af98e390936b8173f9 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Tue, 14 Jan 2025 23:03:24 +0100 Subject: [PATCH 03/19] Added some missing type info --- reviews/BordedDev/sudoku.rewrite.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js index a10e17a..f6371c4 100644 --- a/reviews/BordedDev/sudoku.rewrite.js +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -32,6 +32,10 @@ class SudokuPuzzle extends EventTarget { */ #activeState = new Array(9 * 9) + /** + * + * @returns {(number|null|undefined)[]} + */ get grid() { const gridValue = [...this.#activeState] Object.freeze(gridValue) @@ -216,6 +220,10 @@ class SudokuPuzzle extends EventTarget { return newState } + /** + * + * @returns {(number|null|undefined)[]} + */ #generateRandomState() { const newState = Array.from({ length: SUDOKU_GRID_SIZE }) for (let i = 0; i < 17; i++) { @@ -225,6 +233,10 @@ class SudokuPuzzle extends EventTarget { return newState } + /** + * + * @returns {(number|null|undefined)[]} + */ get baseState() { return this.#state[0] } @@ -239,6 +251,10 @@ class SudokuPuzzle extends EventTarget { ) } + /** + * + * @param newState {(number|null|undefined)[]} + */ applyState(newState) { this.#state.push(newState) this.#activeState = SudokuPuzzle.collapseStates(...this.#state) -- 2.45.2 From ec3b55b2f32e4f8156c2d30ec8492561f3c6e6d0 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Tue, 14 Jan 2025 23:14:14 +0100 Subject: [PATCH 04/19] Updated notes slightly --- reviews/BordedDev/notes.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/reviews/BordedDev/notes.md b/reviews/BordedDev/notes.md index 846954c..0f4d2de 100644 --- a/reviews/BordedDev/notes.md +++ b/reviews/BordedDev/notes.md @@ -2,13 +2,13 @@ It doesn’t render correctly on firefox but does in edge, it’s a rectangle, n 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. +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) +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. @@ -24,4 +24,7 @@ It's not recommended to setup the elements in the constructor https://developer. 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 \ No newline at end of file +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) \ No newline at end of file -- 2.45.2 From dafabec30930dc4b446a61fef7c89a0030c3e8e8 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Tue, 14 Jan 2025 23:19:05 +0100 Subject: [PATCH 05/19] Updated notes slightly --- reviews/BordedDev/notes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reviews/BordedDev/notes.md b/reviews/BordedDev/notes.md index 0f4d2de..c95a753 100644 --- a/reviews/BordedDev/notes.md +++ b/reviews/BordedDev/notes.md @@ -27,4 +27,6 @@ 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) \ No newline at end of file +(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 }` \ No newline at end of file -- 2.45.2 From 70a27f1089b4b979c745055fa74421fe8507b782 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Tue, 14 Jan 2025 23:23:58 +0100 Subject: [PATCH 06/19] Updated notes slightly --- reviews/BordedDev/notes.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/reviews/BordedDev/notes.md b/reviews/BordedDev/notes.md index c95a753..e3c0701 100644 --- a/reviews/BordedDev/notes.md +++ b/reviews/BordedDev/notes.md @@ -29,4 +29,6 @@ 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 }` \ No newline at end of file +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 \ No newline at end of file -- 2.45.2 From a9d1fd2c62e06314786990ed0709b571fd2a6cb3 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Thu, 16 Jan 2025 22:45:47 +0100 Subject: [PATCH 07/19] Fixed type parameter Fixed magic number not using global --- reviews/BordedDev/sudoku.rewrite.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js index f6371c4..7f0842d 100644 --- a/reviews/BordedDev/sudoku.rewrite.js +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -30,7 +30,7 @@ class SudokuPuzzle extends EventTarget { * * @type {(number|null|undefined)[]} */ - #activeState = new Array(9 * 9) + #activeState = new Array(SUDOKU_GRID_SIZE) /** * @@ -61,7 +61,7 @@ class SudokuPuzzle extends EventTarget { /** * * @param serializedState {string} - * @returns {(?string)[]} + * @returns {((number|null|undefined))[]} */ static #stateFromString(serializedState) { let cellIndex = 0 -- 2.45.2 From dfe51421abeae5ab72e01305d58a871612128492 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Thu, 16 Jan 2025 23:05:13 +0100 Subject: [PATCH 08/19] Fixed extra borders at edges --- reviews/BordedDev/sudoku.rewrite.css | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/reviews/BordedDev/sudoku.rewrite.css b/reviews/BordedDev/sudoku.rewrite.css index 4ab1051..f85de58 100644 --- a/reviews/BordedDev/sudoku.rewrite.css +++ b/reviews/BordedDev/sudoku.rewrite.css @@ -26,6 +26,14 @@ 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; -- 2.45.2 From f1c1a02877d9423355d0506d440c7a5736782de3 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 00:05:01 +0100 Subject: [PATCH 09/19] Simplified undefined check --- reviews/BordedDev/sudoku.modified.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/reviews/BordedDev/sudoku.modified.js b/reviews/BordedDev/sudoku.modified.js index 73ae5a6..5d3407b 100644 --- a/reviews/BordedDev/sudoku.modified.js +++ b/reviews/BordedDev/sudoku.modified.js @@ -29,9 +29,7 @@ class EventHandler { if (this.suppresEvents) return [] this.eventCount++ const returnValue = this.events[event].listeners.map((listener) => { - const returnValue = listener(data) - if (returnValue == undefined) return null - return returnValue + return listener(data) ?? null }) this.events[event].callCount++ if (this.debugEvents) { -- 2.45.2 From a24e32249baa6255646c71e387f6a079356fe8fb Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 00:10:56 +0100 Subject: [PATCH 10/19] Correct null assigner instead of or assignment --- reviews/BordedDev/sudoku.modified.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reviews/BordedDev/sudoku.modified.js b/reviews/BordedDev/sudoku.modified.js index 5d3407b..55faac2 100644 --- a/reviews/BordedDev/sudoku.modified.js +++ b/reviews/BordedDev/sudoku.modified.js @@ -13,7 +13,7 @@ class EventHandler { } on(event, listener) { - this.events[event] ||= { name: name, listeners: [], callCount: 0 } + this.events[event] ??= { name: name, listeners: [], callCount: 0 } this.events[event].listeners.push(listener) } -- 2.45.2 From 367e12fe7bb516498162a791eba7fd76bc176df4 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 00:35:20 +0100 Subject: [PATCH 11/19] Removed potential fix for aspect ratio (that didn't work) --- reviews/BordedDev/sudoku.modified.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/reviews/BordedDev/sudoku.modified.js b/reviews/BordedDev/sudoku.modified.js index 55faac2..eea4c4e 100644 --- a/reviews/BordedDev/sudoku.modified.js +++ b/reviews/BordedDev/sudoku.modified.js @@ -724,10 +724,8 @@ class Sudoku extends HTMLElement { for (const row of this.puzzle.rows) { for (const field of row.cols) { const fieldElement = document.createElement("div") - const subFieldElement = document.createElement("div") fieldElement.classList.add("sudoku-field") fieldElement.field = field - fieldElement.appendChild(subFieldElement) field.on("update", (field) => { this._sync() }) -- 2.45.2 From cd18662fed1aa696421abd63cd220b3c8146279b Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 00:50:36 +0100 Subject: [PATCH 12/19] Added typedef for SudokuStates Removed unnecessary parameter --- reviews/BordedDev/sudoku.rewrite.js | 34 ++++++++++++++++------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js index 7f0842d..42d7db5 100644 --- a/reviews/BordedDev/sudoku.rewrite.js +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -21,20 +21,24 @@ function randInt(min, max) { } class SudokuPuzzle extends EventTarget { + /** + * @typedef {(number|null|undefined)[]} SudokuState + */ + /** * - * @type {(number|null)[][]} + * @type {SudokuState[]} */ #state = [] /** * - * @type {(number|null|undefined)[]} + * @type {SudokuState} */ #activeState = new Array(SUDOKU_GRID_SIZE) /** * - * @returns {(number|null|undefined)[]} + * @returns {SudokuState} */ get grid() { const gridValue = [...this.#activeState] @@ -61,7 +65,7 @@ class SudokuPuzzle extends EventTarget { /** * * @param serializedState {string} - * @returns {((number|null|undefined))[]} + * @returns {SudokuState} */ static #stateFromString(serializedState) { let cellIndex = 0 @@ -86,7 +90,7 @@ class SudokuPuzzle extends EventTarget { /** * - * @param stateToSample {(number|null|undefined)[]} + * @param stateToSample {SudokuState} * @returns {undefined|number} */ #sampleRandomEmptyField(stateToSample = this.#activeState) { @@ -98,7 +102,7 @@ class SudokuPuzzle extends EventTarget { /** * - * @param states {...(number|null|undefined)[][]} + * @param states {...SudokuState[]} */ static collapseStates(...states) { return Array.from({ length: SUDOKU_GRID_SIZE }, (_, i) => { @@ -113,7 +117,7 @@ class SudokuPuzzle extends EventTarget { /** * * @param slot {number} - * @param state {(number|null|undefined)[]} + * @param state {SudokuState} * @return {Set} */ #boxGetValues(slot, state = this.#activeState) { @@ -137,7 +141,7 @@ class SudokuPuzzle extends EventTarget { /** * * @param slot {number} - * @param state {(number|null|undefined)[]} + * @param state {SudokuState} * @return {Set} */ #crossGetValues(slot, state = this.#activeState) { @@ -176,7 +180,7 @@ class SudokuPuzzle extends EventTarget { /** * * @param slot {number} - * @param state {(number|null|undefined)[]} + * @param state {SudokuState} * @return {Set} */ getValues(slot, state = this.#activeState) { @@ -189,7 +193,7 @@ class SudokuPuzzle extends EventTarget { * * @param value {number} * @param slot {number} - * @param state {(number|null|undefined)[]} + * @param state {SudokuState} * @return boolean */ isValueValidForSlot(value, slot, state = this.#activeState) { @@ -200,7 +204,7 @@ class SudokuPuzzle extends EventTarget { /** * - * @param newState {(number|null|undefined)[]} + * @param newState {SudokuState} */ fillRandomFieldInState(newState = Array.from({ length: SUDOKU_GRID_SIZE })) { const inProgressState = SudokuPuzzle.collapseStates( @@ -222,7 +226,7 @@ class SudokuPuzzle extends EventTarget { /** * - * @returns {(number|null|undefined)[]} + * @returns {SudokuState} */ #generateRandomState() { const newState = Array.from({ length: SUDOKU_GRID_SIZE }) @@ -235,7 +239,7 @@ class SudokuPuzzle extends EventTarget { /** * - * @returns {(number|null|undefined)[]} + * @returns {SudokuState} */ get baseState() { return this.#state[0] @@ -253,7 +257,7 @@ class SudokuPuzzle extends EventTarget { /** * - * @param newState {(number|null|undefined)[]} + * @param newState {SudokuState} */ applyState(newState) { this.#state.push(newState) @@ -367,7 +371,7 @@ class SudokuHost extends HTMLElement { }) this.#keyProcessors.set("m", () => { - for (const [_, cell] of this.#selectedCells) { + for (const [, cell] of this.#selectedCells) { cell.classList.toggle("marked") cell.classList.remove("selected") } -- 2.45.2 From 4a3aa2cddd6c128806b4aae5bf249b1ac70fa54e Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 00:55:31 +0100 Subject: [PATCH 13/19] Added readonly doc --- reviews/BordedDev/sudoku.rewrite.js | 1 + 1 file changed, 1 insertion(+) diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js index 42d7db5..a5687b3 100644 --- a/reviews/BordedDev/sudoku.rewrite.js +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -39,6 +39,7 @@ class SudokuPuzzle extends EventTarget { /** * * @returns {SudokuState} + * @readonly */ get grid() { const gridValue = [...this.#activeState] -- 2.45.2 From dea989194cbc4ed236992a870106b7c7ac5cc3d2 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 00:59:02 +0100 Subject: [PATCH 14/19] Added def for undefined and null on SudokuState --- reviews/BordedDev/sudoku.rewrite.js | 1 + 1 file changed, 1 insertion(+) diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js index a5687b3..a4d63f7 100644 --- a/reviews/BordedDev/sudoku.rewrite.js +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -22,6 +22,7 @@ function randInt(min, max) { class SudokuPuzzle extends EventTarget { /** + * undefined is an empty cell, null is a cell that was cleared * @typedef {(number|null|undefined)[]} SudokuState */ -- 2.45.2 From 8d1139b2e6c6e94784661f5a12df3ff74d650fdf Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 00:59:52 +0100 Subject: [PATCH 15/19] Removed a line to help with aligning previous comments --- reviews/BordedDev/sudoku.rewrite.js | 1 - 1 file changed, 1 deletion(-) diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js index a4d63f7..ccc2917 100644 --- a/reviews/BordedDev/sudoku.rewrite.js +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -9,7 +9,6 @@ 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} -- 2.45.2 From f08cd376a252d01c65d510725cbe78a19dce11b9 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 01:01:23 +0100 Subject: [PATCH 16/19] Removed extra variable --- reviews/BordedDev/sudoku.rewrite.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js index ccc2917..765bc9b 100644 --- a/reviews/BordedDev/sudoku.rewrite.js +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -95,7 +95,7 @@ class SudokuPuzzle extends EventTarget { * @returns {undefined|number} */ #sampleRandomEmptyField(stateToSample = this.#activeState) { - const iters = [...stateToSample.entries().filter(([_, v]) => v == null)] + const iters = [...stateToSample.entries().filter(([, v]) => v == null)] if (iters.length > 0) { return iters[randInt(0, iters.length - 1)][0] } -- 2.45.2 From ab8b28feba5457cb2d81aac2811de742bb19381c Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 01:12:10 +0100 Subject: [PATCH 17/19] Simplified is valid func --- reviews/BordedDev/sudoku.rewrite.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js index 765bc9b..caf5a73 100644 --- a/reviews/BordedDev/sudoku.rewrite.js +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -24,7 +24,7 @@ class SudokuPuzzle extends EventTarget { * undefined is an empty cell, null is a cell that was cleared * @typedef {(number|null|undefined)[]} SudokuState */ - + /** * * @type {SudokuState[]} @@ -198,9 +198,7 @@ class SudokuPuzzle extends EventTarget { * @return boolean */ isValueValidForSlot(value, slot, state = this.#activeState) { - return !this.#boxGetValues(slot, state) - .union(this.#crossGetValues(slot, state)) - .has(value) + return !this.getValues(slot, state).has(value) } /** -- 2.45.2 From 7ebcbcff413bab9e01753a3f77024d0a9ccde4bd Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 01:20:21 +0100 Subject: [PATCH 18/19] Removed invalid class when valid --- reviews/BordedDev/sudoku.rewrite.js | 38 +++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js index caf5a73..2e49450 100644 --- a/reviews/BordedDev/sudoku.rewrite.js +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -310,6 +310,28 @@ class SudokuHost extends HTMLElement { 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 @@ -379,22 +401,6 @@ class SudokuHost extends HTMLElement { }) } - 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.add("invalid") - } - } - } - } - #selectCell(i, cell) { if (this.hasAttribute("readonly") || !this.isActive) return this.#selectedCells.set(i, cell) -- 2.45.2 From 77e78c8031b333cfc260e08636c20c676d124000 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Fri, 17 Jan 2025 01:21:51 +0100 Subject: [PATCH 19/19] Removed old console.log --- reviews/BordedDev/sudoku.rewrite.js | 1 - 1 file changed, 1 deletion(-) diff --git a/reviews/BordedDev/sudoku.rewrite.js b/reviews/BordedDev/sudoku.rewrite.js index 2e49450..4afd963 100644 --- a/reviews/BordedDev/sudoku.rewrite.js +++ b/reviews/BordedDev/sudoku.rewrite.js @@ -463,7 +463,6 @@ class SudokuHost extends HTMLElement { "state-changed", this.syncCellsToSudoku.bind(this), ) - console.log(this.#activePuzzle) } attributeChangedCallback(name, oldValue, newValue) { -- 2.45.2