Let’s fix that reminder test and set up to allow us to select techniques. A bit of progress, and raggedness tells me I need a break. It’s artificial, it’s intelligent, and it’s not AI.

We have this test failing:

``````    def test_full_3x3(self):
Puzzle.puzzle_count = 0
Solver.solver_count = 0
puzzle = Puzzle('103000001')
solver = Solver(puzzle)
solved = solver.solve()
assert solved.game == '123312231'
assert Solver.solver_count == 10
assert Puzzle.puzzle_count == 10
``````

It’s there to remind us that our SudokuTechniques object interferes with tests of some low-level operations.

When last we met, what I had in mind was making the techniques into a kind of list, so that we could readily control which were applied. I had in mind changing Solver so that it would have a list of techniques to execute. But it occurs to me that we may also wish to allow for running “techniques” without our brute force method. After all, we’d like to solve puzzles intelligently if we can.

So, therefore, it seems to me that the Solver itself should be one of the techniques. Let’s try something. Here’s Solver:

``````class Solver:
solver_count = 0

def __init__(self, puzzle):
Solver.solver_count += 1
self.puzzle = puzzle

def solve(self) -> Puzzle | None:
if self.puzzle.is_filled_in:
return self.puzzle
for new_puzzle in self.puzzle.find_next_puzzles():
techniques = SudokuTechniques(new_puzzle)
improved_puzzle = techniques.apply()
solved_puzzle = Solver(improved_puzzle).solve()
if solved_puzzle:
return solved_puzzle
return None
``````

Let’s first extract almost the complete contents of `solve` to a new method that we’ll call `brute_solve`:

``````    def solve(self) -> Puzzle | None:
if self.puzzle.is_filled_in:
return self.puzzle
return self.brute_solve()

def brute_solve(self):
for new_puzzle in self.puzzle.find_next_puzzles():
techniques = SudokuTechniques(new_puzzle)
improved_puzzle = techniques.apply()
solved_puzzle = Solver(improved_puzzle).solve()
if solved_puzzle:
return solved_puzzle
return None
``````

Now let’s see … let’s assume that Solver has a list of techniques to try, and that we loop over them. But wait! That’s not going to suffice. Suppose we have a few techniques. We’ll want to try the first one until it finds nothing to do, then try the second. If the second finds something to do … don’t we want to go back and try the first one again?

And as for the brute force method, we have seen that once we make a guess with it, there’s an advantage to running the others. So we need each technique to do its best before returning, and in the case of the brute force one, it, too, should just make one guess and then loop back to the others.

Let’s create a list in Solver and iterate it.

This is ragged, and I don’t like it, but I think it works:

``````class Solver:
solver_count = 0

def __init__(self, puzzle, techniques=['brute_solve']):
Solver.solver_count += 1
self.puzzle = puzzle
self.techniques = techniques

def solve(self) -> Puzzle | None:
if self.puzzle.is_filled_in:
return self.puzzle
for technique in self.techniques:
if technique == 'force_solve':
techniques = SudokuTechniques(self.puzzle)
self.puzzle = techniques.apply()
if technique == 'brute_solve':
self.puzzle = self.brute_solve(self.puzzle)
return self.puzzle

def brute_solve(self, puzzle):
for new_puzzle in self.puzzle.find_next_puzzles():
solved_puzzle = Solver(new_puzzle).solve()
if solved_puzzle:
return solved_puzzle
return None
``````

We are green. Before I commit, I want to check that the full list works. I’ll do that by checking the big 9x9 and looking for the smaller value of solutions.

Meh. I thought it was working with the force in place, but it was not. I’ve lost the thread. Roll back. I am really glad I didn’t commit anything.

I could try to debug that but let’s try thinking again.

Let’s first just pass in a flag that says whether to run the force or not.

``````class Solver:
solver_count = 0

def __init__(self, puzzle, apply_techniques=False):
Solver.solver_count += 1
self.apply_techniques = apply_techniques
self.puzzle = puzzle

def solve(self) -> Puzzle | None:
if self.puzzle.is_filled_in:
return self.puzzle
for new_puzzle in self.puzzle.find_next_puzzles():
if self.apply_techniques:
techniques = SudokuTechniques(new_puzzle)
improved_puzzle = techniques.apply()
solved_puzzle = Solver(improved_puzzle).solve()
if solved_puzzle:
return solved_puzzle
return None
``````

PyCharm informs me that `improved_puzzle` is not necessarily defined. I think we can use `new_puzzle` there. Let’s get a test in place first, however:

``````    def test_first_9x9_with_technique(self):
assert self.puzzle_list == self.puzzle_list_2  # ensuring no typos
puzzle = Puzzle.from_list(self.puzzle_list)
Solver.solver_count = 0
solver = Solver(puzzle)
solved_puzzle = solver.solve()
assert solved_puzzle.is_correctly_solved
assert Solver.solver_count < 4000
``````

That fails as intended:

``````Expected :4000
Actual   :78431
``````

And then:

``````    def test_first_9x9_with_technique(self):
assert self.puzzle_list == self.puzzle_list_2  # ensuring no typos
puzzle = Puzzle.from_list(self.puzzle_list)
Solver.solver_count = 0
solver = Solver(puzzle, apply_techniques=True)
solved_puzzle = solver.solve()
assert solved_puzzle.is_correctly_solved
assert Solver.solver_count < 4000
``````

WTF? It still fails. Same 78431. Ah. Need to pass the flag along:

``````    def solve(self) -> Puzzle | None:
if self.puzzle.is_filled_in:
return self.puzzle
for new_puzzle in self.puzzle.find_next_puzzles():
if self.apply_techniques:
techniques = SudokuTechniques(new_puzzle)
new_puzzle = techniques.apply()
solved_puzzle = Solver(new_puzzle, self.apply_techniques).solve()
if solved_puzzle:
return solved_puzzle
return None
``````

Now we are green. Commit: techniques are now optional. Performance improves when applied.

That was ragged enough that I think I’ll do well to stop. Let’s sum up: