Compare commits

..

No commits in common. "main" and "main" have entirely different histories.
main ... main

8 changed files with 0 additions and 2645 deletions

View File

@ -1,34 +0,0 @@
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 (inside out size issues was excuse).
There is a bunch of unused code
Autosolver is broken, I had to move the function around
Switched .forEach to for loop for performance (recommended practice, but honestly its not a big deal)
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
I'd recommend running Biome.js or ESlint + Prettier to clean up the code
(if you're using ML you can even make these strict enough about ordering and naming conventions)
When you create an object where the key's and value's name are the same, you can use the shorthand `{ value }` instead of `{ value: value }`
Border radius doesn't propagate to the children, so the cells are still square and will show if it has a background

View File

@ -1,925 +0,0 @@
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

@ -1,854 +0,0 @@
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) => {
return listener(data) ?? null
})
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")
fieldElement.classList.add("sudoku-field")
fieldElement.field = field
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

@ -1,73 +0,0 @@
:host {
display: inline-block;
--border-radius: 5px;
}
.sudoku {
user-select: none;
border: 1px solid #ccc;
font-size: 13px;
color: #222;
display: grid;
grid-template-columns: repeat(9, 1fr);
grid-auto-rows: auto;
aspect-ratio: 1 / 1;
background-color: #e5e5e5;
border-radius: var(--border-radius);
overflow: hidden;
.sudoku-field {
aspect-ratio: 1 / 1;
width: 1em;
line-height: 1;
text-align: center;
padding: 2px;
border: 1px solid #ccc;
border-left: none;
border-top: none;
/* Disable borders at the edges */
&:nth-child(9n) {
border-right: none;
}
&:nth-last-child(-n + 9) {
border-bottom: none;
}
/* Cross bars */
&:nth-child(3n):not(:nth-child(9n)) {
border-right-color: black;
}
&:nth-child(n + 19):nth-child(-n + 27),
&:nth-child(n + 46):nth-child(-n + 54) {
border-bottom-color: black;
}
/* Match the corners with parent, otherwise they'll render on top */
&:nth-child(1) {
border-top-left-radius: var(--border-radius);
}
&:nth-child(9) {
border-top-right-radius: var(--border-radius);
}
&:nth-last-child(1) {
border-bottom-right-radius: var(--border-radius);
}
&:nth-last-child(9) {
border-bottom-left-radius: var(--border-radius);
}
&.initial {
color: #777;
}
&.selected {
background-color: lightgreen;
}
&.marked {
background-color: blue;
}
&.invalid {
color: red;
}
}
}

View File

@ -1,484 +0,0 @@
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 {
/**
* undefined is an empty cell, null is a cell that was cleared
* @typedef {(number|null|undefined)[]} SudokuState
*/
/**
*
* @type {SudokuState[]}
*/
#state = []
/**
*
* @type {SudokuState}
*/
#activeState = new Array(SUDOKU_GRID_SIZE)
/**
*
* @returns {SudokuState}
* @readonly
*/
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 {SudokuState}
*/
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 {SudokuState}
* @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 {...SudokuState[]}
*/
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 {SudokuState}
* @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 {SudokuState}
* @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 {SudokuState}
* @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 {SudokuState}
* @return boolean
*/
isValueValidForSlot(value, slot, state = this.#activeState) {
return !this.getValues(slot, state).has(value)
}
/**
*
* @param newState {SudokuState}
*/
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
}
/**
*
* @returns {SudokuState}
*/
#generateRandomState() {
const newState = Array.from({ length: SUDOKU_GRID_SIZE })
for (let i = 0; i < 17; i++) {
this.fillRandomFieldInState(newState)
}
return newState
}
/**
*
* @returns {SudokuState}
*/
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 } }),
)
}
/**
*
* @param newState {SudokuState}
*/
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()
this.#generateKeyProcessors()
}
syncCellsToSudoku() {
const puzzleGrid = this.#activePuzzle.grid
const baseGrid = this.#activePuzzle.baseState
for (let i = 0; i < SUDOKU_GRID_SIZE; i++) {
this.#cellGrid[i].innerHTML = puzzleGrid[i] ?? ""
if (baseGrid[i] != null) {
this.#cellGrid[i].classList.add("initial")
} else if (puzzleGrid[i] != null) {
if (this.#activePuzzle.isValueValidForSlot(puzzleGrid[i], i)) {
this.#cellGrid[i].classList.remove("invalid")
} else {
this.#cellGrid[i].classList.add("invalid")
}
}
}
}
#generateKeyProcessors() {
const cellValueProcessor = (event) => {
if (this.#selectedCells.size === 0) return
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()
})
}
#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),
)
}
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

@ -1,83 +0,0 @@
<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

@ -1,96 +0,0 @@
<!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

@ -1,96 +0,0 @@
<!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>