Posts: 9
Threads: 3
Joined: Oct 2024
Oct-23-2024, 01:29 PM
(This post was last modified: Oct-23-2024, 01:29 PM by temlotresid6.)
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.
Posts: 6,794
Threads: 20
Joined: Feb 2020
Oct-23-2024, 07:18 PM
(This post was last modified: Oct-23-2024, 08:06 PM by deanhystad.)
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)
Posts: 1,145
Threads: 114
Joined: Sep 2019
Oct-23-2024, 08:12 PM
(This post was last modified: Oct-23-2024, 08:12 PM by menator01.)
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()
Posts: 6,794
Threads: 20
Joined: Feb 2020
Oct-23-2024, 09:19 PM
(This post was last modified: Oct-24-2024, 03:20 AM by deanhystad.)
@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()
Posts: 1,145
Threads: 114
Joined: Sep 2019
Oct-23-2024, 10:11 PM
(This post was last modified: Oct-23-2024, 10:11 PM by menator01.)
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.
Posts: 6,794
Threads: 20
Joined: Feb 2020
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.
Posts: 9
Threads: 3
Joined: Oct 2024
(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.
Posts: 6,794
Threads: 20
Joined: Feb 2020
"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.
Posts: 9
Threads: 3
Joined: Oct 2024
(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.
|