Python - successive random calls to the same method - python

On the following strip-down version of my program, I want to simulate an exchange where the user and the computer will act following a random sequence.
Here the variable row contains the sequence order. A value of 0 means the program is waiting for a user input (method val0). A value of 1 means there should be an automatic process (method val1).
It seems to work at the beginning when alternating 0 and 1 but as soon as we are waiting for two successive automatic calls, it goes out of whack.
I tried using the after method but I can't see how and where to insert it.
A while loop might do the trick on this example but the end program is more complex, with sequence interruption, reevaluation of the sequence and so on. So I don't know if it would still apply then.
from tkinter import *
class Application:
def __init__(self,master = None):
self.master = master
Label(master,text='press next').grid()
Button(master,text='Next',command=self.val0).grid()
self.index = IntVar()
self.index.set(0)
self.index.trace("w",self.nextTurn)
def val0(self):
print("User action")
self.index.set(self.index.get() +1)
def val1(self):
print("Automatic action")
self.index.set(self.index.get() +1)
def nextTurn(self, *args):
i = self.index.get()
if i >= len(row):
self.master.destroy()
return
if row[i] == 1:
self.val1()
if __name__ == "__main__":
row = [0,1,0,0,1,1,0,0,0,1,1,1]
root = Tk()
win = Application(root)
root.mainloop()

You can easily solve your problem by calling the nextTurn directly in the automatic action function:
def val1(self):
print("Automatic action")
self.index.set(self.index.get() +1)
self.nextTurn() # call nextTurn after this action
So if it was an automatic action, you step into the next row position, and call nextTurn again.
However this may become a problem if your row becomes too large because it uses recursion. In that case you will want another approach with the while you mentioned. For this second option you would only need to change nextTurn:
def nextTurn(self, *args):
i = self.index.get()
# while it is an automatic action and it has row values, keep calling val1
while i < len(row) and row[i] == 1:
self.val1() # call automatic action
i = self.index.get() #update the row position
else:
if i >= len(row):
self.master.destroy()
return

Related

Can you call a function from tk.OptionMenu's command parameter without passing an argument

I want to check if the user has changed any values in tkinter widgets and then prompt to save those values or lose changes.
A lot of applications have 3 buttons at the bottom right of their settings frame OK, Cancel, and Apply where Apply is disabled until a change has been made. I am imitating this feature in my program.
When I use tk.OptionMenu(command=function), by default it sends the current user selection to the function, however I do not need to know the current selection because regardless of what it is, I want to prompt the user to save settings.
Running the program would result in the following error:
TypeError: function() takes 0 positional arguments but 1 was given
A simple workaround I thought of would be to give an arbitrary parameter to function() like so:
def function(param=None):
value_change.status = True
See Example Code for value_change.status usage.
However, PyCharm points out that param is not used and marks it as a weak warning. I don't have any use for the passed value so I can't do much with param except ignore it and the PyCharm warning as well. There's technically nothing wrong here but I like seeing that green checkmark at the top right of my screen so I came up with another workaround:
def function(param=None):
value_change.status = True if param else False
Both param=None and if param else False are redundant since they're only placeholders to make the code run smoothly and not throw any warnings.
Another problem arises when I want to use function() for other widget commands which do not pass arguments such as tk.Checkbutton(command=function) and I have to change my code to the following lines which makes the code appear even more redundant than before.
def function(param=True):
value_change.status = True if param else False
Is there a way to not pass an argument through tk.OptionMenu(command=function)?
Example Code
import time
import tkinter as tk
from threading import Thread
def value_change():
value_change.count = 0
value_change.status = False
print('Has the user changed settings in the past 3 seconds?')
while value_change.status != 'exit':
if value_change.status:
value_change.count += 1
# Reset count if no change after 3 seconds
if value_change.count > 3:
value_change.count = 0
value_change.status = False
print(value_change.status)
time.sleep(1)
def function(param=True):
value_change.status = True if param else False
value_change.count = 0
gui = tk.Tk()
gui.geometry('100x100')
"""Setup OptionMenu"""
menu_options = ['var1', 'var2', 'var3']
menu_string = tk.StringVar()
menu_string.set(menu_options[0])
option_menu = tk.OptionMenu(gui, menu_string, *menu_options, command=function)
option_menu.pack()
"""Setup CheckButton"""
check_int = tk.IntVar()
check = tk.Checkbutton(gui, variable=check_int, command=function)
check.pack()
"""Turn On Thread and Run Program"""
Thread(target=value_change).start()
gui.mainloop()
"""Signal Threaded Function to Exit"""
value_change.status = 'exit'
The above example is a minimal reproduction, I did not include the OK Cancel Apply buttons and their functions as they are not necessary and would only make the code lengthy.
You can use lambda:
option_menu = tk.OptionMenu(gui, menu_string, *menu_options, command=lambda v: function())

