patternpythonMinor
A KenKen puzzle/solver in Python
Viewed 0 times
solverpuzzlepythonkenken
Problem
I've written a simple KenKen puzzle/solver in Python. I'd love some feedback on the design of the puzzle, as well as the architecture for the solver.
To model the puzzle, I have the following classes:
-
-
Now for the code:
``
To model the puzzle, I have the following classes:
Cellis used to model a (row, col, value) tuple
-
Cage (abstract) is used to model a grouping of Cell objects that must collectively satisfy a constraint. From this class, we have the following derived classes:AddCagefor cells involved in addition constraints
MulCagefor cells involved in multiplication constraints
SubCagefor cells involved in subtraction constraints
DivCagefor cells involved in division constraints
ConCagefor constant constraints
RowCagefor unique row/column constraints
-
Puzzle combines cages, cells, and exposes methods for the unassigned cells, whether or not the puzzle is solved, etc.Now for the code:
``
from abc import ABC, abstractmethod
from utils import kk_add, kk_mul, kk_sub, kk_div
class Cell:
def __init__(self, row, col, value=None):
"""
Models a cell in a kenken puzzle
Args:
row: row
col: column
value: cell value
"""
self.row = row
self.col = col
self.value = value
def __str__(self):
return ''.format(self.row, self.col, self.value)
def __hash__(self):
return hash((self.row, self.col))
def accept(self, visitor):
"""
Visitor implementation; accept a visitor object
and call the object's visit method for this object
Args:
visitor: CellVisitor implementation
Returns: None
"""
visitor.visit_cell(self)
class Cage(ABC):
def __init__(self, cells, func):
"""
Base class to model a cage in a kenken puzzle
A cage is a grouping of cells with a constraint
that the values of the cells must collectively satisfy
Args:
cells: the Cell` objects iSolution
I am unfamiliar with both the design pattern and the backtracking algorithm, so almost all of my comments will be nitpicks.
-
-
You could avoid the searches in the collection of the cells in each row and column using something like below.
class definitions
solver code
-
-
I don't like how
-
But that said, why are you finding differences between elements of
utils.pykk_sub: I prefervalues[0] - values[1] in {value, -value}, which will work ifvalueis negative. Maybe we know that it will always be nonnegative, though.
kk_div: I prefervalues[0] == values[1] value or values[1] == values[0] value, which avoids float division and casts.
kk_{add,mul,sub,div}: It would be clearer to me if you used more descriptive argument names than 'values' and 'value'. You could do 'summands' and 'sum' (though that clashes with the built-in) forkk_add, 'factors' and 'product' forkk_mul, etc.
product: I preferreduce(operator.mul, values). You're already usingfunctools, so I don't feel that this is a big jump in complexity.
-
parse_string:- It would be clearer to me if you used a set comprehension to define
cage_cells:{Cell(row, col, None) for (row, col) in cells}. Also, sincevalueis a keyword argument inCell.__init__you could useset(itertools.starmap(Cell, cells))(not that that's any clearer than what you have now).
- If you moved that definition up to right after the definition of
cells, you could replace the test for already-accounted-for cells withif puzzle_cells.intersection(cage_cells), which is easier for me to parse.
- I think you should swap the test for already-accounted-for cells with the test for
valueorcellsnot being provided. It seems strange to usecellsand only afterwards check that it's valid.
-
You could avoid the searches in the collection of the cells in each row and column using something like below.
rows = [[] for _ in range(size)]
cols = [[] for _ in range(size)]
# Later, inside look over cages:
for cell in cage_cells:
rows[cell.row].append(cell)
cols[cell.col].append(cell)
# Then, outside that loop:
puzzle_cages.extend(map(RowCage, itertools.chain(rows, cols)))class definitions
Cage.solved: I think the test thatvaluesandself.cellshave the same length should either be removed or throw an exception if it fails.valuesis freshly created fromself.cellsimmediately before the test; something catastrophic has happened if it fails.
{Add,Mul,Sub,Div}Cage.__init__: if you switched the order of the arguments inkk_{add,mul,sub,div}, thesuper().__init__call could usefunctools.partial(kk_{add,mul,sub,div}, value). I personally prefer that to the lambda, though I'd believe others would disagree with me. Also (this is out of my depth), doesn't this approach result in every instance ofCagehaving a dedicatedfuncfunction object? It seems that all the instances ofAddCagecould sharekk_addand just pass it bothvalueandvalues(and likewise for the others). I think the subclasses ofCageshould overrideevaluate, orfuncshould be made a method.
Puzzle.__init__: the puzzle comprises the cells, not the other way around.
Puzzle.{domain,unassigned}: the docstring incorrectly says that a Boolean is returned.
Puzzle.consistent: you repeat this expression for cages containing a given cell inreduce. Maybe it would be good to make a method for this?
solver code
CageVisitor.visit_add:
- I'd prefer
if value is not Nonein place ofif value. Seeingif valuealong withsummakes me think of zero.
- If you found the difference between
cell.valueandsbefore the loop, you could avoid the additions inside the loop.
- You could prune more aggressively by factoring in how many other cells in the cage are undetermined. For example, if
cage.valueis9,cage.valuesis[3, None, None], andself.candidatesis[1, 4, 6], we can eliminate6because that would force the remaining undetermined cell to have value0. That does couple the candidates for different cells, which it looks like you haven't done so far.
CageVisitor.visit_mul: I preferif cage.value % value. And why not use the quotient ofcage.valueand the product of the values of the cells whose values have been determined?
-
CageVisitor.visit_sub:- In the docstring, 'MulCage' should be 'SubCage'.
-
I don't like how
kk_sub([x, x], cage.value) will get called, and you aren't using the fact that the order of values doesn't affect kk_sub. Maybe you could do something like below. You could do something similar in visit_div.self.candidates = list(itertools.chain.from_iterable(filter(
lambda pair: kk_sub(pair, cage.value),
itertools.combinations(candidates, 2)
)))-
But that said, why are you finding differences between elements of
candidates? It's the difference between x and the candidates of the other cells that matters, unless I misunderstand something. It seems to me that CageVisitor is initialized with a list of candidate values for the particular cell being visited, not a list of candidate values across all cells in the cage. I may well be confused onCode Snippets
rows = [[] for _ in range(size)]
cols = [[] for _ in range(size)]
# Later, inside look over cages:
for cell in cage_cells:
rows[cell.row].append(cell)
cols[cell.col].append(cell)
# Then, outside that loop:
puzzle_cages.extend(map(RowCage, itertools.chain(rows, cols)))self.candidates = list(itertools.chain.from_iterable(filter(
lambda pair: kk_sub(pair, cage.value),
itertools.combinations(candidates, 2)
)))Context
StackExchange Code Review Q#162933, answer score: 7
Revisions (0)
No revisions yet.