Python Forum
How to correctly update score based on elapsed time?
Thread Rating:
  • 0 Vote(s) - 0 Average
  • 1
  • 2
  • 3
  • 4
  • 5
How to correctly update score based on elapsed time?
#1
I've tried several methods but haven't been able to get it right at all.

Every 5 seconds the score (starting at 0) goes up by 1. Every 30 seconds, the score gets doubled, so in the first 30 seconds, the score will be 6, and since that will be 30 seconds, the 6 gets doubled to 12. However in my case, the score goes to 11. My other attempts have made the score go to 10, but I can never get it to to correctly double.

import pygame
import sys

pygame.init()

screen_width = 800
screen_height = 600
screen = pygame.display.set_mode((screen_width, screen_height))
pygame.display.set_caption('game test 1')

white = (255, 255, 255)
black = (0, 0, 0)

clock = pygame.time.Clock()

font = pygame.font.SysFont(None, 48)

score = 0
last_score_increase = 0
last_score_double = 0

def display_time(time_ms):
    # get mm:ss
    seconds = int((time_ms / 1000) % 60)
    minutes = int((time_ms / (1000 * 60)) % 60)
    return f"{minutes:02}:{seconds:02}"

def game_loop():
    global score, last_score_increase, last_score_double
    start_time = pygame.time.get_ticks()
    running = True
    while running:
        current_time = pygame.time.get_ticks() - start_time
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False, score
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    return current_time, score
                if event.key == pygame.K_r:
                    return "restart", score

        if current_time - last_score_increase >= 5000:
            score += 1
            last_score_increase = current_time

        if current_time >= 30000 and current_time - last_score_double >= 30000:
            score *= 2
            last_score_double = current_time

        screen.fill(white)
        score_text = font.render(f'Score: {score}', True, black)
        screen.blit(score_text, (10, 10))
        pygame.display.flip()
        clock.tick(60)
    
    return False, score

