How to ignore user input during sleep in Python 3? - python

I'm new to programming and am stuck with this problem in a small RPG game I'm making. It's mostly text based but uses tkinter for a simple GUI.
During battles, I have a for loop that goes through all creatures that are participating in the battle, and executes their action one by one with small sleeps between each action. My problem is that when the user presses keys or clicks buttons, the commands are buffered and executed after the for loop completes. Is there an easy way to ignore the user commands while this loop is going? I've tried unbinding keys and disabling buttons, then re-enabling them when the loop is complete, but it still doesn't work. I've also read about flushing the user input but I can't figure it out. Since I'm a rookie, I think I'm missing some basic concept here.
Here's my code:
def battle_round(self, command):
''' (Battle, command) -> NoneType
A round of battle where every creature gets an action.
'''
# Pause player input with mode variable.
self.player.mode = 'wait'
# Keep track of number of rounds.
self.round_number += 1
# Get initiative order for all mobs, party members, and player.
ordered_creatures = self.initiative()
# Get commands from each mob.
for mob in self.mobs:
mob.command = self.determine_mob_command()
# Check battle speed option.
delay = 1
# Begin actions for all creatures.
for creature in ordered_creatures:
# Delay between actions. Write a space between lines.
self.text_window.update_idletasks()
self.write(self.text_window, '\n')
time.sleep(delay)
# Player action.
if type(creature) == Player:
if command == 'standard_attack':
self.standard_player_attack()
if command == 'retreat':
self.retreat()
if type(creature) == PartyMember:
pass
# MOB action.
if type(creature) == MOB:
if creature.command == 'standard_attack':
self.standard_mob_attack(creature)
self.start_next_round()
I'm using Python 3.2.3 with Tk 8.5 and IDLE 3.2.3. My OS is Windows 7.
Thanks in advance!
Edit: Thanks for the replies so far guys. I may be in over my head here since I didn't even know what threading was until just now, and I'm not sure how I would go about reading and ignoring user input. As far as the code for the user input goes, I have a lot of it. I'll copy and paste some here:
def attack():
if player.in_battle == True:
if player.mode != 'wait':
player.current_battle.battle_round('standard_attack')
def retreat():
if player.in_battle == True:
if player.mode != 'wait':
player.current_battle.battle_round('retreat')
# Set up battle buttons.
attack_button = Button(battle_button_frame, text='(A)ttack', command=attack,
width=10,)
attack_button.grid(column=1, columnspan=1, row=2, padx=5, pady=5)
retreat_button = Button(battle_button_frame, text='(R)etreat', command=retreat,
width=10,)
retreat_button.grid(column=2, columnspan=1, row=2, padx=5, pady=5)
battle_button_list = [attack_button, retreat_button]
These buttons, for example are to have the user either attack the selected monster, or attempt to run away from the battle.
I also have several key bindings:
# Bind Keys
root.bind('<Escape>', func=keyboard_cancel)
root.bind('<Control-Key-m>', func=keyboard_move_mode)
root.bind('<Control-Key-M>', func=keyboard_move_mode)
root.bind('<Control-Key-l>', func=keyboard_look_mode)
root.bind('<Control-Key-L>', func=keyboard_look_mode)
root.bind('<Control-Key-t>', func=keyboard_talk_mode)
root.bind('<Control-Key-T>', func=keyboard_talk_mode)
root.bind('<space>', func=keyboard_begin_battle)
root.bind('<Left>', func=arrow_key_select_left)
root.bind('<Right>', func=arrow_key_select_right)
My problem remains that when the for loop with the sleeps is going, if the user presses a button or uses a key binding, it will get executed as soon as the battle round is over. (About 7 seconds if there are 6 monsters and the player.) I am a complete beginner at this so I apologize for not being clear with my post and for my code being a horrible mess.

Related

How to pass input from one entry widget into multiple sequential functions OR how to pass button input into a function

