From 4aa189ad7d510e124e2cdbe15083b44f76849f81 Mon Sep 17 00:00:00 2001 From: AnushaGBhat Date: Sun, 16 Aug 2026 12:02:22 +0530 Subject: [PATCH 1/6] Add Copilot project instructions --- instruction.md | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 instruction.md diff --git a/instruction.md b/instruction.md new file mode 100644 index 000000000..42920a558 --- /dev/null +++ b/instruction.md @@ -0,0 +1,75 @@ +# Project Instructions + +## Project Overview + +This project is a Python Flask Sudoku game being refactored from legacy code into a maintainable, responsive, and user-friendly application. + +## Code Quality + +- Follow clean and readable Python coding practices. +- Follow PEP 8 conventions. +- Use meaningful names for variables, functions, and classes. +- Prefer small, focused, reusable functions. +- Avoid unnecessary code duplication. +- Keep Sudoku game logic separate from Flask route handling where practical. +- Preserve existing working functionality unless a requirement requires changing it. +- Add comments and docstrings where they improve understanding. +- Use consistent error handling. + +## Sudoku Requirements + +The completed application must: + +- Generate valid Sudoku puzzles. +- Ensure every generated puzzle has exactly one unique solution. +- Support Easy, Medium, and Hard difficulty levels. +- Adjust the number of prefilled cells according to difficulty. +- Keep prefilled cells locked. +- Provide immediate visual feedback for invalid moves. +- Display a completion message when the puzzle is solved correctly. + +## Game Features + +The application must include: + +- A Hint button that fills one correct empty cell and locks it. +- A Check button that highlights incorrect entries. +- A timer that tracks the player's solving time. +- A Top 10 scoreboard containing player name, time, difficulty, and number of hints. +- Browser localStorage so Top 10 scores persist between sessions. +- A dark mode toggle. + +## UI and Styling + +- Keep the interface clean and consistent. +- Support both light and dark modes. +- Make the layout responsive for desktop and mobile screens. +- Use alternating colors for the 3x3 Sudoku blocks. +- Keep text, buttons, and controls readable. +- Avoid unnecessary layout shifts. + +## Testing + +- Establish the testing framework before refactoring the application. +- Preserve existing behavior during refactoring. +- Run tests after every major refactor or feature update. +- Do not remove or weaken tests just to make them pass. +- Investigate the underlying cause of test failures. + +## Copilot Usage + +- Inspect the existing code before making significant changes. +- Start with the larger architectural problems before smaller refinements. +- Prefer incremental and focused changes. +- Do not rewrite working code unnecessarily. +- Evaluate Copilot-generated suggestions before accepting them. +- Reject or modify suggestions that introduce unnecessary complexity. +- Prefer simple solutions that satisfy the project requirements. +- Explain unfamiliar generated code before relying on it. + +## Documentation + +- Keep the README updated with setup and testing instructions. +- Document the command required to run the test suite. +- Store required Copilot screenshots in the Screenshots folder. +- Never include passwords, API keys, or other sensitive information in the repository. \ No newline at end of file From 24e3878836f2511b567c731a22cd13c07ae8c235 Mon Sep 17 00:00:00 2001 From: AnushaGBhat Date: Sun, 16 Aug 2026 12:05:25 +0530 Subject: [PATCH 2/6] Ignore Python cache files --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 2fcd240b2..da34698f8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,6 @@ Thumbs.db # Ignore Python virtual environment .venv/ +# Ignore Python cache files +__pycache__/ +*.py[cod] From fa8e7fd6704403921ad98c04bb0ea0bd766a6c79 Mon Sep 17 00:00:00 2001 From: AnushaGBhat Date: Sun, 16 Aug 2026 13:20:02 +0530 Subject: [PATCH 3/6] Add baseline pytest tests --- starter/requirements.txt | 1 + starter/tests/test_app.py | 15 ++++ starter/tests/test_sudoku_logic.py | 116 +++++++++++++++++++++++++++++ 3 files changed, 132 insertions(+) create mode 100644 starter/tests/test_app.py create mode 100644 starter/tests/test_sudoku_logic.py diff --git a/starter/requirements.txt b/starter/requirements.txt index 3ab9a28ba..de7fd0181 100644 --- a/starter/requirements.txt +++ b/starter/requirements.txt @@ -1 +1,2 @@ Flask>=2.0 +pytest>=7.0 diff --git a/starter/tests/test_app.py b/starter/tests/test_app.py new file mode 100644 index 000000000..e75e7c194 --- /dev/null +++ b/starter/tests/test_app.py @@ -0,0 +1,15 @@ +import sys +import os + +# Ensure the starter package directory is importable (tests located in starter/tests) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from app import app as flask_app + + +def test_index_route_renders_page(): + client = flask_app.test_client() + res = client.get('/') + assert res.status_code == 200 + text = res.get_data(as_text=True) + assert 'Sudoku Game' in text diff --git a/starter/tests/test_sudoku_logic.py b/starter/tests/test_sudoku_logic.py new file mode 100644 index 000000000..21855b3d8 --- /dev/null +++ b/starter/tests/test_sudoku_logic.py @@ -0,0 +1,116 @@ +import copy +import sys +import os + +# Ensure the starter package directory is importable (tests located in starter/tests) +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +import sudoku_logic + + +def test_constants_and_create_empty_board(): + assert sudoku_logic.SIZE == 9 + assert sudoku_logic.EMPTY == 0 + board = sudoku_logic.create_empty_board() + assert len(board) == sudoku_logic.SIZE + assert all(len(row) == sudoku_logic.SIZE for row in board) + assert all(cell == sudoku_logic.EMPTY for row in board for cell in row) + + +def test_deep_copy_independent(): + orig = sudoku_logic.create_empty_board() + orig[0][0] = 1 + cp = sudoku_logic.deep_copy(orig) + assert cp == orig + cp[0][0] = 2 + # original should remain unchanged + assert orig[0][0] == 1 + + +def test_is_safe_row_column_subgrid(): + b = sudoku_logic.create_empty_board() + # row conflict + b[0][1] = 5 + assert sudoku_logic.is_safe(b, 0, 2, 5) is False + # column conflict + b[1][2] = 6 + assert sudoku_logic.is_safe(b, 0, 2, 6) is False + # 3x3 subgrid conflict + b[1][1] = 7 + assert sudoku_logic.is_safe(b, 2, 2, 7) is False + # a safe number + assert sudoku_logic.is_safe(b, 0, 0, 9) is True + + +def _is_valid_complete_board(board): + SIZE = sudoku_logic.SIZE + # rows + for r in range(SIZE): + row_vals = sorted(board[r]) + assert row_vals == list(range(1, SIZE + 1)) + # cols + for c in range(SIZE): + col_vals = sorted(board[r][c] for r in range(SIZE)) + assert col_vals == list(range(1, SIZE + 1)) + # 3x3 boxes + for br in range(0, SIZE, 3): + for bc in range(0, SIZE, 3): + vals = [] + for r in range(3): + for c in range(3): + vals.append(board[br + r][bc + c]) + assert sorted(vals) == list(range(1, SIZE + 1)) + + +def test_fill_board_completes_board(): + b = sudoku_logic.create_empty_board() + assert sudoku_logic.fill_board(b) is True + # no zeros left + assert all(cell != sudoku_logic.EMPTY for row in b for cell in row) + # board validity checks + _is_valid_complete_board(b) + + +def test_generate_puzzle_has_requested_clues_and_solution_full(): + clues = 30 + puzzle, solution = sudoku_logic.generate_puzzle(clues=clues) + assert len(puzzle) == sudoku_logic.SIZE + assert len(solution) == sudoku_logic.SIZE + # puzzle clues count + non_empty = sum(1 for r in puzzle for c in r if c != sudoku_logic.EMPTY) + assert non_empty == clues + # solution is full (no empty cells) + assert all(cell != sudoku_logic.EMPTY for row in solution for cell in row) + # wherever puzzle has a clue, it must match solution + for r in range(sudoku_logic.SIZE): + for c in range(sudoku_logic.SIZE): + if puzzle[r][c] != sudoku_logic.EMPTY: + assert puzzle[r][c] == solution[r][c] + + +def test_fill_board_solves_known_puzzle(): + 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], + ] + expected = [ + [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], + ] + b = copy.deepcopy(puzzle) + assert sudoku_logic.fill_board(b) is True + assert b == expected From 471feeeaad807c18f911031c33009d7e73bdec4b Mon Sep 17 00:00:00 2001 From: AnushaGBhat Date: Sun, 16 Aug 2026 13:52:34 +0530 Subject: [PATCH 4/6] Ensure generated Sudoku puzzles have unique solutions --- starter/sudoku_logic.py | 59 ++++++++++++++++++++++++++---- starter/tests/test_sudoku_logic.py | 32 ++++++++++++++++ 2 files changed, 83 insertions(+), 8 deletions(-) diff --git a/starter/sudoku_logic.py b/starter/sudoku_logic.py index 443b24524..d6d47c995 100644 --- a/starter/sudoku_logic.py +++ b/starter/sudoku_logic.py @@ -4,12 +4,15 @@ SIZE = 9 EMPTY = 0 + def deep_copy(board): return copy.deepcopy(board) + def create_empty_board(): 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): @@ -24,6 +27,7 @@ def is_safe(board, row, col, num): return False return True + def fill_board(board): for row in range(SIZE): for col in range(SIZE): @@ -39,19 +43,58 @@ def fill_board(board): return False return True + +def count_solutions(board, limit=2): + working = deep_copy(board) + solution_count = 0 + + def search(): + nonlocal solution_count + if solution_count >= limit: + return + + for row in range(SIZE): + for col in range(SIZE): + if working[row][col] == EMPTY: + for num in range(1, SIZE + 1): + if is_safe(working, row, col, num): + working[row][col] = num + search() + if solution_count >= limit: + working[row][col] = EMPTY + return + working[row][col] = EMPTY + return + + solution_count += 1 + + search() + return solution_count + + 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 + target = clues + cells = [(row, col) for row in range(SIZE) for col in range(SIZE)] + random.shuffle(cells) + + for row, col in cells: + if sum(1 for r in board for c in r if c != EMPTY) <= target: + break + if board[row][col] == EMPTY: + continue + + value = board[row][col] + board[row][col] = EMPTY + if count_solutions(board, limit=2) != 1: + board[row][col] = value + + return board + def generate_puzzle(clues=35): board = create_empty_board() fill_board(board) solution = deep_copy(board) - remove_cells(board, clues) puzzle = deep_copy(board) + remove_cells(puzzle, clues) return puzzle, solution diff --git a/starter/tests/test_sudoku_logic.py b/starter/tests/test_sudoku_logic.py index 21855b3d8..88953b926 100644 --- a/starter/tests/test_sudoku_logic.py +++ b/starter/tests/test_sudoku_logic.py @@ -88,6 +88,38 @@ def test_generate_puzzle_has_requested_clues_and_solution_full(): assert puzzle[r][c] == solution[r][c] +def test_count_solutions_unique_puzzle(): + 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], + ] + assert sudoku_logic.count_solutions(puzzle) == 1 + + +def test_count_solutions_multiple_solutions(): + empty_board = sudoku_logic.create_empty_board() + assert sudoku_logic.count_solutions(empty_board, limit=2) == 2 + + +def test_generate_puzzle_has_unique_solution(): + for _ in range(10): + puzzle, solution = sudoku_logic.generate_puzzle(clues=35) + assert sum(1 for row in puzzle for cell in row if cell != sudoku_logic.EMPTY) == 35 + assert sudoku_logic.count_solutions(puzzle) == 1 + assert all(cell != sudoku_logic.EMPTY for row in solution for cell in row) + for r in range(sudoku_logic.SIZE): + for c in range(sudoku_logic.SIZE): + if puzzle[r][c] != sudoku_logic.EMPTY: + assert puzzle[r][c] == solution[r][c] + + def test_fill_board_solves_known_puzzle(): puzzle = [ [5,3,0,0,7,0,0,0,0], From bae8a66337a4c5846688abc6c7dcb6d34f50612d Mon Sep 17 00:00:00 2001 From: AnushaGBhat Date: Sun, 16 Aug 2026 14:17:40 +0530 Subject: [PATCH 5/6] Add Sudoku difficulty and gameplay validation --- starter/app.py | 30 ++++--- starter/static/main.js | 123 ++++++++++++++++++++--------- starter/static/styles.css | 13 ++- starter/sudoku_logic.py | 39 ++++++++- starter/templates/index.html | 9 ++- starter/tests/test_app.py | 13 +++ starter/tests/test_sudoku_logic.py | 57 +++++++++++++ 7 files changed, 231 insertions(+), 53 deletions(-) diff --git a/starter/app.py b/starter/app.py index 0f526b757..76bed43e5 100644 --- a/starter/app.py +++ b/starter/app.py @@ -6,20 +6,35 @@ # Keep a simple in-memory store for current puzzle and solution CURRENT = { 'puzzle': None, - 'solution': None + 'solution': None, + 'difficulty': 'medium' } + @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) + difficulty = request.args.get('difficulty', 'medium') + clues = request.args.get('clues') + + try: + if clues is not None: + clues = int(clues) + else: + clues = sudoku_logic.get_difficulty_clues(difficulty) + puzzle, solution = sudoku_logic.generate_puzzle(clues=clues) + except ValueError: + return jsonify({'error': 'Invalid difficulty or clue count'}), 400 + CURRENT['puzzle'] = puzzle CURRENT['solution'] = solution - return jsonify({'puzzle': puzzle}) + CURRENT['difficulty'] = difficulty + return jsonify({'puzzle': puzzle, 'solution': solution, 'difficulty': difficulty, 'clues': clues}) + @app.route('/check', methods=['POST']) def check_solution(): @@ -28,12 +43,9 @@ def check_solution(): 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]) + incorrect = sudoku_logic.find_incorrect_cells(board, solution) return jsonify({'incorrect': incorrect}) + if __name__ == '__main__': app.run(debug=True) \ No newline at end of file diff --git a/starter/static/main.js b/starter/static/main.js index 2028e1026..365ae4a10 100644 --- a/starter/static/main.js +++ b/starter/static/main.js @@ -1,6 +1,17 @@ // Client-side rendering and interaction for the Flask-backed Sudoku const SIZE = 9; let puzzle = []; +let currentSolution = []; + +function getMessageElement() { + return document.getElementById('message'); +} + +function setMessage(text, isSuccess = false) { + const msg = getMessageElement(); + msg.innerText = text; + msg.style.color = isSuccess ? '#388e3c' : '#d32f2f'; +} function createBoardElement() { const boardDiv = document.getElementById('sudoku-board'); @@ -15,9 +26,10 @@ function createBoardElement() { 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; + input.addEventListener('input', (event) => { + const value = event.target.value.replace(/[^1-9]/g, '').slice(0, 1); + event.target.value = value; + updateBoardFeedback(); }); rowDiv.appendChild(input); } @@ -25,8 +37,9 @@ function createBoardElement() { } } -function renderPuzzle(puz) { +function renderPuzzle(puz, solution = []) { puzzle = puz; + currentSolution = solution; createBoardElement(); const boardDiv = document.getElementById('sudoku-board'); const inputs = boardDiv.getElementsByTagName('input'); @@ -35,26 +48,23 @@ function renderPuzzle(puz) { const idx = i * SIZE + j; const val = puzzle[i][j]; const inp = inputs[idx]; + inp.className = 'sudoku-cell'; if (val !== 0) { - inp.value = val; + inp.value = String(val); inp.disabled = true; - inp.className += ' prefilled'; + inp.readOnly = true; + inp.classList.add('prefilled'); } else { inp.value = ''; inp.disabled = false; + inp.readOnly = false; } } } + updateBoardFeedback(); } -async function newGame() { - const res = await fetch('/new'); - const data = await res.json(); - renderPuzzle(data.puzzle); - document.getElementById('message').innerText = ''; -} - -async function checkSolution() { +function getBoardFromInputs() { const boardDiv = document.getElementById('sudoku-board'); const inputs = boardDiv.getElementsByTagName('input'); const board = []; @@ -66,40 +76,75 @@ async function checkSolution() { 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; + return board; +} + +function boardIsSolved(board) { + if (!currentSolution || !currentSolution.length) { + return false; + } + for (let i = 0; i < SIZE; i++) { + for (let j = 0; j < SIZE; j++) { + if (board[i][j] !== currentSolution[i][j]) { + return false; + } + } } - const incorrect = new Set(data.incorrect.map(x => x[0]*SIZE + x[1])); + return true; +} + +function updateBoardFeedback() { + const inputs = document.getElementById('sudoku-board').getElementsByTagName('input'); + const board = getBoardFromInputs(); + let hasIncorrectEntry = false; + 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 (inp.disabled) { + inp.classList.remove('incorrect'); + continue; + } + + const row = Number(inp.dataset.row); + const col = Number(inp.dataset.col); + const value = board[row][col]; + const isIncorrect = value !== 0 && currentSolution[row]?.[col] !== undefined && value !== currentSolution[row][col]; + inp.classList.toggle('incorrect', isIncorrect); + if (isIncorrect) { + hasIncorrectEntry = true; } } - 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.'; + + if (boardIsSolved(board)) { + setMessage('Congratulations! Puzzle complete!', true); + return; } + + if (hasIncorrectEntry) { + setMessage('Some entries are incorrect.'); + return; + } + + const msg = getMessageElement(); + msg.innerText = ''; + msg.style.color = '#333'; +} + +async function newGame() { + const difficulty = document.getElementById('difficulty').value; + const res = await fetch(`/new?difficulty=${encodeURIComponent(difficulty)}`); + if (!res.ok) { + const data = await res.json().catch(() => ({ error: 'Unable to load puzzle' })); + setMessage(data.error || 'Unable to load puzzle'); + return; + } + const data = await res.json(); + renderPuzzle(data.puzzle, data.solution || []); + setMessage('', false); } -// Wire buttons window.addEventListener('load', () => { document.getElementById('new-game').addEventListener('click', newGame); - document.getElementById('check-solution').addEventListener('click', checkSolution); - // initialize + document.getElementById('difficulty').addEventListener('change', newGame); newGame(); }); \ No newline at end of file diff --git a/starter/static/styles.css b/starter/static/styles.css index 1a6218ff9..983208da7 100644 --- a/starter/static/styles.css +++ b/starter/static/styles.css @@ -59,12 +59,21 @@ h1 { } .controls { + display: flex; + justify-content: center; + align-items: center; + gap: 10px; margin: 20px auto; + flex-wrap: wrap; +} + +#difficulty { + padding: 8px 12px; + font-size: 16px; } button { padding: 8px 18px; - margin: 0 8px; font-size: 16px; border: none; background: #1976d2; @@ -79,7 +88,7 @@ button:hover { } #message { - margin-left: 20px; + min-height: 24px; font-size: 16px; color: #d32f2f; } diff --git a/starter/sudoku_logic.py b/starter/sudoku_logic.py index d6d47c995..e371d2506 100644 --- a/starter/sudoku_logic.py +++ b/starter/sudoku_logic.py @@ -4,6 +4,12 @@ SIZE = 9 EMPTY = 0 +DIFFICULTY_SETTINGS = { + 'easy': {'clues': 45}, + 'medium': {'clues': 36}, + 'hard': {'clues': 30}, +} + def deep_copy(board): return copy.deepcopy(board) @@ -13,6 +19,13 @@ def create_empty_board(): return [[EMPTY for _ in range(SIZE)] for _ in range(SIZE)] +def get_difficulty_clues(difficulty): + normalized = (difficulty or 'medium').lower() + if normalized not in DIFFICULTY_SETTINGS: + raise ValueError(f'Unknown difficulty: {difficulty}') + return DIFFICULTY_SETTINGS[normalized]['clues'] + + def is_safe(board, row, col, num): # Check row and column for x in range(SIZE): @@ -91,10 +104,34 @@ def remove_cells(board, clues): return board -def generate_puzzle(clues=35): +def find_incorrect_cells(board, solution): + incorrect = [] + for row in range(SIZE): + for col in range(SIZE): + if board[row][col] != EMPTY and board[row][col] != solution[row][col]: + incorrect.append([row, col]) + return incorrect + + +def is_board_complete(board): + return all(cell != EMPTY for row in board for cell in row) + + +def is_board_solved(board, solution): + return is_board_complete(board) and not find_incorrect_cells(board, solution) + + +def generate_puzzle(clues=None, difficulty=None): + if clues is None: + clues = get_difficulty_clues(difficulty) board = create_empty_board() fill_board(board) solution = deep_copy(board) puzzle = deep_copy(board) remove_cells(puzzle, clues) return puzzle, solution + + +def generate_puzzle_for_difficulty(difficulty): + clues = get_difficulty_clues(difficulty) + return generate_puzzle(clues=clues) diff --git a/starter/templates/index.html b/starter/templates/index.html index e42ad04da..7a61be009 100644 --- a/starter/templates/index.html +++ b/starter/templates/index.html @@ -7,12 +7,17 @@

Sudoku Game

-
+ + -
+
\ No newline at end of file diff --git a/starter/tests/test_app.py b/starter/tests/test_app.py index e75e7c194..fd3dc55bb 100644 --- a/starter/tests/test_app.py +++ b/starter/tests/test_app.py @@ -13,3 +13,16 @@ def test_index_route_renders_page(): assert res.status_code == 200 text = res.get_data(as_text=True) assert 'Sudoku Game' in text + assert 'difficulty' in text.lower() + + +def test_new_game_route_supports_difficulty_selection(): + client = flask_app.test_client() + res = client.get('/new?difficulty=easy') + assert res.status_code == 200 + payload = res.get_json() + assert payload['difficulty'] == 'easy' + assert payload['solution'] + assert len(payload['puzzle']) == 9 + assert len(payload['solution']) == 9 + assert sum(1 for row in payload['puzzle'] for cell in row if cell != 0) == 45 diff --git a/starter/tests/test_sudoku_logic.py b/starter/tests/test_sudoku_logic.py index 88953b926..e287bcc53 100644 --- a/starter/tests/test_sudoku_logic.py +++ b/starter/tests/test_sudoku_logic.py @@ -88,6 +88,63 @@ def test_generate_puzzle_has_requested_clues_and_solution_full(): assert puzzle[r][c] == solution[r][c] +def test_all_difficulties_generate_unique_puzzles_with_distinct_clue_counts(): + clue_counts = {} + for difficulty in ['easy', 'medium', 'hard']: + puzzle, solution = sudoku_logic.generate_puzzle_for_difficulty(difficulty) + non_empty = sum(1 for row in puzzle for cell in row if cell != sudoku_logic.EMPTY) + clue_counts[difficulty] = non_empty + assert non_empty == sudoku_logic.DIFFICULTY_SETTINGS[difficulty]['clues'] + assert sudoku_logic.count_solutions(puzzle) == 1 + assert all(cell != sudoku_logic.EMPTY for row in solution for cell in row) + + assert clue_counts['easy'] > clue_counts['medium'] > clue_counts['hard'] + + +def test_generate_puzzle_for_invalid_difficulty_raises_error(): + try: + sudoku_logic.generate_puzzle_for_difficulty('impossible') + assert False, 'Expected ValueError for invalid difficulty' + except ValueError: + pass + + +def test_find_incorrect_cells_detects_invalid_entries(): + 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], + ] + board = sudoku_logic.deep_copy(solution) + board[0][0] = 9 + assert sudoku_logic.find_incorrect_cells(board, solution) == [[0, 0]] + + +def test_is_board_solved_recognizes_completed_solution(): + 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], + ] + assert sudoku_logic.is_board_solved(solution, solution) is True + + incomplete = sudoku_logic.deep_copy(solution) + incomplete[0][0] = sudoku_logic.EMPTY + assert sudoku_logic.is_board_solved(incomplete, solution) is False + + def test_count_solutions_unique_puzzle(): puzzle = [ [5,3,0,0,7,0,0,0,0], From 985f53b67ce7d16d83fe9fa620b6a232f9d4830b Mon Sep 17 00:00:00 2001 From: AnushaGBhat Date: Sun, 30 Aug 2026 13:40:23 +0530 Subject: [PATCH 6/6] Enhance Sudoku game UI and functionality --- starter/static/main.js | 352 ++++++++++++++++++++++++++++++----- starter/static/styles.css | 316 +++++++++++++++++++++++++------ starter/templates/index.html | 50 +++-- starter/tests/test_app.py | 13 ++ 4 files changed, 613 insertions(+), 118 deletions(-) diff --git a/starter/static/main.js b/starter/static/main.js index 365ae4a10..51dc1ef65 100644 --- a/starter/static/main.js +++ b/starter/static/main.js @@ -1,7 +1,12 @@ -// Client-side rendering and interaction for the Flask-backed Sudoku + const SIZE = 9; +const STORAGE_KEY = 'sudoku-top-scores'; let puzzle = []; let currentSolution = []; +let timerInterval = null; +let startTime = 0; +let hintsUsed = 0; +let gameCompleted = false; function getMessageElement() { return document.getElementById('message'); @@ -10,29 +15,119 @@ function getMessageElement() { function setMessage(text, isSuccess = false) { const msg = getMessageElement(); msg.innerText = text; - msg.style.color = isSuccess ? '#388e3c' : '#d32f2f'; + msg.style.color = isSuccess ? '#2e7d32' : '#d32f2f'; +} + +function formatTime(totalSeconds) { + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${String(minutes).padStart(2, '0')}:${String(seconds).padStart(2, '0')}`; +} + +function updateTimer() { + if (!startTime) { + document.getElementById('timer').textContent = '00:00'; + return; + } + + const elapsed = Math.floor((Date.now() - startTime) / 1000); + document.getElementById('timer').textContent = formatTime(elapsed); +} + +function startTimer() { + clearInterval(timerInterval); + startTime = Date.now(); + updateTimer(); + timerInterval = setInterval(updateTimer, 1000); +} + +function stopTimer() { + clearInterval(timerInterval); + timerInterval = null; +} + +function getScoreEntries() { + try { + const stored = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]'); + return Array.isArray(stored) ? stored : []; + } catch (error) { + return []; + } +} + +function renderScoreboard() { + const list = document.getElementById('scoreboard'); + const entries = getScoreEntries().slice(0, 10); + + list.innerHTML = ''; + if (!entries.length) { + const emptyItem = document.createElement('li'); + emptyItem.className = 'empty'; + emptyItem.textContent = 'No scores yet'; + list.appendChild(emptyItem); + return; + } + + entries.forEach((entry, index) => { + const item = document.createElement('li'); + item.textContent = `${index + 1}. ${entry.name} ? ${formatTime(entry.time)} ? ${entry.difficulty} ? ${entry.hints} hint${entry.hints === 1 ? '' : 's'}`; + list.appendChild(item); + }); +} + +function markScore(name, elapsedSeconds, difficultyLevel, hintCount) { + const entries = getScoreEntries(); + entries.push({ + name, + time: elapsedSeconds, + difficulty: difficultyLevel, + hints: hintCount, + }); + + entries.sort((a, b) => a.time - b.time || a.hints - b.hints); + localStorage.setItem(STORAGE_KEY, JSON.stringify(entries.slice(0, 10))); + renderScoreboard(); } function createBoardElement() { const boardDiv = document.getElementById('sudoku-board'); boardDiv.innerHTML = ''; - for (let i = 0; i < SIZE; i++) { + + for (let row = 0; row < SIZE; row += 1) { const rowDiv = document.createElement('div'); rowDiv.className = 'sudoku-row'; - for (let j = 0; j < SIZE; j++) { + + for (let col = 0; col < SIZE; col += 1) { const input = document.createElement('input'); input.type = 'text'; input.maxLength = 1; input.className = 'sudoku-cell'; - input.dataset.row = i; - input.dataset.col = j; + input.dataset.row = String(row); + input.dataset.col = String(col); + + if ((Math.floor(row / 3) + Math.floor(col / 3)) % 2 === 0) { + input.classList.add('block-light'); + } else { + input.classList.add('block-dark'); + } + + if ((col + 1) % 3 === 0 && col !== SIZE - 1) { + input.style.borderRightWidth = '3px'; + } + if ((row + 1) % 3 === 0 && row !== SIZE - 1) { + input.style.borderBottomWidth = '3px'; + } + input.addEventListener('input', (event) => { - const value = event.target.value.replace(/[^1-9]/g, '').slice(0, 1); - event.target.value = value; + const target = event.target; + const value = target.value.replace(/[^1-9]/g, '').slice(0, 1); + target.value = value; updateBoardFeedback(); }); + rowDiv.appendChild(input); } + boardDiv.appendChild(rowDiv); } } @@ -40,27 +135,55 @@ function createBoardElement() { function renderPuzzle(puz, solution = []) { puzzle = puz; currentSolution = solution; + hintsUsed = 0; + gameCompleted = false; 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]; - inp.className = 'sudoku-cell'; + + for (let row = 0; row < SIZE; row += 1) { + for (let col = 0; col < SIZE; col += 1) { + const idx = row * SIZE + col; + const val = puzzle[row][col]; + const input = inputs[idx]; + const cellClassNames = ['sudoku-cell']; + + if ((Math.floor(row / 3) + Math.floor(col / 3)) % 2 === 0) { + cellClassNames.push('block-light'); + } else { + cellClassNames.push('block-dark'); + } + + input.className = cellClassNames.join(' '); + input.classList.remove('prefilled', 'hinted', 'incorrect'); + + if ((col + 1) % 3 === 0 && col !== SIZE - 1) { + input.style.borderRightWidth = '3px'; + } else { + input.style.borderRightWidth = '1px'; + } + + if ((row + 1) % 3 === 0 && row !== SIZE - 1) { + input.style.borderBottomWidth = '3px'; + } else { + input.style.borderBottomWidth = '1px'; + } + if (val !== 0) { - inp.value = String(val); - inp.disabled = true; - inp.readOnly = true; - inp.classList.add('prefilled'); + input.value = String(val); + input.disabled = true; + input.readOnly = true; + input.classList.add('prefilled'); } else { - inp.value = ''; - inp.disabled = false; - inp.readOnly = false; + input.value = ''; + input.disabled = false; + input.readOnly = false; } } } + + startTimer(); updateBoardFeedback(); } @@ -68,14 +191,16 @@ function getBoardFromInputs() { 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; + + for (let row = 0; row < SIZE; row += 1) { + board[row] = []; + for (let col = 0; col < SIZE; col += 1) { + const idx = row * SIZE + col; + const value = inputs[idx].value; + board[row][col] = value ? Number.parseInt(value, 10) : 0; } } + return board; } @@ -83,68 +208,203 @@ function boardIsSolved(board) { if (!currentSolution || !currentSolution.length) { return false; } - for (let i = 0; i < SIZE; i++) { - for (let j = 0; j < SIZE; j++) { - if (board[i][j] !== currentSolution[i][j]) { + + for (let row = 0; row < SIZE; row += 1) { + for (let col = 0; col < SIZE; col += 1) { + if (board[row][col] !== currentSolution[row][col]) { return false; } } } + return true; } function updateBoardFeedback() { const inputs = document.getElementById('sudoku-board').getElementsByTagName('input'); const board = getBoardFromInputs(); - let hasIncorrectEntry = false; + const invalidCells = new Set(); - for (let idx = 0; idx < inputs.length; idx++) { - const inp = inputs[idx]; - if (inp.disabled) { - inp.classList.remove('incorrect'); + for (let idx = 0; idx < inputs.length; idx += 1) { + const input = inputs[idx]; + const row = Number(input.dataset.row); + const col = Number(input.dataset.col); + + if (input.disabled) { + input.classList.remove('incorrect'); continue; } - const row = Number(inp.dataset.row); - const col = Number(inp.dataset.col); const value = board[row][col]; const isIncorrect = value !== 0 && currentSolution[row]?.[col] !== undefined && value !== currentSolution[row][col]; - inp.classList.toggle('incorrect', isIncorrect); if (isIncorrect) { - hasIncorrectEntry = true; + invalidCells.add(`${row}-${col}`); } + input.classList.toggle('incorrect', isIncorrect); } if (boardIsSolved(board)) { - setMessage('Congratulations! Puzzle complete!', true); + if (!gameCompleted) { + completeGame(); + } return; } - if (hasIncorrectEntry) { + if (invalidCells.size > 0) { setMessage('Some entries are incorrect.'); return; } - const msg = getMessageElement(); - msg.innerText = ''; - msg.style.color = '#333'; + setMessage(''); +} + +function completeGame() { + gameCompleted = true; + stopTimer(); + + const elapsedSeconds = Math.floor((Date.now() - startTime) / 1000); + const difficultyLevel = document.getElementById('difficulty').value; + const scoreText = `Congratulations! Puzzle complete in ${formatTime(elapsedSeconds)} with ${hintsUsed} hint${hintsUsed === 1 ? '' : 's'}.`; + setMessage(scoreText, true); + + const playerName = window.prompt('Enter your name for the Top 10 leaderboard:', 'Player'); + const trimmedName = (playerName || '').trim(); + + if (trimmedName) { + markScore(trimmedName, elapsedSeconds, difficultyLevel, hintsUsed); + } +} + +function fillHint() { + if (gameCompleted) { + return; + } + + const board = getBoardFromInputs(); + let targetCell = null; + + for (let row = 0; row < SIZE; row += 1) { + for (let col = 0; col < SIZE; col += 1) { + if (board[row][col] === 0 && puzzle[row][col] === 0) { + targetCell = { row, col }; + break; + } + } + if (targetCell) { + break; + } + } + + if (!targetCell) { + setMessage('No hint available for this puzzle.'); + return; + } + + const { row, col } = targetCell; + const targetInput = document.querySelector(`input[data-row="${row}"][data-col="${col}"]`); + if (!targetInput) { + return; + } + + targetInput.value = String(currentSolution[row][col]); + targetInput.disabled = true; + targetInput.readOnly = true; + targetInput.classList.add('prefilled', 'hinted'); + hintsUsed += 1; + setMessage(`Hint used: row ${row + 1}, column ${col + 1}.`, false); + updateBoardFeedback(); +} + +async function checkBoard() { + const board = getBoardFromInputs(); + try { + const res = await fetch('/check', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ board }), + }); + + if (!res.ok) { + const data = await res.json().catch(() => ({ error: 'Unable to check puzzle' })); + setMessage(data.error || 'Unable to check puzzle'); + return; + } + + const data = await res.json(); + const incorrect = data.incorrect || []; + const incorrectSet = new Set(incorrect.map(([row, col]) => `${row}-${col}`)); + + const inputs = document.getElementById('sudoku-board').getElementsByTagName('input'); + for (let idx = 0; idx < inputs.length; idx += 1) { + const input = inputs[idx]; + if (input.disabled) { + continue; + } + const row = Number(input.dataset.row); + const col = Number(input.dataset.col); + input.classList.toggle('incorrect', incorrectSet.has(`${row}-${col}`)); + } + + if (incorrect.length) { + setMessage('Some entries are incorrect.'); + return; + } + + if (boardIsSolved(board)) { + completeGame(); + return; + } + + setMessage('No incorrect entries found.'); + } catch (error) { + setMessage('Unable to check puzzle'); + } } async function newGame() { const difficulty = document.getElementById('difficulty').value; const res = await fetch(`/new?difficulty=${encodeURIComponent(difficulty)}`); + if (!res.ok) { const data = await res.json().catch(() => ({ error: 'Unable to load puzzle' })); setMessage(data.error || 'Unable to load puzzle'); return; } + const data = await res.json(); renderPuzzle(data.puzzle, data.solution || []); - setMessage('', false); + setMessage(''); +} + +function toggleDarkMode() { + const root = document.body; + const isDark = root.classList.toggle('dark-mode'); + const button = document.getElementById('theme-toggle'); + button.textContent = isDark ? 'Light' : 'Dark'; + localStorage.setItem('sudoku-theme', isDark ? 'dark' : 'light'); +} + +function applySavedTheme() { + const preferredTheme = localStorage.getItem('sudoku-theme'); + const isDark = preferredTheme === 'dark'; + document.body.classList.toggle('dark-mode', isDark); + const button = document.getElementById('theme-toggle'); + if (button) { + button.textContent = isDark ? 'Light' : 'Dark'; + } } window.addEventListener('load', () => { + applySavedTheme(); + renderScoreboard(); + document.getElementById('new-game').addEventListener('click', newGame); document.getElementById('difficulty').addEventListener('change', newGame); + document.getElementById('hint-button').addEventListener('click', fillHint); + document.getElementById('check-button').addEventListener('click', checkBoard); + document.getElementById('theme-toggle').addEventListener('click', toggleDarkMode); + newGame(); -}); \ No newline at end of file +}); diff --git a/starter/static/styles.css b/starter/static/styles.css index 983208da7..22d66ef2c 100644 --- a/starter/static/styles.css +++ b/starter/static/styles.css @@ -1,94 +1,292 @@ + +:root { + --bg: #f3f5f9; + --panel: #ffffff; + --panel-soft: #eef4ff; + --text: #1f2937; + --muted: #5b6475; + --board-border: #1f2937; + --cell-bg: #ffffff; + --cell-border: #c7d2df; + --block-light: #f5f7fb; + --block-dark: #edf3ff; + --prefilled-bg: #dde8f8; + --prefilled-text: #1f2937; + --hint-bg: #fff4bf; + --incorrect-bg: #f8d7da; + --button-bg: #2563eb; + --button-text: #ffffff; + --button-hover: #1d4ed8; + --secondary-bg: #e2e8f0; + --secondary-text: #0f172a; + --shadow: rgba(15, 23, 42, 0.12); +} + +body.dark-mode { + --bg: #0f172a; + --panel: #111827; + --panel-soft: #172033; + --text: #e5eefb; + --muted: #a7b3c7; + --board-border: #dbeafe; + --cell-bg: #0b1220; + --cell-border: #32415e; + --block-light: #101c32; + --block-dark: #16243d; + --prefilled-bg: #1e3355; + --prefilled-text: #e5eefb; + --hint-bg: #3e3b2a; + --incorrect-bg: #4c1d1d; + --button-bg: #3b82f6; + --button-hover: #2563eb; + --secondary-bg: #1f2937; + --secondary-text: #e5eefb; + --shadow: rgba(2, 6, 23, 0.5); +} + +* { + box-sizing: border-box; +} + body { - font-family: Arial, sans-serif; - background: #f4f4f4; - text-align: center; - margin: 0; - padding: 0; + margin: 0; + font-family: 'Segoe UI', Arial, sans-serif; + background: var(--bg); + color: var(--text); + min-height: 100vh; + padding: 24px 16px; + transition: background 0.2s ease, color 0.2s ease; +} + +button, +select, +input { + font: inherit; +} + +.app-shell { + width: min(100%, 1100px); + margin: 0 auto; +} + +.app-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 18px; +} + +.eyebrow { + margin: 0 0 4px; + color: var(--muted); + text-transform: uppercase; + letter-spacing: 0.14em; + font-size: 0.72rem; + font-weight: 700; } h1 { - margin-top: 30px; - color: #333; + margin: 0; + font-size: clamp(2.1rem, 4vw, 3rem); +} + +.controls { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + gap: 12px; + padding: 14px 18px; + margin-bottom: 16px; + border-radius: 18px; + background: var(--panel); + box-shadow: 0 10px 25px var(--shadow); +} + +label { + font-weight: 600; +} + +select { + min-width: 140px; + padding: 8px 12px; + border-radius: 10px; + border: 1px solid var(--cell-border); + background: var(--panel-soft); + color: var(--text); +} + +button { + padding: 9px 18px; + border: none; + border-radius: 10px; + background: var(--button-bg); + color: var(--button-text); + cursor: pointer; + font-weight: 600; + transition: background 0.2s ease, transform 0.2s ease; +} + +button:hover { + background: var(--button-hover); +} + +button:active { + transform: translateY(1px); } -#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); +button.secondary { + background: var(--secondary-bg); + color: var(--secondary-text); +} + +button.secondary:hover { + background: rgba(148, 163, 184, 0.35); +} + +.icon-button { + width: 42px; + height: 42px; + border-radius: 50%; + padding: 0; + display: inline-flex; + align-items: center; + justify-content: center; +} + +#timer { + min-width: 78px; + padding: 8px 12px; + border-radius: 10px; + background: var(--panel-soft); + border: 1px solid var(--cell-border); + font-weight: 700; + text-align: center; +} + +#message { + min-height: 28px; + text-align: center; + margin-bottom: 18px; + font-weight: 700; + color: #d32f2f; +} + +.game-wrapper { + display: flex; + align-items: flex-start; + justify-content: center; + gap: 24px; +} + +.sudoku-board { + display: inline-block; + background: var(--panel); + border: 4px solid var(--board-border); + border-radius: 10px; + overflow: hidden; + box-shadow: 0 12px 28px var(--shadow); } .sudoku-row { - display: flex; + 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; + width: clamp(28px, 6vw, 56px); + height: clamp(28px, 6vw, 56px); + border: 1px solid var(--cell-border); + padding: 0; + text-align: center; + font-size: clamp(1.2rem, 2vw, 2rem); + font-weight: 600; + background: var(--cell-bg); + color: var(--text); + outline: none; + transition: background 0.2s ease, color 0.2s ease, border-color 0.2s ease; } .sudoku-cell:focus { - background: #e0f7fa; + box-shadow: inset 0 0 0 2px rgba(96, 165, 250, 0.9); +} + +.block-light { + background: var(--block-light); +} + +.block-dark { + background: var(--block-dark); } .sudoku-cell.prefilled { - background: #e0e0e0; - font-weight: bold; - color: #333; + background: var(--prefilled-bg); + color: var(--prefilled-text); + font-weight: 700; } -.sudoku-cell.incorrect { - background: #ffcdd2; +.sudoku-cell.hinted { + background: var(--hint-bg); + color: var(--prefilled-text); } -.sudoku-cell:nth-child(3), -.sudoku-cell:nth-child(6) { - border-right: 3px solid #333; +.sudoku-cell.incorrect { + background: var(--incorrect-bg); } -.sudoku-row:nth-child(3) .sudoku-cell, -.sudoku-row:nth-child(6) .sudoku-cell { - border-bottom: 3px solid #333; +.score-panel { + width: min(100%, 260px); + padding: 16px 18px; + background: var(--panel); + border-radius: 18px; + box-shadow: 0 10px 25px var(--shadow); } -.controls { - display: flex; - justify-content: center; - align-items: center; - gap: 10px; - margin: 20px auto; - flex-wrap: wrap; +.score-panel h2 { + margin: 0 0 12px; + font-size: 1.2rem; + text-align: center; } -#difficulty { - padding: 8px 12px; - font-size: 16px; +.scoreboard { + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 8px; } -button { - padding: 8px 18px; - font-size: 16px; - border: none; - background: #1976d2; - color: #fff; - border-radius: 4px; - cursor: pointer; - transition: background 0.2s; +.scoreboard li { + padding: 8px 10px; + border-radius: 10px; + background: var(--panel-soft); + color: var(--text); + font-size: 0.92rem; } -button:hover { - background: #1565c0; +.scoreboard li.empty { + text-align: center; + color: var(--muted); } -#message { - min-height: 24px; - font-size: 16px; - color: #d32f2f; +@media (max-width: 760px) { + body { + padding: 18px 12px 28px; + } + + .controls { + padding: 12px 14px; + } + + .game-wrapper { + flex-direction: column; + align-items: center; + } + + .sudoku-board { + width: min(100%, 420px); + } + + .score-panel { + width: min(100%, 420px); + } } diff --git a/starter/templates/index.html b/starter/templates/index.html index 7a61be009..2bd2ff3c2 100644 --- a/starter/templates/index.html +++ b/starter/templates/index.html @@ -1,23 +1,47 @@ + - + + Sudoku Game -

Sudoku Game

-
- - - - +
+
+
+

Daily Logic

+

Sudoku Game

+
+ +
+ + + +
+ +
+
+ + +
-
+ - \ No newline at end of file + diff --git a/starter/tests/test_app.py b/starter/tests/test_app.py index fd3dc55bb..1fffddd84 100644 --- a/starter/tests/test_app.py +++ b/starter/tests/test_app.py @@ -14,8 +14,21 @@ def test_index_route_renders_page(): text = res.get_data(as_text=True) assert 'Sudoku Game' in text assert 'difficulty' in text.lower() + assert 'Hint' in text + assert 'Check' in text + assert 'Top 10 Fastest Times' in text + + +def test_index_page_has_game_controls(): + client = flask_app.test_client() + res = client.get('/') + assert res.status_code == 200 + text = res.get_data(as_text=True) + for token in ['difficulty', 'new-game', 'hint-button', 'check-button', 'theme-toggle', 'scoreboard', 'timer']: + assert token in text + def test_new_game_route_supports_difficulty_selection(): client = flask_app.test_client() res = client.get('/new?difficulty=easy')