Sudoku/sudoku.js
2024-05-12 11:17:03 +02:00

394 lines
12 KiB
JavaScript
Executable File

const VALUES = "123456789"
const UNKNOWN = '.'
let boxes = []
let rows = Array.from(Array(9), x => [])
let columns = Array.from(Array(9), x => [])
let regions = Array.from(Array(9), x => [])
let areaNames = {
ligne: rows,
colonne: columns,
région: regions,
}
let valueToInsert = ""
let easyBoxes = []
let insertRadios = []
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.onmouseenter = onmouseenter
box.onmouseleave = onmouseleave
}
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++
}
if (localStorage["tool"] == "sight") sightCheckbox.checked = true
else if (localStorage["tool"] == "highlighter") highlighterCheckbox.checked = true
colorPickerInput.value = window.getComputedStyle(grid).getPropertyValue("--bs-body-color")
boxes.forEach(box => {
box.neighbourhood = new Set(rows[box.rowId].concat(columns[box.columnId]).concat(regions[box.regionId]))
box.andNeighbourhood = Array.from(box.neighbourhood)
box.neighbourhood.delete(box)
box.neighbourhood = Array.from(box.neighbourhood)
})
insertRadios = Array.from(insertRadioGroup.getElementsByTagName("input")).slice(1)
for (label of document.getElementsByTagName("label")) {
label.control.label = label
}
let accessKeyModifiers = (/Win/.test(navigator.userAgent) || /Linux/.test(navigator.userAgent)) ? "Alt+Maj+"
: (/Mac/.test(navigator.userAgent)) ? "⌃⌥"
: "AccessKey+"
for (node of document.querySelectorAll("*[accesskey]")) {
shortcut = ` [${node.accessKeyLabel||(accessKeyModifiers+node.accessKey)}]`
if (node.title) node.title += shortcut
else if (node.label) node.label.title += shortcut
}
loadGame(history.state)
}
window.onpopstate = (event) => loadGame(event.state)
function loadGame(state) {
if (state) {
boxes.forEach((box, i) => {
if (!box.disabled) {
box.value = state.boxesValues[i]
box.placeholder = state.boxesPlaceholders[i]
}
})
restartLink.classList.remove("disabled")
undoButton.disabled = false
fixGridLink.href = "?" + state.boxesValues.map(value => value || UNKNOWN).join("")
} else {
boxes.filter(box => !box.disabled).forEach(box => {
box.value = ""
box.placeholder = ""
})
restartLink.classList.add("disabled")
undoButton.disabled = true
fixGridLink.href = ""
}
checkBoxes()
enableRadio()
highlight()
}
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 = "Une seule possibilité [Clic-droit]"
break
default:
box.title = box.candidates.size + " possibilités [Clic-droit]"
}
}
}
function onfocus() {
if (pencilRadio.checked) {
this.type = "text"
this.value = this.placeholder
this.placeholder = ""
this.classList.add("pencil")
} else {
this.select()
}
if (penColor && inkPenRadio.checked) {
this.style.setProperty("color", penColor)
}
this.style.caretColor = valueToInsert ? "transparent" : "auto"
}
function onclick() {
if (inkPenRadio.checked) {
if (valueToInsert) {
this.value = valueToInsert
this.oninput()
} else {
this.select()
}
} else if (pencilRadio.checked) {
if (valueToInsert) {
this.value = Array.from(new Set(this.value + valueToInsert)).join("")
this.oninput()
}
} else if (eraserRadio.checked) {
this.value = ""
this.placeholder = ""
this.oninput()
}
}
function oninput() {
if (inkPenRadio.checked) {
checkBoxes()
enableRadio()
highlight()
fixGridLink.href = "?" + boxes.map(box => box.value || UNKNOWN).join("")
}
saveGame()
}
function checkBoxes() {
boxes.forEach(box => {
box.setCustomValidity("")
box.classList.remove("is-invalid")
box.parentElement.classList.remove("table-danger")
searchCandidatesOf(box)
if (box.candidates.size == 0) {
box.setCustomValidity("Aucun chiffre possible !")
box.classList.add("is-invalid")
}
})
for (let [areaName, areas] of Object.entries(areaNames))
for (area of areas)
area.filter(box => box.value).sort((box, neighbour) => {
if(box.value == neighbour.value) {
area.forEach(neighbour => neighbour.parentElement.classList.add("table-danger"))
for (neighbour of [box, neighbour]) {
neighbour.setCustomValidity(`Il y a un autre ${box.value} dans cette ${areaName}.`)
neighbour.classList.add("is-invalid")
}
}
return box.value - neighbour.value
})
if (sudokuForm.checkValidity()) { // Correct grid
if (boxes.filter(box => box.value == "").length == 0) {
grid.classList.add("table-success")
setTimeout(() => {
if (confirm(`Bravo ! Vous avez résolu la grille. En voulez-vous une autre ?`))
location = "."
}, 400)
} else {
grid.classList.remove("table-success")
}
} else { // Errors on grid
grid.classList.remove("table-success")
sudokuForm.reportValidity()
}
}
function enableRadio() {
for (radio of insertRadios) {
if (boxes.filter(box => box.value == "").some(box => box.candidates.has(radio.value))) {
radio.disabled = false
radio.label.title = `Insérer un ${radio.value} [${radio.accessKeyLabel||(accessKeyModifiers+radio.accessKey)}]`
} else {
radio.disabled = true
radio.label.title = `Tous les ${radio.value} sont posés.`
if (valueToInsert == radio.value) {
insertRadio0.checked = true
valueToInsert = ""
grid.style.cursor = "text"
}
}
}
}
function highlight() {
hintButton.disabled = true
easyBoxes = []
boxes.forEach(box => {
if (valueToInsert && box.value == valueToInsert) {
box.parentElement.classList.add("table-primary")
box.tabIndex = -1
} else {
box.parentElement.classList.remove("table-primary")
box.tabIndex = 0
}
if (valueToInsert && highlighterCheckbox.checked && !box.candidates.has(valueToInsert)) {
box.parentElement.classList.add("table-active")
box.tabIndex = -1
} else {
box.parentElement.classList.remove("table-active")
box.tabIndex = 0
}
if (!box.value && box.candidates.size == 1) {
hintButton.disabled = false
easyBoxes.push(box)
}
})
highlighterCheckbox.label.title = "Surligner les lignes, colonnes et régions contenant déjà " + (valueToInsert ? "un " + valueToInsert : "le chiffre sélectionné")
}
function onblur() {
if (this.classList.contains("pencil")) {
this.placeholder = this.value
this.value = ""
this.type = "number"
this.classList.remove("pencil")
}
}
function saveGame() {
history.pushState({
boxesValues: boxes.map(box => box.value),
boxesPlaceholders: boxes.map(box => box.placeholder)
}, "")
restartLink.classList.remove("disabled")
undoButton.disabled = false
}
function onmouseenter(event) {
if (sightCheckbox.checked){
box = event.target
box.andNeighbourhood.forEach(neighbour => {
neighbour.parentElement.classList.add("table-active")
})
box.neighbourhood.forEach(neighbour => {
if (valueToInsert && neighbour.value == valueToInsert) {
for (neighbour of [box, neighbour]) {
neighbour.parentElement.classList.add("table-danger", "not-allowed")
}
}
})
}
}
function onmouseleave(event) {
if (sightCheckbox.checked){
box = event.target
box.andNeighbourhood.forEach(neighbour => {
neighbour.parentElement.classList.remove("table-active", "table-danger", "not-allowed")
})
}
}
function insert(radio) {
if (radio.value && valueToInsert == radio.value) {
radio.blur()
insertRadio0.checked = true
insert(0)
} else {
valueToInsert = radio.value
grid.style.cursor = valueToInsert ? "pointer" : "text"
highlight()
}
}
let penColor
function changeColor() {
penColor = colorPickerInput.value
colorPickerLabel.style.color = colorPickerInput.value
}
function restart() {
if (confirm("Effacer toutes les cases ?")) {
restartButton.disabled = true
location.hash = ""
}
}
function showHint() {
if (easyBoxes.length) {
shuffle(easyBoxes)
let box = easyBoxes.pop()
box.placeholder = "💡"
box.focus()
return box
}
hintButton.disabled = true
}
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.classList = "list-group-item list-group-item-action"
li.onclick = function(e) {
contextMenu.style.display = "none"
valueToInsert = e.target.innerText
grid.style.cursor = "pointer"
document.getElementById("insertRadio" + valueToInsert).checked = true
box.onclick()
}
li.oncontextmenu = function(e) {
e.preventDefault()
li.onclick(e)
}
contextMenu.appendChild(li)
})
} else {
li = document.createElement("li")
li.innerText = "Aucune possibilité !"
li.classList = "list-group-item list-group-item-action disabled"
contextMenu.appendChild(li)
}
contextMenu.style.left = `${event.pageX}px`
contextMenu.style.top = `${event.pageY}px`
contextMenu.style.display = "block"
document.onclick = function(event) {
contextMenu.style.display = "none"
document.onclick = null
}
return false
}
document.onkeydown = function(event) {
if (event.key == "Escape" && contextMenu.style.display == "block") {
event.preventDefault()
contextMenu.style.display = "none"
}
}
window.onbeforeunload = function(event) {
saveGame()
if (sightCheckbox.checked) localStorage["tool"] = "sight"
else if (highlighterCheckbox.checked) localStorage["tool"] = "highlighter"
}