So I essentially have 2 codes at the moment, a text-based game and a GUI, that function separately that I'm trying to combine. The game basically has a series of functions it can call based on user input - eg:
def game():
print("Would you like to play? [Y/N]")
start = inpupt().lower().strip()
if start == 'y':
adventure()
elif start == 'n':
goodbye()
with adventure() calling two or more options leading to their respective functions, etc. etc.
The GUI is tkinter based, has a label, a scrolled text box, an entry widget, and an "enter" button. I've created my own "print" functions that allow the code to input text into the scrolled text box, and then disable the box again, so that the user doesn't directly edit it. I've also been able to have the button/enter key pull text from the entry widget and print it in the text box, and then clear the entry widget.
What I don't know how to do is get the entry widget/button to integrate with the different game functions. It seems like you can set a function for the button - which is how I was able to pull the entry text, but I don't know how to then pass that pulled information on to another function.
I tried something like this which worked for just entering text into the scrollbox with this attached to the button as the command:
def clicked(self, variable, event=None):
self.variable = self.userin.get()
self.print1(self.variable)
self.clear_text()
But then I don't know how to call that as a part of my game function. This was all I could think of (and I know it's wrong and bad, ok?):
def game():
print1("Would you like to go on an adventure? [Y/N]")
clicked(self, start) #trying to use "start" as the variable
if self.start == "y":
self.print1("Let's go!")
elif self.start == "n":
self.print1("Okay, bye!")
else:
self.print1("What?")
But then game() is trying to call the clicked() function instead of waiting for the button, so it's not really pulling the input from the user. I also kept getting the error that "start" was not defined, probably for that reason. (Also please ignore any errors in where "self" is supposed to go... this was my first stab at using classes, and I know I'm probably doing more wrong than right, I'd figure it out if you had a solution in this vein)
So is there a way to get the button to pass the user input into the function, without the function being exclusive to the button? (Like I know I could get it to make one choice, but then how would I get the button to function for the next choice?)
My "Plan B" was to just create buttons for user input - either yes/no or a/b/c/d - so that the function of the buttons would be the same : return "B" (or something like that), but then how would I get the function to pull that input? Or wait for that input?
I have googled this six ways to Sunday, and have not found anything that works for this situation. I thought I'd found something with the CallBack function - but then I realized they were using it for multiple buttons, instead of the same button for multiple functions.
Here is a quick example using some of what you provided that should demonstrate how you can link your conditions for the button. Regardless of how you actually structure your code, the concept should demonstrate one way you could do it. I would look into some tkinter tutorials as these concepts are well documented. I hope this helps.
import tkinter as tk
def clicked():
entry_value = entry.get()
entry.delete("0", tk.END)
print(entry_value)
# set up whatever conditions you want the button to be linked with
if str(entry_value) == "y":
print("Alright, get ready!")
# now you call your function to start the game here
start_game()
elif str(entry_value) == "n":
print("Ok, maybe another time")
label.config(text="Ok, maybe another time, closing the game now")
root.after(2500, lambda: root.destroy())
else:
label.config(text="Invalid entry, please enter a lower case 'y' or 'n'")
label.after(3000, reset_label)
def start_game():
# just an example this creates a new window to simulate some bhavior of the game
top = tk.Toplevel()
top.geometry("500x500")
top.title("New Game")
top.config(bg="blue")
# add things to the toplevel that shows the game? not sure what you want to do
def reset_label():
label.config(text="Would you like to play? [y/n]")
root = tk.Tk()
root.geometry("300x200")
label = tk.Label(root, text="Would you like to play? [y/n]")
label.pack()
entry = tk.Entry(root)
entry.pack()
button = tk.Button(root, text="Go", command=clicked)
button.pack()
root.mainloop()
Thank you for the suggestions! This is what I worked out (once I figured out the right way to change the command - no extra parenthesis!):
def choice1(event=None):
whereto = userin.get().lower().strip()
if whereto == "woods":
print1("The woods are spooky")
button["command"] = woods
elif whereto == "tavern":
print1("The tavern is crowded")
button["command"] = tavern
else:
print1("What?")
print1("Woods or Tavern?")
def clicked(event=None):
start = userin.get().lower().strip()
print1(userin.get())
clear_text()
changelabel("Let's go!")
if start == "y":
print1("Lets go!")
print1("Woods or Tavern?")
button["command"] = choice1
elif start == "n":
print1("Ok bye!")
else:
print1("What?")
print1("Do you want to play a game?")
I'd like to make the return key work the same as the button click, so I suspect I'm going to have to add the root.bind('<Return>', func) everywhere too

Tkinter "intervention" in a logic loop

