Why shouldn't I update the screen inside a loop?

369 views
Skip to first unread message

Max Fritzler

unread,
Sep 7, 2021, 11:24:04 AM9/7/21
to Kivy users support
I'm teaching myself Kivy by writing a card game, a version of rummy.
My pattern is the kvsol Solitaire program, available here: https://github.com/jnb666/kvsol This is quite a complex program, and I find myself not following it very well.
I'm trying to simulate a game with 4 players, each with a hand of 11 cards, and a game consisting of 7 hands, or rounds.

I found a post which identifies my problem, but I can't envision the solution it recommends, which is to not use the loops shown below.  I am just not getting the concept of scheduling the work and controlling flow with callbacks instead of in a loop.  I have of course read the kivy docs, the developer's guide, and the API on clock.schedule, done much of the Kivy crash course.  I'm pretty sure I could animate moving a card from one spot to another once displayed.  I'm just not seeing how to apply those concepts to progressively displaying widgets added to a layout inside a loop.  I expected to be able to call some sort of "player_layout.draw" or something to force a screen update.

The code "works" without error, but the screen only updates at the very end of the outer loop showing the game layout and cards as they would be in the last round (aka hand).

I'm a "top down" thinker and there is something about the whole event-driven concept with Kivy that I'm just missing.  I haven't found (or perhaps, understood) the right piece of doc, or the right tutorial, to get the correct pattern of usage in my mind.  Any comments to help with that would be much appreciated.

My code where I'm stuck is:
def play_game(self):
     for goal in self.game.round_goals:
     self.round = Round(self.num_decks, self.players, round_goal=goal) 
     self.round.prepare_new_round()

def prepare_new_round(self):
Logger.debug('\n\nPreparing a new round. Round goal is {}'.format(self.goal))
self.deck = Deck(jokers=True, num_jokers=2, decks_used=self.num_decks)
self.deck.shuffle(2) # shuffle it 2 times. Second time should be pointless since it uses random.shuffle
self.clear_board()
for player in self.players:
         player.hand.empty()

# EVIDENTLY, in the loop below I am commiting something like the error described in this post
# https://stackoverflow.com/questions/62966256/kivy-animations-in-a-loop
"""Instead of using a loop, bind to the on_complete even of the animation a function that runs your next task.
This will let the animation finish and then run your next piece of code.
...
get a list of parameters for making your cards, then use Clock.schedule_once each time the animation runs to
create the next one from the list, until the list is empty. """

for _ in range(11):
    for player in self.players:
    # Deal cards to players, one card per player at a time, like a real game
     card = self.deck.deal(1)[0] # deck.deal returns a list of dealt cards
     player.hand.draw_card(card)
    # Display the cards being added to the player's hand on the player_layout
    if config.RUN_MODE == "play":
        img = Image(source='images/' + card.image)
        player_layout = self.root.ids['P' + str(player.num)]
        player_layout.add_widget(img) # P1 is id for player_layout #1
       # Somehow I need to block or schedule so Kivy can display the layout
      # player_layout._trigger_layout() # does nothing
      # Clock.schedule_once(partial(self.prepare_new_round)) 

Evidently, instead of the loops I need to do something like:
Round
       if last round exit else next round
             deal a card to a player
                      If last player deal next card to first player
                             if card = 11 (the last card in a hand) stop dealing, display screen wait for input
                   if card =11

Thanks in advance for any help you can give.

Elliot Garbus

unread,
Sep 7, 2021, 2:45:22 PM9/7/21
to kivy-...@googlegroups.com

Here is a section of documentation you should read: https://kivy.org/doc/stable/guide/events.html

 

The key element to conceptualize is that kivy (or any gui) is an event driven system.  There is a main loop in kivy.  When nothing is running, the main loop is running waiting for an event.  That event could be a clock time out,  a keyboard press, a mouse event, a touch event… When these events occur, the main event loop then launches your appropriate code.  If you need to wait, use Clock.  Never sleep in the main thread.  This will prevent the kivy event loop from executing.  If you execute a long loop – the event loop does not run.

 

You do not need to issue draw events, generally kivy will keep the screen up to date.

 For your code you want to break the loop into steps.  Rather than using a for loop,  Create a counter instance variable.  Every 'step’ count down your instance variable,  and setup a call back to the same method, until the counter reaches zero (or a set value…).  The example below uses this approach to create a countdown timer.

 

There is a second example at the bottom of this note that does some simple animation, and creates a pause between the motions.

 

Here is an example that creates a count down using this approach.

from kivy.app import App
from kivy.lang import Builder
from kivy.properties import NumericProperty
from kivy.clock import Clock

kv =
"""
BoxLayout:
    orientation: 'vertical'
    Label:
        text: str(app.count_down)
        font_size: 80
    Button:
        size_hint_y: None
        height: 48
        text: 'Restart'
        on_release:
            app.count_down = 11
            app.execute_count_down()
        disabled: app.count_down != 0

"""


class CountDownApp(App):
    count_down = NumericProperty(
10)
   
# Kivy property used because it is being displayed,
    # if the count is not displayed you could use an instance variable

   
def build(self):
       
return Builder.load_string(kv)

   
def on_start(self):
        Clock.schedule_once(
self.execute_count_down, 3)

   
def execute_count_down(self, *args):
       
if not self.count_down:
           
return # do not schedule a callback
       
else:
           
self.count_down -= 1
           
Clock.schedule_once(self.execute_count_down, 1)


CountDownApp().run()

 

Here is a simple example that draws 2 buttons.  Click on a button and it moves to the center, pauses, and then moves back.

 

from kivy.app import App
from kivy.lang import Builder
from kivy.uix.button import Button
from kivy.properties import NumericProperty
from kivy.clock import Clock
from kivy.animation import Animation


kv =
"""
<CardButton>:
    size_hint: None, None
    size: 100, 150
    on_release: self.move_card()

FloatLayout:
    id: float
    CardButton:
        id: card_1
        text: 'Card 1'
        pos: 0, float.center_y - self.height/2
        home_pos: 0
    CardButton:
        id: card_2
        text: 'Card 2'
        pos: float.right - self.width, float.center_y - self.height/2
        home_pos: float.right - self.width
"""


class CardButton(Button):
    home_pos = NumericProperty()

   
def move_card(self):
        to_center = Animation(
x=self.parent.center_x - self.width/2, duration=4# animates the position
       
to_center.bind(on_complete=self._wait_move_to_home)   # when animation complete, callback
       
to_center.start(self)

   
def _wait_move_to_home(self, *args):
        Clock.schedule_once(
self._move_to_home, 2# wait for 2 seconds

   
def _move_to_home(self, *args):
        to_home = Animation(
x=self.home_pos, duration=2# move back with duration 2 seconds
       
to_home.start(self)


class AnimCards(App):
   
def build(self):
        
return Builder.load_string(kv)

   
def on_start(self):
       
pass


AnimCards().run()

--
You received this message because you are subscribed to the Google Groups "Kivy users support" group.
To unsubscribe from this group and stop receiving emails from it, send an email to kivy-users+...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/kivy-users/09649cb0-9593-4fe6-8496-daf6a6d4ade8n%40googlegroups.com.

 

Max Fritzler

unread,
Sep 30, 2021, 2:30:36 PM9/30/21
to Kivy users support
First, thanks for the response and examples.  I was able to make some progress with those.
Second, due diligence: I also purchased and read Ulloa, Roberto. Kivy – Interactive Applications and Games in Python - Second Edition . Packt Publishing. Kindle Edition.  That helped with a number of things, but not this.
I found a code snippet # https://github.com/kivy/kivy/wiki/Delayed-Work-using-Clock, and got that working in a sample app.
But I'm still stumbling over the program flow using callbacks.  Because the code is lengthy, I'll put extracts in the attachment, but put what I think are the essentials here, and hope nobody needs to read the whole code.

I'll start with my log, which shows where I'm puzzled, and that will tie to the code.  See "Why is it processing this line after dealing only one card ??!!??"

[INFO   ] [Base        ] Start application main loop
[DEBUG  ] [ImageSDL2   ] Load <C:\Users\Max Fritzler\PycharmProjects\rsv2\images\6s.png>
playing round 2S


Preparing a new round.  Round goal is 2S
Before the dealing is done, the deck size is 108

Starting trick number 1.
Added card 6♠ to player Player 1's hand
After the dealing is done, the deck size is 107
Why is it processing this line after dealing only one card ??!!??
Deck size should be 64
Joker 9♠ 4♦ 9♣ 10♥ 3♣ 7♥ J♦ J♥ 2♠ 8♣ A♣ 3♥ K♥ J♥ 10♥ A♠ K♠ 5♥ 7♥ 3♥ 2♣ 3♠ 3♦ 7♣ K♥ 9♣ A♦ 2♦ Q♠ 4♣ 7♣ J♣ A♠ 10♠ 2♣ K♠ Q♠ 7♦ A♦ 4♠ 6♥ A♥ 8♣ 6♦ 9♥ 9♠ A♥ Q♥ 2♥ Q♣ 2♠ 8♥ Joker A♣ J♣ 8♦ 7♠ 5♣ 10♠ 7♦ J♦ K♦ 5♠ 3♦ 8♠ 6♠ 5♦ 9♦ 4♥ 6♣ 9♦ Q♦ 3♣ 5♦ 6♣ Joker 10♣ 4♣ 10♣ J♠ J♠ 3♠ K♦ K♣ Q♥ 8♥ 4♥ 4♠ 10♦ 5♠ 5♣ 9♥ 10♦ 6♦ 8♠ 6♥ Joker 4♦ Q♣ 8♦ 2♥ K♣ 5♥ 7♠ 2♦ Q♦
Displaying the cards in Player 1's hand.
hand size: 1
6♠
Displaying the cards in Player 2's hand.
hand size: 0

Displaying the cards in Player 3's hand.
hand size: 0

Displaying the cards in Player 4's hand.
hand size: 0

[DEBUG  ] [ImageSDL2   ] Load <C:\Users\Max Fritzler\PycharmProjects\rsv2\images\12d.png>
At end of prepare-new-round.

[INFO   ] [Base        ] Leaving application in progress...

Process finished with exit code 0

Essentials of the code flow.  More in the attachment

class RummySimulator(App):
    ...
    build
    ...
    on_start:
         Clock.schedule_once(self.game.play_game, 1)

    def play_game(self, *args):
                 ....
        round = Round(self.num_decks, self.players, round_goal=self.round_goal)
        round.prepare_new_round()

... NOTE WELL. Round is in a separate module
class Round(EventDispatcher):
    # I added EventDispatcher because without it, I got a type error.  Since Round produces events, it seems to make sense to add it.
    """The Round class is responsible for creating the player layouts, dealing cards to the layouts,
    and controlling turns the players must take."""
    card_dealt = NumericProperty()   # Unless I display this, I probably don't need to declare this as a NumericProperty
    def __init__(self, num_decks, players, round_goal):
        super().__init__()
        self.deck = None
        self.num_decks = num_decks
        self.players = players
        self.player = players[0]    # This will be an instance variable to control callbacks
        self.num_players = len(self.players)
        self.goal = round_goal
        from kivy.app import App
        app = App.get_running_app()
        self.root = app.root
        self.delay = app.framerate()
        self.num_cards = app.game.num_cards
        self.cards_dealt_per_round = self.num_cards * len(self.players)
        assert self.cards_dealt_per_round == 11 * 4
        self.trick_num = 1      # This will be an instance variable to control callbacks
        self.counter=0          # This will be an instance variable to control callbacks
        print('playing round {}'.format(self.goal))

    def prepare_new_round(self):
        print('\n\nPreparing a new round.  Round goal is {}'.format(self.goal))
            ... Get and shuffle the deck, etc.
        print('Before the dealing is done, the deck size is {}'.format(str(self.deck.size)))
        self.deal_cards(self.players)
        print('After the dealing is done, the deck size is {}'.format(str(self.deck.size)))
        print('Why is it processing this line after dealing only one card ??!!??')
        print('Deck size should be ' + str((self.num_decks * 54) - (len(self.players) * 11)))
        print(self.deck)
        for player in self.players:
            print('Displaying the cards in ' + player.name + "'s hand.")
            print('hand size: ' + str(player.hand.size))
            print(player.get_hand())

        # draw a card to go on the discard stack
        discard = self.deck.deal(1)[0]  # deck returns a stack.  [0] gets the card from the stack.
        img = Image(source='../images/' + discard.image)
        self.root.ids['discard'] = img
        self.root.ids.pack_and_discard.add_widget(img)
        print('At end of prepare-new-round.\n')

    def deal_cards(self, *args):
        """Deal cards to players, one card per player at a time, like a real game.
        In trick-taking games, this would be known as a trick.
        Stop when each player has trick_num cards.
        Using callback instead of a loop, thus allowing kivy to update the screen and show the cards being dealt."""
        """DESIGN NOTES:
        The simplest way would be to deal cards to the players until each had 11 cards.  But I'm trying to understand
        how to do with callbacks what I would ordinarily do with nested loops.  Ordinarily, within a round I would
        loop 11 times, once for each trick that will be played in the round.  For each trick I would loop for example
        4 times, dealing a card to a player, once for each player.  I would like to program it this way, because it
        makes it easier to debug the dealing, particularly if I later simulate dealing from a stacked deck.
        So, here I want deal_cards to call deal_a_trick num_cards times.  num_cards is short for 'number of cards in
        a hand', which is the same as the number of tricks in a round.
        Then, I want deal_a_trick to call deal_a_card num_players times.  Since I'm using callbacks,  I need instance counters which
        I can then compare to num_cards and num_players to
        determine when to stop calling the callback function."""
    print('\nStarting trick number {}.'.format(self.trick_num))
    if self.trick_num > self.num_cards:
        Logger.debug('Stopping because self.trick_num > self.num_cards.')
        return False
    # Another way of deciding when to stop follows
    if self.card_dealt == self.cards_dealt_per_round:
        # The entire expected number of cards has been dealt to all players, so quit
        Logger.debug('self.card_dealt == self.cards_dealt_per_round.')
        return False        # I think we want to stop event processing now, so return False
    self.deal_a_trick()
    self.trick_num += 1


    def deal_a_trick(self, *args):
        if self.trick_num > self.num_cards:
            return False
        else:
            # the first player is defined in the __init__ constructor
            self.deal_a_card(self.player)
            # CRUCIAL.  Define the stop condition for calling deal_a_card
            # player is defined in init for the class, and starts at player.num==Player 1, 0th in the list
            if self.player.num == self.num_players:
                self.player = self.players[0]       # reset the current player
                return False    # Done dealing this trick
            else:
                # remember player.num is one more than the index to players, so this effectively increments the player
                self.player = self.players[self.player.num]
            Clock.schedule_once(self.deal_a_trick, 2)


    def deal_a_card(self, player, *args):

        card = self.deck.deal(1)[0]  # deck.deal returns a list of dealt cards
        self.player.hand.draw_card(card)

        # Display the cards being added to the player's hand on the player_layout
        if config.RUN_MODE=="play":
            # I am not sure why adding an image to the layout causes a screen update on callback.  I don't seem
            # to be declaring either as a kivy ObjectProperty.
            img = Image(source='../images/' + card.image)
            player_layout = self.root.ids['P' + str(self.player.num)]

            player_layout.add_widget(img)  # P1 is id for player_layout #1
            print('Added card {card_name} to player {player_name}\'s hand'.format(card_name=card.name,
                                                                                        player_name=self.player.name))
            self.card_dealt += 1    # Increment the counter used to tell when dealing is done
            # Unless I display the card_dealt property in the gui, changing the property wont' trigger an event

            # Clock.schedule_once(self.deal_a_card, 1)  # DO NOT callback here, let deal_a_trick do it.

AS ALWAYS, Thanks in advance for any help anyone can give.  I feel like if I can understand how this should work, I'll be able to go
a long way on my own with just the developer's guide and the API.
src.zipx

Elliot Garbus

unread,
Sep 30, 2021, 6:57:44 PM9/30/21
to kivy-...@googlegroups.com

This is too much code for me to go through.  The zipped code is missing a dependency and will not run… and it is also a lot of code. 

Try stepping through the code with a debugger and see what is happening. 

Or share a minimal, executable example that shows your issue.

Max Fritzler

unread,
Oct 5, 2021, 8:58:01 AM10/5/21
to Kivy users support
Per your suggestion, I created a minimal executable example, which helped a lot.  I made the Round class subclass Widget, not EventDispatcher, which made the code in it work.  Then I expanded the minimal example to mimic 4 player layouts.  By setting different delays on the Clock.schedule_once commands for their callbacks, I was able to really get a sense of how Clock was queuing things.  Player 4, with a shorter delay, would deal a card before Player 2, with a longer delay.  So let's close this question, because further questions will need a different subject line.

As always, thanks for your patience and you help.
Max
Reply all
Reply to author
Forward
0 new messages