Initial comments + rewrite + modifications
This commit is contained in:
		
							parent
							
								
									52ddba9744
								
							
						
					
					
						commit
						b5c1058b3b
					
				
							
								
								
									
										27
									
								
								reviews/BordedDev/notes.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								reviews/BordedDev/notes.md
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
							
								
								
									
										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; | ||||||
|  | } | ||||||
|  | 
 | ||||||
							
								
								
									
										858
									
								
								reviews/BordedDev/sudoku.modified.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										858
									
								
								reviews/BordedDev/sudoku.modified.js
									
									
									
									
									
										Normal file
									
								
							| @ -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 | ||||||
|  | } | ||||||
							
								
								
									
										460
									
								
								reviews/BordedDev/sudoku.rewrite.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										460
									
								
								reviews/BordedDev/sudoku.rewrite.js
									
									
									
									
									
										Normal file
									
								
							| @ -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<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 {(number|null|undefined)[]} | ||||||
|  | 	 * @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 {(number|null|undefined)[]} | ||||||
|  | 	 * @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 {(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<number, HTMLElement>} | ||||||
|  | 	 */ | ||||||
|  | 	#selectedCells = new Map() | ||||||
|  | 
 | ||||||
|  | 	/** | ||||||
|  | 	 * | ||||||
|  | 	 * @type {Map<string, (event: KeyboardEvent) => 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) | ||||||
							
								
								
									
										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
	 BordedDev
						BordedDev