first commit
This commit is contained in:
+711
@@ -0,0 +1,711 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user