Python: Rock, Paper, Scissors but GUI

Python: Rock, Paper, Scissors but GUI

a different take on the good old RPS tutorial

·

7 min read

One of the most overused examples for tutorials is making a "rock, paper, scissors" game. I mean, it covers user input, comparisons, if/else, lists/arrays, etc. but .. it's just not interesting.

So, the other day I had the idea to make the same old tutorial but skip the first step (meaning console application) that everybody does and just make it a lot more interesting.

This will likely be a series to not overwhelm anyone and in this part, we'll start with the base application and adding a GUI to it. We'll be using Python and Tkinter. Let's go!

We'll start by creating the main window in Tkinter (ignore the imports for now, they'll become important later):

from tkinter import *
from tkinter import ttk
import random

root = Tk()
root.title("Rock Paper Scissors")

root.mainloop()

That's a pretty nice but empty window. Before adding any components, let's quickly add some variables and a skeleton function. Every code we're going to write in this article should go below root.title(..) and above root.mainloop().

# possible choices
rps_choices = ["rock", "paper", "scissors"]

# functions
def fight():
    pass

rps_choices contains our possible choices for this example as a list and the function fight is a very dramatic placeholder for our evaluation of the game.

Let's get into the GUI. I'm thinking of having some labels with "player", "vs" and "opponent", some form of choice-list for the player to pick and a button for evaluation (I mean fighting obviously).

Now, we could do this with the old widget.pack() function of Tkinter but this would likely look very bad. Since we won't alter the GUI dynamically and we kind of know where things should go, we'll use a grid:

# row 0
lbl_player = Label(root, text="Player:", width=16, foreground="black", background="green")
lbl_player.grid(row=0, column=0)

lbl_vs = Label(root, text=" .. VS .. ", width=16, foreground="black")
lbl_vs.grid(row=0, column=1)

lbl_opponent = Label(root, text="Opponent:", width=16, foreground="black", background="blue")
lbl_opponent.grid(row=0, column=2)

note: the foreground="black" is not really required, it's only because Tkinter and MacOS darkmode aren't friends

In the above code we're creating the individual components and we're adding them to our root, meaning the Tk instance. The interesting part is the second line of each component when we're using .grid(..).

.grid(..) places our widget on an invisible grid that we're defining for our application. Hence the "row 0" comment - think of the whole thing like an Excel-spreadsheet and in the first row we only want our header-elements. Since there are more than one widget, we'll advance the column for each of them.

The above code should produce a window similar to this:

Bildschirmfoto 2021-12-16 um 12.43.44.png

As you can see, everything's neatly lined up in one row, column after column. This will get more interesting now that we're adding another row:

# row 1
box = ttk.Combobox(root, values=rps_choices, width=12, foreground="black", state="readonly")
box.grid(row=1, column=0)
box.current(0)

btn_fight = Button(root, command=fight, width=10, text="Fight!", foreground="black")
btn_fight.grid(row=1, column=1)

lbl_op_selection = Label(root, text=" .. ", width=16, foreground="black")
lbl_op_selection.grid(row=1, column=2)

So, for this piece of code, there's some more explaining to do. The first widget is part of ttk (which is why we imported it right at the start) and is your basic ComboBox-component. Basically a list-dropdown with set values for choosing - well, usually a ComboBox also enables the user to type in values but in our case we don't really need (or want) that, so we're using state="readonly" to prevent it.

ComboBoxes are really neat in that you can simply assign an already constructed list - like our rps_choices - as its values.

As far as the grid goes, you can see we're starting back at column=0 but using row=1 to place the ComboBox directly underneath our player-label. box.current(0) selects the first element in the list of the ComboBox.

Next up is the button, which we're giving the fight-skeleton function as the command parameter and then another label for the choice the opponent will make.

Running that code should produce a window like this:

Bildschirmfoto 2021-12-16 um 12.53.34.png

Great! So far, so good. As you can see in the screenshot above, I already went ahead and added another row, so let's also add that together:

# row 2
lbl_result = Label(root, text="", foreground="black")
lbl_result.grid(row=2, column=0, columnspan=3)

This will produce an empty label for our result to be displayed. On the grid we're adding another row (2) for this label and to make sure it's somewhat in the center and not just glued to the left, we're using "columnspan". The same thing exists for rows (rowspan) and basically means this widget is taking up this many columns/rows - as in it will span across 3 columns.

If you're not sure, just add some text to the widget and play around with the columnspan for a bit.

OK - we're almost there. Scroll back to our "fight"-function and remove the pass and instead add the following:

    # player
    player = box.get()
    opponent = random.choice(rps_choices)
    # update opponent choice
    lbl_op_selection["text"] = opponent

