Compare commits

...

100 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
e1cd9ca1a2 check unknown grid 2020-11-13 21:08:04 +01:00
58c50f0e1d php session 2020-11-13 13:10:56 +01:00
bda833f6e3 404! 2020-11-13 05:08:21 +01:00
df2bfa1333 show when no candidates 2020-11-13 02:55:51 +01:00
c09d51555d automatically insert only candidate on click 2020-11-13 02:12:58 +01:00
b20c6b72bd PWA saves curent grid for when offline 2020-11-13 01:40:29 +01:00
576cd3619f cursors! 2020-11-11 05:25:40 +01:00
d84d77e083 code cleanup 2020-11-09 17:37:29 +01:00
a475c7fbf3 style cursor on tool selected 2020-11-09 13:20:10 +01:00
2ac980bb82 use input radio and checkbox instead of buttons 2020-11-09 12:30:14 +01:00
ccb8e91728 fix undo 2020-11-09 02:19:25 +01:00
ef758fac04 write values on click, choose highlighting, svg icons 2020-11-09 01:24:09 +01:00
d5655b7a9a fix context menu input 2020-11-03 13:34:30 +01:00
e23dfc9f7f web app 2020-11-02 21:12:43 +01:00
1826e1e671 more readable colors 2020-10-30 20:00:57 +01:00
2fe57eeab4 save game 2020-10-30 19:47:01 +01:00
8295fed903 generate favicon 2020-10-30 19:44:22 +01:00
22 changed files with 1176 additions and 752 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>

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)

295
app.js
View File

@ -1,295 +0,0 @@
const VALUES = "123456789"
const SUGESTION_DELAY = 60000 //ms
let boxes = []
let rows = Array.from(Array(9), x => [])
let columns = Array.from(Array(9), x => [])
let regions = Array.from(Array(9), x => [])
let suggestionTimer= null
let highlightedValue = ""
let history = []
let accessKeyModifiers = "AccessKey+"
let penStyle = "ink-pen"
window.onload = function() {
let rowId = 0
for (let row of grid.getElementsByTagName('tr')) {
let columnId = 0
for (let box of row.getElementsByTagName('input')) {
let regionId = rowId - rowId%3 + Math.floor(columnId/3)
if (!box.disabled) {
box.onfocus = onfocus
box.oninput = oninput
box.onblur = onblur
}
box.oncontextmenu = oncontextmenu
box.rowId = rowId
box.columnId = columnId
box.regionId = regionId
boxes.push(box)
rows[rowId].push(box)
columns[columnId].push(box)
regions[regionId].push(box)
columnId++
}
rowId++
}
boxes.forEach(box => {
box.neighbourhood = new Set(rows[box.rowId].concat(columns[box.columnId]).concat(regions[box.regionId]))
box.neighbourhood.delete(box)
box.neighbourhood = Array.from(box.neighbourhood)
searchCandidatesOf(box)
})
enableButtons()
highlightAndTab()
if (/Win/.test(navigator.platform) || /Linux/.test(navigator.platform)) accessKeyModifiers = "Alt+Maj+"
else if (/Mac/.test(navigator.platform)) accessKeyModifiers = "⌃⌥"
for(node of document.querySelectorAll("*[accesskey]")) {
node.title += " [" + (node.accessKeyLabel || accessKeyModifiers + node.accessKey) + "]"
}
document.onclick = function (event) {
contextMenu.style.display = "none"
}
suggestionTimer = setTimeout(showSuggestion, 30000)
}
function searchCandidatesOf(box) {
box.candidates = new Set(VALUES)
box.neighbourhood.forEach(neighbour => box.candidates.delete(neighbour.value))
if (!box.disabled)
box.title = box.candidates.size + (box.candidates.size <= 1 ? " possibilité [Clic-droit]" : " possibilités [Clic-droit]")
}
function onfocus() {
if (penStyle == "pencil" && this.value == "") {
this.value = this.placeholder
this.placeholder = ""
this.classList.add("pencil")
} else {
this.select()
}
}
function oninput() {
history.push({box: this, value: this.previousValue, placeholder: this.previousPlaceholder})
undoButton.disabled = false
if (penStyle != "pencil") {
refresh(this)
}
}
function undo() {
if (history.length) {
previousState = history.pop()
previousState.box.value = previousState.value
previousState.box.placeholder = previousState.placeholder
refresh(previousState.box)
if (history.length < 1) undoButton.disabled = true
}
}
function refresh(box) {
box.neighbourhood.concat([box]).forEach(neighbour => {
searchCandidatesOf(neighbour)
neighbour.setCustomValidity("")
neighbour.required = false
})
enableButtons()
highlightAndTab()
for (neighbour1 of box.neighbourhood) {
if (neighbour1.value.length == 1) {
for (area of [
{name: "région", neighbours: regions[neighbour1.regionId]},
{name: "ligne", neighbours: rows[neighbour1.rowId]},
{name: "colonne", neighbours: columns[neighbour1.columnId]},
])
for (neighbour2 of area.neighbours)
if (neighbour2 != neighbour1 && neighbour2.value == neighbour1.value) {
for (neighbour of [neighbour1, neighbour2]) {
neighbour.setCustomValidity(`Il y a un autre ${neighbour.value} dans cette ${area.name}.`)
}
}
} else {
if (neighbour1.candidates.size == 0) {
neighbour1.setCustomValidity("Aucun chiffre possible !")
neighbour1.required = true
}
}
}
if (box.form.checkValidity()) { // Correct grid
if (boxes.filter(box => box.value == "").length == 0) {
alert(`Bravo ! Vous avez résolu la grille.`)
} else {
if (suggestionTimer) clearTimeout(suggestionTimer)
suggestionTimer = setTimeout(showSuggestion, SUGESTION_DELAY)
}
} else { // Errors on grid
box.form.reportValidity()
box.select()
}
}
function onblur() {
if (this.classList.contains("pencil")) {
this.placeholder = this.value
this.value = ""
this.classList.remove("pencil")
}
this.previousValue = this.value
this.previousPlaceholder = this.placeholder
}
function enableButtons() {
for (button of buttons.getElementsByTagName("button")) {
if (boxes.filter(box => box.value == "").some(box => box.candidates.has(button.textContent))) {
button.disabled = false
} else {
button.disabled = true
if (highlightedValue == button.textContent) highlightedValue = ""
}
}
}
function highlight(value) {
if (value == highlightedValue) {
highlightedValue = ""
} else {
highlightedValue = value
}
for (button of buttons.getElementsByTagName("button")) {
if (button.textContent == highlightedValue) button.classList.add("pressed")
else button.classList.remove("pressed")
}
highlightAndTab()
boxes.filter(box => box.value == "" && box.tabIndex == 0)[0].focus()
}
function highlightAndTab() {
if (highlightedValue) {
boxes.forEach(box => {
if (box.value == highlightedValue) {
box.classList.add("same-value")
box.tabIndex = -1
}
else {
box.classList.remove("same-value")
if (box.candidates.has(highlightedValue)) {
box.classList.remove("forbidden-value")
box.tabIndex = 0
} else {
box.classList.add("forbidden-value")
box.tabIndex = -1
}
}
})
} else {
boxes.forEach(box => {
box.classList.remove("same-value", "forbidden-value")
box.tabIndex = 0
})
}
}
function shuffle(iterable) {
array = Array.from(iterable)
if (array.length > 1) {
let i, j, tmp
for (i = array.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i+1))
tmp = array[i]
array[i] = array[j]
array[j] = tmp
}
}
return array
}
easyFirst = (box1, box2) => box1.candidates.size - box2.candidates.size
function showSuggestion() {
const emptyBoxes = boxes.filter(box => box.value == "" && box.candidates.size == 1)
if (emptyBoxes.length) {
shuffle(emptyBoxes)[0].placeholder = "💡"
} else {
clearTimeout(suggestionTimer)
suggestionTimer = null
}
}
function oncontextmenu(event) {
event.preventDefault()
while (contextMenu.firstChild) contextMenu.firstChild.remove()
const box = event.target
if (box.candidates.size) {
candidatesArray = Array.from(box.candidates).sort().forEach(candidate => {
li = document.createElement("li")
li.innerText = candidate
li.onclick = function (event) {
contextMenu.style.display = "none"
box.value = event.target.innerText
oninput.apply(box)
}
contextMenu.appendChild(li)
})
} else {
li = document.createElement("li")
li.innerText = "Aucun chiffre possible"
li.classList.add("error")
contextMenu.appendChild(li)
}
contextMenu.style.left = `${event.pageX}px`
contextMenu.style.top = `${event.pageY}px`
contextMenu.style.display = "block"
return false
}
function useInkPen() {
inkPenButton.classList.add("pressed")
pencilButton.classList.remove("pressed")
penStyle = "ink-pen"
}
function usePencil() {
pencilButton.classList.add("pressed")
inkPenButton.classList.remove("pressed")
penStyle = "pencil"
}
function erase(someBoxes) {
for (box of someBoxes) {
box.value = ""
box.placeholder = ""
searchCandidatesOf(box)
refresh(box)
}
enableButtons()
highlightAndTab()
}
function erasePencil() {
if (confirm("Effacer les chiffres écrits au crayon ?")) {
boxes.filter(box => !box.disabled).forEach(box => {
box.placeholder = ""
})
}
}
function eraseAll() {
if (confirm("Effacer tous les chiffres écrits au crayon et au stylo ?")) {
boxes.filter(box => !box.disabled).forEach(box => {
box.value = ""
box.placeholder = ""
box.setCustomValidity("")
box.required = false
})
boxes.forEach(searchCandidatesOf)
enableButtons()
highlightAndTab()
}
}

147
classes.php Normal file → Executable file
View File

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

View File

@ -1,61 +0,0 @@
<?php
header ("Content-type: image/png");
const UNKNOWN = ".";
$gridStr = strip_tags($_GET['grid']);
$size = (int) $_GET['size'];
$icon = imagecreate($size, $size);
$transparent = imagecolorallocate($icon, 1, 1, 1);
imagecolortransparent($icon, $transparent);
$gridBorder = imagecolorallocate($icon, 0, 0, 0);
$known = imagecolorallocate($icon, 102, 102, 255);
$unknown = imagecolorallocate($icon, 255, 255, 255);
if ($size == 16) {
ImageLine($icon, 2, 1, 12, 1, $gridBorder);
ImageLine($icon, 2, 5, 12, 5, $gridBorder);
ImageLine($icon, 2, 9, 12, 9, $gridBorder);
ImageLine($icon, 2, 13, 12, 13, $gridBorder);
ImageLine($icon, 1, 2, 1, 12, $gridBorder);
ImageLine($icon, 5, 2, 5, 12, $gridBorder);
ImageLine($icon, 9, 2, 9, 12, $gridBorder);
ImageLine($icon, 13, 2, 13, 12, $gridBorder);
$x = 1;
$y = 0;
foreach(str_split($gridStr) as $i => $value) {
$x++;
if ($i % 3 == 0) $x++;
if ($i % 9 == 0) {
$y++;
$x = 2;
}
if ($i % 27 == 0) $y++;
if ($value == UNKNOWN) $pixelColor = $unknown;
else $pixelColor = $known;
ImageSetPixel($icon, $x, $y, $pixelColor);
}
} else {
$boxSize = floor(($size-5) / 9);
$start = 1;
$end = 9*$boxSize + 2;
for ($y=0; $y < $size; $y += 3*$boxSize + 1)
ImageLine($icon, $start, $y, $end, $y, $gridBorder);
for ($x=0; $x < $size; $x += 3*$boxSize +1)
ImageLine($icon, $x, $start, $x, $end, $gridBorder);
$x = 0;
$y = 0;
$boxSizeMinusOne = $boxSize - 1;
foreach(str_split($gridStr) as $i => $value) {
if ($i % 3 == 0) $x++;
if ($i % 27 == 0) $y++;
if ($value == UNKNOWN) $color = $unknown;
else $color = $known;
imagefilledrectangle($icon, $x, $y, $x+$boxSizeMinusOne, $y+$boxSizeMinusOne, $color);
$x += $boxSize;
if ($i % 9 == 8) {
$y += $boxSize;
$x = 0;
}
}
}
imagepng($icon);
?>

0
favicon.png Normal file → Executable file
View File

Before

Width:  |  Height:  |  Size: 542 B

After

Width:  |  Height:  |  Size: 542 B

129
game.php
View File

@ -1,129 +0,0 @@
<?php
require("classes.php");
$gridStr = basename(strip_tags($_SERVER["REQUEST_URI"]));
// URL contains grid
if (preg_match("#^[1-9.]{81}$#", $gridStr)) {
?>
<!DOCTYPE html>
<html lang='fr'>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width' />
<title>Sudoku</title>
<link rel='stylesheet' type='text/css' href='style.css' />
<script src='app.js'></script>
<link rel="icon" type="image/png" href="favicon.php?size=16&grid=<?=$gridStr?>" sizes="16x16">
<link rel="icon" type="image/png" href="favicon.php?size=32&grid=<?=$gridStr?>" sizes="32x32">
</head>
<body>
<header>
<h1>Sudoku</h1>
</header>
<section>
Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.
</section>
<form id='sudokuForm'>
<div>
<table id='grid' class='grid'>
<tbody>
<?php
for ($row = 0; $row < 9; $row++) {
?>
<tr>
<?php
for ($column = 0; $column < 9; $column++) {
$value = $gridStr[9*$row+$column];
?>
<td>
<?php
if ($value == UNKNOWN) {
?>
<input type='number' min='1' max='9' step='1' value='' title='Valeurs possibles [Clic-droit]'/>
<?php
} else {
?>
<input type='number' min='1' max='9' step='1' value='<?=$value?>' disabled/>
<?php
}
?>
</td>
<?php
}
?>
</tr>
<?php
}
?>
</tbody>
</table>
</div>
<div id='buttons' class='highlight-buttons'>
<?php
for($value=1; $value<=9; $value++) {
echo " <button type='button' onclick='highlight(\"$value\")' title='Surligner les $value' accesskey='$value'>$value</button>\n";
}
?>
</div>
<div>
<button id='inkPenButton' type='button' onclick='useInkPen()' title='Stylo' class='pressed'>
<img src="img/ink-pen.png" alt='Stylo' width=16 height=16/>
</button>
<button id='pencilButton' type='button' onclick='usePencil()' title='Crayon'>
<img src="img/pencil.png" alt='Crayon' width=16 height=16/>
</button>
<button type='button' onclick='erasePencil()' title='Effacer le crayon'>
<img src="img/pencil-eraser.png" alt="Gomme blanche" width=16 height=16/>
</button>
<button class="warning" type='button' onclick='eraseAll()' title='Effacer tout'>
<img src="img/ink-eraser.png" alt="Gomme bleue" width=16 height=16/>
</button>
<button id='undoButton' type='button' onclick='undo()' disabled title='Annuler' accesskey='z'>
<img src="img/undo.png" alt="Annuler" width=16 height=16/>
</button>
<!--<input id='colorPicker' type='color' title='Changer de couleur de stylo' value='#00008b'/> -->
</div>
</form>
<ul id="contextMenu" class="context-menu">
</ul>
<footer>
<a href=''>Lien vers cette grille</a><br/>
<a href='.'>Nouvelle grille</a>
</footer>
</body>
</html>
<?php
} else {
$grid = new Grid();
$grid->generate();
header("HTTP/1.0 400 Bad Request", true, 400);
$urlDir = $_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . dirname($_SERVER["DOCUMENT_URI"]);
$urlExample = $urlDir . "/" . $grid->toString();
?>
<!DOCTYPE html>
<html lang='fr'>
<head>
<meta charset='utf-8' />
<meta name='viewport' content='width=device-width' />
<title>Grille incorrecte</title>
<link rel='stylesheet' type='text/css' href='style.css' />
<link rel="icon" type="image/png" href="favicon.png">
</head>
<body>
<header>
<h1>Grille incorrecte</h1>
</header>
L'adresse URL doit être de la forme : <?=$urlDir?>/<em>grille</em>,<br/>
<em>grille</em> étant une suite de 81 caractères représentant la grille de gauche à droite puis de haut en bas, soit :
<ul>
<li>un chiffre entre 1 et 9 pour les cases connues</li>
<li>un point pour les case vides</li>
</ul>
Exemple : <a href='<?=$urlExample?>'><?=$urlExample?></a><br/>
</body>
</html>
<?php
}
?>

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">

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

45
index.php Normal file → Executable file
View File

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

137
manifest.php Executable file
View File

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

View File

@ -1,23 +0,0 @@
location /sudoku/ {
alias /var/www/sudoku/;
index index.php;
try_files $uri $uri/ @sudoku;
location ~ [^/]\.php(/|$) {
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
fastcgi_pass unix:/var/run/php/php7.3-fpm-sudoku.sock;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param REMOTE_USER $remote_user;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param SCRIPT_FILENAME $request_filename;
}
}
location @sudoku {
fastcgi_pass unix:/var/run/php/php7.3-fpm-sudoku.sock;
include fastcgi_params;
fastcgi_param REMOTE_USER $remote_user;
fastcgi_param PATH_INFO $fastcgi_path_info;
fastcgi_param SCRIPT_FILENAME sudoku/game.php;
}

86
service-worker.js Executable file
View File

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

327
style.css Normal file → Executable file
View File

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

394
sudoku.js Executable file
View File

@ -0,0 +1,394 @@
const VALUES = "123456789"
const UNKNOWN = '.'
let boxes = []
let rows = Array.from(Array(9), x => [])
let columns = Array.from(Array(9), x => [])
let regions = Array.from(Array(9), x => [])
let areaNames = {
ligne: rows,
colonne: columns,
région: regions,
}
let valueToInsert = ""
let easyBoxes = []
let insertRadios = []
function shuffle(iterable) {
array = Array.from(iterable)
if (array.length > 1) {
let i, j, tmp
for (i = array.length - 1; i > 0; i--) {
j = Math.floor(Math.random() * (i + 1))
tmp = array[i]
array[i] = array[j]
array[j] = tmp
}
}
return array
}
window.onload = function() {
let rowId = 0
for (let row of grid.getElementsByTagName('tr')) {
let columnId = 0
for (let box of row.getElementsByTagName('input')) {
let regionId = rowId - rowId % 3 + Math.floor(columnId / 3)
if (!box.disabled) {
box.onfocus = onfocus
box.oninput = oninput
box.onblur = onblur
box.onclick = onclick
box.onmouseenter = onmouseenter
box.onmouseleave = onmouseleave
}
box.oncontextmenu = oncontextmenu
box.rowId = rowId
box.columnId = columnId
box.regionId = regionId
boxes.push(box)
rows[rowId].push(box)
columns[columnId].push(box)
regions[regionId].push(box)
columnId++
}
rowId++
}
if (localStorage["tool"] == "sight") sightCheckbox.checked = true
else if (localStorage["tool"] == "highlighter") highlighterCheckbox.checked = true
colorPickerInput.value = window.getComputedStyle(grid).getPropertyValue("--bs-body-color")
boxes.forEach(box => {
box.neighbourhood = new Set(rows[box.rowId].concat(columns[box.columnId]).concat(regions[box.regionId]))
box.andNeighbourhood = Array.from(box.neighbourhood)
box.neighbourhood.delete(box)
box.neighbourhood = Array.from(box.neighbourhood)
})
insertRadios = Array.from(insertRadioGroup.getElementsByTagName("input")).slice(1)
for (label of document.getElementsByTagName("label")) {
label.control.label = label
}
let accessKeyModifiers = (/Win/.test(navigator.userAgent) || /Linux/.test(navigator.userAgent)) ? "Alt+Maj+"
: (/Mac/.test(navigator.userAgent)) ? "⌃⌥"
: "AccessKey+"
for (node of document.querySelectorAll("*[accesskey]")) {
shortcut = ` [${node.accessKeyLabel||(accessKeyModifiers+node.accessKey)}]`
if (node.title) node.title += shortcut
else if (node.label) node.label.title += shortcut
}
loadGame(history.state)
}
window.onpopstate = (event) => loadGame(event.state)
function loadGame(state) {
if (state) {
boxes.forEach((box, i) => {
if (!box.disabled) {
box.value = state.boxesValues[i]
box.placeholder = state.boxesPlaceholders[i]
}
})
restartLink.classList.remove("disabled")
undoButton.disabled = false
fixGridLink.href = "?" + state.boxesValues.map(value => value || UNKNOWN).join("")
} else {
boxes.filter(box => !box.disabled).forEach(box => {
box.value = ""
box.placeholder = ""
})
restartLink.classList.add("disabled")
undoButton.disabled = true
fixGridLink.href = ""
}
checkBoxes()
enableRadio()
highlight()
}
function searchCandidatesOf(box) {
box.candidates = new Set(VALUES)
box.neighbourhood.forEach(neighbour => box.candidates.delete(neighbour.value))
if (!box.disabled) {
switch (box.candidates.size) {
case 0:
box.title = "Aucune possibilité !"
break
case 1:
box.title = "Une seule possibilité [Clic-droit]"
break
default:
box.title = box.candidates.size + " possibilités [Clic-droit]"
}
}
}
function onfocus() {
if (pencilRadio.checked) {
this.type = "text"
this.value = this.placeholder
this.placeholder = ""
this.classList.add("pencil")
} else {
this.select()
}
if (penColor && inkPenRadio.checked) {
this.style.setProperty("color", penColor)
}
this.style.caretColor = valueToInsert ? "transparent" : "auto"
}
function onclick() {
if (inkPenRadio.checked) {
if (valueToInsert) {
this.value = valueToInsert
this.oninput()
} else {
this.select()
}
} else if (pencilRadio.checked) {
if (valueToInsert) {
this.value = Array.from(new Set(this.value + valueToInsert)).join("")
this.oninput()
}
} else if (eraserRadio.checked) {
this.value = ""
this.placeholder = ""
this.oninput()
}
}
function oninput() {
if (inkPenRadio.checked) {
checkBoxes()
enableRadio()
highlight()
fixGridLink.href = "?" + boxes.map(box => box.value || UNKNOWN).join("")
}
saveGame()
}
function checkBoxes() {
boxes.forEach(box => {
box.setCustomValidity("")
box.classList.remove("is-invalid")
box.parentElement.classList.remove("table-danger")
searchCandidatesOf(box)
if (box.candidates.size == 0) {
box.setCustomValidity("Aucun chiffre possible !")
box.classList.add("is-invalid")
}
})
for (let [areaName, areas] of Object.entries(areaNames))
for (area of areas)
area.filter(box => box.value).sort((box, neighbour) => {
if(box.value == neighbour.value) {
area.forEach(neighbour => neighbour.parentElement.classList.add("table-danger"))
for (neighbour of [box, neighbour]) {
neighbour.setCustomValidity(`Il y a un autre ${box.value} dans cette ${areaName}.`)
neighbour.classList.add("is-invalid")
}
}
return box.value - neighbour.value
})
if (sudokuForm.checkValidity()) { // Correct grid
if (boxes.filter(box => box.value == "").length == 0) {
grid.classList.add("table-success")
setTimeout(() => {
if (confirm(`Bravo ! Vous avez résolu la grille. En voulez-vous une autre ?`))
location = "."
}, 400)
} else {
grid.classList.remove("table-success")
}
} else { // Errors on grid
grid.classList.remove("table-success")
sudokuForm.reportValidity()
}
}
function enableRadio() {
for (radio of insertRadios) {
if (boxes.filter(box => box.value == "").some(box => box.candidates.has(radio.value))) {
radio.disabled = false
radio.label.title = `Insérer un ${radio.value} [${radio.accessKeyLabel||(accessKeyModifiers+radio.accessKey)}]`
} else {
radio.disabled = true
radio.label.title = `Tous les ${radio.value} sont posés.`
if (valueToInsert == radio.value) {
insertRadio0.checked = true
valueToInsert = ""
grid.style.cursor = "text"
}
}
}
}
function highlight() {
hintButton.disabled = true
easyBoxes = []
boxes.forEach(box => {
if (valueToInsert && box.value == valueToInsert) {
box.parentElement.classList.add("table-primary")
box.tabIndex = -1
} else {
box.parentElement.classList.remove("table-primary")
box.tabIndex = 0
}
if (valueToInsert && highlighterCheckbox.checked && !box.candidates.has(valueToInsert)) {
box.parentElement.classList.add("table-active")
box.tabIndex = -1
} else {
box.parentElement.classList.remove("table-active")
box.tabIndex = 0
}
if (!box.value && box.candidates.size == 1) {
hintButton.disabled = false
easyBoxes.push(box)
}
})
highlighterCheckbox.label.title = "Surligner les lignes, colonnes et régions contenant déjà " + (valueToInsert ? "un " + valueToInsert : "le chiffre sélectionné")
}
function onblur() {
if (this.classList.contains("pencil")) {
this.placeholder = this.value
this.value = ""
this.type = "number"
this.classList.remove("pencil")
}
}
function saveGame() {
history.pushState({
boxesValues: boxes.map(box => box.value),
boxesPlaceholders: boxes.map(box => box.placeholder)
}, "")
restartLink.classList.remove("disabled")
undoButton.disabled = false
}
function onmouseenter(event) {
if (sightCheckbox.checked){
box = event.target
box.andNeighbourhood.forEach(neighbour => {
neighbour.parentElement.classList.add("table-active")
})
box.neighbourhood.forEach(neighbour => {
if (valueToInsert && neighbour.value == valueToInsert) {
for (neighbour of [box, neighbour]) {
neighbour.parentElement.classList.add("table-danger", "not-allowed")
}
}
})
}
}
function onmouseleave(event) {
if (sightCheckbox.checked){
box = event.target
box.andNeighbourhood.forEach(neighbour => {
neighbour.parentElement.classList.remove("table-active", "table-danger", "not-allowed")
})
}
}
function insert(radio) {
if (radio.value && valueToInsert == radio.value) {
radio.blur()
insertRadio0.checked = true
insert(0)
} else {
valueToInsert = radio.value
grid.style.cursor = valueToInsert ? "pointer" : "text"
highlight()
}
}
let penColor
function changeColor() {
penColor = colorPickerInput.value
colorPickerLabel.style.color = colorPickerInput.value
}
function restart() {
if (confirm("Effacer toutes les cases ?")) {
restartButton.disabled = true
location.hash = ""
}
}
function showHint() {
if (easyBoxes.length) {
shuffle(easyBoxes)
let box = easyBoxes.pop()
box.placeholder = "💡"
box.focus()
return box
}
hintButton.disabled = true
}
function oncontextmenu(event) {
event.preventDefault()
while (contextMenu.firstChild) contextMenu.firstChild.remove()
const box = event.target
if (box.candidates.size) {
Array.from(box.candidates).sort().forEach(candidate => {
li = document.createElement("li")
li.innerText = candidate
li.classList = "list-group-item list-group-item-action"
li.onclick = function(e) {
contextMenu.style.display = "none"
valueToInsert = e.target.innerText
grid.style.cursor = "pointer"
document.getElementById("insertRadio" + valueToInsert).checked = true
box.onclick()
}
li.oncontextmenu = function(e) {
e.preventDefault()
li.onclick(e)
}
contextMenu.appendChild(li)
})
} else {
li = document.createElement("li")
li.innerText = "Aucune possibilité !"
li.classList = "list-group-item list-group-item-action disabled"
contextMenu.appendChild(li)
}
contextMenu.style.left = `${event.pageX}px`
contextMenu.style.top = `${event.pageY}px`
contextMenu.style.display = "block"
document.onclick = function(event) {
contextMenu.style.display = "none"
document.onclick = null
}
return false
}
document.onkeydown = function(event) {
if (event.key == "Escape" && contextMenu.style.display == "block") {
event.preventDefault()
contextMenu.style.display = "none"
}
}
window.onbeforeunload = function(event) {
saveGame()
if (sightCheckbox.checked) localStorage["tool"] = "sight"
else if (highlighterCheckbox.checked) localStorage["tool"] = "highlighter"
}

90
sudoku.php Executable file
View File

@ -0,0 +1,90 @@
<!DOCTYPE html>
<html lang='fr' prefix="og: https://ogp.me/ns#">
<head>
<?php require_once("head.php") ?>
</head>
<body>
<nav class="navbar mb-4">
<h1 class="display-4 text-center m-auto">Sudoku</h1>
</nav>
<div class="row g-0">
<main class="col-md-6 order-md-1">
<div class="text-center m-auto" style="width: min-content;">
<div class='d-flex justify-content-between mb-2'>
<div class='btn-group'>
<input type='radio' id='inkPenRadio' class='btn-check' name='penRadioGroup' checked />
<label for='inkPenRadio' class='btn btn-primary' title='Écrire un chiffre'><i class="ri-ball-pen-fill"></i></label>
<input type='radio' id='pencilRadio' class='btn-check' name='penRadioGroup' />
<label for='pencilRadio' class='btn btn-primary' title='Prendre des notes'><i class="ri-pencil-fill"></i></label>
<input type='radio' id='eraserRadio' class='btn-check' name='penRadioGroup' />
<label for='eraserRadio' class='btn btn-primary' title='Effacer une case'><i class="ri-eraser-fill"></i></label>
</div>
<input type="color" class="btn-check" id="colorPickerInput" title="Changer la couleur" oninput="changeColor()"/>
<label id="colorPickerLabel" for="colorPickerInput" class="btn btn-primary" title="Changer de couleur"><i class="ri-palette-fill"></i></label>
<div class='btn-group'>
<input type='checkbox' id='sightCheckbox' class='btn-check' onclick='highlighterCheckbox.checked = false; highlight()' />
<label for='sightCheckbox' class='btn btn-info' title='Surligner la ligne, la colonne et la région de la case survolée'><i class="ri-focus-3-line"></i></label>
<input type='checkbox' id='highlighterCheckbox' class='btn-check' onclick='sightCheckbox.checked = false; highlight()' />
<label for='highlighterCheckbox' class='btn btn-info' title='Surligner les lignes, colonnes et régions contenant déjà le chiffre sélectionné'><i class="ri-mark-pen-fill"></i></label>
</div>
<button id="hintButton" type="button" class='btn btn-info' onclick="showHint()" title="Montrer une case avec une seule possibilité" accesskey="H" disabled=""><i class="ri-lightbulb-line"></i></button>
<a id='restartLink' class='btn btn-primary disabled' href="" title='Recommencer'><i class="ri-restart-line"></i></a>
<button id='undoButton' type='button' class='btn btn-primary' onclick='window.history.back()' disabled title='Annuler' accesskey='Z'><i class="ri-arrow-go-back-fill"></i></button>
</div>
<form id='sudokuForm' class='needs-validation' novalidate>
<table id='grid' class='table mb-2'>
<tbody>
<?php for ($row = 0; $row < 81; $row += 9): ?>
<tr class="input-group d-inline-block w-auto">
<?php for ($column = 0; $column < 9; $column++): $value = $currentGrid[$row+$column]; ?>
<?php if ($value == UNKNOWN): ?>
<td><input type='number' min='1' max='9' step='1' value='' class='form-control' /></td>
<?php else: ?>
<td><input type='number' min='1' max='9' step='1' value='<?=$value?>' class='form-control' disabled /></td>
<?php endif ?>
<?php endfor?>
</tr>
<?php endfor?>
</tbody>
</table>
</form>
<div class='d-flex mb-4'>
<div id='insertRadioGroup' class='radioGroup btn-group flex-fill'>
<input type='radio' class='btn-check' id='insertRadio0' value='' name='insertRadioGroup' onclick='insert(this)' accesskey='0' checked /><label for='insertRadio0' class='btn btn-primary' title='Clavier'><i class="ri-input-cursor-move"></i></label>
<?php for($value=1; $value<=9; $value++): ?>
<input type='radio' class='btn-check' id='insertRadio<?=$value?>' value='<?=$value?>' name='insertRadioGroup' onclick='insert(this)' accesskey='<?=$value?>' disabled />
<label for='insertRadio<?=$value?>' class='btn btn-primary' title='Insérer un <?=$value?>'><?=$value?></label>
<?php endfor ?>
</div>
</div>
<div class='mb-3'>
<?php if (isset($warning)): ?>
<strong>⚠️ <?=$warning?> ⚠️</strong><br/>
<?php else: ?>
Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.
<?php endif?>
</div>
</div>
</main>
<aside class="col-md-3 text-center text-md-start">
<div class="d-flex flex-column flex-shrink-0 p-3">
<ul class="nav nav-pills flex-column">
<li><a href="." class="nav-link link-body-emphasis">Nouvelle grille</a></li>
<li><a href="" class="nav-link link-body-emphasis">Lien vers cette grille</a></li>
<li><a href="?---------------------------------------------------------------------------------" class="nav-link link-body-emphasis">Grille vierge</a></li>
<li><a id="fixGridLink" href="" class="nav-link link-body-emphasis">Figer la grille</a></li>
<li><a href="https://git.malingrey.fr/adrien/Sudoku" class="nav-link link-body-emphasis">Code source</a></li>
<li><a href=".." class="nav-link link-body-emphasis">Autres jeux</a></li>
</ul>
</div>
</aside>
</div>
<ul id='contextMenu' class='context-menu modal-content shadow list-group w-auto position-absolute'></ul>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
<script src='sudoku.js' defer></script>
<script>navigator?.serviceWorker.register('service-worker.js')</script>
</body>
</html>

120
thumbnail.php Executable file
View File

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

BIN
thumbnail.png Executable file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB