711 lines
17 KiB
HTML
Raw Normal View History

2025-01-10 21:53:10 +00:00
<html>
<head>
</head>
<body>
<div id="app" class="app"></div>
<pre>
TODO:
- if orange fields, no other selections should be possible than deselect.
</pre>
<template id="template_sudoku_container">
<div style="width:640px;height:640px;font-size:1.2em;" class="container-content"></div>
</template>
<template id="template_sudoku_parser">
<div style="width:100%;height:100%" class="parser-content"></div>
</template>
<template id="template_sudoku_cell">
<div class="cell-content"></div>
</template>
<style type="text/css">
.app {
width: 400px;
height: 400px;
}
.cell-content {
font-family: 'Courier New', Courier, monospace;
user-select: none;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
border: 1px solid #CCCCCC;
text-align: center;
height: 10%;
width: 10%;
float: left;
display: flex;
justify-content: center;
align-items: center;
font-size: calc(5px + 1vw);
}
.sudoku-cell-selected {
background-color: lightgreen;
}
.sudoku-cell-invalid {
color: red;
font-weight: bold;
}
</style>
<script type="text/javascript">
const game_size = 9;
const container_template = document.getElementById('template_sudoku_container');
const cell_template = document.getElementById("template_sudoku_cell");
const sudokuParserTemplate = document.getElementById('template_sudoku_parser');
const container = document.getElementById('app'); //.importNode(container_template.content, true);
const _hashChars = "abcdefghijklmnopqrstuvwxyz\"':[]0123456789";
function hashString(str) {
let result = 0;
for (let i = 0; i < str.length; i++) {
result += (_hashChars.indexOf(str[i]) + 1) * (i * 40);
}
return result;
}
class State {
number = 0
cells = []
selection = []
selectionCount = 0
_string = null
_hash = null
_json = null;
constructor(arg) {
if (isNaN(arg)) {
this.number = arg.number;
this.cells = arg.cells;
this.selectCount = arg.selectionCount;
this._hash = arg._hash;
this._json = arg;
} else {
this.number = arg;
}
}
toJson() {
if (this._json == null) {
const json = {
number: this.number,
cells: this.cells,
selectionCount: this.selectionCount,
_hash: this.getHash()
};
this._json = json;
}
return this._json;
}
getHash() {
if (this._hash == null) {
let started = false;
let count = 0;
let hash = this.cells.filter((cell) => {
if (!started && cell.state[0] == 0 && cell.state[1] == 1)
return false;
started = true;;
count++;
return true;
}).map(cell => {
return cell.state;
}).join('');
this._hash = `${count}${hash}`;
}
return this._hash
}
equals(otherState) {
if (otherState.selectionCount != this.selectionCount)
return false;
return otherState.getHash() == this.getHash();
}
toString() {
if (this._string == null) {
this._string = JSON.stringify({
number: this.number,
selection: this.selection.map(cell => cell.toString()),
cells: this.cells
});
}
return this._string
}
};
class Cell {
row = 0;
col = 0;
initial = false;
letter = null
name = null
options = [];
value = 0;
values = []
element = null;
app = null;
selected = false;
container = null;
async solve(){
this.app.pushState();
let originalValues = this.values;
let valid = false;
this.selected = false;
for(let i = 1; i < 10; i++){
this.addNumber(i);
if(this.validate())
if(await this.app.solve())
return true;
this.value = 0;
this.values = originalValues;
}
return false;
}
getState() {
return `${this.selected ? 1 : 0}${this.valid ? 1 : 0}`;
}
toggleSelect() {
this.selected = !this.selected;
if (this.selected) {
this.select();
//this.element.classList.add('sudoku-cell-selected');
} else {
this.deSelect();
//this.element.classList.remove('sudoku-cell-selected');
}
this.update();
return this.selected;
}
async addNumber(value) {
this.values.pop(value);
this.values.push(value);
this.value = Number(value);
const _this = this;
window.requestAnimationFrame(() => {
this.element.textContent = this.value == 0 ? "" : String(this.value);
})
this.validate();
//this.update();
}
onClick() {
//this.
if (!this.initial)
this.toggleSelect();
//this.app.onCellClick(this)
}
deSelect() {
this.selected = false;
this.element.classList.remove('sudoku-cell-selected');
}
select() {
this.selected = true;
this.element.classList.add('sudoku-cell-selected');
}
validateBox(){
let startRow = this.row - this.row % (9 / 3);
let startCol = this.col - this.col % (9 / 3);
for (let i = startRow; i < 9 / 3; i++) {
for (let j = startCol; j < 9 / 3; j++) {
let fieldIndex = (i * 9) + j;
console.info(fieldIndex);
if (this.app.cells[fieldIndex].value == this.value) {
return false;
}
}
return true
}
return true;
}
isValid() {
const _this = this;
this.valid = !this.value && (!this.app.cells.filter(cell => {
return cell.value != 0 &&
cell != _this &&
(cell.row == _this.row || cell.col == _this.col) &&
cell.value == _this.value;
}).length && this.validateBox()); //&& !this.app.getBoxValues(this.name).indexOf(this.value) == -1);
return this.valid;
}
update() {
if (this.selected)
this.select()
else
this.deSelect()
this.app.cells.forEach(cell => {
cell.validate()
})
this.element.textContent = this.value ? String(this.value) : "";
}
validate() {
if (this.isValid() || !this.value) {
this.element.classList.remove('sudoku-cell-invalid');
} else {
this.element.classList.add('sudoku-cell-invalid');
}
return this.valid;
}
destructor() {
//this.container.delete();
}
constructor(app, row, col) {
this.app = app;
this.container = document.importNode(cell_template.content, true);
this.row = row;
this.col = col;
this.selected = false;
this.letter = "abcdefghi"[row];
this.name = `${this.letter}${this.col}`
this.value = 0;
this.values = [];
this.element = this.container.querySelector('.cell-content');
this.valid = true;
const _this = this;
this.element.addEventListener('click', (e) => {
_this.onClick();
});
this.element.addEventListener('mousemove', (e) => {
if (!_this.initial && e.buttons == 1)
_this.select();
else if (!_this.initial && e.buttons == 2)
_this.deSelect();
else
_this.app.pushState()
});
this.element.addEventListener('mouseexit', (e) => {
if (!e.buttons) {
// _this.app.pushState();
}
});
this.element.addEventListener('contextmenu', (e) => {
e.preventDefault();
if (!_this.initial && _this.selected) {
_this.deSelect();
} else {
_this.values.pop(_this.value);
_this.addNumber(0);
_this.deSelect();
_this.update();
//_this.app.deSelectAll();
}
});
}
redraw() {
}
render(container) {
container.appendChild(this.container);
}
toString() {
return `${this.row}:${this.col}`
}
}
class SudokuParser {
app = null
element = null
container = null
blank = true
content = ''
size = 9
constructor(app, container) {
//this.container = container;
this.app = app;
this.container = document.importNode(sudokuParserTemplate.content, true);
this.element = document.createElement('div');
this.element.style.width = '90%';
this.element.style.height = '50%';
this.element.border = '1px solid #CCCCCC';
this.content = '';
this.cells = []
this.element.contentEditable = true;
this.element.textContent = 'Paste puzzle here';//this.container.querySelector('.parser-content');
this.toggle();
this.blank = true;
const _this = this;
this.element.addEventListener('click', (e) => {
if (_this.blank)
_this.element.textContent = '';
_this.blank = false;
});
this.element.addEventListener('contextmenu', (e) => {
if (_this.blank)
_this.element.textContent = '';
this.blank = false;
});
this.element.addEventListener('input', (e) => {
_this.element.innerHTML = this.element.textContent;
_this.parseAndApply();
});
this.element.addEventListener('keyup', (e) => {
//_this.parseAndApply();
});
container.appendChild(this.element);
}
parseAndApply() {
this.parse();
this.apply();
}
parse() {
const content = this.element.textContent;
const regex = /\d/g;
const matches = [...content.matchAll(regex)];
let row = 0;
let col = 0;
const cells = []
const max = this.size * this.size;
matches.forEach(match => {
if (row * col == max) {
return;
}
if (col == 9) {
row++;
col = 0;
}
if (row == 9) {
row = 0;
col = 0;
}
const digit = match[0];
const cell = new Cell(this.app, row, col);
cell.addNumber(digit);
cell.initial = true;
cells.push(cell);
col++;
});
this.cells = cells;
}
apply() {
this.app.cells.forEach(cell => {
cell.initial = false;
cell.number = 0;
cell.numbers = [];
cell.addNumber(0);
});
this.cells.forEach(cell => {
const appCell = this.app.getCellByName(cell.name);
appCell.initial = cell.value != 0;
appCell.addNumber(cell.value);
});
this.toggle();
}
toggle() {
if (this.element.style.display == 'none') {
this.element.innerHTML = 'Paste here your puzzle';
}
this.element.style.display = this.element.style.display != 'none' ? 'none' : 'block';
}
}
class Sudoku {
cells = [];
game_size = 0;
cell_count = 0;
selectedCells = []
container = null
element = null
states = []
previousSelection = []
state_number = 1
parser = null;
status = null;
reset(){
this.cells.forEach(cell=>{
cell.values = []
cell.selected = false;
cell.addNumber(0);
})
}
loadSession() {
const session = this.getSession();
if (!session)
return null;
this.state_number = session.state_number;
this.states = session.states.map(state => {
return new State(state);
});
this.refreshState();
}
async getEmptyCell(){
return this.cells.filter(cell=>{
if(cell.value == 0)
return true
return false
})[0]
}
async solve() {
const cell = await this.getEmptyCell();
if(!cell)
return this.isValid();
return await cell.solve();
}
deleteSession(){
localStorage.removeItem('session');
}
getSession() {
const session = localStorage.getItem('session');
if (!session) {
return null
}
return JSON.parse(session);
}
saveSession() {
this.pushState();
const states = this.states.map(state => {
return state.toJson()
});
const session = {
state_number: this.state_number,
states: states
}
localStorage.setItem('session', JSON.stringify(session));
//console.info('session saved');
}
getBoxValues(cell_name) {
let values = this.cells.filter(cell => {
return cell.name != cell_name && cell.name[0] == cell_name[0]
}).map(cell => {
return cell.value
});
return values;
}
toggle() {
this.container.style.display = this.container.style.display != 'none' ? 'none' : 'block';
}
toggleParser() {
//this.parser.toggle();
this.deSelectAll();
this.parser.toggle()
}
constructor(container, game_size) {
const _this = this;
this.container = container
this.element = container
this.parser = new SudokuParser(this, this.container);
this.game_size = game_size;
this.cell_count = game_size * game_size;
for (let row = 0; row < this.game_size; row++) {
for (let col = 0; col < this.game_size; col++) {
this.cells.push(new Cell(this, row, col));
}
}
console.info("Loading session");
setTimeout(()=>{
if(_this.status == "applying state"){
_this.deleteSession();
window.location.reload();
}else{
console.info("Finished session validation");
}
},10000);
this.loadSession();
document.addEventListener('keypress', (e) => {
if (!isNaN(e.key) || e.key == 'd') {
let number = e.key == 'd' ? 0 : Number(e.key);
_this.addNumberToSelection(number);
//console.info({set:Number(e.key)});
}
if (e.key == 'p') {
_this.toggleParser();
}
if (e.key == 'u') {
_this.popState();
}
if (e.key == 'r') {
if (this.selection().length) {
this.pushState();
_this.deSelectAll();
} else {
let state = this.getLastState();
if (state) {
state.cells.filter(cell => cell.selected).forEach(cell => {
this.getCellByName(cell.name).select();
})
}
}
}
});
this.element.addEventListener('mousemove', (e) => {
//this.pushState();
})
document.addEventListener('dblclick', (e) => {
_this.previousSelection = _this.selection();
_this.cells.forEach(cell => {
cell.deSelect();
});
});
document.addEventListener('contextmenu', (e) => {
});
this.element.addEventListener('mouseexit', (e) => {
// Edge case while holding mouse button while dragging out. Should save state
_this.pushState();
});
this.element.addEventListener('mouseup', (e) => {
_this.pushState();
});
this.pushState()
}
isValid() {
return this.cells.filter(cell => !cell.isValid()).length == 0
}
createState() {
const state = new State(this.state_number)
let selectedCount = 0;
state.cells = this.cells.map(cell => {
if (cell.selected) {
selectedCount++;
}
return { name: cell.name, values: cell.values, value: cell.value, selected: cell.selected, state: cell.getState() }
});
state.selectedCount = selectedCount;
return state;
}
pushState() {
const state = this.createState();
const previousState = this.getLastState();
if (!previousState || !previousState.equals(state)) {
this.states.push(state);
this.state_number++;
this.saveSession();
//console.info({ pushState: state.getHash(), length: state.getHash().length, number: state.number });
}
}
refreshState() {
const state = this.getLastState();
if (!state)
return null;
this.applyState(state)
return state;
}
getLastState() {
return this.states.length ? this.states.at(this.states.length - 1) : null;
}
applyState(state) {
this.status = "applying state";
state.cells.forEach(stateCell => {
const cell = this.getCellByName(stateCell.name);
cell.selected = stateCell.selected;
cell.values = stateCell.values;
cell.value = stateCell.value;
cell.update();
})
this.status = "applied state"
}
popState() {
let state = this.states.pop();
if (!state)
return;
if (state.equals(this.createState())) {
return this.popState();
}
this.applyState(state);
this.saveSession();
//console.info({ popState: state.getHash(), length: state.getHash().length, number: state.number });
}
getCellByName(name) {
return this.cells.filter(cell => {
return cell.name == name
})[0]
}
deSelectAll() {
this.cells.forEach(cell => {
cell.deSelect();
});
}
selection() {
return this.cells.filter(cell => {
return cell.selected
});
}
onSelectionToggle() {
}
addNumberToSelection(number) {
const _this = this;
this.pushState();
this.selection().forEach((cell) => {
cell.addNumber(number)
cell.update();
})
_this.pushState();
if (this.isValid()) {
this.deSelectAll();
}
_this.pushState();
}
onCellDblClick(cell) {
this.previousSelection = this.selection();
this.popState()
let originalSelected = this.selctedCells
if (cell.selected) {
this.selectedCells.push(cell);
} else {
this.selectedCells.pop(cell);
}
if (!this.originalSelected != this.selectedCells) {
this.popState();
}
//console.info({selected:this.selectedCells});
}
render() {
this.cells.forEach(cell => {
cell.render(this.element);
});
}
}
const sudoku = new Sudoku(container, 9);
sudoku.render();
const app = sudoku;
/*
document.addEventListener('contextmenu',(e)=>{
e.preventDefault();
});*/
/*
for(let i = 0; i < game_size*game_size; i++){
const cell = document.importNode(cell_template.content,true);
app.appendChild(cell);
}*/
//document.body.appendChild(app);
</script>
</body>