From 3764ae501a091db862fc19f81497ba0691b525e5 Mon Sep 17 00:00:00 2001 From: soham03patra Date: Fri, 10 Jul 2026 06:55:42 +0530 Subject: [PATCH 01/10] Refactor sudoku logic --- .../__pycache__/sudoku_logic.cpython-313.pyc | Bin 0 -> 3526 bytes starter/sudoku_logic.py | 35 +++++++++++++----- 2 files changed, 25 insertions(+), 10 deletions(-) create mode 100644 starter/__pycache__/sudoku_logic.cpython-313.pyc diff --git a/starter/__pycache__/sudoku_logic.cpython-313.pyc b/starter/__pycache__/sudoku_logic.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c4daea3234d28f95e45730fef7ca89c5d0444c9b GIT binary patch literal 3526 zcmb7GT}&I<6~6Q5|3CgwoRBFHffFFw0NsXUNm)b+p==WdSKi{vG4^1b9X#aDOcK~i zMSa4pv~i+r>THpgtnxslKG3QUsnnOg^f|h@Z5 z&;2>~_}p{8``x?e_j?eO&n7Nk`Mq(!!%uvZ<79=;x zUE(o+Nx*`LBpy2?20PR9Llhf z`jrmJNUI+h;*znlqR3)K&dE5bXVsi&=vgJJXJxH#7Z#%TBCN@OKdb4cFk>t!@~#^O zO<_(=;e8~P&Wrj2_?W#R=ftYt^#A*aZ$!m(5pB|nFxUasHDXgV&y5bnqneDh_~gXI zOgxj-7mT@H(90^S-n+D{FQ~b=rYEs3@HL?EE`)O5 zTpKb0+PwXzm)R*VLS7_-tsH`0q}J<%F>8GOKnvpCKZ@m~E{n;WC@(JQ%i`HvXTK6- zaXzaka$39zu@hHhtZID`&J=Jmmyu0gn)uO}DU3~Bo_RmQm`uWoI|e_HS)Zb&6Vz^k zCJZ;g1c{+G02KuC%-et9e|M_n9scd9%`59;k2=>UOTO+;CJWbZM;}Z+_jUi~)V6o{ zDL?$>Ghg?Y&%DE06D+*jGUDPNhekNh@14;4BPT#@a#+1-aw%0Yxty_>+K&q?AV+}a z;~&uYU-=jhLtOb7Ll#AOT604&gwlS5VpgA?si-vy8J&U^BFZo%_OUvN?F9(-0&nDm zSo?vVqlN>Ct>u5b8#J9?GuI4ru9y&Y_ExtMTd-RP{vU!O?t3tG(#m^=i|{MwtLJG3 z8?tzF0S?fTl1ycD8S&~>h|<-oVp4}@H^i<7PMZ>ek*p@(NGjQM-^E=Lw3!HtgCKDT zs)(DOFWg85NG>pV2z-#urR7_uo1UcvNkp|t(!pd+=9)%$L$r&95NB3PXvujQx4|5- zqx}pjfJI&NJ^g-oP1x~<@T^BxYXnif9`ePLr zs%tEc+{xVWuTJcQ8ti7-Utf6Zqu3f#u4^t_zu&qxvJ-6FsJrvy?O?~o#KWoW;F+z| zi{M$a<>(&RIr5rAfo3q~3t`AK`Uu|8ZS7?@CgO(bV+bfb_fdCT@2V2{py=$_1YS;oR z)#1nintE7;FAMM;x(Mt;*`M-kNExV0pnDTVtBu5zgPmw~&1;;rYQ?pNQ66eh|oZAD|V|7FWisby&A zWPdUBz_Zc+AXpBc*pOd_d$z+puR6NQCr=jzGV!b6YcFa)`A;8eJ+{|`8jl0KL*YuR z4R2FODTb`2s>lIHEre5){~bC9ukP9<-V6JUGL+odC_H?I?je+n0}A%-Ohe8Om-YkC z02_CJ(f@x9ALA$?QyQKnH$5#wH^B>_r`m|EM=%C{vb`Y|AQT0W0A>E zt&LZ_$lFl3a=Yh0d649mB1DJA!HaYd`lJBwk$GC8jGb!{&62bo8qKjac8i5<4*MEi zE_P;srBB4SzGppF;n8BhRI0C7h&1F$6E#)20oefok6AU>2MHk$59@G}cN`5QjFK$S zQ*OF!Wz%69)5uoI!M3idVD*J8C#-3F8m33!)82*(QsHYW`NY++G9Oqop7TxRU@JTi zgU46L%Y3NN^qg-i`{G6&P^g^|^OCUp?eT?YeS=(`)UsThE zA`fHYK4D7j2~-u1VVKWRUIx ztsoFZ;|&oN56fIAj*<3-ipR%<3WF5{qNq`^*+9isKNU{~-jKOUTLARr3IeggzagU1 s?qZG Board: + """Return a deep copy of the given Sudoku board.""" return copy.deepcopy(board) -def create_empty_board(): + +def create_empty_board() -> Board: + """Create an empty 9x9 Sudoku board filled with zeros.""" return [[EMPTY for _ in range(SIZE)] for _ in range(SIZE)] -def is_safe(board, row, col, num): - # Check row and column - for x in range(SIZE): - if board[row][x] == num or board[x][col] == num: + +def is_safe(board: Board, row: int, col: int, num: int) -> bool: + """Return True when placing ``num`` at ``(row, col)`` is valid.""" + for index in range(SIZE): + if board[row][index] == num or board[index][col] == num: return False - # Check 3x3 box + start_row = row - row % 3 start_col = col - col % 3 for i in range(3): @@ -24,7 +33,9 @@ def is_safe(board, row, col, num): return False return True -def fill_board(board): + +def fill_board(board: Board) -> bool: + """Fill the board recursively using a backtracking algorithm.""" for row in range(SIZE): for col in range(SIZE): if board[row][col] == EMPTY: @@ -39,7 +50,9 @@ def fill_board(board): return False return True -def remove_cells(board, clues): + +def remove_cells(board: Board, clues: int) -> None: + """Remove values from the board until it has the requested clue count.""" attempts = SIZE * SIZE - clues while attempts > 0: row = random.randrange(SIZE) @@ -48,7 +61,9 @@ def remove_cells(board, clues): board[row][col] = EMPTY attempts -= 1 -def generate_puzzle(clues=35): + +def generate_puzzle(clues: int = 35) -> Tuple[Board, Board]: + """Generate a Sudoku puzzle and its solved solution.""" board = create_empty_board() fill_board(board) solution = deep_copy(board) From 0beb302c6411011fcf08c02e0026964701a8f490 Mon Sep 17 00:00:00 2001 From: soham03patra Date: Fri, 10 Jul 2026 07:00:18 +0530 Subject: [PATCH 02/10] Refactor Flask application --- starter/app.py | 75 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 45 insertions(+), 30 deletions(-) diff --git a/starter/app.py b/starter/app.py index 0f526b757..74acc8342 100644 --- a/starter/app.py +++ b/starter/app.py @@ -1,39 +1,54 @@ -from flask import Flask, render_template, jsonify, request +"""Flask application for the Sudoku starter project.""" + +from typing import Any, Dict, List, Optional + +from flask import Flask, jsonify, render_template, request + import sudoku_logic app = Flask(__name__) -# Keep a simple in-memory store for current puzzle and solution -CURRENT = { - 'puzzle': None, - 'solution': None +# Keep a simple in-memory store for the current puzzle and solution. +CURRENT: Dict[str, Optional[List[List[int]]]] = { + "puzzle": None, + "solution": None, } -@app.route('/') -def index(): - return render_template('index.html') -@app.route('/new') -def new_game(): - clues = int(request.args.get('clues', 35)) +@app.route("/") +def index() -> str: + """Render the main Sudoku page.""" + return render_template("index.html") + + +@app.route("/new") +def new_game() -> Any: + """Generate a new Sudoku puzzle and store it as the current game.""" + clues = int(request.args.get("clues", 35)) puzzle, solution = sudoku_logic.generate_puzzle(clues) - CURRENT['puzzle'] = puzzle - CURRENT['solution'] = solution - return jsonify({'puzzle': puzzle}) - -@app.route('/check', methods=['POST']) -def check_solution(): - data = request.json - board = data.get('board') - solution = CURRENT.get('solution') + CURRENT["puzzle"] = puzzle + CURRENT["solution"] = solution + return jsonify({"puzzle": puzzle}) + + +@app.route("/check", methods=["POST"]) +def check_solution() -> Any: + """Return the coordinates of incorrect values compared to the solution.""" + data = request.get_json() + board = data.get("board") + solution = CURRENT.get("solution") + if solution is None: - return jsonify({'error': 'No game in progress'}), 400 - incorrect = [] - for i in range(sudoku_logic.SIZE): - for j in range(sudoku_logic.SIZE): - if board[i][j] != solution[i][j]: - incorrect.append([i, j]) - return jsonify({'incorrect': incorrect}) - -if __name__ == '__main__': - app.run(debug=True) \ No newline at end of file + return jsonify({"error": "No game in progress"}), 400 + + incorrect: List[List[int]] = [] + for row_index in range(sudoku_logic.SIZE): + for col_index in range(sudoku_logic.SIZE): + if board[row_index][col_index] != solution[row_index][col_index]: + incorrect.append([row_index, col_index]) + + return jsonify({"incorrect": incorrect}) + + +if __name__ == "__main__": + app.run(debug=True) From 28499f60fc0014e882f0e696a2c224565f5490d4 Mon Sep 17 00:00:00 2001 From: soham03patra Date: Fri, 10 Jul 2026 07:05:45 +0530 Subject: [PATCH 03/10] Added difficulty selector --- starter/static/main.js | 10 +++++++++- starter/templates/index.html | 6 ++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/starter/static/main.js b/starter/static/main.js index 2028e1026..881e9584a 100644 --- a/starter/static/main.js +++ b/starter/static/main.js @@ -1,5 +1,10 @@ // Client-side rendering and interaction for the Flask-backed Sudoku const SIZE = 9; +const DIFFICULTY_CLUES = { + easy: 40, + medium: 32, + hard: 26 +}; let puzzle = []; function createBoardElement() { @@ -48,7 +53,10 @@ function renderPuzzle(puz) { } async function newGame() { - const res = await fetch('/new'); + const difficultySelect = document.getElementById('difficulty'); + const difficulty = difficultySelect.value; + const clues = DIFFICULTY_CLUES[difficulty]; + const res = await fetch(`/new?clues=${clues}`); const data = await res.json(); renderPuzzle(data.puzzle); document.getElementById('message').innerText = ''; diff --git a/starter/templates/index.html b/starter/templates/index.html index e42ad04da..25f189607 100644 --- a/starter/templates/index.html +++ b/starter/templates/index.html @@ -9,6 +9,12 @@

Sudoku Game

+ + From d917a08cc59567bae60850a700e0d94e11edd230 Mon Sep 17 00:00:00 2001 From: soham03patra Date: Fri, 10 Jul 2026 09:31:27 +0530 Subject: [PATCH 04/10] Added Hint feature --- .../__pycache__/sudoku_logic.cpython-313.pyc | Bin 3526 -> 4611 bytes starter/app.py | 24 +++++++++++ starter/static/main.js | 23 ++++++++++- starter/sudoku_logic.py | 38 ++++++++++++++++-- starter/templates/index.html | 1 + starter/tests/test_sudoku_logic.py | 15 +++++++ 6 files changed, 96 insertions(+), 5 deletions(-) create mode 100644 starter/tests/test_sudoku_logic.py diff --git a/starter/__pycache__/sudoku_logic.cpython-313.pyc b/starter/__pycache__/sudoku_logic.cpython-313.pyc index c4daea3234d28f95e45730fef7ca89c5d0444c9b..35dc77cafce4eb2cadd4c765447382c92764e9d3 100644 GIT binary patch delta 1844 zcmZ8hO>7%Q6rQnn9sfK2Yuq?A+t{>q)FzdhKuMuLR8*x9)N~ukk}Wxoy-D0UyXovM zkZ^DW0+j=_WdRupK@mT4FDOzDT#&fI1*Ar4MPx;yhj1tdlr)6{C*GU2)52TLo0)HB z-h1=jxASB2t#oiZ5bzR^FUHPan)lxbCg_73`#-3~WHIlNgL${?%DabLa;TeJ5-w2| z;#--#=~4Vj-%Rzs>?bq4u`mbj*4E;i&F~gwcZr3(cp5#^$YdwoMjQ~kB;P? zUO3hCPF1Q^+eb!x+@zIqr+jFQJ;{|d=KlP}ARZB`mD!3>a=^GP9vC>GZNEba`H18| z+cHjuTVvIc!yR05f>m@y!(S4jI{%Meeh*L~o5Dp3TiEfGaocRcoHV`MQ=z2I%|FJ-eRvpe z+w|-l)7>zaTEc)|rwHrEeW;#-rhfx!k!*y!>%*(zy^H5ILa`4ozW>Hm=Xx~pY3$=; zS5L2ncE4Mz>nowuSLWT&L96EOuPrP&f9*Q4+I3=W@aWAWpM)CDmBhjI^pT}=tLZa! z&-&m{-P=OflIgq2Lu;u6pS`>lAbtH0f+VqfyNkqnx16Lix!FVf0ftO3QBZBcIy<3l z_kj8@#bqkj9M3pp=g3(?(j+4+3|&wRlc~~0W-8KaGm6UVWtU^7s7uQ0#gb8-mo!z` zsolVRUJ}YMqr65LYNy5rfR3?9m@v|e6~IQpR4Z3ZrB#r9n#u-Y?Iqj_e+OpJ2N`V8 z6<>d&xZ)eUEex{Y@|HV9>2ntKrfSHbQWfIkg_SD01g}nMMkx<)Ztw_R$q4+l;bdrf zSWzuQn^sr^T(sYd!!T~Ti-v*kmHw#yI;M#lv(XnrBG6;Rh@}LuzKG5)|;UB zte?F5K>z9O^9G17dVTELK^C;SeO3|!!0XtLPyk1G4$#BaKfW~eTfP2x?I=KbX!_Hj z7RkDQ&#GTqd|_P(UN!Fs@r_WT9$yXhE}q>GIvVz*GNCaZqw_f-F1MVyIJC0qx=wOTL~}isUJb8ry(|! QrSV2}<-oDOan8f|7h^$@00000 delta 850 zcmYjP-D}fO6whswHeH&gb#{zxc3~TC#jzpIFD3)O6>%zIZ-tOGw_R$Q%$sE3Kv(d^ zw{7OZy(vD`7ZLWLf5F~_$@Z{ig$WAcTSbxi=DCSqf%`l6+;h)4zu!H1G_=v1`6h}T z416!H->!Y;S2NvcV|Dn+S+0~)G9_L~D6xrzBK0semPkr2Ix9i6l$wZfkT=09SsJY& zlC8#yhx`KCMMeJr&7k!9bZi_U!G9RC^dq6Csq`=x;nfe|`a?ktZI(Z3=2Hy4jz#TF)-i)Bq#Jl($Q$@AXg zq6xmG%ZBU7j%_Xge=weF*wz?O?u62j`ZV3qPH;Nx|CEN7LMl@AcU3hiM#kaFd05VA zfClp^UD!&?jms@I^Vr*F^DU`+Ex#r8H?FnV>`H!{Ewse06>m#C(zvoC?5u5@ z;lv|BR7=SAB@_0Go|ZpXw{_3NQ-tzIu$;F5?KnbcmnnW^ggr*wX9^#Z{%FpgY!C3L pXN_w!V4jyl)6~CH?`u~Q^{t&~Ghmur6ThBmn(s#@e^5W{_X{ra)&Bqh diff --git a/starter/app.py b/starter/app.py index 74acc8342..9d0cec6aa 100644 --- a/starter/app.py +++ b/starter/app.py @@ -50,5 +50,29 @@ def check_solution() -> Any: return jsonify({"incorrect": incorrect}) +@app.route("/hint", methods=["POST"]) +def get_hint() -> Any: + """Fill the first empty cell with the correct solution value.""" + puzzle = CURRENT.get("puzzle") + solution = CURRENT.get("solution") + + if puzzle is None or solution is None: + return jsonify({"error": "No game in progress"}), 400 + + for row_index in range(sudoku_logic.SIZE): + for col_index in range(sudoku_logic.SIZE): + if puzzle[row_index][col_index] == sudoku_logic.EMPTY: + value = solution[row_index][col_index] + puzzle[row_index][col_index] = value + CURRENT["puzzle"] = puzzle + return jsonify({ + "row": row_index, + "col": col_index, + "value": value, + }) + + return jsonify({"message": "Puzzle already complete"}) + + if __name__ == "__main__": app.run(debug=True) diff --git a/starter/static/main.js b/starter/static/main.js index 881e9584a..6eb300aa4 100644 --- a/starter/static/main.js +++ b/starter/static/main.js @@ -53,7 +53,7 @@ function renderPuzzle(puz) { } async function newGame() { - const difficultySelect = document.getElementById('difficulty'); + const difficultySelect = document.getElementById("difficulty"); const difficulty = difficultySelect.value; const clues = DIFFICULTY_CLUES[difficulty]; const res = await fetch(`/new?clues=${clues}`); @@ -104,9 +104,30 @@ async function checkSolution() { } } +async function hintGame() { + const res = await fetch('/hint', {method: 'POST'}); + const data = await res.json(); + const msg = document.getElementById('message'); + + if (data.message) { + msg.style.color = '#d32f2f'; + msg.innerText = data.message; + return; + } + + const idx = data.row * SIZE + data.col; + const input = document.querySelector(`.sudoku-cell[data-row="${data.row}"][data-col="${data.col}"]`); + input.value = data.value; + input.disabled = true; + input.className = 'sudoku-cell prefilled'; + msg.style.color = '#388e3c'; + msg.innerText = 'Hint used.'; +} + // Wire buttons window.addEventListener('load', () => { document.getElementById('new-game').addEventListener('click', newGame); + document.getElementById('hint-button').addEventListener('click', hintGame); document.getElementById('check-solution').addEventListener('click', checkSolution); // initialize newGame(); diff --git a/starter/sudoku_logic.py b/starter/sudoku_logic.py index 793887a96..5c3e0ad71 100644 --- a/starter/sudoku_logic.py +++ b/starter/sudoku_logic.py @@ -51,22 +51,52 @@ def fill_board(board: Board) -> bool: return True +def count_solutions(board: Board, limit: int = 2) -> int: + """Count the number of solutions for a Sudoku board up to ``limit``.""" + board_copy = deep_copy(board) + + for row in range(SIZE): + for col in range(SIZE): + if board_copy[row][col] == EMPTY: + possible = list(range(1, SIZE + 1)) + random.shuffle(possible) + for candidate in possible: + if is_safe(board_copy, row, col, candidate): + board_copy[row][col] = candidate + solutions = count_solutions(board_copy, limit) + board_copy[row][col] = EMPTY + if solutions >= limit: + return limit + return 0 + + return 1 + + +def _has_unique_solution(board: Board) -> bool: + """Return True when the board has exactly one solution.""" + return count_solutions(board, limit=2) == 1 + + def remove_cells(board: Board, clues: int) -> None: - """Remove values from the board until it has the requested clue count.""" + """Remove values from the board until the clue count is reached.""" attempts = SIZE * SIZE - clues + while attempts > 0: row = random.randrange(SIZE) col = random.randrange(SIZE) + if board[row][col] != EMPTY: board[row][col] = EMPTY attempts -= 1 - def generate_puzzle(clues: int = 35) -> Tuple[Board, Board]: - """Generate a Sudoku puzzle and its solved solution.""" board = create_empty_board() fill_board(board) + solution = deep_copy(board) + remove_cells(board, clues) + puzzle = deep_copy(board) - return puzzle, solution + + return puzzle, solution \ No newline at end of file diff --git a/starter/templates/index.html b/starter/templates/index.html index 25f189607..a46e31735 100644 --- a/starter/templates/index.html +++ b/starter/templates/index.html @@ -16,6 +16,7 @@

