Skip to content

Commit 509d59b

Browse files
Merge pull request steam-bell-92#1288 from bhavyasanthoshi02/sudoku
proposal: Add Sudoku Game & Visual Backtracking Solver
2 parents dbc9796 + ed77b76 commit 509d59b

12 files changed

Lines changed: 1778 additions & 10 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: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -604,6 +604,20 @@
604604
"path": "utilities/Typing-Speed-Tester/Typing-Speed-Tester.py"
605605
},
606606
{
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"
607621
"name": "Fourier Series Visualizer",
608622
"emoji": "📈",
609623
"category": "math",

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
@@ -597,6 +597,26 @@ <h3>Snake Game</h3>
597597
<p>Classic snake game!</p>
598598
</div>
599599

600+
<div
601+
class="project-card"
602+
data-category="games"
603+
data-project="sudoku-game"
604+
>
605+
<img
606+
class="card-banner"
607+
src="assets/banners/sudoku-game.webp"
608+
alt="Sudoku Solver &amp; Game"
609+
loading="lazy"
610+
/ loading="lazy">
611+
<div class="card-actions">
612+
<button class="btn-play" aria-label="Play Sudoku Solver &amp; Game">Try It</button>
613+
</div>
614+
615+
<h3>Sudoku Solver &amp; Game</h3>
616+
<p>Interactive Sudoku puzzle with levels and visual backtracking solver.</p>
617+
</div>
618+
619+
600620
<div
601621
class="project-card"
602622
data-category="games"
@@ -723,21 +743,20 @@ <h3>Word Scramble</h3>
723743
<script defer src="js/projects/simon-says.js"></script>
724744
<script defer src="js/projects/flappy-game.js"></script>
725745
<script defer src="js/projects/tic-tac-toe.js"></script>
726-
<script defer src="js/projects/2048-game.js"></script>
727-
<script defer src="js/projects/dots-boxes.js"></script>
728-
<script defer src="js/projects/emoji-memory-game.js"></script>
729746
<script defer src="js/projects/number-converter.js"></script>
730-
<script defer src="js/projects/password-forge.js"></script>
731747
<script defer src="js/projects/typing-speed-tester.js"></script>
732-
<script defer src="js/projects/whack-a-mole.js"></script>
733748
<script defer src="js/projects/word-scramble.js"></script>
734-
<script defer src="js/projects/snake.js"></script>
735749
<script defer src="js/projects/spot-the-difference.js"></script>
750+
<script defer src="js/projects/sudoku-game.js"></script>
736751
<script defer src="js/projects/number-sliding-puzzle.js"></script>
737752
<script defer src="js/projects.js"></script>
738753
<script defer src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
739754
<script defer type="module" src="js/main.js"></script>
740-
<script>lucide.createIcons();</script>
755+
<script>
756+
window.addEventListener('DOMContentLoaded', () => {
757+
if (typeof lucide !== 'undefined') lucide.createIcons();
758+
});
759+
</script>
741760

742761
</body>
743762
</html>

web-app/generate_banners.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,32 @@ def draw_die(ox, oy):
379379
v_draw.ellipse([cx - 40, cy - 20, cx, cy + 20], fill=color_accent)
380380
v_draw.ellipse([cx, cy - 20, cx + 40, cy + 20], fill=color_accent)
381381
v_draw.polygon([(cx - 38, cy + 5), (cx + 38, cy + 5), (cx, cy + 50)], fill=color_accent)
382+
elif "sudoku" in n_lower:
383+
# Draw a mini neon Sudoku grid block
384+
cx, cy = 400, 225
385+
gx_min, gx_max = 280, 520
386+
gy_min, gy_max = 105, 345
387+
# Main subgrid boundaries
388+
for i in range(4):
389+
val = gx_min + i * 80
390+
v_draw.line([(val, gy_min), (val, gy_max)], fill=color_accent, width=3)
391+
val_y = gy_min + i * 80
392+
v_draw.line([(gx_min, val_y), (gx_max, val_y)], fill=color_accent, width=3)
393+
# Inner fine grid lines
394+
for i in range(1, 9):
395+
if i % 3 != 0:
396+
val = gx_min + int(i * 26.6)
397+
v_draw.line([(val, gy_min), (val, gy_max)], fill=color_accent_dim, width=1)
398+
val_y = gy_min + int(i * 26.6)
399+
v_draw.line([(gx_min, val_y), (gx_max, val_y)], fill=color_accent_dim, width=1)
400+
# Sample numbers
401+
digits = [("5", 0, 0), ("3", 1, 0), ("7", 2, 0),
402+
("6", 0, 1), ("1", 1, 1), ("9", 2, 1),
403+
("8", 0, 2), ("4", 1, 2), ("2", 2, 2)]
404+
for d, r, c in digits:
405+
x = gx_min + r * 80 + 40
406+
y = gy_min + c * 80 + 40
407+
v_draw.text((x, y), d, fill=color_accent, anchor="mm")
382408
elif "blackjack" in n_lower:
383409
# Playing cards
384410
def draw_card(x, y, val):
@@ -613,6 +639,7 @@ def draw_o(ox, oy):
613639
("Simon Says", "games", "simon-says.webp"),
614640
("Tic Tac Toe", "games", "tic-tac-toe.webp"),
615641
("Spot the Difference", "games", "spot-the-difference.webp"),
642+
("Sudoku Solver & Game", "games", "sudoku-game.webp"),
616643
("Productive Pet", "utilities", "productive-pet.webp"),
617644
("Progress Tracker", "utilities", "progress-tracker.webp"),
618645
("Reverse Hangman", "games", "reverse-hangman.webp"),

web-app/index.html

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -877,6 +877,7 @@ <h3>Legal</h3>
877877
<script defer src="js/projects/word-scramble.js"></script>
878878
<script defer src="js/projects/snake.js"></script>
879879
<script defer src="js/projects/spot-the-difference.js"></script>
880+
<script defer src="js/projects/sudoku-game.js"></script>
880881
<script defer src="js/projects/color-palette.js"></script>
881882
<script defer src="js/projects/resume-analyzer.js"></script>
882883
<script src="js/projects/caesar-cipher.js"></script>
@@ -894,7 +895,9 @@ <h3>Legal</h3>
894895
<script defer src="js/projects/number-sliding-puzzle.js"></script>
895896
<script defer src="js/projects/budget-tracker.js"></script>
896897
<script>
897-
lucide.createIcons();
898+
window.addEventListener('DOMContentLoaded', () => {
899+
if (typeof lucide !== 'undefined') lucide.createIcons();
900+
});
898901
</script>
899902

900903
<!-- PROJECT LOADER - WITH FAVORITE BUTTONS -->
@@ -1026,6 +1029,11 @@ <h3>Legal</h3>
10261029
tags: "game,relationship",
10271030
},
10281031
{
1032+
project: "sudoku-game",
1033+
title: "Sudoku Solver & Game",
1034+
category: "games",
1035+
desc: "Interactive Sudoku with difficulty levels and visual backtracking solver",
1036+
tags: "game,puzzle,backtracking,solver",
10291037
project: "war-card-game",
10301038
title: "War Card Game",
10311039
category: "games",

web-app/js/main.js

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,6 +1260,43 @@ document.addEventListener("DOMContentLoaded", function () {
12601260
removeTrap = null;
12611261
}
12621262

1263+
recentSearchesList.innerHTML = '';
1264+
recentSearches.slice(0, 5).forEach((search) => {
1265+
const item = document.createElement('div');
1266+
item.className = 'dropdown-recent-item';
1267+
item.innerHTML = `
1268+
<button type="button" class="dropdown-recent-text" aria-label="Search ${search}">
1269+
<i class="fas fa-history" style="opacity: 0.5; font-size: 0.9rem;"></i>
1270+
<span style="flex: 1; color: var(--text-secondary);">${search}</span>
1271+
</button>
1272+
<button type="button" class="dropdown-recent-remove" aria-label="Remove search">
1273+
<i class="fas fa-x"></i>
1274+
</button>
1275+
`;
1276+
1277+
const textButton = item.querySelector('.dropdown-recent-text');
1278+
const removeBtn = item.querySelector('.dropdown-recent-remove');
1279+
1280+
if (textButton) {
1281+
textButton.addEventListener('click', () => {
1282+
searchInput.value = search;
1283+
currentSearchQuery = search;
1284+
performSearch();
1285+
closeDropdown();
1286+
});
1287+
}
1288+
1289+
if (removeBtn) {
1290+
removeBtn.addEventListener('click', (e) => {
1291+
e.stopPropagation();
1292+
recentSearches = recentSearches.filter(s => s !== search);
1293+
localStorage.setItem('recentSearches', JSON.stringify(recentSearches));
1294+
renderRecentSearches();
1295+
});
1296+
}
1297+
});
1298+
1299+
12631300
// Clear content
12641301
if (modalBody) {
12651302
modalBody.innerHTML = "";

web-app/js/projects.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ function getProjectHTML(projectName) {
2323
'tower-of-hanoi': getTowerOfHanoiHTML(),
2424
'nqueens' : getNQueensHTML(),
2525
'matrix-calculator': () => getMatrixCalculatorHTML(),
26+
'sudoku-game': getSudokuGameHTML()
2627
'unit-converter': getUnitConverterHTML(),
2728
'resume-analyzer': getResumeAnalyzerHTML(),
2829
'reverse-hangman': () => getReverseHangmanHTML,
@@ -55,6 +56,9 @@ function initializeProject(projectName) {
5556
'derivative-calculator': initDerivativeCalculator,
5657
'morse-code': initMorseCode,
5758
'tower-of-hanoi': initTowerOfHanoi,
59+
'nqueens' : initNQueens,
60+
'matrix-calculator': initMatrixCalculator,
61+
'sudoku-game': initSudokuGame
5862
'nqueens' : initNQueens(),
5963
'matrix-calculator': initMatrixCalculator,
6064
'unit-converter':initUnitConverter,
@@ -1566,6 +1570,13 @@ function getFlamesHTML() {
15661570
`;
15671571
}
15681572

1573+
function toPascalCase(str) {
1574+
return str
1575+
.split("-")
1576+
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
1577+
.join("");
1578+
}
1579+
15691580
function getProjectHTML(projectName) {
15701581
const fnName = "get" + toPascalCase(projectName) + "HTML";
15711582

@@ -3192,6 +3203,9 @@ function initializeProject(projectName) {
31923203
"2048-game": "init2048Game",
31933204
"color-palette": "initColorPalette",
31943205
"math-quiz": "initMathQuiz",
3206+
"resume-analyzer": "initAIResumeAnalyzer",
3207+
"caesar-cipher": "initCaesarCipher",
3208+
"sudoku-game": "initSudokuGame"
31953209
"resume-analyzer": "initResumeAnalyzer",
31963210
"caesar-cipher": "initCaesarCipher",
31973211
"war-card-game": "initWarCardGame",

0 commit comments

Comments
 (0)