Skip to content

Commit 8d1628f

Browse files
feat: Add Sudoku Game & Visual Backtracking Solver
1 parent 0358673 commit 8d1628f

12 files changed

Lines changed: 1747 additions & 15 deletions

File tree

games/Sudoku-Game/Sudoku-Game.py

Lines changed: 500 additions & 0 deletions
Large diffs are not rendered by default.

projects_registry.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,5 +602,21 @@
602602
"accuracy"
603603
],
604604
"path": "utilities/Typing-Speed-Tester/Typing-Speed-Tester.py"
605+
},
606+
{
607+
"name": "Sudoku Solver & Game",
608+
"emoji": "🧩",
609+
"category": "games",
610+
"difficulty": "intermediate",
611+
"description": "Interactive Sudoku puzzle with difficulty levels and a visual backtracking solver.",
612+
"keywords": [
613+
"sudoku",
614+
"game",
615+
"backtracking",
616+
"solver",
617+
"puzzle",
618+
"grid"
619+
],
620+
"path": "games/Sudoku-Game/Sudoku-Game.py"
605621
}
606622
]

tests/test_sudoku_game.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import unittest
2+
from unittest.mock import patch
3+
import io
4+
import os
5+
import importlib.util
6+
7+
# Absolute path to Sudoku-Game.py
8+
file_path = os.path.join(
9+
os.path.dirname(__file__), "..",
10+
"games", "Sudoku-Game", "Sudoku-Game.py"
11+
)
12+
file_path = os.path.abspath(file_path)
13+
14+
# Load module dynamically from file path
15+
spec = importlib.util.spec_from_file_location("sudoku_game", file_path)
16+
sudoku_module = importlib.util.module_from_spec(spec)
17+
spec.loader.exec_module(sudoku_module)
18+
19+
is_valid = sudoku_module.is_valid
20+
find_empty = sudoku_module.find_empty
21+
solve_sudoku_backtracking = sudoku_module.solve_sudoku_backtracking
22+
generate_sudoku_puzzle = sudoku_module.generate_sudoku_puzzle
23+
24+
class TestSudokuGame(unittest.TestCase):
25+
def setUp(self):
26+
# A simple solvable Sudoku grid
27+
self.grid = [
28+
[5, 3, 0, 0, 7, 0, 0, 0, 0],
29+
[6, 0, 0, 1, 9, 5, 0, 0, 0],
30+
[0, 9, 8, 0, 0, 0, 0, 6, 0],
31+
[8, 0, 0, 0, 6, 0, 0, 0, 3],
32+
[4, 0, 0, 8, 0, 3, 0, 0, 1],
33+
[7, 0, 0, 0, 2, 0, 0, 0, 6],
34+
[0, 6, 0, 0, 0, 0, 2, 8, 0],
35+
[0, 0, 0, 4, 1, 9, 0, 0, 5],
36+
[0, 0, 0, 0, 8, 0, 0, 7, 9]
37+
]
38+
39+
def test_find_empty(self):
40+
# First empty should be at row 0, col 2
41+
self.assertEqual(find_empty(self.grid), (0, 2))
42+
43+
# A full grid should return None
44+
full_grid = [[1] * 9 for _ in range(9)]
45+
self.assertIsNone(find_empty(full_grid))
46+
47+
def test_is_valid(self):
48+
# Placing 1 in grid at (0, 2) is valid
49+
self.assertTrue(is_valid(self.grid, 0, 2, 1))
50+
# Placing 5 in grid at (0, 2) is invalid (already in row)
51+
self.assertFalse(is_valid(self.grid, 0, 2, 5))
52+
# Placing 9 in grid at (0, 2) is invalid (already in 3x3 box)
53+
self.assertFalse(is_valid(self.grid, 0, 2, 9))
54+
55+
def test_solve_sudoku_backtracking(self):
56+
grid_copy = [row[:] for row in self.grid]
57+
stats = {"steps": 0}
58+
solved = solve_sudoku_backtracking(grid_copy, stats=stats)
59+
self.assertTrue(solved)
60+
self.assertIsNone(find_empty(grid_copy))
61+
self.assertGreater(stats["steps"], 0)
62+
63+
def test_generate_sudoku_puzzle(self):
64+
for diff in ["easy", "medium", "hard"]:
65+
puzzle, solution = generate_sudoku_puzzle(diff)
66+
# Board size should be 9x9
67+
self.assertEqual(len(puzzle), 9)
68+
self.assertEqual(len(puzzle[0]), 9)
69+
self.assertEqual(len(solution), 9)
70+
self.assertEqual(len(solution[0]), 9)
71+
72+
# Count empty cells
73+
empty_count = sum(row.count(0) for row in puzzle)
74+
if diff == "easy":
75+
self.assertEqual(empty_count, 46)
76+
elif diff == "medium":
77+
self.assertEqual(empty_count, 53)
78+
elif diff == "hard":
79+
self.assertEqual(empty_count, 61)
80+
81+
if __name__ == '__main__':
82+
unittest.main()
46.2 KB
Loading

