hog_gui.py (plain text)


"""A graphical user interface (GUI) for the game of Hog.

This file uses many features of Python not yet covered in the course.  A lab
later in the semester will review its implementation and let you extend it.
"""

import hog
import dice
from ucb import main

import tkinter as tk
from tkinter import *
import argparse

#############
# Utilities #
#############

class BetterWidget(object):
    """A BetterWidget returns itself on pack and config for call chaining."""
    def pack(self, **kwargs):
        super().pack(**kwargs)
        return self

    def config(self, **kwargs):
        super().config(**kwargs)
        return self

class TextWidget(BetterWidget):
    """A TextWidget contains a mutable line of text."""
    def __init__(self, **kwargs):
        self.textvar = kwargs.get('textvariable', tk.StringVar())
        self.config(textvariable=self.textvar)
        if 'text' in kwargs:
            self.textvar.set(kwargs['text'])

    @property
    def text(self):
        return self.textvar.get()

    @text.setter
    def text(self, value):
        return self.textvar.set(str(value))

class Label(TextWidget, tk.Label):
    """A Label is a text label."""
    def __init__(self, parent, **kwargs):
        kwargs.update(label_theme)
        tk.Label.__init__(self, parent, **kwargs)
        TextWidget.__init__(self, **kwargs)

class Button(BetterWidget, tk.Button):
    """A Button is an interactive button."""
    def __init__(self, *args, **kwargs):
        kwargs.update(button_theme)
        tk.Button.__init__(self, *args, **kwargs)

class Entry(TextWidget, tk.Entry):
    """An Entry widget accepts text entry."""
    def __init__(self, parent, **kwargs):
        kwargs.update(entry_theme)
        tk.Entry.__init__(self, parent, **kwargs)
        TextWidget.__init__(self, **kwargs)

class Frame(BetterWidget, tk.Frame):
    """A Frame contains other widgets."""
    def __init__(self, *args, **kwargs):
        kwargs.update(frame_theme)
        tk.Frame.__init__(self, *args, **kwargs)

def name(who):
    """Return the name of a player."""
    return "Player {0}".format(who)

#######
# GUI #
#######

class HogGUIException(BaseException):
    """HogGUI-specific Exception. Used to exit a game prematurely."""
    pass

