serkanspace / index.html
sekopst's picture
Add 2 files
d7f47c6 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Premium Sudoku Pro</title>
<script src="https://cdn.tailwindcss.com"></script>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
/* Custom styles for Sudoku board */
.cell {
width: 50px;
height: 50px;
font-size: 1.5rem;
text-align: center;
border: 1px solid #cbd5e0;
position: relative;
transition: all 0.2s ease;
}
.cell:focus {
outline: none;
background-color: #ebf8ff;
}
.cell:nth-child(3n) {
border-right: 2px solid #4a5568;
}
.cell:nth-child(9n) {
border-right: 1px solid #cbd5e0;
}
.row:nth-child(3n) .cell {
border-bottom: 2px solid #4a5568;
}
.row:nth-child(9n) .cell {
border-bottom: 1px solid #cbd5e0;
}
.given {
font-weight: bold;
color: #2d3748;
background-color: #f7fafc;
}
.error {
color: #e53e3e;
animation: shake 0.5s;
}
@keyframes shake {
0%, 100% { transform: translateX(0); }
10%, 30%, 50%, 70%, 90% { transform: translateX(-2px); }
20%, 40%, 60%, 80% { transform: translateX(2px); }
}
.highlight {
background-color: #bee3f8;
}
.selected {
background-color: #90cdf4;
}
.same-number {
background-color: #d6bcfa;
}
.peers {
background-color: #e9d8fd;
}
.conflict {
background-color: #fed7d7;
}
.notes-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(3, 1fr);
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
font-size: 0.7rem;
color: #6b46c1;
}
.notes-corner {
position: absolute;
font-size: 0.7rem;
color: #6b46c1;
}
.corner-number {
position: absolute;
width: 10px;
height: 10px;
}
.cell-value {
z-index: 2;
position: relative;
}
.highlight-mode {
box-shadow: 0 0 0 3px rgba(66, 153, 225, 0.5);
}
.stats-bar {
height: 6px;
border-radius: 3px;
}
@media (max-width: 640px) {
.cell {
width: 35px;
height: 35px;
font-size: 1.2rem;
}
.notes-grid {
font-size: 0.5rem;
}
.notes-corner {
font-size: 0.5rem;
}
}
</style>
</head>
<body class="bg-gray-100 min-h-screen flex flex-col items-center justify-center p-4">
<div class="max-w-6xl w-full bg-white rounded-xl shadow-xl p-6">
<header class="text-center mb-6">
<h1 class="text-4xl font-bold text-indigo-700 mb-2 flex items-center justify-center">
<i class="fas fa-th mr-3"></i> Premium Sudoku Pro
</h1>
<div class="flex flex-wrap justify-center items-center gap-4">
<div class="flex items-center">
<span class="mr-2 text-gray-700 font-medium">Difficulty:</span>
<select id="difficulty" class="border rounded px-3 py-1 bg-white shadow-sm">
<option value="easy">Easy</option>
<option value="medium">Medium</option>
<option value="hard">Hard</option>
<option value="expert">Expert</option>
<option value="insane">Insane</option>
</select>
</div>
<button id="new-game" class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition flex items-center shadow-md">
<i class="fas fa-sync-alt mr-2"></i>New Game
</button>
<div class="flex items-center bg-indigo-50 px-3 py-1 rounded-lg">
<span class="mr-2 text-gray-700 font-medium">Theme:</span>
<button id="theme-light" class="w-6 h-6 rounded-full bg-white border-2 border-gray-300 mx-1 shadow-sm"></button>
<button id="theme-dark" class="w-6 h-6 rounded-full bg-gray-800 border-2 border-gray-700 mx-1 shadow-sm"></button>
<button id="theme-blue" class="w-6 h-6 rounded-full bg-blue-500 border-2 border-blue-700 mx-1 shadow-sm"></button>
</div>
<button id="fullscreen-btn" class="text-gray-700 hover:text-indigo-700 p-2 rounded-lg hover:bg-gray-100 transition">
<i class="fas fa-expand"></i>
</button>
</div>
</header>
<div class="flex flex-col lg:flex-row items-center lg:items-start justify-center gap-8">
<div class="flex-1">
<div class="bg-white p-3 rounded-xl border-2 border-gray-300 shadow-inner relative">
<div id="board" class="grid grid-cols-9 gap-0"></div>
<div class="absolute top-2 right-2 flex space-x-2">
<button id="undo-btn" class="text-gray-500 hover:text-indigo-700" title="Undo">
<i class="fas fa-undo"></i>
</button>
<button id="redo-btn" class="text-gray-500 hover:text-indigo-700" title="Redo">
<i class="fas fa-redo"></i>
</button>
</div>
</div>
<div class="mt-4 grid grid-cols-3 gap-4">
<div class="bg-gray-50 p-3 rounded-lg shadow text-center">
<div class="text-sm text-gray-600 mb-1">Time</div>
<div id="timer" class="font-mono text-xl font-bold">00:00</div>
</div>
<div class="bg-gray-50 p-3 rounded-lg shadow text-center">
<div class="text-sm text-gray-600 mb-1">Mistakes</div>
<div id="mistakes" class="text-xl font-bold">0/3</div>
<div class="stats-bar bg-gray-200 mt-1">
<div id="mistakes-bar" class="stats-bar bg-red-500 h-full w-0"></div>
</div>
</div>
<div class="bg-gray-50 p-3 rounded-lg shadow text-center">
<div class="text-sm text-gray-600 mb-1">Hints</div>
<div id="hints" class="text-xl font-bold">3</div>
<div class="stats-bar bg-gray-200 mt-1">
<div id="hints-bar" class="stats-bar bg-green-500 h-full w-full"></div>
</div>
</div>
</div>
</div>
<div class="flex flex-col space-y-6 w-full lg:w-80">
<div class="bg-gray-50 p-5 rounded-xl shadow-lg">
<h2 class="text-xl font-semibold text-gray-800 mb-3 flex items-center">
<i class="fas fa-gamepad mr-2"></i> Controls
</h2>
<div class="grid grid-cols-3 gap-2 mb-4">
<button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="1">1</button>
<button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="2">2</button>
<button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="3">3</button>
<button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="4">4</button>
<button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="5">5</button>
<button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="6">6</button>
<button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="7">7</button>
<button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="8">8</button>
<button class="number-btn bg-indigo-100 hover:bg-indigo-200 text-indigo-800 font-bold py-4 rounded-lg transition transform hover:scale-105 active:scale-95 shadow-sm" data-number="9">9</button>
</div>
<div class="flex space-x-3">
<button id="erase-btn" class="flex-1 bg-red-100 hover:bg-red-200 text-red-800 font-bold py-3 rounded-lg transition flex items-center justify-center shadow-sm">
<i class="fas fa-eraser mr-2"></i>Erase
</button>
<button id="notes-btn" class="flex-1 bg-yellow-100 hover:bg-yellow-200 text-yellow-800 font-bold py-3 rounded-lg transition flex items-center justify-center shadow-sm">
<i class="fas fa-pencil-alt mr-2"></i>Notes
</button>
<button id="hint-btn" class="flex-1 bg-green-100 hover:bg-green-200 text-green-800 font-bold py-3 rounded-lg transition flex items-center justify-center shadow-sm">
<i class="fas fa-lightbulb mr-2"></i>Hint
</button>
</div>
<div class="mt-4 pt-3 border-t border-gray-200">
<h3 class="text-sm font-medium text-gray-700 mb-2">Notes Style:</h3>
<div class="flex space-x-2">
<button data-note-style="grid" class="note-style-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100">
Grid
</button>
<button data-note-style="corner" class="note-style-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100">
Corners
</button>
<button data-note-style="hidden" class="note-style-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100">
Hidden
</button>
</div>
</div>
<div class="mt-3 pt-3 border-t border-gray-200">
<h3 class="text-sm font-medium text-gray-700 mb-2">Highlight:</h3>
<div class="flex flex-wrap gap-2">
<button data-highlight="number" class="highlight-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100">
Number
</button>
<button data-highlight="peers" class="highlight-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100">
Peers
</button>
<button data-highlight="conflicts" class="highlight-btn px-2 py-1 text-xs rounded border bg-white hover:bg-gray-50 transition active:bg-gray-100">
Conflicts
</button>
</div>
</div>
</div>
<div class="bg-gray-50 p-5 rounded-xl shadow-lg">
<h2 class="text-xl font-semibold text-gray-800 mb-3 flex items-center">
<i class="fas fa-chart-line mr-2"></i> Statistics
</h2>
<div class="space-y-3">
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Games Played:</span>
<span id="games-played">0</span>
</div>
<div class="stats-bar bg-gray-200">
<div id="games-played-bar" class="stats-bar bg-indigo-500 h-full w-0"></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Win Rate:</span>
<span id="win-rate">0%</span>
</div>
<div class="stats-bar bg-gray-200">
<div id="win-rate-bar" class="stats-bar bg-green-500 h-full w-0"></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Average Time:</span>
<span id="average-time">00:00</span>
</div>
<div class="stats-bar bg-gray-200">
<div id="average-time-bar" class="stats-bar bg-blue-500 h-full w-0"></div>
</div>
</div>
<div>
<div class="flex justify-between text-sm text-gray-600 mb-1">
<span>Best Time:</span>
<span id="best-time">--:--</span>
</div>
<div class="stats-bar bg-gray-200">
<div id="best-time-bar" class="stats-bar bg-yellow-500 h-full w-0"></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div id="message" class="mt-6 text-center hidden">
<div id="message-content" class="inline-block px-8 py-4 rounded-xl font-bold text-white shadow-lg"></div>
</div>
<div id="help-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center p-4 hidden z-50">
<div class="bg-white rounded-xl shadow-2xl max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div class="p-6">
<div class="flex justify-between items-center mb-4">
<h2 class="text-2xl font-bold text-indigo-700">Sudoku Pro Help</h2>
<button id="close-help" class="text-gray-500 hover:text-gray-700">
<i class="fas fa-times"></i>
</button>
</div>
<div class="space-y-4">
<div>
<h3 class="text-lg font-semibold mb-2">How to Play</h3>
<p class="text-gray-700">
Fill the 9×9 grid with digits so that each column, each row, and each of the nine 3×3 subgrids
that compose the grid (also called "boxes", "blocks", or "regions") contains all of the digits from 1 to 9.
</p>
</div>
<div>
<h3 class="text-lg font-semibold mb-2">Controls</h3>
<ul class="list-disc pl-5 space-y-2 text-gray-700">
<li><strong>Number Buttons</strong>: Fill the selected cell</li>
<li><strong>Notes Mode</strong>: Toggle to enter small candidate numbers</li>
<li><strong>Erase</strong>: Clear the selected cell</li>
<li><strong>Hint</strong>: Reveals the correct number (limited)</li>
<li><strong>Undo/Redo</strong>: Revert or replay your moves</li>
</ul>
</div>
<div>
<h3 class="text-lg font-semibold mb-2">Advanced Features</h3>
<ul class="list-disc pl-5 space-y-2 text-gray-700">
<li><strong>Notes Styles</strong>: Choose between grid, corner, or hidden notes</li>
<li><strong>Highlighting</strong>: Visualize number conflicts and relationships</li>
<li><strong>Statistics</strong>: Track your performance over time</li>
<li><strong>Multiple Themes</strong>: Light, dark, and blue color schemes</li>
</ul>
</div>
</div>
<div class="mt-6 pt-4 border-t border-gray-200 flex justify-end">
<button id="ok-help" class="bg-indigo-600 text-white px-4 py-2 rounded-lg hover:bg-indigo-700 transition">
Got It!
</button>
</div>
</div>
</div>
</div>
</div>
<div class="absolute bottom-4 right-4">
<button id="help-btn" class="w-12 h-12 bg-indigo-600 text-white rounded-full shadow-lg hover:bg-indigo-700 transition flex items-center justify-center">
<i class="fas fa-question text-xl"></i>
</button>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// Enhanced game state
const gameState = {
board: Array(9).fill().map(() => Array(9).fill(0)),
solution: Array(9).fill().map(() => Array(9).fill(0)),
givenCells: Array(9).fill().map(() => Array(9).fill(false)),
notes: Array(9).fill().map(() => Array(9).fill().map(() => new Set())),
selectedCell: null,
notesMode: false,
noteStyle: 'grid', // 'grid' or 'corner' or 'hidden'
highlightMode: 'number', // 'number' or 'peers' or 'conflicts'
mistakes: 0,
hints: 3,
timer: 0,
timerInterval: null,
gameOver: false,
movesHistory: [],
historyPointer: -1,
stats: {
gamesPlayed: 0,
gamesWon: 0,
totalTime: 0,
bestTime: null,
difficultyStats: {
easy: { played: 0, won: 0, bestTime: null },
medium: { played: 0, won: 0, bestTime: null },
hard: { played: 0, won: 0, bestTime: null },
expert: { played: 0, won: 0, bestTime: null },
insane: { played: 0, won: 0, bestTime: null }
}
},
currentTheme: 'light'
};
// DOM elements
const boardElement = document.getElementById('board');
const numberButtons = document.querySelectorAll('.number-btn');
const eraseButton = document.getElementById('erase-btn');
const notesButton = document.getElementById('notes-btn');
const hintButton = document.getElementById('hint-btn');
const newGameButton = document.getElementById('new-game');
const difficultySelect = document.getElementById('difficulty');
const timerElement = document.getElementById('timer');
const mistakesElement = document.getElementById('mistakes');
const hintsElement = document.getElementById('hints');
const messageElement = document.getElementById('message');
const messageContent = document.getElementById('message-content');
const noteStyleButtons = document.querySelectorAll('.note-style-btn');
const highlightButtons = document.querySelectorAll('.highlight-btn');
const undoButton = document.getElementById('undo-btn');
const redoButton = document.getElementById('redo-btn');
const statsElements = {
gamesPlayed: document.getElementById('games-played'),
winRate: document.getElementById('win-rate'),
averageTime: document.getElementById('average-time'),
bestTime: document.getElementById('best-time')
};
const statBars = {
gamesPlayed: document.getElementById('games-played-bar'),
winRate: document.getElementById('win-rate-bar'),
averageTime: document.getElementById('average-time-bar'),
bestTime: document.getElementById('best-time-bar'),
mistakes: document.getElementById('mistakes-bar'),
hints: document.getElementById('hints-bar')
};
const helpButton = document.getElementById('help-btn');
const helpModal = document.getElementById('help-modal');
const closeHelpButton = document.getElementById('close-help');
const okHelpButton = document.getElementById('ok-help');
const themeButtons = {
light: document.getElementById('theme-light'),
dark: document.getElementById('theme-dark'),
blue: document.getElementById('theme-blue')
};
const fullscreenButton = document.getElementById('fullscreen-btn');
// Initialize the game
initGame();
// Load statistics from localStorage
loadStatistics();
// Update statistics display
updateStatisticsDisplay();
// Event listeners
numberButtons.forEach(button => {
button.addEventListener('click', () => {
if (gameState.selectedCell && !gameState.gameOver) {
const number = parseInt(button.dataset.number);
if (gameState.notesMode) {
toggleNote(gameState.selectedCell.row, gameState.selectedCell.col, number);
} else {
fillCell(gameState.selectedCell.row, gameState.selectedCell.col, number);
}
}
});
});
eraseButton.addEventListener('click', () => {
if (gameState.selectedCell && !gameState.givenCells[gameState.selectedCell.row][gameState.selectedCell.col] && !gameState.gameOver) {
eraseCell(gameState.selectedCell.row, gameState.selectedCell.col);
}
});
notesButton.addEventListener('click', () => {
if (!gameState.gameOver) {
gameState.notesMode = !gameState.notesMode;
notesButton.classList.toggle('bg-yellow-300', gameState.notesMode);
updateSelectedCell();
}
});
hintButton.addEventListener('click', () => {
if (gameState.selectedCell && gameState.hints > 0 && !gameState.gameOver) {
giveHint(gameState.selectedCell.row, gameState.selectedCell.col);
}
});
newGameButton.addEventListener('click', initGame);
noteStyleButtons.forEach(button => {
button.addEventListener('click', () => {
gameState.noteStyle = button.dataset.noteStyle;
noteStyleButtons.forEach(btn =>
btn.classList.toggle('bg-indigo-100', btn.dataset.noteStyle === gameState.noteStyle)
);
renderBoard();
if (gameState.selectedCell) {
selectCell(gameState.selectedCell.row, gameState.selectedCell.col);
}
});
});
highlightButtons.forEach(button => {
button.addEventListener('click', () => {
gameState.highlightMode = button.dataset.highlight;
highlightButtons.forEach(btn =>
btn.classList.toggle('bg-indigo-100', btn.dataset.highlight === gameState.highlightMode)
);
if (gameState.selectedCell && gameState.board[gameState.selectedCell.row][gameState.selectedCell.col] !== 0) {
highlightNumbers(gameState.board[gameState.selectedCell.row][gameState.selectedCell.col]);
}
});
});
undoButton.addEventListener('click', undoMove);
redoButton.addEventListener('click', redoMove);
helpButton.addEventListener('click', () => {
helpModal.classList.remove('hidden');
});
closeHelpButton.addEventListener('click', () => {
helpModal.classList.add('hidden');
});
okHelpButton.addEventListener('click', () => {
helpModal.classList.add('hidden');
});
themeButtons.light.addEventListener('click', () => setTheme('light'));
themeButtons.dark.addEventListener('click', () => setTheme('dark'));
themeButtons.blue.addEventListener('click', () => setTheme('blue'));
fullscreenButton.addEventListener('click', toggleFullscreen);
// Keyboard support
document.addEventListener('keydown', (e) => {
if (gameState.gameOver) return;
if (gameState.selectedCell) {
const { row, col } = gameState.selectedCell;
// Number keys 1-9
if (e.key >= '1' && e.key <= '9') {
const number = parseInt(e.key);
if (gameState.notesMode) {
toggleNote(row, col, number);
} else {
fillCell(row, col, number);
}
}
// Delete or Backspace
else if ((e.key === 'Delete' || e.key === 'Backspace') && !gameState.givenCells[row][col]) {
eraseCell(row, col);
}
// Arrow keys for navigation
else if (e.key === 'ArrowUp' && row > 0) {
selectCell(row - 1, col);
} else if (e.key === 'ArrowDown' && row < 8) {
selectCell(row + 1, col);
} else if (e.key === 'ArrowLeft' && col > 0) {
selectCell(row, col - 1);
} else if (e.key === 'ArrowRight' && col < 8) {
selectCell(row, col + 1);
}
// Space for notes mode
else if (e.key === ' ') {
gameState.notesMode = !gameState.notesMode;
notesButton.classList.toggle('bg-yellow-300', gameState.notesMode);
updateSelectedCell();
}
// Z for undo (with Ctrl for Windows or Cmd for Mac)
else if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
undoMove();
}
// Y for redo (with Ctrl for Windows or Cmd for Mac)
else if ((e.ctrlKey || e.metaKey) && e.key === 'y') {
redoMove();
}
}
});
// Initialize the game
function initGame() {
// Reset game state
gameState.board = Array(9).fill().map(() => Array(9).fill(0));
gameState.solution = Array(9).fill().map(() => Array(9).fill(0));
gameState.givenCells = Array(9).fill().map(() => Array(9).fill(false));
gameState.notes = Array(9).fill().map(() => Array(9).fill().map(() => new Set()));
gameState.selectedCell = null;
gameState.notesMode = false;
gameState.mistakes = 0;
gameState.hints = 3;
gameState.timer = 0;
gameState.gameOver = false;
gameState.movesHistory = [];
gameState.historyPointer = -1;
// Reset UI
notesButton.classList.remove('bg-yellow-300');
messageElement.classList.add('hidden');
updateTimer();
mistakesElement.textContent = '0/3';
hintsElement.textContent = '3';
statBars.mistakes.style.width = '0%';
statBars.hints.style.width = '100%';
// Stop any existing timer
if (gameState.timerInterval) {
clearInterval(gameState.timerInterval);
}
// Generate a new puzzle
generatePuzzle(difficultySelect.value);
// Render the board
renderBoard();
// Start the timer
gameState.timerInterval = setInterval(() => {
gameState.timer++;
updateTimer();
}, 1000);
}
// Generate a Sudoku puzzle
function generatePuzzle(difficulty) {
// First generate a complete solution
generateSolution(0, 0);
// Then remove numbers based on difficulty
let cellsToRemove;
switch (difficulty) {
case 'easy':
cellsToRemove = 40 + Math.floor(Math.random() * 5); // 40-44
break;
case 'medium':
cellsToRemove = 45 + Math.floor(Math.random() * 5); // 45-49
break;
case 'hard':
cellsToRemove = 50 + Math.floor(Math.random() * 5); // 50-54
break;
case 'expert':
cellsToRemove = 55 + Math.floor(Math.random() * 5); // 55-59
break;
case 'insane':
cellsToRemove = 60 + Math.floor(Math.random() * 4); // 60-63
break;
default:
cellsToRemove = 45;
}
// Copy solution to board
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
gameState.board[i][j] = gameState.solution[i][j];
}
}
// Remove cells in a way that maintains puzzle uniqueness
let removed = 0;
const positions = [];
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
positions.push({row: i, col: j});
}
}
shuffleArray(positions);
// Use a more sophisticated removal algorithm that checks for uniqueness
for (const pos of positions) {
if (removed >= cellsToRemove) break;
const {row, col} = pos;
if (gameState.board[row][col] !== 0) {
const temp = gameState.board[row][col];
gameState.board[row][col] = 0;
// Check if the puzzle still has a unique solution
if (!hasUniqueSolution()) {
gameState.board[row][col] = temp;
} else {
removed++;
gameState.givenCells[row][col] = false;
}
}
}
// Mark the remaining cells as given
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
if (gameState.board[i][j] !== 0) {
gameState.givenCells[i][j] = true;
}
}
}
}
// Check if the current puzzle has a unique solution
function hasUniqueSolution() {
const tempBoard = JSON.parse(JSON.stringify(gameState.board));
let solutions = 0;
function countSolutions(r, c) {
if (r === 9) {
solutions++;
return;
}
if (c === 9) {
countSolutions(r + 1, 0);
return;
}
if (tempBoard[r][c] !== 0) {
countSolutions(r, c + 1);
return;
}
for (let num = 1; num <= 9 && solutions < 2; num++) {
if (isValid(tempBoard, r, c, num)) {
tempBoard[r][c] = num;
countSolutions(r, c + 1);
if (solutions < 2) tempBoard[r][c] = 0;
}
}
}
countSolutions(0, 0);
return solutions === 1;
}
// Generate a complete Sudoku solution
function generateSolution(row, col) {
if (row === 9) {
return true;
}
if (col === 9) {
return generateSolution(row + 1, 0);
}
if (gameState.solution[row][col] !== 0) {
return generateSolution(row, col + 1);
}
// Try numbers 1-9 in random order
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
shuffleArray(numbers);
for (const num of numbers) {
if (isValid(gameState.solution, row, col, num)) {
gameState.solution[row][col] = num;
if (generateSolution(row, col + 1)) {
return true;
}
gameState.solution[row][col] = 0;
}
}
return false;
}
// Check if a number can be placed in a cell
function isValid(grid, row, col, num) {
// Check row
for (let i = 0; i < 9; i++) {
if (grid[row][i] === num) return false;
}
// Check column
for (let i = 0; i < 9; i++) {
if (grid[i][col] === num) return false;
}
// Check 3x3 box
const boxRow = Math.floor(row / 3) * 3;
const boxCol = Math.floor(col / 3) * 3;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
if (grid[boxRow + i][boxCol + j] === num) return false;
}
}
return true;
}
// Shuffle an array
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// Render the Sudoku board
function renderBoard() {
boardElement.innerHTML = '';
for (let i = 0; i < 9; i++) {
const rowElement = document.createElement('div');
rowElement.className = 'row flex';
for (let j = 0; j < 9; j++) {
const cell = document.createElement('input');
cell.className = 'cell';
cell.type = 'text';
cell.maxLength = 1;
cell.dataset.row = i;
cell.dataset.col = j;
if (gameState.board[i][j] !== 0) {
const cellValue = document.createElement('div');
cellValue.className = 'cell-value w-full h-full flex items-center justify-center';
cellValue.textContent = gameState.board[i][j];
cell.appendChild(cellValue);
if (gameState.givenCells[i][j]) {
cell.classList.add('given');
cell.readOnly = true;
}
} else if (gameState.notes[i][j].size > 0 && gameState.noteStyle !== 'hidden') {
if (gameState.noteStyle === 'grid') {
const notesContainer = document.createElement('div');
notesContainer.className = 'notes-grid';
for (let n = 1; n <= 9; n++) {
const note = document.createElement('div');
note.className = 'flex items-center justify-center';
note.textContent = gameState.notes[i][j].has(n) ? n : '';
notesContainer.appendChild(note);
}
cell.appendChild(notesContainer);
} else if (gameState.noteStyle === 'corner') {
const notesContainer = document.createElement('div');
notesContainer.className = 'notes-corner w-full h-full';
const positions = {
1: { top: '2px', left: '2px' },
2: { top: '2px', left: '50%', transform: 'translateX(-50%)' },
3: { top: '2px', right: '2px' },
4: { top: '50%', left: '2px', transform: 'translateY(-50%)' },
5: { top: '50%', left: '50%', transform: 'translate(-50%, -50%)' },
6: { top: '50%', right: '2px', transform: 'translateY(-50%)' },
7: { bottom: '2px', left: '2px' },
8: { bottom: '2px', left: '50%', transform: 'translateX(-50%)' },
9: { bottom: '2px', right: '2px' }
};
for (let n = 1; n <= 9; n++) {
if (gameState.notes[i][j].has(n)) {
const note = document.createElement('div');
note.className = 'notes-corner absolute text-xs';
note.textContent = n;
Object.assign(note.style, positions[n]);
notesContainer.appendChild(note);
}
}
cell.appendChild(notesContainer);
}
}
cell.addEventListener('click', () => selectCell(i, j));
cell.addEventListener('focus', () => selectCell(i, j));
rowElement.appendChild(cell);
}
boardElement.appendChild(rowElement);
}
}
// Select a cell
function selectCell(row, col) {
// Deselect previous cell
if (gameState.selectedCell) {
const prevCell = document.querySelector(`.cell[data-row="${gameState.selectedCell.row}"][data-col="${gameState.selectedCell.col}"]`);
if (prevCell) {
prevCell.classList.remove('selected', 'highlight-mode');
// Remove highlights
removeHighlights();
}
}
// Select new cell
gameState.selectedCell = { row, col };
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
if (cell) {
cell.classList.add('selected', 'highlight-mode');
cell.focus();
// Highlight based on current mode
if (gameState.board[row][col] !== 0) {
highlightNumbers(gameState.board[row][col]);
} else if (gameState.highlightMode === 'peers') {
highlightPeers(row, col);
}
}
}
// Update the selected cell appearance
function updateSelectedCell() {
if (gameState.selectedCell) {
const cell = document.querySelector(`.cell[data-row="${gameState.selectedCell.row}"][data-col="${gameState.selectedCell.col}"]`);
if (cell) {
if (gameState.notesMode) {
cell.classList.add('border-yellow-400', 'border-2');
} else {
cell.classList.remove('border-yellow-400', 'border-2');
}
}
}
}
// Fill a cell with a number
function fillCell(row, col, number) {
if (gameState.givenCells[row][col] || gameState.gameOver) return;
// Save current state for undo/redo
saveMove();
// Check if the number is valid
const isValidMove = number === gameState.solution[row][col];
// Check for conflicts with current board (not just solution)
const isConflict = hasConflict(row, col, number);
// Update the board
gameState.board[row][col] = isConflict ? 0 : number;
// Clear any notes for this cell
gameState.notes[row][col].clear();
// Render the board
renderBoard();
selectCell(row, col);
// Highlight the number if not in conflicts mode
if (gameState.highlightMode !== 'conflicts') {
highlightNumbers(number);
}
// Check for mistakes
if (!isValidMove) {
gameState.mistakes++;
mistakesElement.textContent = `${gameState.mistakes}/3`;
statBars.mistakes.style.width = `${(gameState.mistakes / 3) * 100}%`;
// Show error and highlight conflicts
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
if (cell) {
cell.classList.add('error');
setTimeout(() => {
cell.classList.remove('error');
}, 1000);
}
// Check if game over
if (gameState.mistakes >= 3) {
gameState.gameOver = true;
showMessage('Game Over! Too many mistakes.', 'bg-red-500');
clearInterval(gameState.timerInterval);
// Update statistics
gameState.stats.gamesPlayed++;
gameState.stats.totalTime += gameState.timer;
updateDifficultyStats(false);
saveStatistics();
updateStatisticsDisplay();
}
} else {
// Check if puzzle is complete
if (isPuzzleComplete()) {
gameState.gameOver = true;
showMessage('Congratulations! You solved the puzzle!', 'bg-green-500');
clearInterval(gameState.timerInterval);
// Update statistics
gameState.stats.gamesPlayed++;
gameState.stats.gamesWon++;
gameState.stats.totalTime += gameState.timer;
// Update best time if applicable
const currentDifficulty = difficultySelect.value;
if (!gameState.stats.bestTime || gameState.timer < gameState.stats.bestTime) {
gameState.stats.bestTime = gameState.timer;
}
if (!gameState.stats.difficultyStats[currentDifficulty].bestTime ||
gameState.timer < gameState.stats.difficultyStats[currentDifficulty].bestTime) {
gameState.stats.difficultyStats[currentDifficulty].bestTime = gameState.timer;
}
updateDifficultyStats(true);
saveStatistics();
updateStatisticsDisplay();
}
}
}
// Check if a number would conflict with current board
function hasConflict(row, col, number) {
if (number === 0) return false;
// Check row
for (let i = 0; i < 9; i++) {
if (i !== col && gameState.board[row][i] === number) return true;
}
// Check column
for (let i = 0; i < 9; i++) {
if (i !== row && gameState.board[i][col] === number) return true;
}
// Check 3x3 box
const boxRow = Math.floor(row / 3) * 3;
const boxCol = Math.floor(col / 3) * 3;
for (let i = 0; i < 3; i++) {
for (let j = 0; j < 3; j++) {
const r = boxRow + i;
const c = boxCol + j;
if (r !== row && c !== col && gameState.board[r][c] === number) return true;
}
}
return false;
}
// Save current move to history
function saveMove() {
// If we're not at the end of history, discard future moves
if (gameState.historyPointer < gameState.movesHistory.length - 1) {
gameState.movesHistory = gameState.movesHistory.slice(0, gameState.historyPointer + 1);
}
// Save current state
gameState.movesHistory.push({
board: JSON.parse(JSON.stringify(gameState.board)),
notes: JSON.parse(JSON.stringify(gameState.notes.map(row => row.map(col => [...col])))),
selectedCell: gameState.selectedCell ? {...gameState.selectedCell} : null
});
gameState.historyPointer = gameState.movesHistory.length - 1;
// Update undo/redo button states
updateUndoRedoButtons();
}
// Undo the last move
function undoMove() {
if (gameState.historyPointer < 0) return;
gameState.historyPointer--;
if (gameState.historyPointer >= 0) {
const state = gameState.movesHistory[gameState.historyPointer];
gameState.board = JSON.parse(JSON.stringify(state.board));
gameState.notes = state.notes.map(row => row.map(col => new Set(col)));
if (state.selectedCell) {
gameState.selectedCell = {...state.selectedCell};
}
} else {
// If we're undoing the very first move, reset the board
gameState.board = Array(9).fill().map(() => Array(9).fill(0));
gameState.notes = Array(9).fill().map(() => Array(9).fill().map(() => new Set()));
gameState.selectedCell = null;
}
renderBoard();
if (gameState.selectedCell) {
selectCell(gameState.selectedCell.row, gameState.selectedCell.col);
}
updateUndoRedoButtons();
}
// Redo the last undone move
function redoMove() {
if (gameState.historyPointer >= gameState.movesHistory.length - 1) return;
gameState.historyPointer++;
const state = gameState.movesHistory[gameState.historyPointer];
gameState.board = JSON.parse(JSON.stringify(state.board));
gameState.notes = state.notes.map(row => row.map(col => new Set(col)));
if (state.selectedCell) {
gameState.selectedCell = {...state.selectedCell};
}
renderBoard();
if (gameState.selectedCell) {
selectCell(gameState.selectedCell.row, gameState.selectedCell.col);
}
updateUndoRedoButtons();
}
// Update undo/redo button states
function updateUndoRedoButtons() {
undoButton.disabled = gameState.historyPointer < 0;
undoButton.classList.toggle('text-gray-400', gameState.historyPointer < 0);
undoButton.classList.toggle('text-gray-700', gameState.historyPointer >= 0);
redoButton.disabled = gameState.historyPointer >= gameState.movesHistory.length - 1;
redoButton.classList.toggle('text-gray-400', gameState.historyPointer >= gameState.movesHistory.length - 1);
redoButton.classList.toggle('text-gray-700', gameState.historyPointer < gameState.movesHistory.length - 1);
}
// Erase a cell
function eraseCell(row, col) {
if (gameState.givenCells[row][col] || gameState.gameOver) return;
// Save current state for undo/redo
saveMove();
gameState.board[row][col] = 0;
renderBoard();
selectCell(row, col);
}
// Toggle a note for a cell
function toggleNote(row, col, number) {
if (gameState.givenCells[row][col] || gameState.gameOver) return;
// Save current state for undo/redo
saveMove();
if (gameState.notes[row][col].has(number)) {
gameState.notes[row][col].delete(number);
} else {
gameState.notes[row][col].add(number);
}
renderBoard();
selectCell(row, col);
}
// Give a hint
function giveHint(row, col) {
if (gameState.givenCells[row][col] || gameState.board[row][col] !== 0 || gameState.hints <= 0 || gameState.gameOver) return;
gameState.hints--;
hintsElement.textContent = gameState.hints;
// Calculate width percentage for hints bar
const percentage = (gameState.hints / 3) * 100;
statBars.hints.style.width = `${percentage}%`;
// Flash the cell
const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
if (cell) {
cell.classList.add('highlight');
setTimeout(() => {
cell.classList.remove('highlight');
}, 1000);
}
// Fill the cell after a delay to show the highlight
setTimeout(() => {
fillCell(row, col, gameState.solution[row][col]);
}, 300);
}
// Highlight all instances of a number and its peers
function highlightNumbers(number) {
removeHighlights();
if (number === 0) return;
const cells = document.querySelectorAll('.cell');
cells.forEach(cell => {
const row = parseInt(cell.dataset.row);
const col = parseInt(cell.dataset.col);
if (gameState.board[row][col] === number) {
cell.classList.add('same-number');
} else if (gameState.highlightMode === 'peers' && isPeer(row, col, gameState.selectedCell.row, gameState.selectedCell.col)) {
cell.classList.add('peers');
} else if (gameState.highlightMode === 'conflicts' && gameState.board[row][col] !== 0 && hasConflict(row, col, gameState.board[row][col])) {
cell.classList.add('conflict');
}
});
}
// Highlight peers of a cell
function highlightPeers(row, col) {
removeHighlights();
const cells = document.querySelectorAll('.cell');
cells.forEach(cell => {
const cellRow = parseInt(cell.dataset.row);
const cellCol = parseInt(cell.dataset.col);
if (isPeer(cellRow, cellCol, row, col)) {
cell.classList.add('peers');
}
});
}
// Check if two cells are peers (same row, column, or box)
function isPeer(row1, col1, row2, col2) {
// Same row or column
if (row1 === row2 || col1 === col2) return true;
// Same 3x3 box
const boxRow1 = Math.floor(row1 / 3);
const boxCol1 = Math.floor(col1 / 3);
const boxRow2 = Math.floor(row2 / 3);
const boxCol2 = Math.floor(col2 / 3);
return boxRow1 === boxRow2 && boxCol1 === boxCol2;
}
// Remove all number highlights
function removeHighlights() {
const cells = document.querySelectorAll('.cell');
cells.forEach(cell => {
cell.classList.remove('same-number', 'peers', 'conflict');
});
}
// Check if the puzzle is complete
function isPuzzleComplete() {
for (let i = 0; i < 9; i++) {
for (let j = 0; j < 9; j++) {
if (gameState.board[i][j] !== gameState.solution[i][j]) {
return false;
}
}
}
return true;
}
// Update the timer display
function updateTimer() {
const minutes = Math.floor(gameState.timer / 60).toString().padStart(2, '0');
const seconds = (gameState.timer % 60).toString().padStart(2, '0');
timerElement.textContent = `${minutes}:${seconds}`;
}
// Show a message
function showMessage(text, className) {
messageContent.textContent = text;
messageContent.className = `inline-block px-8 py-4 rounded-xl font-bold text-white shadow-lg ${className}`;
messageElement.classList.remove('hidden');
}
// Update difficulty-specific statistics
function updateDifficultyStats(isWin) {
const difficulty = difficultySelect.value;
gameState.stats.difficultyStats[difficulty].played++;
if (isWin) {
gameState.stats.difficultyStats[difficulty].won++;
}
}
// Save statistics to localStorage
function saveStatistics() {
localStorage.setItem('sudokuStats', JSON.stringify(gameState.stats));
}
// Load statistics from localStorage
function loadStatistics() {
const savedStats = localStorage.getItem('sudokuStats');
if (savedStats) {
gameState.stats = JSON.parse(savedStats);
}
}
// Update statistics display
function updateStatisticsDisplay() {
statsElements.gamesPlayed.textContent = gameState.stats.gamesPlayed;
statBars.gamesPlayed.style.width = `${Math.min(gameState.stats.gamesPlayed * 10, 100)}%`;
const winRate = gameState.stats.gamesPlayed > 0 ?
Math.round((gameState.stats.gamesWon / gameState.stats.gamesPlayed) * 100) : 0;
statsElements.winRate.textContent = `${winRate}%`;
statBars.winRate.style.width = `${winRate}%`;
const avgTime = gameState.stats.gamesPlayed > 0 ?
Math.floor(gameState.stats.totalTime / gameState.stats.gamesPlayed) : 0;
const avgMinutes = Math.floor(avgTime / 60).toString().padStart(2, '0');
const avgSeconds = (avgTime % 60).toString().padStart(2, '0');
statsElements.averageTime.textContent = `${avgMinutes}:${avgSeconds}`;
statBars.averageTime.style.width = `${Math.min(avgTime * 100 / 1800, 100)}%`; // Cap at 30 minutes
if (gameState.stats.bestTime) {
const bestMinutes = Math.floor(gameState.stats.bestTime / 60).toString().padStart(2, '0');
const bestSeconds = (gameState.stats.bestTime % 60).toString().padStart(2, '0');
statsElements.bestTime.textContent = `${bestMinutes}:${bestSeconds}`;
statBars.bestTime.style.width = `${Math.max(100 - (gameState.stats.bestTime * 100 / 900), 5)}%`; // Invert scale (5% min)
} else {
statsElements.bestTime.textContent = "--:--";
statBars.bestTime.style.width = "0%";
}
}
// Set theme for the game
function setTheme(theme) {
gameState.currentTheme = theme;
document.body.classList.remove('theme-light', 'theme-dark', 'theme-blue');
document.body.classList.add(`theme-${theme}`);
// Update theme button highlights
Object.keys(themeButtons).forEach(key => {
themeButtons[key].classList.toggle('ring-2', key === theme);
themeButtons[key].classList.toggle('ring-indigo-500', key === theme);
});
// Save theme preference
localStorage.setItem('sudokuTheme', theme);
}
// Toggle fullscreen mode
function toggleFullscreen() {
if (!document.fullscreenElement) {
document.documentElement.requestFullscreen().catch(err => {
console.error(`Error attempting to enable fullscreen: ${err.message}`);
});
} else {
if (document.exitFullscreen) {
document.exitFullscreen();
}
}
}
// Load saved theme
const savedTheme = localStorage.getItem('sudokuTheme') || 'light';
setTheme(savedTheme);
// Set initial note style and highlight mode buttons
noteStyleButtons.forEach(btn =>
btn.classList.toggle('bg-indigo-100', btn.dataset.noteStyle === gameState.noteStyle)
);
highlightButtons.forEach(btn =>
btn.classList.toggle('bg-indigo-100', btn.dataset.highlight === gameState.highlightMode)
);
});
</script>
<p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - <a href="https://enzostvs-deepsite.hf.space?remix=sekopst/serkanspace" style="color: #fff;text-decoration: underline;" target="_blank" >🧬 Remix</a></p></body>
</html>