web-app/games.html

Lines changed: 26 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -576,6 +576,26 @@ <h3>Snake Game</h3>
576576
<p>Classic snake game!</p>
577577
</div>
578578

579+
<div
580+
class="project-card"
581+
data-category="games"
582+
data-project="sudoku-game"
583+
>
584+
<img
585+
class="card-banner"
586+
src="assets/banners/sudoku-game.webp"
587+
alt="Sudoku Solver &amp; Game"
588+
loading="lazy"
589+
/ loading="lazy">
590+
<div class="card-actions">
591+
<button class="btn-play" aria-label="Play Sudoku Solver &amp; Game">Try It</button>
592+
</div>
593+
594+
<h3>Sudoku Solver &amp; Game</h3>
595+
<p>Interactive Sudoku puzzle with levels and visual backtracking solver.</p>
596+
</div>
597+
598+
579599
<div
580600
class="project-card"
581601
data-category="games"
@@ -702,20 +722,19 @@ <h3>Word Scramble</h3>
702722
<script defer src="js/projects/simon-says.js"></script>
703723
<script defer src="js/projects/flappy-game.js"></script>
704724
<script defer src="js/projects/tic-tac-toe.js"></script>
705-
<script defer src="js/projects/2048-game.js"></script>
706-
<script defer src="js/projects/dots-boxes.js"></script>
707-
<script defer src="js/projects/emoji-memory-game.js"></script>
708725
<script defer src="js/projects/number-converter.js"></script>
709-
<script defer src="js/projects/password-forge.js"></script>
710726
<script defer src="js/projects/typing-speed-tester.js"></script>
711-
<script defer src="js/projects/whack-a-mole.js"></script>
712727
<script defer src="js/projects/word-scramble.js"></script>
713-
<script defer src="js/projects/snake.js"></script>
714728
<script defer src="js/projects/spot-the-difference.js"></script>
729+
<script defer src="js/projects/sudoku-game.js"></script>
715730
<script defer src="js/projects.js"></script>
716731
<script defer src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
717732
<script defer type="module" src="js/main.js"></script>
718-
<script>lucide.createIcons();</script>
733+
<script>
734+
window.addEventListener('DOMContentLoaded', () => {
735+
if (typeof lucide !== 'undefined') lucide.createIcons();
736+
});
737+
</script>
719738

720739
</body>
721740
</html>

web-app/generate_banners.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,32 @@ def draw_die(ox, oy):
311311
v_draw.ellipse([cx - 40, cy - 20, cx, cy + 20], fill=color_accent)
312312
v_draw.ellipse([cx, cy - 20, cx + 40, cy + 20], fill=color_accent)
313313
v_draw.polygon([(cx - 38, cy + 5), (cx + 38, cy + 5), (cx, cy + 50)], fill=color_accent)
314+
elif "sudoku" in n_lower:
315+
# Draw a mini neon Sudoku grid block
316+
cx, cy = 400, 225
317+
gx_min, gx_max = 280, 520
318+
gy_min, gy_max = 105, 345
319+
# Main subgrid boundaries
320+
for i in range(4):
321+
val = gx_min + i * 80
322+
v_draw.line([(val, gy_min), (val, gy_max)], fill=color_accent, width=3)
323+
val_y = gy_min + i * 80
324+
v_draw.line([(gx_min, val_y), (gx_max, val_y)], fill=color_accent, width=3)
325+
# Inner fine grid lines
326+
for i in range(1, 9):
327+
if i % 3 != 0:
328+
val = gx_min + int(i * 26.6)
329+
v_draw.line([(val, gy_min), (val, gy_max)], fill=color_accent_dim, width=1)
330+
val_y = gy_min + int(i * 26.6)
331+
v_draw.line([(gx_min, val_y), (gx_max, val_y)], fill=color_accent_dim, width=1)
332+
# Sample numbers
333+
digits = [("5", 0, 0), ("3", 1, 0), ("7", 2, 0),
334+
("6", 0, 1), ("1", 1, 1), ("9", 2, 1),
335+
("8", 0, 2), ("4", 1, 2), ("2", 2, 2)]
336+
for d, r, c in digits:
337+
x = gx_min + r * 80 + 40
338+
y = gy_min + c * 80 + 40
339+
v_draw.text((x, y), d, fill=color_accent, anchor="mm")
314340
elif "blackjack" in n_lower:
315341
# Playing cards
316342
def draw_card(x, y, val):
@@ -474,6 +500,7 @@ def draw_o(ox, oy):
474500
("Simon Says", "games", "simon-says.webp"),
475501
("Tic Tac Toe", "games", "tic-tac-toe.webp"),
476502
("Spot the Difference", "games", "spot-the-difference.webp"),
503+
("Sudoku Solver & Game", "games", "sudoku-game.webp"),
477504
("Productive Pet", "utilities", "productive-pet.webp"),
478505
("Progress Tracker", "utilities", "progress-tracker.webp"),
479506

