Compare commits

...

34 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
13 changed files with 310 additions and 363 deletions

9
400.php Normal file → Executable file
View File

@ -6,17 +6,22 @@
<title>Requête incorrecte</title> <title>Requête incorrecte</title>
</head> </head>
<body> <body>
<nav class="navbar mb-4">
<h1 class="display-4 text-center m-auto">Sudoku</h1>
</nav>
<main class="container my-4">
<header> <header>
<h1>Requête incorrecte</h1> <h1 class="mb-4">Requête incorrecte</h1>
</header> </header>
L'adresse URL doit être de la forme :<br/> L'adresse URL doit être de la forme :<br/>
<?=$dirUrl?>/?<em>grille</em><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 : <em>grille</em> étant une suite de 81 caractères représentant la grille de gauche à droite puis de haut en bas, soit :
<ul> <ul>
<li>un chiffre entre 1 et 9 pour les cases connues</li> <li>un chiffre entre 1 et 9 pour les cases connues</li>
<li>un point pour les case vides</li> <li>un tiret (-) pour les case vides</li>
</ul> </ul>
Exemple :<br/> Exemple :<br/>
<a href='<?=$newGridUrl?>'><?=$newGridUrl?></a> <a href='<?=$newGridUrl?>'><?=$newGridUrl?></a>
</main>
</body> </body>
</html> </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)

27
classes.php Normal file → Executable file
View File

