Reputation: 71
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
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.
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!
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)
...
Upvotes: 2
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