Compare commits

..

1 Commits

Author SHA1 Message Date
2d431a604f generated favicon 2020-10-30 19:30:59 +01:00
21 changed files with 764 additions and 1092 deletions

27
400.php
View File

@ -1,27 +0,0 @@
<?php http_response_code(400); ?>
<!DOCTYPE html>
<html lang='fr'>
<head>
<?php require("head.php"); ?>
<title>Requête incorrecte</title>
</head>
<body>
<nav class="navbar mb-4">
<h1 class="display-4 text-center m-auto">Sudoku</h1>
</nav>
<main class="container my-4">
<header>
<h1 class="mb-4">Requête incorrecte</h1>
</header>
L'adresse URL doit être de la forme :<br/>
<?=$dirUrl?>/?<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>un tiret (-) pour les case vides</li>
</ul>
Exemple :<br/>
<a href='<?=$newGridUrl?>'><?=$newGridUrl?></a>
</main>
</body>
</html>

View File

@ -1,5 +0,0 @@
# Sudoku
Web sudoku assistant
![screenshot](https://git.malingrey.fr/adrien/sudoku/raw/branch/master/thumbnail.png)

315
app.js Normal file
View File

@ -0,0 +1,315 @@
const VALUES = "123456789"
const SUGESTION_DELAY = 60000 //ms
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 = ""
let history = []
let accessKeyModifiers = "AccessKey+"
let penStyle = "ink-pen"
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.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++
}
let savedGame = localStorage[location.href]
if (savedGame) {
boxes.forEach((box, i) => {
if (!box.disabled && savedGame[i] != '.') box.value = savedGame[i]
})
}
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)
searchCandidatesOf(box)
})
enableButtons()
highlightAndTab()
if (/Win/.test(navigator.platform) || /Linux/.test(navigator.platform)) accessKeyModifiers = "Alt+Maj+"
else if (/Mac/.test(navigator.platform)) accessKeyModifiers = "⌃⌥"
for(node of document.querySelectorAll("*[accesskey]")) {
node.title += " [" + (node.accessKeyLabel || accessKeyModifiers + node.accessKey) + "]"
}
document.onclick = function (event) {
contextMenu.style.display = "none"
}
suggestionTimer = setTimeout(showSuggestion, 30000)
}
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 = "1 possibilité [Clic-droit]"
break
default:
box.title = box.candidates.size + " possibilités [Clic-droit]"
}
}
}
function onfocus() {
if (penStyle == "pencil" && this.value == "") {
this.value = this.placeholder
this.placeholder = ""
this.classList.add("pencil")
} else {
this.select()
}
}
function oninput() {
history.push({box: this, value: this.previousValue || "", placeholder: this.previousPlaceholder || ""})
undoButton.disabled = false
if (penStyle != "pencil") {
refresh(this)
}
}
function undo() {
if (history.length) {
previousState = history.pop()
previousState.box.value = previousState.value
previousState.box.placeholder = previousState.placeholder
refresh(previousState.box)
if (history.length < 1) undoButton.disabled = true
}
}
function refresh(box) {
localStorage[location.href] = boxes.map(box => box.value || ".").join("")
box.neighbourhood.concat([box]).forEach(neighbour => {
searchCandidatesOf(neighbour)
neighbour.setCustomValidity("")
neighbour.required = false
})
enableButtons()
highlightAndTab()
for (neighbour1 of box.neighbourhood) {
if (neighbour1.value.length == 1) {
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) {
for (neighbour of [neighbour1, neighbour2]) {
neighbour.setCustomValidity(`Il y a un autre ${neighbour.value} dans cette ${area.name}.`)
}
}
} else {
if (neighbour1.candidates.size == 0) {
neighbour1.setCustomValidity("Aucun chiffre possible !")
neighbour1.required = true
}
}
}
if (box.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, SUGESTION_DELAY)
}
} else { // Errors on grid
box.form.reportValidity()
box.select()
}
}
function onblur() {
if (this.classList.contains("pencil")) {
this.placeholder = this.value
this.value = ""
this.classList.remove("pencil")
}
this.previousValue = this.value
this.previousPlaceholder = this.placeholder
}
function enableButtons() {
for (button of buttons.getElementsByTagName("button")) {
if (boxes.filter(box => box.value == "").some(box => box.candidates.has(button.textContent))) {
button.disabled = false
} else {
button.disabled = true
if (highlightedValue == button.textContent) highlightedValue = ""
}
}
}
function highlight(value) {
if (value == highlightedValue) {
highlightedValue = ""
} else {
highlightedValue = value
}
for (button of buttons.getElementsByTagName("button")) {
if (button.textContent == highlightedValue) button.classList.add("pressed")
else button.classList.remove("pressed")
}
highlightAndTab()
boxes.filter(box => box.value == "" && box.tabIndex == 0)[0].focus()
}
function highlightAndTab() {
if (highlightedValue) {
boxes.forEach(box => {
if (box.value == highlightedValue) {
box.classList.add("same-value")
box.tabIndex = -1
}
else {
box.classList.remove("same-value")
if (box.candidates.has(highlightedValue)) {
box.classList.remove("forbidden-value")
box.tabIndex = 0
} else {
box.classList.add("forbidden-value")
box.tabIndex = -1
}
}
})
} else {
boxes.forEach(box => {
box.classList.remove("same-value", "forbidden-value")
box.tabIndex = 0
})
}
}
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.candidates.size - box2.candidates.size
function showSuggestion() {
const emptyBoxes = boxes.filter(box => box.value == "" && box.candidates.size == 1)
if (emptyBoxes.length) {
shuffle(emptyBoxes)[0].placeholder = "💡"
} else {
clearTimeout(suggestionTimer)
suggestionTimer = null
}
}
function oncontextmenu(event) {
event.preventDefault()
while (contextMenu.firstChild) contextMenu.firstChild.remove()
const box = event.target
if (box.candidates.size) {
candidatesArray = Array.from(box.candidates).sort().forEach(candidate => {
li = document.createElement("li")
li.innerText = candidate
li.onclick = function (event) {
contextMenu.style.display = "none"
box.value = event.target.innerText
oninput.apply(box)
}
contextMenu.appendChild(li)
})
} else {
li = document.createElement("li")
li.innerText = "Aucune possibilité !"
li.classList.add("error")
contextMenu.appendChild(li)
}
contextMenu.style.left = `${event.pageX}px`
contextMenu.style.top = `${event.pageY}px`
contextMenu.style.display = "block"
return false
}
function useInkPen() {
inkPenButton.classList.add("pressed")
pencilButton.classList.remove("pressed")
penStyle = "ink-pen"
}
function usePencil() {
pencilButton.classList.add("pressed")
inkPenButton.classList.remove("pressed")
penStyle = "pencil"
}
function erase(someBoxes) {
for (box of someBoxes) {
box.value = ""
box.placeholder = ""
searchCandidatesOf(box)
refresh(box)
}
enableButtons()
highlightAndTab()
}
function erasePencil() {
if (confirm("Effacer les chiffres écrits au crayon ?")) {
boxes.filter(box => !box.disabled).forEach(box => {
box.placeholder = ""
})
}
}
function eraseAll() {
if (confirm("Effacer tous les chiffres écrits au crayon et au stylo ?")) {
boxes.filter(box => !box.disabled).forEach(box => {
box.value = ""
box.placeholder = ""
box.setCustomValidity("")
box.required = false
})
boxes.forEach(searchCandidatesOf)
enableButtons()
highlightAndTab()
}
}

