input type='number'

This commit is contained in:
Adrien MALINGREY 2020-10-11 00:02:48 +02:00
parent b2e96a060a
commit e0fe365b57
4 changed files with 88 additions and 107 deletions

View File

@ -11,7 +11,7 @@
} }
function easyFirst($box1, $box2) { function easyFirst($box1, $box2) {
return count($box1->allowedValues) - count($box2->allowedValues); return count($box1->candidates) - count($box2->candidates);
} }
function array_unset_value($value, &$array) { function array_unset_value($value, &$array) {
@ -32,16 +32,16 @@
$this->rowId = $rowId; $this->rowId = $rowId;
$this->columnId = $columnId; $this->columnId = $columnId;
$this->regionId = $regionId; $this->regionId = $regionId;
$this->allowedValues = $this->values; $this->candidates = $this->values;
$this->testValueWasAllowed = array(); $this->testValueWasAllowed = array();
$this->neighbourhood = array(); $this->neighbourhood = array();
} }
function searchAllowedValues() { function searchCandidates() {
$this->allowedValues = $this->values; $this->candidates = $this->values;
forEach($this->neighbourhood as $neighbour) { forEach($this->neighbourhood as $neighbour) {
if ($neighbour->value != UNKOWN) 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 = $this->rows[0][$columnId];
$box->value = $value; $box->value = $value;
forEach($box->neighbourhood as $neighbour) forEach($box->neighbourhood as $neighbour)
array_unset_value($box->value, $neighbour->allowedValues); array_unset_value($box->value, $neighbour->candidates);
} }
// Fill grid // Fill grid
$this->findSolutions(true)->current(); $this->solutionsGenerator(true)->current();
// Remove clues while there is still a unique solution // Remove clues while there is still a unique solution
shuffle($this->boxes); shuffle($this->boxes);
@ -105,53 +105,59 @@
$erasedValues[] = $testBox->value; $erasedValues[] = $testBox->value;
$testBox->value = UNKOWN; $testBox->value = UNKOWN;
forEach($testBox->neighbourhood as $neighbour) forEach($testBox->neighbourhood as $neighbour)
$neighbour->searchAllowedValues(); $neighbour->searchCandidates();
} }
if ($this->isValid()) { if ($this->isValid()) {
$nbClues -= count($testBoxes); $nbClues -= count($testBoxes);
} else { } else {
forEach($testBoxes as $i => $testBox) { forEach($testBoxes as $i => $testBox) {
$testBox->value = $erasedValues[$i]; $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() { function countSolutions($max=2) {
$solutionsFinder = $this->findSolutions(false); $solutions = $this->solutionsGenerator(false);
$solutionsFound = array(); $solutionsWithoutDuplicates = array();
foreach($solutionsFinder as $solution) { $nbSolutions = 0;
$solutionsFound[$solution] = true; foreach($solutions as $solution) {
if (count($solutionsFound) > 1) { $solutionsWithoutDuplicates[$solution] = true;
$solutionsFinder->send(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"); $emptyBoxes = array_filter($this->boxes, "isUnknown");
if (count($emptyBoxes)) { if (count($emptyBoxes)) {
if ($randomized) shuffle($emptyBoxes); if ($randomized) shuffle($emptyBoxes);
usort($emptyBoxes, "easyFirst"); usort($emptyBoxes, "easyFirst");
$testBox = $emptyBoxes[0]; $testBox = $emptyBoxes[0];
$nbTries = 0; $nbTries = 0;
if ($randomized) shuffle($testBox->allowedValues); if ($randomized) shuffle($testBox->candidates);
$stop = null; $stop = null;
foreach($testBox->allowedValues as $testBox->value) { foreach($testBox->candidates as $testBox->value) {
foreach($testBox->neighbourhood as $neighbour) 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; $correctGrid = true;
foreach(array_filter($testBox->neighbourhood, "isUnknown") as $neighbour) { foreach(array_filter($testBox->neighbourhood, "isUnknown") as $neighbour) {
if (count($neighbour->allowedValues) == 0) $correctGrid = false; if (count($neighbour->candidates) == 0) $correctGrid = false;
} }
if ($correctGrid) { if ($correctGrid) {
$solutionsFinder = $this->findSolutions($randomized); $solutions = $this->solutionsGenerator($randomized);
foreach($solutionsFinder as $solution) { foreach($solutions as $solution) {
$stop = (yield $solution); $stop = (yield $solution);
if ($stop) { if ($stop) {
$solutionsFinder->send($stop); $solutions->send($stop);
break; break;
} }
} }
@ -159,7 +165,7 @@
} }
forEach($testBox->neighbourhood as $neighbour) forEach($testBox->neighbourhood as $neighbour)
if (array_pop($neighbour->testValueWasAllowed)) if (array_pop($neighbour->testValueWasAllowed))
$neighbour->allowedValues[] = $testBox->value; $neighbour->candidates[] = $testBox->value;
if ($stop) break; if ($stop) break;
} }
$testBox->value = UNKOWN; $testBox->value = UNKOWN;

View File

@ -37,7 +37,7 @@
} else { } else {
$disabled = " disabled"; $disabled = " disabled";
} }
echo " <td><input type='text' inputmode='numeric' minlength=0 maxlength=1 value='$value'$disabled/></td>\n"; echo " <td><input type='number' min='1' max='9' step='1' value='$value'$disabled/></td>\n";
} }
?> ?>
</tr> </tr>
@ -58,7 +58,7 @@
<button type='reset'>Tout effacer</button> <button type='reset'>Tout effacer</button>
<button id='undoButton' type='button' onclick='undo()' disabled accesskey='z'>Annuler</button> <button id='undoButton' type='button' onclick='undo()' disabled accesskey='z'>Annuler</button>
<label for='colorPicker'>🎨</label> <label for='colorPicker'>🎨</label>
<input id='colorPicker' type='color' value='#00008b'/> <input id='colorPicker' type='color' title='Changer de stylo' value='#00008b'/>
</div> </div>
</form> </form>
<section> <section>
@ -78,7 +78,7 @@
<caption>Raccourcis clavier</caption> <caption>Raccourcis clavier</caption>
<tbody> <tbody>
<tr> <tr>
<td><kbd>Tab</kbd> <kbd></kbd> <kbd></kbd> <kbd></kbd> <kbd></kbd></td> <td><kbd>Tab</kbd></td>
<td>Déplacement</td> <td>Déplacement</td>
</tr> </tr>
<tr> <tr>

View File

@ -84,7 +84,7 @@ section, div {
border-right: 1px solid black; border-right: 1px solid black;
} }
input { input[type=number] {
width: 1.6em; width: 1.6em;
height: 1.6em; height: 1.6em;
font-size: 1.5em; font-size: 1.5em;
@ -92,6 +92,13 @@ input {
padding: 0; padding: 0;
text-align: center; text-align: center;
transition: background 0.5s; transition: background 0.5s;
-moz-appearance: textfield;
}
input::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none;
margin: 0;
} }
input:enabled { input:enabled {

122
sudoku.js
View File

@ -15,11 +15,9 @@ window.onload = function() {
for (box of row.getElementsByTagName('input')) { for (box of row.getElementsByTagName('input')) {
let regionId = rowId - rowId%3 + Math.floor(columnId/3) let regionId = rowId - rowId%3 + Math.floor(columnId/3)
if (!box.readOnly) { if (!box.readOnly) {
box.onfocus = onfocus box.onfocus = onfocus
box.oninput = oninput box.oninput = oninput
box.oninvalid = oninvalid
} }
box.onkeydown = keyboardBrowse
box.rowId = rowId box.rowId = rowId
box.columnId = columnId box.columnId = columnId
box.regionId = regionId box.regionId = regionId
@ -36,9 +34,9 @@ window.onload = function() {
box.neighbourhood.delete(box) box.neighbourhood.delete(box)
box.neighbourhood = Array.from(box.neighbourhood) box.neighbourhood = Array.from(box.neighbourhood)
}) })
boxes.forEach(searchAllowedValuesOf) boxes.forEach(searchCandidatesOf)
enableButtons() enableButtons()
boxes.forEach(showAllowedValuesOn) boxes.forEach(showCandidatesOn)
for(box of boxes) { for(box of boxes) {
if (!box.readOnly) { if (!box.readOnly) {
box.focus() box.focus()
@ -48,48 +46,64 @@ window.onload = function() {
suggestionTimer = setTimeout(showSuggestion, 30000) suggestionTimer = setTimeout(showSuggestion, 30000)
} }
function searchAllowedValuesOf(box) { function searchCandidatesOf(box) {
box.allowedValues = new Set(VALUES) box.candidates = new Set(VALUES)
box.neighbourhood.forEach(neighbour => box.allowedValues.delete(neighbour.value)) box.neighbourhood.forEach(neighbour => box.candidates.delete(neighbour.value))
} }
function showAllowedValuesOn(box) { function showCandidatesOn(box) {
box.required = box.allowedValues.size == 0 box.required = box.candidates.size == 0
if (box.value.length) { if (box.value.length) {
box.title = "" box.title = ""
} else if (box.allowedValues.size) { } else if (box.candidates.size) {
const allowedValuesArray = Array.from(box.allowedValues).sort() const candidatesArray = Array.from(box.candidates).sort()
box.title = allowedValuesArray.length ==1 ? allowedValuesArray[0] : allowedValuesArray.slice(0, allowedValuesArray.length-1).join(", ") + " ou " + allowedValuesArray[allowedValuesArray.length-1] box.title = candidatesArray.length ==1 ? candidatesArray[0] : candidatesArray.slice(0, candidatesArray.length-1).join(", ") + " ou " + candidatesArray[candidatesArray.length-1]
} else { } else {
box.title = "Aucune valeur possible !" box.title = "Aucune valeur possible !"
} }
} }
function onfocus() { function onfocus() {
this.oldValue = this.value this.previousValue = this.value
this.select() this.select()
} }
function oninput() { function oninput() {
history.push({input: this, value: this.oldValue}) history.push({input: this, value: this.previousValue})
undoButton.disabled = false undoButton.disabled = false
refresh(this) refresh(this)
} }
function refresh(input) { function refresh(box) {
input.style.color = colorPicker.value box.style.color = colorPicker.value
input.neighbourhood.concat([input]).forEach(box => { box.neighbourhood.concat([box]).forEach(neighbour => {
box.setCustomValidity("") searchCandidatesOf(neighbour)
searchAllowedValuesOf(box) showCandidatesOn(neighbour)
box.pattern = `[${Array.from(box.allowedValues).join("")}]?` 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() enableButtons()
highlightAndTab() 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) { if (boxes.filter(box => box.value == "").length == 0) {
alert(`Bravo ! Vous avez résolu la grille.`) alert(`Bravo ! Vous avez résolu la grille.`)
} else { } else {
@ -97,8 +111,8 @@ function refresh(input) {
suggestionTimer = setTimeout(showSuggestion, 30000) suggestionTimer = setTimeout(showSuggestion, 30000)
} }
} else { // Errors on grid } else { // Errors on grid
input.select() box.select()
input.reportValidity() box.reportValidity()
} }
} }
@ -113,7 +127,7 @@ function undo() {
function enableButtons() { function enableButtons() {
for (button of buttons.getElementsByTagName("button")) { 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 button.disabled = false
} else { } else {
button.disabled = true 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) { function moveOn(area, position, direction) {
if (area.filter(box => box.disabled).length < 9) { if (area.filter(box => box.disabled).length < 9) {
do { do {
@ -194,7 +162,7 @@ function highlightAndTab() {
box.className = "same-value" box.className = "same-value"
box.tabIndex = -1 box.tabIndex = -1
} }
else if (box.allowedValues.has(highlightedValue)) { else if (box.candidates.has(highlightedValue)) {
box.className = "" box.className = ""
box.tabIndex = 0 box.tabIndex = 0
} else { } else {
@ -221,10 +189,10 @@ function shuffle(iterable) {
return array return array
} }
easyFirst = (box1, box2) => box1.allowedValues.size - box2.allowedValues.size easyFirst = (box1, box2) => box1.candidates.size - box2.candidates.size
function showSuggestion() { 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) { if (emptyBoxes.length) {
shuffle(emptyBoxes).placeholder = "!" shuffle(emptyBoxes).placeholder = "!"
} else { } else {