Create Oval Color During Teleport Cooldown in Tkinter Game

I’ve been working on a simple game of tag in Python using Tkinter, and I wanted to add a neat twist: when the teleport ability (TP) is on cooldown, the yellow player should change color to red. This way, players can visually tell when teleportation isn’t available. For balance, I made the tagger (purple) bigger and slower, while the other player (yellow) is smaller, faster, and can teleport to a random location.

The game worked fine overall, but the challenge was getting the yellow oval to change its color during the teleport cooldown and then revert back to yellow once the cooldown was over. I tried a few approaches, such as modifying the color inside the cooldown validation and even within the Teleport function itself. Nothing seemed to work until I decided to implement a periodic update loop that continuously checks the cooldown status.

The Original Code

Below is the original code that sets up the game, handles movement, collision detection, and the teleport functionality:

from tkinter import *
import sys
import math
import time
import random
from tkinter import messagebox
from datetime import datetime

cooldown = {}
Tp_Usage = 0

def move_up(event):
if canva.coords(j1)[1] > 5:
canva.move(j1, 0, -20)
check_collision()

def move_down(event):
if canva.coords(j1)[3] < canva.winfo_height() - 5:
canva.move(j1, 0, 20)
check_collision()

def move_right(event):
if canva.coords(j1)[2] < canva.winfo_width() - 5:
canva.move(j1, 20, 0)
check_collision()

def move_left(event):
if canva.coords(j1)[0] > 5:
canva.move(j1, -20, 0)
check_collision()

def update(action, cool):
cooldown.update({action: time.time() + cool})

def validate(action):
return cooldown.get(action, 0) - time.time() <= 0

def Teleport(event):
global Tp_Usage
if validate("Teleport") and Tp_Usage < 5:
Tp1 = round(random.randint(0, 490), -1)
Tp2 = round(random.randint(0, 490), -1)
canva.coords(j2, Tp1, Tp2, Tp1 + 10, Tp2 + 10)
update("Teleport", 2.5)
Tp_Usage += 1
L1.config(text=f"Tp: {Tp_Usage}/5")
if Tp_Usage == 5:
L1.config(text=f"Tp: {Tp_Usage}/5", fg="red")
check_collision()

def move_up2(event):
if canva.coords(j2)[1] > 5:
canva.move(j2, 0, -10)
check_collision()

def move_down2(event):
if canva.coords(j2)[3] < canva.winfo_height() - 5:
canva.move(j2, 0, 10)
check_collision()

def move_right2(event):
if canva.coords(j2)[2] < canva.winfo_width() - 5:
canva.move(j2, 10, 0)
check_collision()

def move_left2(event):
if canva.coords(j2)[0] > 5:
canva.move(j2, -10, 0)
check_collision()

def salir(event):
sys.exit()

def distance(coord1, coord2):
x1, y1 = coord1
x2, y2 = coord2
return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

def check_collision():
global inicio
coord1 = canva.coords(j1)
coord2 = canva.coords(j2)
center1 = ((coord1[0] + coord1[2]) / 2, (coord1[1] + coord1[3]) / 2)
center2 = ((coord2[0] + coord2[2]) / 2, (coord2[1] + coord2[3]) / 2)
if distance(center1, center2) < 15:
tiempo = datetime.now() - inicio
messagebox.showinfo("Pillado!", "Has tardado " + str(tiempo))
sys.exit()

v = Tk()
v.resizable(width=False, height=False)

v.bind("<Up>", move_up)
v.bind("<Down>", move_down)
v.bind("<Left>", move_left)
v.bind("<Right>", move_right)
v.bind("<w>", move_up2)
v.bind("<s>", move_down2)
v.bind("<a>", move_left2)
v.bind("<d>", move_right2)
v.bind("<Shift_L>", Teleport)
v.bind("<Escape>", salir)

L1 = Label(v, text=f"Tp: {Tp_Usage}/5")
L1.pack()
canva = Canvas(v, width=500, height=500, bg="grey")
canva.pack()
j1 = canva.create_oval(40, 40, 60, 60, fill="purple")
j2 = canva.create_oval(440, 440, 450, 450, fill="yellow")
inicio = datetime.now()
v.mainloop()

How It Works

  • Player Movement:
    The arrow keys move the purple circle (j1) while the WASD keys move the yellow circle (j2). Collision detection is in place to end the game when the players meet.
  • Teleportation Mechanism:
    The yellow player can teleport by pressing the Shift key. A cooldown mechanism is implemented using the update and validate functions, ensuring that teleportation is only available every 2.5 seconds and only up to 5 times.
  • The Issue:
    The yellow oval should change color to red when teleport is on cooldown, but my initial attempts at changing the color within the Teleport function or during cooldown validation did not work consistently.

A Periodic Update Loop

After trying several approaches, I discovered that the key was to continuously check the cooldown status and update the oval’s color accordingly. I implemented a function called update_tp_color that runs periodically (every 100 milliseconds) and adjusts the color of the yellow oval based on whether the teleport ability is on cooldown.

The update_tp_color Function

def update_tp_color():
# If teleport is on cooldown, change j2's color to red; else, set it to yellow.
if not validate("Teleport"):
canva.itemconfig(j2, fill="red")
else:
canva.itemconfig(j2, fill="yellow")

# Optionally, update the window title with remaining cooldown time for feedback.
remaining = cooldown.get("Teleport", 0) - time.time()
if remaining > 0:
v.title(f"Cooldown: {remaining:.1f} sec")
else:
v.title("Teleport Ready!")

# Schedule this function to run again after 100 milliseconds.
v.after(100, update_tp_color)

Integrating the Update Loop

I simply call update_tp_color() just before starting the Tkinter main loop. This function ensures that the color of the yellow oval is updated in near-real-time based on the cooldown state.

The Final Enhanced Code

Below is the complete code with the new periodic update functionality added:

from tkinter import *
import sys
import math
import time
import random
from tkinter import messagebox
from datetime import datetime

cooldown = {}
Tp_Usage = 0

def move_up(event):
if canva.coords(j1)[1] > 5:
canva.move(j1, 0, -20)
check_collision()

def move_down(event):
if canva.coords(j1)[3] < canva.winfo_height() - 5:
canva.move(j1, 0, 20)
check_collision()

def move_right(event):
if canva.coords(j1)[2] < canva.winfo_width() - 5:
canva.move(j1, 20, 0)
check_collision()

def move_left(event):
if canva.coords(j1)[0] > 5:
canva.move(j1, -20, 0)
check_collision()

def update(action, cool):
cooldown[action] = time.time() + cool

def validate(action):
return cooldown.get(action, 0) - time.time() <= 0

def Teleport(event):
global Tp_Usage
if validate("Teleport") and Tp_Usage < 5:
Tp1 = round(random.randint(0, 490), -1)
Tp2 = round(random.randint(0, 490), -1)
canva.coords(j2, Tp1, Tp2, Tp1 + 10, Tp2 + 10)
update("Teleport", 2.5)
Tp_Usage += 1
L1.config(text=f"Tp: {Tp_Usage}/5")
if Tp_Usage == 5:
L1.config(text=f"Tp: {Tp_Usage}/5", fg="red")
check_collision()

def move_up2(event):
if canva.coords(j2)[1] > 5:
canva.move(j2, 0, -10)
check_collision()

def move_down2(event):
if canva.coords(j2)[3] < canva.winfo_height() - 5:
canva.move(j2, 0, 10)
check_collision()

def move_right2(event):
if canva.coords(j2)[2] < canva.winfo_width() - 5:
canva.move(j2, 10, 0)
check_collision()

def move_left2(event):
if canva.coords(j2)[0] > 5:
canva.move(j2, -10, 0)
check_collision()

def salir(event):
sys.exit()

def distance(coord1, coord2):
x1, y1 = coord1
x2, y2 = coord2
return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

def check_collision():
global inicio
coord1 = canva.coords(j1)
coord2 = canva.coords(j2)
center1 = ((coord1[0] + coord1[2]) / 2, (coord1[1] + coord1[3]) / 2)
center2 = ((coord2[0] + coord2[2]) / 2, (coord2[1] + coord2[3]) / 2)
if distance(center1, center2) < 15:
tiempo = datetime.now() - inicio
messagebox.showinfo("Pillado!", "Has tardado " + str(tiempo))
sys.exit()

def update_tp_color():
# Change j2's color to red when teleport is on cooldown; else, yellow.
if not validate("Teleport"):
canva.itemconfig(j2, fill="red")
else:
canva.itemconfig(j2, fill="yellow")

# Optional: update window title with the remaining cooldown time.
remaining = cooldown.get("Teleport", 0) - time.time()
if remaining > 0:
v.title(f"Cooldown: {remaining:.1f} sec")
else:
v.title("Teleport Ready!")

# Schedule to check again after 100 milliseconds.
v.after(100, update_tp_color)

v = Tk()
v.resizable(width=False, height=False)
v.bind("<Up>", move_up)
v.bind("<Down>", move_down)
v.bind("<Left>", move_left)
v.bind("<Right>", move_right)
v.bind("<w>", move_up2)
v.bind("<s>", move_down2)
v.bind("<a>", move_left2)
v.bind("<d>", move_right2)
v.bind("<Shift_L>", Teleport)
v.bind("<Escape>", salir)

L1 = Label(v, text=f"Tp: {Tp_Usage}/5")
L1.pack()
canva = Canvas(v, width=500, height=500, bg="grey")
canva.pack()
j1 = canva.create_oval(40, 40, 60, 60, fill="purple")
j2 = canva.create_oval(440, 440, 450, 450, fill="yellow")
inicio = datetime.now()

# Start the cooldown color update loop.
update_tp_color()

v.mainloop()

Final Thoughts

I’m really pleased with how this project turned out. By adding a periodic update loop, I was able to make the yellow player’s oval change color during the teleport cooldown. Not only does this enhance the visual feedback, but it also makes the game more engaging and intuitive to play. This experience taught me the value of continuously checking and updating UI elements based on dynamic conditions—in this case, using Tkinter’s after method for smooth, periodic updates.

Related blog posts