Source code for dedoku.cli

"""Command-line interface.

Run ``dedoku`` (or ``python -m dedoku``) with an 81-character puzzle to
solve it with pure logic. With ``--explain`` every deduction is printed in
order, showing exactly how each cell was filled and which candidates were
eliminated along the way.

Exit codes: ``0`` solved, ``1`` not fully solvable by the logical
pipeline, ``2`` invalid input.
"""

from __future__ import annotations

import argparse
import sys
from collections.abc import Sequence

from . import __version__, solve
from .exceptions import SudokuError

__all__ = ["build_parser", "main"]


[docs] def build_parser() -> argparse.ArgumentParser: """Build the argument parser for the ``dedoku`` command. :returns: The configured parser. :rtype: argparse.ArgumentParser """ parser = argparse.ArgumentParser( prog="dedoku", description=( "Solve a Sudoku with human-style logical deduction - " "no backtracking, no guessing." ), ) parser.add_argument( "puzzle", nargs="?", help=( "81-character puzzle, digits 1-9 for givens and 0 or . for " "blanks; omitted, the puzzle is read from standard input" ), ) parser.add_argument( "-e", "--explain", action="store_true", help="print every deduction in order before the final board", ) parser.add_argument( "--method", choices=("logic", "hybrid", "backtracking"), default="logic", help=( "logic (default): explainable techniques only, may stall on " "the hardest puzzles; hybrid: techniques first, brute force " "completes any remainder; backtracking: brute force directly" ), ) parser.add_argument( "--multi", action="store_true", help=( "do not assume the puzzle has a unique solution " "(disables uniqueness-based techniques)" ), ) parser.add_argument( "--version", action="version", version=f"dedoku {__version__}", ) return parser
def _describe(step_number: int, technique: str, description: str, what: str) -> str: """Format one line of the ``--explain`` log. :param step_number: One-based step counter. :type step_number: int :param technique: The technique name. :type technique: str :param description: The step's human-readable explanation. :type description: str :param what: Compact summary of the effect (placement or removals). :type what: str :returns: The formatted line. :rtype: str """ return f"{step_number:4d}. {what:<22} [{technique}] {description}"
[docs] def main(argv: Sequence[str] | None = None) -> int: """Run the command-line interface. :param argv: Argument list; defaults to ``sys.argv[1:]``. :type argv: Sequence[str] | None :returns: The process exit code. :rtype: int """ args = build_parser().parse_args(argv) text = args.puzzle if args.puzzle is not None else sys.stdin.read() try: result = solve( text, method=args.method, assume_unique=not args.multi ) except SudokuError as exc: print(f"error: {exc}", file=sys.stderr) return 2 if args.explain: for number, step in enumerate(result.steps, start=1): if step.placements: what = ", ".join( f"R{p.row + 1}C{p.column + 1} = {p.digit}" for p in step.placements ) else: count = len(step.eliminations) plural = "s" if count != 1 else "" what = f"removes {count} candidate{plural}" print(_describe(number, step.technique, step.description, what)) print() print(result.grid) if result.solved: techniques = ", ".join(result.techniques_used) print(f"\nSolved in {len(result.steps)} steps using: {techniques}") return 0 print( f"\nNot fully solvable by pure logic: {len(result.steps)} steps " f"applied, then no technique makes further progress." ) return 1