@ -1,5 +1,5 @@
<?php <?php
const UNKNOWN = "."; const UNKNOWN = "-";
$validGrids = array(); $validGrids = array();
@ -27,15 +27,19 @@
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() {
@ -48,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());
@ -74,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) {

0
favicon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 542 B

65
head.php Normal file → Executable file
View File

@ -1,35 +1,32 @@
<meta charset="utf-8" /> <meta charset="utf-8" />
<title>Sudoku</title> <title>Sudoku</title><meta property="og:title" content="Sudoku" />
<meta name="viewport" content="width=device-width, initial-scale=1" /> <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"]?>" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-dark.min.css" rel="stylesheet" type="text/css" title="Automatique" /> <meta name="viewport" content="width=device-width, initial-scale=1" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/css/bootstrap.min.css" rel="alternate stylesheet" type="text/css" title="Clair" integrity="sha384-KK94CHFLLe+nY2dmCWGMq91rCGa5gtU4mk92HdvYe+M/SXH301p5ILy+dN9+nJOZ" crossorigin="anonymous" />
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-night.min.css" rel="alternate stylesheet" type="text/css" title="Sombre" /> <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="https://cdn.jsdelivr.net/npm/remixicon@3.2.0/fonts/remixicon.css" rel="stylesheet">
<link rel="stylesheet" type="text/css" href="css/style.css" /> <link href="style.css" rel="stylesheet" type="text/css" />
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$currentGrid?>size=196&" sizes="196x196">
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$currentGrid?>size=160&" sizes="160x160"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=196" sizes="196x196" rel="icon" type="image/png">
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$currentGrid?>size=96&" sizes="96x96"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=160" sizes="160x160" rel="icon" type="image/png">
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$currentGrid?>size=16&" sizes="16x16"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=96" sizes="96x96" rel="icon" type="image/png">
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$currentGrid?>size=32&" sizes="32x32"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=16" sizes="16x16" rel="icon" type="image/png">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$currentGrid?>size=57&" sizes="57x57"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=32" sizes="32x32" rel="icon" type="image/png">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$currentGrid?>size=114&" sizes="114x114"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=152" sizes="152x152" rel="apple-touch-icon">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$currentGrid?>size=72&" sizes="72x72"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=144" sizes="144x144" rel="apple-touch-icon">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$currentGrid?>size=144&" sizes="144x144"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=120" sizes="120x120" rel="apple-touch-icon">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$currentGrid?>size=60&" sizes="60x60"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=114" sizes="114x114" rel="apple-touch-icon">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$currentGrid?>size=120&" sizes="120x120"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=57" sizes="57x57" rel="apple-touch-icon">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$currentGrid?>size=76&" sizes="76x76"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=72" sizes="72x72" rel="apple-touch-icon">
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$currentGrid?>size=152&" sizes="152x152"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=60" sizes="60x60" rel="apple-touch-icon">
<link rel="manifest" href="manifest.php?grid=<?=$currentGrid?>"> <link href="thumbnail.php?grid=<?=$currentGrid?>&size=76" sizes="76x76" rel="apple-touch-icon">
<meta property="og:title" content="Sudoku" /> <link href="manifest.php?grid=<?=$currentGrid?>" rel="manifest">
<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?size=200&grid=<?=$currentGrid?>" />
<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"]?>" />

37
index.php Normal file → Executable file
View File

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

0
manifest.php Normal file → Executable file
View File

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

@ -1,11 +1,3 @@
<?php
session_start();
if (isset($_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(

31
css/style.css → style.css Normal file → Executable file
View File

@ -1,12 +1,3 @@
body {
width: min-content;
margin: auto;
}
.btn {
padding: .275rem .625rem;
}
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 !important; -webkit-appearance: none !important;
@ -29,15 +20,25 @@ table input {
padding: 0 !important; padding: 0 !important;
margin: 0 !important; margin: 0 !important;
text-align: center; text-align: center;
-moz-appearance: textfield !important; appearance: textfield !important;
border-radius: 0 !important; border-radius: 0 !important;
} }
table td.table-primary input, table td.table-primary input,
table td.table-active input, table td.table-active input,
table.table-success input, table.table-success input,
td.table-danger input:disabled,
table input:not([disabled]) { table input:not([disabled]) {
background: transparent !important; 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 { tr:nth-child(3n+1) td input {
@ -107,7 +108,7 @@ table input {
cursor: inherit !important; cursor: inherit !important;
} }
.table-danger { .not-allowed {
cursor: not-allowed !important; cursor: not-allowed !important;
} }
@ -135,10 +136,6 @@ table input:enabled {
cursor: inherit; cursor: inherit;
} }
.modal-content {
z-index: 1000;
}
.pencil { .pencil {
color: var(--bs-secondary-color) !important; color: var(--bs-secondary-color) !important;
} }
@ -158,3 +155,7 @@ table input:enabled {
color: #5a5a5a !important; color: #5a5a5a !important;
} }
} }
.context-menu {
z-index: 100;
}

View File

@ -5,9 +5,12 @@ 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 areaNames = {
ligne: rows,
colonne: columns,
région: regions,
}
let valueToInsert = "" let valueToInsert = ""
let history = []
let accessKeyModifiers = "AccessKey+"
let easyBoxes = [] let easyBoxes = []
let insertRadios = [] let insertRadios = []
@ -38,8 +41,6 @@ window.onload = function() {
box.onclick = onclick box.onclick = onclick
box.onmouseenter = onmouseenter box.onmouseenter = onmouseenter
box.onmouseleave = onmouseleave box.onmouseleave = onmouseleave
box.previousValue = ""
box.previousPlaceholder = ""
} }
box.oncontextmenu = oncontextmenu box.oncontextmenu = oncontextmenu
box.rowId = rowId box.rowId = rowId
@ -54,8 +55,10 @@ window.onload = function() {
rowId++ rowId++
} }
if (localStorage["sightCheckbox.checked"] == "true") sightCheckbox.checked = true if (localStorage["tool"] == "sight") sightCheckbox.checked = true
else if (localStorage["highlighterCheckbox.checked"] == "true") highlighterCheckbox.checked = true else if (localStorage["tool"] == "highlighter") highlighterCheckbox.checked = true
colorPickerInput.value = window.getComputedStyle(grid).getPropertyValue("--bs-body-color")
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]))
@ -69,43 +72,44 @@ window.onload = function() {
for (label of document.getElementsByTagName("label")) { for (label of document.getElementsByTagName("label")) {
label.control.label = label label.control.label = label
} }
let accessKeyModifiers = (/Win/.test(navigator.userAgent) || /Linux/.test(navigator.userAgent)) ? "Alt+Maj+"
if (/Win/.test(navigator.platform) || /Linux/.test(navigator.platform)) accessKeyModifiers = "Alt+Maj+" : (/Mac/.test(navigator.userAgent)) ? "⌃⌥"
else if (/Mac/.test(navigator.platform)) accessKeyModifiers = "⌃⌥" : "AccessKey+"
for (node of document.querySelectorAll("*[accesskey]")) { for (node of document.querySelectorAll("*[accesskey]")) {
shortcut = ` [${node.accessKeyLabel||(accessKeyModifiers+node.accessKey)}]` shortcut = ` [${node.accessKeyLabel||(accessKeyModifiers+node.accessKey)}]`
if (node.title) node.title += shortcut if (node.title) node.title += shortcut
else if (node.label) node.label.title += shortcut else if (node.label) node.label.title += shortcut
} }
loadSavedGame() loadGame(history.state)
colorPickerInput.value = window.getComputedStyle(grid).getPropertyValue("--bs-body-color")
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register(`service-worker.php?location=${location.pathname}`)
}
} }
function loadSavedGame() { window.onpopstate = (event) => loadGame(event.state)
const savedGame = location.hash.slice(1)
if (savedGame.match(/[1-9.]{81}/)) { function loadGame(state) {
if (state) {
boxes.forEach((box, i) => { boxes.forEach((box, i) => {
if (!box.disabled && savedGame[i] != UNKNOWN) { if (!box.disabled) {
box.value = savedGame[i] box.value = state.boxesValues[i]
box.previousValue = savedGame[i] box.placeholder = state.boxesPlaceholders[i]
} }
}) })
restartButton.disabled = false restartLink.classList.remove("disabled")
fixGridLink.href = "?" + savedGame 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 = ""
} }
boxes.forEach(searchCandidatesOf)
refreshUI()
}
onhashchange = function(event) { checkBoxes()
if (location.hash.slice(1)) loadSavedGame() enableRadio()
else restart() highlight()
} }
function searchCandidatesOf(box) { function searchCandidatesOf(box) {
@ -127,12 +131,16 @@ function searchCandidatesOf(box) {
function onfocus() { function onfocus() {
if (pencilRadio.checked) { if (pencilRadio.checked) {
//this.type = "text" this.type = "text"
this.value = this.placeholder this.value = this.placeholder
this.placeholder = ""
this.classList.add("pencil") this.classList.add("pencil")
} else { } else {
this.select() this.select()
} }
if (penColor && inkPenRadio.checked) {
this.style.setProperty("color", penColor)
}
this.style.caretColor = valueToInsert ? "transparent" : "auto" this.style.caretColor = valueToInsert ? "transparent" : "auto"
} }
@ -157,81 +165,56 @@ function onclick() {
} }
function oninput() { function oninput() {
history.push({ if (inkPenRadio.checked) {
box: this, checkBoxes()
value: this.previousValue, enableRadio()
placeholder: this.previousPlaceholder highlight()
}) fixGridLink.href = "?" + boxes.map(box => box.value || UNKNOWN).join("")
undoButton.disabled = false
saveButton.disabled = false
restartButton.disabled = false
if (pencilRadio.checked) {
this.previousValue = ""
this.previousPlaceholder = this.value
} else {
this.previousValue = this.value
this.previousPlaceholder = this.placeholder
refreshBox(this)
}
if (penColor) {
this.style.setProperty("color", penColor)
} }
saveGame()
} }
function refreshBox(box) { function checkBoxes() {
checkBox(box) boxes.forEach(box => {
refreshUI() box.setCustomValidity("")
} box.classList.remove("is-invalid")
box.parentElement.classList.remove("table-danger")
function checkBox(box) { searchCandidatesOf(box)
box.andNeighbourhood.forEach(neighbour => { if (box.candidates.size == 0) {
neighbour.setCustomValidity("") box.setCustomValidity("Aucun chiffre possible !")
neighbour.classList.remove("is-invalid") box.classList.add("is-invalid")
searchCandidatesOf(neighbour)
if (neighbour.candidates.size == 0) {
neighbour.setCustomValidity("Aucun chiffre possible !")
} }
}) })
if (box.value) { for (let [areaName, areas] of Object.entries(areaNames))
for (area of[{ for (area of areas)
name: "région", area.filter(box => box.value).sort((box, neighbour) => {
neighbours: regions[box.regionId] if(box.value == neighbour.value) {
}, { area.forEach(neighbour => neighbour.parentElement.classList.add("table-danger"))
name: "ligne", for (neighbour of [box, neighbour]) {
neighbours: rows[box.rowId] neighbour.setCustomValidity(`Il y a un autre ${box.value} dans cette ${areaName}.`)
}, {
name: "colonne",
neighbours: columns[box.columnId]
}, ])
for (neighbour of area.neighbours)
if (box != neighbour && box.value == neighbour.value) {
for (neighbour of[box, neighbour]) {
neighbour.setCustomValidity(`Il y a un autre ${box.value} dans cette ${area.name}.`)
neighbour.classList.add("is-invalid") neighbour.classList.add("is-invalid")
} }
} }
} return box.value - neighbour.value
})
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) {
grid.classList.add("table-success") grid.classList.add("table-success")
saveButton.disabled = true
setTimeout(() => { setTimeout(() => {
if (confirm(`Bravo ! Vous avez résolu la grille. En voulez-vous une autre ?`)) if (confirm(`Bravo ! Vous avez résolu la grille. En voulez-vous une autre ?`))
location = "." location = "."
}, 400) }, 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() {
enableRadio()
highlight()
}
function enableRadio() { function enableRadio() {
for (radio of insertRadios) { for (radio of insertRadios) {
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))) {
@ -281,34 +264,42 @@ 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.type = "number"
this.classList.remove("pencil") 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) { function onmouseenter(event) {
if (sightCheckbox.checked){ if (sightCheckbox.checked){
box = event.target box = event.target
box.neighbourhood.concat([box]).forEach(neighbour => { box.andNeighbourhood.forEach(neighbour => {
neighbour.parentElement.classList.add("table-active") neighbour.parentElement.classList.add("table-active")
}) })
box.neighbourhood.forEach(neighbour => { box.neighbourhood.forEach(neighbour => {
if (valueToInsert && neighbour.value == valueToInsert) { if (valueToInsert && neighbour.value == valueToInsert) {
for (neighbour of[box, neighbour]) { for (neighbour of [box, neighbour]) {
neighbour.parentElement.classList.add("table-danger") neighbour.parentElement.classList.add("table-danger", "not-allowed")
} }
} }
}) })
} }
} }
function onmouseleave(event) { function onmouseleave(event) {
if (sightCheckbox.checked){ if (sightCheckbox.checked){
box = event.target box = event.target
box.neighbourhood.concat([box]).forEach(neighbour => { box.andNeighbourhood.forEach(neighbour => {
neighbour.parentElement.classList.remove("table-active") neighbour.parentElement.classList.remove("table-active", "table-danger", "not-allowed")
neighbour.parentElement.classList.remove("table-danger")
}) })
} }
} }
@ -320,7 +311,7 @@ function insert(radio) {
insert(0) insert(0)
} else { } else {
valueToInsert = radio.value valueToInsert = radio.value
grid.style.cursor = valueToInsert ? "copy" : "text" grid.style.cursor = valueToInsert ? "pointer" : "text"
highlight() highlight()
} }
} }
@ -332,51 +323,10 @@ function changeColor() {
colorPickerLabel.style.color = colorPickerInput.value colorPickerLabel.style.color = colorPickerInput.value
} }
function undo() {
if (history.length) {
const previousState = history.pop()
previousState.box.value = previousState.value
previousState.box.placeholder = previousState.placeholder
refreshBox(previousState.box)
if (history.length < 1) {
undoButton.disabled = true
saveButton.disabled = true
}
}
}
function restart() { function restart() {
if (confirm("Effacer toutes les cases ?")) { if (confirm("Effacer toutes les cases ?")) {
boxes.filter(box => !box.disabled).forEach(box => {
box.value = ""
box.previousValue = ""
box.placeholder = ""
box.previousPlaceholder = ""
box.setCustomValidity("")
})
let history = []
undoButton.disabled = true
restartButton.disabled = true restartButton.disabled = true
location.hash = "" location.hash = ""
boxes.forEach(searchCandidatesOf)
refreshUI()
}
}
function save() {
let saveGame = boxes.map(box => box.value || UNKNOWN).join("")
location.hash = saveGame
fixGridLink.href = "?" + saveGame
saveButton.disabled = true
alert("Partie sauvegardée")
}
window.onbeforeunload = function(event) {
localStorage["sightCheckbox.checked"] = sightCheckbox.checked
localStorage["highlighterCheckbox.checked"] = highlighterCheckbox.checked
if (!saveButton.disabled) {
event.preventDefault()
event.returnValue = "La partie n'est pas sauvegardée. Quitter quand même ?"
} }
} }
@ -403,7 +353,7 @@ function oncontextmenu(event) {
li.onclick = function(e) { li.onclick = function(e) {
contextMenu.style.display = "none" contextMenu.style.display = "none"
valueToInsert = e.target.innerText valueToInsert = e.target.innerText
grid.style.cursor = "copy" grid.style.cursor = "pointer"
document.getElementById("insertRadio" + valueToInsert).checked = true document.getElementById("insertRadio" + valueToInsert).checked = true
box.onclick() box.onclick()
} }
@ -416,7 +366,7 @@ function oncontextmenu(event) {
} 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`
@ -436,3 +386,9 @@ document.onkeydown = function(event) {
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"
}

View File

@ -2,118 +2,89 @@
<html lang='fr' prefix="og: https://ogp.me/ns#"> <html lang='fr' prefix="og: https://ogp.me/ns#">
<head> <head>
<?php require_once("head.php") ?> <?php require_once("head.php") ?>
</head> </head>
<body class="text-center"> <body>
<header> <nav class="navbar mb-4">
<h1 class="display-4 mb-3">Sudoku</h1> <h1 class="display-4 text-center m-auto">Sudoku</h1>
</header> </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='d-flex justify-content-between mb-2'>
<div class='btn-group'> <div class='btn-group'>
<input type='radio' id='inkPenRadio' class='btn-check' name='penRadioGroup' checked /> <input type='radio' id='inkPenRadio' class='btn-check' name='penRadioGroup' checked />
<label for='inkPenRadio' class='btn btn-primary' title='Écrire un chiffre'> <label for='inkPenRadio' class='btn btn-primary' title='Écrire un chiffre'><i class="ri-ball-pen-fill"></i></label>
<i class="ri-ball-pen-fill"></i>
</label>
<input type='radio' id='pencilRadio' class='btn-check' name='penRadioGroup' /> <input type='radio' id='pencilRadio' class='btn-check' name='penRadioGroup' />
<label for='pencilRadio' class='btn btn-primary' title='Prendre des notes'> <label for='pencilRadio' class='btn btn-primary' title='Prendre des notes'><i class="ri-pencil-fill"></i></label>
<i class="ri-pencil-fill"></i>
</label>
<input type='radio' id='eraserRadio' class='btn-check' name='penRadioGroup' /> <input type='radio' id='eraserRadio' class='btn-check' name='penRadioGroup' />
<label for='eraserRadio' class='btn btn-primary' title='Effacer une case'> <label for='eraserRadio' class='btn btn-primary' title='Effacer une case'><i class="ri-eraser-fill"></i></label>
<i class="ri-eraser-fill"></i>
</label>
</div> </div>
<input type="color" class="btn-check" id="colorPickerInput" title="Changer la couleur" oninput="changeColor()"/> <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"> <label id="colorPickerLabel" for="colorPickerInput" class="btn btn-primary" title="Changer de couleur"><i class="ri-palette-fill"></i></label>
<i class="ri-palette-fill"></i>
</label>
<div class='btn-group'> <div class='btn-group'>
<input type='checkbox' id='sightCheckbox' class='btn-check' onclick='highlighterCheckbox.checked = false; refreshUI()' /> <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'> <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>
<i class="ri-focus-3-line"></i> <input type='checkbox' id='highlighterCheckbox' class='btn-check' onclick='sightCheckbox.checked = false; highlight()' />
</label> <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>
<input type='checkbox' id='highlighterCheckbox' class='btn-check' onclick='sightCheckbox.checked = false; refreshUI()' />
<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> </div>
<button id="hintButton" type="button" class='btn btn-info' onclick="showHint()" title="Montrer une case avec une seule possibilité" accesskey="H" disabled=""> <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>
<i class="ri-lightbulb-line"></i> <a id='restartLink' class='btn btn-primary disabled' href="" title='Recommencer'><i class="ri-restart-line"></i></a>
</button> <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>
<button id='restartButton' type='button' class='btn btn-primary' onclick='restart()' disabled title='Recommencer'>
<i class="ri-restart-line"></i>
</button>
<button id='undoButton' type='button' class='btn btn-primary' onclick='undo()' disabled title='Annuler' accesskey='Z'>
<i class="ri-arrow-go-back-fill"></i>
</button>
<button id='saveButton' type='button' class='btn btn-primary' onclick='save()' disabled title='Sauvegarder' accesskey='S'>
<i class="ri-save-2-fill"></i>
</button>
</div> </div>
<form id='sudokuForm' class='needs-validation' novalidate> <form id='sudokuForm' class='needs-validation' novalidate>
<table id='grid' class='table mb-2'> <table id='grid' class='table mb-2'>
<tbody> <tbody>
<?php <?php for ($row = 0; $row < 81; $row += 9): ?>
for ($row = 0; $row < 81; $row += 9) {
?>
<tr class="input-group d-inline-block w-auto"> <tr class="input-group d-inline-block w-auto">
<?php <?php for ($column = 0; $column < 9; $column++): $value = $currentGrid[$row+$column]; ?>
for ($column = 0; $column < 9; $column++) { <?php if ($value == UNKNOWN): ?>
$value = $currentGrid[$row+$column]; <td><input type='number' min='1' max='9' step='1' value='' class='form-control' /></td>
if ($value == UNKNOWN) { <?php else: ?>
?>
<td><input type='number' min='1' max='9' step='1' value='' class='form-control'
title='Valeurs possibles [Clic-droit]' /></td>
<?php
} else {
?>
<td><input type='number' min='1' max='9' step='1' value='<?=$value?>' class='form-control' disabled /></td> <td><input type='number' min='1' max='9' step='1' value='<?=$value?>' class='form-control' disabled /></td>
<?php <?php endif ?>
} <?php endfor?>
}
?>
</tr> </tr>
<?php <?php endfor?>
}
?>
</tbody> </tbody>
</table> </table>
</form> </form>
<div class='d-flex mb-2'> <div class='d-flex mb-4'>
<div id='insertRadioGroup' class='radioGroup btn-group flex-fill'> <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 /> <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>
<label for='insertRadio0' class='btn btn-primary' title='Clavier'> <?php for($value=1; $value<=9; $value++): ?>
<i class="ri-input-cursor-move"></i> <input type='radio' class='btn-check' id='insertRadio<?=$value?>' value='<?=$value?>' name='insertRadioGroup' onclick='insert(this)' accesskey='<?=$value?>' disabled />
</label> <label for='insertRadio<?=$value?>' class='btn btn-primary' title='Insérer un <?=$value?>'><?=$value?></label>
<?php <?php endfor ?>
for($value=1; $value<=9; $value++) {
echo " <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>\n";
}
?>
</div> </div>
</div> </div>
<div class='mb-3'> <div class='mb-3'>
<?php <?php if (isset($warning)): ?>
if (isset($warning)) <strong>⚠️ <?=$warning?> ⚠️</strong><br/>
echo(" <strong>⚠️ $warning ⚠️</strong><br/>\n"); <?php else: ?>
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.
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") <?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> </div>
<ul id='contextMenu' class='context-menu modal-content shadow list-group w-auto position-absolute'></ul> <ul id='contextMenu' class='context-menu modal-content shadow list-group w-auto position-absolute'></ul>
<footer>
<div id='links' class='list-group mb-2'>
<a href='.' class='list-group-item list-group-item-action'>Nouvelle grille</a>
<a href='' class='list-group-item list-group-item-action'>Lien vers cette grille</a>
<a href='?.................................................................................' class='list-group-item list-group-item-action'>Grille
vierge</a>
<a href='' id='fixGridLink' class='list-group-item list-group-item-action'>Figer la grille enregistrée</a>
<a href='https://git.malingrey.fr/adrien/Sudoku' target="_blank" class='list-group-item list-group-item-action'>Code source</a>
</div>
</footer>
<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="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
<script src='js/sudoku.js' defer></script> <script src='sudoku.js' defer></script>
<script>navigator?.serviceWorker.register('service-worker.js')</script>
</body> </body>
</html> </html>

4
thumbnail.php Normal file → Executable file
View File

@ -1,9 +1,9 @@
<?php <?php
require("classes.php"); require("classes.php");
if (isset($_GET["grid"]) && preg_match("/^[1-9.]{81}$/", $_GET["grid"])) if (isset($_GET["grid"]) && preg_match("/^[1-9-]{81}$/", $_GET["grid"]))
$currentGrid = $_GET["grid"]; $currentGrid = $_GET["grid"];
else else
$currentGrid = ".528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287..."; $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");
if (isset($_GET['size'])) if (isset($_GET['size']))
$size = (int) $_GET['size']; $size = (int) $_GET['size'];

0
thumbnail.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB