Scrollable Nav Bar

Rock Paper Scissors Game in Python (Tkinter)

A lot of people learn Python by writing console programs, but the moment you add a graphical interface, your projects start feeling like real applications. That’s why building a Rock Paper Scissors game in Python with Tkinter is such a strong beginner‑to‑intermediate project: it is familiar, fun, and small enough to finish—yet it teaches core GUI skills that are used in larger desktop apps too. In this tutorial, you will build a complete Tkinter game that includes a neat window layout, buttons that react to user clicks, a difficulty selector, score tracking, and a one‑time “Power Up” feature. Along the way, you’ll understand not only what to write, but why the code is structured this way—which matters a lot for students who are learning, and for developers who care about maintainability.

This guide is designed for a wide audience: students who want a clear, step‑by‑step path; professionals who want clean separation of UI and logic; and developers who want a project they can extend into a portfolio‑grade application. You will see how Tkinter works as an event‑driven system (meaning the program mostly “waits” for the user to click something), how state is stored and updated, and how the UI stays in sync with the game rules.


What You Will Build

By the end, you’ll have a desktop window that looks and behaves like a simple game application. The interface will show a title, a difficulty dropdown (Easy/Medium/Hard), a live score label, three move buttons (Rock, Paper, Scissors), a Power Up button that can be used once per match, and two utility buttons (New Game and Exit). Each time the player clicks a move button, the computer selects its move, the program decides who wins, your score updates instantly, and a popup dialog explains what happened in that round. When either the player or the computer reaches 5 points, the game ends with a final message and resets automatically.

The best part is that you’ll build this in a single file with standard Python—no extra frameworks needed—and you’ll be able to run it as a normal .py script.


Prerequisites

You do not need advanced Python to build this project, but you should be comfortable with basic programming concepts: variables, functions, conditionals (if/else), importing modules, and running a Python file from your editor or terminal. If you have never used Tkinter before, that’s absolutely fine—this tutorial introduces the key concepts as you go.

For tools, you can use any Python editor, but a few common options are VS Code, PyCharm, or Anaconda (though GUI apps are best run as a Python script rather than inside a notebook). The important part is that you can execute a .py file. Tkinter comes bundled with most standard Python installations, so you usually don’t need to install anything extra.


Full Project Structure

This project is intentionally kept simple and beginner‑friendly, so it uses a single file:

  • rock_paper_scissors.py

In professional projects, you might separate UI, constants, and game logic into multiple modules. But for learning—and for publishing a tutorial—one file is ideal because readers can copy, run, and understand everything in one place.


Import Libraries (Why Each Import Matters)

At the very top of the file, we import what we need. These imports are not random—they directly map to features in the application.

import random
import tkinter as tk
from tkinter import messagebox
Code language: JavaScript (javascript)

The random module gives us randomness so the computer can choose different moves each round. tkinter is Python’s standard GUI toolkit; it provides the window, widgets (labels and buttons), and layout system. We import it as tk purely for convenience, because tk.Label and tk.Button are easier to read than tkinter.Label and tkinter.Button. Finally, messagebox gives us simple popup dialogs to show round results and game over messages—these popups make the app feel more interactive and “game‑like” compared to printing text in a console.

If you are a student, it’s worth remembering that most GUI apps are built from small building blocks like this: an interface layer (Tkinter), a bit of randomness for game behavior, and a messaging layer for feedback.


Define Game Constants and Global State (Understanding What Changes)

Before we build the UI, we define the game’s choices, difficulty configuration, and state variables. This section is the “data foundation” of the whole program.

choices = ["Rock", "Paper", "Scissors"]
levels = {"Easy": 0.3, "Medium": 0.5, "Hard": 0.7}  # Probability of AI playing optimally
player_score = 0
computer_score = 0
power_up_used = False
Code language: PHP (php)

The choices list is straightforward: these are the three valid moves. The levels dictionary deserves a deeper explanation because it is where difficulty is modeled. Each difficulty label maps to a probability between 0 and 1. When the computer chooses a move, the program uses this probability to decide whether the AI should attempt an “optimal” play or simply pick randomly. On Easy, the computer behaves more randomly; on Hard, it is more likely to behave strategically (in this code, the strategy function is a placeholder, and you can improve it later).

