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') ? true : false 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 ? true : false } 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() { const me = this window.requestAnimationFrame(() => { if (me.fillRandomField()) { if (me.empty.length) return me.autoSolve() } }) } get(row, col){ return this.puzzle.get(row,col) } _syncField(fieldElement) { const field = fieldElement.field fieldElement.classList.remove('sudoku-field-selected') fieldElement.classList.remove('sudoku-field-empty') fieldElement.classList.remove('sudoku-field-invalid') fieldElement.classList.remove('sudoku-field-initial') fieldElement.classList.remove('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) { // Get the parent element const parent = element.parentNode; // Get the index of the element within its parent const index = Array.prototype.indexOf.call(parent.children, element); // Generate a unique ID using the tag name and index const generatedId = `${element.tagName.toLowerCase()}-${index}`; // Assign the generated ID to the element element.id = generatedId.replace('div-', 'session-key-'); return element.id; }