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=` + +
| Rank | +Name | +Difficulty | +Time | +
|---|