Month: December 2020

Rock, Paper, Scissors in Python: Part III – Improving the GUI

Rock, Paper, Scissors in Python: Part III – Improving the GUI

In our previous article, we developed a simple Python tkinter user interface for a rock[paper-scissors game. However, if you tried the code, you might stumble on one infelicitous problem:

No buttons selected

The program comes up with no boxes checked, but if you click on the Play button, the program plays a game anyway, assuming that Scissors had been selected. Why does this happen?

When we set up the 3 radio buttons we set the IntVar to -1 so that none of the buttons would be selected.

ChoiceButton.gvar = IntVar()
ChoiceButton.gvar.set(-1)   # none selected
ChoiceButton(lbf, "Paper", 0).grid(row=0, column=0, sticky=W)
ChoiceButton(lbf,"Rock", 1).grid(row=1, column=0,sticky=W)
ChoiceButton(lbf, "Scissors", 2).grid(row=2,column=0, sticky=W)

But let’s see what happens when our code checks the index to see which button is selected:

# play one game
def playit(self):
    index  = int(ChoiceButton.gvar.get())
    self.play = Player.moves[index]

The index returned is -1 as expected. And the three moves are in a tuple:

moves=("P", "R", "S")   #tuple of legal play characters

 And what do we get when we ask for

Player.moves[-1]

Python is unusual among common computer languages in that indexes of less than zero in arrays (lists), strings and tuples mean start at the end and work backward. So the index of -1 gives us the character ‘S’ and -2 would get us ‘R’ and so forth. This also rotates around so that -4 would give us ‘S’ again. In other words, Python apparently takes the index modulo the length of the array so that negative indexing never gives us a  out of bounds error. If we had set the gvar to 3 or more, no radio button would have been selected, and an error would occur.

So, how do we prevent this infelicitous behavior, where a player can click on the Play button when no radio button has been selected?  It would be possible to trap this out-of-range indexes and prevent play from taking place, but since the program assumes that only legal plays can occur, this would take a good deal of careful checking.

Instead, we do what we should have done in the first place and disable the Play button until a radio button has been clicked.

playButton['state']=DISABLED

But how do we turn the button back on?  We recommend using a Mediator.

The Mediator Pattern

The Mediator Design Pattern is a really simple little software trick that keeps classes from having to know about the internal workings of other classes. And when you are dealing with GUI widgets, there might be a number of GUI classes that needn’t know about each other.

Instead, we create a Mediator class and tell it about the push button and the radio buttons, and let it do the work. The Mediator keeps a reference to the pushbutton and receives a call when a radio button is clicked.  The whole thing is just a few lines of code:

class Mediator() :
    def __init__(self, button):
        self.button = button    # save the button reference    

    # called when a radiobutton is selected
    def choiceClick(self):
        self.button['state']=NORMAL # enable the button

So, how do we go about connecting up this Mediator? We create the button and then create the Mediator, passing it a reference to that button:

playButton = Button(root, text="Play", command=self.playgame)
playButton.grid(row=3, column=0, pady=20)
playButton['state'] = DISABLED
med = Mediator(playButton)     # create the Mediator 

So, now the Mediator knows about the button. What about the radio buttons? You may recall that we have derived a ChoiceButton class so it can contain the gvar variable. All we need to do is pass the mediator reference to each button as we create it:

ChoiceButton(lbf, "Paper", 0, med).grid(row=0, column=0, sticky=W)
ChoiceButton(lbf, "Rock", 1, med).grid(row=1, column=0, sticky=W)
ChoiceButton(lbf, "Scissors", 2, med).grid(row=2, column=0, sticky=W)

And the ChoiceButtpm itself creates a command that calls choiceClick in the Mediator:

class ChoiceButton(Radiobutton):
    gvar = None
    def __init__(self, rt, label, index, med):
        super().__init__(rt, text=label,
                         variable=ChoiceButton.gvar,
                         command=self.comd,
                         value=index)
        self.med = med  # Save the Mediator reference

    def comd(self):
         self.med.choiceClick() #call the Mediator

So anytime a ChoiceButton is clicked, it calls choiceClick and that enables the Play button.

Summary

So, to summarize, we use the Mediator to take care of classes that need to send each other information. In this case, clicks on the radio buttons tell the Mediator to enable the Play button. Before that it is disabled. In general, the Mediator is an excellent solution when two or more classes need to tell each other about click events and the like and prevents you having to write GUI classes that know about other GUI classes, and thus getting entangled.

Code for this and the previous two articles can be found on GitHub at jameswcooper/articles

Rock, Paper Scissors in Python: Part II,  Building a GUI

Rock, Paper Scissors in Python: Part II, Building a GUI

In our first article, we designed a Rock, Paper, Scissors game that works from the keyboard. It printed out messages and shows the total score when you quit. But even a game this simple will benefit from a graphical user interface (GUI). Here we show how to use the Tkinter toolkit to create that simple interface. Specifically, we are going to use the tkinter.ttk toolkit, because it has a nice LabelFrame widget to hold the three RadioButtons.  It looks like this:

You can select the three possible plays from the radio buttons in the “Plays” LabelFrame, and then click on the Play button to have the program player select its move and tell you who won.  The messages from the program are shown in the Listbox on the right, showing what the Auto player has selected, and who wins. The total score for the two players is shown in the blue label above the Listbox.

Creating the GUI

For simple programs like this, we usually create a Builder class with a build method and a few other methods as needed. Here the only method we’ll need is the playit method, that triggers one round of play.

 Here is the program’s outline:

import tkinter as tk
from random import randint
from tkinter import *
from tkinter.ttk import *

class Builder():
    # play one game
    def playgame(self):
        # players play game

    # create window     
    def build(self):
       root = tk.Tk()
       root.geometry("300x200")
       root.title("Rock paper scissors")
        #UI creation code   
        mainloop()     # start the window

# Program starts here
if __name__== "__main__":
    bld = Builder()
    bld.build()

Adding GUI Controls

We use the Grid layout to arrange the GUI widgets, with the LabelFrame and Play  button in column 0 and the score label and Listbox in column 1. The three radio buttons are inserted into the label frame inside their own three line grid.

The only sneaky part is the way the Radio Buttons work. All of radio buttons in a group use the same variable to tell you which button has been selected. That variable is an IntVar, an integer wrapped in an object that is required by the underlying tk toolkit. So, when you create each Radiobutton, you have to pass it a reference to that IntVal variable, and the value that that button will pass to the IntVar, usually small integers, such 0, 1 and 2.

The design question we have to solve is where that IntVar variable will be located so each of the RadioButtons can access it. The obvious place is right inside the radio button code itself. So we derive a new ChoiceButton from Radiobutton that contains the class level variable gvar. Now all instances of the ChoiceButton have access to that same variable, and can change it to 0, 1 or 2 depending on which button is clicked. Here’s the whole new ChoiceButton class.

class ChoiceButton(Radiobutton):
    gvar = None  # will be set to an IntVar

    def __init__(self, rt, label, index):
        super().__init__(rt, text = label,
                         variable =ChoiceButton.gvar,
                         value=index)
        self.text = label
        self.index=index

When we create the GUI, we will set that gvar to an IntVar. The complete GUI code is

def build(self):
    root = tk.Tk()
    root.geometry("300x200")
    root.title("Rock paper scissors")
    # create a label frame for the radio buttons
    lbf = LabelFrame(root, text="Plays")
    lbf.grid(row=0, column=0, rowspan=3, pady=10, padx=10)
    
# create 3 radio buttons, but set none as selected

    ChoiceButton.gvar = IntVar()
    ChoiceButton.gvar.set(-1)   # none selected
    ChoiceButton(lbf, "Paper", 0).grid(row=0, column=0, sticky=W)
    ChoiceButton(lbf,"Rock", 1).grid(row=1, column=0,sticky=W)
    ChoiceButton(lbf, "Scissors", 2).grid(row=2,column=0, sticky=W)

    # create Play button - calls playgame method
    playButton = Button(root,text="Play", command=self.playgame)
    playButton.grid(row=3,column=0,pady=20)

    # create score label
    self.scoreLabel = Label(text="scores", foreground="blue")
    self.scoreLabel.grid(row=0, column=1)

    # create listbox
    self.mesgList = Listbox(root, width=30)
    self.mesgList.grid(row=1, column=1, rowspan=3)

    # create two players
    self.player1 = Player("You")
    self.player2 = AutoPlayer("Auto", self.mesgList)
    mainloop()

Note that we use the same Player and AutoPlayer classes as in the previous example;. The only difference is that the AutoPlayer adds a message to the Listbox instead of printing it on the console:

class AutoPlayer(Player):
    def __init__(self, name, list):
        super().__init__(name)
        self.list = list

    def playit(self):
        playval = randint(0, 2)
        self.play = Player.moves[playval]
        self.list.insert(END, self.name + " selects " + self.play)
        self.list.yview(END)

The Player class differs only in its playit method, which obtains the selected Radiobutton from the index in gvar. Note the yview(END) method on the Listbox, which scrolls to the bottom of the list so that the last line always shows.

# play one game
def playit(self):
    playval = ChoiceButton.gvar.get()
    index  = int(ChoiceButton.gvar.get())
    self.play = Player.moves[index]

And the playGame method of the Builder class differs only in that the winner is added to the Listbox, and the label is updated to show the current score:

def playgame(self):
   self.player1.playit()    # read buttons
   self.player2.playit()    # calc random play  
# compute winner
   winner = Winner(self.player1, self.player2)
   self.mesgList.insert(END, winner.findWin())  # print winner
   self.mesgList.yview(END) #move to bottom of list 
# show current score
   self.scoreLabel.config(text="You: "
                            +str(self.player1.wincount)+"--- Auto: "
                            +str(self.player2.wincount))

Conclusion

The differences in the console game and the GUI version are pretty small. The Player class gets your play from the radio button selected, and all the messages are added to the Listbox. The running score is changed in the scoreLabel each time.

All of the code for this example can be found in Github at jameswcooper/articles/rockpaper

Rock, Paper, Scissors Game in Python: Part I

Rock, Paper, Scissors Game in Python: Part I

You probably have played the little hand game Rock, Paper, Scissors one in a while. Essentially two players bring up a fist, a flat hand ot two fingers to represent rock, paper or scissors. The winner is based on these three simple rules:

  • Rock breaks scissors
  • Scissors cuts paper
  • Paper covers rock

In the case of a tie, no one wins.

This little game is easy to create in Python using the keyboard to enter R, P or S, or Q to quit. The second player is the program itself which uses a random number generator to choose one of the three outcomes. This little game is sometimes given as an early assignment, but beginners usually miss some of the really clever features of Python that can make the game more elegant and succinct. We’ll illustrate tuples, classes and subclasses, and dictionaries in this simple article.

We can start by using a tuple to represent the three possible moves:

moves=("P", "R", "S")   #tuple of legal play characters

These are the only three characters that the player should be allowed to enter. So we can create a simple entry loop that repeats until one of those three characters is entered. We use the upper method to make sure the entry is uppercase. Note that the code “play in moves” returns True if the character entered is P, R or S and otherwise returns False.

legal = False
while not legal:
    play = input("enter play (P, R, S): ").upper()
    legal = play in moves 

 And, if you want to allow the player to enter “Q” to quit the game you could check for that Q and set a quit flag:

def playit(self):
    legal = False
    while not legal:
        play = input("enter play (P, R, S): ").upper()
        if play=="Q":
            quit = True
        legal = play in Player.moves or quit

 As you can see, this is now a function.  But why not make it part of a class?

You may recall that classes are a major part of Python. Classes are components that can contain data, and can exist in many instances with differing data in each.

A Player class

Our complete player class has the name of the player and a play variable containing the latest play. It also keeps the state of the quit flag and the legal flag.

class Player():
    moves=("P", "R", "S")   #tuple of legal play characters

    def __init__(self, name):
        self.name = name    #initialize name,quit, and count
        self.quit = False
        self.wincount = 0   #counter of wins

    def playit(self):
        legal = False
        while not legal:
            self.play = input("enter play (P, R, S): ").upper()
            if self.play=="Q":
                self.quit = True

            legal = self.play in Player.moves or self.quit
 # the number of wins is counted here
def countWin(self):
    self.wincount += 1
 

So, as you can see, the Player class has a playit method which sets the play variable to P, R or S. It also can be set to Q, and the quit flag indicates that this is not a real play. Note that the moves tuple is a class variable. This means it is part of all classes of this type and you can access it by Player.moves.

But, what about the other player? It is the computer program itself, and it uses the randint method to select 0, 1 or 2 and these indices indicate which of the three plays has been choses. So, we need another class like Player for the computer player.

As you may recall, you can derive new classes from old ones, so we create an AutoPlayer class derived from Player. It only contains the new playit method. All the other variables and methods are in the base Player class. So, our simple AutoPlayer class is just

class AutoPlayer(Player):
    def __init__(self, name):
        super().__init__(name) # pass the name into the parent class

    def playit(self):
        playval = randint(0, 2)     # select 0, 1 or 2
        self.play = Player.moves[playval] # get that play from the tuple
        # print out what it has selected
        print(self.name + " selects " + self.play)

So, now, we see how we can write a loop for you to play the game:

player1 = Player("You")
player2 = AutoPlayer("Auto")
while not player1.quit: #loop until a 'Q' is entered
    player1.playit()
    if not player1.quit:
        player2.playit()

Picking a Winner

But how do we know who won? This is really simple, since there are only 3 cases (plus the tie) to check for.  We can create a Dictionary of these cases and the resulting message you print out:

game= {"RS": "rock breaks scissors",
       "SP": "scissors cuts paper",
       "PR": "paper covers rock"
       }

We only need to check for ties and check for who wins, by combining the string for the two players.We do all this within a Winner class which then returns the outcome:  First we initialize the variables:

class Winner():
    game= {"RS": "rock breaks scissors",
           "SP": "scissors cuts paper",
           "PR": "paper covers rock"
           }

    def __init__(self, p1, p2):
        # copy variables into class
        # to make this simpler to read
        self.p1 = p1
        self.p2 = p2

Note that the game dictionary is also a class variable.

Then, the findWin method checks for ties and if there is no tie, it calls checkWin to check if p1 beat p2 or p2 beat p1:

# A winner matches on of the three
# game dictionary entries abouve
# we check p1+2 and p2+p1
def checkWin(self, p1, p2):
    # here are the two outcomes
    # for both player orders
    match1 = p1.play + p2.play
    match2 = p2.play + p1.play
    mesg = Winner.game.get(match1)
    if mesg != None:
        p1.countWin()
        outcome = p1.name + " win -- "+mesg
    else:
        mesg = Winner.game.get(match2)
        p2.countWin()
        outcome =  p2.name + " win -- " + mesg
    return outcome

def findWin(self):
    if self.p1.play == self.p2.play:
       return "Tie, no winner"
    else:
        return self.checkWin(self.p1,self.p2)

Note that Player class contains a countWin method that increments the winner count of that player won. Then, when you finally type ‘Q’ the program can print out the final score.

player1 = Player("You")
player2 = AutoPlayer("Auto")
while not player1.quit: #loop until a 'Q' is entered
    player1.playit()
    if not player1.quit:
        player2.playit()
        # compute the winner
        winner = Winner(player1, player2)
        print(winner.findWin()) # print winner

# print out the wins for each player
# when you type "q" for quit
print(player1.name, player1.wincount)
print(player2.name, player2.wincount)

The final output

Here is how the program works:

enter play (P, R, S): r
Auto selects R
Tie, no winner
enter play (P, R, S): r
Auto selects S
You win -- rock breaks scissors
enter play (P, R, S): q
You 2
Auto 2

Summary

We used the tuple to check for legal input (P,R or S) and then created a Player class which has a playit method and contains a play variable which contains the charact representing that play. Then we created a derived AutoPlayer class which has the same internal variables and methods, but where the playit method uses a random number function to generate 0, 1 or 2. These are then indexes into the tuple  to find the character representing that play.

Finally, we created a dictionary of the three possible plays and the message describing the winning cases and used the Winner class to find if p1+p2 won or id p2+p1 won. Thus, in this simple example we used the tuple, the dictionary, classes and a derived class to write a simple game program.

The complete code for this and the programs in the following articles is available at GitHub under jameswcooper/articles