packages = [] [[fetch]] from = "/sudoku/" files = ["solver.py"]

Sudoku Solver — By Wulfen

Click a cell to highlight it and type digits 1–9. Leave blanks empty. Use Spacebar to skip, Backspace to backtrack. Arrow keys work as well to navigate the grid.

Press Solve to compute a solution. Reset clears the grid.

from js import document from pyodide.ffi import create_proxy from typing import List from time import perf_counter from solver import solve # ensure file is named 'solver.py' next to this HTML Board = List[List[int]] board_el = document.getElementById("board") status_el = document.getElementById("status") btn_solve = document.getElementById("btnSolve") btn_reset = document.getElementById("btnReset") # ---- focus helpers ---- def focus_next(rr:int, cc:int): nr, nc = rr, cc + 1 if nc > 8: nr, nc = rr + 1, 0 if nr <= 8: el = document.getElementById(f"r{nr}c{nc}") if el: el.focus() def focus_prev(rr:int, cc:int): pr, pc = rr, cc - 1 if pc < 0: pr, pc = rr - 1, 8 if pr >= 0: el = document.getElementById(f"r{pr}c{pc}") if el: el.focus() def focus_move(rr:int, cc:int, dr:int, dc:int): nr, nc = rr + dr, cc + dc if 0 <= nr <= 8 and 0 <= nc <= 8: el = document.getElementById(f"r{nr}c{nc}") if el: el.focus() def build_grid(): board_el.innerHTML = "" for r in range(9): for c in range(9): cell = document.createElement("div") cell.className = "cell" cell.setAttribute("data-r", str(r)) cell.setAttribute("data-c", str(c)) inp = document.createElement("input") inp.setAttribute("id", f"r{r}c{c}") inp.setAttribute("inputmode", "numeric") inp.setAttribute("maxlength", "1") inp.setAttribute("autocomplete", "off") inp.setAttribute("aria-label", f"Row {r+1} Column {c+1}") # allow only digits 1-9; auto-advance on valid entry def on_input(evt, el=inp, rr=r, cc=c): v = el.value.strip() if len(v) > 1: v = v[-1] if not v.isdigit() or v == "0": el.value = "" else: el.value = v focus_next(rr, cc) # move to next cell after valid digit inp.addEventListener("input", create_proxy(on_input)) # Keyboard controls: # Space = skip forward; Backspace = clear (if empty, move left); Arrows = navigate def on_keydown(evt, rr=r, cc=c, el=inp): k = evt.key if k in (" ", "Space", "Spacebar"): evt.preventDefault() focus_next(rr, cc) elif k in ("Backspace",): if el.value == "": evt.preventDefault() focus_prev(rr, cc) # else default backspace clears this cell elif k == "ArrowRight": evt.preventDefault(); focus_next(rr, cc) elif k == "ArrowLeft": evt.preventDefault(); focus_prev(rr, cc) elif k == "ArrowUp": evt.preventDefault(); focus_move(rr, cc, -1, 0) elif k == "ArrowDown": evt.preventDefault(); focus_move(rr, cc, 1, 0) inp.addEventListener("keydown", create_proxy(on_keydown)) cell.appendChild(inp) board_el.appendChild(cell) # put cursor on the first cell on load first = document.getElementById("r0c0") if first: first.focus() build_grid() # ----- helpers to read/write board ----- def read_board() -> Board: b: Board = [[0]*9 for _ in range(9)] for r in range(9): for c in range(9): v = document.getElementById(f"r{r}c{c}").value b[r][c] = int(v) if v and v.isdigit() and v != "0" else 0 return b def write_board(b: Board): for r in range(9): for c in range(9): el = document.getElementById(f"r{r}c{c}") el.value = str(b[r][c]) if b[r][c] else "" def clear_board(): for r in range(9): for c in range(9): document.getElementById(f"r{r}c{c}").value = "" status_el.textContent = "" status_el.className = "status" # ----- quick client-side consistency check (duplicates) ----- def ui_board_consistent(b: Board) -> bool: # rows for r in range(9): seen = set() for c in range(9): v = b[r][c] if v == 0: continue if v in seen: return False seen.add(v) # cols for c in range(9): seen = set() for r in range(9): v = b[r][c] if v == 0: continue if v in seen: return False seen.add(v) # boxes for br in range(0,9,3): for bc in range(0,9,3): seen = set() for r in range(br, br+3): for c in range(bc, bc+3): v = b[r][c] if v == 0: continue if v in seen: return False seen.add(v) return True # ----- actions ----- def set_busy(is_busy: bool): btn_solve.disabled = is_busy btn_reset.disabled = is_busy document.body.style.cursor = "progress" if is_busy else "auto" def on_solve(evt=None): b = read_board() # quick UI pre-flight (nice error message) if not ui_board_consistent(b): status_el.textContent = "Board is inconsistent: duplicates in a row, column, or 3×3 box." status_el.className = "status err" return status_el.textContent = "Solving..." status_el.className = "status" set_busy(True) t0 = perf_counter() ok = solve(b) set_busy(False) dt = (perf_counter() - t0)*1000.0 # ms if ok: write_board(b) status_el.textContent = f"Solved ✓ ({dt:.0f} ms)" status_el.className = "status ok" else: status_el.textContent = "No solution found." status_el.className = "status err" def on_reset(evt=None): clear_board() btn_solve.addEventListener("click", create_proxy(on_solve)) btn_reset.addEventListener("click", create_proxy(on_reset))