V2
This commit is contained in:
parent
d752ed6b54
commit
a0af1272cf
19
LICENSE
19
LICENSE
@ -1,19 +0,0 @@
|
||||
MIT License Copyright (c) <year> <copyright holders>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is furnished
|
||||
to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
|
||||
OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
|
||||
OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
101
game.php
Normal file
101
game.php
Normal file
@ -0,0 +1,101 @@
|
||||
<?php
|
||||
$gridStr = basename(strip_tags($_SERVER["REQUEST_URI"]));
|
||||
// URL contains grid
|
||||
if (preg_match("#^[1-9?]{81}$#", $gridStr)) {
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Sudoku</title>
|
||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||
<script type="text/javascript" src="sudoku.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Sudoku</h1>
|
||||
</header>
|
||||
<form id="sudokuForm">
|
||||
<div>
|
||||
<table id="grid">
|
||||
<?php
|
||||
for ($row = 0; $row < 9; $row++) {
|
||||
?>
|
||||
<tr>
|
||||
<?php
|
||||
for ($column = 0; $column < 9; $column++) {
|
||||
switch($row%3) {
|
||||
case 0: $classRegionRow = "regionTop"; break;
|
||||
case 1: $classRegionRow = "regionMiddle"; break;
|
||||
case 2: $classRegionRow = "regionBottom"; break;
|
||||
}
|
||||
switch($column%3) {
|
||||
case 0: $classRegionColumn = "regionLeft"; break;
|
||||
case 1: $classRegionColumn = "regionCenter"; break;
|
||||
case 2: $classRegionColumn = "regionRight"; break;
|
||||
}
|
||||
$value = $gridStr[9*$row+$column];
|
||||
if ($value == "?") {
|
||||
$value = "";
|
||||
$readonly = "";
|
||||
} else {
|
||||
$readonly = "readonly='true'";
|
||||
}
|
||||
echo " <td class='$classRegionRow $classRegionColumn'><input type='text' inputmode='numeric' minlength=0 maxlength=1 value='$value' $readonly /></td>";
|
||||
}
|
||||
?>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</table>
|
||||
</div>
|
||||
<div id="buttons">
|
||||
<?php
|
||||
for($value=1; $value<=9; $value++) {
|
||||
echo "<button type='button' onclick='showValue(this)'>$value</button>";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div>
|
||||
<button type="reset">Tout effacer</button>
|
||||
</div>
|
||||
</form>
|
||||
<div id=help>
|
||||
Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.
|
||||
</div>
|
||||
<div id=links>
|
||||
<a href="">Lien vers cette grille</a><br/>
|
||||
<a href=".">Nouvelle grille</a>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
} else {
|
||||
header("HTTP/1.0 400 Bad Request", true, 400);
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Grille incorrecte</title>
|
||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Grille incorrecte</h1>
|
||||
</header>
|
||||
L'adresse URL doit être de la forme : <?=$_SERVER["REQUEST_SCHEME"]?>://<?=$_SERVER["HTTP_HOST"] . dirname($_SERVER["DOCUMENT_URI"])?>/<em>grille</em>,<br/>
|
||||
<em>grille</em> étant une suite de 81 caractères représentant la grille de gauche à droite puis de haut en bas, soit :
|
||||
<ul>
|
||||
<li>un chiffre entre 1 et 9 pour les cases connues</li>
|
||||
<li>? pour les case vides</li>
|
||||
</ul>
|
||||
<a href=".">Cliquez ici pour générer une nouvelle grille</a>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
?>
|
Binary file not shown.
Before Width: | Height: | Size: 6.3 KiB |
49
index.html
49
index.html
@ -1,49 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>Sudoku</title>
|
||||
<link rel="stylesheet" type="text/css" href="style.css" />
|
||||
<script type="text/javascript" src="sudoku.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Sudoku</h1>
|
||||
</header>
|
||||
<form id="sudokuForm">
|
||||
<div>
|
||||
<table id="gridTable" class="grid"></table>
|
||||
<script>createGrid()</script>
|
||||
</div>
|
||||
</form>
|
||||
<div>
|
||||
Surligner les cases pouvant contenir
|
||||
</div>
|
||||
<div id="highlightRadiosDiv">
|
||||
<input type="radio" id="highlightRadio1" name="highlightRadio" onclick="highlight('1')"><label id="highlightLabel1" for="highlightRadio1">1</label>
|
||||
<input type="radio" id="highlightRadio2" name="highlightRadio" onclick="highlight('2')"><label id="highlightLabel2" for="highlightRadio2">2</label>
|
||||
<input type="radio" id="highlightRadio3" name="highlightRadio" onclick="highlight('3')"><label id="highlightLabel3" for="highlightRadio3">3</label>
|
||||
<input type="radio" id="highlightRadio4" name="highlightRadio" onclick="highlight('4')"><label id="highlightLabel4" for="highlightRadio4">4</label>
|
||||
<input type="radio" id="highlightRadio5" name="highlightRadio" onclick="highlight('5')"><label id="highlightLabel5" for="highlightRadio5">5</label>
|
||||
<input type="radio" id="highlightRadio6" name="highlightRadio" onclick="highlight('6')"><label id="highlightLabel6" for="highlightRadio6">6</label>
|
||||
<input type="radio" id="highlightRadio7" name="highlightRadio" onclick="highlight('7')"><label id="highlightLabel7" for="highlightRadio7">7</label>
|
||||
<input type="radio" id="highlightRadio8" name="highlightRadio" onclick="highlight('8')"><label id="highlightLabel8" for="highlightRadio8">8</label>
|
||||
<input type="radio" id="highlightRadio9" name="highlightRadio" onclick="highlight('9')"><label id="highlightLabel9" for="highlightRadio9">9</label>
|
||||
<input type="radio" id="highlightRadioNone" name="highlightRadio" onclick="highlight(null)"><label for="highlightRadioNone">Effacer</label>
|
||||
</div>
|
||||
<div>
|
||||
<button id="clearButton" type="button" onclick="erase()">Effacer tout</button>
|
||||
<button id="solveButton" type="button" onclick="solve()">Montrer la solution</button>
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" onclick="generateGrid()">Nouvelle partie</button>
|
||||
<button id="customGridButton" type="button" onclick="customGrid()">Grille personnalisée</button>
|
||||
</div>
|
||||
Pour partager cette grille, copiez le lien suivant :<br/>
|
||||
<div id="shareDiv">
|
||||
<a id="shareA" href="">jeux.malingrey.fr/sudoku/<br/></a>
|
||||
</div>
|
||||
<script>loadGrid()</script>
|
||||
</body>
|
||||
</html>
|
163
index.php
Normal file
163
index.php
Normal file
@ -0,0 +1,163 @@
|
||||
<?php
|
||||
function isUnknown($box) {
|
||||
return $box->value == "?";
|
||||
}
|
||||
|
||||
function easyFirst($box1, $box2) {
|
||||
return count($box1->allowedValues) - count($box2->allowedValues);
|
||||
}
|
||||
|
||||
function array_unset_value($value, &$array) {
|
||||
$key = array_search($value, $array);
|
||||
if ($key !== false) {
|
||||
unset($array[$key]);
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
class Box {
|
||||
public $values = array("1", "2", "3", "4", "5", "6", "7", "8", "9");
|
||||
|
||||
function __construct($rowId, $columnId, $regionId) {
|
||||
$this->value = "?";
|
||||
$this->rowId = $rowId;
|
||||
$this->columnId = $columnId;
|
||||
$this->regionId = $regionId;
|
||||
$this->allowedValues = $this->values;
|
||||
$this->testValueWasAllowed = array();
|
||||
$this->neighbourhood = array();
|
||||
}
|
||||
|
||||
function searchAllowedValues() {
|
||||
$this->allowedValues = $this->values;
|
||||
forEach($this->neighbourhood as $neighbour) {
|
||||
if ($neighbour->value != "?")
|
||||
array_unset_value($neighbour->value, $this->allowedValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class Grid {
|
||||
function __construct() {
|
||||
$this->boxes = array();
|
||||
$this->rows = array_fill(0, 9, array());
|
||||
$this->columns = array_fill(0, 9, array());
|
||||
$this->regions = array_fill(0, 9, array());
|
||||
for ($regionRowId = 0; $regionRowId < 3; $regionRowId++) {
|
||||
for($regionColumnId = 0; $regionColumnId < 3; $regionColumnId++) {
|
||||
for ($rowId = 3*$regionRowId; $rowId < 3*($regionRowId+1); $rowId++) {
|
||||
for ($columnId = 3*$regionColumnId; $columnId < 3*($regionColumnId+1); $columnId++) {
|
||||
$regionId = 3*$regionRowId + $regionColumnId;
|
||||
$box = new Box($rowId, $columnId, $regionId);
|
||||
$this->boxes[] = $box;
|
||||
$this->rows[$rowId][] = $box;
|
||||
$this->columns[$columnId][] = $box;
|
||||
$this->regions[$regionId][] = $box;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// box.neighbourhood: boxes in the same row, column and region as box
|
||||
foreach($this->boxes as $box) {
|
||||
foreach(array_merge($this->rows[$box->rowId], $this->columns[$box->columnId], $this->regions[$box->regionId]) as $neighbour)
|
||||
if ($box != $neighbour && !in_array($neighbour, $box->neighbourhood))
|
||||
$box->neighbourhood[] = $neighbour;
|
||||
}
|
||||
|
||||
// Init with a shuffle row
|
||||
$values = array("1", "2", "3", "4", "5", "6", "7", "8", "9");
|
||||
shuffle($values);
|
||||
forEach($values as $columnId => $value) {
|
||||
$box = $this->rows[0][$columnId];
|
||||
$box->value = $value;
|
||||
forEach($box->neighbourhood as $neighbour)
|
||||
array_unset_value($box->value, $neighbour->allowedValues);
|
||||
}
|
||||
// Fill grid
|
||||
$this->findSolutions(true, 1, 4)->current();
|
||||
|
||||
// Remove clues while there is still a unique solution
|
||||
$untestedBoxes = $this->boxes;
|
||||
shuffle($untestedBoxes);
|
||||
$nbClues = count($untestedBoxes);
|
||||
while(count($untestedBoxes)) {
|
||||
$testBoxes = array(array_pop($untestedBoxes));
|
||||
if ($nbClues >=30)
|
||||
$testBoxes[] = $this->rows[8-$testBoxes[0]->rowId][8-$testBoxes[0]->columnId];
|
||||
if ($nbClues >=61) {
|
||||
$testBoxes[] = $this->rows[8-$testBoxes[0]->rowId][$testBoxes[0]->columnId];
|
||||
$testBoxes[] = $this->rows[$testBoxes[0]->rowId][8-$testBoxes[0]->columnId];
|
||||
}
|
||||
$erasedValues = array();
|
||||
forEach($testBoxes as $testBox) {
|
||||
$erasedValues[] = $testBox->value;
|
||||
$testBox->value = "?";
|
||||
forEach($testBox->neighbourhood as $neighbour)
|
||||
$neighbour->searchAllowedValues();
|
||||
}
|
||||
$solutions = array();
|
||||
forEach($this->findSolutions(false, 2, 4) as $solution) $solutions[$solution] = true;
|
||||
if (count($solutions) == 1) {
|
||||
$nbClues -= count($testBoxes);
|
||||
forEach($testBoxes as $testBox) array_unset_value($testBox, $untestedBoxes);
|
||||
} else {
|
||||
forEach($testBoxes as $i => $box) {
|
||||
$box->value = $erasedValues[$i];
|
||||
forEach($box->neighbourhood as $neighbour) array_unset_value($box->value, $neighbour->allowedValues);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findSolutions($randomized=false, $maxSolutions=1, $maxTries=4) {
|
||||
$emptyBoxes = array_filter($this->boxes, "isUnknown");
|
||||
if (count($emptyBoxes)) {
|
||||
if ($randomized) shuffle($emptyBoxes);
|
||||
usort($emptyBoxes, "easyFirst");
|
||||
$testBox = $emptyBoxes[0];
|
||||
$nbSolutionsFound = 0;
|
||||
$nbTries = 0;
|
||||
if ($randomized) shuffle($testBox->allowedValues);
|
||||
foreach($testBox->allowedValues as $testBox->value) {
|
||||
foreach($testBox->neighbourhood as $neighbour)
|
||||
$neighbour->testValueWasAllowed[] = array_unset_value($testBox->value, $neighbour->allowedValues);
|
||||
$correctGrid = true;
|
||||
foreach(array_filter($testBox->neighbourhood, "isUnknown") as $neighbour) {
|
||||
if (count($neighbour->allowedValues) == 0) $correctGrid = false;
|
||||
}
|
||||
if ($correctGrid) {
|
||||
foreach($this->findSolutions($randomized, $maxSolutions-$nbSolutionsFound, $maxTries) as $solution) {
|
||||
yield $solution;
|
||||
$nbSolutionsFound++;
|
||||
}
|
||||
|
||||
}
|
||||
forEach($testBox->neighbourhood as $neighbour)
|
||||
if (array_pop($neighbour->testValueWasAllowed))
|
||||
$neighbour->allowedValues[] = $testBox->value;
|
||||
if (($maxSolutions && $nbSolutionsFound >= $maxSolutions) || ++$nbTries >= $maxTries) break;
|
||||
}
|
||||
$testBox->value = "?";
|
||||
} else {
|
||||
yield $this->toString();
|
||||
}
|
||||
}
|
||||
|
||||
function toString() {
|
||||
$str = "";
|
||||
foreach($this->rows as $row) {
|
||||
forEach($row as $box) {
|
||||
$str .= ($box->value? $box->value : "?");
|
||||
}
|
||||
}
|
||||
return $str;
|
||||
}
|
||||
}
|
||||
|
||||
$grid = new Grid();
|
||||
header("Location: " . $_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . dirname($_SERVER["DOCUMENT_URI"]) . "/" . $grid->toString());
|
||||
exit();
|
||||
?>
|
23
nginx-example.conf
Normal file
23
nginx-example.conf
Normal file
@ -0,0 +1,23 @@
|
||||
location /sudoku/ {
|
||||
alias /var/www/sudoku/;
|
||||
index index.php;
|
||||
try_files $uri $uri/ @sudoku;
|
||||
|
||||
location ~ [^/]\.php(/|$) {
|
||||
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
|
||||
fastcgi_pass unix:/var/run/php/php7.3-fpm-sudoku.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param REMOTE_USER $remote_user;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param SCRIPT_FILENAME $request_filename;
|
||||
}
|
||||
}
|
||||
|
||||
location @sudoku {
|
||||
fastcgi_pass unix:/var/run/php/php7.3-fpm-sudoku.sock;
|
||||
include fastcgi_params;
|
||||
fastcgi_param REMOTE_USER $remote_user;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param SCRIPT_FILENAME sudoku/game.php;
|
||||
}
|
142
style.css
142
style.css
@ -1,18 +1,9 @@
|
||||
body {
|
||||
/* Background pattern from Toptal Subtle Patterns */
|
||||
background: url("handmadepaper.png");
|
||||
text-align: center;
|
||||
* {
|
||||
font-family: sans;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
text-shadow: 1px 1px grey;
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
div {
|
||||
@ -21,69 +12,128 @@ div {
|
||||
row-gap: 0.5em;
|
||||
margin: 1em auto;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
margin-top: -1px;
|
||||
vertical-align: middle;
|
||||
#grid {
|
||||
border-spacing: 0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
label.disabled {
|
||||
color: #aaa;
|
||||
}
|
||||
|
||||
.grid td {
|
||||
border: 2px solid black;
|
||||
td {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.region td {
|
||||
border: 1px solid grey;
|
||||
#grid tr:first-child td:first-child
|
||||
{
|
||||
border-top-left-radius: 6px;
|
||||
}
|
||||
|
||||
.grid input {
|
||||
#grid tr:first-child td
|
||||
{
|
||||
border-top: 2px solid black;
|
||||
}
|
||||
|
||||
#grid tr:first-child td:last-child
|
||||
{
|
||||
border-top-right-radius: 6px;
|
||||
}
|
||||
|
||||
#grid tr td:first-child
|
||||
{
|
||||
border-left: 2px solid black;
|
||||
}
|
||||
|
||||
#grid tr td:last-child
|
||||
{
|
||||
border-right: 2px solid black;
|
||||
}
|
||||
|
||||
#grid tr:last-child td:first-child
|
||||
{
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
|
||||
#grid tr:last-child td
|
||||
{
|
||||
border-bottom: 2px solid black;
|
||||
}
|
||||
|
||||
#grid tr:last-child td:last-child
|
||||
{
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
|
||||
.regionTop {
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
|
||||
.regionMiddle {
|
||||
border-top: 1px solid grey;
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
|
||||
.regionBottom {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
|
||||
.regionLeft {
|
||||
border-left: 1px solid black;
|
||||
}
|
||||
|
||||
.regionCenter {
|
||||
border-left: 1px solid grey;
|
||||
border-right: 1px solid grey;
|
||||
}
|
||||
|
||||
.regionRight {
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 1.6em;
|
||||
height: 1.6em;
|
||||
font-size: 1.5em;
|
||||
border: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.grid input:enabled {
|
||||
font-family: cursive;
|
||||
input:read-write {
|
||||
color: darkblue;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.grid input:disabled {
|
||||
input:read-only {
|
||||
color: black;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
background: #6666ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.grid input::placeholder {
|
||||
input.same-value:read-write {
|
||||
color: #009973;
|
||||
background: #66ffd9;
|
||||
}
|
||||
|
||||
input.forbidden-value:read-write {
|
||||
background: #ccffe6;
|
||||
}
|
||||
|
||||
input.same-value:read-only {
|
||||
color: #00664d;
|
||||
background: #00e6ac;
|
||||
}
|
||||
|
||||
input::placeholder {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.unhighlighted:enabled {
|
||||
background: transparent;
|
||||
#buttons {
|
||||
column-gap: 0.2em;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.highlighted {
|
||||
background: lightYellow;
|
||||
}
|
||||
|
||||
#shareDiv {
|
||||
display: block;
|
||||
line-height: 80%;
|
||||
}
|
||||
|
||||
#shareA {
|
||||
a {
|
||||
text-decoration: none;
|
||||
font-size: 0.8em;
|
||||
letter-spacing: 0.5em;
|
||||
}
|
||||
|
||||
#highlightRadiosDiv {
|
||||
column-gap: 0;
|
||||
}
|
||||
|
508
sudoku.js
508
sudoku.js
@ -1,326 +1,55 @@
|
||||
const VALUES = "123456789"
|
||||
suggestionTimer = null
|
||||
highlightedValue = null
|
||||
|
||||
function createGrid() {
|
||||
rows = Array.from(Array(9), x => [])
|
||||
columns = Array.from(Array(9), x => [])
|
||||
regions = Array.from(Array(9), x => [])
|
||||
for (let regionRow = 0; regionRow < 3; regionRow++) {
|
||||
const gridRow = document.createElement("tr")
|
||||
for(let regionColumn = 0; regionColumn < 3; regionColumn++) {
|
||||
const gridCell = document.createElement("td")
|
||||
const regionTable = document.createElement("table")
|
||||
regionTable.className = "region"
|
||||
for (let row = 3*regionRow; row < 3*(regionRow+1); row++) {
|
||||
const regionTr = document.createElement("tr")
|
||||
for (let column = 3*regionColumn; column < 3*(regionColumn+1); column++) {
|
||||
const regionCell = document.createElement("td")
|
||||
const box = document.createElement("input")
|
||||
box.type = "text"
|
||||
box.oninput = oninput
|
||||
box.oninvalid = oninvalid
|
||||
box.onfocus = box.select
|
||||
box.onkeydown = keyboardBrowse
|
||||
box.setAttribute("inputmode", "numeric")
|
||||
box.required = false
|
||||
box.region = 3*regionRow + regionColumn
|
||||
box.column = column
|
||||
box.row = row
|
||||
box.minLength = 0
|
||||
box.maxLength = 1
|
||||
box.tabIndex = 9
|
||||
rows[row].push(box)
|
||||
columns[column].push(box)
|
||||
regions[box.region].push(box)
|
||||
regionCell.appendChild(box)
|
||||
regionTr.appendChild(regionCell)
|
||||
}
|
||||
regionTable.appendChild(regionTr)
|
||||
}
|
||||
gridCell.appendChild(regionTable)
|
||||
gridRow.appendChild(gridCell)
|
||||
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 highlightedValue = ""
|
||||
|
||||
window.onload = function() {
|
||||
let rowId = 0
|
||||
for (row of grid.getElementsByTagName('tr')) {
|
||||
let columnId = 0
|
||||
for (box of row.getElementsByTagName('input')) {
|
||||
let regionId = rowId - rowId%3 + Math.floor(columnId/3)
|
||||
if (!box.readOnly) box.select
|
||||
box.oninput = oninput
|
||||
box.oninvalid = oninvalid
|
||||
box.onkeydown = keyboardBrowse
|
||||
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++
|
||||
}
|
||||
gridTable.appendChild(gridRow)
|
||||
rowId++
|
||||
}
|
||||
grid = rows.reduce((grid, row) => grid.concat(row), [])
|
||||
// box.neighbourhood: boxes in the same row, column and region as box
|
||||
grid.forEach(box => {
|
||||
box.neighbourhood = new Set(rows[box.row].concat(columns[box.column]).concat(regions[box.region]))
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
function clearGrid() {
|
||||
clearTimeout(suggestionTimer)
|
||||
suggestionTimer = null
|
||||
grid.forEach(box => {
|
||||
box.value = ""
|
||||
box.allowedValues = new Set(VALUES)
|
||||
box.testValueWasAllowed = []
|
||||
box.pattern = "[1-9]?"
|
||||
box.placeholder = ""
|
||||
box.disabled = false
|
||||
box.required = false
|
||||
})
|
||||
enableHighlightOptions()
|
||||
}
|
||||
|
||||
function loadGrid() {
|
||||
window.onhashchange = function() {
|
||||
if (location.hash) {
|
||||
if (location.hash.match(/^#[1-9?]{81}$/)) {
|
||||
gridAsString = location.hash.substring(1)
|
||||
if (gridAsString != grid.map(box => box.disabled? box.value : "?").join("")) {
|
||||
// Load grid from hash if it differs
|
||||
clearGrid()
|
||||
if (savedGame = localStorage.getItem(gridAsString + "SavedGame")) {
|
||||
// We trust grid is valid if it was previously saved
|
||||
grid.forEach((box, i) => {
|
||||
box.value = savedGame[i] == "?"? "" : savedGame[i]
|
||||
box.disabled = Boolean(gridAsString[i] != "?")
|
||||
})
|
||||
startTime = Date.now() - localStorage.getItem(gridAsString + "Time")
|
||||
finishGridLoad()
|
||||
} else {
|
||||
// Else we need to check if grid is valid
|
||||
grid.forEach((box, i) => box.value = gridAsString[i] == "?"? "" : gridAsString[i])
|
||||
grid.forEach(box => searchAllowedValuesOf(box))
|
||||
checkGrid()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
alert(location.hash.substring(1) + " n'est pas une grille valide.")
|
||||
}
|
||||
boxes.forEach(searchAllowedValuesOf)
|
||||
enableButtons()
|
||||
boxes.forEach(showAllowedValuesOn)
|
||||
for(box of boxes) {
|
||||
if (!box.readOnly) {
|
||||
box.focus()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if (location.hash)
|
||||
window.onhashchange()
|
||||
else if ((lastPlayedGrid = localStorage.getItem("lastPlayedGrid")) && localStorage.getItem(lastPlayedGrid + "SavedGame").match("[?]"))
|
||||
location.hash = lastPlayedGrid
|
||||
else
|
||||
generateGrid()
|
||||
}
|
||||
|
||||
function checkGrid() {
|
||||
if (sudokuForm.checkValidity()) {
|
||||
switch(new Set(findSolutions(false, 2, 4)).size) {
|
||||
case 0:
|
||||
this.reportValidity()
|
||||
alert("Cette grille n'a pas de solution. Veuillez la corriger.")
|
||||
break
|
||||
case 1:
|
||||
freezeGrid()
|
||||
break
|
||||
default:
|
||||
alert("Cette grille a plusieurs solutions. Rajoutez des indices.")
|
||||
}
|
||||
} else {
|
||||
sudokuForm.reportValidity()
|
||||
}
|
||||
}
|
||||
|
||||
function freezeGrid() {
|
||||
grid.forEach(box => box.disabled = Boolean(box.value.length))
|
||||
startTime = Date.now()
|
||||
gridAsString = grid.map(box => box.disabled? box.value : "?").join("")
|
||||
location.hash = gridAsString
|
||||
finishGridLoad()
|
||||
}
|
||||
|
||||
function finishGridLoad() {
|
||||
grid.forEach(box => {
|
||||
searchAllowedValuesOf(box)
|
||||
showAllowedValuesOn(box)
|
||||
box.pattern = `[${Array.from(box.allowedValues).join("")}]?`
|
||||
})
|
||||
enableHighlightOptions()
|
||||
highlightAndTabOrder()
|
||||
shareA.href = location
|
||||
shareA.innerHTML = location.hostname + location.pathname + "#<br/>" + Array(9).fill().map((_,i) => gridAsString.slice(9*i, 9*i+9)).join("<br/>")
|
||||
solutions = findSolutions(false, 1, 4)
|
||||
localStorage.setItem("lastPlayedGrid", gridAsString)
|
||||
saveGame()
|
||||
suggestionTimer = setTimeout(showSuggestion, 30000)
|
||||
}
|
||||
|
||||
function saveGame() {
|
||||
localStorage.setItem(gridAsString + "SavedGame", grid.map(box => box.value || "?").join(""))
|
||||
localStorage.setItem(gridAsString + "Time", Date.now() - startTime)
|
||||
}
|
||||
|
||||
fromEasyToDifficult = (box1, box2) => box1.allowedValues.size - box2.allowedValues.size
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
function* findSolutions(randomized=false, maxSolutions=1, maxTries=4) {
|
||||
let emptyBoxes = grid.filter(box => box.value == "")
|
||||
if (emptyBoxes.length) {
|
||||
if (randomized) // Generate random grids
|
||||
emptyBoxes = shuffle(emptyBoxes)
|
||||
const testBox = emptyBoxes.sort(fromEasyToDifficult)[0] // Pick an easy box to solve
|
||||
// Try every allowed value
|
||||
let nbSolutionsFound = 0
|
||||
let nbTries = 0
|
||||
for (testBox.value of randomized? shuffle(testBox.allowedValues) : testBox.allowedValues) {
|
||||
testBox.neighbourhood.forEach(neighbour => {
|
||||
// Remember if testBox.value was in neighbour.allowedValues in case we rewrite it
|
||||
neighbour.testValueWasAllowed.push(neighbour.allowedValues.has(testBox.value))
|
||||
neighbour.allowedValues.delete(testBox.value)
|
||||
})
|
||||
// If grid still correct, yield all solutions with this hypothesis
|
||||
if (testBox.neighbourhood.filter(box => box.value == "").every(emptyBox => emptyBox.allowedValues.size)) {
|
||||
for (const solution of findSolutions(randomized, maxSolutions-nbSolutionsFound, maxTries)) {
|
||||
yield solution
|
||||
nbSolutionsFound++
|
||||
}
|
||||
}
|
||||
testBox.neighbourhood.filter(
|
||||
neighbour => neighbour.testValueWasAllowed.pop()
|
||||
).forEach(neighbour => neighbour.allowedValues.add(testBox.value))
|
||||
if (maxSolutions && nbSolutionsFound >= maxSolutions) break
|
||||
if (++nbTries >= maxTries) break
|
||||
}
|
||||
testBox.value = ""
|
||||
} else {
|
||||
yield grid.map(box => box.value).join("")
|
||||
}
|
||||
return "No more solutions"
|
||||
}
|
||||
|
||||
function generateGrid() {
|
||||
clearGrid()
|
||||
shuffle(Array.from(VALUES)).forEach((value, c) => {
|
||||
rows[0][c].value = value
|
||||
rows[0][c].neighbourhood.forEach(neighbour => neighbour.allowedValues.delete(rows[0][c].value))
|
||||
})
|
||||
// Generate a random valid grid where all boxes are clues
|
||||
while (findSolutions(true, 1, 4).next().done) {}
|
||||
// Remove clues while there is still a unique solution
|
||||
let untestedBoxes = shuffle(grid)
|
||||
let nbClues = 81
|
||||
while(untestedBoxes.length) {
|
||||
const testBoxes = [untestedBoxes.pop()]
|
||||
if (nbClues >= 30)
|
||||
testBoxes.push(rows[8-testBoxes[0].row][8-testBoxes[0].column])
|
||||
if (nbClues >= 61) {
|
||||
testBoxes.push(rows[8-testBoxes[0].row][testBoxes[0].column])
|
||||
testBoxes.push(rows[testBoxes[0].row][8-testBoxes[0].column])
|
||||
}
|
||||
const erasedValues = testBoxes.map(box => box.value)
|
||||
testBoxes.forEach(box => {
|
||||
box.value = ""
|
||||
box.neighbourhood.forEach(neighbour => searchAllowedValuesOf(neighbour))
|
||||
})
|
||||
if (Array.from(findSolutions(false, 2, 4)).length == 1) {
|
||||
nbClues -= testBoxes.length
|
||||
testBoxes.slice(1).forEach(box => untestedBoxes.splice(untestedBoxes.indexOf(box), 1))
|
||||
} else {
|
||||
testBoxes.forEach((box, i) => {
|
||||
box.value = erasedValues[i]
|
||||
box.neighbourhood.forEach(neighbour => neighbour.allowedValues.delete(box.value))
|
||||
})
|
||||
}
|
||||
}
|
||||
freezeGrid()
|
||||
}
|
||||
|
||||
function customGrid() {
|
||||
clearGrid()
|
||||
grid.forEach(box => showAllowedValuesOn(box))
|
||||
enableHighlightOptions()
|
||||
highlightAndTabOrder()
|
||||
solutions = findSolutions(false, 1, 4)
|
||||
customGridButton.innerText = "Figer la grille"
|
||||
customGridButton.onclick = checkGrid
|
||||
location.hash = ""
|
||||
}
|
||||
|
||||
function searchAllowedValuesOf(box) {
|
||||
box.allowedValues = new Set(VALUES)
|
||||
box.neighbourhood.forEach(neighbour => box.allowedValues.delete(neighbour.value))
|
||||
}
|
||||
|
||||
function keyboardBrowse(event) {
|
||||
switch(event.key) {
|
||||
case "ArrowLeft":
|
||||
event.preventDefault()
|
||||
moveOn(rows[this.row], this.column, 8)
|
||||
break
|
||||
case "ArrowRight":
|
||||
event.preventDefault()
|
||||
moveOn(rows[this.row], this.column, 1)
|
||||
break
|
||||
case "ArrowUp":
|
||||
event.preventDefault()
|
||||
moveOn(columns[this.column], this.row, 8)
|
||||
break
|
||||
case "ArrowDown":
|
||||
event.preventDefault()
|
||||
moveOn(columns[this.column], this.row, 1)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
function moveOn(area, position, direction) {
|
||||
if (area.filter(box => box.disabled).length < 9) {
|
||||
do {
|
||||
position = (position + direction) % 9
|
||||
} while (area[position].disabled)
|
||||
area[position].focus()
|
||||
}
|
||||
}
|
||||
|
||||
timeFormat = new Intl.DateTimeFormat("fr-FR", {
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
second: "2-digit",
|
||||
timeZone: "UTC"
|
||||
}).format
|
||||
|
||||
function oninput() {
|
||||
saveGame()
|
||||
|
||||
this.neighbourhood.concat([this]).forEach(box => {
|
||||
box.setCustomValidity("")
|
||||
searchAllowedValuesOf(box)
|
||||
box.pattern = `[${Array.from(box.allowedValues).join("")}]?`
|
||||
})
|
||||
|
||||
highlightAndTabOrder()
|
||||
enableHighlightOptions()
|
||||
this.neighbourhood.concat([this]).forEach(neighbour => showAllowedValuesOn(neighbour))
|
||||
|
||||
if (this.form.checkValidity()) { // Correct grid
|
||||
if (grid.filter(box => box.value == "").length == 0) {
|
||||
alert(`Bravo ! Vous avez résolu la grille en ${timeFormat(Date.now() - startTime)}.`)
|
||||
} else {
|
||||
solutions = findSolutions(false, 1, 4) // Reset solutions generator
|
||||
if (suggestionTimer) clearTimeout(suggestionTimer)
|
||||
suggestionTimer = setTimeout(showSuggestion, 30000)
|
||||
}
|
||||
} else { // Errors on grid
|
||||
this.select()
|
||||
this.reportValidity()
|
||||
}
|
||||
}
|
||||
|
||||
// Help functions
|
||||
|
||||
function showAllowedValuesOn(box) {
|
||||
box.required = box.allowedValues.size == 0
|
||||
if (box.value.length) {
|
||||
@ -333,14 +62,49 @@ function showAllowedValuesOn(box) {
|
||||
}
|
||||
}
|
||||
|
||||
function oninput() {
|
||||
this.neighbourhood.concat([this]).forEach(box => {
|
||||
box.setCustomValidity("")
|
||||
searchAllowedValuesOf(box)
|
||||
box.pattern = `[${Array.from(box.allowedValues).join("")}]?`
|
||||
})
|
||||
|
||||
enableButtons()
|
||||
refreshShowValue()
|
||||
this.neighbourhood.concat([this]).forEach(neighbour => showAllowedValuesOn(neighbour))
|
||||
|
||||
if (this.form.checkValidity()) { // Correct grid
|
||||
if (boxes.filter(box => box.value == "").length == 0) {
|
||||
alert(`Bravo ! Vous avez résolu la grille.`)
|
||||
} else {
|
||||
if (suggestionTimer) clearTimeout(suggestionTimer)
|
||||
suggestionTimer = setTimeout(showSuggestion, 30000)
|
||||
}
|
||||
} else { // Errors on grid
|
||||
this.select()
|
||||
this.reportValidity()
|
||||
}
|
||||
}
|
||||
|
||||
function enableButtons() {
|
||||
for (button of buttons.getElementsByTagName("button")) {
|
||||
if (boxes.filter(box => box.value == "").some(box => box.allowedValues.has(button.textContent))) {
|
||||
button.disabled = false
|
||||
} else {
|
||||
button.disabled = true
|
||||
if (highlightedValue == button.textContent) highlightedValue = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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.region]))
|
||||
else if (sameValueIn(regions[this.regionId]))
|
||||
this.setCustomValidity(`Il y a un autre ${this.value} dans cette région.`)
|
||||
else if (sameValueIn(rows[this.row]))
|
||||
else if (sameValueIn(rows[this.rowId]))
|
||||
this.setCustomValidity(`Il y a un autre ${this.value} dans cette ligne.`)
|
||||
else if (sameValueIn(columns[this.column]))
|
||||
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.")
|
||||
@ -357,90 +121,74 @@ function sameValueIn(area) {
|
||||
return false
|
||||
}
|
||||
|
||||
function highlight(value) {
|
||||
highlightedValue = value
|
||||
highlightAndTabOrder()
|
||||
|
||||
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 highlightAndTabOrder() {
|
||||
if (highlightedValue) {
|
||||
for (const box of grid) {
|
||||
box.tabIndex = -1
|
||||
box.className = "unhighlighted"
|
||||
if (box.value == "") {
|
||||
if (box.allowedValues.has(highlightedValue)) {
|
||||
box.tabIndex = 0
|
||||
box.className = "highlighted"
|
||||
}
|
||||
}
|
||||
}
|
||||
function moveOn(area, position, direction) {
|
||||
position = (position + direction) % 9
|
||||
area[position].focus()
|
||||
}
|
||||
|
||||
function showValue(button) {
|
||||
if (button.textContent == highlightedValue) {
|
||||
highlightedValue = ""
|
||||
} else {
|
||||
for (const box of grid) {
|
||||
box.className = "unhighlighted"
|
||||
if (box.value == "" && box.allowedValues.size == 1) {
|
||||
box.tabIndex = 0
|
||||
} else {
|
||||
box.tabIndex = -1
|
||||
}
|
||||
}
|
||||
highlightedValue = button.textContent
|
||||
}
|
||||
refreshShowValue()
|
||||
}
|
||||
|
||||
function refreshShowValue() {
|
||||
boxes.forEach(box => box.className = "")
|
||||
if (highlightedValue) {
|
||||
boxes.forEach(box => {
|
||||
if (box.value == highlightedValue) box.className = "same-value"
|
||||
if (!box.allowedValues.has(highlightedValue)) box.className = "forbidden-value"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function enableHighlightOptions() {
|
||||
for (value of VALUES) {
|
||||
let highlightRadio = document.getElementById("highlightRadio" + value)
|
||||
let highlightLabel = document.getElementById("highlightLabel" + value)
|
||||
if (grid.filter(box => box.value == "").some(box => box.allowedValues.has(value))) {
|
||||
highlightRadio.disabled = false
|
||||
highlightLabel.className = ""
|
||||
} else {
|
||||
highlightRadio.disabled = true
|
||||
highlightRadio.checked = false
|
||||
highlightLabel.className = "disabled"
|
||||
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
|
||||
}
|
||||
|
||||
easyFirst = (box1, box2) => box1.allowedValues.size - box2.allowedValues.size
|
||||
|
||||
function showSuggestion() {
|
||||
const emptyBoxes = grid.filter(box => box.value == "")
|
||||
const emptyBoxes = boxes.filter(box => box.value == "")
|
||||
if (emptyBoxes.length) {
|
||||
shuffle(emptyBoxes).sort(fromEasyToDifficult)[0].placeholder = "!"
|
||||
shuffle(emptyBoxes).sort(easyFirst)[0].placeholder = "!"
|
||||
} else {
|
||||
clearTimeout(suggestionTimer)
|
||||
suggestionTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function erase() {
|
||||
grid.filter(box => !box.disabled).forEach(box => {
|
||||
box.value = ""
|
||||
box.placeholder = ""
|
||||
})
|
||||
grid.filter(box => !box.disabled).forEach(box => {
|
||||
searchAllowedValuesOf(box)
|
||||
showAllowedValuesOn(box)
|
||||
})
|
||||
sudokuForm.checkValidity()
|
||||
sudokuForm.reportValidity()
|
||||
enableHighlightOptions()
|
||||
solutions = findSolutions(false, 1, 4)
|
||||
solveButton.innerText = "Résoudre"
|
||||
solveButton.type = "button"
|
||||
solveButton.onclick = solve
|
||||
solveButton.disabled = false
|
||||
startTime = Date.now()
|
||||
}
|
||||
|
||||
function solve() {
|
||||
if (sudokuForm.checkValidity()) {
|
||||
if (solutions.next().done) { // End of solutions generator
|
||||
solveButton.innerText = "Montrer la solution"
|
||||
solutions = findSolutions(false, 1, 4)
|
||||
} else {
|
||||
solveButton.innerText = "Effacer la solution"
|
||||
}
|
||||
grid.forEach(box => showAllowedValuesOn(box))
|
||||
} else {
|
||||
sudokuForm.reportValidity()
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user