The variables player_score, computer_score, and power_up_used represent the mutable state of the game. These values change while the program is running, and the UI must reflect their current values at all times. In many production designs, developers store this state on the class instance (self.player_score), because global state can become harder to manage as projects grow. However, for a learning project, global state is acceptable because the app is small and easy to reason about.

A useful mental model is: constants like choices rarely change, configuration like levels usually stays stable, but scores and flags change continuously as the user plays.


Build a Class‑Based Tkinter App (Why Use a Class?)

The next step is organizing the application into a class. A class helps keep UI elements and logic together, and it makes the code easier to expand later.

class RockPaperScissorsGame:
    def __init__(self, root):
        self.root = root
        self.root.title("Rock Paper Scissors Game")
        self.root.geometry("500x500")
        self.level = "Medium"
        self.create_widgets()

The __init__ method runs once when we create the game. It receives root, which is the main Tkinter window, and stores it as self.root so every other method can use it. It sets a title to make the window look professional, sets a fixed window size, chooses a default difficulty, and then calls create_widgets().

From a software design perspective, this is a clean pattern: initialization sets up the environment, then UI creation happens in a dedicated method. That separation is valuable because it keeps your constructor short and readable.


Create Widgets and Layout (How the GUI Is Built)

Tkinter apps are made by creating widgets (Label, Button, Frame, OptionMenu) and placing them on the window using geometry managers like pack() and grid(). In this project, we use both: pack() for vertical stacking and grid() for arranging the move buttons in a row.

Title Label

self.label = tk.Label(self.root, text="Rock, Paper, Scissors!", font=("Arial", 16))
self.label.pack(pady=10)
Code language: PHP (php)

This label gives the app a clear heading. The font option makes it readable, and pady=10 adds breathing space. Small layout touches like padding make the UI feel more polished—this is something both students and professional developers should practice.

Difficulty Dropdown with StringVar

self.level_var = tk.StringVar(value=self.level)
tk.Label(self.root, text="Select Difficulty Level:").pack()
tk.OptionMenu(self.root, self.level_var, *levels.keys()).pack()
Code language: PHP (php)

Tkinter uses special variable wrappers like StringVar to connect widget values to your program. Here, self.level_var stores the selected difficulty. The OptionMenu displays the keys from the levels dictionary (Easy, Medium, Hard), and because it is bound to StringVar, the current selection can be read anytime using self.level_var.get().

This approach is important in GUI programming: widgets have their own state, and you need a clean way to retrieve it. Using StringVar avoids messy manual tracking.

Score Label (Keeping UI in Sync)

self.score_label = tk.Label(
    self.root,
    text=f"Score - You: {player_score} | Computer: {computer_score}",
    font=("Arial", 12)
)
self.score_label.pack(pady=10)
Code language: PHP (php)

This label displays a “live” view of the scores. At the start, both are zero. After each round, we update this label using .config(...). In real applications, this pattern is everywhere: store data in variables, then update UI elements when that data changes.

Move Buttons in a Frame (Why Frame + Grid?)

self.buttons_frame = tk.Frame(self.root)
self.buttons_frame.pack()

self.rock_btn = tk.Button(self.buttons_frame, text="Rock", command=lambda: self.play("Rock"))
self.rock_btn.grid(row=0, column=0, padx=5, pady=5)

self.paper_btn = tk.Button(self.buttons_frame, text="Paper", command=lambda: self.play("Paper"))
self.paper_btn.grid(row=0, column=1, padx=5, pady=5)

self.scissors_btn = tk.Button(self.buttons_frame, text="Scissors", command=lambda: self.play("Scissors"))
self.scissors_btn.grid(row=0, column=2, padx=5, pady=5)
Code language: PHP (php)

