Initial comments + rewrite + modifications

This commit is contained in:
Benjamin Claassen 2025-01-14 09:49:41 +01:00
parent 52ddba9744
commit 01e00f52dc
No known key found for this signature in database
GPG Key ID: C5F495EAE56673BF
7 changed files with 2545 additions and 0 deletions

View File

@ -0,0 +1,27 @@
It doesnt render correctly on firefox but does in edge, its 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 its 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 its 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
View 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() : '&nbsp;'
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;
}

View 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() : "&nbsp;"
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
}

View 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 = "&nbsp;"
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)

View 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>

View 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>

View 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>