So, I'm not sure the title describes my issue in a comprehensive manner.
I have made a working "loop" code in Python (even with working recursive functions) for some sort of a card game "AI". See the following pseudo-code:
def move():
if Turn == "Computer":
if Computer cannot play:
return False
else:
Do some "AI" stuff... the computer plays
Turn = "Player"
move()
elif Turn == "Player":
if Player cannot play:
return False:
else:
in = input("Select Card: ")
****So here the loop stops and waits for the input from the player
...then the input is evaluated and "played" as a player move
Turn = "Computer"
move()
while continue:
continue = move()
So if the computer can play, it plays the card, the turn changes to Player and the move function is called again, otherwise false is returned and the PC-Player back and forth turn is over. In the Player's turn the loop stops to wait for the user input from terminal and then it continues...
So far everything works, but now I want to implement a GUI for player input. I have some basic knowledge with Tkinter and so far I created buttons on screen rapresenting the player's cards. The question is how can I "stop" the loop waiting for the user to click a button (corresponding to the card - or the PASS TURN). Maybe with some sort of threading but sounds complicated... there must be an easier solution like in the "Terminal input version" that I have already working.
Thanks
You can't use a loop in a GUI, because the GUI has it's own loop that needs to run. The mainloop() in tkinter. You need to shift your planning to "event driven" programming. It sounds like in your case the event would be the user choosing a card.
def player_chooses_card():
# graphic and logic updates for player's choice
# computer turn
if Computer cannot play:
trigger graphic for end of game
else:
Do some "AI" stuff... the computer plays
# function is done; back to idle state waiting for an event
FWIW making a loop with recursion is bad style in python.
I feel your pain on the Tkinter loops. It was a hard lesson for me to learn as well. Loops such as while and for DO NOT WORK in tkinter. The way I typically solve this problem is with "root.after." Take the code in your loop and define it as a command (adding root.after(1, name of your command) like so:
def myloop():
move()
root.after(1, myloop)
Then, call it for the first time right before 'root.mainloop', like so:
<your other code>
root.after(1, myloop)
root.mainloop()
Also keep in mind that Tkinter does not make nice nice with multiprocessing/threading within mainloops, so you have to set up each process/thread as its own canvas and root.
Finally, you could set up a series of if statements and variables to separate your loop into chunks so that it stops and starts based on player input. For example,
def myloop():
if playerhasdonething == True:
move()
else:
print("waiting on player")
root.after(1, my mainloop)
I apologize if I missed the point of your question, this is how I personally would try to solve your problem

Tkinter Frame Not Recognizing Keypresses

This question is NOT a duplicate of this question: Why doesn't the .bind() method work with a frame widget in Tkinter?
As you can see, I set the focus to the current frame in my game_frame() method.
I'm writing a Chip-8 emulator in Python and using Tkinter for my GUI. The emulator is running, but I can't get Tkinter to recognize keypresses. Here is my code:
def game_frame(self):
self.screen = Frame(self.emulator_frame, width=640, height=320)
self.screen.focus_set()
self.canvas = Canvas(self.screen, width=640, height=320, bg="black")
self._root.bind("<KeyPress-A>", self.hello)
for key in self.CPU.KEY_MAP.keys():
print(key)
self.screen.bind(key, self.hello)
self.screen.pack()
self.canvas.pack()
def hello(self, event):
if event.keysym in self.CPU.KEY_MAP.keys():
self.CPU.keypad[self.CPU.KEY_MAP[event.keysym]] = 1
self.CPU.key_pressed = True
self.CPU.pc += 2
sys.exit()
def run_game(self, event):
self.game_frame()
self.CPU.load_rom("TANK")
while True:
self._root.update()
self.after(0, self.CPU.emulate_cycle)
Could you please help me figure out what's going wrong? I think it might have something to do with my game loop interfering with the key bindings, but I'm not sure. The hello method never gets called when I run the game because the program continues to run in an infinite loop and never exits, regardless of what key is pressed. Thank you!
The problem could be due to two things. Without seeing all your code it's impossible to say for sure.
For one, you are binding to a capital "A" rather than a lowercase "a" -- have you testing that the binding works or not when you press a capital A?
Also, you are using after and update incorrectly. You may be starving the event loop, preventing it from processing key presses. The right way to run a function periodically is to have a function that (re)schedules itself.
class CPU_Class():
...
def run_cycle(self):
self.emulate_cycle()
self._root.after(1, self.run_cycle)
Two things to note:
don't use after(0, ...) -- you need to give tkinter at least a ms or so to process other events.
the run_cycle function is responsible for running one cycle, and then scheduling the next cycle to run in the future.
Once you do that, you no longer need your while loop. You can simply call run_cycle once, and that will start the CPU running.

How do I make Tkinter entry focusout validation happen more than just the first time?

I have an application that, for a part of it, will take a user's input and format it into a standardized time format. To do this, i have a time input that has a focusout event tied to it that calls the time parser/replacer method. However, while testing it and just setting a message label to output some stuff, i noticed that it only triggers once...
Below is some sample code to show the problem.
from Tkinter import *
root=Tk()
message_var = StringVar()
message = Label(root, textvariable=message_var, height=2, width=35, bg="light grey")
time_var = StringVar()
time = Entry(root, textvariable=time_var, validate="focusout", validatecommand=time_update)
lose_focus_var = StringVar()
lose_focus_textbox = Entry(root, textvariable=lose_focus_var)
message_var.set("Enter a time below.")
lose_focus_var.set("Click here to lose focus.")
def time_update():
"""
Updates the time field to show standardized times.
"""
cur_entry = time_var.get()
if len(cur_entry) == 0:
message_var.set("0!")
elif len(cur_entry) == 1:
message_var.set("1!")
elif len(cur_entry) == 2:
message_var.set("2!")
elif len(cur_entry) == 3:
message_var.set("3!")
elif len(cur_entry) == 4:
message_var.set("4!")
elif len(cur_entry) == 5:
message_var.set("5!")
else:
message_var.set("TOO MANY!")
time_var.set("")
message.pack()
time.pack()
lose_focus_textbox.pack()
To reproduce my issue, run the code above. In the window that appears, click into the blank textbox, enter any number of characters, then click into the textbox that says "Click here to lose focus." You'll see that the message widget updates correctly! Yay!
However, if you click into the first textbox again, change the number of characters, then click the Lose Focus box again, the message will not update again. You will need to kill the window and re-run the code for the messages widget to update again.
If i add the time_update call to the other textbox (and refactor time_update to figure out which textbox called it), the message update will happen once for each text box. But only once.
Is there a way for me to re-trigger the <FocusOut> event, other than destroying the Entry widget and recreating it? Why doesn't it trigger every time?
One problem, as you mention in a comment to your own question, is that you're not returning True or False. The whole point of the validation is to let tkinter know if it should allow the edit or not, and it does this based on the return value.
Beyond that, however, the main problem is that you're changing the value of the widget within the validation function. When you do that, tkinter will automatically disable the validation. There are workarounds, but you really shouldn't be changing the value in the validation function. If I were a user, I would find this behavior to be extremely frustrating.
If you want to change the value when it loses focus, consider adding a binding to <FocusOut> rather than using the validation functions.

How to create a "play again" option with Pygame

I am new to programming with Python. I have been working through a tutorial book that I found, and got the game up and running, but then decided I wanted to have a "Play Again?" option at the end. I can get the game to quit out with a press of the "n" key, but cannot work out how to get the game to restart.
Here is the code I think is giving the trouble:
#player reaches treasure
if player_rectangle.colliderect(treasure_rectangle):
#display text
screen.blit(text,(screen_width/2-195,screen_height/2-25))
if event.type==pygame.KEYDOWN:
if event.key==pygame.K_n:
exit()
elif event.key==pygame.K_y:
pygame.display.update()
I know something needs to go after the elif event, and I have tried all that I can think of. I tried to define the whole program, and call it but that stopped the whole thing running. I have looked around internet sites, but just cannot seem to come up with a answer.
Can some one help out in easy terms how to get the game to restart to the starting position when the y key is pressed? I know it has something to do with a loop, I just cannot place my finger on what.
Many thanks.
It's not entirely clear how your code is organized, so I'll be very general. Usually games are implemented with a "main loop" that handles all of the action. In "pseudo"-python:
def main_loop():
while True:
handle_next_action()
draw_screen()
if game_is_over():
break
Before you start the loop, you usually do some setup to get the game state how you want it:
def main():
setup()
main_loop()
shut_down()
Given those parts, you can reset the game by having the main loop code call setup again (it may need to be specifically designed to be runable more than once):
def main_loop():
while True:
handle_events()
draw_screen()
if game_is_over():
if play_again(): # new code here!
setup()
else:
break
You might want to split the setup code into two parts, one which only needs to be run when the program begins (to read configuration files and set up things like the window system), and a second that gets repeated for each new game (setting up the game's state).
To restart a game you normally need to reset all variables to their initial value (e.g. number of lives, score, initial position, ...).
The best way is to put all initialisations inside a procedure:
def init():
global score,lives # use these global variables
score=0
lives=3
init() # call init() to define and reset all values
while 1: # main loop
...
elif event.key==pygame.K_y:
init() # restart the game

Categories

Resources