We use a Frame as a container so that the move buttons can be managed together. Inside the frame, we use grid() because grid is excellent for row/column layouts. This is a good example of mixing layout systems correctly: use pack() for big vertical sections, then use grid() inside a frame for structured groups.

Each button uses command=lambda: self.play("Rock") (or Paper/Scissors). The lambda is critical because command=self.play("Rock") would call the function immediately when the UI loads. Instead, the lambda delays execution until the user clicks the button, which is exactly what we want in event‑driven programming.

Power Up, New Game, and Exit Buttons

self.power_up_btn = tk.Button(self.root, text="Power Up (1-time Use)", command=self.use_power_up)
self.power_up_btn.pack(pady=10)

self.restart_btn = tk.Button(self.root, text="New Game", command=self.restart_game)
self.restart_btn.pack()

self.quit_btn = tk.Button(self.root, text="Exit", command=self.exit_game)
self.quit_btn.pack()
Code language: PHP (php)

These buttons turn a basic game into a more complete “application.” The Power Up introduces a special feature and teaches you how to enable/disable widgets dynamically. New Game teaches how to reset state and refresh UI. Exit demonstrates clean shutdown. Together, they represent the kind of UX features users expect even in small apps.


Exiting the Application (A Clean Close)

def exit_game(self):
    self.root.destroy()
Code language: CSS (css)

destroy() closes the Tkinter window and ends the program gracefully. In desktop apps, it’s always better to close cleanly than to force‑kill the process, because it ensures resources are released and the event loop stops properly.


The Core Gameplay Loop (What Happens When You Click a Move)

In GUI programs, there is no traditional while loop that continuously asks for input. Instead, Tkinter runs an event loop (mainloop) and waits for user actions. The play() method is called when the user clicks Rock, Paper, or Scissors. This method coordinates the entire round.

