def test_unique_rectangle_block_apart(): sudoku = make_sudoku_with_marks( [ [9, 0, 1, 0, 3, 0, 5, 0, 0], [0, 0, 7, 8, 1, 9, 0, 0, 0], [0, 2, 0, 4, 5, 6, 1, 9, 7], [1, 0, 2, 9, 0, 0, 6, 0, 5], [0, 0, 0, 6, 7, 1, 0, 2, 4], [6, 0, 0, 3, 2, 5, 8, 0, 0], [0, 1, 5, 0, 9, 0, 4, 6, 3], [0, 0, 6, 5, 0, 0, 0, 0, 0], [0, 0, 0, 1, 6, 3, 0, 5, 0], ], box_size=BoxSize(3, 3), ) unique_rectangle = techniques.UniqueRectangle(sudoku).first() assert unique_rectangle.combination.cells == [ Cell(position=Position(0, 3, 1), candidates={2, 7}), Cell(position=Position(0, 5, 1), candidates={2, 7}), Cell(position=Position(6, 3, 7), candidates={2, 7}), Cell(position=Position(6, 5, 7), candidates={2, 7, 8}), ] assert unique_rectangle.changes == [ Cell(position=Position(6, 5, 7), candidates={8}) ]
def test_locked_candidate_in_a_row(): sudoku = make_sudoku_with_marks( [ [9, 0, 0, 5, 3, 1, 2, 8, 0], [0, 2, 5, 7, 0, 4, 9, 3, 0], [0, 3, 0, 0, 0, 2, 0, 4, 0], [4, 8, 1, 2, 5, 7, 6, 9, 3], [3, 5, 9, 0, 0, 8, 0, 0, 2], [7, 6, 2, 3, 1, 9, 8, 5, 4], [0, 1, 0, 0, 0, 0, 0, 6, 8], [6, 0, 8, 1, 0, 5, 3, 0, 0], [0, 0, 3, 8, 7, 6, 0, 0, 0], ], box_size=BoxSize(3, 3), ) locked_candidate = techniques.LockedCandidate(sudoku).first() assert locked_candidate.combination.cells == [ Cell(position=Position(6, 3, 7), candidates={4, 9}), Cell(position=Position(6, 4, 7), candidates={2, 4, 9}), ] assert locked_candidate.combination.values == [9] assert locked_candidate.changes == [ Cell(position=Position(7, 4, 7), candidates={2, 4}) ]
def test_naked_triplet(): sudoku = make_sudoku_with_marks( [ [9, 2, 6, 7, 3, 5, 4, 0, 0], [0, 0, 0, 6, 4, 9, 7, 2, 5], [7, 4, 5, 8, 2, 1, 9, 3, 6], [0, 1, 0, 0, 0, 8, 6, 4, 0], [5, 0, 4, 0, 0, 0, 8, 0, 0], [8, 6, 9, 1, 7, 4, 3, 5, 2], [0, 0, 0, 0, 8, 0, 0, 0, 0], [0, 0, 0, 9, 1, 0, 0, 6, 0], [0, 0, 0, 4, 0, 3, 1, 0, 0], ], box_size=BoxSize(3, 3), ) naked_triplet = techniques.NakedTriplet(sudoku).first() assert naked_triplet.combination.cells == [ Cell(position=Position(6, 7, 8), candidates={7, 9}), Cell(position=Position(8, 7, 8), candidates={7, 8, 9}), Cell(position=Position(8, 8, 8), candidates={7, 8, 9}), ] assert naked_triplet.combination.values == [7, 8, 9] by_position = operator.attrgetter("position") assert sorted(naked_triplet.changes, key=by_position) == [ Cell(position=Position(6, 8, 8), candidates={3, 4}), Cell(position=Position(7, 8, 8), candidates={3, 4}), ]
def test_unique_rectangle_not_found(): sudoku = make_sudoku_with_marks( [ [9, 2, 6, 7, 3, 5, 4, 0, 0], [0, 0, 0, 6, 4, 9, 7, 2, 5], [7, 4, 5, 8, 2, 1, 9, 3, 6], [0, 1, 0, 0, 0, 8, 6, 4, 0], [5, 0, 4, 0, 0, 0, 8, 0, 0], [8, 6, 9, 1, 7, 4, 3, 5, 2], [0, 0, 0, 0, 8, 0, 0, 0, 0], [0, 0, 0, 9, 1, 0, 0, 6, 0], [0, 0, 0, 4, 0, 3, 1, 0, 0], ], box_size=BoxSize(3, 3), ) sudoku.update([ Cell(position=Position(6, 0, 6), candidates={1, 6}), Cell(position=Position(6, 8, 8), candidates={3, 4}), Cell(position=Position(7, 0, 6), candidates={3, 4}), Cell(position=Position(7, 8, 8), candidates={3, 4}), ]) with pytest.raises(techniques.NotFound): techniques.UniqueRectangle(sudoku).first()
def _random_initial_cells(box_size: BoxSize) -> List[Cell]: size = box_size.width * box_size.length all_values = set(range(1, size + 1)) values = random.sample(all_values, k=size) box_values = [ values[i * box_size.length:i * box_size.length + box_size.length] for i in range(box_size.width) ] while True: # pragma: no branch row_values = random.sample(all_values - set(box_values[0]), k=box_size.length) used_values = [sorted(box_values[i]) for i in range(1, box_size.width)] if sorted(row_values) not in used_values: break row_values += random.sample(all_values.difference(box_values[0], row_values), k=box_size.length) return [ Cell( position=Position(row=i, column=j, box=box_size.sequential(i, j)), value=box_values[i][j], ) for i in range(box_size.width) for j in range(box_size.length) ] + [ Cell( position=Position(row=0, column=i, box=box_size.sequential(0, i)), value=value, ) for i, value in enumerate(row_values, start=box_size.length) ]
def test_hidden_single(): sudoku = make_sudoku_with_marks( [ [9, 0, 6, 7, 0, 5, 0, 0, 0], [0, 0, 0, 0, 0, 9, 0, 2, 5], [7, 4, 0, 0, 0, 1, 0, 0, 0], [0, 1, 0, 0, 0, 0, 6, 4, 0], [5, 0, 0, 0, 0, 0, 0, 0, 0], [8, 6, 9, 1, 0, 0, 3, 0, 0], [0, 0, 0, 0, 8, 0, 0, 0, 0], [0, 0, 0, 9, 0, 0, 0, 6, 0], [0, 0, 0, 4, 0, 3, 1, 0, 0], ], box_size=BoxSize(3, 3), ) hidden_single = techniques.HiddenSingle(sudoku).first() assert hidden_single.combination.cells == [ Cell(position=Position(1, 6, 2), candidates={4, 7, 8}) ] assert hidden_single.combination.values == [7] by_position = operator.attrgetter("position") assert sorted(hidden_single.changes, key=by_position) == [ Cell(position=Position(1, 6, 2), value=7), Cell(position=Position(4, 6, 5), candidates={2, 8, 9}), Cell(position=Position(6, 6, 8), candidates={2, 4, 5, 9}), Cell(position=Position(7, 6, 8), candidates={2, 4, 5, 8}), ]
def test_unique_rectangle(): sudoku = make_sudoku_with_marks( [ [0, 6, 0, 8, 0, 2, 3, 7, 1], [3, 0, 7, 1, 6, 5, 8, 0, 4], [0, 8, 1, 3, 7, 0, 5, 6, 0], [8, 7, 4, 9, 2, 3, 1, 5, 6], [9, 1, 3, 6, 5, 8, 2, 4, 7], [6, 0, 0, 4, 1, 7, 9, 3, 8], [0, 3, 8, 0, 0, 0, 6, 1, 5], [0, 0, 6, 0, 8, 1, 4, 0, 3], [1, 4, 0, 5, 3, 6, 7, 8, 0], ], box_size=BoxSize(3, 3), ) assert len(list(techniques.UniqueRectangle(sudoku))) == 1 unique_rectangle = techniques.UniqueRectangle(sudoku).first() assert unique_rectangle.combination.cells == [ Cell(position=Position(6, 0, 6), candidates={2, 7}), Cell(position=Position(6, 3, 7), candidates={2, 7}), Cell(position=Position(7, 0, 6), candidates={2, 5, 7}), Cell(position=Position(7, 3, 7), candidates={2, 7}), ] assert unique_rectangle.combination.values == [2, 7] assert unique_rectangle.changes == [ Cell(position=Position(7, 0, 6), candidates={5}), ]
def test_lone_single(): sudoku = make_sudoku_with_marks( [ [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), ) lone_single = techniques.LoneSingle(sudoku).first() assert lone_single.combination.cells == [ Cell(position=Position(1, 0, 0), candidates={5}) ] assert lone_single.combination.values == [5] by_position = operator.attrgetter("position") assert sorted(lone_single.changes, key=by_position) == [ Cell(position=Position(0, 0, 0), candidates={2, 3}), Cell(position=Position(0, 1, 0), candidates={2, 3, 4, 6, 8}), Cell(position=Position(0, 2, 0), candidates={2, 6, 8}), Cell(position=Position(1, 0, 0), value=5), Cell(position=Position(1, 1, 0), candidates={4, 6, 8, 9}), Cell(position=Position(1, 2, 0), candidates={1, 6, 8}), Cell(position=Position(1, 3, 1), candidates={4, 6, 7, 8}), Cell(position=Position(1, 4, 1), candidates={4, 7, 8}), Cell(position=Position(5, 0, 3), candidates={2, 3, 7}), ]
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 test_locked_candidate_not_found(): sudoku = make_sudoku_with_marks( [ [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([ Cell(position=Position(1, 1, 0), candidates={4, 6}), Cell(position=Position(1, 7, 2), candidates={6, 9}), Cell(position=Position(1, 8, 2), candidates={4, 9}), Cell(position=Position(3, 3, 4), candidates={1, 2}), Cell(position=Position(8, 1, 6), candidates={2, 5, 8}), Cell(position=Position(8, 3, 7), candidates={7, 9}), Cell(position=Position(8, 5, 7), candidates={7, 9}), Cell(position=Position(8, 6, 8), candidates={2, 5}), Cell(position=Position(8, 7, 8), candidates={5, 8}), ]) with pytest.raises(techniques.NotFound): techniques.LockedCandidate(sudoku).first()
def test_combination_as_str(): combination = techniques.Combination( name="Naked Pair", cells=[ Cell(position=Position(6, 3, 7), candidates={2, 5}), Cell(position=Position(6, 6, 8), candidates={2, 5}), ], values=[2, 5], ) assert str(combination) == "Naked Pair: `2, 5` at (6, 3), (6, 6)"
def _get_changes(self, combination: Combination) -> List[Cell]: eliminated = set(combination.values) single = Cell( position=combination.cells[0].position, value=combination.values[0], ) return [ Cell(position=cell.position, candidates=cell.candidates - eliminated) for cell in self.sudoku.intersection(single) if cell.candidates and cell.candidates & eliminated ] + [single]
def test_locked_candidate_in_a_box(): sudoku = make_sudoku_with_marks( [ [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([ Cell(position=Position(1, 1, 0), candidates={4, 6}), Cell(position=Position(1, 7, 2), candidates={6, 9}), Cell(position=Position(1, 8, 2), candidates={4, 9}), Cell(position=Position(8, 3, 7), candidates={7, 9}), Cell(position=Position(8, 7, 8), candidates={5, 8}), ]) locked_candidate = techniques.LockedCandidate(sudoku).first() assert locked_candidate.combination.cells == [ Cell(position=Position(3, 7, 5), candidates={1, 5, 7}), Cell(position=Position(3, 8, 5), candidates={2, 7}), ] assert locked_candidate.combination.values == [7] assert locked_candidate.changes == [ Cell(position=Position(3, 3, 4), candidates={1, 2}) ]
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
def _get_changes(self, combination: Combination) -> List[Cell]: eliminated = set(combination.values) return [ Cell(position=cell.position, candidates=diff) for cell in combination.cells if (diff := cell.candidates - eliminated) ]
def _get_changes(self, combination: Combination) -> List[Cell]: eliminated = set(combination.values) return [ Cell(position=c.position, candidates=c.candidates - eliminated) for c in self.sudoku.intersection( *[x for x in combination.cells[::2]]) if c.candidates and c.candidates & eliminated ]
def _get_changes(self, combination: Combination) -> List[Cell]: result = [] for cell in combination.cells: candidates = self._get_candidates(cell) if not cell.candidates or cell.candidates - candidates: result.append( Cell(position=cell.position, candidates=candidates)) return result
def test_hint_for_invalid_sudoku(rf): sudoku = generators.random_sudoku(avg_rank=1) sudoku.update(techniques.BulkPencilMarking(sudoku).first().changes) sudoku.update( [ Cell(position=Position(0, 0, 0), value=1), Cell(position=Position(0, 1, 0), value=1), ] ) url = reverse("hint") serializer = serializers.SudokuSerializer(sudoku) request = rf.post(url, data=serializer.data, content_type="application/json") response = views.Hints.as_view()(request) content = json.loads(response.rendered_content) assert content["code"] == "invalid_puzzle" assert response.status_code == 400
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_pencil_marking_corrects_invalid_mark(): sudoku = make_sudoku_with_marks( [ [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([Cell(position=Position(1, 0, 0), candidates={3, 5})]) pencil_marks = techniques.PencilMarking(sudoku).first() assert pencil_marks.changes == [ Cell(position=Position(1, 0, 0), candidates={5}) ]
def test_naked_pair(): sudoku = make_sudoku_with_marks( [ [9, 2, 6, 7, 3, 5, 4, 0, 0], [0, 0, 0, 6, 4, 9, 7, 2, 5], [7, 4, 5, 8, 2, 1, 9, 3, 6], [0, 1, 0, 0, 0, 8, 6, 4, 0], [5, 0, 4, 0, 0, 0, 8, 0, 0], [8, 6, 9, 1, 7, 4, 3, 5, 2], [0, 0, 0, 0, 8, 0, 0, 0, 0], [0, 0, 0, 9, 1, 0, 0, 6, 0], [0, 0, 0, 4, 0, 3, 1, 0, 0], ], box_size=BoxSize(3, 3), ) naked_pair = techniques.NakedPair(sudoku).first() assert naked_pair.combination.cells == [ Cell(position=Position(6, 3, 7), candidates={2, 5}), Cell(position=Position(6, 6, 8), candidates={2, 5}), ] assert naked_pair.combination.values == [2, 5] by_position = operator.attrgetter("position") assert sorted(naked_pair.changes, key=by_position) == [ Cell(position=Position(6, 0, 6), candidates={1, 3, 4, 6}), Cell(position=Position(6, 1, 6), candidates={3, 7, 9}), Cell(position=Position(6, 2, 6), candidates={1, 3, 7}), Cell(position=Position(6, 5, 7), candidates={6, 7}), ]
def test_xy_wing(): sudoku = make_sudoku_with_marks( [ [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([Cell(position=Position(3, 3, 4), candidates={1, 2})]) xy_wing = techniques.XYWing(sudoku).first() assert xy_wing.combination.cells == [ Cell(position=Position(3, 3, 4), candidates={1, 2}), Cell(position=Position(5, 4, 4), candidates={2, 5}), Cell(position=Position(5, 7, 5), candidates={1, 5}), ] assert xy_wing.combination.values == [1] assert xy_wing.changes == [ Cell(position=Position(3, 7, 5), candidates={5, 7}), Cell(position=Position(5, 3, 4), candidates={2, 4}), ]
def test_naked_pair_not_found(): sudoku = make_sudoku_with_marks( [ [9, 2, 6, 7, 3, 5, 4, 0, 0], [0, 0, 0, 6, 4, 9, 7, 2, 5], [7, 4, 5, 8, 2, 1, 9, 3, 6], [0, 1, 0, 0, 0, 8, 6, 4, 0], [5, 0, 4, 0, 0, 0, 8, 0, 0], [8, 6, 9, 1, 7, 4, 3, 5, 2], [0, 0, 0, 0, 8, 0, 0, 0, 0], [0, 0, 0, 9, 1, 0, 0, 6, 0], [0, 0, 0, 4, 0, 3, 1, 0, 0], ], box_size=BoxSize(3, 3), ) sudoku.update([ Cell(position=Position(6, 0, 6), candidates={1, 3, 4, 6}), Cell(position=Position(6, 1, 6), candidates={3, 7, 9}), Cell(position=Position(6, 2, 6), candidates={1, 3, 7}), Cell(position=Position(6, 5, 7), candidates={6, 7}), ]) with pytest.raises(techniques.NotFound): techniques.NakedPair(sudoku).first()
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 backtrack(sudoku: Sudoku) -> Sudoku: _sudoku = eliminate(sudoku) cells = sorted( (cell for cell in _sudoku.cells() if not cell.value), key=operator.attrgetter("candidates"), ) for cell in cells: for candidate in cell.candidates: _sudoku.update([Cell(position=cell.position, value=candidate)]) try: return backtrack(_sudoku) except (exceptions.InvalidSudoku, exceptions.NoCandidates): pass _sudoku.update([cell]) else: raise exceptions.NoCandidates return _sudoku
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_getitem(sudoku): assert sudoku[0, 0] == Cell(position=Position(0, 0, 0), candidates=set()) assert sudoku[2, 3] == Cell(position=Position(2, 3, 1), value=9)
def test_cell(): with pytest.raises(ValueError): Cell(position=Position(0, 0, 0), value=2, candidates={2, 6, 9})
def test_update(sudoku): cell_a = Cell(position=Position(0, 0, 0), value=2) cell_b = Cell(position=Position(0, 1, 0), candidates=set()) sudoku.update([cell_a, cell_b]) assert sudoku[0, 0] is cell_a assert sudoku[0, 1] is cell_b