box is our ComboBox and .get() simply returns the text of the current selection, which will be our player variable for comparison. opponent is event easier. We'll use the random-module to simply choose a random element from a list. A list you say? Well, let's simply use our rps_choices list again. This will be important later in the series.

Finally, since it doesn't seem like fair play to simply tell the player if he lost or won, we'll update the text of the label for the opponents choice with what he chose. You can run our code so far and it'll work but you'll have to figure out who won on your own.

That's boring, so let's add an evaluation in the fight-function:

    # get the result
    result = "win"      # also possible loss and draw
    if(player == opponent):
        result = "draw"
    elif(player == "rock"):
        result = "win" if (opponent == "scissors") else "loss"
    elif(player == "paper"):
        result = "win" if (opponent == "rock") else "loss"
    elif(player == "scissors"):
        result = "win" if (opponent == "paper") else "loss"

We're very optimistic, so we'll always start out by assuming the player wins. From there, there's really only 3 choices left to make:

  1. Draw : player and opponent picked the same, so the result is a draw
  2. Win : player picked rock, paper, scissors and the opponent scissors, rock, paper
  3. Loss : player picked scissors, rock, paper and the opponent rock, paper, scissors

Most tutorials at this point will use a very convoluted if-else-statement for every possible combination but Python has this really nice way of doing exactly what we want.

If we ruled out number 1 (the draw) there's only 2 choices left, so the code is only depending on what the player chose.

    elif(player == "rock"):
        result = "win" if (opponent == "scissors") else "loss"

In the above snippet, the player chose rock and we already know it's not a draw, so if the opponent chose scissors we win and if not, we lose. The code "reads" like result is win if opponent is scissors and if not, result is loss.

Ok, so we're almost there, we just need to update the GUI:

    # announce the result to the player
    if(result == "win"):
        lbl_result["text"] = "You win!"
        lbl_result.configure(background="green")
    elif(result == "loss"):
        lbl_result["text"] = "You lose!"
        lbl_result.configure(background="red")
    elif(result == "draw"):
        lbl_result["text"] = "Oh .. a draw .."
        lbl_result.configure(background="grey")

At this point in our "game", the result can only contain win, loss or draw. So we'll go through the possible results and display a corresponding text on our result-label. For fun, we'll also color in the background.

Playing the game will now result in this:

Bildschirmfoto 2021-12-16 um 13.09.13.png

That's it. We made Rock, Paper, Scissors and (hopefully) made it more interesting by adding a GUI.

Can't wait to show what I've planned for the next parts in this series.

If you're here for some goold ol' copy pasting, here's the code in its entirety:

from tkinter import *
from tkinter import ttk
import random

root = Tk()
root.title("Rock Paper Scissors")

# possible choices
rps_choices = ["rock", "paper", "scissors"]

# functions
def fight():
    # player
    player = box.get()
    opponent = random.choice(rps_choices)
    # update opponent choice
    lbl_op_selection["text"] = opponent
    # get the result
    result = "win"      # also possible loss and draw
    if(player == opponent):
        result = "draw"
    elif(player == "rock"):
        result = "win" if (opponent == "scissors") else "loss"
    elif(player == "paper"):
        result = "win" if (opponent == "rock") else "loss"
    elif(player == "scissors"):
        result = "win" if (opponent == "paper") else "loss"

    # announce the result to the player
    if(result == "win"):
        lbl_result["text"] = "You win!"
        lbl_result.configure(background="green")
    elif(result == "loss"):
        lbl_result["text"] = "You lose!"
        lbl_result.configure(background="red")
    elif(result == "draw"):
        lbl_result["text"] = "Oh .. a draw .."
        lbl_result.configure(background="grey")


# row 0
lbl_player = Label(root, text="Player:", width=16, foreground="black", background="green")
lbl_player.grid(row=0, column=0)

lbl_vs = Label(root, text=" .. VS .. ", width=16, foreground="black")
lbl_vs.grid(row=0, column=1)

lbl_opponent = Label(root, text="Opponent:", width=16, foreground="black", background="blue")
lbl_opponent.grid(row=0, column=2)

# row 1
box = ttk.Combobox(root, values=rps_choices, width=12, foreground="black", state="readonly")
box.grid(row=1, column=0)
box.current(0)

btn_fight = Button(root, command=fight, width=10, text="Fight!", foreground="black")
btn_fight.grid(row=1, column=1)

lbl_op_selection = Label(root, text=" .. ", width=16, foreground="black")
lbl_op_selection.grid(row=1, column=2)

# row 2
lbl_result = Label(root, text="", foreground="black")
lbl_result.grid(row=2, column=0, columnspan=3)

root.mainloop()