4
\$\begingroup\$

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.answer = 0
        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.answer = self.options[0]
            self.text.setText(str(self.answer))
            self.marker.setFill(color_rgb(0, 255, 0))
        elif self.answer > 0:
            self.text.setText(str(self.answer))
            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:
                temp.append([cell.answer])
            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()


class AddGroup(Group):
    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:
                self.options = copy.deepcopy(options.add_options[len(self.cells)][goal])
        else:
            self.options = copy.deepcopy(options.add_options[len(self.cells)][goal])

    def happy(self):
        total = 0
        for cell in self.cells:
            total += cell.answer
        return total == int(self.goal)


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):
        return max(self.cells[0].answer, self.cells[1].answer) - min(self.cells[0].answer, self.cells[1].answer) == int(self.goal)


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:
            total *= cell.answer
        return total == int(self.goal)


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):
        return max(self.cells[0].answer, self.cells[1].answer) / min(self.cells[0].answer, self.cells[1].answer) == int(self.goal)


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:
            total += cell.answer
        return total == int(self.goal)

    def _remove_used_answers(self):
        remove = []
        for cell in self.cells:
            if cell.answer != 0:
                remove.append(cell.answer)
        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_answers()
        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.answer = self.cells[x, y].answer
                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
        return total

    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.

\$\endgroup\$
  • 1
    \$\begingroup\$ 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? \$\endgroup\$ – Peilonrayz May 21 '17 at 20:05
3
\$\begingroup\$

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:
    total += cell.answer

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.

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.