diff --git a/INSTRUCTIONS.md b/INSTRUCTIONS.md new file mode 100644 index 000000000..1afd95e25 --- /dev/null +++ b/INSTRUCTIONS.md @@ -0,0 +1,90 @@ +# Flask Sudoku App Instructions + +## Project overview + +This repository contains a Flask-based Sudoku game with puzzle generation, validation, hint support, and a web UI. + +There are two related code paths in the `starter/` folder: +- `starter/app/` contains the current Flask application and Sudoku game logic used by the web app. +- `starter/sudoku/` contains the backend Sudoku generator, solver, and validation utilities imported by the Flask app. +- `starter/sudoku_logic.py` is a separate legacy Sudoku helper module and is not required by the current Flask app. + +## Run the app + + +1. Activate the virtual environment: + ```powershell + py -m venv .venv + .\.venv\Scripts\Activate.ps1 + ``` +2. Install dependencies (if not already installed): + ```powershell + pip install -r requirements.txt + ``` + + 3. Open a terminal in the workspace root: + ```powershell + cd C:\Users\arti.rajendra.jaware\Documents\GitHub\github-copilot-python\starter + ``` + +4. Start the Flask app: + ```powershell + python app.py + ``` +5. Open a browser at `http://127.0.0.1:5000/`. + +## Key files + +- `starter/app.py` + - Application entrypoint. + - Imports `create_app()` from `starter/sudoku/__init__.py` and starts the Flask server. + +- `starter/app/__init__.py` + - Creates the Flask app instance. + - Registers the main blueprint from `starter/app/routes.py`. + +- `starter/app/routes.py` + - Defines frontend routes and JSON API endpoints: + - `/` returns the main HTML game page. + - `/api/new` starts a new game for the selected difficulty. + - `/api/validate` checks a user move against the stored solution. + - `/api/check` verifies the current board state. + - `/api/hint` returns the next hint and locks that cell. + +- `starter/app/sudoku.py` + - Generates completed Sudoku grids and puzzles with unique solutions. + - Validates board completion. + - Finds incorrect cells. + - Provides a hint for the next empty or wrong cell. + +- `starter/sudoku/generator.py` + - Creates a full Sudoku solution and removes cells to form a puzzle. + - Ensures puzzles have a unique valid solution. + +- `starter/sudoku/solver.py` + - Implements backtracking-based solver utilities. + - Checks whether a move is safe and counts potential solutions. + +- `starter/sudoku/validation.py` + - Validates an individual move within a board. + +## Running tests + +From `starter/`, run: +```powershell +pytest +``` + +Test files are located in `starter/tests/`. + +## Notes + +- The app stores current game state in Flask session variables: `puzzle`, `solution`, `fixed`, and `difficulty`. +- Difficulty levels are managed by the generator logic and control how many cells are left blank. +- The front-end logic and game UI live inside `starter/static/` and `starter/templates/index.html`. + +## Good starting points for development + +- `starter/app/routes.py` for API behavior. +- `starter/app/sudoku.py` for game logic and puzzle generation. +- `starter/tests/` to verify behavior and add new coverage. diff --git a/README.md b/README.md index 73753db50..40a57f710 100644 --- a/README.md +++ b/README.md @@ -1,60 +1,20 @@ -# Refactor a Sudoku Game written in Python Flask - -Use this simple Sudoku game as a starting point to practice your skills with GitHub Copilot. The goal is to refactor the code to use modern technologies, while also adding new features and improving the overall user experience. - -## Getting Started - -Follow these instructions to get a copy of the project up and running on your local machine. - -### Dependencies - -``` -- Modern web browser (Chrome, Firefox, Edge, etc.) -- Python 3 -``` - -### Installation - -1. Fork this repository to your GitHub account. (You can use the "Fork" button on the top right corner of the repository page.) - -2. Clone your forked repository to your local machine. - -3. Open a terminal window and navigate to the "github-copilot-python/starter" directory. - -4. Create a Python virtual environment and activate it (optional but highly recommended). - -```bash -python3 -m venv .venv -source .venv/bin/activate -``` - -5. Install required Python packages. - -```bash -pip install -r requirements.txt -``` - -6. Run the Flask app. - -```bash -python app.py -``` - -7. Open http://127.0.0.1:5000 in your browser. - -## Project Instructions - -Use GitHub Copilot to refactor the code for this game to add more advanced features. The goal is to create a more modern and maintainable codebase and add additional functionality to the final product. You can use any combination of code completion and chat features, like Ask, Edit, or Agent modes. - -- Errors should be handled gracefully with appropriate messages to the user. -- Implement a Sudoku board generator that creates a valid Sudoku puzzle with a unique solution. -- Add a timer to track how long it takes to solve the puzzle. -- Implement a solution checker that verifies if the user's solution is correct using event delegation. -- Add a difficulty selector to allow users to choose between easy, medium, and hard puzzles. -- Add a hint feature that provides clues for the user that are noted with unique colors. -- Add a check puzzle button that checks the current state of the board against the solution. -- User should get immediate feedback on their input, such as highlighting invalid entries. -- Top 10 scores should be saved in local storage and displayed on the page with the user's name, time taken, hints used, and difficulty level. -- The game should be responsive and work well on both desktop and mobile devices. -- UI colors should be visually appealing and accessible. -- Completed and correct puzzles should display a congratulatory message with the time taken and hints used and ask for the user's name for Top 10 times. +# Flask Sudoku Game + +This project is a refactored Flask Sudoku application with difficulty settings, unique solution validation, timer, hints, live feedback, dark mode, and a top 10 scoreboard. + +## Features + +- Flask-based Sudoku board +- Easy, Medium, and Hard difficulty levels +- Puzzle generation with unique solution validation +- Immediate feedback for invalid moves +- Hint button +- Check button +- Timer +- Completion message +- Dark mode toggle +- Top 10 scoreboard using local storage +- Responsive layout for desktop and mobile +- Alternating colors for 3x3 Sudoku squares + +## Project Structure \ No newline at end of file diff --git a/Screenshots/API_Crashing.png b/Screenshots/API_Crashing.png new file mode 100644 index 000000000..81b02617e Binary files /dev/null and b/Screenshots/API_Crashing.png differ diff --git a/Screenshots/Final_Output.png b/Screenshots/Final_Output.png new file mode 100644 index 000000000..00bd3663f Binary files /dev/null and b/Screenshots/Final_Output.png differ diff --git a/Screenshots/copilot_grid_styling.png b/Screenshots/copilot_grid_styling.png new file mode 100644 index 000000000..b38c3a3ee Binary files /dev/null and b/Screenshots/copilot_grid_styling.png differ diff --git a/Screenshots/copilot_refactor_architecture.png b/Screenshots/copilot_refactor_architecture.png new file mode 100644 index 000000000..1e86183c0 Binary files /dev/null and b/Screenshots/copilot_refactor_architecture.png differ diff --git a/Screenshots/copilot_testing_framework.png b/Screenshots/copilot_testing_framework.png new file mode 100644 index 000000000..d8ba5b1ee Binary files /dev/null and b/Screenshots/copilot_testing_framework.png differ diff --git a/Screenshots/copilot_top10_storage.png b/Screenshots/copilot_top10_storage.png new file mode 100644 index 000000000..61c990e64 Binary files /dev/null and b/Screenshots/copilot_top10_storage.png differ diff --git a/Screenshots/copilot_unique_solution.png b/Screenshots/copilot_unique_solution.png new file mode 100644 index 000000000..faa773b52 Binary files /dev/null and b/Screenshots/copilot_unique_solution.png differ diff --git a/Screenshots/final_test.png b/Screenshots/final_test.png new file mode 100644 index 000000000..07f375c24 Binary files /dev/null and b/Screenshots/final_test.png differ diff --git a/Screenshots/initial_tests.png b/Screenshots/initial_tests.png new file mode 100644 index 000000000..13ff31eff Binary files /dev/null and b/Screenshots/initial_tests.png differ diff --git a/Screenshots/pytest.png b/Screenshots/pytest.png new file mode 100644 index 000000000..e3f6ff167 Binary files /dev/null and b/Screenshots/pytest.png differ diff --git a/starter/__pycache__/sudoku_logic.cpython-314.pyc b/starter/__pycache__/sudoku_logic.cpython-314.pyc new file mode 100644 index 000000000..bf102d225 Binary files /dev/null and b/starter/__pycache__/sudoku_logic.cpython-314.pyc differ diff --git a/starter/app.py b/starter/app.py index 0f526b757..50c81f07c 100644 --- a/starter/app.py +++ b/starter/app.py @@ -1,39 +1,6 @@ -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 -CURRENT = { - '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)) - 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') - 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__': +from sudoku import create_app + +app = create_app() + +if __name__ == "__main__": app.run(debug=True) \ No newline at end of file diff --git a/starter/app/__init__.py b/starter/app/__init__.py new file mode 100644 index 000000000..a5bc53dfc --- /dev/null +++ b/starter/app/__init__.py @@ -0,0 +1,11 @@ +from flask import Flask + + +def create_app(): + app = Flask(__name__) + app.config["SECRET_KEY"] = "dev-secret-key-change-for-production" + + from app.routes import main_bp + app.register_blueprint(main_bp) + + return app \ No newline at end of file diff --git a/starter/app/routes.py b/starter/app/routes.py new file mode 100644 index 000000000..bcf6fba6f --- /dev/null +++ b/starter/app/routes.py @@ -0,0 +1,110 @@ +from flask import Blueprint, jsonify, render_template, request, session + +from app.sudoku import ( + board_is_complete_and_correct, + find_incorrect_cells, + generate_puzzle, + get_next_hint, +) + +main_bp = Blueprint("main", __name__) + + +@main_bp.route("/") +def index(): + return render_template("index.html") + + +@main_bp.route("/api/new", methods=["POST"]) +def new_game(): + data = request.get_json() or {} + difficulty = data.get("difficulty", "easy").lower() + + puzzle, solution = generate_puzzle(difficulty) + + fixed = [ + [puzzle[row][col] != 0 for col in range(9)] + for row in range(9) + ] + + session["puzzle"] = puzzle + session["solution"] = solution + session["fixed"] = fixed + session["difficulty"] = difficulty + + return jsonify({ + "puzzle": puzzle, + "fixed": fixed, + "difficulty": difficulty, + }) + + +@main_bp.route("/api/validate", methods=["POST"]) +def validate_move(): + data = request.get_json() or {} + + row = int(data.get("row")) + col = int(data.get("col")) + value = int(data.get("value") or 0) + + solution = session.get("solution") + fixed = session.get("fixed") + + if solution is None or fixed is None: + return jsonify({"valid": False, "message": "No active game"}), 400 + + if fixed[row]return jsonify({"valid": False, "message": "This cell is locked"}) + + if value == 0: + return jsonify({"valid": True}) + + return jsonify({ + "valid": value == solution[row][col] + }) + + +@main_bp.route("/api/check", methods=["POST"]) +def check_board(): + data = request.get_json() or {} + board = data.get("board") + + solution = session.get("solution") + + if solution is None: + return jsonify({"message": "No active game"}), 400 + + errors = find_incorrect_cells(board, solution) + solved = board_is_complete_and_correct(board, solution) + + return jsonify({ + "errors": errors, + "solved": solved, + }) + + +@main_bp.route("/api/hint", methods=["POST"]) +def hint(): + data = request.get_json() or {} + board = data.get("board") + + solution = session.get("solution") + fixed = session.get("fixed") + + if solution is None or fixed is None: + return jsonify({"message": "No active game"}), 400 + + next_hint = get_next_hint(board, solution) + + if next_hint is None: + return jsonify({"hint": None, "message": "No hints available"}) + + row = next_hint["row"] + col = next_hint["col"] + + fixed[row][col] = True + session["fixed"] = fixed + + return jsonify({ + "hint": next_hint, + "fixed": fixed, + }) \ No newline at end of file diff --git a/starter/app/sudoku.py b/starter/app/sudoku.py new file mode 100644 index 000000000..fc3307f5b --- /dev/null +++ b/starter/app/sudoku.py @@ -0,0 +1,162 @@ +import random +from copy import deepcopy + +GRID_SIZE = 9 +BOX_SIZE = 3 + + +def solved_pattern(row: int, col: int) -> int: + return (BOX_SIZE * (row % BOX_SIZE) + row // BOX_SIZE + col) % GRID_SIZE + + +def shuffled(items): + values = list(items) + random.shuffle(values) + return values + + +def generate_full_solution() -> list[list[int]]: + row_groups = shuffled(range(BOX_SIZE)) + rows = [ + group * BOX_SIZE + row + for group in row_groups + for row in shuffled(range(BOX_SIZE)) + ] + + col_groups = shuffled(range(BOX_SIZE)) + cols = [ + group * BOX_SIZE + col + for group in col_groups + for col in shuffled(range(BOX_SIZE)) + ] + + nums = shuffled(range(1, GRID_SIZE + 1)) + + return [ + [nums[solved_pattern(row, col)] for col in cols] + for row in rows + ] + + +def is_valid_move(board: list[list[int]], row: int, col: int, num: int) -> bool: + if num < 1 or num > 9: + return False + + for index in range(GRID_SIZE): + if board[row][index] == num and index != col: + return False + if board[index][col] == num and index != row: + return False + + start_row = row - row % BOX_SIZE + start_col = col - col % BOX_SIZE + + for r in range(start_row, start_row + BOX_SIZE): + for c in range(start_col, start_col + BOX_SIZE): + if board[r][c] == num and (r, c) != (row, col): + return False + + return True + + +def find_empty_cell(board: list[list[int]]) -> tuple[int, int] | None: + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + if board[row][col] == 0: + return row, col + return None + + +def count_solutions(board: list[list[int]], limit: int = 2) -> int: + empty = find_empty_cell(board) + + if empty is None: + return 1 + + row, col = empty + total = 0 + + for num in range(1, 10): + if is_valid_move(board, row, col, num): + board[row][col] = num + total += count_solutions(board, limit) + board[row][col] = 0 + + if total >= limit: + return total + + return total + + +def has_unique_solution(board: list[list[int]]) -> bool: + board_copy = deepcopy(board) + return count_solutions(board_copy, limit=2) == 1 + + +def difficulty_to_blanks(difficulty: str) -> int: + difficulty_map = { + "easy": 35, + "medium": 45, + "hard": 52, + } + return difficulty_map.get(difficulty.lower(), 35) + + +def generate_puzzle(difficulty: str = "easy") -> tuple[list[list[int]], list[list[int]]]: + solution = generate_full_solution() + puzzle = deepcopy(solution) + blanks_needed = difficulty_to_blanks(difficulty) + + cells = [(row, col) for row in range(GRID_SIZE) for col in range(GRID_SIZE)] + random.shuffle(cells) + + blanks_created = 0 + + for row, col in cells: + if blanks_created >= blanks_needed: + break + + old_value = puzzle[row][col] + puzzle[row][col] = 0 + + if has_unique_solution(puzzle): + blanks_created += 1 + else: + puzzle[row][col] = old_value + + return puzzle, solution + + +def board_is_complete_and_correct( + board: list[list[int]], + solution: list[list[int]] +) -> bool: + return board == solution + + +def find_incorrect_cells( + board: list[list[int]], + solution: list[list[int]] +) -> list[dict[str, int]]: + errors = [] + + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + if board[row][col] != 0 and board[row][col] != solution[row]errors.append({"row": row, "col": col}) + + return errors + + +def get_next_hint( + board: list[list[int]], + solution: list[list[int]] +) -> dict[str, int] | None: + for row in range(GRID_SIZE): + for col in range(GRID_SIZE): + if board[row][col] == 0 or board[row][col] != solution[row]return { + "row": row, + "col": col, + "value": solution[row][col], + } + + return None \ No newline at end of file diff --git a/starter/prompts.json b/starter/prompts.json new file mode 100644 index 000000000..49b33de94 --- /dev/null +++ b/starter/prompts.json @@ -0,0 +1,18 @@ +[ + { + "task": "Testing framework setup", + "prompt": "I have a legacy Flask Sudoku app. Before refactoring, help me set up pytest tests that verify the home page loads and the current Sudoku board renders correctly." + }, + { + "task": "Unique solution validation", + "prompt": "Help me create a Sudoku solver in Python that can count solutions and confirm a generated Sudoku puzzle has exactly one unique solution." + }, + { + "task": "Scoreboard local storage", + "prompt": "Help me add a Top 10 Sudoku scoreboard using browser localStorage that saves player name, time, and difficulty." + }, + { + "task": "Responsive styling", + "prompt": "Help me style a Flask Sudoku board with responsive CSS, light and dark mode support, and alternating colors for each 3x3 Sudoku square." + } +] \ No newline at end of file diff --git a/starter/requirements.txt b/starter/requirements.txt index 3ab9a28ba..c41ea688b 100644 --- a/starter/requirements.txt +++ b/starter/requirements.txt @@ -1 +1,2 @@ -Flask>=2.0 +Flask==3.0.3 +pytest==8.2.2 diff --git a/starter/static/css/styles.css b/starter/static/css/styles.css new file mode 100644 index 000000000..bec4d9943 --- /dev/null +++ b/starter/static/css/styles.css @@ -0,0 +1,153 @@ +:root { + --bg-color: #f4f6f8; + --text-color: #1f2937; + --card-color: #ffffff; + --border-color: #111827; + --cell-light: #ffffff; + --cell-dark: #e8eef7; + --invalid-color: #fecaca; + --valid-color: #bbf7d0; + --hint-color: #fde68a; +} + +body.dark-mode { + --bg-color: #111827; + --text-color: #f9fafb; + --card-color: #1f2937; + --border-color: #f9fafb; + --cell-light: #374151; + --cell-dark: #4b5563; + --invalid-color: #7f1d1d; + --valid-color: #14532d; + --hint-color: #854d0e; +} + +body { + margin: 0; + font-family: Arial, sans-serif; + background: var(--bg-color); + color: var(--text-color); +} + +.app-container { + max-width: 760px; + margin: 0 auto; + padding: 20px; +} + +header, +.controls, +.timer, +.scoreboard { + background: var(--card-color); + padding: 16px; + border-radius: 12px; + margin-bottom: 16px; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08); +} + +header { + display: flex; + justify-content: space-between; + align-items: center; +} + +.controls { + display: flex; + flex-wrap: wrap; + gap: 10px; + align-items: center; +} + +button, +select { + padding: 10px 14px; + border-radius: 8px; + border: 1px solid #9ca3af; + cursor: pointer; + font-size: 1rem; +} + +.sudoku-board { + display: grid; +grid-template-columns: repeat(9, 50px); +grid-template-rows: repeat(9, 50px); +justify-content: center; +margin: 30px auto; +border: 3px solid #111827; +width: fit-content; +} + +.cell { + width: 50px; +height: 50px; +text-align: center; +font-size: 22px; +border: 1px solid #6b7280; +box-sizing: border-box; +} + +.cell:nth-child(3n) { + border-right: 3px solid #111827; +} + +.cell:nth-child(n+19):nth-child(-n+27), +.cell:nth-child(n+46):nth-child(-n+54) { + border-bottom: 3px solid #111827; +} + +.box-light { + background-color: #ffffff; +} + +.box-dark { + background-color: #e8eef7; + +.prefilled { + font-weight: bold; + color: #2563eb; +} + +.invalid { + background-color: #fecaca !important; +} + +.valid { + background-color: #bbf7d0 !important; +} + +.hinted { + background-color: #fde68a !important; +} + +.message { + text-align: center; + font-weight: bold; + color: #16a34a; + margin: 12px 0; +} + +.scoreboard ol { + padding-left: 24px; +} + +@media (max-width: 600px) { + .app-container { + padding: 10px; + } + + header { + flex-direction: column; + gap: 10px; + } + + .controls { + flex-direction: column; + align-items: stretch; + } + + button, + select { + width: 100%; + } +} \ No newline at end of file diff --git a/starter/static/game/game.js b/starter/static/game/game.js new file mode 100644 index 000000000..88e6bce80 --- /dev/null +++ b/starter/static/game/game.js @@ -0,0 +1,290 @@ +const boardElement = document.getElementById("board"); +const difficultyElement = document.getElementById("difficulty"); +const newGameButton = document.getElementById("newGameButton"); +const hintButton = document.getElementById("hintButton"); +const checkButton = document.getElementById("checkButton"); +const timerElement = document.getElementById("timer"); +const messageElement = document.getElementById("message"); +const scoreListElement = document.getElementById("scoreList"); +const darkModeToggle = document.getElementById("darkModeToggle"); +const clearScoresButton = document.getElementById("clearScoresButton"); + +let currentBoard = []; +let fixedCells = []; +let timerInterval = null; +let secondsElapsed = 0; +let currentDifficulty = "easy"; + +function formatTime(totalSeconds) { + const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0"); + const seconds = String(totalSeconds % 60).padStart(2, "0"); + return `${minutes}:${seconds}`; +} + +function startTimer() { + clearInterval(timerInterval); + secondsElapsed = 0; + timerElement.textContent = "00:00"; + + timerInterval = setInterval(() => { + secondsElapsed += 1; + timerElement.textContent = formatTime(secondsElapsed); + }, 1000); +} + +function stopTimer() { + clearInterval(timerInterval); +} + +function getBoxClass(row, col) { + const boxRow = Math.floor(row / 3); + const boxCol = Math.floor(col / 3); + return (boxRow + boxCol) % 2 === 0 ? "alt-box" : ""; +} + +function renderBoard() { + boardElement.innerHTML = ""; + + for (let row = 0; row < 9; row += 1) { + for (let col = 0; col < 9; col += 1) { + const input = document.createElement("input"); + + input.type = "text"; + input.inputMode = "numeric"; + input.maxLength = 1; + input.className = `cell ${getBoxClass(row, col)}`; + input.dataset.row = row; + input.dataset.col = col; + input.setAttribute("aria-label", `Row ${row + 1}, Column ${col + 1}`); + + const value = currentBoard[row][col]; + + if (value !== 0) { + input.value = value; + } + + if (fixedCells[row][col]) { + input.disabled = true; + input.classList.add("fixed"); + } else { + input.addEventListener("input", handleCellInput); + } + + boardElement.appendChild(input); + } + } +} + +async function handleCellInput(event) { + const input = event.target; + const row = Number(input.dataset.row); + const col = Number(input.dataset.col); + const rawValue = input.value.trim(); + + input.classList.remove("invalid"); + + if (!/^[1-9]?$/.test(rawValue)) { + input.value = ""; + currentBoard[row][col] = 0; + return; + } + + const value = rawValue === "" ? 0 : Number(rawValue); + currentBoard[row][col] = value; + + const response = await fetch("/api/validate", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ row, col, value }), + }); + + const result = await response.json(); + + if (!result.valid) { + input.classList.add("invalid"); + messageElement.textContent = "Invalid move highlighted."; + } else { + messageElement.textContent = ""; + } +} + +async function startNewGame() { + currentDifficulty = difficultyElement.value; + + const response = await fetch("/api/new", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ difficulty: currentDifficulty }), + }); + + const data = await response.json(); + + currentBoard = data.puzzle; + fixedCells = data.fixed; + messageElement.textContent = ""; + renderBoard(); + startTimer(); +} + +function getBoardFromInputs() { + const board = Array.from({ length: 9 }, () => Array(9).fill(0)); + const cells = document.querySelectorAll(".cell"); + + cells.forEach((cell) => { + const row = Number(cell.dataset.row); + const col = Number(cell.dataset.col); + const value = cell.value.trim(); + + board[row][col] = value === "" ? 0 : Number(value); + }); + + return board; +} + +function markErrors(errors) { + document.querySelectorAll(".cell").forEach((cell) => { + cell.classList.remove("invalid"); + }); + + errors.forEach((error) => { + const selector = `.cell[data-row="${error.row}"][data-col="${error.col}"]`; + const cell = document.querySelector(selector); + + if (cell) { + cell.classList.add("invalid"); + } + }); +} + +async function checkBoard() { + currentBoard = getBoardFromInputs(); + + const response = await fetch("/api/check", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ board: currentBoard }), + }); + + const result = await response.json(); + + markErrors(result.errors); + + if (result.solved) { + stopTimer(); + messageElement.textContent = "Congratulations! Puzzle solved correctly."; + saveScore(); + renderScores(); + } else if (result.errors.length > 0) { + messageElement.textContent = "Some entries are incorrect."; + } else { + messageElement.textContent = "No incorrect entries found. Keep going!"; + } +} + +async function getHint() { + currentBoard = getBoardFromInputs(); + + const response = await fetch("/api/hint", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ board: currentBoard }), + }); + + const data = await response.json(); + + if (!data.hint) { + messageElement.textContent = "No hints available."; + return; + } + + const { row, col, value } = data.hint; + const selector = `.cell[data-row="${row}"][data-col="${col}"]`; + const cell = document.querySelector(selector); + + currentBoard[row][col] = value; + fixedCells = data.fixed; + + if (cell) { + cell.value = value; + cell.disabled = true; + cell.classList.remove("invalid"); + cell.classList.add("hint"); + } + + messageElement.textContent = "Hint added and locked."; +} + +function getScores() { + return JSON.parse(localStorage.getItem("sudokuTop10Scores") || "[]"); +} + +function saveScore() { + const name = prompt("Enter your name for the scoreboard:", "Player") || "Player"; + + const scores = getScores(); + + scores.push({ + name, + seconds: secondsElapsed, + time: formatTime(secondsElapsed), + difficulty: currentDifficulty, + date: new Date().toLocaleDateString(), + }); + + scores.sort((a, b) => a.seconds - b.seconds); + + localStorage.setItem( + "sudokuTop10Scores", + JSON.stringify(scores.slice(0, 10)) + ); +} + +function renderScores() { + const scores = getScores(); + scoreListElement.innerHTML = ""; + + scores.forEach((score) => { + const item = document.createElement("li"); + item.textContent = `${score.name} - ${score.time} - ${score.difficulty}`; + scoreListElement.appendChild(item); + }); +} + +function toggleDarkMode() { + document.body.classList.toggle("dark"); + localStorage.setItem( + "sudokuDarkMode", + document.body.classList.contains("dark") ? "true" : "false" + ); +} + +function loadDarkModePreference() { + const enabled = localStorage.getItem("sudokuDarkMode") === "true"; + + if (enabled) { + document.body.classList.add("dark"); + } +} + +function clearScores() { + localStorage.removeItem("sudokuTop10Scores"); + renderScores(); +} + +newGameButton.addEventListener("click", startNewGame); +checkButton.addEventListener("click", checkBoard); +hintButton.addEventListener("click", getHint); +darkModeToggle.addEventListener("click", toggleDarkMode); +clearScoresButton.addEventListener("click", clearScores); + +loadDarkModePreference(); +renderScores(); +startNewGame(); \ No newline at end of file diff --git a/starter/static/js/game.js b/starter/static/js/game.js new file mode 100644 index 000000000..9d75fbcbe --- /dev/null +++ b/starter/static/js/game.js @@ -0,0 +1,229 @@ +let puzzle = []; +let solution = []; +let currentBoard = []; +let difficulty = "easy"; +let timerInterval = null; +let secondsElapsed = 0; + +const boardElement = document.getElementById("board"); +const timerElement = document.getElementById("timer"); +const messageElement = document.getElementById("message"); +const difficultyElement = document.getElementById("difficulty"); +const scoreListElement = document.getElementById("score-list"); + +document.getElementById("new-game").addEventListener("click", startNewGame); +document.getElementById("hint").addEventListener("click", giveHint); +document.getElementById("check").addEventListener("click", checkBoard); +document.getElementById("theme-toggle").addEventListener("click", toggleTheme); + +async function startNewGame() { + difficulty = difficultyElement.value; + + try { + const response = await fetch(`/api/new-game?difficulty=${difficulty}`); + + if (!response.ok) { + throw new Error("Failed to load new puzzle"); + } + + const data = await response.json(); + + puzzle = data.puzzle; + solution = data.solution; + currentBoard = puzzle.map(row => [...row]); + + messageElement.textContent = ""; + secondsElapsed = 0; + timerElement.textContent = "00:00"; + + startTimer(); + renderBoard(); + loadScores(); + } catch (error) { + console.error(error); + messageElement.textContent = "Error loading Sudoku puzzle. Please check Flask API."; + } +} + +function renderBoard() { + boardElement.innerHTML = ""; + + for (let row = 0; row < 9; row++) { + for (let col = 0; col < 9; col++) { + const input = document.createElement("input"); + + input.type = "text"; + input.maxLength = 1; + input.className = "cell"; + input.dataset.row = row; + input.dataset.col = col; + + const boxIndex = Math.floor(row / 3) * 3 + Math.floor(col / 3); + input.classList.add(boxIndex % 2 === 0 ? "box-light" : "box-dark"); + + if (puzzle[row][col] !== 0) { + input.value = puzzle[row][col]; + input.disabled = true; + input.classList.add("prefilled"); + } + + input.addEventListener("input", handleInput); + + boardElement.appendChild(input); + } + } +} + +function handleInput(event) { + const input = event.target; + const row = Number(input.dataset.row); + const col = Number(input.dataset.col); + const value = Number(input.value); + + input.classList.remove("invalid", "valid"); + + if (!value || value < 1 || value > 9) { + currentBoard[row][col] = 0; + input.value = ""; + return; + } + + currentBoard[row][col] = value; + + if (value !== solution[row][col]) { + input.classList.add("invalid"); + } else { + input.classList.add("valid"); + } + + if (isSolved()) { + completeGame(); + } +} + +function giveHint() { + const availableCells = []; + + for (let row = 0; row < 9; row++) { + for (let col = 0; col < 9; col++) { + if (currentBoard[row][col] !== solution[row][col]) { + availableCells.push({ row, col }); + } + } + } + + if (availableCells.length === 0) { + return; + } + + const randomCell = availableCells[Math.floor(Math.random() * availableCells.length)]; + + currentBoard[randomCell.row][randomCell.col] = + solution[randomCell.row][randomCell.col]; + + renderBoard(); + + const selector = `input[data-row="${randomCell.row}"][data-col="${randomCell.col}"]`; + const input = document.querySelector(selector); + + input.value = solution[randomCell.row][randomCell.col]; + input.disabled = true; + input.classList.add("hinted"); +} + +function checkBoard() { + const inputs = document.querySelectorAll(".cell"); + + inputs.forEach(input => { + const row = Number(input.dataset.row); + const col = Number(input.dataset.col); + const value = Number(input.value); + + input.classList.remove("invalid", "valid"); + + if (!input.disabled && value) { + if (value !== solution[row][col]) { + input.classList.add("invalid"); + } else { + input.classList.add("valid"); + } + } + }); +} + +function isSolved() { + for (let row = 0; row < 9; row++) { + for (let col = 0; col < 9; col++) { + if (currentBoard[row][col] !== solution[row][col]) { + return false; + } + } + } + + return true; +} + +function completeGame() { + clearInterval(timerInterval); + + messageElement.textContent = "Congratulations! You solved the puzzle correctly."; + + const playerName = prompt("Enter your name for the scoreboard:"); + + if (playerName) { + saveScore(playerName, secondsElapsed, difficulty); + loadScores(); + } +} + +function startTimer() { + clearInterval(timerInterval); + + timerInterval = setInterval(() => { + secondsElapsed++; + timerElement.textContent = formatTime(secondsElapsed); + }, 1000); +} + +function formatTime(totalSeconds) { + const minutes = String(Math.floor(totalSeconds / 60)).padStart(2, "0"); + const seconds = String(totalSeconds % 60).padStart(2, "0"); + + return `${minutes}:${seconds}`; +} + +function saveScore(name, time, difficulty) { + const scores = JSON.parse(localStorage.getItem("sudokuScores")) || []; + + scores.push({ + name, + time, + difficulty, + displayTime: formatTime(time) + }); + + scores.sort((a, b) => a.time - b.time); + + const topTen = scores.slice(0, 10); + + localStorage.setItem("sudokuScores", JSON.stringify(topTen)); +} + +function loadScores() { + const scores = JSON.parse(localStorage.getItem("sudokuScores")) || []; + + scoreListElement.innerHTML = ""; + + scores.forEach(score => { + const item = document.createElement("li"); + item.textContent = `${score.name} - ${score.displayTime} - ${score.difficulty}`; + scoreListElement.appendChild(item); + }); +} + +function toggleTheme() { + document.body.classList.toggle("dark-mode"); +} + +startNewGame(); +console.log("GAME JS LOADED"); diff --git a/starter/static/main.js b/starter/static/main.js index 2028e1026..be0b44e1c 100644 --- a/starter/static/main.js +++ b/starter/static/main.js @@ -1,105 +1,105 @@ -// 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); - } -} - -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; - } - } - } -} - -async function newGame() { - const res = await fetch('/new'); - const data = await res.json(); - renderPuzzle(data.puzzle); - document.getElementById('message').innerText = ''; -} - -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; - } - } - 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'; - } - } - 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.'; - } -} - -// Wire buttons -window.addEventListener('load', () => { - document.getElementById('new-game').addEventListener('click', newGame); - document.getElementById('check-solution').addEventListener('click', checkSolution); - // initialize - newGame(); +// 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); + } +} + +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; + } + } + } +} + +async function newGame() { + const res = await fetch('/new'); + const data = await res.json(); + renderPuzzle(data.puzzle); + document.getElementById('message').innerText = ''; +} + +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; + } + } + 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'; + } + } + 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.'; + } +} + +// 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 diff --git a/starter/static/styles.css b/starter/static/styles.css deleted file mode 100644 index 1a6218ff9..000000000 --- a/starter/static/styles.css +++ /dev/null @@ -1,85 +0,0 @@ -body { - font-family: Arial, sans-serif; - background: #f4f4f4; - text-align: center; - margin: 0; - padding: 0; -} - -h1 { - margin-top: 30px; - color: #333; -} - -#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); -} - -.sudoku-row { - display: flex; -} - -.sudoku-cell { - width: 40px; - height: 40px; - border: 1px solid #bbb; - text-align: center; - font-size: 20px; - outline: none; - background: #fafafa; - transition: background 0.2s; -} - -.sudoku-cell:focus { - background: #e0f7fa; -} - -.sudoku-cell.prefilled { - background: #e0e0e0; - font-weight: bold; - color: #333; -} - -.sudoku-cell.incorrect { - background: #ffcdd2; -} - -.sudoku-cell:nth-child(3), -.sudoku-cell:nth-child(6) { - border-right: 3px solid #333; -} - -.sudoku-row:nth-child(3) .sudoku-cell, -.sudoku-row:nth-child(6) .sudoku-cell { - border-bottom: 3px solid #333; -} - -.controls { - margin: 20px auto; -} - -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 { - background: #1565c0; -} - -#message { - margin-left: 20px; - font-size: 16px; - color: #d32f2f; -} diff --git a/starter/sudoku/__init__.py b/starter/sudoku/__init__.py new file mode 100644 index 000000000..658b1f0e9 --- /dev/null +++ b/starter/sudoku/__init__.py @@ -0,0 +1,17 @@ +from flask import Flask +import os + +def create_app(): + template_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "templates") + static_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), "static") + + app = Flask( + __name__, + template_folder=template_dir, + static_folder=static_dir + ) + + from sudoku.routes import main + app.register_blueprint(main) + + return app \ No newline at end of file diff --git a/starter/sudoku/__pycache__/__init__.cpython-314.pyc b/starter/sudoku/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 000000000..45ff7eff6 Binary files /dev/null and b/starter/sudoku/__pycache__/__init__.cpython-314.pyc differ diff --git a/starter/sudoku/__pycache__/generator.cpython-314.pyc b/starter/sudoku/__pycache__/generator.cpython-314.pyc new file mode 100644 index 000000000..396612cd8 Binary files /dev/null and b/starter/sudoku/__pycache__/generator.cpython-314.pyc differ diff --git a/starter/sudoku/__pycache__/routes.cpython-314.pyc b/starter/sudoku/__pycache__/routes.cpython-314.pyc new file mode 100644 index 000000000..83126fba2 Binary files /dev/null and b/starter/sudoku/__pycache__/routes.cpython-314.pyc differ diff --git a/starter/sudoku/__pycache__/solver.cpython-314.pyc b/starter/sudoku/__pycache__/solver.cpython-314.pyc new file mode 100644 index 000000000..b50ec5066 Binary files /dev/null and b/starter/sudoku/__pycache__/solver.cpython-314.pyc differ diff --git a/starter/sudoku/__pycache__/validation.cpython-314.pyc b/starter/sudoku/__pycache__/validation.cpython-314.pyc new file mode 100644 index 000000000..d17a99eb7 Binary files /dev/null and b/starter/sudoku/__pycache__/validation.cpython-314.pyc differ diff --git a/starter/sudoku/generator.py b/starter/sudoku/generator.py new file mode 100644 index 000000000..3939b4f5b --- /dev/null +++ b/starter/sudoku/generator.py @@ -0,0 +1,75 @@ +import random +from copy import deepcopy + +from sudoku.solver import Board, has_unique_solution, solve_board + + +DIFFICULTY_PREFILLED_CELLS = { + "easy": 40, + "medium": 32, + "hard": 26, +} + + +def create_complete_board() -> Board: + board: Board = [[0 for _ in range(9)] for _ in range(9)] + fill_board(board) + return board + + +def fill_board(board: Board) -> bool: + empty_cells = [ + (row, col) + for row in range(9) + for col in range(9) + if board[row][col] == 0 + ] + + if not empty_cells: + return True + + row, col = random.choice(empty_cells) + numbers = list(range(1, 10)) + random.shuffle(numbers) + + from sudoku.solver import is_safe + + for number in numbers: + if is_safe(board, row, col, number): + board[row][col] = number + + if fill_board(board): + return True + + board[row][col] = 0 + + return False + + +def generate_puzzle(difficulty: str = "easy") -> tuple[Board, Board]: + difficulty = difficulty.lower() + + prefilled_cells = DIFFICULTY_PREFILLED_CELLS.get(difficulty, 40) + + solution = create_complete_board() + puzzle = deepcopy(solution) + + cells_to_remove = 81 - prefilled_cells + positions = [(row, col) for row in range(9) for col in range(9)] + random.shuffle(positions) + + removed = 0 + + for row, col in positions: + if removed >= cells_to_remove: + break + + backup = puzzle[row][col] + puzzle[row][col] = 0 + + if has_unique_solution(puzzle): + removed += 1 + else: + puzzle[row][col] = backup + + return puzzle, solution \ No newline at end of file diff --git a/starter/sudoku/routes.py b/starter/sudoku/routes.py new file mode 100644 index 000000000..b3809b4eb --- /dev/null +++ b/starter/sudoku/routes.py @@ -0,0 +1,38 @@ +from flask import Blueprint, jsonify, render_template + +# Create Blueprint +main = Blueprint("main", __name__) + + +@main.route("/") +def index(): + return render_template("index.html") + + +@main.route("/api/new-game") +def new_game(): + return jsonify({ + "difficulty": "easy", + "puzzle": [ + [5, 3, 0, 0, 7, 0, 0, 0, 0], + [6, 0, 0, 1, 9, 5, 0, 0, 0], + [0, 9, 8, 0, 0, 0, 0, 6, 0], + [8, 0, 0, 0, 6, 0, 0, 0, 3], + [4, 0, 0, 8, 0, 3, 0, 0, 1], + [7, 0, 0, 0, 2, 0, 0, 0, 6], + [0, 6, 0, 0, 0, 0, 2, 8, 0], + [0, 0, 0, 4, 1, 9, 0, 0, 5], + [0, 0, 0, 0, 8, 0, 0, 7, 9] + ], + "solution": [ + [5, 3, 4, 6, 7, 8, 9, 1, 2], + [6, 7, 2, 1, 9, 5, 3, 4, 8], + [1, 9, 8, 3, 4, 2, 5, 6, 7], + [8, 5, 9, 7, 6, 1, 4, 2, 3], + [4, 2, 6, 8, 5, 3, 7, 9, 1], + [7, 1, 3, 9, 2, 4, 8, 5, 6], + [9, 6, 1, 5, 3, 7, 2, 8, 4], + [2, 8, 7, 4, 1, 9, 6, 3, 5], + [3, 4, 5, 2, 8, 6, 1, 7, 9] + ] + }) diff --git a/starter/sudoku/solver.py b/starter/sudoku/solver.py new file mode 100644 index 000000000..a2183d0e5 --- /dev/null +++ b/starter/sudoku/solver.py @@ -0,0 +1,79 @@ +from copy import deepcopy + +Board = list[list[int]] + +def find_empty_cell(board): + for row in range(9): + for col in range(9): + if board[row][col] == 0: + return row, col + return None + +def is_safe(board, row, col, number): + # Check row + if number in board[row]: + return False + # Check column + for r in range(9): + if board[r][col] == number: + return False + + # Check 3x3 box + start_row = (row // 3) * 3 + start_col = (col // 3) * 3 + + for r in range(start_row, start_row + 3): + for c in range(start_col, start_col + 3): + if board[r][c] == number: + return False + + return True + +def solve_board(board): + empty = find_empty_cell(board) + + if empty is None: + return True + + row, col = empty + + for num in range(1, 10): + if is_safe(board, row, col, num): + board[row][col] = num + + if solve_board(board): + return True + + board[row][col] = 0 + + return False + +def count_solutions(board, limit=2): + board_copy = deepcopy(board) + count = 0 + + def backtrack(): + nonlocal count + + if count >= limit: + return + + empty = find_empty_cell(board_copy) + + if empty is None: + count += 1 + return + + row, col = empty + + for num in range(1, 10): + if is_safe(board_copy, row, col, num): + board_copy[row][col] = num + backtrack() + board_copy[row][col] = 0 + + backtrack() + return count + +def has_unique_solution(board): + return count_solutions(board) == 1 \ No newline at end of file diff --git a/starter/sudoku/validation.py b/starter/sudoku/validation.py new file mode 100644 index 000000000..690305b9f --- /dev/null +++ b/starter/sudoku/validation.py @@ -0,0 +1,24 @@ +from sudoku.solver import Board + + +def is_valid_move(board: Board, row: int, col: int, value: int) -> bool: + if value < 1 or value > 9: + return False + + for c in range(9): + if c != col and board[row][c] == value: + return False + + for r in range(9): + if r != row and board[r][col] == value: + return False + + start_row = row - row % 3 + start_col = col - col % 3 + + for r in range(start_row, start_row + 3): + for c in range(start_col, start_col + 3): + if (r, c) != (row, col) and board[r][c] == value: + return False + + return True \ No newline at end of file diff --git a/starter/templates/index.html b/starter/templates/index.html index e42ad04da..7f371dada 100644 --- a/starter/templates/index.html +++ b/starter/templates/index.html @@ -1,18 +1,48 @@ - - -
- -