{
"extension": ".js",
"source": "\n\n\nfunction randInt(min, max) {\n min = Math.ceil(min);\n max = Math.floor(max);\n return Math.floor(Math.random() * (max - min + 1)) + min;\n}\n\nfunction cssRuleExists(match){\n for(let i = 0; i < document.styleSheets.length; i++){\n let styleSheet = document.styleSheets[i]\n let rules = styleSheet.cssRules\n for(let j = 0; j < rules.length; j++){\n let rule = rules[j]\n if(rule.selectorText && rule.selectorText == match){\n return true;\n }\n }\n }\n return false\n}\n\nclass EventHandler {\n constructor() {\n this.events = {};\n this.eventCount = 0;\n this.suppresEvents = false;\n this.debugEvents = false;\n }\n on(event, listener) {\n if (!this.events[event]) {\n this.events[event] = { name:name, listeners: [],callCount: 0};\n }\n this.events[event].listeners.push(listener);\n }\n off(event, listenerToRemove) {\n if (!this.events[event]) return;\n this.events[event].listeners = this.events[event].listeners.filter(listener => listener !== listenerToRemove);\n }\n emit(event, data) {\n if (!this.events[event]) return [];\n if (this.suppresEvents) return [];\n this.eventCount++;\n const returnValue = this.events[event].listeners.map(listener =>{\n var returnValue = listener(data)\n if(returnValue == undefined)\n return null\n return returnValue\n });\n this.events[event].callCount++;\n if(this.debugEvents){\n console.debug('debugEvent',{event:event, arg:data, callCount: this.events[event].callCount, number:this.eventCount, returnValues:returnValue})\n }\n return returnValue\n }\n suppres(fn) {\n const originallySuppressed = this.suppresEvents\n this.suppresEvents = true \n fn(this)\n this.suppresEvents = originallySuppressed\n return originallySuppressed\n }\n}\n\n\nclass Col extends EventHandler {\n id = 0\n values = []\n initial = false\n _marked = false\n row = null\n index = 0\n valid = true\n _selected = false\n _value = 0\n _isValidXy() {\n if (!this._value)\n return true;\n return this.row.puzzle.fields.filter(field => {\n return (\n field.index == this.index && field.value == this._value\n ||\n field.row.index == this.row.index && field.value == this._value)\n\n }).filter(field => field != this).length == 0\n }\n mark() {\n this.marked = true \n }\n unmark() {\n this.marked = false\n }\n select() {\n this.selected = true\n }\n unselect() {\n this.selected = false\n }\n _isValidBox() {\n if (!this._value)\n return true;\n let startRow = this.row.index - this.row.index % (this.row.puzzle.size / 3);\n let startCol = this.index - this.index % (this.row.puzzle.size / 3);\n for (let i = 0; i < this.row.puzzle.size / 3; i++) {\n for (let j = 0; j < this.row.puzzle.size / 3; j++) {\n const field = this.row.puzzle.get(i + startRow, j + startCol);\n if (field != this && field.value == this._value) {\n return false;\n }\n }\n }\n return true\n }\n validate() {\n if (!this.row.puzzle.initalized) {\n return this.valid;\n }\n if (this.initial) {\n this.valid = true\n return this.valid\n }\n if (!this.value && !this.valid) {\n this.valid = true\n this.emit('update', this)\n return this.valid\n }\n let oldValue = this.valid\n this.valid = this._isValidXy() && this._isValidBox();\n if (oldValue != this.valid) {\n this.emit('update', this)\n }\n return this.valid\n }\n set value(val) {\n if (this.initial)\n return;\n const digit = Number(val)\n const validDigit = digit >= 0 && digit <= 9;\n let update = validDigit && digit != this.value\n if (update) {\n this._value = Number(digit)\n this.validate()\n this.emit('update', this);\n }\n }\n get value() {\n return this._value\n }\n get selected() {\n return this._selected\n }\n set selected(val) {\n if (val != this._selected) {\n this._selected = val\n if(this.row.puzzle.initalized)\n this.emit('update', this);\n }\n }\n get marked() {\n return this._marked\n }\n set marked(val){\n if(val != this._marked){\n this._marked = val \n if(this.row.puzzle.initalized){\n this.emit('update',this)\n }\n }\n }\n constructor(row) {\n super()\n this.row = row\n this.index = this.row.cols.length\n this.id = this.row.puzzle.rows.length * this.row.puzzle.size + this.index;\n this.initial = false\n this.selected = false\n this._value = 0;\n this.marked = false\n this.valid = true\n }\n update() {\n this.emit('update',this)\n }\n toggleSelected() {\n this.selected = !this.selected \n }\n toggleMarked() {\n this.marked = !this.marked \n }\n get data() {\n return {\n values: this.values,\n value: this.value,\n index: this.index,\n id: this.id,\n row: this.row.index,\n col: this.index,\n valid: this.valid,\n initial: this.initial,\n selected: this.selected,\n marked: this.marked \n }\n }\n toString() {\n return String(this.value)\n }\n toText() {\n return this.toString().replace(\"0\", \" \");\n }\n}\nclass Row extends EventHandler {\n cols = []\n puzzle = null\n index = 0\n initialized = false\n constructor(puzzle) {\n super()\n this.puzzle = puzzle\n this.cols = []\n this.index = this.puzzle.rows.length\n const me = this\n this.initialized = false\n for (let i = 0; i < puzzle.size; i++) {\n const col = new Col(this);\n this.cols.push(col);\n col.on('update', (field) => {\n me.emit('update', field)\n })\n }\n this.initialized = true\n }\n get data() {\n return {\n cols: this.cols.map(col => col.data),\n index: this.index\n }\n }\n toText() {\n let result = ''\n for (let col of this.cols) {\n result += col.toText();\n }\n return result\n }\n toString() {\n return this.toText().replaceAll(\" \", \"0\");\n }\n}\n\nclass Puzzle extends EventHandler {\n rows = []\n size = 0\n hash = 0\n states = []\n parsing = false\n _initialized = false\n initalized = false\n _fields = null\n constructor(arg) {\n super()\n this.debugEvents = true;\n this.initalized = false\n this.rows = []\n if (isNaN(arg)) {\n // load session\n } else {\n this.size = Number(arg)\n }\n for (let i = 0; i < this.size; i++) {\n const row = new Row(this);\n this.rows.push(row);\n row.on('update', (field) => {\n this.onFieldUpdate(field)\n })\n }\n this._initialized = true\n this.initalized = true\n this.commitState()\n }\n validate() {\n return this.valid;\n }\n _onEventHandler(){\n this.eventCount++;\n }\n makeInvalid() {\n if (!app.valid) {\n let invalid = this.invalid;\n return invalid[invalid.length - 1];\n }\n this.rows.forEach(row => {\n row.cols.forEach(col => {\n if (col.value) {\n let modify = null;\n if (col.index == this.size) {\n modify = this.get(row.index, col.index - 2);\n } else {\n modify = this.get(row.index, col.index + 1);\n }\n modify.value = col.value\n // last one is invalid\n return modify.index > col.index ? modify : col;\n }\n col.valid = false\n })\n })\n this.get(0, 0).value = 1;\n this.get(0, 1).value = 1;\n return this.get(0, 1);\n }\n reset() {\n this._initialized = false\n this.initalized == false;\n this.parsing = true\n this.fields.forEach(field => {\n field.initial = false\n field.selected = false\n field.marked = false\n field.value = 0\n })\n this.hash = 0\n this.states = []\n this.parsing = false\n this.initalized = true\n this._initialized = true\n this.commitState()\n }\n get valid() {\n return this.invalid.length == 0\n }\n get invalid() {\n this.emit('validating',this)\n const result = this.fields.filter(field => !field.validate())\n this.emit('validated',this)\n return result\n }\n get selected() {\n return this.fields.filter(field => field.selected)\n }\n get marked(){\n return this.fields.filter(field=>field.marked)\n }\n loadString(content) {\n this.emit('parsing', this)\n this.reset()\n this.parsing = true\n this.initalized = false;\n this._initialized = false;\n\n const regex = /\\d/g;\n const matches = [...content.matchAll(regex)]\n let index = 0;\n const max = this.size * this.size;\n matches.forEach(match => {\n const digit = Number(match[0]);\n let field = this.fields[index]\n field.value = digit;\n field.initial = digit != 0\n index++;\n });\n this._initialized = true;\n this.parsing = false\n this.deselect();\n this.initalized = true;\n this.suppres(()=>{\n this.fields.forEach((field)=>{\n field.update()\n })\n })\n this.commitState()\n this.emit('parsed', this)\n this.emit('update',this)\n }\n get state() {\n return this.getData(true)\n }\n get previousState() {\n if (this.states.length == 0)\n return null;\n return this.states.at(this.states.length - 1)\n }\n get stateChanged() {\n if (!this._initialized)\n return false\n return !this.previousState || this.state != this.previousState\n }\n\n commitState() {\n if (!this.initalized)\n return false;\n this.hash = this._generateHash()\n if (this.stateChanged) {\n this.states.push(this.state)\n this.emit('commitState', this)\n return true\n }\n return false\n }\n onFieldUpdate(field) {\n if (!this.initalized)\n return false;\n if (!this._initialized)\n return;\n this.validate();\n this.commitState();\n this.emit('update', this)\n }\n\n get data() {\n return this.getData(true)\n }\n\n popState() {\n let prevState = this.previousState\n if (!prevState)\n {\n this.deselect() \n return null\n }while (prevState && prevState.hash == this.state.hash)\n prevState = this.states.pop()\n if (!prevState)\n {\n this.deselect()\n return null\n }\n this.applyState(prevState)\n this.emit('popState', this)\n return prevState\n }\n applyState(newState) {\n\n this._initialized = false\n newState.fields.forEach(stateField => {\n let field = this.get(stateField.row, stateField.col)\n field.selected = stateField.selected\n field.values = stateField.values\n field.value = stateField.value\n field.initial = stateField.initial\n field.validate()\n })\n this._initialized = true\n this.emit('stateApplied', this)\n this.emit('update', this)\n }\n getData(withHash = false) {\n let result = {\n fields: this.fields.map(field => field.data),\n size: this.size,\n valid: this.valid\n }\n if (withHash) {\n result['hash'] = this._generateHash()\n }\n return result;\n }\n get(row, col) {\n if (!this.initalized)\n return null;\n if (!this.rows.length)\n return null;\n if (!this.rows[row])\n return null;\n return this.rows[row].cols[col];\n }\n get fields() {\n if (this._fields == null) {\n this._fields = []\n for (let row of this.rows) {\n for (let col of row.cols) {\n this._fields.push(col)\n }\n }\n }\n return this._fields\n }\n _generateHash() {\n var result = 0;\n JSON.stringify(this.getData(false)).split('').map(char => {\n return char.charCodeAt(0) - '0'.charCodeAt(0)\n }).forEach(num => {\n result += 26\n result = result + num\n })\n return result\n }\n get text() {\n let result = ''\n for (let row of this.rows) {\n result += row.toText() + \"\\n\"\n }\n result = result.slice(0, result.length - 1)\n return result\n }\n get initialFields() {\n return this.fields.filter(field => field.initial)\n }\n get json() {\n return JSON.stringify(this.data)\n }\n get zeroedText() {\n return this.text.replaceAll(\" \", \"0\")\n }\n get string() {\n return this.toString()\n }\n toString() {\n return this.text.replaceAll(\"\\n\",\"\").replaceAll(\" \", \"0\")\n }\n get humanFormat() {\n return ' ' + this.text.replaceAll(\" \", \"0\").split(\"\").join(\" \")\n }\n getRandomField() {\n const emptyFields = this.empty;\n return emptyFields[randInt(0, emptyFields.length - 1)]\n }\n update(callback) {\n this.commitState()\n this.intalized = false\n callback(this);\n this.intalized = true\n this.validate()\n this.commitState()\n this.intalized = false \n if(this.valid)\n this.deselect()\n this.intalized = true\n this.emit('update', this)\n }\n get empty() {\n return this.fields.filter(field => field.value == 0)\n }\n getRandomEmptyField() {\n let field = this.getRandomField()\n if (!field)\n return null\n return field\n }\n deselect() {\n this.fields.forEach(field => field.selected = false)\n }\n generate() {\n this.reset()\n this.initalized = false\n for (let i = 0; i < 17; i++) {\n this.fillRandomField()\n }\n this.deselect()\n this.initalized = true\n this.commitState()\n this.emit('update',this)\n }\n fillRandomField() {\n let field = this.getRandomEmptyField()\n if (!field)\n return\n this.deselect()\n field.selected = true\n let number = 0\n number++;\n\n while (number <= 9) {\n field.value = randInt(1, 9)\n field.update()\n if (this.validate()) {\n field.initial = true\n return field\n }\n number++;\n }\n return false;\n }\n\n}\n\nclass PuzzleManager {\n constructor(size) {\n this.size = size\n this.puzzles = []\n this._activePuzzle = null\n }\n\n addPuzzle(puzzle){\n this.puzzles.push(puzzle)\n }\n get active(){\n return this.activePuzzle\n }\n set activePuzzle(puzzle){\n this._activePuzzle = puzzle\n }\n get activePuzzle(){\n return this._activePuzzle\n }\n}\n\nconst puzzleManager = new PuzzleManager(9)\n\nclass Sudoku extends HTMLElement {\n styleSheet = `\n .sudoku {\n font-size: 13px;\n color:#222;\n display: grid;\n grid-template-columns: repeat(9, 1fr);\n grid-template-rows: auto;\n gap: 0px;\n user-select: none;\n -webkit-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n background-color: #e5e5e5;\n border-radius: 5px;\n aspect-ratio: 1/1;\n }\n .sudoku-field-initial {\n color: #777;\n }\n .sudoku-field-selected {\n background-color: lightgreen;\n }\n .soduku-field-marked {\n background-color: blue;\n }\n .sudoku-field-invalid {\n color: red;\n }\n .sudoku-field {\n border: 1px solid #ccc;\n text-align: center;\n padding: 2px;\n aspect-ratio: 1/1;\n }`\n set fieldSize(val) {\n this._fieldSize = val ? Number(val) : null\n this.fieldElements.forEach(field => {\n field.style.fontSize = this._fieldSize ? this._fieldSize.toString() +'px' : ''\n })\n }\n get fieldSize(){\n return this._fieldSize\n }\n get eventCount() {\n return this.puzzle.eventCount\n }\n get puzzleContent(){\n return this.puzzle.humanFormat\n }\n set puzzleContent(val) {\n if (val == \"generate\") {\n this.puzzle.generate()\n } else if (val) {\n this.puzzle.loadString(val)\n } else {\n this.puzzle.reset()\n }\n }\n connectedCallback() {\n this.puzzleContent = this.getAttribute('puzzle') ? this.getAttribute('puzzle') : null\n this._fieldSize = null\n this.fieldSize = this.getAttribute('size') ? this.getAttribute('size') : null\n this.readOnly = this.getAttribute('read-only') ? true : false\n this.attachShadow({ mode: 'open' });\n this.shadowRoot.appendChild(this.styleElement)\n this.shadowRoot.appendChild(this.puzzleDiv)\n }\n toString(){\n return this.puzzleContent\n }\n set active(val){\n this._active = val \n if(this._active)\n this.manager.activePuzzle =this \n }\n get active(){\n return this._active\n }\n set readOnly(val){\n this._readOnly = val ? true : false\n }\n get readOnly(){\n return this._readOnly\n }\n constructor() {\n super();\n this._readOnly = false;\n this._active = false\n this.fieldElements = []\n this.puzzle = new Puzzle(9)\n this.fields = []\n this.styleElement = document.createElement('style');\n this.styleElement.textContent = this.styleSheet\n this.puzzleDiv = document.createElement('div')\n this.puzzleDiv.classList.add('sudoku');\n this._bind()\n this.manager.addPuzzle(this)\n }\n get manager() {\n return puzzleManager\n }\n _bind(){\n this._bindFields()\n this._bindEvents()\n this._sync()\n }\n _bindFields(){\n const me = this\n this.puzzle.rows.forEach((row) => {\n row.cols.forEach((field) => {\n const fieldElement = document.createElement('div');\n fieldElement.classList.add('sudoku-field');\n fieldElement.field = field\n field.on('update', (field) => {\n me._sync()\n })\n fieldElement.addEventListener('click', (e) => {\n if(!me.readOnly)\n field.toggleSelected()\n })\n fieldElement.addEventListener('contextmenu',(e)=>{\n e.preventDefault()\n field.row.puzzle.update(()=>{\n field.selected = false\n field.value = 0\n })\n })\n this.fields.push(field)\n this.fieldElements.push(fieldElement)\n this.puzzleDiv.appendChild(fieldElement);\n });\n });\n }\n _bindEvents(){\n const me = this\n this.puzzle.on('update', () => {\n me._sync()\n });\n this.puzzleDiv.addEventListener('mouseenter', (e) => {\n me.active = true \n })\n this.puzzleDiv.addEventListener('mouseexit', (e) => {\n me.active = false \n })\n document.addEventListener('keydown', (e) => {\n if(me.readOnly)\n return\n if (!puzzleManager.active)\n return\n const puzzle = puzzleManager.active.puzzle\n if (e.key == 'u') {\n puzzle.popState();\n } else if (e.key == 'd') {\n puzzle.update((target) => {\n puzzle.selected.forEach(field => {\n field.value = 0\n });\n })\n } else if (e.key == 'a') {\n puzzle.autoSolve()\n } else if (e.key == 'r') {\n puzzle.fillRandomField();\n } else if (!isNaN(e.key)) {\n puzzle.update((target) => {\n puzzle.selected.forEach(field => {\n field.value = Number(e.key)\n })\n });\n } else if(e.key == 'm'){\n let fields = [];\n puzzle.update((target) => {\n target.selected.forEach(field => {\n field.selected = false;\n fields.push(field)\n });\n });\n puzzle.update((target)=>{\n fields.forEach((field)=>{\n field.toggleMarked();\n })\n });\n puzzle.emit('update',puzzle);\n }\n })\n }\n autoSolve() {\n const me = this\n window.requestAnimationFrame(() => {\n if (me.fillRandomField()) {\n if (me.empty.length)\n return me.autoSolve()\n }\n })\n }\n get(row, col){\n return this.puzzle.get(row,col)\n }\n _syncField(fieldElement) {\n const field = fieldElement.field \n fieldElement.classList.remove('sudoku-field-selected')\n fieldElement.classList.remove('sudoku-field-empty')\n fieldElement.classList.remove('sudoku-field-invalid')\n fieldElement.classList.remove('sudoku-field-initial')\n fieldElement.classList.remove('sudoku-field-marked')\n console.info('Removed marked class');\n fieldElement.innerHTML = field.value ? field.value.toString() : '&nbsp;'\n \n if (field.selected) {\n fieldElement.classList.add('sudoku-field-selected')\n window.selected = field.field\n }\n if (!field.valid) {\n fieldElement.classList.add('sudoku-field-invalid')\n }\n if (!field.value) {\n fieldElement.classList.add('sudoku-field-empty')\n }\n if(field.initial){\n fieldElement.classList.add('sudoku-field-initial')\n }\n if(field.marked){\n fieldElement.classList.add('sudoku-field-marked')\n console.info(\"added marked lcass\")\n }\n\n }\n _sync() {\n this.fieldElements.forEach(fieldElement => {\n this._syncField(fieldElement);\n })\n }\n\n}\ncustomElements.define(\"my-sudoku\", Sudoku);\n\nfunction generateIdByPosition(element) {\n const parent = element.parentNode;\n const index = Array.prototype.indexOf.call(parent.children, element);\n const generatedId = `${element.tagName.toLowerCase()}-${index}`;\n element.id = generatedId.replace('div-', 'session-key-');\n return element.id;\n}\n\n",
"review": "# Grade: 6\n\n## Bugs\n- There are several typos and inconsistencies, e.g., the use of both `initalized` and `initialized`.\n- The `suppresEvents` property is not correctly spelt; it should be `suppressEvents`.\n- The method `_isValidXy` contains overlapping conditions which could be logically simplified.\n- The `makeInvalid` method references an `app` object that is not defined within this context.\n- Use of `document.styleSheets` in `cssRuleExists` fails due to potential security restrictions on cross-origin stylesheets.\n- Inconsistent use of `null` vs `undefined` in return values.\n\n## Optimizations\n- Use parameters validation more thoroughly, such as checking for valid inputs in `randInt`.\n- The naming convention could be improved for understandability and consistency, e.g., `suppres` function.\n- Repeated code, such as filtering methods, can be refactored for optimization, like combining filter operations.\n- Use modern ES6+ features, such as default parameters, Spread or Rest operators or ES6 modules, to streamline the code.\n- Better management of state could be achieved through a state management solution or library.\n- Reduce use of `console.debug` and replace with conditional logging through feature toggles.\n- Use a consistent naming convention for properties and methods (e.g., camelCase).\n- Optimize the reactivity in updating UI elements to avoid unnecessary re-renders.\n\n## Good points\n- Modularity by using classes like `Puzzle`, `Row`, and `Col` provides a good separation of concerns.\n- Use of event handling demonstrates a comprehension of the JavaScript event system.\n- `Sudoku` component as a custom HTML element shows an understanding of modern web technologies.\n- Clear intention for a large part of the code logic supporting a Sudoku game, showing substantial problem domain knowledge.\n\n## Summary\nThe code represents a good attempt at creating a Sudoku-like game with extendable and maintainable constructs using custom elements and modular JavaScript. However, it suffers from certain inconsistencies and potential bugs which can lead to execution errors. Code optimization techniques could be applied to increase performance and maintainability, and to fix some of the identified issues. Thoughtful comments and clearer naming can enhance code readability and understandability. \n\n## Open source alternatives\n- [SudokuJS](https://github.com/robatron/sudoku.js) \n- [js-sudoku](https://github.com/robatron/sudoku.js/)\n- [sudoku-generator](https://github.com/robatron/sudoku.js/)",
"filename": "sudoku.js",
"path": "sudoku.js",
"directory": "",
"grade": 6,
"size": 23740,
"line_count": 843
}