Module pacai.ui.gui

Expand source code
import math
import sys
import time
import tkinter

from PIL import Image
from PIL import ImageTk

from pacai.ui.keyboard import Keyboard
from pacai.ui import spritesheet
from pacai.ui.view import AbstractView

MAX_FPS = 1000
TK_BASE_NAME = 'pacai'
DEATH_SLEEP_TIME = 0.5

MIN_WINDOW_HEIGHT = 100
MIN_WINDOW_WIDTH = 100

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

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.

Expand source code
def finish(self):
    super().finish()

    self._canvas.delete("all")
def getKeyboard(self)

Inherited from: AbstractView.getKeyboard

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

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

    return self._keyboard
def initialize(self, state)

Inherited from: AbstractView.initialize

Perform an initial drawing of the view.

Expand source code
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 update(self, state, forceDraw=False)

Inherited from: AbstractView.update

Materialize the view, given a state.