How to recursively call different methods for levels of a video game?

I attempting to design a simple choice based video game. Essentially what I want is a recursive loop that will continue to call the new levels based off the results of the previous. For example, in level 1 based off choice made it will either trigger level 2 or 3. This is the code I have so far:
class Levels:
def __init__(self, Next = 1):
self.Next = Next
def Call(self):
VarLevel = "Level" + "{}".format(self.Next)
return ("{}".format(VarLevel))
This is the super class, it returns the VarLevel which equals Level1 to trigger the subclass Level1. This is the code I have for the levels, I've excluded the context of the game because I don't think it is necessary.
class Level1(Levels):
def __init__(self,):
# this just includes information about the level to show the user
# (objective etc.)
def Action(self):
# this will include the content of the level. based off the choices
# made I want it to return to the super with VarLevel as Level2 or
# Level3 and then to trigger the next level running and repeat
# throughout the program to run the game. For the purpose of testing
# the program the only content of Level1 is setting the return to 2 so
# that Level2 is called. I'm having trouble actually getting it to
# recognize my return and to trigger the next level. This is the
# actual method I want to do the calling with
class LevelCall():
def __init__(self, Levels = Levels):
self.Levels = Levels
def Calling(self):
result = (Levels, "{}".format(Levels()))()
it gives me the error TypeError: 'tuple' object is not callable. I have been doing a lot of different attempts to get it to work so I'm not certain that this is even the real problem with the code. Also of note I am decent in Java and am now transitioning to Python (this is my first attempt in Python other then basic tests to read/write etc.) Any help is greatly appreciated to help figure out how to format the game and I apologize in advance because I know this is a long question, I've never posted here before so if you need more info or clarification please feel free to ask.
Edit:
This is the full error message
Traceback (most recent call last):
line 54, in <module>
Tester.Calling()
line 50, in Calling
result = (Levels, "{}".format(Levels()))()
TypeError: 'tuple' object is not callable
Another Edit:
I think I am getting closer. I made the following changes
class LevelCall():
def __init__(self, Levels = Levels):
self.Levels = Levels
def Calling(self):
Hold = Levels()
result = (getattr(Levels, "{}".format(Hold.Call()))())
It now gives the following error message.
Traceback (most recent call last):
line 55, in <module>
Tester.Calling()
line 51, in Calling
result = (getattr(Levels, "{}".format(Hold.Call()))())
AttributeError: type object 'Levels' has no attribute 'Level1'
If I understand correctly it is now attempting to do what I want but isn't finding the class "Level1". Again all help is much appreciated.
Edit______________________
I would like to thank all who replied and attempted to help, I am truly grateful for the support. With the clarification you were able to help me with as well as a fresh start today and mapping it out in java first to make the transition easier I was able to solve my problem. Once again thank you all very much I will add the solution I found beneath this edit.
global Stop
class Level1 :
def __init__(self):
self
def Action(self):
print ("1")
global Stop
Stop = input("Would you like to advance to the next level?")
if (Stop == "yes"):
# Lev = Level2()
# return Lev.Action()
return Level2
if (Stop == "no"):
return "stop"
class Level2:
def __init__(self):
self
def Action(self):
print("2")
global Stop
Stop = input("Would you like to advance to the next level?")
if (Stop == "yes"):
# Lev = Level3()
# return Lev.Action()
return Level3
if (Stop == "no"):
return "stop"
class Level3 :
def __init__(self):
self
def Action(self):
print ("3")
global Stop
Stop = input ("Next level??")
if (Stop == "yes"):
# Lev = Level4()
# return Lev.Action()
return Level4
if (Stop == "no"):
return "stop"
class Level4:
def __init__(self):
self
def Action(self):
print ("Complete")
return "Done"
def Runner (Level):
if (Level == "Done"):
print ("Bye")
else :
if (Level != "stop"):
Lev = Level()
Next = Lev.Action()
Runner(Next)
if (Level == "stop"):
print ("you chose to stop")
Runner(Level1)
(a,b,c) is tuple syntax. (a,b,c)() is a tuple being called like a function. That is what the error is referring to.
If we break the offending code down you can tell. What does it look like when you replace the call to format with an arg placeholder:
(Levels, "{}".format(Levels()))() becomes...
(Levels, arg)() # this is now clearly a tuple and you're treating it like a function.
Not really sure how fixing that will help you with your levels problem tho.
If you want to call a function, do so like: func(args).
If you want to define a tuple, do so like: (a, b, ..., z).
But don't call a tuple like a function.

