def test_random_initial_cells(): box_size = BoxSize(3, 3) cells = generators._random_initial_cells(box_size) assert len(cells) == 15 sudoku = Sudoku(*cells, box_size=box_size) assert sudoku.is_valid() is True assert sudoku.is_solved() is False
def steps(sudoku: Sudoku) -> Iterator[Step]: _sudoku = Sudoku(*sudoku.cells(), box_size=sudoku.box_size) all_techniques: Tuple[Type[Technique], ...] = ( techniques.LoneSingle, techniques.HiddenSingle, techniques.NakedPair, techniques.NakedTriplet, techniques.LockedCandidate, techniques.XYWing, techniques.UniqueRectangle, ) for step in techniques.BulkPencilMarking(_sudoku): _sudoku.update(step.changes) yield step while not _sudoku.is_solved(): for technique in all_techniques: try: step = technique(_sudoku).first() except techniques.NotFound: continue else: _sudoku.update(step.changes) yield step break else: raise exceptions.Unsolvable
def _get_correct_cells(self) -> List[Cell]: sudoku = Sudoku(*[c for c in self.sudoku.cells() if c.value], box_size=self.sudoku.box_size) all_techniques = ( BulkPencilMarking, techniques.NakedPair, techniques.NakedTriplet, techniques.LockedCandidate, techniques.XYWing, techniques.UniqueRectangle, ) for technique in all_techniques: for result in technique(sudoku): sudoku.update(result.changes) return [cell for cell in sudoku.cells() if cell.candidates]
def __call__(self, sudoku: Sudoku) -> str: rows = [[self.render_cell(cell, sudoku.box_size) for cell in row] for row in sudoku.rows()] template = self.border.template(sudoku.box_size) return template.format( *[self.render_row(row, sudoku.box_size) for row in rows])
def test_steps(): given = Sudoku.from_list( [ [0, 0, 0, 0, 9, 0, 1, 0, 0], [0, 0, 0, 0, 0, 2, 3, 0, 0], [0, 0, 7, 0, 0, 1, 8, 2, 5], [6, 0, 4, 0, 3, 8, 9, 0, 0], [8, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 9, 0, 0, 0, 0, 0, 8], [1, 7, 0, 0, 0, 0, 6, 0, 0], [9, 0, 0, 0, 1, 0, 7, 4, 3], [4, 0, 3, 0, 6, 0, 0, 0, 1], ], box_size=BoxSize(3, 3), ) assert [step.combination.name for step in solvers.steps(given)] == [ "Bulk Pencil Marking", *["Lone Single"] * 8, *["Hidden Single"] * 7, "Lone Single", "Hidden Single", *["Naked Pair"] * 3, "Locked Candidate", "XY Wing", *["Hidden Single"] * 2, "Unique Rectangle", "Hidden Single", *["Lone Single"] * 2, "Hidden Single", *["Lone Single"] * 28, ]
def test_eliminate(): given = Sudoku.from_list( [ [0, 0, 0, 0, 9, 0, 1, 0, 0], [0, 0, 0, 0, 0, 2, 3, 0, 0], [0, 0, 7, 0, 0, 1, 8, 2, 5], [6, 0, 4, 0, 3, 8, 9, 0, 0], [8, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 9, 0, 0, 0, 0, 0, 8], [1, 7, 0, 0, 0, 0, 6, 0, 0], [9, 0, 0, 0, 1, 0, 7, 4, 3], [4, 0, 3, 0, 6, 0, 0, 0, 1], ], box_size=BoxSize(3, 3), ) solution = solvers.eliminate(given) assert str(solution) == ("200593100" "501002300" "397641825" "604038900" "810000036" "739006008" "170304600" "900015743" "403060001")
def test_backtrack(): given = Sudoku.from_list( [ [0, 0, 0, 8, 0, 1, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 4, 3], [5, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 7, 0, 8, 0, 0], [0, 0, 0, 0, 0, 0, 1, 0, 0], [0, 2, 0, 0, 3, 0, 0, 0, 0], [6, 0, 0, 0, 0, 0, 0, 7, 5], [0, 0, 3, 4, 0, 0, 0, 0, 0], [0, 0, 0, 2, 0, 0, 6, 0, 0], ], box_size=BoxSize(3, 3), ) solution = solvers.backtrack(given) assert solution.is_solved() is True assert solution.is_valid() is True assert str(solution) == ("237841569" "186795243" "594326718" "315674892" "469582137" "728139456" "642918375" "853467921" "971253684")
def get_object(self): queryset = self.filter_queryset(self.get_queryset()) instance = queryset.random() if not instance: raise exceptions.NotFound return SudokuGrid.from_string( instance.puzzle, box_size=BoxSize(instance.box_width, instance.box_length), )
def test_plain(): expected = ( "┌─────────┬─────────┬─────────╥─────────┬─────────┬─────────╥─────────┬─────────┬─────────┐\n" # noqa: E501 "│ 2 3 │ 2 3 │ 2 ║ 3 │ │ 3 ║ │ │ │\n" # noqa: E501 "│ 5 │ 4 5 6 │ 5 6 ║ 4 5 6 │ 9 │ 4 5 6 ║ 1 │ 6 │ 4 6 │\n" # noqa: E501 "│ │ 8 │ 8 ║ 7 8 │ │ 7 ║ │ 7 │ 7 │\n" # noqa: E501 "├─────────┼─────────┼─────────╫─────────┼─────────┼─────────╫─────────┼─────────┼─────────┤\n" # noqa: E501 "│ │ │ 1 ║ │ │ ║ │ │ │\n" # noqa: E501 "│ 5 │ 4 5 6 │ 5 6 ║ 4 5 6 │ 4 5 │ 2 ║ 3 │ 6 │ 4 6 │\n" # noqa: E501 "│ │ 8 9 │ 8 ║ 7 8 │ 7 8 │ ║ │ 7 9 │ 7 9 │\n" # noqa: E501 "├─────────┼─────────┼─────────╫─────────┼─────────┼─────────╫─────────┼─────────┼─────────┤\n" # noqa: E501 "│ 3 │ 3 │ ║ 3 │ │ ║ │ │ │\n" # noqa: E501 "│ │ 4 6 │ 7 ║ 4 6 │ 4 │ 1 ║ 8 │ 2 │ 5 │\n" # noqa: E501 "│ │ 9 │ ║ │ │ ║ │ │ │\n" # noqa: E501 "╞═════════╪═════════╪═════════╬═════════╪═════════╪═════════╬═════════╪═════════╪═════════╡\n" # noqa: E501 "│ │ 2 │ ║ 1 2 │ │ ║ │ 1 │ 2 │\n" # noqa: E501 "│ 6 │ 5 │ 4 ║ 5 │ 3 │ 8 ║ 9 │ 5 │ │\n" # noqa: E501 "│ │ │ ║ 7 │ │ ║ │ 7 │ 7 │\n" # noqa: E501 "├─────────┼─────────┼─────────╫─────────┼─────────┼─────────╫─────────┼─────────┼─────────┤\n" # noqa: E501 "│ │ │ 2 ║ 2 │ 2 │ ║ 2 │ 3 │ 2 │\n" # noqa: E501 "│ 8 │ 1 │ 5 ║ 4 5 6 │ 4 5 │ 4 5 6 ║ 4 5 │ 5 6 │ 4 6 │\n" # noqa: E501 "│ │ │ ║ 7 9 │ 7 │ 7 9 ║ │ 7 │ 7 │\n" # noqa: E501 "├─────────┼─────────┼─────────╫─────────┼─────────┼─────────╫─────────┼─────────┼─────────┤\n" # noqa: E501 "│ 2 3 │ 2 3 │ ║ 1 2 │ 2 │ ║ 2 │ 1 3 │ │\n" # noqa: E501 "│ 5 │ 5 │ 9 ║ 4 5 6 │ 4 5 │ 4 5 6 ║ 4 5 │ 5 6 │ 8 │\n" # noqa: E501 "│ 7 │ │ ║ 7 │ 7 │ 7 ║ │ 7 │ │\n" # noqa: E501 "╞═════════╪═════════╪═════════╬═════════╪═════════╪═════════╬═════════╪═════════╪═════════╡\n" # noqa: E501 "│ │ │ 2 ║ 2 3 │ 2 │ 3 ║ │ │ 2 │\n" # noqa: E501 "│ 1 │ 7 │ 5 ║ 4 5 │ 4 5 │ 4 5 ║ 6 │ 5 │ │\n" # noqa: E501 "│ │ │ 8 ║ 8 9 │ 8 │ 9 ║ │ 8 9 │ 9 │\n" # noqa: E501 "├─────────┼─────────┼─────────╫─────────┼─────────┼─────────╫─────────┼─────────┼─────────┤\n" # noqa: E501 "│ │ 2 │ 2 ║ 2 │ │ ║ │ │ │\n" # noqa: E501 "│ 9 │ 5 6 │ 5 6 ║ 5 │ 1 │ 5 ║ 7 │ 4 │ 3 │\n" # noqa: E501 "│ │ 8 │ 8 ║ 8 │ │ ║ │ │ │\n" # noqa: E501 "├─────────┼─────────┼─────────╫─────────┼─────────┼─────────╫─────────┼─────────┼─────────┤\n" # noqa: E501 "│ │ 2 │ ║ 2 │ │ ║ 2 │ │ │\n" # noqa: E501 "│ 4 │ 5 │ 3 ║ 5 │ 6 │ 5 ║ 5 │ 5 │ 1 │\n" # noqa: E501 "│ │ 8 │ ║ 7 8 9 │ │ 7 9 ║ │ 8 9 │ │\n" # noqa: E501 "└─────────┴─────────┴─────────╨─────────┴─────────┴─────────╨─────────┴─────────┴─────────┘" # noqa: E501 ) sudoku = Sudoku.from_list( [ [0, 0, 0, 0, 9, 0, 1, 0, 0], [0, 0, 0, 0, 0, 2, 3, 0, 0], [0, 0, 7, 0, 0, 1, 8, 2, 5], [6, 0, 4, 0, 3, 8, 9, 0, 0], [8, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 9, 0, 0, 0, 0, 0, 8], [1, 7, 0, 0, 0, 0, 6, 0, 0], [9, 0, 0, 0, 1, 0, 7, 4, 3], [4, 0, 3, 0, 6, 0, 0, 0, 1], ], box_size=BoxSize(3, 3), ) sudoku.update(techniques.BulkPencilMarking(sudoku).first().changes) assert renderers.plain(sudoku) == expected
def test_sudoku(): sudoku = Sudoku( Cell(position=Position(0, 0, 0), value=2), Cell(position=Position(0, 1, 0), candidates=set()), box_size=BoxSize(3, 3), ) assert sudoku[0, 0].value == 2 assert sudoku[0, 1].candidates == set() assert sudoku[0, 2].value is None assert len(sudoku[0, 2].candidates) == 0
def create(self, validated_data): return Sudoku( *[ Cell( position=Position(*cell["position"]), value=cell["value"], candidates=set(cell["candidates"]), ) for cell in validated_data["cells"] ], box_size=BoxSize(*validated_data["box_size"]), )
def test_rank_sudoku_with_multiple_solutions(): puzzle = [ [8, 1, 0, 0, 0, 0, 6, 7, 9], [0, 0, 0, 6, 7, 9, 0, 2, 0], [0, 0, 0, 1, 2, 8, 3, 0, 0], [0, 3, 4, 0, 5, 7, 0, 0, 0], [2, 0, 0, 0, 0, 0, 7, 0, 4], [0, 0, 0, 0, 0, 6, 0, 0, 0], [0, 0, 3, 7, 0, 1, 0, 6, 2], [0, 0, 0, 0, 0, 0, 4, 0, 0], [0, 0, 1, 0, 3, 0, 0, 8, 0], ] sudoku = Sudoku.from_list(puzzle, box_size=BoxSize(3, 3)) with pytest.raises(exceptions.MultipleSolutions): stats.rank(sudoku)
def sudoku(): return Sudoku.from_list( [ [0, 0, 7, 0, 3, 0, 8, 0, 0], [0, 0, 0, 2, 0, 5, 0, 0, 0], [4, 0, 0, 9, 0, 6, 0, 0, 1], [0, 4, 3, 0, 0, 0, 2, 1, 0], [1, 0, 0, 0, 0, 0, 0, 0, 5], [0, 5, 8, 0, 0, 0, 6, 7, 0], [5, 0, 0, 1, 0, 8, 0, 0, 9], [0, 0, 0, 5, 0, 3, 0, 0, 0], [0, 0, 2, 0, 9, 0, 5, 0, 0], ], box_size=BoxSize(3, 3), )
def test_from_string(sudoku_12x12): sudoku = Sudoku.from_string( "300974B1068C" "800692C0B430" "00040365020A" "C561BA000923" "284A6073C0B5" "7B93C0504006" "A4072B309000" "0900000CA060" "031C08A9270B" "4000A0276019" "92A030100070" "060B0C000052", box_size=BoxSize(3, 4), ) assert list(sudoku.cells()) == list(sudoku_12x12.cells())
def test_colorful(): sudoku = Sudoku.from_list( [ [0, 0, 0, 0, 9, 0, 1, 0, 0], [0, 0, 0, 0, 0, 2, 3, 0, 0], [0, 0, 7, 0, 0, 1, 8, 2, 5], [6, 0, 4, 0, 3, 8, 9, 0, 0], [8, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 9, 0, 0, 0, 0, 0, 8], [1, 7, 0, 0, 0, 0, 6, 0, 0], [9, 0, 0, 0, 1, 0, 7, 4, 3], [4, 0, 3, 0, 6, 0, 0, 0, 1], ], box_size=BoxSize(3, 3), ) sudoku.update(techniques.BulkPencilMarking(sudoku).first().changes) assert "\033[93m2\033[0m" in renderers.colorful(sudoku)
def sudoku_12x12(): return Sudoku.from_list( [ [3, 0, 0, 9, 7, 4, 11, 1, 0, 6, 8, 12], [8, 0, 0, 6, 9, 2, 12, 0, 11, 4, 3, 0], [0, 0, 0, 4, 0, 3, 6, 5, 0, 2, 0, 10], [12, 5, 6, 1, 11, 10, 0, 0, 0, 9, 2, 3], [2, 8, 4, 10, 6, 0, 7, 3, 12, 0, 11, 5], [7, 11, 9, 3, 12, 0, 5, 0, 4, 0, 0, 6], [10, 4, 0, 7, 2, 11, 3, 0, 9, 0, 0, 0], [0, 9, 0, 0, 0, 0, 0, 12, 10, 0, 6, 0], [0, 3, 1, 12, 0, 8, 10, 9, 2, 7, 0, 11], [4, 0, 0, 0, 10, 0, 2, 7, 6, 0, 1, 9], [9, 2, 10, 0, 3, 0, 1, 0, 0, 0, 7, 0], [0, 6, 0, 11, 0, 12, 0, 0, 0, 0, 5, 2], ], box_size=BoxSize(3, 4), )
def test_steps_raises_unsolvable(): given = Sudoku.from_list( [ [7, 0, 0, 0, 8, 2, 5, 0, 0], [0, 5, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 9, 0, 7, 0, 2, 6], [0, 0, 8, 0, 9, 0, 0, 7, 5], [3, 0, 0, 6, 7, 5, 0, 0, 0], [0, 0, 0, 0, 2, 0, 0, 9, 0], [9, 0, 1, 0, 0, 3, 0, 0, 0], [0, 0, 0, 0, 6, 0, 0, 0, 3], [6, 0, 2, 0, 0, 0, 0, 0, 0], ], box_size=BoxSize(3, 3), ) with pytest.raises(exceptions.Unsolvable): list(solvers.steps(given))
def random_sudoku(avg_rank: int = 150, box_size: BoxSize = BoxSize(3, 3)) -> Sudoku: sudoku = Sudoku(*_random_initial_cells(box_size), box_size=box_size) solution = solvers.backtrack(sudoku) iterations = min(avg_rank, MAX_ITERATIONS) for i in range(iterations): size = random.randint(1, 2) rows = [random.randint(0, solution.size - 1) for _ in range(size)] columns = [random.randint(0, solution.size - 1) for _ in range(size)] cells = [solution[row, column] for (row, column) in zip(rows, columns)] if all(cell.value for cell in cells): solution.update([Cell(position=cell.position) for cell in cells]) try: stats.rank(solution) except exceptions.MultipleSolutions: solution.update(cells) return solution
def test_bulk_pencil_marking(): sudoku = Sudoku.from_list( [ [0, 0, 0, 0, 9, 0, 1, 0, 0], [0, 0, 0, 0, 0, 2, 3, 0, 0], [0, 0, 7, 0, 0, 1, 8, 2, 5], [6, 0, 4, 0, 3, 8, 9, 0, 0], [8, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 9, 0, 0, 0, 0, 0, 8], [1, 7, 0, 0, 0, 0, 6, 0, 0], [9, 0, 0, 0, 1, 0, 7, 4, 3], [4, 0, 3, 0, 6, 0, 0, 0, 1], ], box_size=BoxSize(3, 3), ) pencil_marks = techniques.BulkPencilMarking(sudoku).first() assert len(pencil_marks.changes) == 51 assert len(list(techniques.BulkPencilMarking(sudoku))) == 1
def test_pencil_marking(): sudoku = Sudoku.from_list( [ [0, 0, 0, 0, 9, 0, 1, 0, 0], [0, 0, 0, 0, 0, 2, 3, 0, 0], [0, 0, 7, 0, 0, 1, 8, 2, 5], [6, 0, 4, 0, 3, 8, 9, 0, 0], [8, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 9, 0, 0, 0, 0, 0, 8], [1, 7, 0, 0, 0, 0, 6, 0, 0], [9, 0, 0, 0, 1, 0, 7, 4, 3], [4, 0, 3, 0, 6, 0, 0, 0, 1], ], box_size=BoxSize(3, 3), ) pencil_marks = techniques.PencilMarking(sudoku).first() assert pencil_marks.changes == [ Cell(position=Position(0, 0, 0), candidates={2, 3, 5}) ]
def test_pencil_marking_all_board(): sudoku = Sudoku.from_list( [ [0, 0, 0, 0, 9, 0, 1, 0, 0], [0, 0, 0, 0, 0, 2, 3, 0, 0], [0, 0, 7, 0, 0, 1, 8, 2, 5], [6, 0, 4, 0, 3, 8, 9, 0, 0], [8, 1, 0, 0, 0, 0, 0, 0, 0], [0, 0, 9, 0, 0, 0, 0, 0, 8], [1, 7, 0, 0, 0, 0, 6, 0, 0], [9, 0, 0, 0, 1, 0, 7, 4, 3], [4, 0, 3, 0, 6, 0, 0, 0, 1], ], box_size=BoxSize(3, 3), ) for step in techniques.PencilMarking(sudoku): sudoku.update(step.changes) assert sudoku.is_valid()
def eliminate(sudoku: Sudoku) -> Sudoku: _sudoku = Sudoku(*sudoku.cells(), box_size=sudoku.box_size) all_techniques = ( techniques.LoneSingle, techniques.HiddenSingle, ) for step in techniques.BulkPencilMarking(_sudoku): _sudoku.update(step.changes) has_result = True while has_result: for technique in all_techniques: has_result = False for step in technique(_sudoku): _sudoku.update(step.changes) has_result = True return _sudoku
def step(sudoku: Sudoku, with_pencil_marking: bool = False) -> Step: all_techniques: Tuple[Type[Technique], ...] = ( techniques.LoneSingle, techniques.HiddenSingle, techniques.NakedPair, techniques.NakedTriplet, techniques.LockedCandidate, techniques.XYWing, techniques.UniqueRectangle, ) if with_pencil_marking: all_techniques = (BulkPencilMarking, PencilMarking) + all_techniques if sudoku.is_solved(): raise Solved for technique in all_techniques: try: return technique(sudoku).first() except techniques.NotFound: continue raise exceptions.Unsolvable
def rank(sudoku: Sudoku) -> int: total_solutions = 0 total_branch_factor = 0 def count(sudoku: Sudoku) -> None: nonlocal total_solutions nonlocal total_branch_factor _sudoku = solvers.eliminate(sudoku) cells = sorted( (cell for cell in _sudoku.cells() if not cell.value), key=operator.attrgetter("candidates"), ) for cell in cells: branch_factor = len(cell.candidates) for candidate in cell.candidates: _sudoku.update([Cell(position=cell.position, value=candidate)]) try: count(_sudoku) except (exceptions.InvalidSudoku, exceptions.NoCandidates): pass else: total_solutions += 1 if total_solutions > 1: raise exceptions.MultipleSolutions total_branch_factor += pow(branch_factor - 1, 2) _sudoku.update([cell]) else: raise exceptions.NoCandidates try: count(sudoku) except exceptions.NoCandidates: pass return (total_branch_factor * 100) + sum(1 for c in sudoku.cells() if not c.value)
def test_advanced_pencil_marking(): sudoku = Sudoku.from_list( [ [2, 0, 0, 5, 9, 3, 1, 0, 0], [5, 0, 1, 0, 0, 2, 3, 0, 0], [3, 9, 7, 6, 4, 1, 8, 2, 5], [6, 0, 4, 0, 3, 8, 9, 0, 0], [8, 1, 0, 0, 0, 0, 0, 3, 6], [7, 3, 9, 0, 0, 6, 0, 0, 8], [1, 7, 0, 3, 0, 4, 6, 0, 0], [9, 0, 0, 0, 1, 5, 7, 4, 3], [4, 0, 3, 0, 6, 0, 0, 0, 1], ], box_size=BoxSize(3, 3), ) sudoku.update(BulkPencilMarking(sudoku).first().changes) # there is a naked pair [7, 8] in the first row at (1, 3) and (1, 4) # replace {4, 6, 8} with {4, 8} sudoku[1, 1].candidates = {4, 8} pencil_marks = PencilMarking(sudoku).first() assert len(pencil_marks.changes) == 1 assert pencil_marks.changes[0].candidates == {4, 6} assert pencil_marks.combination.cells == []
def test_is_solved_false(puzzle, solved): sudoku = Sudoku.from_list(puzzle, box_size=BoxSize(3, 3)) assert sudoku.is_solved() is solved
def test_is_valid(puzzle, is_valid): sudoku = Sudoku.from_list(puzzle, box_size=BoxSize(3, 3)) assert sudoku.is_valid() is is_valid
def make_sudoku_with_marks(puzzle: List[List[int]], box_size: BoxSize) -> Sudoku: sudoku = Sudoku.from_list(puzzle, box_size=box_size) sudoku.update(techniques.BulkPencilMarking(sudoku).first().changes) return sudoku
def test_rank(puzzle, solutions, rank): sudoku = Sudoku.from_list(puzzle, box_size=BoxSize(3, 3)) assert stats.rank(sudoku) == rank