Compare commits

..

83 Commits

Author SHA1 Message Date
c477355fe9 Ajouter README.md 2025-05-21 10:38:18 +02:00
f52c970bef meta 2025-05-09 11:50:32 +02:00
cf0a5a465e fix 2025-05-09 11:13:29 +02:00
310a1883d2 declare properties, use session without cookies 2025-05-06 18:20:08 +02:00
4b92464c94 format 2025-05-05 23:18:26 +02:00
c09ed80a52 serviceWorker in html 2024-05-12 11:17:03 +02:00
b145ae566a hover 2024-04-09 01:38:42 +02:00
21e8f4134f auto import 2023-12-23 13:25:04 +01:00
31a40a7e93 format 2023-12-23 13:24:44 +01:00
ad3992ac30 navbar 2023-12-23 13:24:22 +01:00
5feaa65955 global $sudokuGridSolutions 2023-12-23 13:24:12 +01:00
ed5795a6cc save directly nbsolutions in $_SESSION 2023-12-12 18:36:37 +01:00
bdb1a094c3 back to bootstrap dark 2023-12-11 09:13:35 +01:00
85efaca248 color: inherit 2023-12-08 01:17:20 +01:00
39c564fb89 $_SESSION["sudokuGridSolutions"] 2023-12-08 01:17:06 +01:00
ba07a531d0 save nbSolutions in session 2023-12-06 15:10:06 +01:00
f510310549 use bootstrap default color theme switcher 2023-12-06 12:08:45 +01:00
5091e6a888 update 400.php style 2023-12-06 11:02:46 +01:00
67cd3594a6 fix context-menu z-index 2023-12-06 10:38:57 +01:00
4027a8b36f md 2023-12-05 17:36:00 +01:00
e1fc974372 links on aside 2023-12-05 17:32:40 +01:00
853d93f8ff fixes 2023-11-25 22:38:03 +01:00
ddc1a51899 sight fixes 2023-11-25 22:37:44 +01:00
77886a6878 fix area on showDuplicates 2023-11-25 21:51:56 +01:00
80bc9f083d refactor with bind 2023-11-25 21:40:35 +01:00
1365ef65dd optimize with sort 2023-11-25 19:55:59 +01:00
ad414d0da8 refactor 2023-11-25 19:03:21 +01:00
57d06c3b53 fix check on onpopstate 2023-11-25 18:47:11 +01:00
84d9222a1c undo button is back 2023-11-25 11:58:18 +01:00
fa979cb973 use window.history 2023-11-25 04:40:40 +01:00
7245e0f073 ternary accessKeyModifiers 2023-11-02 10:37:07 +01:00
2429845dd6 navigator.platform is depreciated 2023-11-02 10:28:39 +01:00
956966fbe9 html format 2023-11-02 08:45:59 +01:00
ddcabbdd39 html format 2023-11-02 08:40:13 +01:00
743f7fa72d format 2023-10-31 03:29:02 +01:00
3a243c38f0 color picker icon 2023-10-23 19:28:03 +02:00
beebb14464 improve color picker 2023-10-23 17:25:21 +02:00
24806a289f improve color picker 2023-10-23 16:43:19 +02:00
4a0d03f445 add color picker 2023-10-23 09:16:46 +02:00
2e5125c298 uncheck radio if clicked twice 2023-10-17 23:33:39 +02:00
959deb5e14 icon margin 2023-06-21 08:38:19 +02:00
562cd7964b negative icon margin 2023-06-21 08:25:20 +02:00
e437a01e30 more button padding 2023-06-18 12:32:00 +02:00
f811000ea0 less button padding 2023-06-18 12:08:43 +02:00
35f1bee053 update thumbnail 2023-04-30 10:59:58 +02:00
79a20f323a update manifest 2023-04-30 10:27:41 +02:00
e80fef6c08 force favicon and manifest reload 2023-04-30 03:43:18 +02:00
7c65480bdd CDN 2023-04-22 16:14:46 +02:00
7043fb8bb6 CDN 2023-04-22 16:09:00 +02:00
15ed790caf smartphone friendly 2023-04-21 03:40:11 +02:00
c0c815e757 new thumbnail 2023-04-21 01:53:16 +02:00
5bd46082d5 move insertRadio0 2023-04-16 16:11:09 +02:00
de7b013716 text cursor 2023-04-16 15:14:08 +02:00
dab898c391 focus 2023-04-16 03:37:24 +02:00
f53e09b52e remix icons 2023-04-16 03:36:30 +02:00
176e72c465 small changes 2023-04-05 21:54:42 +02:00
cedd63dd90 source code 2023-03-31 02:33:57 +02:00
60b5f74e94 restart on no hash 2023-03-31 02:22:07 +02:00
c47cd498c0 fix onhashchange 2023-03-31 02:15:39 +02:00
6ee740cebd onhashchange 2023-03-31 01:55:12 +02:00
785f1460fb save to location.hash 2023-03-31 01:47:22 +02:00
80fa25c092 fix disable insertRadio 2023-03-31 01:35:47 +02:00
ac91453013 table-success 2023-03-30 21:26:14 +02:00
8c62d6bb0e text insert radio to the right 2023-03-30 19:38:36 +02:00
8f412bdd0f keyboard button 2023-03-30 03:42:32 +02:00
dee27c9ca7 ui 2023-03-30 02:55:15 +02:00
0f83b8de42 Merge branch 'master' of https://git.malingrey.fr/adrien/sudoku 2023-03-30 02:23:06 +02:00
ab11f85215 add sight function 2023-03-30 02:22:59 +02:00
9191b6836c fix insertRadio filter 2023-03-30 01:21:34 +02:00
b5345e1a4d optimize 2023-03-30 01:12:02 +02:00
fa970170d4 bootstrap css 2023-03-29 23:28:48 +02:00
573608b63c alternate stylesheet 2021-11-08 19:19:27 +01:00
ca22cb129d V2.8 2021-04-17 15:55:41 +02:00
80d368446a Merge branch 'master' of https://git.malingrey.fr/adrien/sudoku 2020-11-23 00:02:44 +01:00
f7d6dbf3f3 save at will, not automatically 2020-11-23 00:02:12 +01:00
cfc535b772 save at will (not automatically) 2020-11-22 23:53:57 +01:00
019ce058cd small changes 2020-11-18 02:59:18 +01:00
a75b898ea6 little enhancements 2020-11-16 20:21:30 +01:00
cd22ded40e show same value even when highlight disabled 2020-11-16 03:45:37 +01:00
5836f53a24 manage url args 2020-11-14 12:45:49 +01:00
9b2f1f9d78 freeze grid, fix containsDuplicates 2020-11-14 01:24:43 +01:00
3b8e9b85ea fixes 2020-11-14 00:41:03 +01:00
8121768fe4 rename to .php 2020-11-14 00:00:44 +01:00
21 changed files with 708 additions and 988 deletions

27
400.php Executable file
View File

@ -0,0 +1,27 @@
<?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>

32
404.php
View File

@ -1,32 +0,0 @@
<?php
require("classes.php");
$grid = new Grid();
$grid->generate();
header("HTTP/1.0 404 Not Found", true, 404);
$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>Sudoku non trouvé</title>
<link rel='stylesheet' type='text/css' href='style.css' />
<link rel="icon" type="image/png" href="favicon.png">
</head>
<body>
<header>
<h1>#404</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>

5
README.md Normal file
View File

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

136
classes.php Normal file → Executable file
View File