Sudoku Game

+
diff --git a/starter/tests/test_sudoku_logic.py b/starter/tests/test_sudoku_logic.py new file mode 100644 index 000000000..0c99a9856 --- /dev/null +++ b/starter/tests/test_sudoku_logic.py @@ -0,0 +1,15 @@ +import unittest + +import sudoku_logic + + +class SudokuLogicTests(unittest.TestCase): + def test_generate_puzzle_has_a_unique_solution(self): + for clues in (26, 32, 35, 40): + with self.subTest(clues=clues): + puzzle, _ = sudoku_logic.generate_puzzle(clues) + self.assertTrue(sudoku_logic._has_unique_solution(puzzle)) + + +if __name__ == "__main__": + unittest.main() From ed8e0aa4586dbbd261d8d67305cd9b9d38ea25e4 Mon Sep 17 00:00:00 2001 From: soham03patra Date: Fri, 10 Jul 2026 09:36:47 +0530 Subject: [PATCH 05/10] Added live validation --- starter/app.py | 16 ++++++++++++++++ starter/static/main.js | 24 ++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/starter/app.py b/starter/app.py index 9d0cec6aa..78f4da688 100644 --- a/starter/app.py +++ b/starter/app.py @@ -74,5 +74,21 @@ def get_hint() -> Any: return jsonify({"message": "Puzzle already complete"}) +@app.route("/validate", methods=["POST"]) +def validate_move() -> Any: + """Check whether a submitted move matches the current solution.""" + data = request.get_json() + row = data.get("row") + col = data.get("col") + value = data.get("value") + solution = CURRENT.get("solution") + + if solution is None: + return jsonify({"error": "No game in progress"}), 400 + + correct = solution[row][col] == value + return jsonify({"correct": correct}) + + if __name__ == "__main__": app.run(debug=True) diff --git a/starter/static/main.js b/starter/static/main.js index 6eb300aa4..7833ea94b 100644 --- a/starter/static/main.js +++ b/starter/static/main.js @@ -7,6 +7,27 @@ const DIFFICULTY_CLUES = { }; let puzzle = []; +async function validateCellInput(event) { + const input = event.target; + const value = input.value; + + if (value === '') { + input.className = 'sudoku-cell'; + return; + } + + const row = parseInt(input.dataset.row, 10); + const col = parseInt(input.dataset.col, 10); + const res = await fetch('/validate', { + method: 'POST', + headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({row, col, value: parseInt(value, 10)}) + }); + const data = await res.json(); + + input.className = data.correct ? 'sudoku-cell' : 'sudoku-cell incorrect'; +} + function createBoardElement() { const boardDiv = document.getElementById('sudoku-board'); boardDiv.innerHTML = ''; @@ -23,6 +44,9 @@ function createBoardElement() { input.addEventListener('input', (e) => { const val = e.target.value.replace(/[^1-9]/g, ''); e.target.value = val; + if (val !== '') { + validateCellInput(e); + } }); rowDiv.appendChild(input); } From 6fce522ef5d4c026dd9406d996f137bd68c8e5a4 Mon Sep 17 00:00:00 2001 From: soham03patra Date: Fri, 10 Jul 2026 09:45:32 +0530 Subject: [PATCH 06/10] Added game timer --- starter/static/main.js | 37 ++++++++++++++++++++++++++++++++++++ starter/static/styles.css | 10 ++++++++++ starter/templates/index.html | 3 +++ 3 files changed, 50 insertions(+) diff --git a/starter/static/main.js b/starter/static/main.js index 7833ea94b..570738d37 100644 --- a/starter/static/main.js +++ b/starter/static/main.js @@ -6,6 +6,40 @@ const DIFFICULTY_CLUES = { hard: 26 }; let puzzle = []; +let timerInterval; +let elapsedSeconds = 0; + +function startTimer() { + if (timerInterval) { + return; + } + timerInterval = setInterval(() => { + elapsedSeconds += 1; + updateTimer(); + }, 1000); +} + +function stopTimer() { + if (timerInterval) { + clearInterval(timerInterval); + timerInterval = null; + } +} + +function resetTimer() { + stopTimer(); + elapsedSeconds = 0; + updateTimer(); +} + +function updateTimer() { + const minutes = String(Math.floor(elapsedSeconds / 60)).padStart(2, '0'); + const seconds = String(elapsedSeconds % 60).padStart(2, '0'); + const timer = document.getElementById('timer'); + if (timer) { + timer.textContent = `${minutes}:${seconds}`; + } +} async function validateCellInput(event) { const input = event.target; @@ -84,6 +118,8 @@ async function newGame() { const data = await res.json(); renderPuzzle(data.puzzle); document.getElementById('message').innerText = ''; + resetTimer(); + startTimer(); } async function checkSolution() { @@ -120,6 +156,7 @@ async function checkSolution() { } } if (incorrect.size === 0) { + stopTimer(); msg.style.color = '#388e3c'; msg.innerText = 'Congratulations! You solved it!'; } else { diff --git a/starter/static/styles.css b/starter/static/styles.css index 1a6218ff9..3b2c79d14 100644 --- a/starter/static/styles.css +++ b/starter/static/styles.css @@ -78,6 +78,16 @@ button:hover { background: #1565c0; } +#timer-container { + text-align: center; + margin-bottom: 10px; +} + +#timer { + font-size: 22px; + font-weight: bold; +} + #message { margin-left: 20px; font-size: 16px; diff --git a/starter/templates/index.html b/starter/templates/index.html index a46e31735..9b63d55ff 100644 --- a/starter/templates/index.html +++ b/starter/templates/index.html @@ -7,6 +7,9 @@

Sudoku Game

+
+ 00:00 +
From d89d51ee3bfb07f017cf631634f23fd8d22618a4 Mon Sep 17 00:00:00 2001 From: soham03patra Date: Fri, 10 Jul 2026 09:50:21 +0530 Subject: [PATCH 07/10] Added dark mode --- starter/static/main.js | 19 ++++++++++++++ starter/static/styles.css | 48 ++++++++++++++++++++++++++++++++++++ starter/templates/index.html | 1 + 3 files changed, 68 insertions(+) diff --git a/starter/static/main.js b/starter/static/main.js index 570738d37..fc1050b36 100644 --- a/starter/static/main.js +++ b/starter/static/main.js @@ -9,6 +9,21 @@ let puzzle = []; let timerInterval; let elapsedSeconds = 0; +function applyTheme(theme) { + document.body.classList.toggle('dark-mode', theme === 'dark'); + const themeButton = document.getElementById('theme-toggle'); + if (themeButton) { + themeButton.textContent = theme === 'dark' ? '🌞 Light Mode' : '🌙 Dark Mode'; + } +} + +function toggleTheme() { + const isDarkMode = document.body.classList.contains('dark-mode'); + const nextTheme = isDarkMode ? 'light' : 'dark'; + localStorage.setItem('sudoku-theme', nextTheme); + applyTheme(nextTheme); +} + function startTimer() { if (timerInterval) { return; @@ -187,9 +202,13 @@ async function hintGame() { // Wire buttons window.addEventListener('load', () => { + const savedTheme = localStorage.getItem('sudoku-theme') || 'light'; + applyTheme(savedTheme); + document.getElementById('new-game').addEventListener('click', newGame); document.getElementById('hint-button').addEventListener('click', hintGame); document.getElementById('check-solution').addEventListener('click', checkSolution); + document.getElementById('theme-toggle').addEventListener('click', toggleTheme); // initialize newGame(); }); \ No newline at end of file diff --git a/starter/static/styles.css b/starter/static/styles.css index 3b2c79d14..312cfb02f 100644 --- a/starter/static/styles.css +++ b/starter/static/styles.css @@ -6,11 +6,20 @@ body { padding: 0; } +body.dark-mode { + background: #121212; + color: #f5f5f5; +} + h1 { margin-top: 30px; color: #333; } +body.dark-mode h1 { + color: #f5f5f5; +} + #sudoku-board { display: inline-block; margin: 30px auto; @@ -19,6 +28,12 @@ h1 { box-shadow: 0 2px 8px rgba(0,0,0,0.1); } +body.dark-mode #sudoku-board { + border-color: #555; + background: #1e1e1e; + box-shadow: 0 2px 8px rgba(0,0,0,0.4); +} + .sudoku-row { display: flex; } @@ -34,20 +49,40 @@ h1 { transition: background 0.2s; } +body.dark-mode .sudoku-cell { + background: #2a2a2a; + color: #f5f5f5; + border-color: #666; +} + .sudoku-cell:focus { background: #e0f7fa; } +body.dark-mode .sudoku-cell:focus { + background: #37474f; +} + .sudoku-cell.prefilled { background: #e0e0e0; font-weight: bold; color: #333; } +body.dark-mode .sudoku-cell.prefilled { + background: #444; + color: #f5f5f5; +} + .sudoku-cell.incorrect { background: #ffcdd2; } +body.dark-mode .sudoku-cell.incorrect { + background: #7f1d1d; + color: #fff; +} + .sudoku-cell:nth-child(3), .sudoku-cell:nth-child(6) { border-right: 3px solid #333; @@ -74,10 +109,19 @@ button { transition: background 0.2s; } +body.dark-mode button { + background: #2f6fed; + color: #f5f5f5; +} + button:hover { background: #1565c0; } +body.dark-mode button:hover { + background: #2457b3; +} + #timer-container { text-align: center; margin-bottom: 10px; @@ -93,3 +137,7 @@ button:hover { font-size: 16px; color: #d32f2f; } + +body.dark-mode #message { + color: #ff8a80; +} diff --git a/starter/templates/index.html b/starter/templates/index.html index 9b63d55ff..29dafd943 100644 --- a/starter/templates/index.html +++ b/starter/templates/index.html @@ -7,6 +7,7 @@

Sudoku Game

+
00:00
From 4ba26d48215a36d9e400972c751b3f182684b60f Mon Sep 17 00:00:00 2001 From: soham03patra Date: Fri, 10 Jul 2026 09:55:39 +0530 Subject: [PATCH 08/10] Added leaderboard --- starter/static/main.js | 66 ++++++++++++++++++++++++++++++ starter/static/styles.css | 78 ++++++++++++++++++++++++++++++++++++ starter/templates/index.html | 14 +++++++ 3 files changed, 158 insertions(+) diff --git a/starter/static/main.js b/starter/static/main.js index fc1050b36..d1571c7e3 100644 --- a/starter/static/main.js +++ b/starter/static/main.js @@ -56,6 +56,56 @@ function updateTimer() { } } +function loadLeaderboard() { + const stored = localStorage.getItem('sudoku-leaderboard'); + return stored ? JSON.parse(stored) : []; +} + +function saveLeaderboard(entries) { + localStorage.setItem('sudoku-leaderboard', JSON.stringify(entries)); +} + +function renderLeaderboard() { + const tbody = document.querySelector('#leaderboard tbody'); + if (!tbody) { + return; + } + + const entries = loadLeaderboard(); + tbody.innerHTML = ''; + + if (entries.length === 0) { + const row = document.createElement('tr'); + const cell = document.createElement('td'); + cell.colSpan = 4; + cell.textContent = 'No scores yet'; + row.appendChild(cell); + tbody.appendChild(row); + return; + } + + entries.forEach((entry, index) => { + const row = document.createElement('tr'); + const rankCell = document.createElement('td'); + rankCell.textContent = index + 1; + row.appendChild(rankCell); + + const nameCell = document.createElement('td'); + nameCell.textContent = entry.name; + row.appendChild(nameCell); + + const difficultyCell = document.createElement('td'); + difficultyCell.textContent = entry.difficulty; + row.appendChild(difficultyCell); + + const timeCell = document.createElement('td'); + timeCell.textContent = entry.time; + row.appendChild(timeCell); + + tbody.appendChild(row); + }); +} + async function validateCellInput(event) { const input = event.target; const value = input.value; @@ -172,6 +222,21 @@ async function checkSolution() { } if (incorrect.size === 0) { stopTimer(); + const playerName = window.prompt('Enter your name for the leaderboard:') || 'Anonymous'; + const difficultySelect = document.getElementById('difficulty'); + const difficulty = difficultySelect.value; + const minutes = String(Math.floor(elapsedSeconds / 60)).padStart(2, '0'); + const seconds = String(elapsedSeconds % 60).padStart(2, '0'); + const time = `${minutes}:${seconds}`; + const entries = loadLeaderboard(); + entries.push({name: playerName, difficulty, time}); + entries.sort((a, b) => { + const aTime = a.time.split(':').reduce((total, part) => total * 60 + parseInt(part, 10), 0); + const bTime = b.time.split(':').reduce((total, part) => total * 60 + parseInt(part, 10), 0); + return aTime - bTime; + }); + saveLeaderboard(entries.slice(0, 10)); + renderLeaderboard(); msg.style.color = '#388e3c'; msg.innerText = 'Congratulations! You solved it!'; } else { @@ -209,6 +274,7 @@ window.addEventListener('load', () => { document.getElementById('hint-button').addEventListener('click', hintGame); document.getElementById('check-solution').addEventListener('click', checkSolution); document.getElementById('theme-toggle').addEventListener('click', toggleTheme); + renderLeaderboard(); // initialize newGame(); }); \ No newline at end of file diff --git a/starter/static/styles.css b/starter/static/styles.css index 312cfb02f..078318bb4 100644 --- a/starter/static/styles.css +++ b/starter/static/styles.css @@ -141,3 +141,81 @@ body.dark-mode button:hover { body.dark-mode #message { color: #ff8a80; } + +#leaderboard { + max-width: 700px; + margin: 30px auto 40px; + padding: 20px; + background: #fff; + border: 1px solid #ddd; + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0,0,0,0.08); +} + +body.dark-mode #leaderboard { + background: #1e1e1e; + border-color: #444; + box-shadow: 0 2px 8px rgba(0,0,0,0.35); +} + +#leaderboard h2 { + margin-top: 0; + margin-bottom: 12px; + font-size: 20px; +} + +#leaderboard table { + width: 100%; + border-collapse: collapse; + border-radius: 8px; + overflow: hidden; +} + +#leaderboard th, +#leaderboard td { + padding: 10px 12px; + border: 1px solid #ddd; + text-align: center; +} + +body.dark-mode #leaderboard th, +body.dark-mode #leaderboard td { + border-color: #555; +} + +#leaderboard th { + background: #1976d2; + color: #fff; +} + +body.dark-mode #leaderboard th { + background: #2f6fed; +} + +#leaderboard tbody tr:nth-child(even) { + background: #f7f7f7; +} + +body.dark-mode #leaderboard tbody tr:nth-child(even) { + background: #2a2a2a; +} + +#leaderboard tbody tr:hover { + background: #e3f2fd; +} + +body.dark-mode #leaderboard tbody tr:hover { + background: #333; +} + +@media (max-width: 600px) { + #leaderboard { + padding: 12px; + } + + #leaderboard th, + #leaderboard td { + padding: 8px 6px; + font-size: 14px; + } +} diff --git a/starter/templates/index.html b/starter/templates/index.html index 29dafd943..0f735b128 100644 --- a/starter/templates/index.html +++ b/starter/templates/index.html @@ -12,6 +12,20 @@

Sudoku Game

00:00
+
+

Top 10 Leaderboard

+ + + + + + + + + + +
RankPlayerDifficultyTime
+