diff --git a/classes.php b/classes.php index be9ae35..a414893 100644 --- a/classes.php +++ b/classes.php @@ -11,7 +11,7 @@ } function easyFirst($box1, $box2) { - return count($box1->allowedValues) - count($box2->allowedValues); + return count($box1->candidates) - count($box2->candidates); } function array_unset_value($value, &$array) { @@ -32,16 +32,16 @@ $this->rowId = $rowId; $this->columnId = $columnId; $this->regionId = $regionId; - $this->allowedValues = $this->values; + $this->candidates = $this->values; $this->testValueWasAllowed = array(); $this->neighbourhood = array(); } - function searchAllowedValues() { - $this->allowedValues = $this->values; + function searchCandidates() { + $this->candidates = $this->values; forEach($this->neighbourhood as $neighbour) { if ($neighbour->value != UNKOWN) - array_unset_value($neighbour->value, $this->allowedValues); + array_unset_value($neighbour->value, $this->candidates); } } } @@ -83,10 +83,10 @@ $box = $this->rows[0][$columnId]; $box->value = $value; forEach($box->neighbourhood as $neighbour) - array_unset_value($box->value, $neighbour->allowedValues); + array_unset_value($box->value, $neighbour->candidates); } // Fill grid - $this->findSolutions(true)->current(); + $this->solutionsGenerator(true)->current(); // Remove clues while there is still a unique solution shuffle($this->boxes); @@ -105,53 +105,59 @@ $erasedValues[] = $testBox->value; $testBox->value = UNKOWN; forEach($testBox->neighbourhood as $neighbour) - $neighbour->searchAllowedValues(); + $neighbour->searchCandidates(); } if ($this->isValid()) { $nbClues -= count($testBoxes); } else { forEach($testBoxes as $i => $testBox) { $testBox->value = $erasedValues[$i]; - forEach($testBox->neighbourhood as $neighbour) array_unset_value($testBox->value, $neighbour->allowedValues); + forEach($testBox->neighbourhood as $neighbour) array_unset_value($testBox->value, $neighbour->candidates); } } } } - function isValid() { - $solutionsFinder = $this->findSolutions(false); - $solutionsFound = array(); - foreach($solutionsFinder as $solution) { - $solutionsFound[$solution] = true; - if (count($solutionsFound) > 1) { - $solutionsFinder->send(true); + function countSolutions($max=2) { + $solutions = $this->solutionsGenerator(false); + $solutionsWithoutDuplicates = array(); + $nbSolutions = 0; + foreach($solutions as $solution) { + $solutionsWithoutDuplicates[$solution] = true; + $nbSolutions = count($solutionsWithoutDuplicates); + if ($nbSolutions >= $max) { + $solutions->send(true); } } - return count($solutionsFound) == 1; + return $nbSolutions; } - function findSolutions($randomized=false) { + function isValid() { + return $this->countSolutions(2) == 1; + } + + function solutionsGenerator($randomized=false) { $emptyBoxes = array_filter($this->boxes, "isUnknown"); if (count($emptyBoxes)) { if ($randomized) shuffle($emptyBoxes); usort($emptyBoxes, "easyFirst"); $testBox = $emptyBoxes[0]; $nbTries = 0; - if ($randomized) shuffle($testBox->allowedValues); + if ($randomized) shuffle($testBox->candidates); $stop = null; - foreach($testBox->allowedValues as $testBox->value) { + foreach($testBox->candidates as $testBox->value) { foreach($testBox->neighbourhood as $neighbour) - $neighbour->testValueWasAllowed[] = array_unset_value($testBox->value, $neighbour->allowedValues); + $neighbour->testValueWasAllowed[] = array_unset_value($testBox->value, $neighbour->candidates); $correctGrid = true; foreach(array_filter($testBox->neighbourhood, "isUnknown") as $neighbour) { - if (count($neighbour->allowedValues) == 0) $correctGrid = false; + if (count($neighbour->candidates) == 0) $correctGrid = false; } if ($correctGrid) { - $solutionsFinder = $this->findSolutions($randomized); - foreach($solutionsFinder as $solution) { + $solutions = $this->solutionsGenerator($randomized); + foreach($solutions as $solution) { $stop = (yield $solution); if ($stop) { - $solutionsFinder->send($stop); + $solutions->send($stop); break; } } @@ -159,7 +165,7 @@ } forEach($testBox->neighbourhood as $neighbour) if (array_pop($neighbour->testValueWasAllowed)) - $neighbour->allowedValues[] = $testBox->value; + $neighbour->candidates[] = $testBox->value; if ($stop) break; } $testBox->value = UNKOWN; diff --git a/game.php b/game.php index 93f8c83..18802cc 100644 --- a/game.php +++ b/game.php @@ -37,7 +37,7 @@ } else { $disabled = " disabled"; } - echo " \n"; + echo " \n"; } ?> @@ -58,7 +58,7 @@ - +
@@ -78,7 +78,7 @@ Raccourcis clavier - Tab ⬅ ⬆ ⬇ ➡ + Tab Déplacement diff --git a/style.css b/style.css index 96f416a..e3ac053 100644 --- a/style.css +++ b/style.css @@ -84,7 +84,7 @@ section, div { border-right: 1px solid black; } -input { +input[type=number] { width: 1.6em; height: 1.6em; font-size: 1.5em; @@ -92,6 +92,13 @@ input { padding: 0; text-align: center; transition: background 0.5s; + -moz-appearance: textfield; +} + +input::-webkit-outer-spin-button, +input::-webkit-inner-spin-button { + -webkit-appearance: none; + margin: 0; } input:enabled { diff --git a/sudoku.js b/sudoku.js index d81e1f0..c759651 100644 --- a/sudoku.js +++ b/sudoku.js @@ -15,11 +15,9 @@ window.onload = function() { for (box of row.getElementsByTagName('input')) { let regionId = rowId - rowId%3 + Math.floor(columnId/3) if (!box.readOnly) { - box.onfocus = onfocus - box.oninput = oninput - box.oninvalid = oninvalid + box.onfocus = onfocus + box.oninput = oninput } - box.onkeydown = keyboardBrowse box.rowId = rowId box.columnId = columnId box.regionId = regionId @@ -36,9 +34,9 @@ window.onload = function() { box.neighbourhood.delete(box) box.neighbourhood = Array.from(box.neighbourhood) }) - boxes.forEach(searchAllowedValuesOf) + boxes.forEach(searchCandidatesOf) enableButtons() - boxes.forEach(showAllowedValuesOn) + boxes.forEach(showCandidatesOn) for(box of boxes) { if (!box.readOnly) { box.focus() @@ -48,48 +46,64 @@ window.onload = function() { suggestionTimer = setTimeout(showSuggestion, 30000) } -function searchAllowedValuesOf(box) { - box.allowedValues = new Set(VALUES) - box.neighbourhood.forEach(neighbour => box.allowedValues.delete(neighbour.value)) +function searchCandidatesOf(box) { + box.candidates = new Set(VALUES) + box.neighbourhood.forEach(neighbour => box.candidates.delete(neighbour.value)) } -function showAllowedValuesOn(box) { - box.required = box.allowedValues.size == 0 +function showCandidatesOn(box) { + box.required = box.candidates.size == 0 if (box.value.length) { box.title = "" - } else if (box.allowedValues.size) { - const allowedValuesArray = Array.from(box.allowedValues).sort() - box.title = allowedValuesArray.length ==1 ? allowedValuesArray[0] : allowedValuesArray.slice(0, allowedValuesArray.length-1).join(", ") + " ou " + allowedValuesArray[allowedValuesArray.length-1] + } else if (box.candidates.size) { + const candidatesArray = Array.from(box.candidates).sort() + box.title = candidatesArray.length ==1 ? candidatesArray[0] : candidatesArray.slice(0, candidatesArray.length-1).join(", ") + " ou " + candidatesArray[candidatesArray.length-1] } else { box.title = "Aucune valeur possible !" } } function onfocus() { - this.oldValue = this.value + this.previousValue = this.value this.select() } function oninput() { - history.push({input: this, value: this.oldValue}) + history.push({input: this, value: this.previousValue}) undoButton.disabled = false refresh(this) } -function refresh(input) { - input.style.color = colorPicker.value +function refresh(box) { + box.style.color = colorPicker.value - input.neighbourhood.concat([input]).forEach(box => { - box.setCustomValidity("") - searchAllowedValuesOf(box) - box.pattern = `[${Array.from(box.allowedValues).join("")}]?` + box.neighbourhood.concat([box]).forEach(neighbour => { + searchCandidatesOf(neighbour) + showCandidatesOn(neighbour) + neighbour.setCustomValidity("") }) - + + for (neighbour1 of box.neighbourhood) { + neighbour1.setCustomValidity("") + if (neighbour1.value.length) { + 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) { + neighbour1.setCustomValidity(`Il y a un autre ${neighbour1.value} dans cette ${area.name}.`) + neighbour2.setCustomValidity(`Il y a un autre ${neighbour1.value} dans cette ${area.name}.`) + } + } + } + enableButtons() highlightAndTab() - input.neighbourhood.concat([input]).forEach(neighbour => showAllowedValuesOn(neighbour)) - if (input.form.checkValidity()) { // Correct grid + + if (box.form.checkValidity()) { // Correct grid if (boxes.filter(box => box.value == "").length == 0) { alert(`Bravo ! Vous avez résolu la grille.`) } else { @@ -97,8 +111,8 @@ function refresh(input) { suggestionTimer = setTimeout(showSuggestion, 30000) } } else { // Errors on grid - input.select() - input.reportValidity() + box.select() + box.reportValidity() } } @@ -113,7 +127,7 @@ function undo() { function enableButtons() { for (button of buttons.getElementsByTagName("button")) { - if (boxes.filter(box => box.value == "").some(box => box.allowedValues.has(button.textContent))) { + if (boxes.filter(box => box.value == "").some(box => box.candidates.has(button.textContent))) { button.disabled = false } else { button.disabled = true @@ -122,52 +136,6 @@ function enableButtons() { } } -function oninvalid() { - if (this.value.length && !this.value.match(/[1-9]/)) - this.setCustomValidity("Entrez un chiffre entre 1 et 9.") - else if (sameValueIn(regions[this.regionId])) - this.setCustomValidity(`Il y a un autre ${this.value} dans cette région.`) - else if (sameValueIn(rows[this.rowId])) - this.setCustomValidity(`Il y a un autre ${this.value} dans cette ligne.`) - else if (sameValueIn(columns[this.columnId])) - this.setCustomValidity(`Il y a un autre ${this.value} dans cette colonne.`) - else if (this.allowedValues.size == 0) - this.setCustomValidity("La grille est incorrecte.") -} - -function sameValueIn(area) { - for (const box1 of area) { - for (const box2 of area) { - if (box1 != box2 && box1.value.length && box1.value == box2.value) { - return true - } - } - } - return false -} - - -function keyboardBrowse(event) { - switch(event.key) { - case "ArrowLeft": - event.preventDefault() - moveOn(rows[this.rowId], this.columnId, 8) - break - case "ArrowRight": - event.preventDefault() - moveOn(rows[this.rowId], this.columnId, 1) - break - case "ArrowUp": - event.preventDefault() - moveOn(columns[this.columnId], this.rowId, 8) - break - case "ArrowDown": - event.preventDefault() - moveOn(columns[this.columnId], this.rowId, 1) - break - } -} - function moveOn(area, position, direction) { if (area.filter(box => box.disabled).length < 9) { do { @@ -194,7 +162,7 @@ function highlightAndTab() { box.className = "same-value" box.tabIndex = -1 } - else if (box.allowedValues.has(highlightedValue)) { + else if (box.candidates.has(highlightedValue)) { box.className = "" box.tabIndex = 0 } else { @@ -221,10 +189,10 @@ function shuffle(iterable) { return array } -easyFirst = (box1, box2) => box1.allowedValues.size - box2.allowedValues.size +easyFirst = (box1, box2) => box1.candidates.size - box2.candidates.size function showSuggestion() { - const emptyBoxes = boxes.filter(box => box.value == "" && box.allowedValues.size == 1) + const emptyBoxes = boxes.filter(box => box.value == "" && box.candidates.size == 1) if (emptyBoxes.length) { shuffle(emptyBoxes).placeholder = "!" } else {