153
classes.php Executable file → Normal file
View File

@ -1,7 +1,5 @@
<?php
const UNKNOWN = "-";
$validGrids = array();
const UNKNOWN = ".";
function isKnown($box) {
return $box->value != UNKNOWN;
@ -26,20 +24,16 @@
}
class Box {
public $values = array('1', '2', '3', '4', '5', '6', '7', '8', '9');
public $value = UNKNOWN;
public $rowId;
public $columnId;
public $regionId;
public $candidates;
public $candidateRemoved = array();
public $neighbourhood = array();
public $values = array("1", "2", "3", "4", "5", "6", "7", "8", "9");
function __construct($rowId, $columnId, $regionId) {
$this->value = UNKNOWN;
$this->rowId = $rowId;
$this->columnId = $columnId;
$this->regionId = $regionId;
$this->candidates = $this->values;
$this->candidateRemoved = array();
$this->neighbourhood = array();
}
function searchCandidates() {
@ -52,19 +46,14 @@
}
class Grid {
private $boxes = array();
private $rows;
private $columns;
private $regions;
function __construct($gridStr="") {
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 ($rowId = 3*$regionRowId; $rowId < 3*($regionRowId+1); $rowId++) {
for($regionColumnId = 0; $regionColumnId < 3; $regionColumnId++) {
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);
@ -83,103 +72,60 @@
if ($box != $neighbour && !in_array($neighbour, $box->neighbourhood))
$box->neighbourhood[] = $neighbour;
}
if ($gridStr) {
$this->import($gridStr);
} else {
$this->generate();
}
}
function import($gridStr) {
foreach ($this->boxes as $i => $box) {
$box->value = $gridStr[$i];
}
forEach($this->boxes as $box) {
forEach($box->neighbourhood as $neighbour)
array_unset_value($box->value, $neighbour->candidates);
}
}
function generate() {
// Init with a shuffle row
$values = array("1", "2", "3", "4", "5", "6", "7", "8", "9");
shuffle($values);
forEach($this->rows[0] as $columnId => $box) {
$box->value = $values[$columnId];
forEach($values as $columnId => $value) {
$box = $this->rows[0][$columnId];
$box->value = $value;
forEach($box->neighbourhood as $neighbour)
array_unset_value($box->value, $neighbour->candidates);
}
// Fill grid
$this->solutionsGenerator(true)->current();
// Group boxes with their groupedSymetricals
$groupedSymetricals = array(array($this->rows[4][4]));
for ($rowId = 0; $rowId <= 3; $rowId++) {
for ($columnId = 0; $columnId <= 3; $columnId++) {
$groupedSymetricals[] = array(
$this->rows[$rowId][$columnId],
$this->rows[8-$rowId][8-$columnId],
$this->rows[8-$rowId][$columnId],
$this->rows[$rowId][8-$columnId]
);
// Remove clues while there is still a unique solution
shuffle($this->boxes);
$nbClues = count($this->boxes);
foreach($this->boxes as $testBox) {
$testBoxes = array($testBox);
if ($nbClues >=30)
$testBoxes[] = $this->rows[8-$testBox->rowId][8-$testBox->columnId];
if ($nbClues >=61) {
$testBoxes[] = $this->rows[8-$testBox->rowId][$testBox->columnId];
$testBoxes[] = $this->rows[$testBox->rowId][8-$testBox->columnId];
}
$groupedSymetricals[] = array(
$this->rows[$rowId][4],
$this->rows[8-$rowId][4]
);
}
for ($columnId = 0; $columnId <= 3; $columnId++) {
$groupedSymetricals[] = array(
$this->rows[4][$columnId],
$this->rows[4][8-$columnId]
);
}
// Remove clues randomly and their groupedSymetricals while there is still a unique solution
shuffle($groupedSymetricals);
foreach($groupedSymetricals as $symetricals) {
shuffle($symetricals);
foreach ($symetricals as $testBox) {
$erasedValue = $testBox->value;
$testBoxes = array_filter($testBoxes, "isKnown");
$erasedValues = array();
forEach($testBoxes as $testBox) {
$erasedValues[] = $testBox->value;
$testBox->value = UNKNOWN;
forEach($testBox->neighbourhood as $neighbour)
$neighbour->searchCandidates();
if (!$this->isValid()) {
$testBox->value = $erasedValue;
}
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->candidates);
}
}
}
$validGrids[] = $this->toString();
}
function containsDuplicates() {
foreach(array_merge($this->rows, $this->columns, $this->regions) as $area) {
$knownBoxes = array_filter($area, "isKnown");
foreach($knownBoxes as $box1) {
foreach($knownBoxes as $box2) {
if (($box1 != $box2) && ($box1->value == $box2->value)) {
return true;
}
}
}
}
return false;
}
function countSolutions($max=2) {
$solutions = $this->solutionsGenerator(false);
$solutionsWithoutDuplicates = array();
$nbSolutions = 0;
foreach($solutions as $solution) {
if (!in_array($solution, $solutionsWithoutDuplicates)) {
$solutionsWithoutDuplicates[] = $solution;
$nbSolutions ++;
if ($nbSolutions >= $max) {
$solutions->send(true);
break;
}
$solutionsWithoutDuplicates[$solution] = true;
$nbSolutions = count($solutionsWithoutDuplicates);
if ($nbSolutions >= $max) {
$solutions->send(true);
}
}
return $nbSolutions;
@ -195,12 +141,20 @@
if ($randomized) shuffle($emptyBoxes);
usort($emptyBoxes, "easyFirst");
$testBox = $emptyBoxes[0];
$nbTries = 0;
if ($randomized) shuffle($testBox->candidates);
$stop = null;
foreach($testBox->candidates as $testBox->value) {
$correctGrid = true;
foreach(array_filter($testBox->neighbourhood, "isUnknown") as $neighbour)
$neighbour->candidateRemoved[] = array_unset_value($testBox->value, $neighbour->candidates);
if ($this->candidatesOnEachUnknownBoxeOf($testBox->neighbourhood)) {
foreach(array_filter($testBox->neighbourhood, "isUnknown") as $neighbour) {
if (count($neighbour->candidates) == 0) {
$correctGrid = false;
break;
}
}
if ($correctGrid) {
$solutions = $this->solutionsGenerator($randomized);
foreach($solutions as $solution) {
$stop = (yield $solution);
@ -220,21 +174,12 @@
yield $this->toString();
}
}
function candidatesOnEachUnknownBoxeOf($area) {
foreach($area as $box) {
if (($box->value == UNKNOWN) && (count($box->candidates) == 0)) {
return false;
}
}
return true;
}
function toString() {
$str = "";
foreach($this->rows as $row) {
forEach($row as $box) {
$str .= $box->value;
$str .= ($box->value? $box->value : UNKNOWN);
}
}
return $str;

0
favicon.png Executable file → Normal file
View File

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 542 B

View File

@ -1,32 +0,0 @@
<meta charset="utf-8" />
<title>Sudoku</title><meta property="og:title" content="Sudoku" />
<meta property="og:type" content="game" />
<meta name="description" property="og:description" content="Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9." />
<link rel="canonical" href="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].dirname($_SERVER["DOCUMENT_URI"])?>" />
<meta property="og:url" content="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].$_SERVER["DOCUMENT_URI"]?>" />
<meta property="og:image" content="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].dirname($_SERVER["DOCUMENT_URI"])?>/thumbnail.php?size=200&grid=<?=$currentGrid?>" />
<meta property="og:image:width" content="200" />
<meta property="og:image:height" content="200" />
<meta name="Language" CONTENT="fr" /><meta property="og:locale" content="fr_FR" />
<meta property="og:site_name" content="<?=$_SERVER["HTTP_HOST"]?>" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-dark.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.2.0/fonts/remixicon.css" rel="stylesheet">
<link href="style.css" rel="stylesheet" type="text/css" />
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=196" sizes="196x196" rel="icon" type="image/png">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=160" sizes="160x160" rel="icon" type="image/png">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=96" sizes="96x96" rel="icon" type="image/png">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=16" sizes="16x16" rel="icon" type="image/png">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=32" sizes="32x32" rel="icon" type="image/png">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=152" sizes="152x152" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=144" sizes="144x144" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=120" sizes="120x120" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=114" sizes="114x114" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=57" sizes="57x57" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=72" sizes="72x72" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=60" sizes="60x60" rel="apple-touch-icon">
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=76" sizes="76x76" rel="apple-touch-icon">
<link href="manifest.php?grid=<?=$currentGrid?>" rel="manifest">

BIN
img/ink-eraser.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

BIN
img/ink-pen.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
img/pencil-eraser.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
img/pencil.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.0 KiB

BIN
img/undo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

49
index.php Executable file → Normal file
View File

@ -1,45 +1,8 @@
<?php
<?php
require("classes.php");
$fullUrl = $_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].$_SERVER["DOCUMENT_URI"];
$dirUrl = dirname($fullUrl);
$currentGrid = strip_tags($_SERVER['QUERY_STRING']);
if (preg_match("/^[1-9-]{81}$/", $currentGrid)) {
session_id($currentGrid);
session_start(["use_cookies" => false]);
if (!array_key_exists("nbSolutions", $_SESSION)) {
$grid = new Grid($currentGrid);
$_SESSION["nbSolutions"] = $grid->containsDuplicates() ? -1 : $grid->countSolutions(2);
}
switch($_SESSION["nbSolutions"]) {
case -1:
$warning = "Cette grille contient des doublons.";
break;
case 0:
$warning = "Cette grille n'a pas de solution.";
break;
case 1:
break;
default:
$warning = "Cette grille a plusieurs solutions.";
}
require("sudoku.php");
} else {
if ($currentGrid) {
require("400.php");
} else {
$grid = new Grid();
$gridAsString = $grid->toString();
$newGridUrl = "$dirUrl/?$gridAsString";
session_id($gridAsString);
session_start(["use_cookies" => false]);
$_SESSION["nbSolutions"] = 1;
header("Location: $newGridUrl");
}
}
?>
$grid = new Grid();
$grid->generate();
header("Location: " . $_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . dirname($_SERVER["DOCUMENT_URI"]) . "/" . $grid->toString());
exit();
?>

View File

@ -1,137 +0,0 @@
<?php
if (isset($_GET["grid"]))
$currentGrid = $_GET["grid"];
else
$currentGrid = ".";
?>
{
"short_name": "Sudoku",
"name": "Sudoku",
"description": "Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.",
"icons": [{
"src": "thumbnail.php?size=48&grid=<?=$currentGrid?>",
"sizes": "48x48",
"type": "image/png"
}, {
"src": "thumbnail.php?size=72&grid=<?=$currentGrid?>",
"sizes": "72x72",
"type": "image/png"
}, {
"src": "thumbnail.php?size=96&grid=<?=$currentGrid?>",
"sizes": "96x96",
"type": "image/png"
}, {
"src": "thumbnail.php?size=144&grid=<?=$currentGrid?>",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "thumbnail.php?size=168&grid=<?=$currentGrid?>",
"sizes": "168x168",
"type": "image/png"
}, {
"src": "thumbnail.php?size=192&grid=<?=$currentGrid?>",
"sizes": "192x192",
"type": "image/png"
}],
"start_url": ".",
"background_color": "#fff",
"display": "standalone",
"scope": ".",
"theme_color": "#fff",
"orientation": "portrait-primary",
"shortcuts": [
{
"name": "Sudoku : cette grille",
"short_name": "Ce sudoku",
"description": "Continuer cette grille de sudoku",
"url": "<?=$currentGrid?>",
"icons": [{
"src": "thumbnail.php?size=48&grid=<?=$currentGrid?>",
"sizes": "48x48",
"type": "image/png"
}, {
"src": "thumbnail.php?size=72&grid=<?=$currentGrid?>",
"sizes": "72x72",
"type": "image/png"
}, {
"src": "thumbnail.php?size=96&grid=<?=$currentGrid?>",
"sizes": "96x96",
"type": "image/png"
}, {
"src": "thumbnail.php?size=144&grid=<?=$currentGrid?>",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "thumbnail.php?size=168&grid=<?=$currentGrid?>",
"sizes": "168x168",
"type": "image/png"
}, {
"src": "thumbnail.php?size=192&grid=<?=$currentGrid?>",
"sizes": "192x192",
"type": "image/png"
}]
},
{
"name": "Sudoku : Grille vierge",
"short_name": "Sudoku vierge",
"description": "Grille de sudoku vierge",
"url": ".................................................................................",
"icons": [{
"src": "thumbnail.php?size=48&grid=.................................................................................",
"sizes": "48x48",
"type": "image/png"
}, {
"src": "thumbnail.php?size=72&grid=.................................................................................",
"sizes": "72x72",
"type": "image/png"
}, {
"src": "thumbnail.php?size=96&grid=.................................................................................",
"sizes": "96x96",
"type": "image/png"
}, {
"src": "thumbnail.php?size=144&grid=.................................................................................",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "thumbnail.php?size=168&grid=.................................................................................",
"sizes": "168x168",
"type": "image/png"
}, {
"src": "thumbnail.php?size=192&grid=.................................................................................",
"sizes": "192x192",
"type": "image/png"
}]
},
{
"name": "Sudoku : Nouvelle grille",
"short_name": "Nouveau sudoku",
"description": "Nouvelle grille de sudoku",
"url": ".",
"icons": [{
"src": "thumbnail.php?size=48&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "48x48",
"type": "image/png"
}, {
"src": "thumbnail.php?size=72&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "72x72",
"type": "image/png"
}, {
"src": "thumbnail.php?size=96&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "96x96",
"type": "image/png"
}, {
"src": "thumbnail.php?size=144&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "144x144",
"type": "image/png"
}, {
"src": "thumbnail.php?size=168&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "168x168",
"type": "image/png"
}, {
"src": "thumbnail.php?size=192&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
"sizes": "192x192",
"type": "image/png"
}]
}
]
}

23
nginx-example.conf Normal file
View 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/sudoku.php;
}

View File

@ -1,86 +0,0 @@
/*
Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Incrementing OFFLINE_VERSION will kick off the install event and force
// previously cached resources to be updated from the network.
const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline";
// Customize this with a different URL if needed.
const OFFLINE_URL = ".";
self.addEventListener("install", (event) => {
event.waitUntil(
(async () => {
const cache = await caches.open(CACHE_NAME);
// Setting {cache: 'reload'} in the new request will ensure that the
// response isn't fulfilled from the HTTP cache; i.e., it will be from
// the network.
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
})()
);
// Force the waiting service worker to become the active service worker.
self.skipWaiting();
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
// Enable navigation preload if it's supported.
// See https://developers.google.com/web/updates/2017/02/navigation-preload
if ("navigationPreload" in self.registration) {
await self.registration.navigationPreload.enable();
}
})()
);
// Tell the active service worker to take control of the page immediately.
self.clients.claim();
});
self.addEventListener("fetch", (event) => {
// We only want to call event.respondWith() if this is a navigation request
// for an HTML page.
if (event.request.mode === "navigate") {
event.respondWith(
(async () => {
try {
// First, try to use the navigation preload response if it's supported.
const preloadResponse = await event.preloadResponse;
if (preloadResponse) {
return preloadResponse;
}
// Always try the network first.
const networkResponse = await fetch(event.request);
return networkResponse;
} catch (error) {
// catch is only triggered if an exception is thrown, which is likely
// due to a network error.
// If fetch() returns a valid HTTP response with a response code in
// the 4xx or 5xx range, the catch() will NOT be called.
console.log("Fetch failed; returning offline page instead.", error);
const cache = await caches.open(CACHE_NAME);
const cachedResponse = await cache.match(OFFLINE_URL);
return cachedResponse;
}
})()
);
}
// If our if() condition is false, then this fetch handler won't intercept the
// request. If there are any other fetch handlers registered, they will get a
// chance to call event.respondWith(). If no fetch handlers call
// event.respondWith(), the request will be handled by the browser as if there
// were no service worker involvement.
});

342
style.css Executable file → Normal file
View File

@ -1,161 +1,213 @@
input[type="number"]::-webkit-outer-spin-button,
input::-webkit-inner-spin-button {
-webkit-appearance: none !important;
margin: 0 !important;
body {
font-family: sans-serif;
width: min-content;
margin: auto;
}
input[type="number"]::-webkit-calendar-picker-indicator {
display: none !important;
h1 {
text-align: center;
margin: 1rem;
}
table {
border-collapse: separate;
section, div, footer {
display: flex;
flex-wrap: wrap;
align-items: center;
row-gap: 0.5rem;
column-gap: 0.5rem;
margin: 0.8rem auto;
justify-content: center;
text-align: center;
}
.grid {
border-spacing: 0;
border: 1px solid black;
border-radius: 6px;
}
.grid td, tr {
padding: 0;
}
.grid tr:first-child td:first-child {
border-top-left-radius: 5px;
}
.grid tr:first-child td:first-child input {
border-top-left-radius: 4px;
}
.grid tr:first-child td:last-child {
border-top-right-radius: 5px;
}
.grid tr:first-child td:last-child input {
border-top-right-radius: 4px;
}
.grid tr:last-child td:first-child {
border-bottom-left-radius: 5px;
}
.grid tr:last-child td:first-child > input {
border-bottom-left-radius: 4px;
}
.grid tr:last-child td:last-child {
border-bottom-right-radius: 5px;
}
.grid tr:last-child td:last-child input {
border-bottom-right-radius: 4px;
}
.grid tr:nth-child(3n+1) td {
border-top: 1px solid black;
}
.grid tr:nth-child(3n+2) td {
border-top: 1px solid grey;
border-bottom: 1px solid grey;
}
.grid tr:nth-child(3n) td {
border-bottom: 1px solid black;
}
.grid td:nth-child(3n+1) {
border-left: 1px solid black;
}
.grid td:nth-child(3n+2) {
border-left: 1px solid grey;
border-right: 1px solid grey;
}
.grid td:nth-child(3n+3) {
border-right: 1px solid black;
}
table input {
.grid input {
width: 2.5rem;
height: 2.5rem;
font-size: 1.5rem;
border: 0;
border-radius: 0;
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::-webkit-calendar-picker-indicator {
display: none;
}
.grid input:enabled {
background: white;
color: darkblue;
}
.grid input:disabled, button:enabled {
color: white;
background: #6666ff;
}
.grid input.forbidden-value:enabled {
background: #ffff77;
}
.grid input.same-value:enabled {
background: #ffff33;
}
.grid input.forbidden-value:disabled {
color: #ffff99;
background: #6666ff;
}
.grid input.same-value:disabled, button.same-value:enabled {
color: #ffff99 !important;
background: #00cc66 !important;
}
.grid input.pencil, input::placeholder {
color: #888 !important;
font-size: 1rem !important;
width: 2.5rem !important;
height: 2.5rem !important;
font-size: 1.3rem !important;
padding: 0 !important;
margin: 0 !important;
text-align: center;
appearance: textfield !important;
border-radius: 0 !important;
}
table td.table-primary input,
table td.table-active input,
table.table-success input,
td.table-danger input:disabled,
table input:not([disabled]) {
background: transparent !important;
color: inherit !important;
.highlight-buttons {
column-gap: 2px;
}
button, input[type="color"] {
border: 2px outset #6666ff;
border-radius: 4px;
font-size: 1.3rem;
padding: 4px 9px 5px 9px;
margin: 0px 1px 1px 1px;
}
button:enabled:hover {
border-width: 1px;
border-style: outset;
padding: 5px 9px 5px 10px;
margin: 1px 1px 1px 2px;
}
button.pressed:enabled:hover {
border-width: 3px;
border-style: inset;
padding: 4px 6px 2px 9px;
margin: 1px 1px 1px 2px;
}
button.pressed {
border: 2px inset #00cc66;
background: #00cc66;
padding: 4px 8px 4px 9px;
margin: 1px 1px 0px 2px;
}
button:enabled:active {
border-width: 4px !important;
border-style: inset !important;
padding: 4px 4px 0px 9px !important;
margin: 0px 1px 0px 2px !important;
}
button:disabled {
color: #666;
background: darkgrey;
border: 1px outset darkgrey;
padding: 5px 10px 6px 10px;
margin: 0px 1px 1px 1px;
}
button.warning {
background: #ff5050;
border-color: #ff5050;
}
input[type="color"] {
padding: 0;
}
table input:not([disabled]):hover {
background: #9fb9b945 !important;
}
table input:disabled {
background-position: center !important;
}
tr:nth-child(3n+1) td input {
border-top-width: 3px !important;
}
tr:last-child td input {
border-bottom-width: 3px !important;
}
td:nth-child(3n+1) input {
border-left-width: 3px !important;
}
td:last-child input {
border-right-width: 3px !important;
}
tr:first-child td:first-child {
border-top-left-radius: .7rem !important;
}
tr:first-child td:first-child input {
border-top-left-radius: .5rem !important;
}
tr:first-child td:last-child {
border-top-right-radius: .7rem !important;
}
tr:first-child td:last-child input {
border-top-right-radius: .5rem !important;
}
tr:last-child td:first-child {
border-bottom-left-radius: .7rem !important;
}
tr:last-child td:first-child input {
border-bottom-left-radius: .5rem !important;
}
tr:last-child td:last-child {
border-bottom-right-radius: .7rem !important;
}
tr:last-child td:last-child input {
border-bottom-right-radius: .5rem !important;
}
td {
padding: 0 !important;
margin: 0 !important;
border: 0 !important;
}
td,
table input {
transition: background-color .4s, box-shadow .4s !important;
}
.context-menu li {
cursor: default;
}
.table-active {
cursor: inherit !important;
}
.not-allowed {
cursor: not-allowed !important;
}
table {
cursor: text;
}
button,
label {
/*! padding: .375rem .65rem !important; */
cursor: pointer;
}
button i,
label i {
margin: 0 -.125rem;
}
button:disabled,
:disabled+label {
cursor: not-allowed !important;
}
table input:enabled {
cursor: inherit;
}
.pencil {
color: var(--bs-secondary-color) !important;
}
#colorPickerInput{
width: 2.3rem;
height: auto;
padding: .375rem;
}
#colorPickerLabel {
color: var(--bs-body-color);
}
@media (prefers-color-scheme:dark) {
.pencil {
color: #5a5a5a !important;
}
a {
text-decoration: none;
}
.context-menu {
z-index: 100;
}
display: none;
z-index: 1000;
position: absolute;
overflow: hidden;
border: 1px solid #CCC;
white-space: nowrap;
font-family: sans-serif;
background: #EEE;
color: #333;
border-top-right-radius: 3px;
border-bottom-left-radius: 3px;
border-bottom-right-radius: 3px;
padding: 0;
margin: 0;
}
.context-menu li {
padding: 6px 10px;
cursor: default;
list-style-type: none;
transition: all .3s ease;
user-select: none;
font-size: 1rem;
}
.context-menu li:hover {
background-color: #DEF;
}
.context-menu li.error {
color: #888
}
.context-menu li.error:hover {
background-color: #EEE;
}

394
sudoku.js
View File

@ -1,394 +0,0 @@
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"
}

226
sudoku.php Executable file → Normal file
View File

@ -1,90 +1,150 @@
<?php
require("classes.php");
$gridStr = basename(strip_tags($_SERVER["REQUEST_URI"]));
// URL contains grid
if (preg_match("#^[1-9.]{81}$#", $gridStr)) {
?>
<!DOCTYPE html>
<html lang='fr' prefix="og: https://ogp.me/ns#">
<head>
<?php require_once("head.php") ?>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width' />
<title>Sudoku</title>
<link rel='stylesheet' type='text/css' href='style.css' />
<script src='app.js'></script>
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=57" sizes="57x57">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=114" sizes="114x114">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=72" sizes="72x72">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=144" sizes="144x144">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=60" sizes="60x60">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=120" sizes="120x120">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=76" sizes="76x76">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=152" sizes="152x152">
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$gridStr?>&size=196" sizes="196x196">
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$gridStr?>&size=160" sizes="160x160">
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$gridStr?>&size=96" sizes="96x96">
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$gridStr?>&size=16" sizes="16x16">
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$gridStr?>&size=32" sizes="32x32">
<meta property="og:title" content="Sudoku"/>
<meta property="og:type" content="website"/>
<meta property="og:url" content="<?=$_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . $_SERVER["DOCUMENT_URI"];
?>"/>
<meta property="og:image" content="<?=$_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . dirname($_SERVER["DOCUMENT_URI"])?>/thumbnail.php?grid=<?=$gridStr?>&size=200"/>
<meta property="og:image:width" content="200"/>
<meta property="og:image:height" content="200"/>
<meta property="og:description" content="Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9."/>
<meta property="og:locale" content="fr_FR"/>
<meta property="og:site_name" content="<?=$_SERVER["HTTP_HOST"]?>"/>
</head>
<body>
<nav class="navbar mb-4">
<h1 class="display-4 text-center m-auto">Sudoku</h1>
</nav>
<div class="row g-0">
<main class="col-md-6 order-md-1">
<div class="text-center m-auto" style="width: min-content;">
<div class='d-flex justify-content-between mb-2'>
<div class='btn-group'>
<input type='radio' id='inkPenRadio' class='btn-check' name='penRadioGroup' checked />
<label for='inkPenRadio' class='btn btn-primary' title='Écrire un chiffre'><i class="ri-ball-pen-fill"></i></label>
<input type='radio' id='pencilRadio' class='btn-check' name='penRadioGroup' />
<label for='pencilRadio' class='btn btn-primary' title='Prendre des notes'><i class="ri-pencil-fill"></i></label>
<input type='radio' id='eraserRadio' class='btn-check' name='penRadioGroup' />
<label for='eraserRadio' class='btn btn-primary' title='Effacer une case'><i class="ri-eraser-fill"></i></label>
</div>
<input type="color" class="btn-check" id="colorPickerInput" title="Changer la couleur" oninput="changeColor()"/>
<label id="colorPickerLabel" for="colorPickerInput" class="btn btn-primary" title="Changer de couleur"><i class="ri-palette-fill"></i></label>
<div class='btn-group'>
<input type='checkbox' id='sightCheckbox' class='btn-check' onclick='highlighterCheckbox.checked = false; highlight()' />
<label for='sightCheckbox' class='btn btn-info' title='Surligner la ligne, la colonne et la région de la case survolée'><i class="ri-focus-3-line"></i></label>
<input type='checkbox' id='highlighterCheckbox' class='btn-check' onclick='sightCheckbox.checked = false; highlight()' />
<label for='highlighterCheckbox' class='btn btn-info' title='Surligner les lignes, colonnes et régions contenant déjà le chiffre sélectionné'><i class="ri-mark-pen-fill"></i></label>
</div>
<button id="hintButton" type="button" class='btn btn-info' onclick="showHint()" title="Montrer une case avec une seule possibilité" accesskey="H" disabled=""><i class="ri-lightbulb-line"></i></button>
<a id='restartLink' class='btn btn-primary disabled' href="" title='Recommencer'><i class="ri-restart-line"></i></a>
<button id='undoButton' type='button' class='btn btn-primary' onclick='window.history.back()' disabled title='Annuler' accesskey='Z'><i class="ri-arrow-go-back-fill"></i></button>
</div>
<form id='sudokuForm' class='needs-validation' novalidate>
<table id='grid' class='table mb-2'>
<tbody>
<?php for ($row = 0; $row < 81; $row += 9): ?>
<tr class="input-group d-inline-block w-auto">
<?php for ($column = 0; $column < 9; $column++): $value = $currentGrid[$row+$column]; ?>
<?php if ($value == UNKNOWN): ?>
<td><input type='number' min='1' max='9' step='1' value='' class='form-control' /></td>
<?php else: ?>
<td><input type='number' min='1' max='9' step='1' value='<?=$value?>' class='form-control' disabled /></td>
<?php endif ?>
<?php endfor?>
</tr>
<?php endfor?>
</tbody>
</table>
</form>
<div class='d-flex mb-4'>
<div id='insertRadioGroup' class='radioGroup btn-group flex-fill'>
<input type='radio' class='btn-check' id='insertRadio0' value='' name='insertRadioGroup' onclick='insert(this)' accesskey='0' checked /><label for='insertRadio0' class='btn btn-primary' title='Clavier'><i class="ri-input-cursor-move"></i></label>
<?php for($value=1; $value<=9; $value++): ?>
<input type='radio' class='btn-check' id='insertRadio<?=$value?>' value='<?=$value?>' name='insertRadioGroup' onclick='insert(this)' accesskey='<?=$value?>' disabled />
<label for='insertRadio<?=$value?>' class='btn btn-primary' title='Insérer un <?=$value?>'><?=$value?></label>
<?php endfor ?>
</div>
</div>
<div class='mb-3'>
<?php if (isset($warning)): ?>
<strong>⚠️ <?=$warning?> ⚠️</strong><br/>
<?php else: ?>
Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.
<?php endif?>
</div>
</div>
</main>
<aside class="col-md-3 text-center text-md-start">
<div class="d-flex flex-column flex-shrink-0 p-3">
<ul class="nav nav-pills flex-column">
<li><a href="." class="nav-link link-body-emphasis">Nouvelle grille</a></li>
<li><a href="" class="nav-link link-body-emphasis">Lien vers cette grille</a></li>
<li><a href="?---------------------------------------------------------------------------------" class="nav-link link-body-emphasis">Grille vierge</a></li>
<li><a id="fixGridLink" href="" class="nav-link link-body-emphasis">Figer la grille</a></li>
<li><a href="https://git.malingrey.fr/adrien/Sudoku" class="nav-link link-body-emphasis">Code source</a></li>
<li><a href=".." class="nav-link link-body-emphasis">Autres jeux</a></li>
</ul>
</div>
</aside>
</div>
<ul id='contextMenu' class='context-menu modal-content shadow list-group w-auto position-absolute'></ul>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
<script src='sudoku.js' defer></script>
<script>navigator?.serviceWorker.register('service-worker.js')</script>
<header>
<h1>Sudoku</h1>
</header>
<section>
Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.
</section>
<form id='sudokuForm'>
<div>
<table id='grid' class='grid'>
<tbody>
<?php
for ($row = 0; $row < 9; $row++) {
?>
<tr>
<?php
for ($column = 0; $column < 9; $column++) {
$value = $gridStr[9*$row+$column];
?>
<td>
<?php
if ($value == UNKNOWN) {
?>
<input type='number' min='1' max='9' step='1' value='' title='Valeurs possibles [Clic-droit]'/>
<?php
} else {
?>
<input type='number' min='1' max='9' step='1' value='<?=$value?>' disabled/>
<?php
}
?>
</td>
<?php
}
?>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<div id='buttons' class='highlight-buttons'>
<?php
for($value=1; $value<=9; $value++) {
echo " <button type='button' onclick='highlight(\"$value\")' title='Surligner les $value' accesskey='$value'>$value</button>\n";
}
?>
</div>
<div>
<button id='inkPenButton' type='button' onclick='useInkPen()' title='Stylo' class='pressed'>
<img src="img/ink-pen.png" alt='Stylo' width=16 height=16/>
</button>
<button id='pencilButton' type='button' onclick='usePencil()' title='Crayon'>
<img src="img/pencil.png" alt='Crayon' width=16 height=16/>
</button>
<button type='button' onclick='erasePencil()' title='Effacer le crayon'>
<img src="img/pencil-eraser.png" alt="Gomme blanche" width=16 height=16/>
</button>
<button class="warning" type='button' onclick='eraseAll()' title='Effacer tout'>
<img src="img/ink-eraser.png" alt="Gomme bleue" width=16 height=16/>
</button>
<button id='undoButton' type='button' onclick='undo()' disabled title='Annuler' accesskey='z'>
<img src="img/undo.png" alt="Annuler" width=16 height=16/>
</button>
<!--<input id='colorPicker' type='color' title='Changer de couleur de stylo' value='#00008b'/> -->
</div>
</form>
<ul id="contextMenu" class="context-menu">
</ul>
<footer>
<a href=''>Lien vers cette grille</a><br/>
<a href='.'>Nouvelle grille</a>
</footer>
</body>
</html>
<?php
} else {
$grid = new Grid();
$grid->generate();
</html>
header("HTTP/1.0 400 Bad Request", true, 400);
$urlDir = $_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . dirname($_SERVER["DOCUMENT_URI"]);
$urlExample = $urlDir . "/" . $grid->toString();
?>
<!DOCTYPE html>
<html lang='fr'>
<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' />
<link rel="icon" type="image/png" href="favicon.png">
</head>
<body>
<header>
<h1>Grille incorrecte</h1>
</header>
L'adresse URL doit être de la forme : <?=$urlDir?>/<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>un point pour les case vides</li>
</ul>
Exemple : <a href='<?=$urlExample?>'><?=$urlExample?></a><br/>
</body>
</html>
<?php
}
?>

3
test.php Normal file
View File

@ -0,0 +1,3 @@
<?php
echo $_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . $_SERVER["DOCUMENT_URI"];
?>

64
thumbnail.php Executable file → Normal file
View File

@ -1,23 +1,15 @@
<?php
require("classes.php");
if (isset($_GET["grid"]) && preg_match("/^[1-9-]{81}$/", $_GET["grid"]))
$currentGrid = $_GET["grid"];
else
$currentGrid = "-528-3----4-9-1---39-562------73-129---1-64-7---42-3656-13-5---28-6-4---4-5287---";
header ("Content-type: image/png");
if (isset($_GET['size']))
$size = (int) $_GET['size'];
else
$size = 196;
const UNKNOWN = ".";
$gridStr = strip_tags($_GET['grid']);
$size = (int) $_GET['size'];
$thumbnail = imagecreate($size, $size);
$transparent = imagecolorallocate($thumbnail, 1, 1, 1);
imagecolortransparent($thumbnail, $transparent);
$darkerBorder = imagecolorallocate($thumbnail, 150, 155, 160);
$lighterBorder = imagecolorallocate($thumbnail, 210, 225, 230);
$emptyBoxBC = imagecolorallocate($thumbnail, 255, 255, 255);
$clueBC = imagecolorallocate($thumbnail, 255, 255, 255);
$clueFC = imagecolorallocate($thumbnail, 150, 155, 160);
$black = imagecolorallocate($thumbnail, 0, 0, 0);
$grey = imagecolorallocate($thumbnail, 128, 128, 128);
$blue = imagecolorallocate($thumbnail, 102, 102, 255);
$white = imagecolorallocate($thumbnail, 255, 255, 255);
if ($size <= 36) {
$boxSize = floor(($size-4) / 9);
@ -27,19 +19,19 @@
$lineStart = $start + 1;
$lineEnd = $end - 2;
for ($i = $start; $i < $end; $i += 3*$boxSize + 1) {
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $darkerBorder);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $darkerBorder);
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $black);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $black);
}
$x = $start;
$y = $start;
$boxSizeMinusOne = $boxSize - 1;
foreach(str_split($currentGrid) as $i => $value) {
foreach(str_split($gridStr) as $i => $value) {
if ($i % 3 == 0) $x++;
if ($i % 27 == 0) $y++;
if ($value == UNKNOWN) {
$bgColor = $emptyBoxBC;
$bgColor = $white;
} else {
$bgColor = $clueFC;
$bgColor = $blue;
}
imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusOne, $y+$boxSizeMinusOne, $bgColor);
$x += $boxSize;
@ -56,21 +48,21 @@
$lineStart = $start + 1;
$lineEnd = $end - 2;
for ($i = $start + $boxSize; $i < $end - $boxSize; $i += $boxSize) {
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $lighterBorder);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $lighterBorder);
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $grey);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $grey);
}
for ($i = $start; $i < $end; $i += 3*$boxSize) {
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $darkerBorder);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $darkerBorder);
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $black);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $black);
}
$x = $start + 1;
$y = $start + 1;
$boxSizeMinusTwo = $boxSize - 2;
foreach(str_split($currentGrid) as $i => $value) {
foreach(str_split($gridStr) as $i => $value) {
if ($value == UNKNOWN) {
$bgColor = $emptyBoxBC;
$bgColor = $white;
} else {
$bgColor = $clueFC;
$bgColor = $blue;
}
imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusTwo, $y+$boxSizeMinusTwo, $bgColor);
$x += $boxSize;
@ -89,26 +81,26 @@
$fontSize = floor($boxSize/2) - 4;
$fdx = floor(($boxSize - imagefontwidth($fontSize)) / 2);
$fdy = ceil(($boxSize - imagefontheight($fontSize)) / 2) - 1;
$fontColor = $emptyBoxBC;
$fontColor = $white;
for ($i = $start + $boxSize; $i < $end - $boxSize; $i += $boxSize) {
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $lighterBorder);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $lighterBorder);
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $grey);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $grey);
}
for ($i = $start; $i < $end; $i += 3*$boxSize) {
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $darkerBorder);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $darkerBorder);
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $black);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $black);
}
$x = $start + 1;
$y = $start + 1;
$boxSizeMinusTwo = $boxSize - 2;
foreach(str_split($currentGrid) as $i => $value) {
foreach(str_split($gridStr) as $i => $value) {
if ($value == UNKNOWN) {
$bgColor = $emptyBoxBC;
$bgColor = $white;
} else {
$bgColor = $clueBC;
$bgColor = $blue;
}
imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusTwo, $y+$boxSizeMinusTwo, $bgColor);
if ($value != UNKNOWN) imagestring($thumbnail, $fontSize, $x + $fdx, $y + $fdy, $value, $clueFC);
if ($value != UNKNOWN) imagestring($thumbnail, $fontSize, $x + $fdx, $y + $fdy, $value, $fontColor);
$x += $boxSize;
if ($i % 9 == 8) {
$y += $boxSize;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB