Compare commits
1 Commits
master
...
2d431a604f
Author | SHA1 | Date | |
---|---|---|---|
2d431a604f |
27
400.php
@ -1,27 +0,0 @@
|
||||
<?php http_response_code(400); ?>
|
||||
<!DOCTYPE html>
|
||||
<html lang='fr'>
|
||||
<head>
|
||||
<?php require("head.php"); ?>
|
||||
<title>Requête incorrecte</title>
|
||||
</head>
|
||||
<body>
|
||||
<nav class="navbar mb-4">
|
||||
<h1 class="display-4 text-center m-auto">Sudoku</h1>
|
||||
</nav>
|
||||
<main class="container my-4">
|
||||
<header>
|
||||
<h1 class="mb-4">Requête incorrecte</h1>
|
||||
</header>
|
||||
L'adresse URL doit être de la forme :<br/>
|
||||
<?=$dirUrl?>/?<em>grille</em><br/>
|
||||
<em>grille</em> étant une suite de 81 caractères représentant la grille de gauche à droite puis de haut en bas, soit :
|
||||
<ul>
|
||||
<li>un chiffre entre 1 et 9 pour les cases connues</li>
|
||||
<li>un tiret (-) pour les case vides</li>
|
||||
</ul>
|
||||
Exemple :<br/>
|
||||
<a href='<?=$newGridUrl?>'><?=$newGridUrl?></a>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
@ -1,5 +0,0 @@
|
||||
# Sudoku
|
||||
|
||||
Web sudoku assistant
|
||||
|
||||

