Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added starter/__pycache__/app.cpython-313.pyc
Binary file not shown.
Binary file added starter/__pycache__/sudoku_logic.cpython-313.pyc
Binary file not shown.
115 changes: 85 additions & 30 deletions starter/app.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,94 @@
from flask import Flask, render_template, jsonify, request
"""Flask application for the Sudoku starter project."""

from typing import Any, Dict, List, Optional

from flask import Flask, jsonify, render_template, request

import sudoku_logic

app = Flask(__name__)

# Keep a simple in-memory store for current puzzle and solution
CURRENT = {
'puzzle': None,
'solution': None
# Keep a simple in-memory store for the current puzzle and solution.
CURRENT: Dict[str, Optional[List[List[int]]]] = {
"puzzle": None,
"solution": None,
}

@app.route('/')
def index():
return render_template('index.html')

@app.route('/new')
def new_game():
clues = int(request.args.get('clues', 35))
@app.route("/")
def index() -> str:
"""Render the main Sudoku page."""
return render_template("index.html")


@app.route("/new")
def new_game() -> Any:
"""Generate a new Sudoku puzzle and store it as the current game."""
clues = int(request.args.get("clues", 35))
puzzle, solution = sudoku_logic.generate_puzzle(clues)
CURRENT['puzzle'] = puzzle
CURRENT['solution'] = solution
return jsonify({'puzzle': puzzle})

@app.route('/check', methods=['POST'])
def check_solution():
data = request.json
board = data.get('board')
solution = CURRENT.get('solution')
CURRENT["puzzle"] = puzzle
CURRENT["solution"] = solution
return jsonify({"puzzle": puzzle})


@app.route("/check", methods=["POST"])
def check_solution() -> Any:
"""Return the coordinates of incorrect values compared to the solution."""
data = request.get_json()
board = data.get("board")
solution = CURRENT.get("solution")

if solution is None:
return jsonify({"error": "No game in progress"}), 400

incorrect: List[List[int]] = []
for row_index in range(sudoku_logic.SIZE):
for col_index in range(sudoku_logic.SIZE):
if board[row_index][col_index] != solution[row_index][col_index]:
incorrect.append([row_index, col_index])

return jsonify({"incorrect": incorrect})


@app.route("/hint", methods=["POST"])
def get_hint() -> Any:
"""Fill the first empty cell with the correct solution value."""
puzzle = CURRENT.get("puzzle")
solution = CURRENT.get("solution")

if puzzle is None or solution is None:
return jsonify({"error": "No game in progress"}), 400

for row_index in range(sudoku_logic.SIZE):
for col_index in range(sudoku_logic.SIZE):
if puzzle[row_index][col_index] == sudoku_logic.EMPTY:
value = solution[row_index][col_index]
puzzle[row_index][col_index] = value
CURRENT["puzzle"] = puzzle
return jsonify({
"row": row_index,
"col": col_index,
"value": value,
})

return jsonify({"message": "Puzzle already complete"})


@app.route("/validate", methods=["POST"])
def validate_move() -> Any:
"""Check whether a submitted move matches the current solution."""
data = request.get_json()
row = data.get("row")
col = data.get("col")
value = data.get("value")
solution = CURRENT.get("solution")

if solution is None:
return jsonify({'error': 'No game in progress'}), 400
incorrect = []
for i in range(sudoku_logic.SIZE):
for j in range(sudoku_logic.SIZE):
if board[i][j] != solution[i][j]:
incorrect.append([i, j])
return jsonify({'incorrect': incorrect})

if __name__ == '__main__':
app.run(debug=True)
return jsonify({"error": "No game in progress"}), 400

correct = solution[row][col] == value
return jsonify({"correct": correct})


if __name__ == "__main__":
app.run(debug=True)
1 change: 1 addition & 0 deletions starter/requirements.txt
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Flask>=2.0
pytest>=8.0
177 changes: 176 additions & 1 deletion starter/static/main.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,131 @@
// Client-side rendering and interaction for the Flask-backed Sudoku
const SIZE = 9;
const DIFFICULTY_CLUES = {
easy: 40,
medium: 32,
hard: 26
};
let puzzle = [];
let timerInterval;
let elapsedSeconds = 0;

function applyTheme(theme) {
document.body.classList.toggle('dark-mode', theme === 'dark');
const themeButton = document.getElementById('theme-toggle');
if (themeButton) {
themeButton.textContent = theme === 'dark' ? '🌞 Light Mode' : '🌙 Dark Mode';
}
}

function toggleTheme() {
const isDarkMode = document.body.classList.contains('dark-mode');
const nextTheme = isDarkMode ? 'light' : 'dark';
localStorage.setItem('sudoku-theme', nextTheme);
applyTheme(nextTheme);
}

function startTimer() {
if (timerInterval) {
return;
}
timerInterval = setInterval(() => {
elapsedSeconds += 1;
updateTimer();
}, 1000);
}

function stopTimer() {
if (timerInterval) {
clearInterval(timerInterval);
timerInterval = null;
}
}

function resetTimer() {
stopTimer();
elapsedSeconds = 0;
updateTimer();
}

function updateTimer() {
const minutes = String(Math.floor(elapsedSeconds / 60)).padStart(2, '0');
const seconds = String(elapsedSeconds % 60).padStart(2, '0');
const timer = document.getElementById('timer');
if (timer) {
timer.textContent = `${minutes}:${seconds}`;
}
}

function loadLeaderboard() {
const stored = localStorage.getItem('sudoku-leaderboard');
return stored ? JSON.parse(stored) : [];
}

function saveLeaderboard(entries) {
localStorage.setItem('sudoku-leaderboard', JSON.stringify(entries));
}

function renderLeaderboard() {
const tbody = document.querySelector('#leaderboard tbody');
if (!tbody) {
return;
}

const entries = loadLeaderboard();
tbody.innerHTML = '';

if (entries.length === 0) {
const row = document.createElement('tr');
const cell = document.createElement('td');
cell.colSpan = 4;
cell.textContent = 'No scores yet';
row.appendChild(cell);
tbody.appendChild(row);
return;
}

entries.forEach((entry, index) => {
const row = document.createElement('tr');
const rankCell = document.createElement('td');
rankCell.textContent = index + 1;
row.appendChild(rankCell);

const nameCell = document.createElement('td');
nameCell.textContent = entry.name;
row.appendChild(nameCell);

const difficultyCell = document.createElement('td');
difficultyCell.textContent = entry.difficulty;
row.appendChild(difficultyCell);

const timeCell = document.createElement('td');
timeCell.textContent = entry.time;
row.appendChild(timeCell);

tbody.appendChild(row);
});
}

async function validateCellInput(event) {
const input = event.target;
const value = input.value;

if (value === '') {
input.className = 'sudoku-cell';
return;
}

const row = parseInt(input.dataset.row, 10);
const col = parseInt(input.dataset.col, 10);
const res = await fetch('/validate', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({row, col, value: parseInt(value, 10)})
});
const data = await res.json();

input.className = data.correct ? 'sudoku-cell' : 'sudoku-cell incorrect';
}

function createBoardElement() {
const boardDiv = document.getElementById('sudoku-board');
Expand All @@ -18,6 +143,9 @@ function createBoardElement() {
input.addEventListener('input', (e) => {
const val = e.target.value.replace(/[^1-9]/g, '');
e.target.value = val;
if (val !== '') {
validateCellInput(e);
}
});
rowDiv.appendChild(input);
}
Expand Down Expand Up @@ -48,10 +176,15 @@ function renderPuzzle(puz) {
}

async function newGame() {
const res = await fetch('/new');
const difficultySelect = document.getElementById("difficulty");
const difficulty = difficultySelect.value;
const clues = DIFFICULTY_CLUES[difficulty];
const res = await fetch(`/new?clues=${clues}`);
const data = await res.json();
renderPuzzle(data.puzzle);
document.getElementById('message').innerText = '';
resetTimer();
startTimer();
}

async function checkSolution() {
Expand Down Expand Up @@ -88,6 +221,22 @@ async function checkSolution() {
}
}
if (incorrect.size === 0) {
stopTimer();
const playerName = window.prompt('Enter your name for the leaderboard:') || 'Anonymous';
const difficultySelect = document.getElementById('difficulty');
const difficulty = difficultySelect.value;
const minutes = String(Math.floor(elapsedSeconds / 60)).padStart(2, '0');
const seconds = String(elapsedSeconds % 60).padStart(2, '0');
const time = `${minutes}:${seconds}`;
const entries = loadLeaderboard();
entries.push({name: playerName, difficulty, time});
entries.sort((a, b) => {
const aTime = a.time.split(':').reduce((total, part) => total * 60 + parseInt(part, 10), 0);
const bTime = b.time.split(':').reduce((total, part) => total * 60 + parseInt(part, 10), 0);
return aTime - bTime;
});
saveLeaderboard(entries.slice(0, 10));
renderLeaderboard();
msg.style.color = '#388e3c';
msg.innerText = 'Congratulations! You solved it!';
} else {
Expand All @@ -96,10 +245,36 @@ async function checkSolution() {
}
}

async function hintGame() {
const res = await fetch('/hint', {method: 'POST'});
const data = await res.json();
const msg = document.getElementById('message');

if (data.message) {
msg.style.color = '#d32f2f';
msg.innerText = data.message;
return;
}

const idx = data.row * SIZE + data.col;
const input = document.querySelector(`.sudoku-cell[data-row="${data.row}"][data-col="${data.col}"]`);
input.value = data.value;
input.disabled = true;
input.className = 'sudoku-cell prefilled';
msg.style.color = '#388e3c';
msg.innerText = 'Hint used.';
}

// Wire buttons
window.addEventListener('load', () => {
const savedTheme = localStorage.getItem('sudoku-theme') || 'light';
applyTheme(savedTheme);

document.getElementById('new-game').addEventListener('click', newGame);
document.getElementById('hint-button').addEventListener('click', hintGame);
document.getElementById('check-solution').addEventListener('click', checkSolution);
document.getElementById('theme-toggle').addEventListener('click', toggleTheme);
renderLeaderboard();
// initialize
newGame();
});
Loading