# KenKen puzzle solver

I made a program that solves KenKen Puzzles using graphics.py, and I was just wondering if my code was reasonably Pythonic.

neknek.py

import options
import copy
from graphics import *
from random import randint
from itertools import product

class Cell:
def __init__(self, x, y):
self.options = [1, 2, 3, 4, 5, 6]
self.x = x
self.y = y
self.text = Text(Point(x * 100 + 50, y * 100 + 50), str(self.options))
self.text.setSize(10)
self.marker = Circle(Point(self.x * 100 + 50, self.y * 100 + 80), 10)
self.marker.setFill(color_rgb(255, 0, 0))

def update(self):
if len(self.options) == 1 and self.answer == 0:
self.marker.setFill(color_rgb(0, 255, 0))
elif self.answer > 0:
self.marker.setFill(color_rgb(0, 255, 0))
elif self.answer == 0:
self.text.setText(str(self.options))

def remove_options(self, options):
if len(list(set(self.options) - set(options))) > 0:
self.options = list(set(self.options) - set(options))

class Group:
def __init__(self, cells, goal):
self.cells = cells
self.goal = goal
self.options = []

def happy(self):
raise NotImplementedError

def check_bend(self):
x = []
y = []
for cell in self.cells:
x.append(cell.x)
y.append(cell.y)
return not (self._bend_helper(x) or self._bend_helper(y))

def _all_options(self):
temp = []
for cell in self.cells:
if cell.answer > 0:
else:
temp.append(cell.options)
temp = list(product(*temp))
li = []
for x in temp:
li.append(list(x))
return li

def _bend_helper(self, li):
for i in li:
if li.count(i) == len(li):
return True
return False

def _check_group_options(self, cell, i):
for option in self.options:
if i == option[self.cells.index(cell)]:
return True
return False

def _remove_group_options(self):
possible = self._all_options()
remove = []
for option in self.options:
if option not in possible:
remove.append(option)
for option in remove:
self.options.remove(option)

def _remove_cell_options(self):
for cell in self.cells:
remove = []
for option in cell.options:
if not self._check_group_options(cell, option):
remove.append(option)
cell.remove_options(remove)

def solve(self):
self._remove_group_options()
self._remove_cell_options()
for cell in self.cells:
cell.update()

def __init__(self, cells, goal):
super().__init__(cells, goal)
if self.check_bend():
try:
self.options = copy.deepcopy(options.add_options[len(self.cells)][goal + '*'])
except KeyError:
else:

def happy(self):
total = 0
for cell in self.cells:

class SubGroup(Group):
def __init__(self, cells, goal):
super().__init__(cells, goal)
if self.check_bend():
try:
self.options = copy.deepcopy(options.sub_options[len(self.cells)][goal + '*'])
except KeyError:
self.options = copy.deepcopy(options.sub_options[len(self.cells)][goal])
else:
self.options = copy.deepcopy(options.sub_options[len(self.cells)][goal])

def happy(self):

class MulGroup(Group):
def __init__(self, cells, goal):
super().__init__(cells, goal)
if self.check_bend():
try:
self.options = copy.deepcopy(options.mul_options[len(self.cells)][goal + '*'])
except KeyError:
self.options = copy.deepcopy(options.mul_options[len(self.cells)][goal])
else:
self.options = copy.deepcopy(options.mul_options[len(self.cells)][goal])

def happy(self):
total = 1
for cell in self.cells:

class DivGroup(Group):
def __init__(self, cells, goal):
super().__init__(cells, goal)
if self.check_bend():
try:
self.options = copy.deepcopy(options.div_options[len(self.cells)][goal + '*'])
except KeyError:
self.options = copy.deepcopy(options.div_options[len(self.cells)][goal])
else:
self.options = copy.deepcopy(options.div_options[len(self.cells)][goal])

def happy(self):

class RowColGroup(Group):
def __init__(self, cells):
super().__init__(cells, '21')
self.options = options.row_col_options

def happy(self):
total = 0
for cell in self.cells:

remove = []
for cell in self.cells:
if cell.answer != 0:
for cell in self.cells:
cell.remove_options(remove)

def _remove_used_options(self):
remove = []
used_options = []
for cell in self.cells:
if len(cell.options) == used_options.count(cell.options) + 1:
for i in cell.options:
remove.append(i)
else:
used_options.append(cell.options)
for cell in self.cells:
cell.remove_options(remove)

