Sudoku/app.js
2020-11-02 21:12:43 +01:00

320 lines
9.6 KiB
JavaScript

const VALUES = "123456789"
const SUGESTION_DELAY = 60000 //ms
let boxes = []
let rows = Array.from(Array(9), x => [])
let columns = Array.from(Array(9), x => [])
let regions = Array.from(Array(9), x => [])
let suggestionTimer= null
let highlightedValue = ""
let history = []
let accessKeyModifiers = "AccessKey+"
let penStyle = "ink-pen"
window.onload = function() {
let rowId = 0
for (let row of grid.getElementsByTagName('tr')) {
let columnId = 0
for (let box of row.getElementsByTagName('input')) {
let regionId = rowId - rowId%3 + Math.floor(columnId/3)
if (!box.disabled) {
box.onfocus = onfocus
box.oninput = oninput
box.onblur = onblur
}
box.oncontextmenu = oncontextmenu
box.rowId = rowId
box.columnId = columnId
box.regionId = regionId
boxes.push(box)
rows[rowId].push(box)
columns[columnId].push(box)
regions[regionId].push(box)
columnId++
}
rowId++
}
let savedGame = localStorage[location.href]
if (savedGame) {
boxes.forEach((box, i) => {
if (!box.disabled && savedGame[i] != '.') box.value = savedGame[i]
})
}
boxes.forEach(box => {
box.neighbourhood = new Set(rows[box.rowId].concat(columns[box.columnId]).concat(regions[box.regionId]))
box.neighbourhood.delete(box)
box.neighbourhood = Array.from(box.neighbourhood)
searchCandidatesOf(box)
})
enableButtons()
highlightAndTab()
if (/Win/.test(navigator.platform) || /Linux/.test(navigator.platform)) accessKeyModifiers = "Alt+Maj+"
else if (/Mac/.test(navigator.platform)) accessKeyModifiers = "⌃⌥"
for(node of document.querySelectorAll("*[accesskey]")) {
node.title += " [" + (node.accessKeyLabel || accessKeyModifiers + node.accessKey) + "]"
}
document.onclick = function (event) {
contextMenu.style.display = "none"
}
suggestionTimer = setTimeout(showSuggestion, 30000)
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("service-worker.js")
}
}
function searchCandidatesOf(box) {
box.candidates = new Set(VALUES)
box.neighbourhood.forEach(neighbour => box.candidates.delete(neighbour.value))
if (!box.disabled) {
switch (box.candidates.size) {
case 0:
box.title = "Aucune possibilité !"
break
case 1:
box.title = "1 possibilité [Clic-droit]"
break
default:
box.title = box.candidates.size + " possibilités [Clic-droit]"
}
}
}
function onfocus() {
if (penStyle == "pencil" && this.value == "") {
this.value = this.placeholder
this.placeholder = ""
this.classList.add("pencil")
} else {
this.select()
}
}
function oninput() {
history.push({box: this, value: this.previousValue || "", placeholder: this.previousPlaceholder || ""})
undoButton.disabled = false
if (penStyle != "pencil") {
refresh(this)
}
}
function undo() {
if (history.length) {
previousState = history.pop()
previousState.box.value = previousState.value
previousState.box.placeholder = previousState.placeholder
refresh(previousState.box)
if (history.length < 1) undoButton.disabled = true
}
}
function refresh(box) {
localStorage[location.href] = boxes.map(box => box.value || ".").join("")
box.neighbourhood.concat([box]).forEach(neighbour => {
searchCandidatesOf(neighbour)
neighbour.setCustomValidity("")
neighbour.required = false
})
enableButtons()
highlightAndTab()
for (neighbour1 of box.neighbourhood) {
if (neighbour1.value.length == 1) {
for (area of [
{name: "région", neighbours: regions[neighbour1.regionId]},
{name: "ligne", neighbours: rows[neighbour1.rowId]},
{name: "colonne", neighbours: columns[neighbour1.columnId]},
])
for (neighbour2 of area.neighbours)
if (neighbour2 != neighbour1 && neighbour2.value == neighbour1.value) {
for (neighbour of [neighbour1, neighbour2]) {
neighbour.setCustomValidity(`Il y a un autre ${neighbour.value} dans cette ${area.name}.`)
}
}
} else {
if (neighbour1.candidates.size == 0) {
neighbour1.setCustomValidity("Aucun chiffre possible !")
neighbour1.required = true
}
}
}
if (box.form.checkValidity()) { // Correct grid
if (boxes.filter(box => box.value == "").length == 0) {
alert(`Bravo ! Vous avez résolu la grille.`)
} else {
if (suggestionTimer) clearTimeout(suggestionTimer)
suggestionTimer = setTimeout(showSuggestion, SUGESTION_DELAY)
}
} else { // Errors on grid
box.form.reportValidity()
box.select()
}
}
function onblur() {
if (this.classList.contains("pencil")) {
this.placeholder = this.value
this.value = ""
this.classList.remove("pencil")
}
this.previousValue = this.value
this.previousPlaceholder = this.placeholder
}
function enableButtons() {
for (button of buttons.getElementsByTagName("button")) {
if (boxes.filter(box => box.value == "").some(box => box.candidates.has(button.textContent))) {
button.disabled = false
} else {
button.disabled = true
if (highlightedValue == button.textContent) highlightedValue = ""
}
}
}
function highlight(value) {
if (value == highlightedValue) {
highlightedValue = ""
} else {
highlightedValue = value
}
for (button of buttons.getElementsByTagName("button")) {
if (button.textContent == highlightedValue) button.classList.add("pressed")
else button.classList.remove("pressed")
}
highlightAndTab()
boxes.filter(box => box.value == "" && box.tabIndex == 0)[0].focus()
}
function highlightAndTab() {
if (highlightedValue) {
boxes.forEach(box => {
if (box.value == highlightedValue) {
box.classList.add("same-value")
box.tabIndex = -1
}
else {
box.classList.remove("same-value")
if (box.candidates.has(highlightedValue)) {
box.classList.remove("forbidden-value")
box.tabIndex = 0
} else {
box.classList.add("forbidden-value")
box.tabIndex = -1
}
}
})
} else {
boxes.forEach(box => {
box.classList.remove("same-value", "forbidden-value")
box.tabIndex = 0
})
}
}
function shuffle(iterable) {
array = Array.from(iterable)
if (array.length > 1) {
let i, j, tmp
for (i = array.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i+1))
tmp = array[i]
array[i] = array[j]
array[j] = tmp
}
}
return array
}
easyFirst = (box1, box2) => box1.candidates.size - box2.candidates.size
function showSuggestion() {
const emptyBoxes = boxes.filter(box => box.value == "" && box.candidates.size == 1)
if (emptyBoxes.length) {
shuffle(emptyBoxes)[0].placeholder = "💡"
} else {
clearTimeout(suggestionTimer)
suggestionTimer = null
}
}
function oncontextmenu(event) {
event.preventDefault()
while (contextMenu.firstChild) contextMenu.firstChild.remove()
const box = event.target
if (box.candidates.size) {
candidatesArray = Array.from(box.candidates).sort().forEach(candidate => {
li = document.createElement("li")
li.innerText = candidate
li.onclick = function (event) {
contextMenu.style.display = "none"
box.value = event.target.innerText
oninput.apply(box)
}
contextMenu.appendChild(li)
})
} else {
li = document.createElement("li")
li.innerText = "Aucune possibilité !"
li.classList.add("error")
contextMenu.appendChild(li)
}
contextMenu.style.left = `${event.pageX}px`
contextMenu.style.top = `${event.pageY}px`
contextMenu.style.display = "block"
return false
}
function useInkPen() {
inkPenButton.classList.add("pressed")
pencilButton.classList.remove("pressed")
penStyle = "ink-pen"
}
function usePencil() {
pencilButton.classList.add("pressed")
inkPenButton.classList.remove("pressed")
penStyle = "pencil"
}
function erase(someBoxes) {
for (box of someBoxes) {
box.value = ""
box.placeholder = ""
searchCandidatesOf(box)
refresh(box)
}
enableButtons()
highlightAndTab()
}
function erasePencil() {
if (confirm("Effacer les chiffres écrits au crayon ?")) {
boxes.filter(box => !box.disabled).forEach(box => {
box.placeholder = ""
})
}
}
function eraseAll() {
if (confirm("Effacer tous les chiffres écrits au crayon et au stylo ?")) {
boxes.filter(box => !box.disabled).forEach(box => {
box.value = ""
box.placeholder = ""
box.setCustomValidity("")
box.required = false
})
boxes.forEach(searchCandidatesOf)
enableButtons()
highlightAndTab()
}
}