Search code examples
pythontkintercx-freeze

cx_Freeze Build of Tkinter Application Extremely Buggy


I recently created a small game using tkinter (python version 3.6.1) and froze it using cx_Freeze. The game has four buttons: an undo button, a restart button, a "find legal moves" button, and a "find best move button". The "find best move" button uses a shelve database to find the best move for the first three turns and a recursive function that traverses the move tree on the fly for the fourth turn and up. My code disables the buttons when they should not be used.

I made sure to include the necessary DLLs in the setup script and I was able to run the executable without errors. However, three of the buttons are disabled until the fourth turn (when the recursive function begins to be used) and the application is extremely buggy in many other ways. However, it works perfectly when I run the unfrozen version.

I honestly don't know what code snippets I would need to provide to you guys, as this issue has me utterly at a loss. The only clue I have is that the pyc files in the build differ in size from the unfrozen app. I know this is rather vague, but I do not know what specifics would be useful to give. Any help, if possible, would be greatly appreciated.

"Find best move" method:

def _find_best_move(self):
    """Finds best move possible for current game."""
    if len(self.game.moves) <= 3:
        with shelve.open("paths") as db:
            best_paths = db[str(self.game.moves)]
            best_path = choice(best_paths)
    else:
        self.path_finder(self.game)
        best_path = self.path_finder.best_path
    best_move = best_path[len(self.game.moves)]
    best_move = (__class__._add_offset(best_move[0]), best_move[1])
    return best_move

Updates Button State:

def update_gui(self):
    """Updates GUI to reflect current game conditions."""
    legal_moves = self.game.find_legal_moves()
    if self.game.moves:
        self.undo_btn["state"] = "!disabled"
        self.restart_btn["state"] = "!disabled"
        self.best_move_btn["state"] = "!disabled"
    else:
        self.undo_btn["state"] = "disabled"
        self.restart_btn["state"] = "disabled"
    if legal_moves:
        self.show_moves_btn["state"] = "!disabled"
    else:
        self.show_moves_btn["state"] = "disabled"
    if legal_moves and self.game.moves:
        self.best_move_btn["state"] = "!disabled"
    else:
        self.best_move_btn["state"] = "disabled"

My __init__ file:

initpath = os.path.dirname(__file__)
os.chdir(os.path.join(initpath, "data"))

PathFinder class (traverses move tree on the fly):

class PathFinder:
    """Provides methods to find move paths that meet various criteria.

    Designed to be called after the player makes a move.
    """

    _game = None
    best_path = None
    best_score = None

    def __call__(self, game):
        """Call self as function."""
        if not game:
            self._game = DummyGame()
        elif not isinstance(game, DummyGame):
            self._game = DummyGame(game)
        else:
            self._game = game
        moves = self._game.moves
        self.possible_paths = dict.fromkeys(range(1,9))
        root = Node(moves[-1])
        self._find_paths(root)
        self._find_paths.cache_clear()
        found_scores = [score for score in self.possible_paths.keys() if
                        self.possible_paths[score]]
        self.best_score = min(found_scores)
        self.best_path = self.possible_paths[self.best_score]

    @lru_cache(None)
    def _find_paths(self, node):
        """Finds possible paths and records them in 'possible_paths'."""
        legal_moves = self._game.find_legal_moves()
        if not legal_moves:
            score = self._game.peg_count
            if not self.possible_paths[score]:
                self.possible_paths[score] = self._game.moves.copy()
        else:
            children = []
            for peg in legal_moves:
                for move in legal_moves[peg]:
                    children.append(Node((peg, move)))
            for child in children:
                self._game.move(*child.data)
                self._find_paths(child)
        try:
            self._game.undo()
        except IndexError:
            pass

Peg class:

class Peg(RawPen):
    """A specialized 'RawPen' that represents a peg."""

    def __init__(self, start_point, graphics):
        """Initialize self. See help(type(self)) for accurate signature."""
        self.graphics = graphics
        self.possible_moves = []
        super().__init__(self.graphics.canvas, "circle", _CFG["undobuffersize"],
                     True)
        self.pen(pendown=False, speed=0, outline=2, fillcolor="red",
             pencolor="black", stretchfactor=(1.25,1.25))
        self.start_point = start_point
        self.goto(start_point)
        self.ondrag(self._remove)
        self.onrelease(self._place)

    def _remove(self, x, y):
        """Removes peg from hole if it has moves."""
        if self.possible_moves:
            self.goto(x,y)

    def _place(self, x, y):
        """Places peg in peg hole if legal."""
        if self.possible_moves:
            target_holes = [tuple(map(add, self.start_point, move)) for move in
                        self.possible_moves]
            distances = [self.distance(hole) for hole in target_holes]
            hole_distances = dict(zip(distances, target_holes))
            nearest_hole = hole_distances[min(hole_distances)]
            if self.distance(nearest_hole) <= 0.45:
                self.goto(nearest_hole)
                peg = self.graphics._subtract_offset(self.start_point)
                move = tuple(map(sub, self.pos(), self.start_point))
                move = tuple(map(int, move))
                self.graphics.game.move(peg, move)
                self.start_point = self.pos()
            else:
                self.goto(self.start_point)

Solution

  • The frozen application is going to have a different value for __value__ then the unfrozen application. You will have to deal with that accordingly! This is a common issue that bites a lot of people. Anything that assumes that the module is found in the file system is going to stop working properly when frozen. The other gotcha is dynamic importing of modules.

    The documentation covers this and other topics that will hopefully help you out!