From 4d59a610a06be8d2d7fcccbaa878853937c3c8a2 Mon Sep 17 00:00:00 2001 From: Storm Dragon Date: Mon, 24 Feb 2025 01:59:47 -0500 Subject: [PATCH] Optional timeouts for wait methods. Various bug fixes. --- __init__.py | 39 +++++++++++++++++++++++++++------------ display.py | 15 +++++++++++++-- scoreboard.py | 19 +++++++++++++------ sound.py | 21 ++++++++++++++------- 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/__init__.py b/__init__.py index b6733f8..49b680e 100644 --- a/__init__.py +++ b/__init__.py @@ -89,46 +89,55 @@ class pygstormgames: self.speech.cleanup() pyglet.app.exit() - def wait(self, validKeys=None): - """Wait for key press(es). + def wait(self, validKeys=None, timeout=None): + """Wait for key press(es) with optional timeout. Args: validKeys (list, optional): List of pyglet.window.key values to wait for. If None, accepts any key press. + timeout (float, optional): Time in seconds to wait for input. + If None, waits indefinitely. Returns: - tuple: (key, modifiers) that were pressed + tuple: (key, modifiers) that were pressed. Returns (None, None) on timeout. """ keyResult = [None, None] # Use list to allow modification in closure - + start_time = time.time() + def on_key_press(symbol, modifiers): if validKeys is None or symbol in validKeys: keyResult[0] = symbol keyResult[1] = modifiers return pyglet.event.EVENT_HANDLED - + # Register temporary handler self.display.window.push_handlers(on_key_press=on_key_press) - # Wait for valid key press + # Wait for valid key press or timeout while keyResult[0] is None: self.display.window.dispatch_events() pyglet.clock.tick() - + + # Check for timeout + if timeout is not None and time.time() - start_time > timeout: + break + # Clean up self.display.window.remove_handlers() return tuple(keyResult) - def wait_for_completion(self, condition, validKeys=None): - """Wait for either a condition to be met or valid keys to be pressed. + def wait_for_completion(self, condition, validKeys=None, timeout=None): + """Wait for either a condition to be met, valid keys to be pressed, or timeout. Args: condition: Function that returns True when waiting should end validKeys (list, optional): List of pyglet.window.key values that can interrupt If None, uses ESCAPE, RETURN, and SPACE + timeout (float, optional): Time in seconds to wait before timing out + If None, waits indefinitely Returns: - bool: True if interrupted by key press, False if condition was met + bool: True if interrupted by key press or timeout, False if condition was met """ if validKeys is None: validKeys = [pyglet.window.key.ESCAPE, @@ -136,18 +145,24 @@ class pygstormgames: pyglet.window.key.SPACE] interrupted = [False] # Use list to allow modification in closure + start_time = time.time() def on_key_press(symbol, modifiers): if symbol in validKeys: interrupted[0] = True return pyglet.event.EVENT_HANDLED - + self.display.window.push_handlers(on_key_press=on_key_press) while not condition() and not interrupted[0]: self.display.window.dispatch_events() pyglet.clock.tick() - + + # Check for timeout + if timeout is not None and time.time() - start_time > timeout: + interrupted[0] = True + break + self.display.window.remove_handlers() return interrupted[0] diff --git a/display.py b/display.py index 2692b8a..d332376 100644 --- a/display.py +++ b/display.py @@ -37,6 +37,8 @@ class Display: text (list): List of text lines to display speech (Speech): Speech system for audio output """ + # Stop bgm if present. + self.game.sound.pause_bgm() # Store original text with blank lines for copying self.originalText = text.copy() @@ -152,22 +154,31 @@ class Display: def get_input(self, prompt="Enter text:", defaultText=""): """Display a dialog box for text input. - + Args: prompt (str): Prompt text to display defaultText (str): Initial text in input box - + Returns: str: User input text, or None if cancelled """ app = wx.App(False) dialog = wx.TextEntryDialog(None, prompt, "Input", defaultText) dialog.SetValue(defaultText) + + # Bring dialog to front and give it focus + dialog.Raise() + dialog.SetFocus() + if dialog.ShowModal() == wx.ID_OK: userInput = dialog.GetValue() else: userInput = None dialog.Destroy() + + # Return focus to game window + self.window.activate() + return userInput def donate(self, speech): diff --git a/scoreboard.py b/scoreboard.py index 1c31189..2cbc608 100644 --- a/scoreboard.py +++ b/scoreboard.py @@ -3,6 +3,7 @@ Handles high score tracking with player names and score management. """ +import pyglet import time class Scoreboard: @@ -93,34 +94,40 @@ class Scoreboard: def add_high_score(self): """Add current score to high scores if it qualifies. - + Returns: bool: True if score was added, False if not """ position = self.check_high_score() if position is None: return False - + # Get player name self.game.speech.speak("New high score! Enter your name:") name = self.game.display.get_input("New high score! Enter your name:", "Player") if name is None: # User cancelled name = "Player" - + # Insert new score at correct position self.highScores.insert(position - 1, { 'name': name, 'score': self.currentScore }) - + # Keep only top 10 self.highScores = self.highScores[:10] - + # Save to config for i, entry in enumerate(self.highScores): self.game.config.set_value("scoreboard", f"score_{i+1}", str(entry['score'])) self.game.config.set_value("scoreboard", f"name_{i+1}", entry['name']) - + + # Force window refresh after dialog + self.game.display.window.dispatch_events() + self.game.speech.speak(f"Congratulations {name}! You got position {position} on the scoreboard!") time.sleep(1) + + # Make sure window is still responsive + self.game.display.window.dispatch_events() return True diff --git a/sound.py b/sound.py index b85b196..36dfcfe 100644 --- a/sound.py +++ b/sound.py @@ -236,23 +236,30 @@ class Sound: self.stop_all_sounds() if self.currentBgm: self.currentBgm.pause() - - if soundName not in self.sounds: + + # Find all matching sound variations + matches = [name for name in self.sounds.keys() + if re.match(f"^{soundName}.*", name)] + + if not matches: return - + + # Pick a random variation + selected_sound = random.choice(matches) + # Create and configure the player player = pyglet.media.Player() - player.queue(self.sounds[soundName]) + player.queue(self.sounds[selected_sound]) player.volume = self.sfxVolume * self.masterVolume # Start playback player.play() - + # Make sure to give pyglet enough cycles to start playing startTime = time.time() - duration = self.sounds[soundName].duration + duration = self.sounds[selected_sound].duration pyglet.clock.tick() - + # Wait for completion or skip interrupted = self.game.wait_for_completion( lambda: not player.playing or (time.time() - startTime) >= duration