Pyglet, exit after all sounds played

AVbin is installed. Both .wav and .mp3 files work.
import pyglet
music = pyglet.media.load('A.mp3')
music.play()
player = pyglet.media.Player()
player.queue( pyglet.media.load('B.mp3'))
player.queue( pyglet.media.load('C.wav'))
player.play()
pyglet.app.run()
pyglet.app.exit()
I want to create a program that plays A, then plays the queue with B and then C, and finally quits after all three sounds play.
I tried the code above but according to this post, "this is [solely] because app.run() is a never-ending loop."
How can I modify my code minimally so that the program quits after the three sounds are played?
Bonus, but how can I modify my code minimally so that the program can play two (or more) sound files, E.mp3 and F.mp3, at once?
Thanks!
Because what you're asking is not as simple as you'd might think it is.
I've put together a code example with as much comments as I possibly could fit in without making the example to hard to read.
Below the code, I'll try to explain a few key functions as detailed as possible.
import pyglet
from pyglet.gl import *
from collections import OrderedDict
key = pyglet.window.key
class main(pyglet.window.Window):
def __init__ (self, width=800, height=600, fps=False, *args, **kwargs):
super(main, self).__init__(width, height, *args, **kwargs)
self.keys = OrderedDict() # This just keeps track of which keys we're holding down. In case we want to do repeated input.
self.alive = 1 # And as long as this is True, we'll keep on rendering.
## Add more songs to the list, either here, via input() from the console or on_key_ress() function below.
self.songs = ['A.wav', 'B.wav', 'C.wav']
self.song_pool = None
self.player = pyglet.media.Player()
for song in self.songs:
media = pyglet.media.load(song)
if self.song_pool is None:
## == if the Song Pool hasn't been setup,
## we'll set one up. Because we need to know the audio_format()
## we can't really set it up in advance (consists more information than just 'mp3' or 'wav')
self.song_pool = pyglet.media.SourceGroup(media.audio_format, None)
## == Queue the media into the song pool.
self.song_pool.queue(pyglet.media.load(song))
## == And then, queue the song_pool into the player.
## We do this because SourceGroup (song_pool) as a function called
## .has_next() which we'll require later on.
self.player.queue(self.song_pool)
## == Normally, you would do self.player.eos_action = self.function()
## But for whatever windows reasons, this doesn't work for me in testing.
## So below is a manual workaround that works about as good.
self.current_track = pyglet.text.Label('', x=width/2, y=height/2+50, anchor_x='center', anchor_y='center')
self.current_time = pyglet.text.Label('', x=width/2, y=height/2-50, anchor_x='center', anchor_y='center')
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_key_release(self, symbol, modifiers):
try:
del self.keys[symbol]
except:
pass
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
elif symbol == key.SPACE:
if self.player.playing:
self.player.pause()
else:
self.player.play()
elif symbol == key.RIGHT:
self.player.seek(self.player.time + 15)
## == You could check the user input here,
## and add more songs via the keyboard here.
## For as long as self.song_pool has tracks,
## this player will continue to play.
self.keys[symbol] = True
def end_of_tracks(self, *args, **kwargs):
self.alive=0
def render(self):
## Clear the screen
self.clear()
## == You could show some video, image or text here while the music plays.
## I'll drop in a example where the current Track Name and time are playing.
## == Grab the media_info (if any, otherwise this returns None)
media_info = self.player.source.info
if not media_info:
## == if there were no meta-data, we'll show the file-name instead:
media_info = self.player.source._file.name
else:
## == But if we got meta data, we'll show "Artist - Track Title"
media_info = media_info.author + ' - ' + media_info.title
self.current_track.text = media_info
self.current_track.draw()
## == This part exists of two things,
## 1. Grab the Current Time Stamp and the Song Duration.
## Check if the song_pool() is at it's end, and if the track Cur>=Max -> We'll quit.
## * (This is the manual workaround)
cur_t, end_t = int(self.player.time), int(self.player.source._get_duration())
if self.song_pool.has_next() is False and cur_t >= end_t:
self.alive=False
## 2. Show the current time and maximum time in seconds to the user.
self.current_time.text = str(cur_t)+'/'+str(end_t) + 'seconds'
self.current_time.draw()
## This "renders" the graphics:
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
x = main()
x.run()
Now, normally you'd decorate your way trough this with a bunch of functions.
But I like to subclass and OOP my way through any graphical libraries, because it gets messy quite fast otherwise.
So instead of pyglet.app.run(), I've got a custom made run() function.
All this does is mimic the pyglet.app.run(), for the most part. Enough to get going at least.
Because player.eos_* events appears to be broken.
I've added a manual example of how you could check if the songs are done playing or not.
This is a combination of self.song_pool pyglet.media.SourceGroup, self.player.time pyglet.media.player.time and self.player.source._get_duration() which returns the track duration.
The SourceGroup gives us a has_next() function which tells us if we're at the end of the queued songs. The other two variables tells us if we've reached the end of the current track. This is all we need to determinate if we want to exit or not.
Now, I haven't technically added a way to add more songs. Because again, that would also be harder than you think. Unless you opt in for if symbol == key.LCTRL: self.song_pool.queue(pyglet.media.load(input('Song: '))) for instance. But again, all you would need to do, is add more songs to the self.song_pool queue, and there you go.
I hope this answers your question. Even the bonus one.

Blocking button click signals in PyQt

I have a program that uses pyqt's .animateClick() feature to show the user a sequence of different button clicks that the user has to copy in that specific order. The problem is I don't want the animateClick() to send a signal, I only want the button click signals from the user. Here is some of my code to demonstrate what I mean, and how I tried to solve that problem (that doesn't work). I simplified my code quite a bit so its easier to read, let me know if you have any questions.
from PyQt4 import QtCore,QtGui
global flag
global ai_states
ai_states = []
user_states = []
class Program(object):
# Set up the push buttons
#Code Here.
# Connect push buttons to function get_state()
self.pushButton.clicked.connect(self.get_state)
self.pushButton_2.clicked.connect(self.get_state)
self.pushButton_3.clicked.connect(self.get_state)
self.pushButton_4.clicked.connect(self.get_state)
# Code that starts the start() function
def start(self):
flag = 0
ai_states[:] = []
i = -1
# Code here that generates ai_states, numbers 1-4, in any order, based on button numbers.
for k in ai_states:
i = i + 1
# Code here that animates button clicks determined by ai_states
# Changes the flag to 1 once the loop ends
if i == len(ai_states):
flag = 1
def get_state(self):
if flag == 1:
user_states.append(str(self.centralWidget.sender().text()))
else:
pass
if len(user_states) == len(ai_states):
# Checks to make sure the user inputted the same clicks as the ai_states
Even though the flag does come out to be 1 after the start() function, it is still appending the animatedClick() signals. What am I doing wrong? I'm new to GUI programming, so I'm probably going about this in a very bad way. Any help would be appreciated.
Never use global variables unless you really have to. If you need shared access to variables, use instance attributes:
from PyQt4 import QtCore,QtGui
class Program(object):
def __init__(self):
self.ai_states = []
self.user_states = []
self.flag = 1
# Set up the push buttons
# Code Here
# Connect push buttons to function get_state()
self.pushButton.clicked.connect(self.get_state)
self.pushButton_2.clicked.connect(self.get_state)
self.pushButton_3.clicked.connect(self.get_state)
self.pushButton_4.clicked.connect(self.get_state)
# Code that starts the start() function
def start(self):
self.flag = 0
del self.ai_states[:]
i = -1
# Code here that generates ai_states, numbers 1-4, in any order, based on button numbers.
for k in self.ai_states:
i = i + 1
# Code here that animates button clicks determined by ai_states
# Changes the flag to 1 once the loop ends
self.flag = 1
def get_state(self):
if self.flag == 1:
self.user_states.append(str(self.centralWidget.sender().text()))
if len(self.user_states) == len(self.ai_states):
# Checks to make sure the user inputted the same clicks as the ai_states

Python - How To Get Cursor Position in Tkinter Text Widget

I want to get the cursor position (line and column) of the insertion point of a Tkinter.Text, but for the specific situation below.
PROBLEM: My text editor project requires a custom undo/redo for Tkinter.Text. I put in the same string for both Test One and Test Two below, but undo does not act consistently due to a inconsistent column variable in KeyRelease event handler given by Tkinter. The problem seems to be that I type too fast for second test which produces a bad column value. Can you help me find the problem?
TWO TEST PROCESS TO REPRODUCE THE ERROR:
TEST ONE
Type this string slowly: 'one two three'
Press F1 to see each word undo.
Result: Works fine. (For me atleast. Ephasis: type slowly.)
TEST TWO
Type the same string as fast as you can: 'one two three'
Press F1 to see each word undo.
Result: Gets the wrong column and does not undo properly. (Restart script and repeat this step if you don't see the error at first, it sometimes works fine with fast typing. I usually get it with 3 to 4 tries at the most.)
QUESTION: Is this a bug in Tkinter, or am I not understanding something specific within Tkinter that would produce consistent columns for my undo/redo records?
from Tkinter import *
class TextView(Text):
def __init__(self, root):
Text.__init__(self, root)
self.history = History(self)
self.bind("<KeyRelease>", self.keyRelease)
# used to capture a char at a time in keyRelease. If space char is pressed it creates a Word object and adds it to undo/redo history.
self.word = ""
def keyRelease(self, event):
if event.keysym == "space":
self.word += " "
self.makeWordRecord()
else:
self.word += event.char
def makeWordRecord(self, ):
if len(self.word):
index = self.index(INSERT)
wordEvent = Word(index, self.word)
self.history.addEvent(wordEvent)
self.word = ""
def undo(self, event):
self.makeWordRecord()
self.history.undo()
def redo(self, event):
self.history.redo()
class History(object):
def __init__(self, text):
self.text = text
self.events = []
self.index = -1
# create blank document record, line one, column one, no text
self.addEvent(Word("1.0", ""))
def addEvent(self, event):
if self.index +1 < len(self.events):
self.events = self.events[:self.index +1]
self.events.append(event)
self.index +=1
def undo(self):
if self.index > 0:
self.events[self.index].undo(self.text)
self.index -=1
def redo(self):
if self.index +1 < len(self.events):
self.index +=1
self.events[self.index].redo(self.text)
class Word(object):
def __init__(self, index, word):
self.index = index
self.word = word
def undo(self, text):
line = self.index.split(".")[0]
column = int(self.index.split(".")[-1])
startIndex = line + "." + str(column - len(self.word))
endIndex = line + "." + str(int(column))
text.delete(startIndex, endIndex)
def redo(self, text):
line = self.index.split(".")[0]
column = int(self.index.split(".")[-1])
startIndex = line + "." + str(column - len(self.word))
text.insert(startIndex, self.word)
if __name__ == "__main__":
root = Tk()
root.geometry("400x200+0+0")
textView = TextView(root)
textView.pack()
root.bind("<F1>", textView.undo)
root.bind("<F2>", textView.redo)
root.mainloop()
I finally figured out what was going on and has nothing to do with Tkinter, but all Toolkits. I can now honestly say that I can add something to Best Programming Practices and :
Do Not Process Key Events by Binding to a Key Release Method, Instead Process Keys with a Key Press Method
Why?
It's not really a programming issue, it's a hardware issue. When a key is pressed, the physical key goes down. There is a spring that pushes the key back up. If that spring or anything about your keyboard causes a key to be even 200ths of second slower, typing even 60 words a minute may cause keys that were typed in one order to come back in another order. This may happen because a spring may be slightly thicker, stiffer, over used, or even sticky mocha cause more resistance.
Capitalization can be affected as well. Pressing the shift key and another key to get an upper case must be simultaneously pressed down. But if you process keys on key release, it is possible the shift key springs up faster than the character key you are capitalizing, which will result in a lower case. This also allows characters to be inverted. If you check on positions of a character when it's typed, you can get the wrong position due to this as well.
Stick with Key Press.

Categories