def play(self, player_choice):
    global player_score, computer_score, power_up_used
    computer_choice = self.get_computer_choice()
    result = self.determine_winner(player_choice, computer_choice)

    if result == "win":
        player_score += 1
    elif result == "lose":
        computer_score += 1

    self.score_label.config(text=f"Score - You: {player_score} | Computer: {computer_score}")
    messagebox.showinfo("Round Result", f"You chose {player_choice}
Computer chose {computer_choice}
Result: You {result}!")

    if player_score == 5:
        messagebox.showinfo("Game Over", "Congratulations! You won!")
        self.restart_game()
    elif computer_score == 5:
        messagebox.showinfo("Game Over", "Game Over! The computer won!")
        self.restart_game()
Code language: PHP (php)

Let’s break the flow down in a way that is easy to visualize. First, the player’s move is already known because the button click passed it in. Next, the computer’s move is generated based on the selected difficulty. After that, the program calculates the winner using a dedicated rules function. If the player wins, the player score increases; if the player loses, the computer score increases; ties do not change scores. Then the UI is updated immediately by changing the score label text. Finally, a popup explains the round result so the user doesn’t have to guess what happened.

The last part checks for a “match end” condition: if either score reaches 5, the game announces the winner and resets. This is a clean approach because it keeps the rule “first to 5 wins” in one place, and it ensures the UI and state always remain consistent.


Computer Choice and Difficulty (Probability‑Driven Behavior)

The function below is where the difficulty level actually affects gameplay.

def get_computer_choice(self):
    difficulty = self.level_var.get()
    if random.random() < levels[difficulty]:  # AI plays optimally sometimes
        return self.optimal_choice()
    return random.choice(choices)
Code language: PHP (php)

self.level_var.get() reads whatever the user selected in the dropdown. Then we generate a random number between 0 and 1. If that random number is less than the probability stored in levels[difficulty], the AI uses optimal_choice(). Otherwise, it chooses randomly.

This approach is a practical, beginner‑friendly way to simulate difficulty without writing complex AI. Even many commercial games use probability and weighting to create “believable” difficulty rather than pure perfect play.

About optimal_choice() in this code

def optimal_choice(self):
    return random.choice(choices)
Code language: PHP (php)

In the current version, optimal_choice() still returns random moves. Think of it as a placeholder or a “hook” where you can later add smarter AI logic. For example, you could track the player’s last move and bias the computer toward the counter move, or keep frequency counts of player moves and adapt over time. Professionals often design systems with hooks like this because it makes future upgrades simpler.


Winner Decision Function (Encapsulating Game Rules)

def determine_winner(self, player, computer):
    if player == computer:
        return "tied"
    if (player == "Rock" and computer == "Scissors") or \
       (player == "Paper" and computer == "Rock") or \
       (player == "Scissors" and computer == "Paper"):
        return "win"
    return "lose"
Code language: PHP (php)

This method isolates the rules of Rock Paper Scissors so they don’t get mixed into UI code. That separation is important: you should be able to test rule logic without touching GUI. The logic checks for a tie first because that is the simplest condition. Then it checks the three player‑win combinations. If the round is not a tie and not a win, then by elimination it must be a loss.

For students, this is a good example of writing clear conditional logic. For developers, it highlights a simple “rules engine” pattern—small, deterministic function, easy to unit test later.


One‑Time Power Up (State Flags and Button Disabling)

Power ups are common in games because they add strategy and variety. In this project, the Power Up is a one‑time action per match. Implementing it teaches you two valuable GUI skills: using a boolean flag to prevent repeated use, and disabling a button so the UI matches the rules.

def use_power_up(self):
    global power_up_used
    if power_up_used:
        messagebox.showinfo("Power Up", "You have already used the Power Up!")
        return
    power_up_used = True
    self.play("Rock")  # Guarantees a win against Scissors
    messagebox.showinfo("Power Up", "Power Up used! You played Rock this turn.")
    self.power_up_btn.config(state=tk.DISABLED)
Code language: PHP (php)

The method first checks the flag. If the power up has already been used, the program shows a message and exits early. If not, it sets power_up_used = True so it cannot be used again. Then it triggers a round by calling self.play("Rock"), which means the player’s move is forced to Rock for that round. After the round ends, the button is disabled so the interface visually communicates the one‑time rule.

One important learning note: the comment says “guarantees a win against Scissors,” which is only true if the computer chooses Scissors. In practice, Rock can still lose to Paper. This is not a problem for the tutorial; it’s actually a good point for learners because it shows how a feature can be improved later (for example, by changing power up behavior to adapt to the computer’s move).


Restarting the Game (Resetting State + Refreshing UI)

Resetting a game is more than just setting scores to zero. You also have to restore UI elements to their initial state, like re‑enabling buttons.

def restart_game(self):
    global player_score, computer_score, power_up_used
    player_score = 0
    computer_score = 0
    power_up_used = False
    self.power_up_btn.config(state=tk.NORMAL)
    self.score_label.config(text=f"Score - You: {player_score} | Computer: {computer_score}")
Code language: PHP (php)

This method resets all state variables and brings the UI back to the starting conditions. The Power Up button is re‑enabled, and the score label is updated to show zeros again. This is a strong example of keeping state and UI synchronized—if you reset one but forget the other, the program becomes confusing or buggy.


Running the App (Understanding mainloop())

if __name__ == "__main__":
    root = tk.Tk()
    game = RockPaperScissorsGame(root)
    root.mainloop()
Code language: JavaScript (javascript)

This block is how you start the application. The tk.Tk() call creates the main window. Then we create our game class instance and pass the window into it so the class can attach widgets to it. Finally, root.mainloop() starts Tkinter’s event loop. Once mainloop is running, Tkinter listens for events like button clicks and dropdown changes, and it calls your functions (like play() and use_power_up()) when those events happen.

A common beginner mistake is forgetting mainloop(). Without it, the window may appear briefly and then close instantly because the script ends.


Full Code (Copy & Run)

Below is the complete code exactly as used in this tutorial.

import random
import tkinter as tk
from tkinter import messagebox

# Game variables
choices = ["Rock", "Paper", "Scissors"]
levels = {"Easy": 0.3, "Medium": 0.5, "Hard": 0.7}  # Probability of AI playing optimally
player_score = 0
computer_score = 0
power_up_used = False


class RockPaperScissorsGame:
    def __init__(self, root):
        self.root = root
        self.root.title("Rock Paper Scissors Game")
        self.root.geometry("500x500")
        self.level = "Medium"
        self.create_widgets()

    def create_widgets(self):
        self.label = tk.Label(self.root, text="Rock, Paper, Scissors!", font=("Arial", 16))
        self.label.pack(pady=10)

        self.level_var = tk.StringVar(value=self.level)
        tk.Label(self.root, text="Select Difficulty Level:").pack()
        tk.OptionMenu(self.root, self.level_var, *levels.keys()).pack()

        self.score_label = tk.Label(
            self.root,
            text=f"Score - You: {player_score} | Computer: {computer_score}",
            font=("Arial", 12)
        )
        self.score_label.pack(pady=10)

        self.buttons_frame = tk.Frame(self.root)
        self.buttons_frame.pack()

        self.rock_btn = tk.Button(self.buttons_frame, text="Rock", command=lambda: self.play("Rock"))
        self.rock_btn.grid(row=0, column=0, padx=5, pady=5)

        self.paper_btn = tk.Button(self.buttons_frame, text="Paper", command=lambda: self.play("Paper"))
        self.paper_btn.grid(row=0, column=1, padx=5, pady=5)

        self.scissors_btn = tk.Button(self.buttons_frame, text="Scissors", command=lambda: self.play("Scissors"))
        self.scissors_btn.grid(row=0, column=2, padx=5, pady=5)

        self.power_up_btn = tk.Button(self.root, text="Power Up (1-time Use)", command=self.use_power_up)
        self.power_up_btn.pack(pady=10)

        self.restart_btn = tk.Button(self.root, text="New Game", command=self.restart_game)
        self.restart_btn.pack()

        self.quit_btn = tk.Button(self.root, text="Exit", command=self.exit_game)
        self.quit_btn.pack()

    def exit_game(self):
        self.root.destroy()

    def play(self, player_choice):
        global player_score, computer_score, power_up_used
        computer_choice = self.get_computer_choice()
        result = self.determine_winner(player_choice, computer_choice)

        if result == "win":
            player_score += 1
        elif result == "lose":
            computer_score += 1

        self.score_label.config(text=f"Score - You: {player_score} | Computer: {computer_score}")
        messagebox.showinfo(
            "Round Result",
            f"You chose {player_choice}
Computer chose {computer_choice}
Result: You {result}!"
        )

        if player_score == 5:
            messagebox.showinfo("Game Over", "Congratulations! You won!")
            self.restart_game()
        elif computer_score == 5:
            messagebox.showinfo("Game Over", "Game Over! The computer won!")
            self.restart_game()

    def get_computer_choice(self):
        difficulty = self.level_var.get()
        if random.random() < levels[difficulty]:  # AI plays optimally sometimes
            return self.optimal_choice()
        return random.choice(choices)

    def optimal_choice(self):
        return random.choice(choices)

    def determine_winner(self, player, computer):
        if player == computer:
            return "tied"
        if (player == "Rock" and computer == "Scissors") or \
           (player == "Paper" and computer == "Rock") or \
           (player == "Scissors" and computer == "Paper"):
            return "win"
        return "lose"

    def use_power_up(self):
        global power_up_used
        if power_up_used:
            messagebox.showinfo("Power Up", "You have already used the Power Up!")
            return
        power_up_used = True
        self.play("Rock")  # Guarantees a win against Scissors
        messagebox.showinfo("Power Up", "Power Up used! You played Rock this turn.")
        self.power_up_btn.config(state=tk.DISABLED)

    def restart_game(self):
        global player_score, computer_score, power_up_used
        player_score = 0
        computer_score = 0
        power_up_used = False
        self.power_up_btn.config(state=tk.NORMAL)
        self.score_label.config(text=f"Score - You: {player_score} | Computer: {computer_score}")


if __name__ == "__main__":
    root = tk.Tk()
    game = RockPaperScissorsGame(root)
    root.mainloop()
Code language: HTML, XML (xml)