The Sudoku solver and generator embodied in sudoku.py (see code listing below) is the result of a deliberate layering of concerns and patterns, each chosen to mirror how a human might reason through a puzzle and to ensure that the codebase remains both adaptable and understandable. At its core lies the representation of the puzzle’s state in a way that automatically enforces consistency, layered atop that are modular strategy implementations capturing common pencil-and-paper techniques, and finally an interactive command-line interface that ties them all together.
Every puzzle begins life on the Board object, which holds a nine-by-nine grid of Cell instances. A Cell tracks either a fixed digit or a set of possible candidates. By representing candidates with a Python set, the implementation leverages built-in operations like union, intersection, and difference to simplify logic such as pairs, triples, and quads. Whenever a value is assigned to a cell through the set_value(r, c, v) method, that cell’s candidate set is cleared and the digit v is immediately removed from every peer cell in the same row, column, and three-by-three box. This single method call enforces the invariant that no two cells in a unit ever share the same digit, obviating the need for any separate consistency checks or manual bookkeeping of peer relationships elsewhere in the code.
Building on that always-consistent backbone is the implementation of solving techniques, each encapsulated in its own class deriving from the IStrategy interface. The signature of IStrategy is deliberately minimal: it requires only a single method, apply(board), which returns a list of one or more Action instances. An Action object names exactly which row and column it affects, what digit it assigns or candidate it eliminates, and carries a human-readable description for display. By treating every single elimination and assignment as a first-class object, the solver gains both the ability to narrate its reasoning step by step and a uniform mechanism for undoing those steps without tacking on ad hoc rollback code to each strategy.
The simplest strategies—naked singles and hidden singles—serve to illustrate this pattern. The naked single strategy iterates through every cell and, as soon as it finds one with exactly one remaining candidate, emits an action to assign that digit. The hidden single strategy scans each row, column, and box, counts how often each candidate appears among unsolved cells, and when it finds a candidate that appears exactly once, emits an action to assign it. Both strategies run in linear or near-linear time and typically resolve most of the easy clues in a puzzle.
Once those basic moves are exhausted, the solver moves on to techniques that involve pairs, triples, and quads. For instance, the naked pair strategy locates two cells in the same unit that share an identical two-element candidate set; it then issues elimination actions for those two digits in every other cell of the unit. The hidden pair strategy flips the perspective: it detects two candidates that appear together only in exactly two cells of a unit, and emits elimination actions for any extra candidates those two cells might hold. The code for triples and quads follows the same template, using combinations of three or four cells and set unions to identify when three or four candidates occupy exactly three or four cells.
When candidates span box and line boundaries, the implementation employs pointing and claiming techniques. In pointing, the code inspects each three-by-three block and groups each candidate by its row or column within that block. If all occurrences of a candidate lie in a single row of the box, the code emits elimination actions for that candidate in the rest of the row outside the box. Claiming inverts the logic: it looks at each row (or column) and, if all instances of a candidate in that row fall within a single box, emits elimination actions in the other cells of that box. These two dual strategies clean up interactions between global rows and local boxes.
Fish patterns add another layer of sophistication. The X-Wing strategy maps occurrences of a candidate in two rows that each have exactly two possible columns, checks whether those column pairs align, and if so, eliminates the candidate from those columns in every other row. The Swordfish strategy generalizes this pattern to three rows and three columns, requiring that the union of three rows’ candidate columns contains exactly three distinct columns. Because the candidate lists remain small, sampling combinations of rows remains performant even in Python.
For advanced logical deductions, the solver implements the XY-Wing technique. This strategy identifies a pivot cell with exactly two candidates {x,y} and two “wing” cells—one sharing x with the pivot and the other sharing y—such that the wings themselves intersect on a third candidate z. By the logic of XY-Wings, z can be safely eliminated from any cell that peers with both wings. Equally, the Unique Rectangle Type 1 pattern guards against multi-solution rectangles: when four corner cells of a rectangle share exactly two common candidates and two of those corners are bi-value cells, then any extra candidate in the other two cells must be eliminated to prevent a second valid solution.
Coordinating all of these strategies is the SolverController. It holds a prioritized list of strategy instances and, when asked to advance one step, iterates through them in order of increasing complexity. The first strategy to return any actions causes the controller to take the first action, deep-copy the entire board before the change (implementing the Memento pattern), annotate the action with its originating strategy name, and apply it. This snapshot-and-apply approach yields a reliable undo capability: stepping backward simply pops the last saved board from the history stack and restores it, without any need for per-strategy undo code.
Reusing the identical solver in reverse powers the puzzle generator. After backtracking to fill an empty grid into a complete solution, the generator shuffles all 81 cell positions and removes clues one at a time. Following each removal, it hands the partially emptied grid to the solver. If no strategy applies and the grid remains unsolved or if the solver uses any technique outside the set permitted for the chosen difficulty, the removal is rolled back. By defining “easy” to allow only naked and hidden singles, “medium” to add pairs and pointing, and “hard” to include fish and triple/quad patterns, the generator yields puzzles whose required logical complexity matches human expectations.
Running the script is straightforward. To generate a medium puzzle you type:
The output appears as an ASCII board, with thick box boundaries and dots marking blanks. Immediately the prompt offers [n]ext to apply the next logical deduction, [p]rev to undo, or [q]uit to exit. When you press n, the next action’s description—such as “X-Wing remove 5 at (2,7)”—appears and the board is redrawn with the affected cell highlighted in red via ANSI escape codes.
To load an existing puzzle from a file, the --read option parses nine lines of nine tokens each, where digits 1–9 are clues and dots or zeros represent blanks. For example, saving the following content in puzzle.txt:
and then invoking
python sudoku.py --read puzzle.txt
will load and display that exact board, ready for interactive stepping.
Underneath the hood, the choice to deep-copy the board on each step and to represent every logical deduction as an Action ensures that the interactive undo mechanism remains simple and robust. Each Action object records the row, column, digit, and description, letting the display code highlight precisely which cell was changed and why. Meanwhile, isolating each human-style solving technique in its own class adhering to IStrategy makes it trivial to add new patterns: one need only write another subclass with an apply(board) method and insert it into the ALL_STRATS registry in the desired priority order. This commitment to single responsibility and the Strategy and Memento patterns turns sudoku.py into not just a working solver and generator, but a living demonstration of how clear architectural boundaries can produce both maintainable code and an educational tool for understanding Sudoku logic.
Here is the full source code:
#!/usr/bin/env python3
import sys
import random
import copy
from itertools import combinations
from abc import ABC, abstractmethod
# -------------------------
# Core Data Structures
# -------------------------
class Cell:
def __init__(self, value=None):
self.value = value
# If unsolved, candidates are 1–9; else empty
self.candidates = set() if value else set(range(1, 10))
def is_solved(self):
return self.value is not None
def __repr__(self):
return str(self.value) if self.value else "."
class Board:
def __init__(self, grid=None):
# 9×9 of Cells
self.cells = [[Cell() for _ in range(9)] for _ in range(9)]
if grid:
for r in range(9):
for c in range(9):
v = grid[r][c]
if v:
self.set_value(r, c, v)
def get_row(self, r):
return self.cells[r]
def get_col(self, c):
return [self.cells[r][c] for r in range(9)]
def get_box(self, br, bc):
return [ self.cells[br*3 + dr][bc*3 + dc]
for dr in range(3) for dc in range(3) ]
def peers(self, r, c):
s = set(self.get_row(r) + self.get_col(c) + self.get_box(r//3, c//3))
s.discard(self.cells[r][c])
return s
def set_value(self, r, c, v):
cell = self.cells[r][c]
cell.value = v
cell.candidates.clear()
for p in self.peers(r, c):
p.candidates.discard(v)
def eliminate_candidate(self, r, c, v):
cell = self.cells[r][c]
if not cell.is_solved() and v in cell.candidates:
cell.candidates.discard(v)
# Auto-assign if only one candidate left
if len(cell.candidates) == 1:
only = next(iter(cell.candidates))
self.set_value(r, c, only)
# -------------------------
# Actions & History
# -------------------------
class Action:
def __init__(self, row, col, value, description, is_assignment=True):
self.row = row
self.col = col
self.value = value
self.description = description
self.is_assignment = is_assignment
self.strategy = None # will be set by solver
def __repr__(self):
t = "Assign" if self.is_assignment else "Eliminate"
return f"{t}({self.row},{self.col})={self.value} [{self.strategy}]: {self.description}"
class HistoryEntry:
def __init__(self, board_state, action):
self.board_state = board_state
self.action = action
# -------------------------
# Strategy Interface
# -------------------------
class IStrategy(ABC):
@abstractmethod
def apply(self, board):
"""Return list of Action."""
pass
def all_units(board):
units = []
for i in range(9):
units.append(board.get_row(i))
units.append(board.get_col(i))
for br in range(3):
for bc in range(3):
units.append(board.get_box(br, bc))
return units
# -------------------------
# Strategies
# -------------------------
class NakedSingleStrategy(IStrategy):
def apply(self, board):
actions = []
for r in range(9):
for c in range(9):
cell = board.cells[r][c]
if not cell.is_solved() and len(cell.candidates) == 1:
v = next(iter(cell.candidates))
actions.append(Action(r, c, v, f"Naked Single at {r,c}", True))
return actions
class HiddenSingleStrategy(IStrategy):
def apply(self, board):
actions = []
for unit in all_units(board):
count = {}
for cell in unit:
if not cell.is_solved():
for v in cell.candidates:
count[v] = count.get(v, 0) + 1
for v, cnt in count.items():
if cnt == 1:
# find the unique cell for v
for r in range(9):
for c in range(9):
if board.cells[r][c] in unit and not board.cells[r][c].is_solved() and v in board.cells[r][c].candidates:
actions.append(Action(r, c, v, f"Hidden Single at {r,c}", True))
return actions
class NakedPairStrategy(IStrategy):
def apply(self, board):
actions = []
for unit in all_units(board):
pairs = {}
for idx, cell in enumerate(unit):
if not cell.is_solved() and len(cell.candidates) == 2:
key = frozenset(cell.candidates)
pairs.setdefault(key, []).append(idx)
for candset, idxs in pairs.items():
if len(idxs) == 2:
for idx2, cell2 in enumerate(unit):
if idx2 not in idxs and not cell2.is_solved():
for v in candset:
if v in cell2.candidates:
# locate row,col
for r in range(9):
for c in range(9):
if board.cells[r][c] is cell2:
A = Action(r, c, v, f"Naked Pair {sorted(candset)} remove {v} at {r,c}", False)
actions.append(A)
return actions
class HiddenPairStrategy(IStrategy):
def apply(self, board):
actions = []
for unit in all_units(board):
pos = {}
for idx, cell in enumerate(unit):
if not cell.is_solved():
for v in cell.candidates:
pos.setdefault(v, []).append(idx)
for (v1, p1), (v2, p2) in combinations(pos.items(), 2):
if set(p1) == set(p2) and len(p1) == 2:
for idx in p1:
cell = unit[idx]
extras = cell.candidates - {v1, v2}
for e in extras:
for r in range(9):
for c in range(9):
if board.cells[r][c] is cell:
actions.append(Action(r, c, e, f"Hidden Pair {(v1,v2)} remove {e} at {r,c}", False))
return actions
class NakedTripleStrategy(IStrategy):
def apply(self, board):
actions = []
for unit in all_units(board):
infos = [(i, cell) for i, cell in enumerate(unit) if not cell.is_solved() and 2 <= len(cell.candidates) <= 3]
for combo in combinations(infos, 3):
idxs, cells = zip(*combo)
union = set().union(*[c.candidates for c in cells])
if len(union) == 3:
for idx2, cell2 in enumerate(unit):
if idx2 not in idxs and not cell2.is_solved():
for v in union:
if v in cell2.candidates:
for r in range(9):
for c in range(9):
if board.cells[r][c] is cell2:
actions.append(Action(r, c, v, f"Naked Triple {sorted(union)} remove {v} at {r,c}", False))
return actions
class HiddenTripleStrategy(IStrategy):
def apply(self, board):
actions = []
for unit in all_units(board):
pos = {}
for idx, cell in enumerate(unit):
if not cell.is_solved():
for v in cell.candidates:
pos.setdefault(v, []).append(idx)
for trio in combinations(pos.keys(), 3):
pu = set(pos[trio[0]]) | set(pos[trio[1]]) | set(pos[trio[2]])
if len(pu) == 3:
for idx in pu:
cell = unit[idx]
extras = cell.candidates - set(trio)
for e in extras:
for r in range(9):
for c in range(9):
if board.cells[r][c] is cell:
actions.append(Action(r, c, e, f"Hidden Triple {trio} remove {e} at {r,c}", False))
return actions
class NakedQuadStrategy(IStrategy):
def apply(self, board):
actions = []
for unit in all_units(board):
infos = [(i, cell) for i, cell in enumerate(unit) if not cell.is_solved() and 2 <= len(cell.candidates) <= 4]
for combo in combinations(infos, 4):
idxs, cells = zip(*combo)
union = set().union(*[c.candidates for c in cells])
if len(union) == 4:
for idx2, cell2 in enumerate(unit):
if idx2 not in idxs and not cell2.is_solved():
for v in union:
if v in cell2.candidates:
for r in range(9):
for c in range(9):
if board.cells[r][c] is cell2:
actions.append(Action(r, c, v, f"Naked Quad {sorted(union)} remove {v} at {r,c}", False))
return actions
class HiddenQuadStrategy(IStrategy):
def apply(self, board):
actions = []
for unit in all_units(board):
pos = {}
for idx, cell in enumerate(unit):
if not cell.is_solved():
for v in cell.candidates:
pos.setdefault(v, []).append(idx)
for quad in combinations(pos.keys(), 4):
pu = set().union(*[set(pos[v]) for v in quad])
if len(pu) == 4:
for idx in pu:
cell = unit[idx]
extras = cell.candidates - set(quad)
for e in extras:
for r in range(9):
for c in range(9):
if board.cells[r][c] is cell:
actions.append(Action(r, c, e, f"Hidden Quad {quad} remove {e} at {r,c}", False))
return actions
class PointingPairStrategy(IStrategy):
def apply(self, board):
actions = []
for br in range(3):
for bc in range(3):
box = board.get_box(br, bc)
pos = {}
for idx, cell in enumerate(box):
if not cell.is_solved():
for v in cell.candidates:
pos.setdefault(v, []).append(idx)
for v, locs in pos.items():
if len(locs) > 1:
rows = {i//3 for i in locs}
cols = {i%3 for i in locs}
if len(rows) == 1:
row = br*3 + rows.pop()
for c in range(9):
if c//3 != bc:
cell2 = board.cells[row][c]
if not cell2.is_solved() and v in cell2.candidates:
actions.append(Action(row, c, v, f"Pointing {v} remove at {(row,c)}", False))
if len(cols) == 1:
col = bc*3 + cols.pop()
for r in range(9):
if r//3 != br:
cell2 = board.cells[r][col]
if not cell2.is_solved() and v in cell2.candidates:
actions.append(Action(r, col, v, f"Pointing {v} remove at {(r,col)}", False))
return actions
class ClaimingPairStrategy(IStrategy):
def apply(self, board):
actions = []
# row-claiming
for r in range(9):
pos = {}
for c, cell in enumerate(board.get_row(r)):
if not cell.is_solved():
for v in cell.candidates:
pos.setdefault(v, []).append(c)
for v, cols in pos.items():
if len(cols) > 1 and len({c//3 for c in cols}) == 1:
bc = cols[0]//3; br = r//3
for rr in range(br*3, br*3+3):
if rr != r:
for cc in range(bc*3, bc*3+3):
cell2 = board.cells[rr][cc]
if not cell2.is_solved() and v in cell2.candidates:
actions.append(Action(rr, cc, v, f"Claiming {v} remove at {(rr,cc)}", False))
# col-claiming
for c in range(9):
pos = {}
for r, cell in enumerate(board.get_col(c)):
if not cell.is_solved():
for v in cell.candidates:
pos.setdefault(v, []).append(r)
for v, rows in pos.items():
if len(rows) > 1 and len({r//3 for r in rows}) == 1:
br = rows[0]//3; bc = c//3
for rr in range(br*3, br*3+3):
for cc in range(bc*3, bc*3+3):
if cc != c:
cell2 = board.cells[rr][cc]
if not cell2.is_solved() and v in cell2.candidates:
actions.append(Action(rr, cc, v, f"Claiming {v} remove at {(rr,cc)}", False))
return actions
class XWingStrategy(IStrategy):
def apply(self, board):
actions = []
for v in range(1,10):
rows = {r:[c for c,cell in enumerate(board.get_row(r))
if not cell.is_solved() and v in cell.candidates]
for r in range(9)}
filt = {r:cols for r,cols in rows.items() if len(cols) == 2}
for (r1,c1),(r2,c2) in combinations(filt.items(), 2):
if c1 == c2:
for r in set(range(9)) - {r1, r2}:
for c in c1:
cell = board.cells[r][c]
if not cell.is_solved() and v in cell.candidates:
actions.append(Action(r, c, v, f"X-Wing {v} remove at {(r,c)}", False))
return actions
class SwordfishStrategy(IStrategy):
def apply(self, board):
actions = []
for v in range(1,10):
rows = {r:[c for c,cell in enumerate(board.get_row(r))
if not cell.is_solved() and v in cell.candidates]
for r in range(9)}
filt = {r:cols for r,cols in rows.items() if 2 <= len(cols) <= 3}
for trio in combinations(filt, 3):
uc = set().union(*(filt[r] for r in trio))
if len(uc) == 3:
for r in set(range(9)) - set(trio):
for c in uc:
cell = board.cells[r][c]
if not cell.is_solved() and v in cell.candidates:
actions.append(Action(r, c, v, f"Swordfish {v} remove at {(r,c)}", False))
return actions
class XYWingStrategy(IStrategy):
def apply(self, board):
actions = []
# pivot with exactly 2 candidates {x,y}
for pr in range(9):
for pc in range(9):
pivot = board.cells[pr][pc]
if not pivot.is_solved() and len(pivot.candidates) == 2:
x, y = pivot.candidates
peers_pivot = board.peers(pr, pc)
# find wing1 {x,z} and wing2 {y,z}
wing1 = [(r,c,board.cells[r][c]) for (r,c) in [(rr,cc) for rr in range(9) for cc in range(9)]
if board.cells[r][c] in peers_pivot and not board.cells[r][c].is_solved() and
len(board.cells[r][c].candidates) == 2 and x in board.cells[r][c].candidates and y not in board.cells[r][c].candidates]
wing2 = [(r,c,board.cells[r][c]) for (r,c) in [(rr,cc) for rr in range(9) for cc in range(9)]
if board.cells[r][c] in peers_pivot and not board.cells[r][c].is_solved() and
len(board.cells[r][c].candidates) == 2 and y in board.cells[r][c].candidates and x not in board.cells[r][c].candidates]
for r1,c1,w1 in wing1:
for r2,c2,w2 in wing2:
if (r1,c1) != (r2,c2):
# z is the other candidate
z = (w1.candidates | w2.candidates) - {x,y}
if len(z) == 1:
z = z.pop()
# eliminate z from cells that see both wing1 and wing2
common = board.peers(r1, c1) & board.peers(r2, c2)
for cell in common:
if not cell.is_solved() and z in cell.candidates:
for rr in range(9):
for cc in range(9):
if board.cells[rr][cc] is cell:
actions.append(Action(rr, cc, z, f"XY-Wing remove {z} at {(rr,cc)}", False))
return actions
class UniqueRectangleStrategy(IStrategy):
def apply(self, board):
actions = []
# Type 1 UR only: look for 4 corners of a rectangle with exactly two common candidates in all four
for r1 in range(8):
for r2 in range(r1+1, 9):
for c1 in range(8):
for c2 in range(c1+1, 9):
corners = [board.cells[r][c] for r in (r1,r2) for c in (c1,c2)]
unsolved = [c for c in corners if not c.is_solved()]
if len(unsolved) == 4:
common = set.intersection(*(c.candidates for c in unsolved))
if len(common) == 2:
# if exactly two are bi-value corners and the other two have extra
bivals = [c for c in unsolved if len(c.candidates) == 2]
if len(bivals) == 2:
# eliminate the common pair from the other two corners
for c in unsolved:
if len(c.candidates) > 2:
extras = c.candidates - common
for e in extras:
for rr in (r1, r2):
for cc in (c1, c2):
if board.cells[rr][cc] is c:
actions.append(Action(rr, cc, e, f"UR Type1 remove {e} at {(rr,cc)}", False))
return actions
# -------------------------
# Registry & Solver
# -------------------------
class StrategyRegistry:
def __init__(self, strategies):
self.strategies = strategies
def __iter__(self):
return iter(self.strategies)
class SolverController:
def __init__(self, board, registry):
self.board = board
self.registry = registry
self.history = []
def next_step(self):
for strat in self.registry:
actions = strat.apply(self.board)
if actions:
act = actions[0]
act.strategy = strat.__class__.__name__
prev = copy.deepcopy(self.board)
self.history.append(HistoryEntry(prev, act))
if act.is_assignment:
self.board.set_value(act.row, act.col, act.value)
else:
self.board.eliminate_candidate(act.row, act.col, act.value)
return act
return None
def prev_step(self):
if not self.history:
return None
entry = self.history.pop()
self.board = entry.board_state
return entry.action
# Instantiate strategy list in desired priority order
ALL_STRATS = [
NakedSingleStrategy(), HiddenSingleStrategy(),
NakedPairStrategy(), HiddenPairStrategy(),
PointingPairStrategy(), ClaimingPairStrategy(),
NakedTripleStrategy(), HiddenTripleStrategy(),
NakedQuadStrategy(), HiddenQuadStrategy(),
XWingStrategy(), SwordfishStrategy(),
XYWingStrategy(), UniqueRectangleStrategy(),
]
STRATEGY_REGISTRY = StrategyRegistry(ALL_STRATS)
# -------------------------
# ASCII Renderer
# -------------------------
class AsciiRenderer:
RED = "\033[31m"
RESET = "\033[0m"
@staticmethod
def render(board, highlight=None):
for i, row in enumerate(board.cells):
if i % 3 == 0:
print("+-------+-------+-------+")
line = "| "
for j, cell in enumerate(row):
v = repr(cell)
if highlight and (i, j) == (highlight.row, highlight.col):
v = f"{AsciiRenderer.RED}{v}{AsciiRenderer.RESET}"
line += v + " "
if (j + 1) % 3 == 0:
line += "| "
print(line)
print("+-------+-------+-------+")
# -------------------------
# Puzzle Generator
# -------------------------
def fill_grid(grid):
for i in range(9):
for j in range(9):
if grid[i][j] is None:
vals = list(range(1,10))
random.shuffle(vals)
for v in vals:
if all(grid[i][c]!=v for c in range(9)) and \
all(grid[r][j]!=v for r in range(9)) and \
all(grid[bi*3+ri][bj*3+rj]!=v
for bi in range(3) for bj in range(3)
for ri in range(3) for rj in range(3)
if bi*3+ri!=i or bj*3+rj!=j):
grid[i][j] = v
if fill_grid(grid):
return True
grid[i][j] = None
return False
return True
def generate_full_solution():
grid = [[None]*9 for _ in range(9)]
fill_grid(grid)
return grid
# Which strategies are allowed at each difficulty
DIFFICULTY_ALLOWED = {
"easy": {"NakedSingleStrategy", "HiddenSingleStrategy"},
"medium": {"NakedSingleStrategy", "HiddenSingleStrategy",
"NakedPairStrategy", "HiddenPairStrategy",
"PointingPairStrategy", "ClaimingPairStrategy"},
"hard": {"NakedSingleStrategy", "HiddenSingleStrategy",
"NakedPairStrategy", "HiddenPairStrategy",
"PointingPairStrategy", "ClaimingPairStrategy",
"XWingStrategy", "NakedTripleStrategy",
"HiddenTripleStrategy", "NakedQuadStrategy",
"HiddenQuadStrategy"},
"expert": {s.__class__.__name__ for s in ALL_STRATS},
}
def analyze_puzzle(grid, allowed):
b = Board(grid)
ctl = SolverController(b, STRATEGY_REGISTRY)
used = set()
while True:
a = ctl.next_step()
if not a:
break
used.add(a.strategy)
if all(cell.is_solved() for row in b.cells for cell in row):
return used
return None
def generate_puzzle(difficulty="easy"):
full = generate_full_solution()
puzzle = copy.deepcopy(full)
positions = [(r,c) for r in range(9) for c in range(9)]
random.shuffle(positions)
for r,c in positions:
backup = puzzle[r][c]
puzzle[r][c] = None
used = analyze_puzzle(puzzle, DIFFICULTY_ALLOWED[difficulty])
if used is None or not used.issubset(DIFFICULTY_ALLOWED[difficulty]):
puzzle[r][c] = backup
return puzzle
# -------------------------
# Interactive Loop & Main
# -------------------------
def interactive_loop(ctl):
while True:
cmd = input("[n]ext, [p]rev, [q]uit: ").strip().lower()
if cmd == "n":
a = ctl.next_step()
if a:
print("Applied:", a)
AsciiRenderer.render(ctl.board, highlight=a)
else:
print("No more logical steps.")
elif cmd == "p":
a = ctl.prev_step()
if a:
print("Reverted:", a)
AsciiRenderer.render(ctl.board)
else:
print("At initial state.")
elif cmd == "q":
break
else:
print("Unknown command.")
def main():
import argparse
p = argparse.ArgumentParser(description="Sudoku Solver & Generator")
p.add_argument("-g","--generate", choices=["easy","medium","hard","expert"])
args = p.parse_args()
if args.generate:
puz = generate_puzzle(args.generate)
board = Board(puz)
print(f"{args.generate.capitalize()} puzzle:")
AsciiRenderer.render(board)
ctl = SolverController(board, STRATEGY_REGISTRY)
interactive_loop(ctl)
else:
print("Usage: sudoku.py --generate <easy|medium|hard|expert>")
if __name__ == "__main__":
main()
No comments:
Post a Comment