web-app/index.html

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -872,6 +872,7 @@ <h3>Legal</h3>
872872
<script defer src="js/projects/word-scramble.js"></script>
873873
<script defer src="js/projects/snake.js"></script>
874874
<script defer src="js/projects/spot-the-difference.js"></script>
875+
<script defer src="js/projects/sudoku-game.js"></script>
875876
<script defer src="js/projects/color-palette.js"></script>
876877
<script defer src="js/projects/resume-analyzer.js"></script>
877878
<script src="js/projects/caesar-cipher.js"></script>
@@ -882,7 +883,9 @@ <h3>Legal</h3>
882883
<script defer type="module" src="js/main.js"></script>
883884
<script defer src="js/hero-canvas.js"></script>
884885
<script>
885-
lucide.createIcons();
886+
window.addEventListener('DOMContentLoaded', () => {
887+
if (typeof lucide !== 'undefined') lucide.createIcons();
888+
});
886889
</script>
887890

888891
<!-- PROJECT LOADER - WITH FAVORITE BUTTONS -->
@@ -1006,6 +1009,13 @@ <h3>Legal</h3>
10061009
desc: "Friendship relationship calculator",
10071010
tags: "game,relationship",
10081011
},
1012+
{
1013+
project: "sudoku-game",
1014+
title: "Sudoku Solver & Game",
1015+
category: "games",
1016+
desc: "Interactive Sudoku with difficulty levels and visual backtracking solver",
1017+
tags: "game,puzzle,backtracking,solver",
1018+
},
10091019

10101020
// MATH (15+)
10111021
{

web-app/js/main.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1276,6 +1276,7 @@ document.addEventListener("DOMContentLoaded", function () {
12761276
renderRecentSearches();
12771277
});
12781278
}
1279+
});
12791280

12801281
// Clear content
12811282
if (modalBody) {

web-app/js/projects.js

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Project Registry
1+
// Project Registry
22
// Each project's HTML and logic lives in its own file under js/projects/
33

44
function getProjectHTML(projectName) {
@@ -22,7 +22,8 @@ function getProjectHTML(projectName) {
2222
'morse-code': getMorseCodeHTML(),
2323
'tower-of-hanoi': getTowerOfHanoiHTML(),
2424
'nqueens' : getNQueensHTML(),
25-
'matrix-calculator': () => getMatrixCalculatorHTML()
25+
'matrix-calculator': () => getMatrixCalculatorHTML(),
26+
'sudoku-game': getSudokuGameHTML()
2627
};
2728

2829
return projects[projectName] || '<h2>Project Coming Soon!</h2>';
@@ -48,8 +49,9 @@ function initializeProject(projectName) {
4849
'derivative-calculator': initDerivativeCalculator,
4950
'morse-code': initMorseCode,
5051
'tower-of-hanoi': initTowerOfHanoi,
51-
'nqueens' : initNQueens(),
52-
'matrix-calculator': initMatrixCalculator
52+
'nqueens' : initNQueens,
53+
'matrix-calculator': initMatrixCalculator,
54+
'sudoku-game': initSudokuGame
5355
};
5456

5557
if (initializers[projectName]) {
@@ -1554,6 +1556,13 @@ function getFlamesHTML() {
15541556
`;
15551557
}
15561558

1559+
function toPascalCase(str) {
1560+
return str
1561+
.split("-")
1562+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
1563+
.join("");
1564+
}
1565+
15571566
function getProjectHTML(projectName) {
15581567
const fnName = "get" + toPascalCase(projectName) + "HTML";
15591568

@@ -3121,7 +3130,8 @@ function initializeProject(projectName) {
31213130
"color-palette": "initColorPalette",
31223131
"math-quiz": "initMathQuiz",
31233132
"resume-analyzer": "initAIResumeAnalyzer",
3124-
"caesar-cipher": "initCaesarCipher"
3133+
"caesar-cipher": "initCaesarCipher",
3134+
"sudoku-game": "initSudokuGame"
31253135
};
31263136

31273137
const initializerName = initializers[projectName];

0 commit comments

Comments
 (0)