394 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			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"
 | 
						|
} |