@ -1,5 +1,7 @@
<?php <?php
const UNKNOWN = "."; const UNKNOWN = "-";
$validGrids = array();
function isKnown($box) { function isKnown($box) {
return $box->value != UNKNOWN; return $box->value != UNKNOWN;
@ -24,16 +26,20 @@
} }
class Box { class Box {
public $values = array("1", "2", "3", "4", "5", "6", "7", "8", "9"); 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();
function __construct($rowId, $columnId, $regionId) { function __construct($rowId, $columnId, $regionId) {
$this->value = UNKNOWN;
$this->rowId = $rowId; $this->rowId = $rowId;
$this->columnId = $columnId; $this->columnId = $columnId;
$this->regionId = $regionId; $this->regionId = $regionId;
$this->candidates = $this->values; $this->candidates = $this->values;
$this->candidateRemoved = array();
$this->neighbourhood = array();
} }
function searchCandidates() { function searchCandidates() {
@ -46,8 +52,13 @@
} }
class Grid { class Grid {
function __construct() {
$this->boxes = array(); private $boxes = array();
private $rows;
private $columns;
private $regions;
function __construct($gridStr="") {
$this->rows = array_fill(0, 9, array()); $this->rows = array_fill(0, 9, array());
$this->columns = array_fill(0, 9, array()); $this->columns = array_fill(0, 9, array());
$this->regions = array_fill(0, 9, array()); $this->regions = array_fill(0, 9, array());
@ -72,6 +83,12 @@
if ($box != $neighbour && !in_array($neighbour, $box->neighbourhood)) if ($box != $neighbour && !in_array($neighbour, $box->neighbourhood))
$box->neighbourhood[] = $neighbour; $box->neighbourhood[] = $neighbour;
} }
if ($gridStr) {
$this->import($gridStr);
} else {
$this->generate();
}
} }
function import($gridStr) { function import($gridStr) {
@ -84,20 +101,6 @@
} }
} }
function containsDuplicates() {
foreach(array_merge($this->rows, $this->columns, $this->regions) as $area) {
$unknownBoxes = array_filter($area, "isUnknown");
foreach($unknownBoxes as $box1) {
foreach($unknownBoxes as $box2) {
if (($box1 !== $box2) && ($box1->value == $box2->value)) {
return true;
}
}
}
return false;
}
}
function generate() { function generate() {
// Init with a shuffle row // Init with a shuffle row
$values = array("1", "2", "3", "4", "5", "6", "7", "8", "9"); $values = array("1", "2", "3", "4", "5", "6", "7", "8", "9");
@ -110,34 +113,59 @@
// Fill grid // Fill grid
$this->solutionsGenerator(true)->current(); $this->solutionsGenerator(true)->current();
// Remove clues while there is still a unique solution // Group boxes with their groupedSymetricals
shuffle($this->boxes); $groupedSymetricals = array(array($this->rows[4][4]));
$nbClues = count($this->boxes); for ($rowId = 0; $rowId <= 3; $rowId++) {
foreach($this->boxes as $testBox) { for ($columnId = 0; $columnId <= 3; $columnId++) {
$testBoxes = array($testBox); $groupedSymetricals[] = array(
if ($nbClues >=30) $this->rows[$rowId][$columnId],
$testBoxes[] = $this->rows[8-$testBox->rowId][8-$testBox->columnId]; $this->rows[8-$rowId][8-$columnId],
if ($nbClues >=61) { $this->rows[8-$rowId][$columnId],
$testBoxes[] = $this->rows[8-$testBox->rowId][$testBox->columnId]; $this->rows[$rowId][8-$columnId]
$testBoxes[] = $this->rows[$testBox->rowId][8-$testBox->columnId]; );
} }
$testBoxes = array_filter($testBoxes, "isKnown"); $groupedSymetricals[] = array(
$erasedValues = array(); $this->rows[$rowId][4],
forEach($testBoxes as $testBox) { $this->rows[8-$rowId][4]
$erasedValues[] = $testBox->value; );
}
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;
$testBox->value = UNKNOWN; $testBox->value = UNKNOWN;
forEach($testBox->neighbourhood as $neighbour) forEach($testBox->neighbourhood as $neighbour)
$neighbour->searchCandidates(); $neighbour->searchCandidates();
} if (!$this->isValid()) {
if ($this->isValid()) { $testBox->value = $erasedValue;
$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); 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) { function countSolutions($max=2) {
@ -145,10 +173,13 @@
$solutionsWithoutDuplicates = array(); $solutionsWithoutDuplicates = array();
$nbSolutions = 0; $nbSolutions = 0;
foreach($solutions as $solution) { foreach($solutions as $solution) {
$solutionsWithoutDuplicates[$solution] = true; if (!in_array($solution, $solutionsWithoutDuplicates)) {
$nbSolutions = count($solutionsWithoutDuplicates); $solutionsWithoutDuplicates[] = $solution;
if ($nbSolutions >= $max) { $nbSolutions ++;
$solutions->send(true); if ($nbSolutions >= $max) {
$solutions->send(true);
break;
}
} }
} }
return $nbSolutions; return $nbSolutions;
@ -164,7 +195,6 @@
if ($randomized) shuffle($emptyBoxes); if ($randomized) shuffle($emptyBoxes);
usort($emptyBoxes, "easyFirst"); usort($emptyBoxes, "easyFirst");
$testBox = $emptyBoxes[0]; $testBox = $emptyBoxes[0];
$nbTries = 0;
if ($randomized) shuffle($testBox->candidates); if ($randomized) shuffle($testBox->candidates);
$stop = null; $stop = null;
foreach($testBox->candidates as $testBox->value) { foreach($testBox->candidates as $testBox->value) {
@ -187,20 +217,6 @@
} }
$testBox->value = UNKNOWN; $testBox->value = UNKNOWN;
} else { } else {
foreach(array($this->rows, $this->columns, $this->regions) as $areas) {
foreach ($areas as $area) {
foreach($area as $box1) {
if (($box1->value == UNKNOWN) && (count($box1->candidates) == 0)) {
return;
}
foreach($area as $box2) {
if (($box1 !== $box2) && ($box1->value != UNKNOWN) && ($box1->value == $box2->value)) {
return;
}
}
}
}
}
yield $this->toString(); yield $this->toString();
} }
} }

0
favicon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 542 B

32
head.php Executable file
View File

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

View File

@ -1 +0,0 @@
<svg id="Capa_1" enable-background="new 0 0 512 512" height="24" viewBox="0 0 512 512" width="24" xmlns="http://www.w3.org/2000/svg"><g><path d="m214.28 498.762c4.098 0 8.029-1.628 10.927-4.526l278.144-278.144c11.398-11.398 11.398-29.879 0-41.277l-152.928-152.928c-11.398-11.398-29.878-11.398-41.277 0l-300.597 300.598c-11.398 11.398-11.398 29.878 0 41.277l130.474 130.474c2.898 2.898 6.828 4.526 10.927 4.526z" fill="#d3e1f5"/><path d="m275.208 386.418-78.91 78.91c-2.898 2.898-6.828 4.526-10.927 4.526h-64.331c-4.098 0-8.029-1.628-10.927-4.526l28.909 28.909c2.898 2.898 6.829 4.526 10.927 4.526h64.331c4.098 0 8.029-1.628 10.927-4.526l78.91-78.91z" fill="#c0d6f2"/><path d="m505.059 176.523-156.344-156.344c-10.455-10.455-28.935-8.925-41.277 3.417l-228.75 228.75 194.205 194.204 228.75-228.75c12.341-12.342 13.871-30.822 3.416-41.277z" fill="#0473ce"/><path d="m187.955 245.061 134.545-134.545c4.023-4.023 10.546-4.023 14.569 0l77.653 77.653c4.023 4.023 4.023 10.546 0 14.569l-134.545 134.545c-4.023 4.023-10.546 4.023-14.569 0l-77.653-77.653c-4.023-4.023-4.023-10.546 0-14.569z" fill="#0055a3"/><path d="m505.059 176.523-28.908-28.909c10.455 10.455 8.925 28.935-3.417 41.277l-228.75 228.75 28.909 28.909 228.75-228.75c12.341-12.342 13.871-30.822 3.416-41.277z" fill="#0067c5"/></g></svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,44 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<polygon style="fill:#FFCC75;" points="47.202,417.53 94.404,464.732 47.202,511.945 0,464.743 "/>
<polygon style="fill:#082947;" points="236.149,414.811 212.545,438.4 111.564,368.892 166.634,313.823 "/>
<polygon style="fill:#274B6D;" points="182.37,329.559 127.301,384.628 73.529,299.384 97.126,275.788 "/>
<polygon style="fill:#FF9D49;" points="512,118.078 452.926,59.003 294.044,186.413 166.634,345.296 251.886,430.548 "/>
<path style="fill:#274B6D;" d="M143.037,368.892l69.508,69.508c0,0-61.136,0.802-102.397,42.063l-39.35-39.333l20.383-51.855
L143.037,368.892z"/>
<polygon style="fill:#FFCC75;" points="393.977,0.055 81.39,260.051 166.634,345.296 452.926,59.003 "/>
<path style="fill:#185F8D;" d="M73.529,299.384l69.508,69.508l-72.239,72.239L31.458,401.79
C72.719,360.529,73.529,299.384,73.529,299.384z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,48 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 511.989 511.989" style="enable-background:new 0 0 511.989 511.989;" xml:space="preserve" width="24" height="24">
<rect x="408.399" y="26.854" transform="matrix(-0.7071 0.7071 -0.7071 -0.7071 819.7593 -231.0359)" style="fill:#9EC8E8;" width="98.659" height="54.811"/>
<polygon style="fill:#082947;" points="430.604,174.402 186.029,418.976 124.017,372.468 376.343,120.141 "/>
<polygon style="fill:#274B6D;" points="384.094,127.892 131.769,380.219 93.012,325.958 337.586,81.384 "/>
<polygon style="fill:#4284B2;" points="477.112,127.892 430.604,174.402 376.343,120.141 430.604,81.384 "/>
<rect x="20.377" y="439.543" transform="matrix(-0.7071 -0.7071 0.7071 -0.7071 -273.1405 837.2019)" style="fill:#082947;" width="32.886" height="71.254"/>
<polygon style="fill:#4284B2;" points="186.029,418.976 69.757,488.74 46.502,465.484 77.509,403.473 139.52,372.468 "/>
<g>
<polygon style="fill:#6DA8D6;" points="139.52,372.468 46.502,465.484 23.248,442.23 93.012,325.958 "/>
<rect x="351.209" y="48.511" transform="matrix(-0.7071 0.7071 -0.7071 -0.7071 713.2485 -132.6418)" style="fill:#6DA8D6;" width="65.773" height="65.773"/>
</g>
<polygon style="fill:#9EC8E8;" points="178.681,193.781 155.426,170.527 302.704,23.249 442.229,162.774 418.975,186.028
302.704,69.757 "/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,45 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve" width="24" height="24">
<polygon style="fill:#FF9D49;" points="393.29,213.665 142.441,464.524 87.044,424.954 345.817,150.364 "/>
<polygon style="fill:#FFCC75;" points="353.73,158.277 87.044,424.954 47.473,369.557 298.333,118.708 "/>
<polygon style="fill:#BD1515;" points="512,94.965 440.78,166.185 377.475,118.708 464.522,47.486 "/>
<polygon style="fill:#F2484B;" points="464.516,47.482 385.382,126.615 345.817,71.224 417.038,0.002 "/>
<g>
<rect x="335.974" y="75.292" transform="matrix(-0.7071 0.7071 -0.7071 -0.7071 731.5841 -18.1559)" style="fill:#082947;" width="67.157" height="134.292"/>
<polygon style="fill:#082947;" points="71.22,488.261 0,511.998 23.736,440.776 63.305,448.692 "/>
</g>
<polygon style="fill:#F2EBD9;" points="47.473,369.557 23.736,440.776 71.22,488.261 142.441,464.524 "/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,55 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="574.859px" height="574.86px" viewBox="0 0 574.859 574.86" style="enable-background:new 0 0 574.859 574.86;"
xml:space="preserve">
<g>
<path d="M181.688,521.185V353.841H19.125v167.344c0,10.566,13.34,23.906,23.906,23.906h124.312
C177.91,545.091,181.688,531.751,181.688,521.185z M66.938,502.06c0,2.64-2.142,4.781-4.781,4.781s-4.781-2.142-4.781-4.781
V377.748c0-2.64,2.142-4.781,4.781-4.781s4.781,2.142,4.781,4.781V502.06z M105.188,502.06c0,2.64-2.142,4.781-4.781,4.781
s-4.781-2.142-4.781-4.781V377.748c0-2.64,2.142-4.781,4.781-4.781s4.781,2.142,4.781,4.781V502.06z M143.438,502.06
c0,2.64-2.142,4.781-4.781,4.781s-4.781-2.142-4.781-4.781V377.748c0-2.64,2.142-4.781,4.781-4.781s4.781,2.142,4.781,4.781V502.06
z"/>
<path d="M19.125,334.716h162.562v-19.125h19.125v-19.125h-57.375c0-10.566-6.828-19.125-15.243-19.125H77.399
c-8.415,0-15.243,8.559-15.243,19.125H0v19.125h19.125V334.716z"/>
<path d="M357.007,191.556C370.968,329.811,243.892,542.08,243.892,542.08c145.235-78.212,169.189-207.363,169.189-207.363
c42.333,66.479,44.475,228.305,44.475,228.305c80.995-194.109,0-377.049,0-377.049l117.304,48.874
c-19.546-74.014-141.047-125.68-141.047-125.68c-110.322,50.27-249.974,44.686-249.974,44.686
C259.249,226.469,357.007,191.556,357.007,191.556z"/>
<circle cx="369.782" cy="55.128" r="43.29"/>
<path d="M94.43,229.529c5.977-2.391,27.492-13.148,28.764,0c1.271,13.148,11.876,9.562,19.048,0s3.586-25.102,11.953-23.906
s15.539-10.758,17.93-21.735c2.391-10.978-22.711-18.905-33.469-21.458s-20.32,13.321-27.492,13.321s-17.93-20.33-25.102-10.768
s-11.953,40.641-11.953,40.641c-10.758-5.977-21.516,7.172-25.102,16.734S88.453,231.919,94.43,229.529z"/>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.2 KiB

View File

@ -1,46 +0,0 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 16.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="299.021px" height="299.021px" viewBox="0 0 299.021 299.021" style="enable-background:new 0 0 299.021 299.021;"
xml:space="preserve">
<g>
<g>
<path d="M292.866,254.432c-2.288,0-4.443-1.285-5.5-3.399c-0.354-0.684-28.541-52.949-146.169-54.727v51.977
c0,2.342-1.333,4.48-3.432,5.513c-2.096,1.033-4.594,0.793-6.461-0.63L2.417,154.392C0.898,153.227,0,151.425,0,149.516
c0-1.919,0.898-3.72,2.417-4.888l128.893-98.77c1.87-1.426,4.365-1.667,6.461-0.639c2.099,1.026,3.432,3.173,3.432,5.509v54.776
c3.111-0.198,7.164-0.37,11.947-0.37c43.861,0,145.871,13.952,145.871,143.136c0,2.858-1.964,5.344-4.75,5.993
C293.802,254.384,293.34,254.432,292.866,254.432z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

49
index.php Normal file → Executable file
View File

@ -1,10 +1,45 @@
<?php <?php
require("classes.php"); require("classes.php");
session_start();
$grid = new Grid(); $fullUrl = $_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].$_SERVER["DOCUMENT_URI"];
$grid->generate(); $dirUrl = dirname($fullUrl);
$currentGrid = $grid->toString(); $currentGrid = strip_tags($_SERVER['QUERY_STRING']);
$_SESSION[$currentGrid] = "checked";
header("Location: ".$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].dirname($_SERVER["DOCUMENT_URI"])."/".$currentGrid); if (preg_match("/^[1-9-]{81}$/", $currentGrid)) {
exit(); 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");
}
}
?> ?>

