diff --git a/starter/My_Project.zip b/starter/My_Project.zip new file mode 100644 index 00000000..61998de7 Binary files /dev/null and b/starter/My_Project.zip differ diff --git a/starter/__pycache__/sudoku_logic.cpython-313.pyc b/starter/__pycache__/sudoku_logic.cpython-313.pyc new file mode 100644 index 00000000..78cc7242 Binary files /dev/null and b/starter/__pycache__/sudoku_logic.cpython-313.pyc differ diff --git a/starter/app.py b/starter/app.py index 0f526b75..5181d82e 100644 --- a/starter/app.py +++ b/starter/app.py @@ -1,39 +1,163 @@ +""" +Flask Sudoku Application + +This file contains the Flask routes that power the Sudoku game. +It handles puzzle generation, solution checking, hints, +real-time validation and communication with the frontend. +""" + from flask import Flask, render_template, jsonify, request import sudoku_logic app = Flask(__name__) -# Keep a simple in-memory store for current puzzle and solution +# Stores the current puzzle and its solution in memory. CURRENT = { - 'puzzle': None, - 'solution': None + "puzzle": None, + "solution": None } -@app.route('/') + +@app.route("/") def index(): - return render_template('index.html') + """ + Render the main Sudoku game page. + """ + return render_template("index.html") -@app.route('/new') + +@app.route("/new") def new_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}) + """ + Generate a new Sudoku puzzle. + + Query Parameters: + clues (int): Number of visible cells. + + Returns: + JSON response containing the generated puzzle. + """ + try: + clues = int(request.args.get("clues", 35)) + + if clues < 20 or clues > 60: + return jsonify({"error": "Clues must be between 20 and 60."}), 400 + + puzzle, solution = sudoku_logic.generate_puzzle(clues) + + CURRENT["puzzle"] = puzzle + CURRENT["solution"] = solution + + return jsonify({"puzzle": puzzle}) + + except ValueError: + return jsonify({"error": "Invalid clues value."}), 400 -@app.route('/check', methods=['POST']) + except Exception as error: + print(f"Error generating puzzle: {error}") + return jsonify({"error": "Unable to generate puzzle."}), 500 + + +@app.route("/check", methods=["POST"]) def check_solution(): - data = request.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__': + """ + Compare the player's board with the solution. + + Returns: + List of incorrect cell coordinates. + """ + try: + data = request.get_json() + + if not data or "board" not in data: + return jsonify({"error": "Board data is missing."}), 400 + + board = data["board"] + + solution = CURRENT.get("solution") + + if solution is None: + return jsonify({"error": "No active game."}), 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}) + + except Exception as error: + print(f"Check error: {error}") + return jsonify({"error": "Unable to check solution."}), 500 + + +@app.route("/hint") +def hint(): + """ + Reveal one correct cell and lock it. + + Returns: + JSON containing row, column and value. + """ + try: + puzzle = CURRENT.get("puzzle") + solution = CURRENT.get("solution") + + if puzzle is None or solution is None: + return jsonify({"error": "No active game."}), 400 + + for i in range(sudoku_logic.SIZE): + for j in range(sudoku_logic.SIZE): + + if puzzle[i][j] == 0: + puzzle[i][j] = solution[i][j] + + return jsonify({ + "row": i, + "col": j, + "value": solution[i][j] + }) + + return jsonify({"error": "Puzzle already complete."}), 400 + + except Exception as error: + print(f"Hint error: {error}") + return jsonify({"error": "Unable to generate hint."}), 500 + + +@app.route("/validate", methods=["POST"]) +def validate(): + """ + Validate a single Sudoku cell. + + Returns: + JSON indicating whether the entered value is correct. + """ + try: + data = request.get_json() + + if not data: + return jsonify({"error": "Missing request data."}), 400 + + row = data.get("row") + col = data.get("col") + value = data.get("value") + + solution = CURRENT.get("solution") + + if solution is None: + return jsonify({"error": "No active game."}), 400 + + return jsonify({ + "correct": solution[row][col] == value + }) + + except Exception as error: + print(f"Validation error: {error}") + return jsonify({"error": "Validation failed."}), 500 + + +if __name__ == "__main__": app.run(debug=True) \ No newline at end of file diff --git a/starter/instruction.md b/starter/instruction.md new file mode 100644 index 00000000..071351ac --- /dev/null +++ b/starter/instruction.md @@ -0,0 +1,47 @@ +# GitHub Copilot Instructions + +## Project +Refactor a legacy Python Flask Sudoku application into a modern, maintainable web application. + +## Coding Standards + +- Use Python best practices (PEP 8). +- Write modular and reusable functions. +- Keep functions small and focused. +- Add comments where logic is complex. +- Handle errors gracefully. +- Do not duplicate code. +- Prefer readability over clever code. + +## Sudoku Requirements + +- Generate puzzles with exactly one unique solution. +- Support Easy, Medium and Hard difficulty levels. +- Lock all prefilled cells. +- Validate user moves. +- Provide Hint and Check features. +- Display a completion message. +- Maintain a Top 10 leaderboard using browser local storage. +- Support dark mode. +- Display a timer. + +## Frontend + +- Use responsive design. +- Alternate colours for each 3×3 Sudoku block. +- Keep the UI usable in both light and dark themes. + +## Testing + +- Use pytest. +- Ensure existing functionality continues to work after refactoring. + +## Copilot Guidance + +When suggesting code: + +- Explain complex logic. +- Prefer modular implementations. +- Avoid unnecessary libraries. +- Keep Flask routes simple. +- Write maintainable code. \ No newline at end of file diff --git a/starter/screenshots/copilot_dark_mode.png b/starter/screenshots/copilot_dark_mode.png new file mode 100644 index 00000000..3b05848c Binary files /dev/null and b/starter/screenshots/copilot_dark_mode.png differ diff --git a/starter/screenshots/copilot_grid_styling.png b/starter/screenshots/copilot_grid_styling.png new file mode 100644 index 00000000..5a39203a Binary files /dev/null and b/starter/screenshots/copilot_grid_styling.png differ diff --git a/starter/screenshots/copilot_hint_button.png b/starter/screenshots/copilot_hint_button.png new file mode 100644 index 00000000..4edb8b6a Binary files /dev/null and b/starter/screenshots/copilot_hint_button.png differ diff --git a/starter/screenshots/copilot_leaderboard.png b/starter/screenshots/copilot_leaderboard.png new file mode 100644 index 00000000..d7d0e44a Binary files /dev/null and b/starter/screenshots/copilot_leaderboard.png differ diff --git a/starter/screenshots/copilot_rejected_suggestion.png b/starter/screenshots/copilot_rejected_suggestion.png new file mode 100644 index 00000000..d593bacd Binary files /dev/null and b/starter/screenshots/copilot_rejected_suggestion.png differ diff --git a/starter/screenshots/copilot_testing_framework.png b/starter/screenshots/copilot_testing_framework.png new file mode 100644 index 00000000..fc91df9b Binary files /dev/null and b/starter/screenshots/copilot_testing_framework.png differ diff --git a/starter/screenshots/copilot_unique_solution.png b/starter/screenshots/copilot_unique_solution.png new file mode 100644 index 00000000..d66249b4 Binary files /dev/null and b/starter/screenshots/copilot_unique_solution.png differ diff --git a/starter/screenshots/initial_tests.png b/starter/screenshots/initial_tests.png new file mode 100644 index 00000000..22d9d485 Binary files /dev/null and b/starter/screenshots/initial_tests.png differ diff --git a/starter/static/main.js b/starter/static/main.js index 2028e102..491e4229 100644 --- a/starter/static/main.js +++ b/starter/static/main.js @@ -1,105 +1,336 @@ -// Client-side rendering and interaction for the Flask-backed Sudoku -const SIZE = 9; -let puzzle = []; - -function createBoardElement() { - const boardDiv = document.getElementById('sudoku-board'); - boardDiv.innerHTML = ''; - for (let i = 0; i < SIZE; i++) { - const rowDiv = document.createElement('div'); - rowDiv.className = 'sudoku-row'; - for (let j = 0; j < SIZE; j++) { - const input = document.createElement('input'); - input.type = 'text'; - input.maxLength = 1; - input.className = 'sudoku-cell'; - input.dataset.row = i; - input.dataset.col = j; - input.addEventListener('input', (e) => { - const val = e.target.value.replace(/[^1-9]/g, ''); - e.target.value = val; - }); - rowDiv.appendChild(input); - } - boardDiv.appendChild(rowDiv); - } +const board = document.getElementById("sudoku-board"); +const message = document.getElementById("message"); +const timerElement = document.getElementById("timer"); + +let currentBoard = []; +let seconds = 0; +let timer = null; + +// ---------- TIMER ---------- + +function startTimer() { + clearInterval(timer); + + seconds = 0; + + timer = setInterval(() => { + + seconds++; + + let mins = Math.floor(seconds / 60) + .toString() + .padStart(2, "0"); + + let secs = (seconds % 60) + .toString() + .padStart(2, "0"); + + timerElement.textContent = `${mins}:${secs}`; + + }, 1000); +} + +function stopTimer() { + clearInterval(timer); } -function renderPuzzle(puz) { - puzzle = puz; - createBoardElement(); - const boardDiv = document.getElementById('sudoku-board'); - const inputs = boardDiv.getElementsByTagName('input'); - for (let i = 0; i < SIZE; i++) { - for (let j = 0; j < SIZE; j++) { - const idx = i * SIZE + j; - const val = puzzle[i][j]; - const inp = inputs[idx]; - if (val !== 0) { - inp.value = val; - inp.disabled = true; - inp.className += ' prefilled'; - } else { - inp.value = ''; - inp.disabled = false; - } +// ---------- BOARD ---------- + +function renderBoard(puzzle) { + + currentBoard = JSON.parse(JSON.stringify(puzzle)); + + board.innerHTML = ""; + + for (let i = 0; i < 9; i++) { + + const row = document.createElement("div"); + row.className = "sudoku-row"; + + for (let j = 0; j < 9; j++) { + + const input = document.createElement("input"); + + input.type = "number"; + + input.min = 1; + input.max = 9; + + input.className = "sudoku-cell"; + + input.dataset.row = i; + input.dataset.col = j; + + if (puzzle[i][j] !== 0) { + + input.value = puzzle[i][j]; + input.readOnly = true; + input.classList.add("prefilled"); + + } + + input.addEventListener("input", () => { + + let value = parseInt(input.value); + + if (isNaN(value)) { + + currentBoard[i][j] = 0; + + } else { + + currentBoard[i][j] = value; + + } + + input.classList.remove("incorrect"); + + }); + + row.appendChild(input); + + } + + board.appendChild(row); + } - } + } +// ---------- NEW GAME ---------- + async function newGame() { - const res = await fetch('/new'); - const data = await res.json(); - renderPuzzle(data.puzzle); - document.getElementById('message').innerText = ''; + + const clues = + document.getElementById("difficulty").value; + + const response = + await fetch(`/new?clues=${clues}`); + + const data = + await response.json(); + + renderBoard(data.puzzle); + + message.textContent = ""; + + startTimer(); + } -async function checkSolution() { - const boardDiv = document.getElementById('sudoku-board'); - const inputs = boardDiv.getElementsByTagName('input'); - const board = []; - for (let i = 0; i < SIZE; i++) { - board[i] = []; - for (let j = 0; j < SIZE; j++) { - const idx = i * SIZE + j; - const val = inputs[idx].value; - board[i][j] = val ? parseInt(val, 10) : 0; +document +.getElementById("new-game") +.addEventListener("click", newGame); + +// ---------- CHECK ---------- + +async function checkBoard() { + + document.querySelectorAll(".incorrect") + .forEach(c => c.classList.remove("incorrect")); + + const response = await fetch("/check", { + + method: "POST", + + headers: { + + "Content-Type": "application/json" + + }, + + body: JSON.stringify({ + + board: currentBoard + + }) + + }); + + const data = await response.json(); + + if (data.incorrect.length === 0) { + + stopTimer(); + + message.textContent = + "🎉 Congratulations! Puzzle Solved."; + + saveScore(); + + } else { + + message.textContent = + `❌ ${data.incorrect.length} incorrect cell(s).`; + + data.incorrect.forEach(cell => { + + const selector = + `.sudoku-cell[data-row="${cell[0]}"][data-col="${cell[1]}"]`; + + document + .querySelector(selector) + .classList.add("incorrect"); + + }); + } - } - const res = await fetch('/check', { - method: 'POST', - headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({board}) - }); - const data = await res.json(); - const msg = document.getElementById('message'); - if (data.error) { - msg.style.color = '#d32f2f'; - msg.innerText = data.error; - return; - } - const incorrect = new Set(data.incorrect.map(x => x[0]*SIZE + x[1])); - for (let idx = 0; idx < inputs.length; idx++) { - const inp = inputs[idx]; - if (inp.disabled) continue; - inp.className = 'sudoku-cell'; - if (incorrect.has(idx)) { - inp.className = 'sudoku-cell incorrect'; + +} + +document +.getElementById("check-solution") +.addEventListener("click", checkBoard); + +// ---------- HINT ---------- + +document +.getElementById("hint-btn") +.addEventListener("click", async () => { + + try{ + + const response = + await fetch("/hint"); + + if(!response.ok){ + + alert("Hint backend not added yet."); + + return; + + } + + const hint = + await response.json(); + + currentBoard[hint.row][hint.col] = + hint.value; + + const selector = + `.sudoku-cell[data-row="${hint.row}"][data-col="${hint.col}"]`; + + const cell = + document.querySelector(selector); + + cell.value = hint.value; + + cell.readOnly = true; + + cell.classList.add("prefilled"); + + } + + catch{ + + alert("Hint backend not implemented."); + + } + +}); + +// ---------- DARK MODE ---------- + +const themeBtn = +document.getElementById("theme-toggle"); + +themeBtn.addEventListener("click",()=>{ + + document.body.classList.toggle("dark"); + + if(document.body.classList.contains("dark")){ + + themeBtn.innerHTML="☀️ Light Mode"; + + }else{ + + themeBtn.innerHTML="🌙 Dark Mode"; + } - } - if (incorrect.size === 0) { - msg.style.color = '#388e3c'; - msg.innerText = 'Congratulations! You solved it!'; - } else { - msg.style.color = '#d32f2f'; - msg.innerText = 'Some cells are incorrect.'; - } + +}); + +// ---------- SCOREBOARD ---------- + +function saveScore(){ + + let scores = + JSON.parse(localStorage.getItem("scores")) || []; + + const name = + prompt("Enter your name"); + + scores.push({ + + name:name || "Anonymous", + + difficulty: + document.getElementById("difficulty") + .selectedOptions[0].text, + + time:seconds + + }); + + scores.sort((a,b)=>a.time-b.time); + + scores=scores.slice(0,10); + + localStorage.setItem("scores", + JSON.stringify(scores)); + + loadScores(); + +} + + +function loadScores(){ + + const body= + document.getElementById("leaderboard-body"); + + body.innerHTML=""; + + let scores= + JSON.parse(localStorage.getItem("scores")) || []; + + scores.forEach((score,index)=>{ + + const row=document.createElement("tr"); + + const mins=Math.floor(score.time/60) + .toString() + .padStart(2,"0"); + + const secs=(score.time%60) + .toString() + .padStart(2,"0"); + + row.innerHTML=` + + ${index+1} + + ${score.name} + + ${score.difficulty} + + ${mins}:${secs} + + `; + + body.appendChild(row); + + }); + } -// Wire buttons -window.addEventListener('load', () => { - document.getElementById('new-game').addEventListener('click', newGame); - document.getElementById('check-solution').addEventListener('click', checkSolution); - // initialize - newGame(); -}); \ No newline at end of file +// ---------- START ---------- + +window.onload=()=>{ + + loadScores(); + + newGame(); + +}; +const playerNameInput = document.getElementById("player-name"); \ No newline at end of file diff --git a/starter/static/styles.css b/starter/static/styles.css index 1a6218ff..6305658c 100644 --- a/starter/static/styles.css +++ b/starter/static/styles.css @@ -1,85 +1,221 @@ -body { - font-family: Arial, sans-serif; - background: #f4f4f4; - text-align: center; - margin: 0; - padding: 0; +:root{ + --bg:#f4f4f4; + --card:#ffffff; + --text:#222222; + --primary:#1976d2; + --secondary:#eeeeee; + --incorrect:#ff6b6b; + --correct:#d4ffd4; + --yellow:#fdf6d8; + --grey:#dddddd; } -h1 { - margin-top: 30px; - color: #333; +body.dark{ + --bg:#121212; + --card:#1e1e1e; + --text:#f2f2f2; + --primary:#42a5f5; + --secondary:#2b2b2b; + --yellow:#5a5225; + --grey:#4a4a4a; } -#sudoku-board { - display: inline-block; - margin: 30px auto; - border: 4px solid #333; - background: #fff; - box-shadow: 0 2px 8px rgba(0,0,0,0.1); +*{ + margin:0; + padding:0; + box-sizing:border-box; } -.sudoku-row { - display: flex; +body{ + font-family:Arial,Helvetica,sans-serif; + background:var(--bg); + color:var(--text); + transition:0.3s; } -.sudoku-cell { - width: 40px; - height: 40px; - border: 1px solid #bbb; - text-align: center; - font-size: 20px; - outline: none; - background: #fafafa; - transition: background 0.2s; +.container{ + width:95%; + max-width:900px; + margin:auto; + text-align:center; + padding:20px; } -.sudoku-cell:focus { - background: #e0f7fa; +h1{ + margin:20px 0; + font-size:48px; } -.sudoku-cell.prefilled { - background: #e0e0e0; - font-weight: bold; - color: #333; +.top-controls{ + display:flex; + justify-content:space-between; + align-items:center; + flex-wrap:wrap; + gap:20px; + margin-bottom:25px; } -.sudoku-cell.incorrect { - background: #ffcdd2; +.control-group{ + display:flex; + flex-direction:column; + align-items:center; + gap:6px; } -.sudoku-cell:nth-child(3), -.sudoku-cell:nth-child(6) { - border-right: 3px solid #333; +select, +button, +input{ + font-size:16px; + padding:10px 15px; + border-radius:6px; } -.sudoku-row:nth-child(3) .sudoku-cell, -.sudoku-row:nth-child(6) .sudoku-cell { - border-bottom: 3px solid #333; +select{ + border:1px solid #ccc; } -.controls { - margin: 20px auto; +button{ + border:none; + background:var(--primary); + color:white; + cursor:pointer; } -button { - padding: 8px 18px; - margin: 0 8px; - font-size: 16px; - border: none; - background: #1976d2; - color: #fff; - border-radius: 4px; - cursor: pointer; - transition: background 0.2s; +button:hover{ + opacity:0.9; } -button:hover { - background: #1565c0; +.controls{ + display:flex; + justify-content:center; + gap:15px; + flex-wrap:wrap; + margin-top:25px; } -#message { - margin-left: 20px; - font-size: 16px; - color: #d32f2f; +#timer{ + font-size:34px; + font-weight:bold; } + +#message{ + margin-top:20px; + font-size:22px; + font-weight:bold; +} + +#sudoku-board{ + display:inline-block; + background:var(--card); + padding:12px; + border-radius:15px; + box-shadow:0 5px 15px rgba(0,0,0,.25); +} + +.sudoku-row{ + display:flex; +} + +.sudoku-cell{ + width:50px; + height:50px; + border:1px solid #999; + text-align:center; + font-size:28px; + outline:none; + transition:.2s; +} + +/* 3x3 borders */ + +.sudoku-row:nth-child(3n) .sudoku-cell{ + border-bottom:3px solid black; +} + +.sudoku-cell:nth-child(3n){ + border-right:3px solid black; +} + +/* Alternate box colours */ + +.sudoku-row:nth-child(-n+3) .sudoku-cell:nth-child(-n+3), +.sudoku-row:nth-child(-n+3) .sudoku-cell:nth-child(n+7), +.sudoku-row:nth-child(n+4):nth-child(-n+6) .sudoku-cell:nth-child(n+4):nth-child(-n+6), +.sudoku-row:nth-child(n+7) .sudoku-cell:nth-child(-n+3), +.sudoku-row:nth-child(n+7) .sudoku-cell:nth-child(n+7){ + background:var(--yellow); +} + +/* Locked cells */ + +.prefilled{ + background:var(--grey) !important; + font-weight:bold; +} + +/* Incorrect cells - ALWAYS RED */ + +.incorrect{ + background:var(--incorrect) !important; + color:#000 !important; +} + +/* Correct cells */ + +.correct{ + background:var(--correct) !important; +} + +/* Leaderboard */ + +.leaderboard{ + margin-top:35px; +} + +table{ + width:100%; + border-collapse:collapse; + margin-top:15px; +} + +th,td{ + border:1px solid #ccc; + padding:10px; +} + +th{ + background:var(--primary); + color:white; +} + +/* Mobile */ + +@media(max-width:700px){ + + h1{ + font-size:34px; + } + + .top-controls{ + flex-direction:column; + } + + .controls{ + flex-direction:column; + align-items:center; + } + + button{ + width:200px; + } + + .sudoku-cell{ + width:38px; + height:38px; + font-size:22px; + } + + #timer{ + font-size:26px; + } +} \ No newline at end of file diff --git a/starter/sudoku_logic.py b/starter/sudoku_logic.py index 443b2452..a8608439 100644 --- a/starter/sudoku_logic.py +++ b/starter/sudoku_logic.py @@ -4,54 +4,155 @@ SIZE = 9 EMPTY = 0 + def deep_copy(board): + """Return a deep copy of the Sudoku board.""" return copy.deepcopy(board) + def create_empty_board(): + """Create an empty 9x9 Sudoku board.""" return [[EMPTY for _ in range(SIZE)] for _ in range(SIZE)] + def is_safe(board, row, col, num): - # Check row and column + """Check whether a number can be placed safely.""" + for x in range(SIZE): - if board[row][x] == num or board[x][col] == num: + if board[row][x] == num: return False - # Check 3x3 box + if board[x][col] == num: + return False + start_row = row - row % 3 start_col = col - col % 3 + for i in range(3): for j in range(3): if board[start_row + i][start_col + j] == num: return False + return True + def fill_board(board): + """Generate a complete solved Sudoku using recursive backtracking.""" + for row in range(SIZE): for col in range(SIZE): + if board[row][col] == EMPTY: - possible = list(range(1, SIZE + 1)) - random.shuffle(possible) - for candidate in possible: - if is_safe(board, row, col, candidate): - board[row][col] = candidate + + nums = list(range(1, 10)) + random.shuffle(nums) + + for num in nums: + + if is_safe(board, row, col, num): + + board[row][col] = num + if fill_board(board): return True + board[row][col] = EMPTY + return False + return True + +def find_empty(board): + """Return the next empty cell.""" + + for i in range(SIZE): + for j in range(SIZE): + if board[i][j] == EMPTY: + return i, j + + return None + + +def count_solutions(board): + """ + Count Sudoku solutions. + + Stops searching once more than one solution is found. + """ + + solutions = 0 + + def solve(): + + nonlocal solutions + + if solutions > 1: + return + + empty = find_empty(board) + + if not empty: + solutions += 1 + return + + row, col = empty + + for num in range(1, 10): + + if is_safe(board, row, col, num): + + board[row][col] = num + + solve() + + board[row][col] = EMPTY + + solve() + + return solutions + + def remove_cells(board, clues): - 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 + """ + Remove cells while ensuring the puzzle still + has exactly one solution. + """ + + cells = [(r, c) for r in range(SIZE) for c in range(SIZE)] + random.shuffle(cells) + + cells_to_remove = SIZE * SIZE - clues + + for row, col in cells: + + if cells_to_remove == 0: + break + + backup = board[row][col] + + board[row][col] = EMPTY + + test = deep_copy(board) + + if count_solutions(test) != 1: + board[row][col] = backup + else: + cells_to_remove -= 1 + def generate_puzzle(clues=35): + """ + Generate a Sudoku puzzle with one unique 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 e42ad04d..6aa9e9a1 100644 --- a/starter/templates/index.html +++ b/starter/templates/index.html @@ -1,18 +1,96 @@ - - + + - - Sudoku Game - + + + Sudoku Game + + -

Sudoku Game

-
-
- - - -
- +
+ + +
+
+ +

Sudoku Game

+ +
+ +
+ + + +
+ +
+ + 00:00 +
+ +
+ +
+ +
+ +
+ +
+ + + + + + + +
+ +
+ +
+ +

+ 🏆 Top 10 Fastest Times +

+ + + + + + + + + + + + + + + + + + +
RankNameDifficultyTime
+ +
+ +
+ + + \ No newline at end of file