|
315
app.js
Normal file
@ -0,0 +1,315 @@
|
||||
const VALUES = "123456789"
|
||||
const SUGESTION_DELAY = 60000 //ms
|
||||
|
||||
let boxes = []
|
||||
let rows = Array.from(Array(9), x => [])
|
||||
let columns = Array.from(Array(9), x => [])
|
||||
let regions = Array.from(Array(9), x => [])
|
||||
let suggestionTimer= null
|
||||
let highlightedValue = ""
|
||||
let history = []
|
||||
let accessKeyModifiers = "AccessKey+"
|
||||
let penStyle = "ink-pen"
|
||||
|
||||
window.onload = function() {
|
||||
let rowId = 0
|
||||
for (let row of grid.getElementsByTagName('tr')) {
|
||||
let columnId = 0
|
||||
for (let box of row.getElementsByTagName('input')) {
|
||||
let regionId = rowId - rowId%3 + Math.floor(columnId/3)
|
||||
if (!box.disabled) {
|
||||
box.onfocus = onfocus
|
||||
box.oninput = oninput
|
||||
box.onblur = onblur
|
||||
}
|
||||
box.oncontextmenu = oncontextmenu
|
||||
box.rowId = rowId
|
||||
box.columnId = columnId
|
||||
box.regionId = regionId
|
||||
boxes.push(box)
|
||||
rows[rowId].push(box)
|
||||
columns[columnId].push(box)
|
||||
regions[regionId].push(box)
|
||||
columnId++
|
||||
}
|
||||
rowId++
|
||||
}
|
||||
|
||||
let savedGame = localStorage[location.href]
|
||||
if (savedGame) {
|
||||
boxes.forEach((box, i) => {
|
||||
if (!box.disabled && savedGame[i] != '.') box.value = savedGame[i]
|
||||
})
|
||||
}
|
||||
|
||||
boxes.forEach(box => {
|
||||
box.neighbourhood = new Set(rows[box.rowId].concat(columns[box.columnId]).concat(regions[box.regionId]))
|
||||
box.neighbourhood.delete(box)
|
||||
box.neighbourhood = Array.from(box.neighbourhood)
|
||||
searchCandidatesOf(box)
|
||||
})
|
||||
|
||||
enableButtons()
|
||||
highlightAndTab()
|
||||
|
||||
if (/Win/.test(navigator.platform) || /Linux/.test(navigator.platform)) accessKeyModifiers = "Alt+Maj+"
|
||||
else if (/Mac/.test(navigator.platform)) accessKeyModifiers = "⌃⌥"
|
||||
for(node of document.querySelectorAll("*[accesskey]")) {
|
||||
node.title += " [" + (node.accessKeyLabel || accessKeyModifiers + node.accessKey) + "]"
|
||||
}
|
||||
|
||||
document.onclick = function (event) {
|
||||
contextMenu.style.display = "none"
|
||||
}
|
||||
suggestionTimer = setTimeout(showSuggestion, 30000)
|
||||
}
|
||||
|
||||
function searchCandidatesOf(box) {
|
||||
box.candidates = new Set(VALUES)
|
||||
box.neighbourhood.forEach(neighbour => box.candidates.delete(neighbour.value))
|
||||
if (!box.disabled) {
|
||||
switch (box.candidates.size) {
|
||||
case 0:
|
||||
box.title = "Aucune possibilité !"
|
||||
break
|
||||
case 1:
|
||||
box.title = "1 possibilité [Clic-droit]"
|
||||
break
|
||||
default:
|
||||
box.title = box.candidates.size + " possibilités [Clic-droit]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onfocus() {
|
||||
if (penStyle == "pencil" && this.value == "") {
|
||||
this.value = this.placeholder
|
||||
this.placeholder = ""
|
||||
this.classList.add("pencil")
|
||||
} else {
|
||||
this.select()
|
||||
}
|
||||
}
|
||||
|
||||
function oninput() {
|
||||
history.push({box: this, value: this.previousValue || "", placeholder: this.previousPlaceholder || ""})
|
||||
undoButton.disabled = false
|
||||
if (penStyle != "pencil") {
|
||||
refresh(this)
|
||||
}
|
||||
}
|
||||
|
||||
function undo() {
|
||||
if (history.length) {
|
||||
previousState = history.pop()
|
||||
previousState.box.value = previousState.value
|
||||
previousState.box.placeholder = previousState.placeholder
|
||||
refresh(previousState.box)
|
||||
if (history.length < 1) undoButton.disabled = true
|
||||
}
|
||||
}
|
||||
|
||||
function refresh(box) {
|
||||
localStorage[location.href] = boxes.map(box => box.value || ".").join("")
|
||||
|
||||
box.neighbourhood.concat([box]).forEach(neighbour => {
|
||||
searchCandidatesOf(neighbour)
|
||||
neighbour.setCustomValidity("")
|
||||
neighbour.required = false
|
||||
})
|
||||
|
||||
enableButtons()
|
||||
highlightAndTab()
|
||||
|
||||
for (neighbour1 of box.neighbourhood) {
|
||||
if (neighbour1.value.length == 1) {
|
||||
for (area of [
|
||||
{name: "région", neighbours: regions[neighbour1.regionId]},
|
||||
{name: "ligne", neighbours: rows[neighbour1.rowId]},
|
||||
{name: "colonne", neighbours: columns[neighbour1.columnId]},
|
||||
])
|
||||
for (neighbour2 of area.neighbours)
|
||||
if (neighbour2 != neighbour1 && neighbour2.value == neighbour1.value) {
|
||||
for (neighbour of [neighbour1, neighbour2]) {
|
||||
neighbour.setCustomValidity(`Il y a un autre ${neighbour.value} dans cette ${area.name}.`)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (neighbour1.candidates.size == 0) {
|
||||
neighbour1.setCustomValidity("Aucun chiffre possible !")
|
||||
neighbour1.required = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (box.form.checkValidity()) { // Correct grid
|
||||
if (boxes.filter(box => box.value == "").length == 0) {
|
||||
alert(`Bravo ! Vous avez résolu la grille.`)
|
||||
} else {
|
||||
if (suggestionTimer) clearTimeout(suggestionTimer)
|
||||
suggestionTimer = setTimeout(showSuggestion, SUGESTION_DELAY)
|
||||
}
|
||||
} else { // Errors on grid
|
||||
box.form.reportValidity()
|
||||
box.select()
|
||||
}
|
||||
}
|
||||
|
||||
function onblur() {
|
||||
if (this.classList.contains("pencil")) {
|
||||
this.placeholder = this.value
|
||||
this.value = ""
|
||||
this.classList.remove("pencil")
|
||||
}
|
||||
this.previousValue = this.value
|
||||
this.previousPlaceholder = this.placeholder
|
||||
}
|
||||
|
||||
function enableButtons() {
|
||||
for (button of buttons.getElementsByTagName("button")) {
|
||||
if (boxes.filter(box => box.value == "").some(box => box.candidates.has(button.textContent))) {
|
||||
button.disabled = false
|
||||
} else {
|
||||
button.disabled = true
|
||||
if (highlightedValue == button.textContent) highlightedValue = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function highlight(value) {
|
||||
if (value == highlightedValue) {
|
||||
highlightedValue = ""
|
||||
} else {
|
||||
highlightedValue = value
|
||||
}
|
||||
for (button of buttons.getElementsByTagName("button")) {
|
||||
if (button.textContent == highlightedValue) button.classList.add("pressed")
|
||||
else button.classList.remove("pressed")
|
||||
}
|
||||
highlightAndTab()
|
||||
boxes.filter(box => box.value == "" && box.tabIndex == 0)[0].focus()
|
||||
}
|
||||
|
||||
function highlightAndTab() {
|
||||
if (highlightedValue) {
|
||||
boxes.forEach(box => {
|
||||
if (box.value == highlightedValue) {
|
||||
box.classList.add("same-value")
|
||||
box.tabIndex = -1
|
||||
}
|
||||
else {
|
||||
box.classList.remove("same-value")
|
||||
if (box.candidates.has(highlightedValue)) {
|
||||
box.classList.remove("forbidden-value")
|
||||
box.tabIndex = 0
|
||||
} else {
|
||||
box.classList.add("forbidden-value")
|
||||
box.tabIndex = -1
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
boxes.forEach(box => {
|
||||
box.classList.remove("same-value", "forbidden-value")
|
||||
box.tabIndex = 0
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function shuffle(iterable) {
|
||||
array = Array.from(iterable)
|
||||
if (array.length > 1) {
|
||||
let i, j, tmp
|
||||
for (i = array.length - 1; i > 0; i--) {
|
||||
j = Math.floor(Math.random() * (i+1))
|
||||
tmp = array[i]
|
||||
array[i] = array[j]
|
||||
array[j] = tmp
|
||||
}
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
easyFirst = (box1, box2) => box1.candidates.size - box2.candidates.size
|
||||
|
||||
function showSuggestion() {
|
||||
const emptyBoxes = boxes.filter(box => box.value == "" && box.candidates.size == 1)
|
||||
if (emptyBoxes.length) {
|
||||
shuffle(emptyBoxes)[0].placeholder = "💡"
|
||||
} else {
|
||||
clearTimeout(suggestionTimer)
|
||||
suggestionTimer = null
|
||||
}
|
||||
}
|
||||
|
||||
function oncontextmenu(event) {
|
||||
event.preventDefault()
|
||||
while (contextMenu.firstChild) contextMenu.firstChild.remove()
|
||||
const box = event.target
|
||||
if (box.candidates.size) {
|
||||
candidatesArray = Array.from(box.candidates).sort().forEach(candidate => {
|
||||
li = document.createElement("li")
|
||||
li.innerText = candidate
|
||||
li.onclick = function (event) {
|
||||
contextMenu.style.display = "none"
|
||||
box.value = event.target.innerText
|
||||
oninput.apply(box)
|
||||
}
|
||||
contextMenu.appendChild(li)
|
||||
})
|
||||
} else {
|
||||
li = document.createElement("li")
|
||||
li.innerText = "Aucune possibilité !"
|
||||
li.classList.add("error")
|
||||
contextMenu.appendChild(li)
|
||||
}
|
||||
contextMenu.style.left = `${event.pageX}px`
|
||||
contextMenu.style.top = `${event.pageY}px`
|
||||
contextMenu.style.display = "block"
|
||||
return false
|
||||
}
|
||||
|
||||
function useInkPen() {
|
||||
inkPenButton.classList.add("pressed")
|
||||
pencilButton.classList.remove("pressed")
|
||||
penStyle = "ink-pen"
|
||||
}
|
||||
|
||||
function usePencil() {
|
||||
pencilButton.classList.add("pressed")
|
||||
inkPenButton.classList.remove("pressed")
|
||||
penStyle = "pencil"
|
||||
}
|
||||
|
||||
function erase(someBoxes) {
|
||||
for (box of someBoxes) {
|
||||
box.value = ""
|
||||
box.placeholder = ""
|
||||
searchCandidatesOf(box)
|
||||
refresh(box)
|
||||
}
|
||||
enableButtons()
|
||||
highlightAndTab()
|
||||
}
|
||||
|
||||
function erasePencil() {
|
||||
if (confirm("Effacer les chiffres écrits au crayon ?")) {
|
||||
boxes.filter(box => !box.disabled).forEach(box => {
|
||||
box.placeholder = ""
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function eraseAll() {
|
||||
if (confirm("Effacer tous les chiffres écrits au crayon et au stylo ?")) {
|
||||
boxes.filter(box => !box.disabled).forEach(box => {
|
||||
box.value = ""
|
||||
box.placeholder = ""
|
||||
box.setCustomValidity("")
|
||||
box.required = false
|
||||
})
|
||||
boxes.forEach(searchCandidatesOf)
|
||||
enableButtons()
|
||||
highlightAndTab()
|
||||
}
|
||||
}
|
153
classes.php
Executable file → Normal file
@ -1,7 +1,5 @@
|
||||
<?php
|
||||
const UNKNOWN = "-";
|
||||
|
||||
$validGrids = array();
|
||||
const UNKNOWN = ".";
|
||||
|
||||
function isKnown($box) {
|
||||
return $box->value != UNKNOWN;
|
||||
@ -26,20 +24,16 @@
|
||||
}
|
||||
|
||||
class Box {
|
||||
public $values = array('1', '2', '3', '4', '5', '6', '7', '8', '9');
|
||||
public $value = UNKNOWN;
|
||||
public $rowId;
|
||||
public $columnId;
|
||||
public $regionId;
|
||||
public $candidates;
|
||||
public $candidateRemoved = array();
|
||||
public $neighbourhood = array();
|
||||
|
||||
public $values = array("1", "2", "3", "4", "5", "6", "7", "8", "9");
|
||||
|
||||
function __construct($rowId, $columnId, $regionId) {
|
||||
$this->value = UNKNOWN;
|
||||
$this->rowId = $rowId;
|
||||
$this->columnId = $columnId;
|
||||
$this->regionId = $regionId;
|
||||
$this->candidates = $this->values;
|
||||
$this->candidateRemoved = array();
|
||||
$this->neighbourhood = array();
|
||||
}
|
||||
|
||||
function searchCandidates() {
|
||||
@ -52,19 +46,14 @@
|
||||
}
|
||||
|
||||
class Grid {
|
||||
|
||||
private $boxes = array();
|
||||
private $rows;
|
||||
private $columns;
|
||||
private $regions;
|
||||
|
||||
function __construct($gridStr="") {
|
||||
function __construct() {
|
||||
$this->boxes = array();
|
||||
$this->rows = array_fill(0, 9, array());
|
||||
$this->columns = array_fill(0, 9, array());
|
||||
$this->regions = array_fill(0, 9, array());
|
||||
for ($regionRowId = 0; $regionRowId < 3; $regionRowId++) {
|
||||
for ($rowId = 3*$regionRowId; $rowId < 3*($regionRowId+1); $rowId++) {
|
||||
for($regionColumnId = 0; $regionColumnId < 3; $regionColumnId++) {
|
||||
for($regionColumnId = 0; $regionColumnId < 3; $regionColumnId++) {
|
||||
for ($rowId = 3*$regionRowId; $rowId < 3*($regionRowId+1); $rowId++) {
|
||||
for ($columnId = 3*$regionColumnId; $columnId < 3*($regionColumnId+1); $columnId++) {
|
||||
$regionId = 3*$regionRowId + $regionColumnId;
|
||||
$box = new Box($rowId, $columnId, $regionId);
|
||||
@ -83,103 +72,60 @@
|
||||
if ($box != $neighbour && !in_array($neighbour, $box->neighbourhood))
|
||||
$box->neighbourhood[] = $neighbour;
|
||||
}
|
||||
|
||||
if ($gridStr) {
|
||||
$this->import($gridStr);
|
||||
} else {
|
||||
$this->generate();
|
||||
}
|
||||
}
|
||||
|
||||
function import($gridStr) {
|
||||
foreach ($this->boxes as $i => $box) {
|
||||
$box->value = $gridStr[$i];
|
||||
}
|
||||
forEach($this->boxes as $box) {
|
||||
forEach($box->neighbourhood as $neighbour)
|
||||
array_unset_value($box->value, $neighbour->candidates);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function generate() {
|
||||
// Init with a shuffle row
|
||||
$values = array("1", "2", "3", "4", "5", "6", "7", "8", "9");
|
||||
shuffle($values);
|
||||
forEach($this->rows[0] as $columnId => $box) {
|
||||
$box->value = $values[$columnId];
|
||||
forEach($values as $columnId => $value) {
|
||||
$box = $this->rows[0][$columnId];
|
||||
$box->value = $value;
|
||||
forEach($box->neighbourhood as $neighbour)
|
||||
array_unset_value($box->value, $neighbour->candidates);
|
||||
}
|
||||
// Fill grid
|
||||
$this->solutionsGenerator(true)->current();
|
||||
|
||||
// Group boxes with their groupedSymetricals
|
||||
$groupedSymetricals = array(array($this->rows[4][4]));
|
||||
for ($rowId = 0; $rowId <= 3; $rowId++) {
|
||||
for ($columnId = 0; $columnId <= 3; $columnId++) {
|
||||
$groupedSymetricals[] = array(
|
||||
$this->rows[$rowId][$columnId],
|
||||
$this->rows[8-$rowId][8-$columnId],
|
||||
$this->rows[8-$rowId][$columnId],
|
||||
$this->rows[$rowId][8-$columnId]
|
||||
);
|
||||
// Remove clues while there is still a unique solution
|
||||
shuffle($this->boxes);
|
||||
$nbClues = count($this->boxes);
|
||||
foreach($this->boxes as $testBox) {
|
||||
$testBoxes = array($testBox);
|
||||
if ($nbClues >=30)
|
||||
$testBoxes[] = $this->rows[8-$testBox->rowId][8-$testBox->columnId];
|
||||
if ($nbClues >=61) {
|
||||
$testBoxes[] = $this->rows[8-$testBox->rowId][$testBox->columnId];
|
||||
$testBoxes[] = $this->rows[$testBox->rowId][8-$testBox->columnId];
|
||||
}
|
||||
$groupedSymetricals[] = array(
|
||||
$this->rows[$rowId][4],
|
||||
$this->rows[8-$rowId][4]
|
||||
);
|
||||
}
|
||||
for ($columnId = 0; $columnId <= 3; $columnId++) {
|
||||
$groupedSymetricals[] = array(
|
||||
$this->rows[4][$columnId],
|
||||
$this->rows[4][8-$columnId]
|
||||
);
|
||||
}
|
||||
|
||||
// Remove clues randomly and their groupedSymetricals while there is still a unique solution
|
||||
shuffle($groupedSymetricals);
|
||||
foreach($groupedSymetricals as $symetricals) {
|
||||
shuffle($symetricals);
|
||||
foreach ($symetricals as $testBox) {
|
||||
$erasedValue = $testBox->value;
|
||||
$testBoxes = array_filter($testBoxes, "isKnown");
|
||||
$erasedValues = array();
|
||||
forEach($testBoxes as $testBox) {
|
||||
$erasedValues[] = $testBox->value;
|
||||
$testBox->value = UNKNOWN;
|
||||
forEach($testBox->neighbourhood as $neighbour)
|
||||
$neighbour->searchCandidates();
|
||||
if (!$this->isValid()) {
|
||||
$testBox->value = $erasedValue;
|
||||
}
|
||||
if ($this->isValid()) {
|
||||
$nbClues -= count($testBoxes);
|
||||
} else {
|
||||
forEach($testBoxes as $i => $testBox) {
|
||||
$testBox->value = $erasedValues[$i];
|
||||
forEach($testBox->neighbourhood as $neighbour) array_unset_value($testBox->value, $neighbour->candidates);
|
||||
}
|
||||
}
|
||||
}
|
||||
$validGrids[] = $this->toString();
|
||||
}
|
||||
|
||||
function containsDuplicates() {
|
||||
foreach(array_merge($this->rows, $this->columns, $this->regions) as $area) {
|
||||
$knownBoxes = array_filter($area, "isKnown");
|
||||
foreach($knownBoxes as $box1) {
|
||||
foreach($knownBoxes as $box2) {
|
||||
if (($box1 != $box2) && ($box1->value == $box2->value)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
function countSolutions($max=2) {
|
||||
$solutions = $this->solutionsGenerator(false);
|
||||
$solutionsWithoutDuplicates = array();
|
||||
$nbSolutions = 0;
|
||||
foreach($solutions as $solution) {
|
||||
if (!in_array($solution, $solutionsWithoutDuplicates)) {
|
||||
$solutionsWithoutDuplicates[] = $solution;
|
||||
$nbSolutions ++;
|
||||
if ($nbSolutions >= $max) {
|
||||
$solutions->send(true);
|
||||
break;
|
||||
}
|
||||
$solutionsWithoutDuplicates[$solution] = true;
|
||||
$nbSolutions = count($solutionsWithoutDuplicates);
|
||||
if ($nbSolutions >= $max) {
|
||||
$solutions->send(true);
|
||||
}
|
||||
}
|
||||
return $nbSolutions;
|
||||
@ -195,12 +141,20 @@
|
||||
if ($randomized) shuffle($emptyBoxes);
|
||||
usort($emptyBoxes, "easyFirst");
|
||||
$testBox = $emptyBoxes[0];
|
||||
$nbTries = 0;
|
||||
if ($randomized) shuffle($testBox->candidates);
|
||||
$stop = null;
|
||||
foreach($testBox->candidates as $testBox->value) {
|
||||
$correctGrid = true;
|
||||
foreach(array_filter($testBox->neighbourhood, "isUnknown") as $neighbour)
|
||||
$neighbour->candidateRemoved[] = array_unset_value($testBox->value, $neighbour->candidates);
|
||||
if ($this->candidatesOnEachUnknownBoxeOf($testBox->neighbourhood)) {
|
||||
foreach(array_filter($testBox->neighbourhood, "isUnknown") as $neighbour) {
|
||||
if (count($neighbour->candidates) == 0) {
|
||||
$correctGrid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ($correctGrid) {
|
||||
$solutions = $this->solutionsGenerator($randomized);
|
||||
foreach($solutions as $solution) {
|
||||
$stop = (yield $solution);
|
||||
@ -220,21 +174,12 @@
|
||||
yield $this->toString();
|
||||
}
|
||||
}
|
||||
|
||||
function candidatesOnEachUnknownBoxeOf($area) {
|
||||
foreach($area as $box) {
|
||||
if (($box->value == UNKNOWN) && (count($box->candidates) == 0)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function toString() {
|
||||
$str = "";
|
||||
foreach($this->rows as $row) {
|
||||
forEach($row as $box) {
|
||||
$str .= $box->value;
|
||||
$str .= ($box->value? $box->value : UNKNOWN);
|
||||
}
|
||||
}
|
||||
return $str;
|
||||
|
0
favicon.png
Executable file → Normal file
Before Width: | Height: | Size: 542 B After Width: | Height: | Size: 542 B |
32
head.php
@ -1,32 +0,0 @@
|
||||
<meta charset="utf-8" />
|
||||
<title>Sudoku</title><meta property="og:title" content="Sudoku" />
|
||||
<meta property="og:type" content="game" />
|
||||
<meta name="description" property="og:description" content="Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9." />
|
||||
<link rel="canonical" href="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].dirname($_SERVER["DOCUMENT_URI"])?>" />
|
||||
<meta property="og:url" content="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].$_SERVER["DOCUMENT_URI"]?>" />
|
||||
<meta property="og:image" content="<?=$_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].dirname($_SERVER["DOCUMENT_URI"])?>/thumbnail.php?size=200&grid=<?=$currentGrid?>" />
|
||||
<meta property="og:image:width" content="200" />
|
||||
<meta property="og:image:height" content="200" />
|
||||
<meta name="Language" CONTENT="fr" /><meta property="og:locale" content="fr_FR" />
|
||||
<meta property="og:site_name" content="<?=$_SERVER["HTTP_HOST"]?>" />
|
||||
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-dark-5@1.1.3/dist/css/bootstrap-dark.min.css" rel="stylesheet">
|
||||
<link href="https://cdn.jsdelivr.net/npm/remixicon@3.2.0/fonts/remixicon.css" rel="stylesheet">
|
||||
<link href="style.css" rel="stylesheet" type="text/css" />
|
||||
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=196" sizes="196x196" rel="icon" type="image/png">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=160" sizes="160x160" rel="icon" type="image/png">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=96" sizes="96x96" rel="icon" type="image/png">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=16" sizes="16x16" rel="icon" type="image/png">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=32" sizes="32x32" rel="icon" type="image/png">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=152" sizes="152x152" rel="apple-touch-icon">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=144" sizes="144x144" rel="apple-touch-icon">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=120" sizes="120x120" rel="apple-touch-icon">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=114" sizes="114x114" rel="apple-touch-icon">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=57" sizes="57x57" rel="apple-touch-icon">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=72" sizes="72x72" rel="apple-touch-icon">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=60" sizes="60x60" rel="apple-touch-icon">
|
||||
<link href="thumbnail.php?grid=<?=$currentGrid?>&size=76" sizes="76x76" rel="apple-touch-icon">
|
||||
<link href="manifest.php?grid=<?=$currentGrid?>" rel="manifest">
|
BIN
img/ink-eraser.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
img/ink-pen.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
img/pencil-eraser.png
Normal file
After Width: | Height: | Size: 8.8 KiB |
BIN
img/pencil.png
Normal file
After Width: | Height: | Size: 8.0 KiB |
BIN
img/undo.png
Normal file
After Width: | Height: | Size: 21 KiB |
49
index.php
Executable file → Normal file
@ -1,45 +1,8 @@
|
||||
<?php
|
||||
<?php
|
||||
require("classes.php");
|
||||
|
||||
$fullUrl = $_SERVER["REQUEST_SCHEME"]."://".$_SERVER["HTTP_HOST"].$_SERVER["DOCUMENT_URI"];
|
||||
$dirUrl = dirname($fullUrl);
|
||||
$currentGrid = strip_tags($_SERVER['QUERY_STRING']);
|
||||
|
||||
if (preg_match("/^[1-9-]{81}$/", $currentGrid)) {
|
||||
session_id($currentGrid);
|
||||
session_start(["use_cookies" => false]);
|
||||
|
||||
if (!array_key_exists("nbSolutions", $_SESSION)) {
|
||||
$grid = new Grid($currentGrid);
|
||||
$_SESSION["nbSolutions"] = $grid->containsDuplicates() ? -1 : $grid->countSolutions(2);
|
||||
}
|
||||
switch($_SESSION["nbSolutions"]) {
|
||||
case -1:
|
||||
$warning = "Cette grille contient des doublons.";
|
||||
break;
|
||||
case 0:
|
||||
$warning = "Cette grille n'a pas de solution.";
|
||||
break;
|
||||
case 1:
|
||||
break;
|
||||
default:
|
||||
$warning = "Cette grille a plusieurs solutions.";
|
||||
}
|
||||
require("sudoku.php");
|
||||
} else {
|
||||
if ($currentGrid) {
|
||||
require("400.php");
|
||||
} else {
|
||||
$grid = new Grid();
|
||||
$gridAsString = $grid->toString();
|
||||
$newGridUrl = "$dirUrl/?$gridAsString";
|
||||
|
||||
session_id($gridAsString);
|
||||
session_start(["use_cookies" => false]);
|
||||
|
||||
$_SESSION["nbSolutions"] = 1;
|
||||
|
||||
header("Location: $newGridUrl");
|
||||
}
|
||||
}
|
||||
?>
|
||||
$grid = new Grid();
|
||||
$grid->generate();
|
||||
header("Location: " . $_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . dirname($_SERVER["DOCUMENT_URI"]) . "/" . $grid->toString());
|
||||
exit();
|
||||
?>
|
||||
|
137
manifest.php
@ -1,137 +0,0 @@
|
||||
<?php
|
||||
if (isset($_GET["grid"]))
|
||||
$currentGrid = $_GET["grid"];
|
||||
else
|
||||
$currentGrid = ".";
|
||||
?>
|
||||
{
|
||||
"short_name": "Sudoku",
|
||||
"name": "Sudoku",
|
||||
"description": "Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.",
|
||||
"icons": [{
|
||||
"src": "thumbnail.php?size=48&grid=<?=$currentGrid?>",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=72&grid=<?=$currentGrid?>",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=96&grid=<?=$currentGrid?>",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=144&grid=<?=$currentGrid?>",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=168&grid=<?=$currentGrid?>",
|
||||
"sizes": "168x168",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=192&grid=<?=$currentGrid?>",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}],
|
||||
"start_url": ".",
|
||||
"background_color": "#fff",
|
||||
"display": "standalone",
|
||||
"scope": ".",
|
||||
"theme_color": "#fff",
|
||||
"orientation": "portrait-primary",
|
||||
"shortcuts": [
|
||||
{
|
||||
"name": "Sudoku : cette grille",
|
||||
"short_name": "Ce sudoku",
|
||||
"description": "Continuer cette grille de sudoku",
|
||||
"url": "<?=$currentGrid?>",
|
||||
"icons": [{
|
||||
"src": "thumbnail.php?size=48&grid=<?=$currentGrid?>",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=72&grid=<?=$currentGrid?>",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=96&grid=<?=$currentGrid?>",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=144&grid=<?=$currentGrid?>",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=168&grid=<?=$currentGrid?>",
|
||||
"sizes": "168x168",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=192&grid=<?=$currentGrid?>",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "Sudoku : Grille vierge",
|
||||
"short_name": "Sudoku vierge",
|
||||
"description": "Grille de sudoku vierge",
|
||||
"url": ".................................................................................",
|
||||
"icons": [{
|
||||
"src": "thumbnail.php?size=48&grid=.................................................................................",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=72&grid=.................................................................................",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=96&grid=.................................................................................",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=144&grid=.................................................................................",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=168&grid=.................................................................................",
|
||||
"sizes": "168x168",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=192&grid=.................................................................................",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}]
|
||||
},
|
||||
{
|
||||
"name": "Sudoku : Nouvelle grille",
|
||||
"short_name": "Nouveau sudoku",
|
||||
"description": "Nouvelle grille de sudoku",
|
||||
"url": ".",
|
||||
"icons": [{
|
||||
"src": "thumbnail.php?size=48&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
|
||||
"sizes": "48x48",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=72&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
|
||||
"sizes": "72x72",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=96&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
|
||||
"sizes": "96x96",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=144&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
|
||||
"sizes": "144x144",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=168&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
|
||||
"sizes": "168x168",
|
||||
"type": "image/png"
|
||||
}, {
|
||||
"src": "thumbnail.php?size=192&grid=.528.3....4.9.1...39.562......73.129...1.64.7...42.3656.13.5...28.6.4...4.5287...",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}]
|
||||
}
|
||||
]
|
||||
}
|
23
nginx-example.conf
Normal file
@ -0,0 +1,23 @@
|
||||
location /sudoku/ {
|
||||
alias /var/www/sudoku/;
|
||||
index index.php;
|
||||
try_files $uri $uri/ @sudoku;
|
||||
|
||||
location ~ [^/]\.php(/|$) {
|
||||
fastcgi_split_path_info ^(.+?\.php)(/.*)$;
|
||||
fastcgi_pass unix:/var/run/php/php7.3-fpm-sudoku.sock;
|
||||
fastcgi_index index.php;
|
||||
include fastcgi_params;
|
||||
fastcgi_param REMOTE_USER $remote_user;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param SCRIPT_FILENAME $request_filename;
|
||||
}
|
||||
}
|
||||
|
||||
location @sudoku {
|
||||
fastcgi_pass unix:/var/run/php/php7.3-fpm-sudoku.sock;
|
||||
include fastcgi_params;
|
||||
fastcgi_param REMOTE_USER $remote_user;
|
||||
fastcgi_param PATH_INFO $fastcgi_path_info;
|
||||
fastcgi_param SCRIPT_FILENAME sudoku/sudoku.php;
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
/*
|
||||
Copyright 2015, 2019, 2020 Google LLC. All Rights Reserved.
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
// Incrementing OFFLINE_VERSION will kick off the install event and force
|
||||
// previously cached resources to be updated from the network.
|
||||
const OFFLINE_VERSION = 1;
|
||||
const CACHE_NAME = "offline";
|
||||
// Customize this with a different URL if needed.
|
||||
const OFFLINE_URL = ".";
|
||||
|
||||
self.addEventListener("install", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
// Setting {cache: 'reload'} in the new request will ensure that the
|
||||
// response isn't fulfilled from the HTTP cache; i.e., it will be from
|
||||
// the network.
|
||||
await cache.add(new Request(OFFLINE_URL, { cache: "reload" }));
|
||||
})()
|
||||
);
|
||||
// Force the waiting service worker to become the active service worker.
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
self.addEventListener("activate", (event) => {
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
// Enable navigation preload if it's supported.
|
||||
// See https://developers.google.com/web/updates/2017/02/navigation-preload
|
||||
if ("navigationPreload" in self.registration) {
|
||||
await self.registration.navigationPreload.enable();
|
||||
}
|
||||
})()
|
||||
);
|
||||
|
||||
// Tell the active service worker to take control of the page immediately.
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
self.addEventListener("fetch", (event) => {
|
||||
// We only want to call event.respondWith() if this is a navigation request
|
||||
// for an HTML page.
|
||||
if (event.request.mode === "navigate") {
|
||||
event.respondWith(
|
||||
(async () => {
|
||||
try {
|
||||
// First, try to use the navigation preload response if it's supported.
|
||||
const preloadResponse = await event.preloadResponse;
|
||||
if (preloadResponse) {
|
||||
return preloadResponse;
|
||||
}
|
||||
|
||||
// Always try the network first.
|
||||
const networkResponse = await fetch(event.request);
|
||||
return networkResponse;
|
||||
} catch (error) {
|
||||
// catch is only triggered if an exception is thrown, which is likely
|
||||
// due to a network error.
|
||||
// If fetch() returns a valid HTTP response with a response code in
|
||||
// the 4xx or 5xx range, the catch() will NOT be called.
|
||||
console.log("Fetch failed; returning offline page instead.", error);
|
||||
|
||||
const cache = await caches.open(CACHE_NAME);
|
||||
const cachedResponse = await cache.match(OFFLINE_URL);
|
||||
return cachedResponse;
|
||||
}
|
||||
})()
|
||||
);
|
||||
}
|
||||
|
||||
// If our if() condition is false, then this fetch handler won't intercept the
|
||||
// request. If there are any other fetch handlers registered, they will get a
|
||||
// chance to call event.respondWith(). If no fetch handlers call
|
||||
// event.respondWith(), the request will be handled by the browser as if there
|
||||
// were no service worker involvement.
|
||||
});
|
342
style.css
Executable file → Normal file
@ -1,161 +1,213 @@
|
||||
input[type="number"]::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none !important;
|
||||
margin: 0 !important;
|
||||
body {
|
||||
font-family: sans-serif;
|
||||
width: min-content;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
input[type="number"]::-webkit-calendar-picker-indicator {
|
||||
display: none !important;
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 1rem;
|
||||
}
|
||||
|
||||
table {
|
||||
border-collapse: separate;
|
||||
section, div, footer {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
row-gap: 0.5rem;
|
||||
column-gap: 0.5rem;
|
||||
margin: 0.8rem auto;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.grid {
|
||||
border-spacing: 0;
|
||||
border: 1px solid black;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.grid td, tr {
|
||||
padding: 0;
|
||||
}
|
||||
.grid tr:first-child td:first-child {
|
||||
border-top-left-radius: 5px;
|
||||
}
|
||||
.grid tr:first-child td:first-child input {
|
||||
border-top-left-radius: 4px;
|
||||
}
|
||||
.grid tr:first-child td:last-child {
|
||||
border-top-right-radius: 5px;
|
||||
}
|
||||
.grid tr:first-child td:last-child input {
|
||||
border-top-right-radius: 4px;
|
||||
}
|
||||
.grid tr:last-child td:first-child {
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
.grid tr:last-child td:first-child > input {
|
||||
border-bottom-left-radius: 4px;
|
||||
}
|
||||
.grid tr:last-child td:last-child {
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
.grid tr:last-child td:last-child input {
|
||||
border-bottom-right-radius: 4px;
|
||||
}
|
||||
.grid tr:nth-child(3n+1) td {
|
||||
border-top: 1px solid black;
|
||||
}
|
||||
.grid tr:nth-child(3n+2) td {
|
||||
border-top: 1px solid grey;
|
||||
border-bottom: 1px solid grey;
|
||||
}
|
||||
.grid tr:nth-child(3n) td {
|
||||
border-bottom: 1px solid black;
|
||||
}
|
||||
.grid td:nth-child(3n+1) {
|
||||
border-left: 1px solid black;
|
||||
}
|
||||
.grid td:nth-child(3n+2) {
|
||||
border-left: 1px solid grey;
|
||||
border-right: 1px solid grey;
|
||||
}
|
||||
.grid td:nth-child(3n+3) {
|
||||
border-right: 1px solid black;
|
||||
}
|
||||
|
||||
table input {
|
||||
.grid input {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
font-size: 1.5rem;
|
||||
border: 0;
|
||||
border-radius: 0;
|
||||
padding: 0;
|
||||
text-align: center;
|
||||
transition: background 0.5s;
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
input::-webkit-outer-spin-button,
|
||||
input::-webkit-inner-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
input::-webkit-calendar-picker-indicator {
|
||||
display: none;
|
||||
}
|
||||
.grid input:enabled {
|
||||
background: white;
|
||||
color: darkblue;
|
||||
}
|
||||
.grid input:disabled, button:enabled {
|
||||
color: white;
|
||||
background: #6666ff;
|
||||
}
|
||||
.grid input.forbidden-value:enabled {
|
||||
background: #ffff77;
|
||||
}
|
||||
.grid input.same-value:enabled {
|
||||
background: #ffff33;
|
||||
}
|
||||
.grid input.forbidden-value:disabled {
|
||||
color: #ffff99;
|
||||
background: #6666ff;
|
||||
}
|
||||
.grid input.same-value:disabled, button.same-value:enabled {
|
||||
color: #ffff99 !important;
|
||||
background: #00cc66 !important;
|
||||
}
|
||||
.grid input.pencil, input::placeholder {
|
||||
color: #888 !important;
|
||||
font-size: 1rem !important;
|
||||
width: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
font-size: 1.3rem !important;
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
text-align: center;
|
||||
appearance: textfield !important;
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
table td.table-primary input,
|
||||
table td.table-active input,
|
||||
table.table-success input,
|
||||
td.table-danger input:disabled,
|
||||
table input:not([disabled]) {
|
||||
background: transparent !important;
|
||||
color: inherit !important;
|
||||
.highlight-buttons {
|
||||
column-gap: 2px;
|
||||
}
|
||||
button, input[type="color"] {
|
||||
border: 2px outset #6666ff;
|
||||
border-radius: 4px;
|
||||
font-size: 1.3rem;
|
||||
padding: 4px 9px 5px 9px;
|
||||
margin: 0px 1px 1px 1px;
|
||||
}
|
||||
button:enabled:hover {
|
||||
border-width: 1px;
|
||||
border-style: outset;
|
||||
padding: 5px 9px 5px 10px;
|
||||
margin: 1px 1px 1px 2px;
|
||||
}
|
||||
button.pressed:enabled:hover {
|
||||
border-width: 3px;
|
||||
border-style: inset;
|
||||
padding: 4px 6px 2px 9px;
|
||||
margin: 1px 1px 1px 2px;
|
||||
}
|
||||
button.pressed {
|
||||
border: 2px inset #00cc66;
|
||||
background: #00cc66;
|
||||
padding: 4px 8px 4px 9px;
|
||||
margin: 1px 1px 0px 2px;
|
||||
}
|
||||
button:enabled:active {
|
||||
border-width: 4px !important;
|
||||
border-style: inset !important;
|
||||
padding: 4px 4px 0px 9px !important;
|
||||
margin: 0px 1px 0px 2px !important;
|
||||
}
|
||||
button:disabled {
|
||||
color: #666;
|
||||
background: darkgrey;
|
||||
border: 1px outset darkgrey;
|
||||
padding: 5px 10px 6px 10px;
|
||||
margin: 0px 1px 1px 1px;
|
||||
}
|
||||
button.warning {
|
||||
background: #ff5050;
|
||||
border-color: #ff5050;
|
||||
}
|
||||
input[type="color"] {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
table input:not([disabled]):hover {
|
||||
background: #9fb9b945 !important;
|
||||
}
|
||||
|
||||
table input:disabled {
|
||||
background-position: center !important;
|
||||
}
|
||||
|
||||
tr:nth-child(3n+1) td input {
|
||||
border-top-width: 3px !important;
|
||||
}
|
||||
|
||||
tr:last-child td input {
|
||||
border-bottom-width: 3px !important;
|
||||
}
|
||||
|
||||
td:nth-child(3n+1) input {
|
||||
border-left-width: 3px !important;
|
||||
}
|
||||
|
||||
td:last-child input {
|
||||
border-right-width: 3px !important;
|
||||
}
|
||||
|
||||
tr:first-child td:first-child {
|
||||
border-top-left-radius: .7rem !important;
|
||||
}
|
||||
|
||||
tr:first-child td:first-child input {
|
||||
border-top-left-radius: .5rem !important;
|
||||
}
|
||||
|
||||
tr:first-child td:last-child {
|
||||
border-top-right-radius: .7rem !important;
|
||||
}
|
||||
|
||||
tr:first-child td:last-child input {
|
||||
border-top-right-radius: .5rem !important;
|
||||
}
|
||||
|
||||
tr:last-child td:first-child {
|
||||
border-bottom-left-radius: .7rem !important;
|
||||
}
|
||||
|
||||
tr:last-child td:first-child input {
|
||||
border-bottom-left-radius: .5rem !important;
|
||||
}
|
||||
|
||||
tr:last-child td:last-child {
|
||||
border-bottom-right-radius: .7rem !important;
|
||||
}
|
||||
|
||||
tr:last-child td:last-child input {
|
||||
border-bottom-right-radius: .5rem !important;
|
||||
}
|
||||
|
||||
td {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
border: 0 !important;
|
||||
}
|
||||
|
||||
td,
|
||||
table input {
|
||||
transition: background-color .4s, box-shadow .4s !important;
|
||||
}
|
||||
|
||||
.context-menu li {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
.table-active {
|
||||
cursor: inherit !important;
|
||||
}
|
||||
|
||||
.not-allowed {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
table {
|
||||
cursor: text;
|
||||
}
|
||||
|
||||
button,
|
||||
label {
|
||||
/*! padding: .375rem .65rem !important; */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button i,
|
||||
label i {
|
||||
margin: 0 -.125rem;
|
||||
}
|
||||
|
||||
button:disabled,
|
||||
:disabled+label {
|
||||
cursor: not-allowed !important;
|
||||
}
|
||||
|
||||
table input:enabled {
|
||||
cursor: inherit;
|
||||
}
|
||||
|
||||
.pencil {
|
||||
color: var(--bs-secondary-color) !important;
|
||||
}
|
||||
|
||||
#colorPickerInput{
|
||||
width: 2.3rem;
|
||||
height: auto;
|
||||
padding: .375rem;
|
||||
}
|
||||
|
||||
#colorPickerLabel {
|
||||
color: var(--bs-body-color);
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme:dark) {
|
||||
.pencil {
|
||||
color: #5a5a5a !important;
|
||||
}
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.context-menu {
|
||||
z-index: 100;
|
||||
}
|
||||
display: none;
|
||||
z-index: 1000;
|
||||
position: absolute;
|
||||
overflow: hidden;
|
||||
border: 1px solid #CCC;
|
||||
white-space: nowrap;
|
||||
font-family: sans-serif;
|
||||
background: #EEE;
|
||||
color: #333;
|
||||
border-top-right-radius: 3px;
|
||||
border-bottom-left-radius: 3px;
|
||||
border-bottom-right-radius: 3px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.context-menu li {
|
||||
padding: 6px 10px;
|
||||
cursor: default;
|
||||
list-style-type: none;
|
||||
transition: all .3s ease;
|
||||
user-select: none;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.context-menu li:hover {
|
||||
background-color: #DEF;
|
||||
}
|
||||
|
||||
.context-menu li.error {
|
||||
color: #888
|
||||
}
|
||||
|
||||
.context-menu li.error:hover {
|
||||
background-color: #EEE;
|
||||
}
|
||||
|
394
sudoku.js
@ -1,394 +0,0 @@
|
||||
const VALUES = "123456789"
|
||||
const UNKNOWN = '.'
|
||||
|
||||
let boxes = []
|
||||
let rows = Array.from(Array(9), x => [])
|
||||
let columns = Array.from(Array(9), x => [])
|
||||
let regions = Array.from(Array(9), x => [])
|
||||
let areaNames = {
|
||||
ligne: rows,
|
||||
colonne: columns,
|
||||
région: regions,
|
||||
}
|
||||
let valueToInsert = ""
|
||||
let easyBoxes = []
|
||||
let insertRadios = []
|
||||
|
||||
function shuffle(iterable) {
|
||||
array = Array.from(iterable)
|
||||
if (array.length > 1) {
|
||||
let i, j, tmp
|
||||
for (i = array.length - 1; i > 0; i--) {
|
||||
j = Math.floor(Math.random() * (i + 1))
|
||||
tmp = array[i]
|
||||
array[i] = array[j]
|
||||
array[j] = tmp
|
||||
}
|
||||
}
|
||||
return array
|
||||
}
|
||||
|
||||
window.onload = function() {
|
||||
let rowId = 0
|
||||
for (let row of grid.getElementsByTagName('tr')) {
|
||||
let columnId = 0
|
||||
for (let box of row.getElementsByTagName('input')) {
|
||||
let regionId = rowId - rowId % 3 + Math.floor(columnId / 3)
|
||||
if (!box.disabled) {
|
||||
box.onfocus = onfocus
|
||||
box.oninput = oninput
|
||||
box.onblur = onblur
|
||||
box.onclick = onclick
|
||||
box.onmouseenter = onmouseenter
|
||||
box.onmouseleave = onmouseleave
|
||||
}
|
||||
box.oncontextmenu = oncontextmenu
|
||||
box.rowId = rowId
|
||||
box.columnId = columnId
|
||||
box.regionId = regionId
|
||||
boxes.push(box)
|
||||
rows[rowId].push(box)
|
||||
columns[columnId].push(box)
|
||||
regions[regionId].push(box)
|
||||
columnId++
|
||||
}
|
||||
rowId++
|
||||
}
|
||||
|
||||
if (localStorage["tool"] == "sight") sightCheckbox.checked = true
|
||||
else if (localStorage["tool"] == "highlighter") highlighterCheckbox.checked = true
|
||||
|
||||
colorPickerInput.value = window.getComputedStyle(grid).getPropertyValue("--bs-body-color")
|
||||
|
||||
boxes.forEach(box => {
|
||||
box.neighbourhood = new Set(rows[box.rowId].concat(columns[box.columnId]).concat(regions[box.regionId]))
|
||||
box.andNeighbourhood = Array.from(box.neighbourhood)
|
||||
box.neighbourhood.delete(box)
|
||||
box.neighbourhood = Array.from(box.neighbourhood)
|
||||
})
|
||||
|
||||
insertRadios = Array.from(insertRadioGroup.getElementsByTagName("input")).slice(1)
|
||||
|
||||
for (label of document.getElementsByTagName("label")) {
|
||||
label.control.label = label
|
||||
}
|
||||
let accessKeyModifiers = (/Win/.test(navigator.userAgent) || /Linux/.test(navigator.userAgent)) ? "Alt+Maj+"
|
||||
: (/Mac/.test(navigator.userAgent)) ? "⌃⌥"
|
||||
: "AccessKey+"
|
||||
for (node of document.querySelectorAll("*[accesskey]")) {
|
||||
shortcut = ` [${node.accessKeyLabel||(accessKeyModifiers+node.accessKey)}]`
|
||||
if (node.title) node.title += shortcut
|
||||
else if (node.label) node.label.title += shortcut
|
||||
}
|
||||
|
||||
loadGame(history.state)
|
||||
}
|
||||
|
||||
window.onpopstate = (event) => loadGame(event.state)
|
||||
|
||||
function loadGame(state) {
|
||||
if (state) {
|
||||
boxes.forEach((box, i) => {
|
||||
if (!box.disabled) {
|
||||
box.value = state.boxesValues[i]
|
||||
box.placeholder = state.boxesPlaceholders[i]
|
||||
}
|
||||
})
|
||||
restartLink.classList.remove("disabled")
|
||||
undoButton.disabled = false
|
||||
fixGridLink.href = "?" + state.boxesValues.map(value => value || UNKNOWN).join("")
|
||||
} else {
|
||||
boxes.filter(box => !box.disabled).forEach(box => {
|
||||
box.value = ""
|
||||
box.placeholder = ""
|
||||
})
|
||||
restartLink.classList.add("disabled")
|
||||
undoButton.disabled = true
|
||||
fixGridLink.href = ""
|
||||
}
|
||||
|
||||
checkBoxes()
|
||||
enableRadio()
|
||||
highlight()
|
||||
}
|
||||
|
||||
function searchCandidatesOf(box) {
|
||||
box.candidates = new Set(VALUES)
|
||||
box.neighbourhood.forEach(neighbour => box.candidates.delete(neighbour.value))
|
||||
if (!box.disabled) {
|
||||
switch (box.candidates.size) {
|
||||
case 0:
|
||||
box.title = "Aucune possibilité !"
|
||||
break
|
||||
case 1:
|
||||
box.title = "Une seule possibilité [Clic-droit]"
|
||||
break
|
||||
default:
|
||||
box.title = box.candidates.size + " possibilités [Clic-droit]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function onfocus() {
|
||||
if (pencilRadio.checked) {
|
||||
this.type = "text"
|
||||
this.value = this.placeholder
|
||||
this.placeholder = ""
|
||||
this.classList.add("pencil")
|
||||
} else {
|
||||
this.select()
|
||||
}
|
||||
if (penColor && inkPenRadio.checked) {
|
||||
this.style.setProperty("color", penColor)
|
||||
}
|
||||
this.style.caretColor = valueToInsert ? "transparent" : "auto"
|
||||
}
|
||||
|
||||
function onclick() {
|
||||
if (inkPenRadio.checked) {
|
||||
if (valueToInsert) {
|
||||
this.value = valueToInsert
|
||||
this.oninput()
|
||||
} else {
|
||||
this.select()
|
||||
}
|
||||
} else if (pencilRadio.checked) {
|
||||
if (valueToInsert) {
|
||||
this.value = Array.from(new Set(this.value + valueToInsert)).join("")
|
||||
this.oninput()
|
||||
}
|
||||
} else if (eraserRadio.checked) {
|
||||
this.value = ""
|
||||
this.placeholder = ""
|
||||
this.oninput()
|
||||
}
|
||||
}
|
||||
|
||||
function oninput() {
|
||||
if (inkPenRadio.checked) {
|
||||
checkBoxes()
|
||||
enableRadio()
|
||||
highlight()
|
||||
fixGridLink.href = "?" + boxes.map(box => box.value || UNKNOWN).join("")
|
||||
}
|
||||
saveGame()
|
||||
}
|
||||
|
||||
function checkBoxes() {
|
||||
boxes.forEach(box => {
|
||||
box.setCustomValidity("")
|
||||
box.classList.remove("is-invalid")
|
||||
box.parentElement.classList.remove("table-danger")
|
||||
searchCandidatesOf(box)
|
||||
if (box.candidates.size == 0) {
|
||||
box.setCustomValidity("Aucun chiffre possible !")
|
||||
box.classList.add("is-invalid")
|
||||
}
|
||||
})
|
||||
|
||||
for (let [areaName, areas] of Object.entries(areaNames))
|
||||
for (area of areas)
|
||||
area.filter(box => box.value).sort((box, neighbour) => {
|
||||
if(box.value == neighbour.value) {
|
||||
area.forEach(neighbour => neighbour.parentElement.classList.add("table-danger"))
|
||||
for (neighbour of [box, neighbour]) {
|
||||
neighbour.setCustomValidity(`Il y a un autre ${box.value} dans cette ${areaName}.`)
|
||||
neighbour.classList.add("is-invalid")
|
||||
}
|
||||
}
|
||||
return box.value - neighbour.value
|
||||
})
|
||||
|
||||
if (sudokuForm.checkValidity()) { // Correct grid
|
||||
if (boxes.filter(box => box.value == "").length == 0) {
|
||||
grid.classList.add("table-success")
|
||||
setTimeout(() => {
|
||||
if (confirm(`Bravo ! Vous avez résolu la grille. En voulez-vous une autre ?`))
|
||||
location = "."
|
||||
}, 400)
|
||||
} else {
|
||||
grid.classList.remove("table-success")
|
||||
}
|
||||
} else { // Errors on grid
|
||||
grid.classList.remove("table-success")
|
||||
sudokuForm.reportValidity()
|
||||
}
|
||||
}
|
||||
|
||||
function enableRadio() {
|
||||
for (radio of insertRadios) {
|
||||
if (boxes.filter(box => box.value == "").some(box => box.candidates.has(radio.value))) {
|
||||
radio.disabled = false
|
||||
radio.label.title = `Insérer un ${radio.value} [${radio.accessKeyLabel||(accessKeyModifiers+radio.accessKey)}]`
|
||||
} else {
|
||||
radio.disabled = true
|
||||
radio.label.title = `Tous les ${radio.value} sont posés.`
|
||||
if (valueToInsert == radio.value) {
|
||||
insertRadio0.checked = true
|
||||
valueToInsert = ""
|
||||
grid.style.cursor = "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function highlight() {
|
||||
hintButton.disabled = true
|
||||
easyBoxes = []
|
||||
boxes.forEach(box => {
|
||||
if (valueToInsert && box.value == valueToInsert) {
|
||||
box.parentElement.classList.add("table-primary")
|
||||
box.tabIndex = -1
|
||||
} else {
|
||||
box.parentElement.classList.remove("table-primary")
|
||||
box.tabIndex = 0
|
||||
}
|
||||
|
||||
if (valueToInsert && highlighterCheckbox.checked && !box.candidates.has(valueToInsert)) {
|
||||
box.parentElement.classList.add("table-active")
|
||||
box.tabIndex = -1
|
||||
} else {
|
||||
box.parentElement.classList.remove("table-active")
|
||||
box.tabIndex = 0
|
||||
}
|
||||
|
||||
if (!box.value && box.candidates.size == 1) {
|
||||
hintButton.disabled = false
|
||||
easyBoxes.push(box)
|
||||
}
|
||||
})
|
||||
highlighterCheckbox.label.title = "Surligner les lignes, colonnes et régions contenant déjà " + (valueToInsert ? "un " + valueToInsert : "le chiffre sélectionné")
|
||||
}
|
||||
|
||||
function onblur() {
|
||||
if (this.classList.contains("pencil")) {
|
||||
this.placeholder = this.value
|
||||
this.value = ""
|
||||
this.type = "number"
|
||||
this.classList.remove("pencil")
|
||||
}
|
||||
}
|
||||
|
||||
function saveGame() {
|
||||
history.pushState({
|
||||
boxesValues: boxes.map(box => box.value),
|
||||
boxesPlaceholders: boxes.map(box => box.placeholder)
|
||||
}, "")
|
||||
restartLink.classList.remove("disabled")
|
||||
undoButton.disabled = false
|
||||
}
|
||||
|
||||
function onmouseenter(event) {
|
||||
if (sightCheckbox.checked){
|
||||
box = event.target
|
||||
box.andNeighbourhood.forEach(neighbour => {
|
||||
neighbour.parentElement.classList.add("table-active")
|
||||
})
|
||||
|
||||
box.neighbourhood.forEach(neighbour => {
|
||||
if (valueToInsert && neighbour.value == valueToInsert) {
|
||||
for (neighbour of [box, neighbour]) {
|
||||
neighbour.parentElement.classList.add("table-danger", "not-allowed")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function onmouseleave(event) {
|
||||
if (sightCheckbox.checked){
|
||||
box = event.target
|
||||
box.andNeighbourhood.forEach(neighbour => {
|
||||
neighbour.parentElement.classList.remove("table-active", "table-danger", "not-allowed")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function insert(radio) {
|
||||
if (radio.value && valueToInsert == radio.value) {
|
||||
radio.blur()
|
||||
insertRadio0.checked = true
|
||||
insert(0)
|
||||
} else {
|
||||
valueToInsert = radio.value
|
||||
grid.style.cursor = valueToInsert ? "pointer" : "text"
|
||||
highlight()
|
||||
}
|
||||
}
|
||||
|
||||
let penColor
|
||||
|
||||
function changeColor() {
|
||||
penColor = colorPickerInput.value
|
||||
colorPickerLabel.style.color = colorPickerInput.value
|
||||
}
|
||||
|
||||
function restart() {
|
||||
if (confirm("Effacer toutes les cases ?")) {
|
||||
restartButton.disabled = true
|
||||
location.hash = ""
|
||||
}
|
||||
}
|
||||
|
||||
function showHint() {
|
||||
if (easyBoxes.length) {
|
||||
shuffle(easyBoxes)
|
||||
let box = easyBoxes.pop()
|
||||
box.placeholder = "💡"
|
||||
box.focus()
|
||||
return box
|
||||
}
|
||||
hintButton.disabled = true
|
||||
}
|
||||
|
||||
function oncontextmenu(event) {
|
||||
event.preventDefault()
|
||||
while (contextMenu.firstChild) contextMenu.firstChild.remove()
|
||||
const box = event.target
|
||||
if (box.candidates.size) {
|
||||
Array.from(box.candidates).sort().forEach(candidate => {
|
||||
li = document.createElement("li")
|
||||
li.innerText = candidate
|
||||
li.classList = "list-group-item list-group-item-action"
|
||||
li.onclick = function(e) {
|
||||
contextMenu.style.display = "none"
|
||||
valueToInsert = e.target.innerText
|
||||
grid.style.cursor = "pointer"
|
||||
document.getElementById("insertRadio" + valueToInsert).checked = true
|
||||
box.onclick()
|
||||
}
|
||||
li.oncontextmenu = function(e) {
|
||||
e.preventDefault()
|
||||
li.onclick(e)
|
||||
}
|
||||
contextMenu.appendChild(li)
|
||||
})
|
||||
} else {
|
||||
li = document.createElement("li")
|
||||
li.innerText = "Aucune possibilité !"
|
||||
li.classList = "list-group-item list-group-item-action disabled"
|
||||
contextMenu.appendChild(li)
|
||||
}
|
||||
contextMenu.style.left = `${event.pageX}px`
|
||||
contextMenu.style.top = `${event.pageY}px`
|
||||
contextMenu.style.display = "block"
|
||||
|
||||
document.onclick = function(event) {
|
||||
contextMenu.style.display = "none"
|
||||
document.onclick = null
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
document.onkeydown = function(event) {
|
||||
if (event.key == "Escape" && contextMenu.style.display == "block") {
|
||||
event.preventDefault()
|
||||
contextMenu.style.display = "none"
|
||||
}
|
||||
}
|
||||
|
||||
window.onbeforeunload = function(event) {
|
||||
saveGame()
|
||||
if (sightCheckbox.checked) localStorage["tool"] = "sight"
|
||||
else if (highlighterCheckbox.checked) localStorage["tool"] = "highlighter"
|
||||
}
|
226
sudoku.php
Executable file → Normal file
@ -1,90 +1,150 @@
|
||||
<?php
|
||||
require("classes.php");
|
||||
|
||||
$gridStr = basename(strip_tags($_SERVER["REQUEST_URI"]));
|
||||
// URL contains grid
|
||||
if (preg_match("#^[1-9.]{81}$#", $gridStr)) {
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang='fr' prefix="og: https://ogp.me/ns#">
|
||||
|
||||
<head>
|
||||
<?php require_once("head.php") ?>
|
||||
<meta charset='utf-8' />
|
||||
<meta name='viewport' content='width=device-width' />
|
||||
<title>Sudoku</title>
|
||||
<link rel='stylesheet' type='text/css' href='style.css' />
|
||||
<script src='app.js'></script>
|
||||
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=57" sizes="57x57">
|
||||
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=114" sizes="114x114">
|
||||
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=72" sizes="72x72">
|
||||
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=144" sizes="144x144">
|
||||
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=60" sizes="60x60">
|
||||
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=120" sizes="120x120">
|
||||
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=76" sizes="76x76">
|
||||
<link rel="apple-touch-icon" href="thumbnail.php?grid=<?=$gridStr?>&size=152" sizes="152x152">
|
||||
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$gridStr?>&size=196" sizes="196x196">
|
||||
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$gridStr?>&size=160" sizes="160x160">
|
||||
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$gridStr?>&size=96" sizes="96x96">
|
||||
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$gridStr?>&size=16" sizes="16x16">
|
||||
<link rel="icon" type="image/png" href="thumbnail.php?grid=<?=$gridStr?>&size=32" sizes="32x32">
|
||||
<meta property="og:title" content="Sudoku"/>
|
||||
<meta property="og:type" content="website"/>
|
||||
<meta property="og:url" content="<?=$_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . $_SERVER["DOCUMENT_URI"];
|
||||
?>"/>
|
||||
<meta property="og:image" content="<?=$_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . dirname($_SERVER["DOCUMENT_URI"])?>/thumbnail.php?grid=<?=$gridStr?>&size=200"/>
|
||||
<meta property="og:image:width" content="200"/>
|
||||
<meta property="og:image:height" content="200"/>
|
||||
<meta property="og:description" content="Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9."/>
|
||||
<meta property="og:locale" content="fr_FR"/>
|
||||
<meta property="og:site_name" content="<?=$_SERVER["HTTP_HOST"]?>"/>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<nav class="navbar mb-4">
|
||||
<h1 class="display-4 text-center m-auto">Sudoku</h1>
|
||||
</nav>
|
||||
<div class="row g-0">
|
||||
<main class="col-md-6 order-md-1">
|
||||
<div class="text-center m-auto" style="width: min-content;">
|
||||
<div class='d-flex justify-content-between mb-2'>
|
||||
<div class='btn-group'>
|
||||
<input type='radio' id='inkPenRadio' class='btn-check' name='penRadioGroup' checked />
|
||||
<label for='inkPenRadio' class='btn btn-primary' title='Écrire un chiffre'><i class="ri-ball-pen-fill"></i></label>
|
||||
<input type='radio' id='pencilRadio' class='btn-check' name='penRadioGroup' />
|
||||
<label for='pencilRadio' class='btn btn-primary' title='Prendre des notes'><i class="ri-pencil-fill"></i></label>
|
||||
<input type='radio' id='eraserRadio' class='btn-check' name='penRadioGroup' />
|
||||
<label for='eraserRadio' class='btn btn-primary' title='Effacer une case'><i class="ri-eraser-fill"></i></label>
|
||||
</div>
|
||||
<input type="color" class="btn-check" id="colorPickerInput" title="Changer la couleur" oninput="changeColor()"/>
|
||||
<label id="colorPickerLabel" for="colorPickerInput" class="btn btn-primary" title="Changer de couleur"><i class="ri-palette-fill"></i></label>
|
||||
<div class='btn-group'>
|
||||
<input type='checkbox' id='sightCheckbox' class='btn-check' onclick='highlighterCheckbox.checked = false; highlight()' />
|
||||
<label for='sightCheckbox' class='btn btn-info' title='Surligner la ligne, la colonne et la région de la case survolée'><i class="ri-focus-3-line"></i></label>
|
||||
<input type='checkbox' id='highlighterCheckbox' class='btn-check' onclick='sightCheckbox.checked = false; highlight()' />
|
||||
<label for='highlighterCheckbox' class='btn btn-info' title='Surligner les lignes, colonnes et régions contenant déjà le chiffre sélectionné'><i class="ri-mark-pen-fill"></i></label>
|
||||
</div>
|
||||
<button id="hintButton" type="button" class='btn btn-info' onclick="showHint()" title="Montrer une case avec une seule possibilité" accesskey="H" disabled=""><i class="ri-lightbulb-line"></i></button>
|
||||
<a id='restartLink' class='btn btn-primary disabled' href="" title='Recommencer'><i class="ri-restart-line"></i></a>
|
||||
<button id='undoButton' type='button' class='btn btn-primary' onclick='window.history.back()' disabled title='Annuler' accesskey='Z'><i class="ri-arrow-go-back-fill"></i></button>
|
||||
</div>
|
||||
<form id='sudokuForm' class='needs-validation' novalidate>
|
||||
<table id='grid' class='table mb-2'>
|
||||
<tbody>
|
||||
<?php for ($row = 0; $row < 81; $row += 9): ?>
|
||||
<tr class="input-group d-inline-block w-auto">
|
||||
<?php for ($column = 0; $column < 9; $column++): $value = $currentGrid[$row+$column]; ?>
|
||||
<?php if ($value == UNKNOWN): ?>
|
||||
<td><input type='number' min='1' max='9' step='1' value='' class='form-control' /></td>
|
||||
<?php else: ?>
|
||||
<td><input type='number' min='1' max='9' step='1' value='<?=$value?>' class='form-control' disabled /></td>
|
||||
<?php endif ?>
|
||||
<?php endfor?>
|
||||
</tr>
|
||||
<?php endfor?>
|
||||
</tbody>
|
||||
</table>
|
||||
</form>
|
||||
<div class='d-flex mb-4'>
|
||||
<div id='insertRadioGroup' class='radioGroup btn-group flex-fill'>
|
||||
<input type='radio' class='btn-check' id='insertRadio0' value='' name='insertRadioGroup' onclick='insert(this)' accesskey='0' checked /><label for='insertRadio0' class='btn btn-primary' title='Clavier'><i class="ri-input-cursor-move"></i></label>
|
||||
<?php for($value=1; $value<=9; $value++): ?>
|
||||
<input type='radio' class='btn-check' id='insertRadio<?=$value?>' value='<?=$value?>' name='insertRadioGroup' onclick='insert(this)' accesskey='<?=$value?>' disabled />
|
||||
<label for='insertRadio<?=$value?>' class='btn btn-primary' title='Insérer un <?=$value?>'><?=$value?></label>
|
||||
<?php endfor ?>
|
||||
</div>
|
||||
</div>
|
||||
<div class='mb-3'>
|
||||
<?php if (isset($warning)): ?>
|
||||
<strong>⚠️ <?=$warning?> ⚠️</strong><br/>
|
||||
<?php else: ?>
|
||||
Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.
|
||||
<?php endif?>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<aside class="col-md-3 text-center text-md-start">
|
||||
<div class="d-flex flex-column flex-shrink-0 p-3">
|
||||
<ul class="nav nav-pills flex-column">
|
||||
<li><a href="." class="nav-link link-body-emphasis">Nouvelle grille</a></li>
|
||||
<li><a href="" class="nav-link link-body-emphasis">Lien vers cette grille</a></li>
|
||||
<li><a href="?---------------------------------------------------------------------------------" class="nav-link link-body-emphasis">Grille vierge</a></li>
|
||||
<li><a id="fixGridLink" href="" class="nav-link link-body-emphasis">Figer la grille</a></li>
|
||||
<li><a href="https://git.malingrey.fr/adrien/Sudoku" class="nav-link link-body-emphasis">Code source</a></li>
|
||||
<li><a href=".." class="nav-link link-body-emphasis">Autres jeux</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
</div>
|
||||
<ul id='contextMenu' class='context-menu modal-content shadow list-group w-auto position-absolute'></ul>
|
||||
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha3/dist/js/bootstrap.bundle.min.js" integrity="sha384-ENjdO4Dr2bkBIFxQpeoTz1HIcje39Wm4jDKdf19U8gI4ddQ3GYNS7NTKfAdVQSZe" crossorigin="anonymous"></script>
|
||||
<script src='sudoku.js' defer></script>
|
||||
<script>navigator?.serviceWorker.register('service-worker.js')</script>
|
||||
<header>
|
||||
<h1>Sudoku</h1>
|
||||
</header>
|
||||
<section>
|
||||
Remplissez la grille de sorte que chaque ligne, colonne et région (carré de 3×3 cases) contienne tous les chiffres de 1 à 9.
|
||||
</section>
|
||||
<form id='sudokuForm'>
|
||||
<div>
|
||||
<table id='grid' class='grid'>
|
||||
<tbody>
|
||||
<?php
|
||||
for ($row = 0; $row < 9; $row++) {
|
||||
?>
|
||||
<tr>
|
||||
<?php
|
||||
for ($column = 0; $column < 9; $column++) {
|
||||
$value = $gridStr[9*$row+$column];
|
||||
?>
|
||||
<td>
|
||||
<?php
|
||||
if ($value == UNKNOWN) {
|
||||
?>
|
||||
<input type='number' min='1' max='9' step='1' value='' title='Valeurs possibles [Clic-droit]'/>
|
||||
<?php
|
||||
} else {
|
||||
?>
|
||||
<input type='number' min='1' max='9' step='1' value='<?=$value?>' disabled/>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</td>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</tr>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div id='buttons' class='highlight-buttons'>
|
||||
<?php
|
||||
for($value=1; $value<=9; $value++) {
|
||||
echo " <button type='button' onclick='highlight(\"$value\")' title='Surligner les $value' accesskey='$value'>$value</button>\n";
|
||||
}
|
||||
?>
|
||||
</div>
|
||||
<div>
|
||||
<button id='inkPenButton' type='button' onclick='useInkPen()' title='Stylo' class='pressed'>
|
||||
<img src="img/ink-pen.png" alt='Stylo' width=16 height=16/>
|
||||
</button>
|
||||
<button id='pencilButton' type='button' onclick='usePencil()' title='Crayon'>
|
||||
<img src="img/pencil.png" alt='Crayon' width=16 height=16/>
|
||||
</button>
|
||||
<button type='button' onclick='erasePencil()' title='Effacer le crayon'>
|
||||
<img src="img/pencil-eraser.png" alt="Gomme blanche" width=16 height=16/>
|
||||
</button>
|
||||
<button class="warning" type='button' onclick='eraseAll()' title='Effacer tout'>
|
||||
<img src="img/ink-eraser.png" alt="Gomme bleue" width=16 height=16/>
|
||||
</button>
|
||||
<button id='undoButton' type='button' onclick='undo()' disabled title='Annuler' accesskey='z'>
|
||||
<img src="img/undo.png" alt="Annuler" width=16 height=16/>
|
||||
</button>
|
||||
<!--<input id='colorPicker' type='color' title='Changer de couleur de stylo' value='#00008b'/> -->
|
||||
</div>
|
||||
</form>
|
||||
<ul id="contextMenu" class="context-menu">
|
||||
</ul>
|
||||
<footer>
|
||||
<a href=''>Lien vers cette grille</a><br/>
|
||||
<a href='.'>Nouvelle grille</a>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
} else {
|
||||
$grid = new Grid();
|
||||
$grid->generate();
|
||||
|
||||
</html>
|
||||
header("HTTP/1.0 400 Bad Request", true, 400);
|
||||
|
||||
$urlDir = $_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . dirname($_SERVER["DOCUMENT_URI"]);
|
||||
$urlExample = $urlDir . "/" . $grid->toString();
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang='fr'>
|
||||
<head>
|
||||
<meta charset='utf-8' />
|
||||
<meta name='viewport' content='width=device-width' />
|
||||
<title>Grille incorrecte</title>
|
||||
<link rel='stylesheet' type='text/css' href='style.css' />
|
||||
<link rel="icon" type="image/png" href="favicon.png">
|
||||
</head>
|
||||
<body>
|
||||
<header>
|
||||
<h1>Grille incorrecte</h1>
|
||||
</header>
|
||||
L'adresse URL doit être de la forme : <?=$urlDir?>/<em>grille</em>,<br/>
|
||||
<em>grille</em> étant une suite de 81 caractères représentant la grille de gauche à droite puis de haut en bas, soit :
|
||||
<ul>
|
||||
<li>un chiffre entre 1 et 9 pour les cases connues</li>
|
||||
<li>un point pour les case vides</li>
|
||||
</ul>
|
||||
Exemple : <a href='<?=$urlExample?>'><?=$urlExample?></a><br/>
|
||||
</body>
|
||||
</html>
|
||||
<?php
|
||||
}
|
||||
?>
|
||||
|
3
test.php
Normal file
@ -0,0 +1,3 @@
|
||||
<?php
|
||||
echo $_SERVER["REQUEST_SCHEME"] . "://" . $_SERVER["HTTP_HOST"] . $_SERVER["DOCUMENT_URI"];
|
||||
?>
|
64
thumbnail.php
Executable file → Normal file
@ -1,23 +1,15 @@
|
||||
<?php
|
||||
require("classes.php");
|
||||
if (isset($_GET["grid"]) && preg_match("/^[1-9-]{81}$/", $_GET["grid"]))
|
||||
$currentGrid = $_GET["grid"];
|
||||
else
|
||||
$currentGrid = "-528-3----4-9-1---39-562------73-129---1-64-7---42-3656-13-5---28-6-4---4-5287---";
|
||||
header ("Content-type: image/png");
|
||||
if (isset($_GET['size']))
|
||||
$size = (int) $_GET['size'];
|
||||
else
|
||||
$size = 196;
|
||||
|
||||
const UNKNOWN = ".";
|
||||
$gridStr = strip_tags($_GET['grid']);
|
||||
$size = (int) $_GET['size'];
|
||||
$thumbnail = imagecreate($size, $size);
|
||||
$transparent = imagecolorallocate($thumbnail, 1, 1, 1);
|
||||
imagecolortransparent($thumbnail, $transparent);
|
||||
$darkerBorder = imagecolorallocate($thumbnail, 150, 155, 160);
|
||||
$lighterBorder = imagecolorallocate($thumbnail, 210, 225, 230);
|
||||
$emptyBoxBC = imagecolorallocate($thumbnail, 255, 255, 255);
|
||||
$clueBC = imagecolorallocate($thumbnail, 255, 255, 255);
|
||||
$clueFC = imagecolorallocate($thumbnail, 150, 155, 160);
|
||||
$black = imagecolorallocate($thumbnail, 0, 0, 0);
|
||||
$grey = imagecolorallocate($thumbnail, 128, 128, 128);
|
||||
$blue = imagecolorallocate($thumbnail, 102, 102, 255);
|
||||
$white = imagecolorallocate($thumbnail, 255, 255, 255);
|
||||
|
||||
if ($size <= 36) {
|
||||
$boxSize = floor(($size-4) / 9);
|
||||
@ -27,19 +19,19 @@
|
||||
$lineStart = $start + 1;
|
||||
$lineEnd = $end - 2;
|
||||
for ($i = $start; $i < $end; $i += 3*$boxSize + 1) {
|
||||
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $darkerBorder);
|
||||
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $darkerBorder);
|
||||
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $black);
|
||||
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $black);
|
||||
}
|
||||
$x = $start;
|
||||
$y = $start;
|
||||
$boxSizeMinusOne = $boxSize - 1;
|
||||
foreach(str_split($currentGrid) as $i => $value) {
|
||||
foreach(str_split($gridStr) as $i => $value) {
|
||||
if ($i % 3 == 0) $x++;
|
||||
if ($i % 27 == 0) $y++;
|
||||
if ($value == UNKNOWN) {
|
||||
$bgColor = $emptyBoxBC;
|
||||
$bgColor = $white;
|
||||
} else {
|
||||
$bgColor = $clueFC;
|
||||
$bgColor = $blue;
|
||||
}
|
||||
imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusOne, $y+$boxSizeMinusOne, $bgColor);
|
||||
$x += $boxSize;
|
||||
@ -56,21 +48,21 @@
|
||||
$lineStart = $start + 1;
|
||||
$lineEnd = $end - 2;
|
||||
for ($i = $start + $boxSize; $i < $end - $boxSize; $i += $boxSize) {
|
||||
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $lighterBorder);
|
||||
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $lighterBorder);
|
||||
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $grey);
|
||||
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $grey);
|
||||
}
|
||||
for ($i = $start; $i < $end; $i += 3*$boxSize) {
|
||||
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $darkerBorder);
|
||||
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $darkerBorder);
|
||||
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $black);
|
||||
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $black);
|
||||
}
|
||||
$x = $start + 1;
|
||||
$y = $start + 1;
|
||||
$boxSizeMinusTwo = $boxSize - 2;
|
||||
foreach(str_split($currentGrid) as $i => $value) {
|
||||
foreach(str_split($gridStr) as $i => $value) {
|
||||
if ($value == UNKNOWN) {
|
||||
$bgColor = $emptyBoxBC;
|
||||
$bgColor = $white;
|
||||
} else {
|
||||
$bgColor = $clueFC;
|
||||
$bgColor = $blue;
|
||||
}
|
||||
imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusTwo, $y+$boxSizeMinusTwo, $bgColor);
|
||||
$x += $boxSize;
|
||||
@ -89,26 +81,26 @@
|
||||
$fontSize = floor($boxSize/2) - 4;
|
||||
$fdx = floor(($boxSize - imagefontwidth($fontSize)) / 2);
|
||||
$fdy = ceil(($boxSize - imagefontheight($fontSize)) / 2) - 1;
|
||||
$fontColor = $emptyBoxBC;
|
||||
$fontColor = $white;
|
||||
for ($i = $start + $boxSize; $i < $end - $boxSize; $i += $boxSize) {
|
||||
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $lighterBorder);
|
||||
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $lighterBorder);
|
||||
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $grey);
|
||||
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $grey);
|
||||
}
|
||||
for ($i = $start; $i < $end; $i += 3*$boxSize) {
|
||||
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $darkerBorder);
|
||||
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $darkerBorder);
|
||||
ImageLine($thumbnail, $lineStart, $i, $lineEnd, $i, $black);
|
||||
ImageLine($thumbnail, $i, $lineStart, $i, $lineEnd, $black);
|
||||
}
|
||||
$x = $start + 1;
|
||||
$y = $start + 1;
|
||||
$boxSizeMinusTwo = $boxSize - 2;
|
||||
foreach(str_split($currentGrid) as $i => $value) {
|
||||
foreach(str_split($gridStr) as $i => $value) {
|
||||
if ($value == UNKNOWN) {
|
||||
$bgColor = $emptyBoxBC;
|
||||
$bgColor = $white;
|
||||
} else {
|
||||
$bgColor = $clueBC;
|
||||
$bgColor = $blue;
|
||||
}
|
||||
imagefilledrectangle($thumbnail, $x, $y, $x+$boxSizeMinusTwo, $y+$boxSizeMinusTwo, $bgColor);
|
||||
if ($value != UNKNOWN) imagestring($thumbnail, $fontSize, $x + $fdx, $y + $fdy, $value, $clueFC);
|
||||
if ($value != UNKNOWN) imagestring($thumbnail, $fontSize, $x + $fdx, $y + $fdy, $value, $fontColor);
|
||||
$x += $boxSize;
|
||||
if ($i % 9 == 8) {
|
||||
$y += $boxSize;
|
||||
|
BIN
thumbnail.png
Before Width: | Height: | Size: 15 KiB |