53
manifest.json.php → manifest.php Normal file → Executable file
View File

@ -1,7 +1,6 @@
<?php <?php
session_start(); if (isset($_GET["grid"]))
if ($_SESSION["currentGrid"]) $currentGrid = $_GET["grid"];
$currentGrid = $_SESSION["currentGrid"];
else else
$currentGrid = "."; $currentGrid = ".";
?> ?>
@ -10,27 +9,27 @@
"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.", "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": [{ "icons": [{
"src": "thumbnail.png?size=48", "src": "thumbnail.php?size=48&grid=<?=$currentGrid?>",
"sizes": "48x48", "sizes": "48x48",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=72", "src": "thumbnail.php?size=72&grid=<?=$currentGrid?>",
"sizes": "72x72", "sizes": "72x72",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=96", "src": "thumbnail.php?size=96&grid=<?=$currentGrid?>",
"sizes": "96x96", "sizes": "96x96",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=144", "src": "thumbnail.php?size=144&grid=<?=$currentGrid?>",
"sizes": "144x144", "sizes": "144x144",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=168", "src": "thumbnail.php?size=168&grid=<?=$currentGrid?>",
"sizes": "168x168", "sizes": "168x168",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=192", "src": "thumbnail.php?size=192&grid=<?=$currentGrid?>",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}], }],
@ -47,27 +46,27 @@
"description": "Continuer cette grille de sudoku", "description": "Continuer cette grille de sudoku",
"url": "<?=$currentGrid?>", "url": "<?=$currentGrid?>",
"icons": [{ "icons": [{
"src": "thumbnail.png?size=48", "src": "thumbnail.php?size=48&grid=<?=$currentGrid?>",
"sizes": "48x48", "sizes": "48x48",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=72", "src": "thumbnail.php?size=72&grid=<?=$currentGrid?>",
"sizes": "72x72", "sizes": "72x72",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=96", "src": "thumbnail.php?size=96&grid=<?=$currentGrid?>",
"sizes": "96x96", "sizes": "96x96",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=144", "src": "thumbnail.php?size=144&grid=<?=$currentGrid?>",
"sizes": "144x144", "sizes": "144x144",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=168", "src": "thumbnail.php?size=168&grid=<?=$currentGrid?>",
"sizes": "168x168", "sizes": "168x168",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=192", "src": "thumbnail.php?size=192&grid=<?=$currentGrid?>",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}] }]
@ -78,27 +77,27 @@
"description": "Grille de sudoku vierge", "description": "Grille de sudoku vierge",
"url": ".................................................................................", "url": ".................................................................................",
"icons": [{ "icons": [{
"src": "thumbnail.png?size=48", "src": "thumbnail.php?size=48&grid=.................................................................................",
"sizes": "48x48", "sizes": "48x48",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=72", "src": "thumbnail.php?size=72&grid=.................................................................................",
"sizes": "72x72", "sizes": "72x72",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=96", "src": "thumbnail.php?size=96&grid=.................................................................................",
"sizes": "96x96", "sizes": "96x96",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=144", "src": "thumbnail.php?size=144&grid=.................................................................................",
"sizes": "144x144", "sizes": "144x144",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=168", "src": "thumbnail.php?size=168&grid=.................................................................................",
"sizes": "168x168", "sizes": "168x168",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=192", "src": "thumbnail.php?size=192&grid=.................................................................................",
"sizes": "192x192", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}] }]
@ -109,27 +108,27 @@
"description": "Nouvelle grille de sudoku", "description": "Nouvelle grille de sudoku",
"url": ".", "url": ".",
"icons": [{ "icons": [{
"src": "thumbnail.png?size=48", "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", "sizes": "48x48",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=72", "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", "sizes": "72x72",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=96", "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", "sizes": "96x96",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=144", "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", "sizes": "144x144",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=168", "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", "sizes": "168x168",
"type": "image/png" "type": "image/png"
}, { }, {
"src": "thumbnail.png?size=192", "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", "sizes": "192x192",
"type": "image/png" "type": "image/png"
}] }]

View File

@ -1,35 +0,0 @@
location /sudoku/ {
alias /var/www/sudoku/;
if ($scheme = http) {
rewrite ^ https://$server_name$request_uri? permanent;
}
index index.php;
try_files $uri $uri/ @add-php-extention;
location ~ [^/]\.php(/|$) {
try_files $uri =404;
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;
}
error_page 404 /sudoku/404.php;
location /sudoku/404.php {
internal;
}
}
location ~ "^/sudoku/([1-9.]{81})$" {
rewrite "^/sudoku/([1-9.]{81})$" /sudoku/sudoku.php?grid=$1 last;
}
location @add-php-extention {
rewrite ^([^?#]*)(\?.*)?(#.*)?$ $1.php$2$3 last;
}

10
service-worker.js.php → service-worker.js Normal file → Executable file
View File

@ -1,11 +1,3 @@
<?php
session_start();
if ($_SESSION["currentGrid"])
$currentGrid = $_SESSION["currentGrid"];
else
$currentGrid = ".";
header ("Content-type: application/javascript");
?>
/* /*
Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved. Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
Licensed under the Apache License, Version 2.0 (the "License"); Licensed under the Apache License, Version 2.0 (the "License");
@ -24,7 +16,7 @@ Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
const OFFLINE_VERSION = 1; const OFFLINE_VERSION = 1;
const CACHE_NAME = "offline"; const CACHE_NAME = "offline";
// Customize this with a different URL if needed. // Customize this with a different URL if needed.
const OFFLINE_URL = "<?=$currentGrid?>"; const OFFLINE_URL = ".";
self.addEventListener("install", (event) => { self.addEventListener("install", (event) => {
event.waitUntil( event.waitUntil(

372
style.css Normal file → Executable file
View File

@ -1,243 +1,161 @@
body {
font-family: sans-serif;
width: min-content;
margin: auto;
}
h1 {
text-align: center;
margin: 1rem;
}
section, div, footer {
align-items: center;
align-items: center;
justify-content: center;
text-align: center;
margin: 0.8rem 0;
}
div {
display: flex;
flex-wrap: wrap;
row-gap: 0.5rem;
column-gap: 0.3rem;
margin: 0.5rem auto;
}
.grid {
border-spacing: 0;
border: 1px solid black;
border-radius: 6px;
margin: auto;
cursor: url(img/ink-pen.svg) 2 22, text;
}
.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;
}
.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[type="number"]::-webkit-outer-spin-button, input[type="number"]::-webkit-outer-spin-button,
input::-webkit-inner-spin-button { input::-webkit-inner-spin-button {
-webkit-appearance: none; -webkit-appearance: none !important;
margin: 0; margin: 0 !important;
}
input[type="number"]::-webkit-calendar-picker-indicator {
display: none;
}
.grid input:enabled {
background: white;
color: darkblue;
cursor: inherit;
}
.grid input.pencil,
.grid input::placeholder {
color: #666 !important;
font-size: 0.9rem !important;
}
.grid input:disabled {
color: white;
background: #6666ff;
}
.grid input:disabled,
.grid input.forbidden {
cursor: not-allowed !important;
}
.grid input.forbidden:enabled {
background: #ffffaa;
}
.grid input.same-value:enabled {
background: #ffff33;
}
.grid input.forbidden:disabled {
color: #ffffaa;
background: #6666ff;
}
.grid input.same-value:disabled,
.tools button.same-value:enabled,
.tools input:enabled:checked+label {
color: #ffffaa !important;
background: #00b359 !important;
}
.grid input.one-candidate {
cursor: help !important;
} }
.tools button, input[type="number"]::-webkit-calendar-picker-indicator {
.tools input+label { display: none !important;
color: white; }
text-shadow: -1px -1px #5b6c9e;
background: #8ca6f2; table {
border: 2px outset #8ca6f2; border-collapse: separate;
border-radius: 4px; border-spacing: 0;
font-size: 1.3rem; }
min-width:20px;
padding: 4px 5px 5px 5px; table input {
margin: 0px 1px 1px 1px; 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;
}
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; cursor: pointer;
} }
.tools img {
display: block; button i,
width: 24px; label i {
height: 24px; margin: 0 -.125rem;
} }
.tools input {
display:none; button:disabled,
:disabled+label {
cursor: not-allowed !important;
} }
.tools button:enabled:hover,
.tools input:enabled:hover+label { table input:enabled {
border-width: 1px; cursor: inherit;
border-style: outset;
padding: 5px 5px 5px 6px;
margin: 1px 1px 1px 2px;
} }
.tools input:enabled:checked:hover+label {
border-width: 3px; .pencil {
border-style: inset; color: var(--bs-secondary-color) !important;
padding: 4px 2px 2px 5px;
margin: 1px 1px 1px 2px;
} }
.tools input:enabled:checked+label {
text-shadow: -1px -1px #005f2f; #colorPickerInput{
border: 2px inset #00b359; width: 2.3rem;
background: #00b359; height: auto;
padding: 4px 4px 4px 5px; padding: .375rem;
margin: 1px 1px 0px 2px;
} }
.tools button:enabled:active,
.tools input:enabled:active+label { #colorPickerLabel {
border-width: 4px !important; color: var(--bs-body-color);
border-style: inset !important;
padding: 4px 0px 0px 5px !important;
margin: 0px 1px 0px 2px !important;
} }
.tools button:disabled,
.tools input:disabled+label { @media (prefers-color-scheme:dark) {
text-shadow: -1px -1px #555; .pencil {
color: #ccc; color: #5a5a5a !important;
background: darkgrey; }
border: 1px outset darkgrey;
padding: 5px 6px 6px 6px;
margin: 0px 1px 1px 1px;
cursor: not-allowed;
}
.tools button.warning {
background: #ff5050;
border-color: #ff5050;
} }
.context-menu { .context-menu {
display: none; z-index: 100;
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;
font-size: 1rem;
}
.context-menu li:hover {
background-color: #DEF;
}
.context-menu li.error {
color: #888
}
.context-menu li.error:hover {
background-color: #EEE;
}
a {
text-decoration: none;
}
.credits {
font-size: 0.8rem;
margin: 0;
} }

420
sudoku.js Normal file → Executable file
View File

@ -1,15 +1,18 @@
const VALUES = "123456789" const VALUES = "123456789"
const UNKNOWN = '.' const UNKNOWN = '.'
const SUGESTION_DELAY = 60000 //ms
let boxes = [] let boxes = []
let rows = Array.from(Array(9), x => []) let rows = Array.from(Array(9), x => [])
let columns = Array.from(Array(9), x => []) let columns = Array.from(Array(9), x => [])
let regions = Array.from(Array(9), x => []) let regions = Array.from(Array(9), x => [])
let suggestionTimer = null let areaNames = {
ligne: rows,
colonne: columns,
région: regions,
}
let valueToInsert = "" let valueToInsert = ""
let history = [] let easyBoxes = []
let accessKeyModifiers = "AccessKey+" let insertRadios = []
function shuffle(iterable) { function shuffle(iterable) {
array = Array.from(iterable) array = Array.from(iterable)
@ -25,24 +28,24 @@ function shuffle(iterable) {
return array return array
} }
window.onload = function () { window.onload = function() {
let rowId = 0 let rowId = 0
for (let row of grid.getElementsByTagName('tr')) { for (let row of grid.getElementsByTagName('tr')) {
let columnId = 0 let columnId = 0
for (let box of row.getElementsByTagName('input')) { for (let box of row.getElementsByTagName('input')) {
let regionId = rowId - rowId % 3 + Math.floor(columnId / 3) let regionId = rowId - rowId % 3 + Math.floor(columnId / 3)
if (!box.disabled) { if (!box.disabled) {
box.onfocus = onfocus box.onfocus = onfocus
box.oninput = oninput box.oninput = oninput
box.onblur = onblur box.onblur = onblur
box.onclick = onclick box.onclick = onclick
box.previousValue = "" box.onmouseenter = onmouseenter
box.previousPlaceholder = "" box.onmouseleave = onmouseleave
} }
box.oncontextmenu = oncontextmenu box.oncontextmenu = oncontextmenu
box.rowId = rowId box.rowId = rowId
box.columnId = columnId box.columnId = columnId
box.regionId = regionId box.regionId = regionId
boxes.push(box) boxes.push(box)
rows[rowId].push(box) rows[rowId].push(box)
columns[columnId].push(box) columns[columnId].push(box)
@ -52,34 +55,61 @@ window.onload = function () {
rowId++ rowId++
} }
const savedGame = localStorage[location.href] if (localStorage["tool"] == "sight") sightCheckbox.checked = true
if (savedGame) { else if (localStorage["tool"] == "highlighter") highlighterCheckbox.checked = true
boxes.forEach((box, i) => {
if (!box.disabled && savedGame[i] != UNKNOWN) { colorPickerInput.value = window.getComputedStyle(grid).getPropertyValue("--bs-body-color")
box.value = savedGame[i]
box.previousValue = savedGame[i]
}
})
}
boxes.forEach(box => { boxes.forEach(box => {
box.neighbourhood = new Set(rows[box.rowId].concat(columns[box.columnId]).concat(regions[box.regionId])) 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.delete(box)
box.neighbourhood = Array.from(box.neighbourhood) box.neighbourhood = Array.from(box.neighbourhood)
searchCandidatesOf(box)
}) })
if (/Win/.test(navigator.platform) || /Linux/.test(navigator.platform)) accessKeyModifiers = "Alt+Maj+" insertRadios = Array.from(insertRadioGroup.getElementsByTagName("input")).slice(1)
else if (/Mac/.test(navigator.platform)) accessKeyModifiers = "⌃⌥"
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]")) { for (node of document.querySelectorAll("*[accesskey]")) {
node.title += " [" + (node.accessKeyLabel || accessKeyModifiers + node.accessKey) + "]" shortcut = ` [${node.accessKeyLabel||(accessKeyModifiers+node.accessKey)}]`
if (node.title) node.title += shortcut
else if (node.label) node.label.title += shortcut
} }
refreshUI() loadGame(history.state)
}
if ("serviceWorker" in navigator) { window.onpopstate = (event) => loadGame(event.state)
navigator.serviceWorker.register(`service-worker.js?location=${location.href}`)
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) { function searchCandidatesOf(box) {
@ -89,10 +119,10 @@ function searchCandidatesOf(box) {
switch (box.candidates.size) { switch (box.candidates.size) {
case 0: case 0:
box.title = "Aucune possibilité !" box.title = "Aucune possibilité !"
break break
case 1: case 1:
box.title = "1 possibilité [Clic-droit]" box.title = "Une seule possibilité [Clic-droit]"
break break
default: default:
box.title = box.candidates.size + " possibilités [Clic-droit]" box.title = box.candidates.size + " possibilités [Clic-droit]"
} }
@ -101,21 +131,21 @@ function searchCandidatesOf(box) {
function onfocus() { function onfocus() {
if (pencilRadio.checked) { if (pencilRadio.checked) {
this.value = this.placeholder this.type = "text"
this.value = this.placeholder
this.placeholder = ""
this.classList.add("pencil") this.classList.add("pencil")
} else { } else {
this.select() this.select()
} }
this.style.caretColor = valueToInsert? "transparent": "auto" if (penColor && inkPenRadio.checked) {
this.style.setProperty("color", penColor)
}
this.style.caretColor = valueToInsert ? "transparent" : "auto"
} }
function onclick() { function onclick() {
if (this.value == "" && this.candidates.size == 1) { if (inkPenRadio.checked) {
valueToInsert = this.candidates.values().next().value
document.getElementById("insertRadio" + valueToInsert).checked = true
this.value = valueToInsert
this.oninput()
} else if (inkPenRadio.checked) {
if (valueToInsert) { if (valueToInsert) {
this.value = valueToInsert this.value = valueToInsert
this.oninput() this.oninput()
@ -123,9 +153,10 @@ function onclick() {
this.select() this.select()
} }
} else if (pencilRadio.checked) { } else if (pencilRadio.checked) {
if (valueToInsert) if (valueToInsert) {
this.value += valueToInsert this.value = Array.from(new Set(this.value + valueToInsert)).join("")
this.oninput() this.oninput()
}
} else if (eraserRadio.checked) { } else if (eraserRadio.checked) {
this.value = "" this.value = ""
this.placeholder = "" this.placeholder = ""
@ -134,179 +165,180 @@ function onclick() {
} }
function oninput() { function oninput() {
history.push({ box: this, value: this.previousValue, placeholder: this.previousPlaceholder }) if (inkPenRadio.checked) {
undoButton.disabled = false checkBoxes()
if (pencilRadio.checked) { enableRadio()
this.value = Array.from(new Set(this.value)).sort().join("") highlight()
this.previousValue = "" fixGridLink.href = "?" + boxes.map(box => box.value || UNKNOWN).join("")
this.previousPlaceholder = this.value
} else {
this.previousValue = this.value
this.previousPlaceholder = this.placeholder
refreshBox(this)
} }
saveGame()
} }
function refreshBox(box) { function checkBoxes() {
localStorage[location.href] = boxes.map(box => box.value || ".").join("") boxes.forEach(box => {
box.setCustomValidity("")
box.neighbourhood.concat([box]).forEach(neighbour => { box.classList.remove("is-invalid")
searchCandidatesOf(neighbour) box.parentElement.classList.remove("table-danger")
neighbour.setCustomValidity("") searchCandidatesOf(box)
if (box.candidates.size == 0) {
box.setCustomValidity("Aucun chiffre possible !")
box.classList.add("is-invalid")
}
}) })
refreshUI() for (let [areaName, areas] of Object.entries(areaNames))
for (area of areas)
for (neighbour1 of box.neighbourhood) { area.filter(box => box.value).sort((box, neighbour) => {
if (neighbour1.value) { if(box.value == neighbour.value) {
for (area of [ area.forEach(neighbour => neighbour.parentElement.classList.add("table-danger"))
{ name: "région", neighbours: regions[neighbour1.regionId] }, for (neighbour of [box, neighbour]) {
{ name: "ligne", neighbours: rows[neighbour1.rowId] }, neighbour.setCustomValidity(`Il y a un autre ${box.value} dans cette ${areaName}.`)
{ name: "colonne", neighbours: columns[neighbour1.columnId] }, neighbour.classList.add("is-invalid")
])
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) { return box.value - neighbour.value
neighbour1.setCustomValidity("Aucun chiffre possible !") })
}
}
}
if (box.form.checkValidity()) { // Correct grid if (sudokuForm.checkValidity()) { // Correct grid
if (boxes.filter(box => box.value == "").length == 0) if (boxes.filter(box => box.value == "").length == 0) {
setTimeout(() => alert(`Bravo ! Vous avez résolu la grille.`), 500) 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 } else { // Errors on grid
box.form.reportValidity() grid.classList.remove("table-success")
sudokuForm.reportValidity()
} }
} }
function refreshUI() { function enableRadio() {
for (radio of insertRadioGroup.getElementsByTagName("input")) { for (radio of insertRadios) {
const label = radio.nextElementSibling
if (boxes.filter(box => box.value == "").some(box => box.candidates.has(radio.value))) { if (boxes.filter(box => box.value == "").some(box => box.candidates.has(radio.value))) {
radio.disabled = false radio.disabled = false
if (radio.previousTitle) { radio.label.title = `Insérer un ${radio.value} [${radio.accessKeyLabel||(accessKeyModifiers+radio.accessKey)}]`
label.title = radio.previousTitle
label.previousTitle = null
}
} else { } else {
radio.disabled = true radio.disabled = true
label.previousTitle = label.title radio.label.title = `Tous les ${radio.value} sont posés.`
label.title = `Tous les ${radio.value} sont posés` if (valueToInsert == radio.value) {
if (valueToInsert == radio.value) valueToInsert = "" insertRadio0.checked = true
valueToInsert = ""
grid.style.cursor = "text"
}
} }
} }
highlight()
boxes.filter(box => !box.disabled).forEach(box => {
if (!box.value && box.candidates.size == 1) box.classList.add("one-candidate")
else box.classList.remove("one-candidate")
})
if (suggestionTimer) clearTimeout(suggestionTimer)
suggestionTimer = setTimeout(showSuggestion, SUGESTION_DELAY)
} }
function highlight() { function highlight() {
if (valueToInsert) { hintButton.disabled = true
if (highlighterCheckbox.checked) { easyBoxes = []
boxes.forEach(box => { boxes.forEach(box => {
if (box.value == valueToInsert) { if (valueToInsert && box.value == valueToInsert) {
box.classList.add("same-value") box.parentElement.classList.add("table-primary")
box.tabIndex = -1 box.tabIndex = -1
}
else {
box.classList.remove("same-value")
if (box.candidates.has(valueToInsert) && !box.disabled) {
box.classList.remove("forbidden")
box.tabIndex = 0
} else {
box.classList.add("forbidden")
box.tabIndex = -1
}
}
})
} else { } else {
boxes.forEach(box => { box.parentElement.classList.remove("table-primary")
box.classList.remove("same-value")
if (box.disabled) {
box.classList.add("forbidden")
} else {
box.classList.remove("forbidden")
}
box.tabIndex = 0
})
}
} else {
boxes.forEach(box => {
box.classList.remove("same-value", "forbidden")
box.tabIndex = 0 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() { function onblur() {
if (this.classList.contains("pencil")) { if (this.classList.contains("pencil")) {
this.placeholder = this.value this.placeholder = this.value
this.value = "" this.value = ""
this.type = "number"
this.classList.remove("pencil") this.classList.remove("pencil")
} }
} }
function insert(radio) { function saveGame() {
if (radio.value == valueToInsert) { history.pushState({
valueToInsert = "" boxesValues: boxes.map(box => box.value),
radio.checked = false boxesPlaceholders: boxes.map(box => box.placeholder)
} else { }, "")
valueToInsert = radio.value restartLink.classList.remove("disabled")
} undoButton.disabled = false
if (inkPenRadio.checked) customCursor = "url(img/ink-pen.svg) 2 22"
if (pencilRadio.checked) customCursor = "url(img/pencil.svg) 2 22"
if (eraserRadio.checked) customCursor = "url(img/eraser.svg) 2 22"
fallbackCursor = valueToInsert? "copy": "text"
grid.style.cursor = `${customCursor}, ${fallbackCursor}`
highlight()
} }
function undo() { function onmouseenter(event) {
if (history.length) { if (sightCheckbox.checked){
const previousState = history.pop() box = event.target
previousState.box.value = previousState.value box.andNeighbourhood.forEach(neighbour => {
previousState.box.placeholder = previousState.placeholder neighbour.parentElement.classList.add("table-active")
refreshBox(previousState.box) })
if (history.length < 1) undoButton.disabled = true
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() { function restart() {
if (confirm("Effacer toutes les cases ?")) { if (confirm("Effacer toutes les cases ?")) {
boxes.filter(box => !box.disabled).forEach(box => { restartButton.disabled = true
box.value = "" location.hash = ""
box.previousValue = ""
box.placeholder = ""
box.previousPlaceholder = ""
box.setCustomValidity("")
})
let history = []
undoButton.disabled = true
boxes.forEach(searchCandidatesOf)
refreshUI()
} }
} }
function showSuggestion() { function showHint() {
const easyBoxes = boxes.filter(box => box.value == "" && box.candidates.size == 1)
if (easyBoxes.length) { if (easyBoxes.length) {
shuffle(easyBoxes)[0].placeholder = "💡" shuffle(easyBoxes)
} else { let box = easyBoxes.pop()
clearTimeout(suggestionTimer) box.placeholder = "💡"
suggestionTimer = null box.focus()
return box
} }
hintButton.disabled = true
} }
function oncontextmenu(event) { function oncontextmenu(event) {
@ -317,34 +349,46 @@ function oncontextmenu(event) {
Array.from(box.candidates).sort().forEach(candidate => { Array.from(box.candidates).sort().forEach(candidate => {
li = document.createElement("li") li = document.createElement("li")
li.innerText = candidate li.innerText = candidate
li.onclick = function (event) { li.classList = "list-group-item list-group-item-action"
li.onclick = function(e) {
contextMenu.style.display = "none" contextMenu.style.display = "none"
box.onfocus() valueToInsert = e.target.innerText
box.value = event.target.innerText grid.style.cursor = "pointer"
box.oninput() document.getElementById("insertRadio" + valueToInsert).checked = true
box.onblur() box.onclick()
}
li.oncontextmenu = function(e) {
e.preventDefault()
li.onclick(e)
} }
contextMenu.appendChild(li) contextMenu.appendChild(li)
}) })
} else { } else {
li = document.createElement("li") li = document.createElement("li")
li.innerText = "Aucune possibilité !" li.innerText = "Aucune possibilité !"
li.classList.add("error") li.classList = "list-group-item list-group-item-action disabled"
contextMenu.appendChild(li) contextMenu.appendChild(li)
} }
contextMenu.style.left = `${event.pageX}px` contextMenu.style.left = `${event.pageX}px`
contextMenu.style.top = `${event.pageY}px` contextMenu.style.top = `${event.pageY}px`
contextMenu.style.display = "block" contextMenu.style.display = "block"
document.onclick = function(event) {
contextMenu.style.display = "none"
document.onclick = null
}
return false return false
} }
document.onclick = function (event) {
contextMenu.style.display = "none"
}
document.onkeydown = function(event) { document.onkeydown = function(event) {
if (event.key == "Escape") { if (event.key == "Escape" && contextMenu.style.display == "block") {
event.preventDefault() event.preventDefault()
contextMenu.style.display = "none" contextMenu.style.display = "none"
} }
} }
window.onbeforeunload = function(event) {
saveGame()
if (sightCheckbox.checked) localStorage["tool"] = "sight"
else if (highlighterCheckbox.checked) localStorage["tool"] = "highlighter"
}

211
sudoku.php Normal file → Executable file
View File

@ -1,135 +1,90 @@
<?php
require("classes.php");
session_start();
$currentGrid = strip_tags($_GET['grid']);
$_SESSION["currentGrid"] = $currentGrid;
if (!isset($_SESSION[$currentGrid])) {
$grid = new Grid();
$grid->import($currentGrid);
if ($grid->containsDuplicates()) {
$warning = "Cette grille contient des doublons.";
} else {
switch($grid->countSolutions(2)) {
case 0:
$warning = "Cette grille n'a pas de solution.";
break;
case 1:
break;
default:
$warning = "Cette grille a plusieurs solutions.";
}
}
}
?>
<!DOCTYPE html> <!DOCTYPE html>
<html lang='fr' prefix="og: https://ogp.me/ns#"> <html lang='fr' prefix="og: https://ogp.me/ns#">
<head> <head>
<meta charset='utf-8' /> <?php require_once("head.php") ?>
<meta name='viewport' content='width=device-width' />
<title>Sudoku</title>
<link rel='stylesheet' type='text/css' href='style.css' />
<script src='sudoku.js'></script>
<link rel="apple-touch-icon" href="thumbnail.png?size=57" sizes="57x57">
<link rel="apple-touch-icon" href="thumbnail.png?size=114" sizes="114x114">
<link rel="apple-touch-icon" href="thumbnail.png?size=72" sizes="72x72">
<link rel="apple-touch-icon" href="thumbnail.png?size=144" sizes="144x144">
<link rel="apple-touch-icon" href="thumbnail.png?size=60" sizes="60x60">
<link rel="apple-touch-icon" href="thumbnail.png?size=120" sizes="120x120">
<link rel="apple-touch-icon" href="thumbnail.png?size=76" sizes="76x76">
<link rel="apple-touch-icon" href="thumbnail.png?size=152" sizes="152x152">
<link rel="icon" type="image/png" href="thumbnail.png?size=196" sizes="196x196">
<link rel="icon" type="image/png" href="thumbnail.png?size=160" sizes="160x160">
<link rel="icon" type="image/png" href="thumbnail.png?size=96" sizes="96x96">
<link rel="icon" type="image/png" href="thumbnail.png?size=16" sizes="16x16">
<link rel="icon" type="image/png" href="thumbnail.png?size=32" sizes="32x32">
<link rel="manifest" href="manifest.json">
<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.png?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> </head>
<body> <body>
<header> <nav class="navbar mb-4">
<h1>Sudoku</h1> <h1 class="display-4 text-center m-auto">Sudoku</h1>
</header> </nav>
<form id='sudokuForm'> <div class="row g-0">
<table id='grid' class='grid'> <main class="col-md-6 order-md-1">
<tbody> <div class="text-center m-auto" style="width: min-content;">
<?php <div class='d-flex justify-content-between mb-2'>
for ($row = 0; $row < 9; $row++) { <div class='btn-group'>
?> <input type='radio' id='inkPenRadio' class='btn-check' name='penRadioGroup' checked />
<tr> <label for='inkPenRadio' class='btn btn-primary' title='Écrire un chiffre'><i class="ri-ball-pen-fill"></i></label>
<?php <input type='radio' id='pencilRadio' class='btn-check' name='penRadioGroup' />
for ($column = 0; $column < 9; $column++) { <label for='pencilRadio' class='btn btn-primary' title='Prendre des notes'><i class="ri-pencil-fill"></i></label>
$value = $currentGrid[9*$row+$column]; <input type='radio' id='eraserRadio' class='btn-check' name='penRadioGroup' />
if ($value == UNKNOWN) { <label for='eraserRadio' class='btn btn-primary' title='Effacer une case'><i class="ri-eraser-fill"></i></label>
?> </div>
<td><input type='number' min='1' max='9' step='1' value='' title='Valeurs possibles [Clic-droit]'/></td> <input type="color" class="btn-check" id="colorPickerInput" title="Changer la couleur" oninput="changeColor()"/>
<?php <label id="colorPickerLabel" for="colorPickerInput" class="btn btn-primary" title="Changer de couleur"><i class="ri-palette-fill"></i></label>
} else { <div class='btn-group'>
?> <input type='checkbox' id='sightCheckbox' class='btn-check' onclick='highlighterCheckbox.checked = false; highlight()' />
<td><input type='number' min='1' max='9' step='1' value='<?=$value?>' disabled/></td> <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>
<?php <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>
</tr> <a id='restartLink' class='btn btn-primary disabled' href="" title='Recommencer'><i class="ri-restart-line"></i></a>
<?php <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>
</tbody> <table id='grid' class='table mb-2'>
</table> <tbody>
</form> <?php for ($row = 0; $row < 81; $row += 9): ?>
<section class='tools'> <tr class="input-group d-inline-block w-auto">
<div id='insertRadioGroup' class='insertRadioGroup'> <?php for ($column = 0; $column < 9; $column++): $value = $currentGrid[$row+$column]; ?>
<?php <?php if ($value == UNKNOWN): ?>
for($value=1; $value<=9; $value++) { <td><input type='number' min='1' max='9' step='1' value='' class='form-control' /></td>
echo " <input type='radio' id='insertRadio$value' value='$value' name='insertRadioGroup' onclick='insert(this)' accesskey='$value'/>\n"; <?php else: ?>
echo " <label for='insertRadio$value' title='Insérer un $value'>$value</label>\n"; <td><input type='number' min='1' max='9' step='1' value='<?=$value?>' class='form-control' disabled /></td>
} <?php endif ?>
?> <?php endfor?>
</div> </tr>
<div> <?php endfor?>
<input id='highlighterCheckbox' type="checkbox" onclick='highlight()'/> </tbody>
<label for='highlighterCheckbox' title='Surligner les cases interdites'><img src='img/highlighter.svg' alt='Surligneur'></label> </table>
<input type='radio' id='inkPenRadio' name='tool' onclick='grid.style.cursor = "url(img/ink-pen.svg) 2 22, auto"' checked/> </form>
<label for='inkPenRadio' title='Écrire au stylo'><img src='img/ink-pen.svg' alt='Stylo indélébile'/></label> <div class='d-flex mb-4'>
<input type='radio' id='pencilRadio' name='tool' onclick='grid.style.cursor = "url(img/pencil.svg) 2 22, auto"'/> <div id='insertRadioGroup' class='radioGroup btn-group flex-fill'>
<label for='pencilRadio' title='Écrire au crayon'><img src='img/pencil.svg' alt='Crayon'/></label> <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>
<input type='radio' id='eraserRadio' name='tool' onclick='grid.style.cursor = "url(img/eraser.svg) 2 22, auto"'/> <?php for($value=1; $value<=9; $value++): ?>
<label for='eraserRadio' title='Effacer une case'><img src='img/eraser.svg' alt='Gomme'/></label> <input type='radio' class='btn-check' id='insertRadio<?=$value?>' value='<?=$value?>' name='insertRadioGroup' onclick='insert(this)' accesskey='<?=$value?>' disabled />
<button type='button' class='warning' onclick='restart()' title='Recommencer'> <label for='insertRadio<?=$value?>' class='btn btn-primary' title='Insérer un <?=$value?>'><?=$value?></label>
<img src='img/restart.svg' alt='Recommencer'/> <?php endfor ?>
</button> </div>
<button id='undoButton' type='button' onclick='undo()' disabled title='Annuler' accesskey='z'> </div>
<img src='img/undo.svg' alt='Annuler'/> <div class='mb-3'>
</button> <?php if (isset($warning)): ?>
</div> <strong>⚠️ <?=$warning?> ⚠️</strong><br/>
</section> <?php else: ?>
<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.
<?php <?php endif?>
if (isset($warning)) </div>
echo(" <strong>⚠️ $warning</strong><br/>\n"); </div>
else </main>
echo(" Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.\n") <aside class="col-md-3 text-center text-md-start">
?> <div class="d-flex flex-column flex-shrink-0 p-3">
</section> <ul class="nav nav-pills flex-column">
<ul id="contextMenu" class="context-menu"></ul> <li><a href="." class="nav-link link-body-emphasis">Nouvelle grille</a></li>
<footer> <li><a href="" class="nav-link link-body-emphasis">Lien vers cette grille</a></li>
<div id='links'> <li><a href="?---------------------------------------------------------------------------------" class="nav-link link-body-emphasis">Grille vierge</a></li>
<a href=''>Lien vers cette grille</a><br/> <li><a id="fixGridLink" href="" class="nav-link link-body-emphasis">Figer la grille</a></li>
<a href='.................................................................................'>Grille vierge</a><br/> <li><a href="https://git.malingrey.fr/adrien/Sudoku" class="nav-link link-body-emphasis">Code source</a></li>
<a href='.'>Nouvelle grille</a> <li><a href=".." class="nav-link link-body-emphasis">Autres jeux</a></li>
</div> </ul>
<div class='credits'> </div>
Icônes par <a href="https://www.flaticon.com/authors/freepik" title="Freepik">Freepik</a> chez <a href="https://www.flaticon.com/" title="Flaticon">www.flaticon.com</a> </aside>
</div> </div>
</footer> <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>
</body> </body>
</html> </html>

63
thumbnail.png.php → thumbnail.php Normal file → Executable file
View File

@ -1,20 +1,23 @@
<?php <?php
require("classes.php"); require("classes.php");
session_start(); if (isset($_GET["grid"]) && preg_match("/^[1-9-]{81}$/", $_GET["grid"]))
if ($_SESSION["currentGrid"]) $currentGrid = $_GET["grid"];
$currentGrid = $_SESSION["currentGrid"];
else else
$currentGrid = ".528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...w"; $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"); header ("Content-type: image/png");
$size = (int) $_GET['size']; if (isset($_GET['size']))
$size = (int) $_GET['size'];
else
$size = 196;
$thumbnail = imagecreate($size, $size); $thumbnail = imagecreate($size, $size);
$transparent = imagecolorallocate($thumbnail, 1, 1, 1); $transparent = imagecolorallocate($thumbnail, 1, 1, 1);
imagecolortransparent($thumbnail, $transparent); imagecolortransparent($thumbnail, $transparent);
$black = imagecolorallocate($thumbnail, 0, 0, 0); $darkerBorder = imagecolorallocate($thumbnail, 150, 155, 160);
$grey = imagecolorallocate($thumbnail, 128, 128, 128); $lighterBorder = imagecolorallocate($thumbnail, 210, 225, 230);
$blue = imagecolorallocate($thumbnail, 102, 102, 255); $emptyBoxBC = imagecolorallocate($thumbnail, 255, 255, 255);
$white = imagecolorallocate($thumbnail, 255, 255, 255); $clueBC = imagecolorallocate($thumbnail, 255, 255, 255);
$clueFC = imagecolorallocate($thumbnail, 150, 155, 160);
if ($size <= 36) { if ($size <= 36) {
$boxSize = floor(($size-4) / 9); $boxSize = floor(($size-4) / 9);
@ -24,19 +27,19 @@
$lineStart = $start + 1; $lineStart = $start + 1;
$lineEnd = $end - 2; $lineEnd = $end - 2;
for ($i = $start; $i < $end; $i += 3*$boxSize + 1) { for ($i = $start; $i < $end; $i += 3*$boxSize + 1) {
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $black); ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $darkerBorder);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $black); ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $darkerBorder);
} }
$x = $start; $x = $start;
$y = $start; $y = $start;
$boxSizeMinusOne = $boxSize - 1; $boxSizeMinusOne = $boxSize - 1;
foreach(str_split($_SESSION["currentGrid"]) as $i => $value) { foreach(str_split($currentGrid) as $i => $value) {
if ($i % 3 == 0) $x++; if ($i % 3 == 0) $x++;
if ($i % 27 == 0) $y++; if ($i % 27 == 0) $y++;
if ($value == UNKNOWN) { if ($value == UNKNOWN) {
$bgColor = $white; $bgColor = $emptyBoxBC;
} else { } else {
$bgColor = $blue; $bgColor = $clueFC;
} }
imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusOne, $y+$boxSizeMinusOne, $bgColor); imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusOne, $y+$boxSizeMinusOne, $bgColor);
$x += $boxSize; $x += $boxSize;
@ -53,21 +56,21 @@
$lineStart = $start + 1; $lineStart = $start + 1;
$lineEnd = $end - 2; $lineEnd = $end - 2;
for ($i = $start + $boxSize; $i < $end - $boxSize; $i += $boxSize) { for ($i = $start + $boxSize; $i < $end - $boxSize; $i += $boxSize) {
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $grey); ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $lighterBorder);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $grey); ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $lighterBorder);
} }
for ($i = $start; $i < $end; $i += 3*$boxSize) { for ($i = $start; $i < $end; $i += 3*$boxSize) {
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $black); ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $darkerBorder);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $black); ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $darkerBorder);
} }
$x = $start + 1; $x = $start + 1;
$y = $start + 1; $y = $start + 1;
$boxSizeMinusTwo = $boxSize - 2; $boxSizeMinusTwo = $boxSize - 2;
foreach(str_split($_SESSION["currentGrid"]) as $i => $value) { foreach(str_split($currentGrid) as $i => $value) {
if ($value == UNKNOWN) { if ($value == UNKNOWN) {
$bgColor = $white; $bgColor = $emptyBoxBC;
} else { } else {
$bgColor = $blue; $bgColor = $clueFC;
} }
imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusTwo, $y+$boxSizeMinusTwo, $bgColor); imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusTwo, $y+$boxSizeMinusTwo, $bgColor);
$x += $boxSize; $x += $boxSize;
@ -86,26 +89,26 @@
$fontSize = floor($boxSize/2) - 4; $fontSize = floor($boxSize/2) - 4;
$fdx = floor(($boxSize - imagefontwidth($fontSize)) / 2); $fdx = floor(($boxSize - imagefontwidth($fontSize)) / 2);
$fdy = ceil(($boxSize - imagefontheight($fontSize)) / 2) - 1; $fdy = ceil(($boxSize - imagefontheight($fontSize)) / 2) - 1;
$fontColor = $white; $fontColor = $emptyBoxBC;
for ($i = $start + $boxSize; $i < $end - $boxSize; $i += $boxSize) { for ($i = $start + $boxSize; $i < $end - $boxSize; $i += $boxSize) {
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $grey); ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $lighterBorder);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $grey); ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $lighterBorder);
} }
for ($i = $start; $i < $end; $i += 3*$boxSize) { for ($i = $start; $i < $end; $i += 3*$boxSize) {
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $black); ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $darkerBorder);
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $black); ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $darkerBorder);
} }
$x = $start + 1; $x = $start + 1;
$y = $start + 1; $y = $start + 1;
$boxSizeMinusTwo = $boxSize - 2; $boxSizeMinusTwo = $boxSize - 2;
foreach(str_split($_SESSION["currentGrid"]) as $i => $value) { foreach(str_split($currentGrid) as $i => $value) {
if ($value == UNKNOWN) { if ($value == UNKNOWN) {
$bgColor = $white; $bgColor = $emptyBoxBC;
} else { } else {
$bgColor = $blue; $bgColor = $clueBC;
} }
imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusTwo, $y+$boxSizeMinusTwo, $bgColor); imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusTwo, $y+$boxSizeMinusTwo, $bgColor);
if ($value != UNKNOWN) imagestring($thumbnail, $fontSize, $x + $fdx, $y + $fdy, $value, $fontColor); if ($value != UNKNOWN) imagestring($thumbnail, $fontSize, $x + $fdx, $y + $fdy, $value, $clueFC);
$x += $boxSize; $x += $boxSize;
if ($i % 9 == 8) { if ($i % 9 == 8) {
$y += $boxSize; $y += $boxSize;

BIN
thumbnail.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB