Python Forum
[Tkinter] Lambda seems to call expression 1 extra time every time it's called.
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
[Tkinter] Lambda seems to call expression 1 extra time every time it's called.
#1
Now, excuse if this is in the forum forum, even though I use a GUI, I don't beleive that's what's causing the problem.

The title makes this a little more confusing so let me explain.
I if do
a = lambda: some_function(16)
then call 'a' (I do this when a button is clicked, but that's beside the point). And 'some_function' is:
def some_function(some_number):
    print(some_number)
the output should be
Output:
>12
Then when 'a' is called again, it should again print
Output:
>12
However, with my code, I call 'a' once, and like expected it prints '12'. I then call it again, but this time, instead of printing '12' once, it print it twice. Like this:
Output:
#called once for 1st time >12 #called once for 2nd time >12 >12 #called once for 3rd time >12 >12 >12 .....
Even though the example is what happens, I will explain it with my real code, as it slightly different. When a button is pressed, a random number 1 through 6 is picked. When another button is pressed, it will call a function with parameters of either "P1" meaning player 1 or "P2" meaning player 2 and the random number. The player is changed every go (e.g. You press the button, a number is generated, you move your character. It becomes player 2's go. Player 2 does the same thing, it's now player 1's go).
This is how I do it:
a = lambda: self.move_players("P1" if self.currPl == 1 else "P2", random_number) #self.currPl evaluates to either 1 or two.
Now, on the first go, if player 1 rolles a 3, the function is called like
self.move_players("P1", 3)
Fine - it works. Then it becomes player 2's go. Let's say player 2 roll a 5, the function is, again, called like
self.move_players("P2", 5)
However, here lies the problem: if in the 'self.move_playersI(self, players, spaces)' function I do 'print(players, spaces)'. On player 1's first go it says
Output:
>P1, 3
Then on player 2's go it says
Output:
>P2, 3 >P2, 5
Remember how player 1 rolled a 3. It calls the function once with player 1's value for player 2, then again this time with player 2's actuall value. In some cases, this means player 2 moves double the intended spaces.
Why does this happen?
Is it the way I use lambda or the way I detect if a button is clicked?

Thanks in advance,
Dream

the full script can be found here: https://pastebin.com/yh5jxHFU
Full code
import sys
from PyQt5 import QtWidgets
from PyQt5 import QtCore
from PyQt5 import QtGui
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *
from PyQt5.QtCore import *
import time
import random
from guiLoop import guiLoop
from functools import partial

class Settings():
    SCENE_SIZE_X = 1200 #Settings for everything
    SCENE_SIZE_Y = 900
    GRID_COUNT_X = 4
    GRID_COUNT_Y = 5
    GRID_COUNT_TOTAL = GRID_COUNT_X * GRID_COUNT_Y
    GRID_BOX_WIDTH = 100
    GRID_BOX_HEIGHT = 100
    DEF_FONT_SIZE = 15
    DEF_PLAYER_SIZE = 25
    DEF_LEFT_PADDING = 5
    P1_TOP_PADDING = 10
    P2_TOP_PADDING = DEF_PLAYER_SIZE * 2 + 10
    ROLL_TIME = 20

class Drawing():
    p1 = QPen(QColor(66, 134, 244), 1, Qt.SolidLine) #p1 blue, #4286f4
    brushP1 = QBrush(QColor(66, 134, 244), Qt.SolidPattern)
    p2 = QPen(QColor(244, 65, 65), 1, Qt.SolidLine) #p2 red, #f44141
    brushP2 = QBrush(QColor(244, 65, 65), Qt.SolidPattern)
    transparent = QPen(QColor(255, 255, 255), 1, Qt.SolidLine)
    transparentB = QBrush(QColor(255, 255, 255), Qt.SolidPattern)

class Grid(QtWidgets.QGraphicsScene):

    def __init__(self):
        super().__init__()
        
        self.scrolled = 0
        self.ps = Players()
        self.texts = []
        self.initUI()

    def initUI(self):
        self.draw_grid(Settings.GRID_COUNT_X, Settings.GRID_COUNT_Y, Settings.GRID_BOX_WIDTH, Settings.GRID_BOX_HEIGHT) #drawing grid first
        self.draw_text(Settings.GRID_COUNT_X, Settings.GRID_COUNT_Y, Settings.GRID_BOX_WIDTH, Settings.GRID_BOX_HEIGHT, Settings.DEF_FONT_SIZE) #drawing text next
        rect = self.ps.draw_players("P1", 0)
        self.addRect(rect[0], Drawing.p1, Drawing.brushP1)
        rect = self.ps.draw_players("P2", 0)
        self.addRect(rect[1], Drawing.p2, Drawing.brushP2)

    def draw_grid(self, x_count, y_count, x_size, y_size):
        width = x_count * x_size
        height = y_count * y_size
        self.setSceneRect(0, 0, width, height)
        self.setItemIndexMethod(QtWidgets.QGraphicsScene.NoIndex)

        pen = QPen(QColor(0,0,0), 1, Qt.SolidLine)

        for x in range(0,x_count+1):    
            for y in range(0,y_count+1): #nested for loop for x and y (0,0) - (0,1) - (0,2) etc
                xc = x * x_size
                yc = y * y_size 
                self.addLine(xc,0,xc,height,pen)
                self.addLine(0,yc,width,yc,pen)

    def draw_text(self, x_count, y_count, x_size, y_size, font_size):
        font = QFont("Courier New", font_size, QFont.Bold)
        
        total = Settings.GRID_COUNT_TOTAL
        row_count = Settings.GRID_COUNT_Y
        text_pos_ev = []
        text_pos_od = []
        
        for y in range(0,y_count): #another nested for loop with out the +1 to y and x count
            for x in range(0,x_count):
                xcTEXT = x * x_size
                ycTEXT = y * y_size
                if row_count % 2 == 0: #if the row is even
                    text_pos_ev.append([xcTEXT, ycTEXT, total]) #add to even array
                else:
                    text_pos_od.append([xcTEXT, ycTEXT, total]) #otherwise add to odd array
                total = total - 1                
            row_count = row_count - 1

        for pos in text_pos_ev: #draws the even numbers
            text = self.addText(str(pos[2]), font)
            self.texts.append(text)
            text.setPos(pos[0], pos[1])
            text.setOpacity(0.6)
            
        new_text_pos_od = reverse_section(text_pos_od, 4) #flips the odd numbers so they snake around the board
        for pos in new_text_pos_od: #draws new odd numbers
            text = self.addText(str(pos[2]), font)
            self.texts.append(text)
            text.setPos(pos[0], pos[1])
            text.setOpacity(0.6)

    def move_players(self, player, spaces):
        rect = []
        if(player == "P1"):
            self.remove_player("P1")
            rect = self.ps.draw_players("P1", spaces)
            self.addRect(rect[0], Drawing.p1, Drawing.brushP1)
        elif(player == "P2"):
            self.remove_player("P2")
            rect = self.ps.draw_players("P2", spaces)
            self.addRect(rect[1], Drawing.p2, Drawing.brushP2)
        
    def remove_player(self, player):
        rect = self.ps.clear_old_player(player)
        self.addRect(rect, Drawing.transparent, Drawing.transparentB)
        self.redraw_text()

    def redraw_text(self):
        for text in self.texts:
            self.removeItem(text)
        del self.texts[:]
        self.draw_text(Settings.GRID_COUNT_X, Settings.GRID_COUNT_Y, Settings.GRID_BOX_WIDTH, Settings.GRID_BOX_HEIGHT, Settings.DEF_FONT_SIZE)
        
class Players(QRectF):
    def __init__(self):
        super().__init__()

        self.poses = []
        self.row_count = 0
        self.p1 = QRectF()
        self.p2 = QRectF()
        self.p1pos = 0
        self.p2pos = 0
        
        self.initUI()

    def initUI(self):
        self.gen_pos_arr()
 
    def draw_players(self, player, spaces): #space 1 is box 1, 2 is box 2, 3 is box 3..      
        if(player == "P1"):
            self.p1pos += spaces;
        elif(player == "P2"):
            self.p2pos += spaces;
        
        self.p1 = QRectF(self.poses[self.p1pos][0] + Settings.DEF_LEFT_PADDING, self.poses[self.p1pos][1] + Settings.P1_TOP_PADDING, Settings.DEF_PLAYER_SIZE, Settings.DEF_PLAYER_SIZE)
        self.p2 = QRectF(self.poses[self.p2pos][0] + Settings.DEF_LEFT_PADDING, self.poses[self.p2pos][1] + Settings.P2_TOP_PADDING, Settings.DEF_PLAYER_SIZE, Settings.DEF_PLAYER_SIZE)

        return [self.p1, self.p2]

    def clear_old_player(self, player):
        pos = []
        if(player == "P1"):
            pos = self.poses[self.p1pos]
            pos[0] += Settings.DEF_LEFT_PADDING
            pos[1] += Settings.P1_TOP_PADDING 
        elif(player == "P2"):
            pos = self.poses[self.p2pos]
            pos[0] += Settings.DEF_LEFT_PADDING 
            pos[1] += Settings.P2_TOP_PADDING

        return QRectF(pos[0], pos[1], Settings.DEF_PLAYER_SIZE, Settings.DEF_PLAYER_SIZE)
        

    def gen_pos_arr(self):
        y = Settings.GRID_COUNT_Y-1
        for i in range(1, Settings.GRID_COUNT_Y+1):
            if(y % 2 == 0):
                for x in range(0, Settings.GRID_COUNT_X):
                    self.poses.append([x*Settings.GRID_BOX_WIDTH,y*Settings.GRID_BOX_HEIGHT])
            else:
                for x in range(Settings.GRID_COUNT_X-1, -1, -1):
                    self.poses.append([x*Settings.GRID_BOX_WIDTH,y*Settings.GRID_BOX_HEIGHT])
            y = y - 1

class App(QtWidgets.QDialog):

    def __init__(self):
        super().__init__()
        
        self.images = ['1.png','2.png','3.png','4.png','5.png','6.png']
        self.image = QLabel(self)
        self.currTurn = "<font color='#4286f4'>Player 1's turn</font>"
        self.turn = QLabel(self)
        self.roll = QPushButton("Roll!")
        self.move = QPushButton("Move player")
        self.currPl = 1
        self.vbox1 = QVBoxLayout()
        self.vbox2 = QVBoxLayout()
        self.grid = Grid()
        self.initUI()

    def initUI(self):
        self.setGeometry(300, 100, Settings.SCENE_SIZE_X, Settings.SCENE_SIZE_Y)
        self.setWindowTitle("NEA Board Game")

        self.layout()

    @guiLoop
    def move_players(self, players, spaces):
        temp = 0

        print(players, spaces)
        
        self.move.setEnabled(False)
            
        self.turn.setText(self.currTurn)
        
        #self.grid.move_players(players, spaces)

        if(self.currPl == 1):
            while(temp < spaces):
                self.grid.move_players("P1", 1)
                temp += 1
                yield 0.5
        else:
            while(temp < spaces):
                self.grid.move_players("P2", 1)
                temp += 1
                yield 0.5
        
        if(self.currPl == 1):
            self.currPl = 2
            self.currTurn = "<font color='#f44141'>Player 2's turn</font>" 
            self.turn.setText(self.currTurn)
        elif(self.currPl == 2):
            self.currPl = 1
            self.currTurn = "<font color='#4286f4'>Player 1's turn</font>"
            self.turn.setText(self.currTurn)

        self.roll.setEnabled(True)
        spaces = 0
            


    def layout(self):
        gridGview = QtWidgets.QGraphicsView(self.grid)

        d = self.dice()
        
        layout = QGridLayout(self)
        layout.addWidget(d[1], 2, 1)
        layout.addWidget(d[0], 1, 1)
        layout.addWidget(gridGview, 0, 1)

    def dice(self):
        rollBox = QGroupBox("")
        font = QtGui.QFont("Courier New", 23, QtGui.QFont.Bold)
        
        self.roll.clicked.connect(self.diceRoll)
        
        self.turn.setText(self.currTurn)
        self.turn.setFont(font)
        self.turn.setAlignment(QtCore.Qt.AlignCenter)
        
        self.vbox1.addWidget(self.turn)
        self.vbox1.addWidget(self.roll)
        
        rollBox.setLayout(self.vbox1)

        diceBox = QGroupBox("")
        self.vbox2.addWidget(self.move)
        self.move.setEnabled(False)
        diceBox.setLayout(self.vbox2)
        

        return [rollBox, diceBox]

    @guiLoop
    def diceRoll(self, argument):
        temp = 0
        rollT = Settings.ROLL_TIME
        self.roll.setEnabled(False)
        while(temp <= rollT):
            yield ((temp * 0.02) + 0.05)
            img = random.choice(self.images)
            pixmap = QPixmap(img)
            self.image.setPixmap(pixmap)
            self.vbox2.addWidget(self.image)
            temp += 1
        self.move.setEnabled(True)
        a = lambda: self.move_players("P1" if self.currPl == 1 else "P2", int(img[0:1]))
        self.move.clicked.connect(a)
            
        
def reverse_section(arr, subsection):
    ml2 = [ (arr[i*subsection:(i+1)*subsection]) for i in range(int(len(arr)/subsection)) ]
    ml3 = [ sl[i][0:subsection-2] + [sl[len(sl)-i-1][2]] for sl in ml2 for i in range(len(sl)) ]
    return ml3


if __name__ == '__main__':
    app = QtWidgets.QApplication(sys.argv)
    app.setStyle("Fusion")
    ex = App()
    ex.show()
    sys.exit(app.exec_())
Reply
#2
The problem is not with lambda:

>>> def fred(number):
...     print(number)
...
>>> a = lambda: fred(16)
>>> a()
16
>>> a()
16
>>>
I expect the problem is that you are using yield in move_players instead of return. That means you are creating a generator on a click rather than returning from a function.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#3
(May-16-2019, 05:32 PM)ichabod801 Wrote: I expect the problem is that you are using yield in move_players instead of return. That means you are creating a generator on a click rather than returning from a function.

Oh I see. How can I get around this? I have to use yield so my gui doesn't freeze in the while loop.
Reply
#4
This is interesting. I looked a little bit but don't have time to deep-dive on this. If you can provide a runnable snippet of code which reproduces this problem without GUI code though (ideally zero imports), I'll make time to look deeper.
Reply
#5
(May-16-2019, 06:09 PM)DreamingInsanity Wrote: Oh I see. How can I get around this? I have to use yield so my gui doesn't freeze in the while loop.

I think you need to refigure how you are doing movement. When you click on something to move it, you should change it's state to 'trying to move to x, y'. Then you should periodically update your game. When you update things, you check every object for movement. If it is trying to move somewhere, you move it a little closer. If it gets there, change it's state to 'not moving'.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#6
(May-16-2019, 07:24 PM)micseydel Wrote: This is interesting. I looked a little bit but don't have time to deep-dive on this. If you can provide a runnable snippet of code which reproduces this problem without GUI code though (ideally zero imports), I'll make time to look deeper.

I don't think I can without a gui.

(May-16-2019, 07:53 PM)ichabod801 Wrote: I think you need to refigure how you are doing movement. When you click on something to move it, you should change it's state to 'trying to move to x, y'. Then you should periodically update your game. When you update things, you check every object for movement. If it is trying to move somewhere, you move it a little closer. If it gets there, change it's state to 'not moving'.

If I remove out the while loops and add this line in:
self.grid.move_players(players, spaces)
which is commented out in my code I provided, even though there is no yield, I still get the same problem. So correct me if I'm wrong but, changing the way I do movement, won't really do much.

D
Reply
#7
(May-17-2019, 05:22 PM)DreamingInsanity Wrote: If I remove out the while loops and add this line in:
Python Code: (Double-click to select all)
1

self.grid.move_players(players, spaces)
which is commented out in my code I provided, even though there is no yield, I still get the same problem. So correct me if I'm wrong but, changing the way I do movement, won't really do much.

Seeing as what you describe is not what I suggested you do, it's not really a counter argument to what I suggested you do. And if you GUI hangs up without the loops and the yields, there's something else going on.
Craig "Ichabod" O'Brien - xenomind.com
I wish you happiness.
Recommended Tutorials: BBCode, functions, classes, text adventures
Reply
#8
(May-17-2019, 05:37 PM)ichabod801 Wrote: Seeing as what you describe is not what I suggested you do, it's not really a counter argument to what I suggested you do. And if you GUI hangs up without the loops and the yields, there's something else going on.


Ok, fine, I will try out what you said.
Also, my GUI only hangs if I am using while loops with
time.sleep(x)
(it hangs until the loop is over)
To stop it hangning, I use this script (modified to work with PyQt5) and use yields instead of 'time.sleep()'.

D
Reply
#9
It looks like every time the roll button is clicked the method diceRoll is called in which a new lambda is connected to the move player button.

player 1 rolls, 1 lambda function gets connected to the player button.
player 1 clicks move, the 1st lambda function is called.
player 2 rolls, another lambda function gets connected to the player button.
player 2 clicks move, the 1st lambda plus the 2nd lambda gets called.
This will continue to increase every time roll is clicked.
Reply
#10
(May-17-2019, 09:46 PM)Yoriz Wrote: It looks like every time the roll button is clicked the method diceRoll is called in which a new lambda is connected to the move player button.

That is kind of what I thought might have been happening. Can remove the lambda though so there is always 1?
I have tried using partial from functools but that does the same.

D
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Doubt approach update 2 Treeview at the same time TomasSanchexx 7 1,864 Sep-19-2023, 01:19 AM
Last Post: SaintOtis12
  Figure Gets Larger Every time I click a show button joshuagreineder 2 1,272 Aug-11-2022, 06:25 AM
Last Post: chinky
  tkinter get method is not accepting value when called by function jagasrik 1 2,496 Sep-16-2020, 05:28 AM
Last Post: Yoriz
  How to stop time counter in Tkinter LoneStar 1 4,297 Sep-11-2020, 08:56 PM
Last Post: Yoriz
  Display text 3 words at a time algae 5 2,706 Jun-27-2020, 10:25 AM
Last Post: menator01
Photo Visualizing time series data of graph nodes in plotly deepa 3 3,450 Mar-16-2020, 07:06 AM
Last Post: Larz60+
  [Tkinter] How to adjust time - tkinter Ondrej 2 2,811 Jun-20-2019, 05:53 PM
Last Post: Yoriz
  [Tkinter] Unable to create checkbox and select at run time tej7gandhi 5 4,585 May-05-2019, 04:57 PM
Last Post: tej7gandhi
  Buttons not appearing, first time button making admiral_hawk 5 3,332 Dec-31-2018, 03:26 AM
Last Post: admiral_hawk
  clear all widgets at same time (not delete/remove) shift838 0 2,716 Dec-17-2018, 11:55 PM
Last Post: shift838

Forum Jump:

User Panel Messages

Announcements
Announcement #1 8/1/2020
Announcement #2 8/2/2020
Announcement #3 8/6/2020