class HogGUI(Frame):
    """Tkinter GUI for Hog."""

    KILL = -9   # kill signal to stop a game

    #########################
    # Widget Initialization #
    #########################

    def __init__(self, parent, computer=False):
        """Replace hog module's dice with hooks to GUI and start a game.

        parent   -- parent widget (should be root)
        computer -- True if playing against a computer
        """
        super().__init__(parent)
        self.pack(fill=BOTH)
        self.parent = parent
        self.who = 0

        self.init_scores()
        self.init_rolls()
        self.init_dice()
        self.init_status()
        self.init_restart()

        hog.six_sided = self.make_dice(6)
        hog.four_sided = self.make_dice(4)
        self.computer, self.turn = computer, 0
        self.play()

    def init_scores(self):
        """Creates child widgets associated with scoring.

        Each player has a score Label that is updated each turn. Scores can be
        accessed and modified through Tkinter variables in self.score_vars.
        """
        self.score_frame = Frame(self).pack()

        self.p_frames = [None, None]
        self.p_labels = [None, None]
        self.s_labels = [None, None]
        for i in (0, 1):
            self.p_frames[i] = Frame(self.score_frame, padx=25).pack(side=LEFT)
            self.p_labels[i] = Label(self.p_frames[i],
                        text=name(i) + ':').pack()
            self.s_labels[i] = Label(self.p_frames[i]).pack()

    def init_rolls(self):
        """Creates child widgets associated with the number of rolls.

        The primary widget is an Entry that accepts user input. An intermediate
        Tkinter variable, self.roll_verified, is set to the final number of
        rolls. Once it is updated, the player immediately takes a turn based on
        its value.
        """
        self.roll_frame = Frame(self).pack()

        self.roll_label = Label(self.roll_frame).pack()
        self.roll_entry = Entry(self.roll_frame,
                                justify=CENTER).pack()
        self.roll_entry.bind('<Return>',
                             lambda event: self.roll_button.invoke())
        self.roll_verified = IntVar()
        self.roll_button = Button(self.roll_frame,
                                  text='Roll!',
                                  command=self.roll).pack()

    def init_dice(self):
        """Creates child widgets associated with dice. Each dice is stored in a
        Label. Dice Labels will be packed or unpacked depending on how many dice
        are rolled.
        """
        self.dice_frames = [
            Frame(self).pack(),
            Frame(self).pack()
        ]
        self.dice = {
            i: Label(self.dice_frames[i//5]).
                    config(image=HogGUI.IMAGES[6]).
                    pack(side=LEFT)
            for i in range(10)
        }

    def init_status(self):
        """Creates child widgets associated with the game status. For example,
        Hog Wild is displayed here."""
        self.status_label = Label(self).pack()

    def init_restart(self):
        """Creates child widgets associated with restarting the game."""
        self.restart_button = Button(self, text='Restart',
                                     command=self.restart).pack()

    ##############
    # Game Logic #
    ##############

    def make_dice(self, sides):
        """Creates a dice function that hooks to the GUI and wraps
        dice.make_fair_dice.

        sides -- number of sides for the die
        """
        fair_dice = dice.make_fair_dice(sides)
        def gui_dice():
            """Roll fair_dice and add a corresponding image to self.dice."""
            result = fair_dice()
            img = HogGUI.IMAGES[result]
            self.dice[self.dice_count].config(image=img).pack(side=LEFT)
            self.dice_count += 1
            return result
        return gui_dice

    def clear_dice(self):
        """Unpacks (hides) all dice Labels."""
        for i in range(10):
            self.dice[i].pack_forget()

    def roll(self):
        """Verify and set the number of rolls based on user input. As
        per game rules, a valid number of rolls must be an integer
        greater than or equal to 0.
        """
        result = self.roll_entry.text
        if result.isnumeric() and 10 >= int(result) >= 0:
            self.roll_verified.set(int(result))

    def switch(self, who=None):
        """Switches players. self.who is either 0 or 1."""
        self.p_frames[self.who].config(bg=bg)
        self.p_labels[self.who].config(bg=bg)
        self.s_labels[self.who].config(bg=bg)
        self.who = 1 - self.who if who is None else who
        self.p_frames[self.who].config(bg=select_bg)
        self.p_labels[self.who].config(bg=select_bg)
        self.s_labels[self.who].config(bg=select_bg)

    def strategy(self, score, opp_score):
        """A strategy with a hook to the GUI. This strategy gets
        passed into the PLAY function from the HOG module. At its
        core, the strategy waits until a number of rolls has been
        verified, then returns that number. Game information is
        updated as well.

        score     -- player's score
        opp_score -- opponent's score
        """
        s0 = score if self.who == 0 else opp_score
        s1 = opp_score if self.who == 0 else score
        self.s_labels[0].text = s0
        self.s_labels[1].text = s1
        self.roll_label.text = name(self.who) +' will roll:'
        status = self.status_label.text
        if hog.select_dice(score, opp_score) == hog.four_sided:
            status += ' Hog Wild!'
        self.status_label.text = status

        if self.computer and self.who == self.turn:
            self.update()
            self.after(DELAY)
            result = hog.final_strategy(score, opp_score)
        else:
            self.roll_entry.focus_set()
            self.wait_variable(self.roll_verified)
            result = self.roll_verified.get()
            self.roll_entry.text = ''
        if result == HogGUI.KILL:
            raise HogGUIException

        self.clear_dice()
        self.dice_count = 0
        self.status_label.text = '{} chose to roll {}.'.format(name(self.who),
                                                               result)
        self.switch()
        return result

    def play(self):
        """Simulates a game of Hog by calling hog.play with the GUI strategies.

        If the player destroys the window prematurely (i.e. in the
        middle of a game), a HogGUIException is raised to exit out
        of play's loop. Otherwise, the widget will be destroyed,
        but the strategy will continue waiting.
        """
        self.turn = 1 - self.turn
        self.switch(0)
        self.s_labels[0].text = '0'
        self.s_labels[1].text = '0'
        self.status_label.text = ''
        try:
            score, opponent_score = hog.play(self.strategy,
                                             self.strategy)
        except HogGUIException:
            pass
        else:
            self.s_labels[0].text = score
            self.s_labels[1].text = opponent_score
            winner = 0 if score > opponent_score else 1
            self.status_label.text = 'Game over! {} wins!'.format(
                                        name(winner))

    def restart(self):
        """Kills the current game and begins another game."""
        self.roll_verified.set(HogGUI.KILL)
        self.status_label.text = ''
        self.clear_dice()
        self.play()

    def destroy(self):
        """Overrides the destroy method to end the current game."""
        self.roll_verified.set(HogGUI.KILL)
        super().destroy()

def run_GUI(computer=False):
    """Start the GUI.

    computer -- True if playing against computer
    """
    root = Tk()
    root.title('The Game of Hog')
    root.minsize(520, 400)
    root.geometry("520x400")

    # Tkinter only works with GIFs
    HogGUI.IMAGES = {
        1: PhotoImage(file='images/die1.gif'),
        2: PhotoImage(file='images/die2.gif'),
        3: PhotoImage(file='images/die3.gif'),
        4: PhotoImage(file='images/die4.gif'),
        5: PhotoImage(file='images/die5.gif'),
        6: PhotoImage(file='images/die6.gif'),
    }

    app = HogGUI(root, computer)
    root.mainloop()

##########
# THEMES #
##########

select_bg = '#a6d785'
bg='#ffffff'
fg='#000000'
font=('Arial', 14)

frame_theme = {
    'bg': bg,
}

label_theme = {
    'font': font,
    'bg': bg,
    'fg': fg,
}

button_theme = {
    'font': font,
    'activebackground': select_bg,
    'bg': bg,
    'fg': fg,
}

entry_theme = {
    'fg': fg,
    'bg': bg,
    'font': font,
    'insertbackground': fg,
}

##########################
# Command Line Interface #
##########################

DELAY=2000

@main
def run(*args):
    parser = argparse.ArgumentParser(description='Hog GUI')
    parser.add_argument('-f', '--final',
                        help='play against the final strategy in hog.py. '
                             'Computer alternates playing as player 0 and 1.',
                        action='store_true')
    parser.add_argument('-d', '--delay',
                        help='time delay for computer, in seconds', type=int,
                        default=2)
    args = parser.parse_args()
    global DELAY
    DELAY = args.delay * 1000
    run_GUI(computer=args.final)