def end_screen(elapsed_time, score):
    screen.fill(white)
    time_text = font.render(f"Time: {display_time(elapsed_time)}  Score: {score}", True, black)
    screen.blit(time_text, (screen_width // 2 - time_text.get_width() // 2, screen_height // 2 - time_text.get_height() // 2))
    restart_text = font.render("r to restart gane", True, black)
    screen.blit(restart_text, (screen_width // 2 - restart_text.get_width() // 2, screen_height // 2 + 50))
    pygame.display.flip()

    waiting = True
    while waiting:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_r:
                    return True
    return False

# the game loop
while True:
    result, final_score = game_loop()
    if result == "restart":
        score = 0
        continue
    elif result is False:
        break
    
    if end_screen(result, final_score):
        score = 0
        continue
    else:
        break

pygame.quit()
sys.exit()
Edit: Nevermind I managed to solve it.
Reply
#2
Since you don't have hours, this is wrong:
def display_time(time_ms):
    # get mm:ss
    seconds = int((time_ms / 1000) % 60)
    minutes = int((time_ms / (1000 * 60)) % 60)
    return f"{minutes:02}:{seconds:02}"
I would write it like this:
def display_time(time_ms):
    """Return time in milliseconds as string mm:ss."""
    minutes, seconds = divmod(int(time_ms / 1000), 60)
    return f"{minutes: 2}:{seconds:02}"
But that has nothing to do with your timing problem. That problem is caused by you not keeping track of time. Each time you increase or double your score you restart your clock. This introduces errors in the measurement of last_score_increase and last_score_double so they are not synchronized. In your example this resulted in the score being doubled before it was incremented 6 times.

Instead of setting last_score_increase = current_time, you should set last_score_increase += 5000. This keeps loast_score_increase and last_score_double synchronized because they always measure from the same starting point.
def game_loop():
    global score
    start_time = pygame.time.get_ticks()
    last_score_increase = last_score_double = 0
    while True:
        current_time = pygame.time.get_ticks() - start_time
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False, score
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    return current_time, score
                if event.key == pygame.K_r:
                    return "restart", score
        if current_time - last_score_increase >= 5000:
            score += 1
            last_score_increase += 5000

        if current_time - last_score_double >= 30000:
            score *= 2
            last_score_double += 30000

        screen.fill("white")
        score_text = font.render(f"Score: {score}", True, "black")
        screen.blit(score_text, (10, 10))
        pygame.display.flip()
        clock.tick(60)
Reply
#3
My approach is a little different. The code can be optimized better.

import pygame
from datetime import datetime


pygame.init()
pygame.font.init()

window_size = (1280,740)

window = pygame.display.set_mode(window_size)

clock = pygame.time.Clock()

font_size = 38

running = True

class Score:
    def __init__(self):
        self.score = 0


class MyText:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def update(self, surface, mytext=None):
        font = pygame.font.SysFont(None, font_size)
        text = font.render(str(mytext), True, 'darkgreen')
        surface.blit(text, (self.x, self.y))


class MyTimer:
    def __init__(self, x, y, counter=5, sometext='Default', color='black'):
        self.counter = counter
        self.default = counter
        self.x = x
        self.y = y
        self.time = pygame.time.get_ticks()
        self.sometext = sometext
        self.color = color

    def update(self, surface, score):
        if pygame.time.get_ticks() - self.time >= 1000:
            self.time = pygame.time.get_ticks()
            self.counter -= 1
        
        if self.counter <= 0:
            self.counter = self.default
            if self.default == 5:
                score.score += 1

            elif self.default == 30:
                score.score *= 2

        font = pygame.font.SysFont(None, font_size)
        text = font.render(f'{self.sometext}: {self.counter:02}', True, self.color)
        surface.blit(text, (self.x, self.y))


class MyClock:
    def __init__(self, x, y):
        self.x = x
        self.y = y
        self.current_time = pygame.time.get_ticks()
        self.time = datetime.now().strftime('%I:%M:%S %p')

    def update(self, surface, mytext=None):
        if pygame.time.get_ticks() - 1000 >= self.current_time:
            self.current_time = pygame.time.get_ticks()
            self.time = datetime.now().strftime('%I:%M:%S %p')
            

        font = pygame.font.SysFont(None, font_size)
        text = font.render(f'Clock - {self.time}', True, 'red')
        surface.blit(text, (self.x, self.y))


myclock = MyClock(10,10)
score = Score()
mytext = MyText(300, 10)
counter1 = MyTimer(450, 10, sometext='Timer 1')
counter2 = MyTimer(600, 10, 30, sometext='Timer 2', color='orange')

while running:
    window.fill('white')

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    myclock.update(window)
    mytext.update(window, f'Score: {score.score}')
    counter1.update(window, score)
    counter2.update(window, score)

    pygame.display.update()
    clock.tick(60)

pygame.quit()
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags
Download my project scripts


Reply
#4
@menator's solution has the same problem as the original post. The timers used to increment and double the score don't remain synchronized because they are reset to a new time base each time this happens:
        if pygame.time.get_ticks() - self.time >= 1000:
            self.time = pygame.time.get_ticks()
            self.counter -= 1
This is actually worse than what happens in the original post as the reset happens every second instead of every 5 seconds. Even if MyTimer was reworked to remain synchronized to the starting time, there is a problem with having multiple MyTimer's updating at different times. counter1 is called before counter2, and this will occasionally result in get_ticks() not returning the same value for both calls. If counter1 is called at 30 seconds - 1ms and counter2 and 30 seconds, the score will be incorrect. Updating the two timers needs to be an atomic operation.
class MyTimer:
    def __init__(self, x, y, counter=5, sometext="Default", color="black", time=None):
        self.counter = counter
        self.default = counter
        self.x = x
        self.y = y
        self.time = time or pygame.time.get_ticks()
        self.sometext = sometext
        self.color = color

    def update(self, surface, score, time=None):
        sync_time = time or pygame.time.get_ticks()
        while sync_time - self.time >= 1000:
            self.time += 1000
            self.counter -= 1
            if self.counter <= 0:
                self.counter = self.default
                if self.default == 5:
                    score.score += 1

                elif self.default == 30:
                    score.score *= 2

        font = pygame.font.SysFont(None, font_size)
        text = font.render(f"{self.sometext}: {self.counter}", True, self.color)
        surface.blit(text, (self.x, self.y))


myclock = MyClock(10, 10)
score = Score()
mytext = MyText(300, 10)
start_time = pygame.time.get_ticks()
counter1 = MyTimer(450, 10, sometext="Timer 1", time=start_time)
counter2 = MyTimer(600, 10, 30, sometext="Timer 2", color="orange", time=start_time)

running = True
while running:
    window.fill("white")

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False

    myclock.update(window)
    mytext.update(window, f"Score: {score.score}")
    current_time = pygame.time.get_ticks()
    counter1.update(window, score, time=current_time)
    counter2.update(window, score, time=current_time)

    pygame.display.update()
    clock.tick(60)

pygame.quit()
Time is tricky to do right. Just for fun I tried reworking @menator's code in a less interconnected way.
import pygame
from datetime import datetime


pygame.init()
pygame.font.init()
window_size = (1280, 740)
window = pygame.display.set_mode(window_size)
clock = pygame.time.Clock()
default_font = pygame.font.SysFont(None, 38)


class TextDisplay(pygame.sprite.Sprite):
    def __init__(self, x, y, text="", prefix="", color="black", font=default_font):
        super().__init__()
        self.rect = pygame.Rect(x, y, 0, 0)
        self.prefix = prefix
        self.color = color
        self.font = font
        self.update(text)

    def update(self, text=None):
        if text:
            self.text = text
        self.image = self.font.render(f"{self.prefix}{self.text}", True, self.color)


class Score:
    def __init__(self):
        self.score = 0

    def increment(self, amount=1):
        self.score += amount

    def double(self):
        self.score *= 2

    def __str__(self):
        return str(self.score)


class MyTimer:
    instances = []

    def __init__(self, counts=10, callback=None, time=None):
        self.counter = self.default = counts
        self.callback = callback
        self.time = time or pygame.time.get_ticks()
        self.instances.append(self)

    def update(self, time=None):
        sync_time = time or pygame.time.get_ticks()
        while sync_time - self.time >= 1000:
            self.time += 1000
            self.counter -= 1
            if self.counter <= 0:
                self.counter = self.default
                if self.callback is not None:
                    self.callback()

    def __str__(self):
        return str(self.counter)

    @classmethod
    def update_all(cls):
        time = pygame.time.get_ticks()
        for instance in cls.instances:
            instance.update(time)


class MyClock:
    def __str__(self):
        return datetime.now().strftime("%I:%M:%S %p")


myclock = MyClock()
score = Score()
start_time = pygame.time.get_ticks()
displays = pygame.sprite.Group(
    [
        TextDisplay(10, 10, myclock, "Clock: ", "green"),
        TextDisplay(300, 10, score, "Score: ", "blue"),
        TextDisplay(450, 10, MyTimer(5, score.increment, start_time), "Timer 1: ", "orange"),
        TextDisplay(600, 10, MyTimer(30, score.double, start_time), "Timer 2: ", "red"),
    ]
)
running = True
while running:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            running = False
    window.fill("white")
    MyTimer.update_all()
    displays.update()
    displays.draw(window)
    pygame.display.update()
    clock.tick(60)

pygame.quit()
Reply
#5
I've tried both the original (one I wrote) and the edited one.
Both seem to have the same problem. When doubling sometimes it will not double correct.
Usually on the second or third pass of the 30 second timer. Example would be when doubling on the second pass it should be 36 but goes to 35. Note that this is not every time on either code. Just sometimes.
I welcome all feedback.
The only dumb question, is one that doesn't get asked.
My Github
How to post code using bbtags
Download my project scripts


Reply
#6
I cannot duplicate the issue you describe. I do see that the score should be updated after the timers so it is in sync with the displayed count, but the score is always correct, just one second behind. I never see an incorrect score.

For a test I modified the MyTimer code to decrement the counter 100 times a second. Each time the score changed I verified that the change was correct:
    myclock.update(window)
    mytext.update(window, f"Score: {score.score}")
    current_time = pygame.time.get_ticks()
    counter1.update(window, score, time=current_time)
    counter2.update(window, score, time=current_time)
    if score.score != prev_score:
        if score.score != prev_score + 1 and score.score != (prev_score + 1) * 2:
            print(score.score)
        prev_score = score.score
I waited until the score grew so large it could not be displayed on the screen (didn't take long) and no errors were identified.
Reply
#7
(Oct-23-2024, 07:18 PM)deanhystad Wrote: Since you don't have hours, this is wrong:
def display_time(time_ms):
    # get mm:ss
    seconds = int((time_ms / 1000) % 60)
    minutes = int((time_ms / (1000 * 60)) % 60)
    return f"{minutes:02}:{seconds:02}"
I would write it like this:
def display_time(time_ms):
    """Return time in milliseconds as string mm:ss."""
    minutes, seconds = divmod(int(time_ms / 1000), 60)
    return f"{minutes: 2}:{seconds:02}"
But that has nothing to do with your timing problem. That problem is caused by you not keeping track of time. Each time you increase or double your score you restart your clock. This introduces errors in the measurement of last_score_increase and last_score_double so they are not synchronized. In your example this resulted in the score being doubled before it was incremented 6 times.

Instead of setting last_score_increase = current_time, you should set last_score_increase += 5000. This keeps loast_score_increase and last_score_double synchronized because they always measure from the same starting point.
def game_loop():
    global score
    start_time = pygame.time.get_ticks()
    last_score_increase = last_score_double = 0
    while True:
        current_time = pygame.time.get_ticks() - start_time
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                return False, score
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_q:
                    return current_time, score
                if event.key == pygame.K_r:
                    return "restart", score
        if current_time - last_score_increase >= 5000:
            score += 1
            last_score_increase += 5000

        if current_time - last_score_double >= 30000:
            score *= 2
            last_score_double += 30000

        screen.fill("white")
        score_text = font.render(f"Score: {score}", True, "black")
        screen.blit(score_text, (10, 10))
        pygame.display.flip()
        clock.tick(60)

Thanks for the reply and updated code. I never would have guessed to use "last_score_increase = last_score_double = 0" so maybe that just means I need to learn more. This seems to work completely fine in every scenario so I appreciate it.
Reply
#8
"last_score_increase = last_score_double = 0" is just shorthand for:
last_score_increase = 0
last_score_double = 0
Its not great programming practice to put multiple statements on one line. I only do it for short snippets.

The important difference is not setting last_score_increase and last_score_double to the current time. The two must remain rooted to some common time in the past. That is why I increment the time.
Reply
#9
(Oct-25-2024, 03:04 PM)deanhystad Wrote: "last_score_increase = last_score_double = 0" is just shorthand for:
last_score_increase = 0
last_score_double = 0
Its not great programming practice to put multiple statements on one line. I only do it for short snippets.

The important difference is not setting last_score_increase and last_score_double to the current time. The two must remain rooted to some common time in the past. That is why I increment the time.

Yeah now I get it, I figured that out shortly after while testing it a bit but thanks for the clarification. I also realised I approached my question in a poor way by trying to tackle scoring + elapsed time all at once and by trying to somehow combine them.

I did them individually (rather than trying to make score be directly based on the elapsed_time related variables) and it worked out great, and I even implemented other similar time-based code that now works flawlessly.
Reply


Possibly Related Threads…
Thread Author Replies Views Last Post
  Using Time to Update a Global Variable and Label in TkInter cameron121901 5 5,352 Apr-22-2019, 05:08 PM
Last Post: SheeppOSU
  Need help fixing Time and Score issues Kingrocket10 5 5,397 Dec-07-2017, 12:48 AM
Last Post: hammza
  Time limit and Score not working Coding help Kingrocket10 1 3,520 Nov-09-2017, 03:20 PM
Last Post: heiner55

Forum Jump:

User Panel Messages

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