Module pacai.ui.gui

Classes

class AbstractGUIView (fps=0, title='pacai', **kwargs)

Most of the functionality necessary to draw graphics in a window. tkinter is used, so Tk must be installed on the machine.

Expand source code
class AbstractGUIView(AbstractView):
    """
    Most of the functionality necessary to draw graphics in a window.
    `tkinter` is used, so Tk must be installed on the machine.
    """

    def __init__(self, fps = 0, title = 'pacai', **kwargs):
        super().__init__(**kwargs)

        self._fps = int(max(0, min(MAX_FPS, fps)))

        # To make computations easier, we will actually convert "unlimited FPS" to our FPS max.
        if (self._fps == 0):
            self._fps = MAX_FPS

        self._timePerFrame = 1.0 / self._fps

        self._totalDrawRequests = None
        self._totalDroppedFrames = None

        self._firstDrawTime = None
        self._lastDrawTime = None

        if (title != 'pacai'):
            title = 'pacai - %s' % (str(title))

        self._root = tkinter.Tk(baseName = TK_BASE_NAME)
        self._root.protocol('WM_DELETE_WINDOW', self._windowClosed)
        self._root.minsize(width = MIN_WINDOW_WIDTH, height = MIN_WINDOW_HEIGHT)
        self._root.resizable(True, True)
        self._root.title(title)

        self._root.bind("<Configure>", self._resize)

        self._canvas = None
        self._imageArea = None

        self._height = None
        self._width = None

        self._dead = False
        self._keyboard = None

    # Override
    def finish(self):
        super().finish()

        self._canvas.delete("all")

    def getKeyboard(self):
        # tkinter is not good with multiple keybinds.
        if (self._keyboard is None):
            self._keyboard = Keyboard(self._root)

        return self._keyboard

    # Override
    def initialize(self, state):
        super().initialize(state)

        # Height is +1 for the score.
        self._height = max(
            MIN_WINDOW_HEIGHT,
            (state.getInitialLayout().getHeight() + 1) * spritesheet.SQUARE_SIZE)
        self._width = max(
            MIN_WINDOW_WIDTH,
            state.getInitialLayout().getWidth() * spritesheet.SQUARE_SIZE)

        if (self._canvas is None):
            self._canvas = tkinter.Canvas(self._root, height = self._height, width = self._width,
                    highlightthickness = 0)

        self._imageArea = self._canvas.create_image(0, 0, image = None, anchor = tkinter.NW)
        self._canvas.pack(fill = 'both', expand = True)

        self._totalDrawRequests = 0
        self._totalDroppedFrames = 0
        self._firstDrawTime = None

    def _cleanup(self, exit = True):
        """
        This GUI has been killed, clean up.
        This is one of the rare cases where we will exit outside of the bin package.
        """

        # Sleep for a short period, so the last state of the game can be seen.
        time.sleep(DEATH_SLEEP_TIME)

        if (self._root is not None):
            self._root.destroy()
            self._root = None

        if (exit):
            sys.exit(0)

    def _adjustFPS(self):
        """
        Decide if we need to take some action to adjust the FPS.
        If we are drawing too slow, we will drop a frame.
        If we are drawing too fast, we will block and timeout.
        In the case of a dropped frame, this will return true.
        """

        now = time.time()

        if (math.isclose(0.0, now - self._firstDrawTime)):
            return True

        # The FPS after accounting for dropped frames.
        adjustedFPS = self._totalDrawRequests / (now - self._firstDrawTime)

        # To keep the FPS up, we may skip animating frames.
        if (adjustedFPS < self._fps):
            self._totalDroppedFrames += 1
            return True

        if (self._lastDrawTime is not None):
            timeLeft = self._timePerFrame - (now - self._lastDrawTime)
            if (timeLeft > 0):
                # Use Tkinter to block.
                self._root.after(int(1000 * timeLeft))

        return False

    # Override
    def _drawFrame(self, state, frame, forceDraw = False):
        if (self._dead):
            self._cleanup()

        self._totalDrawRequests += 1

        if (self._firstDrawTime is None):
            self._firstDrawTime = time.time()

            # This is our first frame, we do not have an FPS to stabilize yet.
            forceDraw = True

        if (not forceDraw and self._adjustFPS()):
            return

        image = frame.toImage(self._sprites, self._font)

        # Check for a resize.
        if (self._height != frame.getImageHeight() or self._width != frame.getImageWidth()):
            image = image.resize((self._width, self._height), resample = Image.LANCZOS)

        # Convert the image into a tk image.
        image = ImageTk.PhotoImage(image)
        self._canvas.itemconfig(self._imageArea, image = image)

        self._root.update_idletasks()
        self._root.update()

        self._lastDrawTime = time.time()

    def _resize(self, event):
        if (self._width == event.width and self._height == event.height):
            return

        # Ignore resize requests that are for a single pixel.
        # (These requests are sometimes generated from OSX.)
        if (event.width == 1 and event.height == 1):
            return

        self._width = max(MIN_WINDOW_WIDTH, event.width)
        self._height = max(MIN_WINDOW_HEIGHT, event.height)

        self._canvas.config(width = self._width, height = self._height)
        self._canvas.pack(fill = 'both', expand = True)

    def _windowClosed(self, event = None):
        """
        Handler for the TK window closing.
        """

        self._dead = True

Ancestors

Subclasses

Methods

def finish(self)

Inherited from: AbstractView.finish

Signal that the game is over and the UI should cleanup.

def getKeyboard(self)

Inherited from: AbstractView.getKeyboard

For views that support keyboards, get an instance of a pacai.ui.keyboard.Keyboard.

def initialize(self, state)

Inherited from: AbstractView.initialize

Perform an initial drawing of the view.

def update(self, state, forceDraw=False)

Inherited from: AbstractView.update

Materialize the view, given a state.