342 lines
11 KiB
JavaScript
342 lines
11 KiB
JavaScript
const VALUES = "123456789"
|
|
const UNKNOWN = '.'
|
|
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 valueToInsert = ""
|
|
let history = []
|
|
let accessKeyModifiers = "AccessKey+"
|
|
|
|
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
|
|
}
|
|
|
|
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.onclick = onclick
|
|
box.previousValue = ""
|
|
box.previousPlaceholder = ""
|
|
}
|
|
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++
|
|
}
|
|
|
|
const savedGame = localStorage[location.href]
|
|
if (savedGame) {
|
|
boxes.forEach((box, i) => {
|
|
if (!box.disabled && savedGame[i] != UNKNOWN) {
|
|
box.value = savedGame[i]
|
|
box.previousValue = 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)
|
|
})
|
|
|
|
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) + "]"
|
|
}
|
|
|
|
refreshUI()
|
|
|
|
if ("serviceWorker" in navigator) {
|
|
navigator.serviceWorker.register(`service-worker.js?location=${location.href}`)
|
|
}
|
|
}
|
|
|
|
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 (pencilRadio.checked) {
|
|
this.value = this.placeholder
|
|
this.classList.add("pencil")
|
|
} else {
|
|
this.select()
|
|
}
|
|
this.style.caretColor = valueToInsert? "transparent": "auto"
|
|
}
|
|
|
|
function onclick() {
|
|
if (inkPenRadio.checked) {
|
|
this.value = valueToInsert
|
|
} else if (pencilRadio.checked) {
|
|
this.value += valueToInsert
|
|
} else {
|
|
this.value = ""
|
|
this.placeholder = ""
|
|
}
|
|
this.oninput()
|
|
}
|
|
|
|
function oninput() {
|
|
history.push({ box: this, value: this.previousValue, placeholder: this.previousPlaceholder })
|
|
undoButton.disabled = false
|
|
if (pencilRadio.checked) {
|
|
this.value = Array.from(new Set(this.value)).sort().join("")
|
|
this.previousValue = ""
|
|
this.previousPlaceholder = this.value
|
|
} else {
|
|
this.previousValue = this.value
|
|
this.previousPlaceholder = this.placeholder
|
|
refreshBox(this)
|
|
}
|
|
}
|
|
|
|
function refreshBox(box) {
|
|
localStorage[location.href] = boxes.map(box => box.value || ".").join("")
|
|
|
|
box.neighbourhood.concat([box]).forEach(neighbour => {
|
|
searchCandidatesOf(neighbour)
|
|
neighbour.setCustomValidity("")
|
|
neighbour.required = false
|
|
})
|
|
|
|
refreshUI()
|
|
|
|
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)
|
|
setTimeout(() => alert(`Bravo ! Vous avez résolu la grille.`), 500)
|
|
} else { // Errors on grid
|
|
box.form.reportValidity()
|
|
box.select()
|
|
}
|
|
}
|
|
|
|
function refreshUI() {
|
|
for (radio of insertRadioGroup.getElementsByTagName("input")) {
|
|
const label = radio.nextElementSibling
|
|
if (boxes.filter(box => box.value == "").some(box => box.candidates.has(radio.value))) {
|
|
radio.disabled = false
|
|
if (radio.previousTitle) {
|
|
label.title = radio.previousTitle
|
|
label.previousTitle = null
|
|
}
|
|
} else {
|
|
radio.disabled = true
|
|
label.previousTitle = label.title
|
|
label.title = `Tous les ${radio.value} sont posés`
|
|
if (valueToInsert == radio.value) valueToInsert = ""
|
|
}
|
|
}
|
|
|
|
highlight()
|
|
|
|
boxes.filter(box => !box.disabled).forEach(box => {
|
|
if (!box.value && box.candidates.size == 1) box.classList.add("one-candidate")
|
|
else box.classList.remove("one-candidate")
|
|
})
|
|
if (suggestionTimer) clearTimeout(suggestionTimer)
|
|
suggestionTimer = setTimeout(showSuggestion, SUGESTION_DELAY)
|
|
}
|
|
|
|
function highlight() {
|
|
if (valueToInsert) {
|
|
if (highlighterCheckbox.checked) {
|
|
boxes.forEach(box => {
|
|
if (box.value == valueToInsert) {
|
|
box.classList.add("same-value")
|
|
box.tabIndex = -1
|
|
}
|
|
else {
|
|
box.classList.remove("same-value")
|
|
if (box.candidates.has(valueToInsert) && !box.disabled) {
|
|
box.classList.remove("forbidden")
|
|
box.tabIndex = 0
|
|
} else {
|
|
box.classList.add("forbidden")
|
|
box.tabIndex = -1
|
|
}
|
|
}
|
|
})
|
|
} else {
|
|
boxes.forEach(box => {
|
|
box.classList.remove("same-value")
|
|
if (box.disabled) {
|
|
box.classList.add("forbidden")
|
|
} else {
|
|
box.classList.remove("forbidden")
|
|
}
|
|
box.tabIndex = 0
|
|
})
|
|
}
|
|
} else {
|
|
boxes.forEach(box => {
|
|
box.classList.remove("same-value", "forbidden")
|
|
box.tabIndex = 0
|
|
})
|
|
}
|
|
}
|
|
|
|
function onblur() {
|
|
if (this.classList.contains("pencil")) {
|
|
this.placeholder = this.value
|
|
this.value = ""
|
|
this.classList.remove("pencil")
|
|
}
|
|
}
|
|
|
|
function insert(radio) {
|
|
if (radio.value == valueToInsert) {
|
|
valueToInsert = ""
|
|
radio.checked = false
|
|
} else {
|
|
valueToInsert = radio.value
|
|
}
|
|
if (inkPenRadio.checked) customCursor = "url(img/ink-pen.svg) 2 22"
|
|
if (pencilRadio.checked) customCursor = "url(img/pencil.svg) 2 22"
|
|
if (eraserRadio.checked) customCursor = "url(img/eraser.svg) 2 22"
|
|
fallbackCursor = valueToInsert? "copy": "text"
|
|
grid.style.cursor = `${customCursor}, ${fallbackCursor}`
|
|
highlight()
|
|
}
|
|
|
|
function undo() {
|
|
if (history.length) {
|
|
const previousState = history.pop()
|
|
previousState.box.value = previousState.value
|
|
previousState.box.placeholder = previousState.placeholder
|
|
refreshBox(previousState.box)
|
|
if (history.length < 1) undoButton.disabled = true
|
|
}
|
|
}
|
|
|
|
function restart() {
|
|
if (confirm("Effacer toutes les cases ?")) {
|
|
boxes.filter(box => !box.disabled).forEach(box => {
|
|
box.value = ""
|
|
box.previousValue = ""
|
|
box.placeholder = ""
|
|
box.previousPlaceholder = ""
|
|
box.required = false
|
|
box.setCustomValidity("")
|
|
})
|
|
let history = []
|
|
undoButton.disabled = true
|
|
boxes.forEach(searchCandidatesOf)
|
|
refreshUI()
|
|
}
|
|
}
|
|
|
|
function showSuggestion() {
|
|
const easyBoxes = boxes.filter(box => box.value == "" && box.candidates.size == 1)
|
|
if (easyBoxes.length) {
|
|
shuffle(easyBoxes)[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) {
|
|
Array.from(box.candidates).sort().forEach(candidate => {
|
|
li = document.createElement("li")
|
|
li.innerText = candidate
|
|
li.onclick = function (event) {
|
|
contextMenu.style.display = "none"
|
|
box.onfocus()
|
|
box.value = event.target.innerText
|
|
box.oninput()
|
|
box.onblur()
|
|
}
|
|
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
|
|
}
|
|
|
|
document.onclick = function (event) {
|
|
contextMenu.style.display = "none"
|
|
}
|
|
|
|
document.onkeydown = function(event) {
|
|
if (event.key == "Escape") {
|
|
event.preventDefault()
|
|
contextMenu.style.display = "none"
|
|
}
|
|
} |