def _count_options(self):
options = [0, 0, 0, 0, 0, 0, 0]
for cell in self.cells:
for option in cell.options:
options[option] += 1
for i in range(1, 7):
if options[i] == 1:
for cell in self.cells:
if i in cell.options and cell.answer != i:
cell.options = [i]

def solve(self):
self._remove_used_options()
self._count_options()
for cell in self.cells:
cell.update()

class Puzzle:
def __init__(self, cells, groups, rows, cols, win):
self.width = 100 * 6
self.height = 100 * 6
self.cells = cells
self.win = win
self.prompt = Text(Point((self.width + 150) / 3, self.height + 25), '')
self.prompt.setSize(10)
self.prompt.draw(self.win)
self.input = Entry(Point(2 * (self.width + 150) / 3, self.height + 25), 5)
self.input.setSize(10)
self.input.draw(self.win)
self.groups = groups
self.rows = rows
self.cols = cols

@classmethod
def from_gui(cls):
width = 100 * 6
height = 100 * 6
cells = {}
win = GraphWin('KenKen', width + 200, height + 50)
prompt = Text(Point((width + 150) / 3, height + 25), 'How many groups are there?')
prompt.setSize(10)
prompt.draw(win)
input = Entry(Point(2 * (width + 150) / 3, height + 25), 5)
input.setSize(10)
input.draw(win)
groups = []
rows = []
cols = []
r = Rectangle(Point(width + 10, 20), Point(width + 190, 50))
r.setFill(color_rgb(255, 0, 0))
r.draw(win)
t = Text(Point(width + 100, 35), 'Remove')
t.setSize(10)
t.draw(win)
r = Rectangle(Point(width + 10, 60), Point(width + 190, 90))
r.setFill(color_rgb(0, 255, 0))
r.draw(win)
t = Text(Point(width + 100, 75), 'Enter')
t.setSize(10)
t.draw(win)
for i in range(6):
l = Line(Point((i + 1) * 100, 0), Point((i + 1) * 100, height))
l.setWidth(4)
l.draw(win)
l = Line(Point(0, (i + 1) * 100), Point(width, (i + 1) * 100))
l.setWidth(4)
l.draw(win)
for y in range(6):
for x in range(6):
c = Cell(x, y)
cells[x, y] = c
c.text.draw(win)
win.getMouse()
group_num = int(input.getText())
input.setText('')
for i in range(group_num):
g = randint(0, 255)
r = randint(g, 255)
b = randint(g, 255)
color = color_rgb(r, g, b)
prompt.setText("Enter the goal and operator for group {}, then select the cells.".format(i))
group_cells = []
while True:
p = win.getMouse()
inp = input.getText().split()
op = inp[1]
goal = inp[0]
x, y = p.getX(), p.getY()
if y > height or x > width:
if op == '+':
g = AddGroup(group_cells, goal)
elif op == '-':
g = SubGroup(group_cells, goal)
elif op == '*':
g = MulGroup(group_cells, goal)
else:
g = DivGroup(group_cells, goal)
groups.append(g)
g.solve()
input.setText('')
break
else:
if cells[x // 100, y // 100] in cells:
group_cells.remove(cells[x // 100, y // 100])
cells[x // 100, y // 100].marker.undraw()
else:
group_cells.append(cells[x // 100, y // 100])
cells[x // 100, y // 100].marker.setFill(color)
cells[x // 100, y // 100].marker.draw(win)
for y in range(6):
group_cells = []
for x in range(6):
group_cells.append(cells[x, y])
rows.append(RowColGroup(group_cells))
for x in range(6):
group_cells = []
for y in range(6):
group_cells.append(cells[x, y])
cols.append(RowColGroup(group_cells))
prompt.setText('')
input.setText('')
puzzle = cls(cells, groups, rows, cols, win)
return puzzle

def copy(self):
win = GraphWin('KenKen', 100 * 6, 100 * 6 + 50)
for i in range(6):
l = Line(Point((i + 1) * 100, 0), Point((i + 1) * 100, 600))
l.setWidth(4)
l.draw(win)
l = Line(Point(0, (i + 1) * 100), Point(600, (i + 1) * 100))
l.setWidth(4)
l.draw(win)
cells = {}
for y in range(6):
for x in range(6):
c = Cell(x, y)
c.options = copy.deepcopy(self.cells[x, y].options)
c.marker.draw(win)
c.text.draw(win)
if c.answer > 0:
c.update()
cells[x, y] = c
groups = []
for group in self.groups:
c = []
for cell in group.cells:
c.append(cells[cell.x, cell.y])
if type(group) is AddGroup:
g = AddGroup(c, group.goal)
elif type(group) is SubGroup:
g = SubGroup(c, group.goal)
elif type(group) is MulGroup:
g = MulGroup(c, group.goal)
else:
g = DivGroup(c, group.goal)
g.options = copy.deepcopy(group.options)
groups.append(g)
rows = []
for row in self.rows:
c = []
for cell in row.cells:
c.append(cells[cell.x, cell.y])
r = RowColGroup(c)
r.options = copy.deepcopy(row.options)
rows.append(r)
cols = []
for col in self.cols:
c = []
for cell in col.cells:
c.append(cells[cell.x, cell.y])
c = RowColGroup(c)
c.options = copy.deepcopy(col.options)
cols.append(c)
return Puzzle(cells, groups, rows, cols, win)

def solve(self):
tries = 0
while self.total() < 36 and tries < 60:
tries += 1
for row in self.rows:
row.solve()
self.prompt.setText('Solving Rows... Continue.')
for col in self.cols:
col.solve()
self.prompt.setText('Solving Columns... Continue.')
for group in self.groups:
group.solve()
self.prompt.setText('Solving Groups... Continue.')
if self.total() < 36:
for row in self.rows:
for cell in row.cells:
if cell.answer == 0:
for option in cell.options:
result = self.try_solve(cell, option)
if result is not None:
self.win.close()
return result
elif self.happy():
return self
self.win.close()
return None

def try_solve(self, cell, option):
c = self.copy()
c.cells[cell.x, cell.y].answer = option
c.cells[cell.x, cell.y].options = [option]
result = c.solve()
return result
# cell.answer = option
# return self.solve()

def happy(self):
all_groups = self.rows + self.cols + self.groups
for group in all_groups:
if not group.happy():
return False
return True

def total(self):
total = 0
for y in range(6):
for x in range(6):
if self.cells[x, y].answer > 0:
total += 1

def get_input(self):
p = self.win.getMouse()
x, y = p.getX(), p.getY()
if x >= self.width:
if 20 < y < 50:
self.remove_options(self.win.getMouse(), int(self.prompt.getText()))
else:
self.solve()

def remove_options(self, p, i):
pass

if __name__ == '__main__':
p = Puzzle.from_gui()
p = p.solve()
p.win.getMouse()


options.py is just every possible combination for different groups.

• Welcome to Code Review Alecs Gavyn, I hope you get a good answer. :) @close-voter, would you mind to say what is unclear so OP can fix it? – Peilonrayz May 21 '17 at 20:05

There was no real explanation here of what this code does, and frankly there was a lot of it. Also, I had no way to run this code, so I will stick with some more generally pythonic things, which is what you asked for anyways.

In general your code looks pretty good. For code tagged beginner, it is in fact excellent. I will however recommend getting familiar with python comprehensions. You have lots of loops that can be made considerably more pythonic with comprehension. I will show a few examples from your code, but first...

## Pep8:

Python has a strong idea of how the code should be styled, and it is expressed in pep8.

I suggest you get a style/lint checker. I use the pycharm ide which will show you style and compile issues right in the editor.

The primary violation was due to line length issues. Breaking things to 80 columns can seem a bit uncomfortable at first, but it makes the code easier to read.

## Comprehensions

So, this will go over a few styles of loops in your code and show a comprehension equivalent. I won't spend any time discussing the details, but hopefully the syntax is fairly straightforward, and the compactness and clarity will motivate you you to go study them.

This:

temp = list(product(*temp))
li = []
for x in temp:
li.append(list(x))


Can be:

li = [list(x) for x in product(*temp)]


This:

remove = []
for option in self.options:
if option not in possible:
remove.append(option)

for option in remove:
self.options.remove(option)


Can be:

self.options = [o for o in self.options if o in possible]


This:

total = 0
for cell in self.cells:


Can be:

total = sum(cell.answer for cell in self.cells)


This:

x = []
y = []
for cell in self.cells:
x.append(cell.x)
y.append(cell.y)


Can be:

x, y = zip(*((cell.x, cell.y) for cell in a))


## Note:

I didn't actually test any of these, so they may contain silly typos. Have any questions? Hit me up in comments.

• Thank you so much! This is exactly the type of response I was looking for. – Alecs Gavyn May 31 '17 at 18:00