Python: Rock, Paper, Scissors but multiple Games

Python: Rock, Paper, Scissors but multiple Games

Adding loading capabilities for multiple variants of the game

·

4 min read

This is part 3 of an ongoing series in which we try to take the usual "Rock, Paper, Scissors" example to new and interesting heights.

In this part, we'll implement loading of our variants. Last time, we implemented rock, paper, scissors, lizard, spock and created a JSON-file accordingly. Since we're loading the JSON statically within the code, we can't easily switch variants if we fancy some good old vanilla rock, paper, scissors.

Let's change that. Start by creating a JSON with the regular rock, paper, scissors modifiers:

{
    "elements": [
        {
            "lose": [
                "paper"
            ],
            "name": "rock",
            "win": [
                "scissors"
            ]
        },
        {
            "lose": [
                "scissors"
            ],
            "name": "paper",
            "win": [
                "rock"
            ]
        },
        {
            "lose": [
                "rock"
            ],
            "name": "scissors",
            "win": [
                "paper"
            ]
        }
    ],
    "filename": "vanilla.json"
}

I adjusted the filename to vanilla.json which was previously rps.json.

Back to the GUI. Some way of loading whatever variant we want to play would be nice. A great way of doing this would be the Tkinter OpenFileDialog.

One of the disadvantages of defining a Tkinter GUI using grid is that it's not very fun to modify. We could place our button right at the top and then create some spacers and other widgets but unfortunately, we would need to re-place all of our other components manually and that's not fun.

So, to make it "less unappealing" to the users eyes, we'll add two buttons right at the end (but above root.mainloop()) like so:

# row 3 open file
btn_load = Button(root, command=load, width=10, text="Load", foreground="black")
btn_load.grid(row=3, column=0)

btn_quit = Button(root, command=root.destroy, width=10, text="Quit", foreground="black")
btn_quit.grid(row=3, column=2)

Add a skeleton function for load with only pass in it and you can try it out, which results in:

Bildschirmfoto 2021-12-17 um 14.38.42.png

If you played around a little, you'll have noticed that the Quit-Button works without any defined function because we're calling the destroy()-function that is already defined. This function from Tkinter will quit the application and stop the mainloop.

The load-function is where it gets interesting and where we can figure out the filedialog - let's start by importing some more:

from os import curdir
from tkinter import filedialog

curdir is what we're using, so the filedialog knows in which directory we're currently in and filedialog is pretty self-explanatory.

Next, let's add the load function, right above our fight function:

def load():
    file = filedialog.askopenfilename(filetypes=[("JSON files", ".json")], initialdir=curdir)
    print(file)

There's really not much more to the filedialog than this one line. In our example, we want to get a simple filename since our RpsGame-class already has the capabilities to load a file. You can experiment with the different types of dialogs, there's one for directories, one for opening the file straight away, etc.

The above script will present the following dialog:

Bildschirmfoto 2021-12-17 um 14.50.50.png

This might be different, depending on your operating system. I know that in Windows, the supported filetypes are displayed a little more on the nose than in MacOS. If I choose a file and open it, the file gets printed to the console with the full path:

/Users/ahose/PythonProjects/rps.json

Ok - now we'll need to modify and polish our code a bit. We were using rps_choices and just assigning game.elements to it - we can remove the unnecessary variable and just use game.elements.

Next - before opening our file in the load-function within the RpsGame-class, we'll need to add a call to clear():

    def load(self):
        try:
            # clear everything
            self.elements.clear()
            # open the file
            f = open(self.filename, "r")

This is to ensure that whenever we load a different kind of JSON-configuration there's no leftover from the old file.

And finally, we can write our actual load()-function, this time for our main script:

# functions
def load():
    file = filedialog.askopenfilename(filetypes=[("JSON files", ".json")], initialdir=curdir)
    # reinitialize our game if a file was picked
    if file:
        game.filename = file
        game.load()
        # refresh GUI possibilities
        box.configure(values=game.elements)

We didn't reall add much to it and it kind of makes sense .. right? If file is None/undefined, we don't want to do anything, but if the user has picked a file, we load a new game. The only thing within the GUI we need to manually update for now is the values in our ComboBox.

Bildschirmfoto 2021-12-17 um 15.06.08.png

And that's basically it. Try it out, load a JSON-file and see for yourself. Bonus: there's now a Quit-Button within the app.

You might have realized that we were already optimizing and refactoring every now and then and if you're familiar with coding just a bit, there might have been some question marks on your mind.

I hope to remedy all of that in the next part - stay tuned!