Oliver.G
Oliver.G

Reputation: 71

How do I make buttons move randomly in Tkinter

This is a section of a game I'm creating. In it, players have to click on the button marked 'deer' rather than 'doe' as quick as possible:

import tkinter
import time
import random

from __main__ import *

level = 2
name = "Oliver"
stop = False
score = 0
global level, name, score
if level >= 10:
    level = 10 + level/10
difficulty = level * 2
bonus = 1

def shot(animal):
    root.destroy()
    time.sleep(1)
    if animal == "Doe":
        print("You shot a doe!"),time.sleep(1)
        print("The rest of the herd have trampled you to death"),time.sleep(1.2)
        print("Oh deer!")
    elif animal == "Deer":
        print("You shot the deer!")


time.sleep(1), print("***Deer Shooting***\n\nShoot the deer, not the does"), time.sleep(2.5)

print("Ready"),time.sleep(1),print("Steady"),time.sleep(1),print("Go!\n\n\n")

root = tkinter.Tk()

NumOfDoe = difficulty

for i in range(0, NumOfDoe):
    tkinter.Button(root, text = "Doe", command = lambda: shot("Doe")).pack()

tkinter.Button(root, text = "Deer", command = lambda: shot("Deer")).pack()

root.mainloop()

When this runs, the Tkinter window looks a bit boring, as all the buttons are lined up

I'm aware of the '.place()' function, but how do I use it here to randomly scatter the buttons and make them move randomly of their own accord?

Thank you

Upvotes: 1

Views: 1178

Answers (2)

CommonSense
CommonSense

Reputation: 4482

First of all, as @furas mentioned, sleep is a wrong option, so remember to use after for time delays in tkinter!

Also, remember to structure your code (link#1, link#2) and don't try to put unnecessary multiple statements on one line.

Trivia

For movable buttons it's better to use Canvas widget instead of any of Layout Managers (unless your real goal is teleportable buttons). It's easy to place the button randomly two times in a row, but if you want to simulate move you need not a set of new random coordinates, but old coordinates, random distance (offset) and random direction.

It's possible to implement this idea with place, but there sweet move method, which do all of this for you.

All you need is to place each button on canvas with create_window (also it's gives you object ID to control your widget on canvas) and simulate mass moving!

Example

import tkinter as tk
import random


class App(tk.Tk):
    def __init__(self, difficulty, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.difficulty = difficulty
        self.play_area = tk.Canvas(self, background='bisque')
        self.play_area.pack(expand=True, fill='both')
        self.animals = self.generate_animals()
        self.play()

    def play(self):
        #   move all animals continuously (each 100 ms)
        self.move_animals()
        self.after(100, self.play)

    def generate_animals(self):
        #   generate all button-like animals
        animals = [Animal('deer', self, text='DEER')]

        for _ in range(self.difficulty * 2):
            animals.append(Animal('doe', self, text='DOE'))

        return animals

    def move_animals(self):
        #   move all animals
        for animal in self.animals:
            animal.move()


class Animal(tk.Button):
    def __init__(self, animal_type, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.animal_type = animal_type
        self.base_speed = self.master.difficulty
        self.play_area = self.master.play_area
        self.move_sets = ['n', 's', 'w', 'e']

        #   place button on canvas (it's possible to randomize starting locations)
        self.id = self.play_area.create_window(0, 0, window=self)

        self.configure(command=self.kill)

    def move(self):
        #   move animal
        #   get random speed and direction
        distance = random.randint(0, 15) * self.base_speed
        direction = random.choice(self.move_sets)

        if direction in self.move_sets[:2]:
            if direction == 'n':
                distance *= -1

            #   to prevent case when an animal leaves play area
            if 0 <= self.play_area.coords(self.id)[1] + distance <= self.play_area.winfo_height():
                self.play_area.move(self.id, 0, distance)

        else:
            if direction == 'w':
                distance *= -1

            #   to prevent case when an animal leaves play area
            if 0 <= self.play_area.coords(self.id)[0] + distance <= self.play_area.winfo_width():
                self.play_area.move(self.id, distance, 0)

    def kill(self):
        if self.animal_type == 'deer':
            print('You shot the deer!')
        else:
            print('You shot a doe!')
            print('The rest of the herd have trampled you to death')
            print('Oh deer!')

        self.master.destroy()

app = App(difficulty=2)
app.mainloop()

As you can see - it works somehow.

However, there's a big room to improve and adjust things. For example, a "smoother" movement. Although it's depends on how further we move objects it's also depends on frame rate, which 24 fps for a human specie (high frame rate). Thanks to after again, we can control this parameter via time delay, which can be calculated by a formula time_delay = 1000 // desired_fps:

...
    def play(self):
        #   move all animals continuously
        self.move_animals()
        self.after(41, self.play)        #    24 fps
        #   self.after(33, self.play)    #    30 fps
...

It's also possible to improve movement with additional directions and simplify conditional logic to one statement:

...
class Animal(tk.Button):
    def __init__(self, animal_type, *args, **kwargs):
        super().__init__(*args, **kwargs)
        ...
        #   dictionary for representation purpose
        self.move_sets = {'n': (0, -1), 's': (0, 1), 'w': (-1, 0), 'e': (1, 0),
                          'nw': (-1, -1), 'sw': (-1, 1), 'ne': (1, -1), 'se': (1, 1)}
        ...

    def move(self):
        #   move animal
        #   get random distance and direction
        distance = random.randint(0, 5) * self.base_speed
        direction = random.choice(list(self.move_sets.values()))
        movement = [_ * distance for _ in direction]
        current_coords = self.play_area.coords(self.id)

        #   to prevent case when an animal leaves play area
        if 0 <= current_coords[0] + movement[0] <= self.play_area.winfo_width() and \
           0 <= current_coords[1] + movement[1] <= self.play_area.winfo_height():
            self.play_area.move(self.id, *movement)
        ...

Similar problems

Upvotes: 2

furas
furas

Reputation: 142651

One method is to put buttons in one line (as before) but in random order.

You have to create list with all words (with many "Doe"),
shuffle list and then use this list to create Buttons

num_of_doe = level * 2 # we use CamelCaseNames for class names

# create list with words
all_words = ["Doe"]*num_of_doe + ["Deer"]

# shuffle list ("in-place")
random.shuffle(all_words)

#  create buttons using words
for word in all_words:
    b = tk.Button(root, text=word, command=lambda txt=word:shot(txt))
    b.pack()

BTW: if in lambda you use word from for then you have to use lambda txt=word:shot(txt). If you use lambda:shot(word) then it assign the same value - last value from list - because it put reference to variable word, not value from this variable.


If you need more random places then instead pack() you can use place() or grid() to put buttons in random places or cells (in grid)

b.place(x=random.randint(0, 200), y=random.randint(0, 100))

or

b.grid(row=random.randint(0, 10), column=random.randint(0, 10))

but it makes problem because you can put two buttons in the same place.

You need to generate all values first. It needs to keep values on list, then get new random value, compare with values on list and generate new one if it is already on list.

It is work for you ;)

Upvotes: 0

Related Questions