# libstormgames A Python library to make creating audio games easier. ## Overview `libstormgames` provides a comprehensive set of tools for developing accessible games with audio-first design. It handles common game development tasks including: - Sound and music playback with positional audio and 3D spatialization - Text-to-speech integration - Configuration management - Score tracking and high score tables - Input handling and keyboard controls - Menu systems and text display - GUI initialization ## Installation ### Requirements - pygame>=2.0.0 - pyperclip>=1.8.0 - requests>=2.25.0 - pyxdg>=0.27 - setproctitle>=1.2.0 - numpy>=1.19.0 - wxpython #### Speech Providers (one required) - Linux/Unix: `python-speechd` or `accessible-output2>=0.14` - Windows/macOS: `accessible-output2>=0.14` ### Install from source If you are on Linux, check your package manager first to see if the packages in requirements.txt are available. ```bash git clone https://git.stormux.org/storm/libstormgames cd libstormgames pip install -e . ``` ## Getting Started You can use libstormgames in two ways: the traditional function-based approach or the new class-based approach. ### Traditional Function-Based Approach ```python #!/usr/bin/env python3 import libstormgames as sg def main(): # Initialize the game sounds = sg.initialize_gui("My First Audio Game") # Welcome message sg.speak("Welcome to My First Audio Game!") # Create a scoreboard scoreboard = sg.Scoreboard() # Main game loop def play_game(): sg.speak("Game started!") scoreboard.increase_score(10) sg.speak(f"Your score is {scoreboard.get_score()}") scoreboard.add_high_score() return "menu" # Return to menu after game ends # Define menu options while True: choice = sg.game_menu(sounds, play_game) # The game_menu function already includes standard options like # high scores, instructions, credits, and exit if __name__ == "__main__": main() ``` ### Modern Class-Based Approach ```python #!/usr/bin/env python3 import libstormgames as sg def main(): # Create and initialize a game game = sg.Game("My First Audio Game").initialize() # Welcome message game.speak("Welcome to My First Audio Game!") # Main game loop def play_game(): game.speak("Game started!") game.scoreboard.increase_score(10) game.speak(f"Your score is {game.scoreboard.get_score()}") game.scoreboard.add_high_score() return "menu" # Define menu options while True: choice = sg.game_menu(game.sound.sounds, play_game) # The game_menu function already includes standard options like # high scores, instructions, credits, and exit if __name__ == "__main__": main() ``` ## Library Structure The library is organized into modules, each with a specific focus: - **config**: Configuration management - **services**: Core services that replace global variables - **sound**: Sound and music playback with 3D positional audio - **speech**: Text-to-speech functionality - **scoreboard**: High score tracking - **input**: Input handling and dialogs - **display**: Text display and GUI functionality - **menu**: Menu systems - **utils**: Utility functions and Game class ## Core Classes ### Game The Game class provides a central way to manage all game systems: ```python # Create and initialize a game game = sg.Game("My Game").initialize() # Use fluent API for chaining commands game.speak("Hello").play_bgm("music/theme") # Access components directly game.scoreboard.increase_score(10) game.sound.play_random("explosion") # Display text game.display_text(["Line 1", "Line 2"]) # Clean exit game.exit() ``` The initialization process sets up proper configuration paths: - Creates game-specific directories if they don't exist - Ensures all services have access to correct paths - Connects all components for seamless operation ### Services The library includes several service classes that replace global variables: ```python # Volume service manages all volume settings volume = sg.VolumeService.get_instance() volume.adjust_master_volume(0.1) # Path service manages file paths paths = sg.PathService.get_instance() print(paths.game_path) # Config service manages configuration config = sg.ConfigService.get_instance() ``` ### Config Handles configuration file management with local and global settings. ```python # Create a configuration manager config = sg.Config("My Game") # Access the local configuration config.local_config.add_section("settings") config.local_config.set("settings", "difficulty", "easy") config.write_local_config() # Read settings config.read_local_config() difficulty = config.local_config.get("settings", "difficulty") ``` Configuration files are automatically stored in standard system locations: - Linux/Unix: `~/.config/storm-games/` - Windows: `%APPDATA%\storm-games\` - macOS: `~/Library/Application Support/storm-games/` Each game has its own subdirectory based on the game name (lowercase with hyphens), for example, Wicked Quest becomes wicked-quest. # Create a sound manager sound_system = sg.Sound("sounds/") # Get the dictionary of loaded sounds sounds = sound_system.sounds # Play a sound channel = sound_system.play_sound("explosion") # Play a looping sound channel = sound_system.play_sound("ambient", loop=True) # Play a sound with 3D positional audio (horizontal and vertical positioning) channel = sound_system.obj_play("footsteps", playerPos=5, objPos=10, playerY=0, objY=5, loop=True) # Update sound position as player or object moves channel = sound_system.obj_update(channel, 6, 10, 0, 5) # Player moved to x=6 # Stop the sound channel = sound_system.obj_stop(channel) # Update all active looping sounds with a single call updated_count = sound_system.update_all_active_loops(player_x=6, player_y=0) # Global function equivalents channel = sg.obj_play(sounds, "footsteps", 5, 10, 0, 5, loop=True) channel = sg.obj_update(channel, 6, 10, 0, 5) channel = sg.obj_stop(channel) # Play background music sg.play_bgm("sounds/background.ogg", loop=True) # Adjust volume sg.adjust_master_volume(0.1) # Increase master volume sg.adjust_bgm_volume(-0.1) # Decrease background music volume sg.adjust_sfx_volume(0.1) # Increase sound effects volume # Create a sound manager sound_system = sg.Sound("sounds/") # Get the dictionary of loaded sounds sounds = sound_system.sounds # Play a sound channel = sound_system.play_sound("explosion") # Play a looping sound channel = sound_system.play_sound("ambient", loop=True) # Play a sound with 3D positional audio (horizontal and vertical positioning) channel = sound_system.obj_play("footsteps", playerPos=5, objPos=10, playerY=0, objY=5, loop=True) # Update sound position as player or object moves channel = sound_system.obj_update(channel, 6, 10, 0, 5) # Player moved to x=6 # Stop the sound channel = sound_system.obj_stop(channel) # Update all active looping sounds with a single call updated_count = sound_system.update_all_active_loops(player_x=6, player_y=0) # Global function equivalents channel = sg.obj_play(sounds, "footsteps", 5, 10, 0, 5, loop=True) channel = sg.obj_update(channel, 6, 10, 0, 5) channel = sg.obj_stop(channel) # Play background music (now with loop parameter) sg.play_bgm("sounds/background.ogg", loop=True) # Adjust volume sg.adjust_master_volume(0.1) # Increase master volume sg.adjust_bgm_volume(-0.1) # Decrease background music volume sg.adjust_sfx_volume(0.1) # Increase sound effects volume ### Complete Game Structure with Class-Based Architecture ```python import libstormgames as sg import pygame import random class MyGame: def __init__(self): # Create a Game instance that manages all subsystems self.game = sg.Game("My Advanced Game").initialize() # Game state self.player_x = 5 self.player_y = 0 self.difficulty = "normal" # Load settings try: self.difficulty = self.game.config_service.local_config.get("settings", "difficulty") except: self.game.config_service.local_config.add_section("settings") self.game.config_service.local_config.set("settings", "difficulty", "normal") self.game.config_service.write_local_config() def play_game(self): self.game.speak(f"Starting game on {self.difficulty} difficulty") self.game.play_bgm("sounds/game_music.ogg") # Game loop running = True # Create enemies with positional audio enemies = [] for i in range(3): enemy = { 'x': random.uniform(10, 30), 'y': random.uniform(-5, 5), 'channel': None } # Start looping sound for each enemy enemy['channel'] = self.game.sound.play_random_positional( "enemy", self.player_x, enemy['x'], self.player_y, enemy['y'], loop=True ) enemies.append(enemy) while running: # Update game state for enemy in enemies: # Move enemies toward player if enemy['x'] > self.player_x: enemy['x'] -= 0.1 else: enemy['x'] += 0.1 # Handle input for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False elif event.key == pygame.K_LEFT: self.player_x -= 1 elif event.key == pygame.K_RIGHT: self.player_x += 1 elif event.key == pygame.K_SPACE: self.game.scoreboard.increase_score() self.game.speak(f"Score: {self.game.scoreboard.get_score()}") # Update all sound positions with one call self.game.sound.update_all_active_loops(self.player_x, self.player_y) pygame.time.delay(50) # Stop all enemy sounds for enemy in enemies: if enemy['channel']: self.game.sound.obj_stop(enemy['channel']) # Game over position = self.game.scoreboard.check_high_score() if position: self.game.speak(f"New high score! Position {position}") self.game.scoreboard.add_high_score() return "menu" def settings(self): options = ["easy", "normal", "hard", "back"] current = options.index(self.difficulty) if self.difficulty in options else 1 while True: self.game.speak(f"Current difficulty: {options[current]}") # Wait for input event = pygame.event.wait() if event.type == pygame.KEYDOWN: if event.key == pygame.K_UP and current > 0: current -= 1 elif event.key == pygame.K_DOWN and current < len(options) - 1: current += 1 elif event.key == pygame.K_RETURN: if options[current] == "back": return self.difficulty = options[current] self.game.config_service.local_config.set("settings", "difficulty", self.difficulty) self.game.config_service.write_local_config() self.game.speak(f"Difficulty set to {self.difficulty}") return elif event.key == pygame.K_ESCAPE: return def run(self): # Main menu loop while True: # Use playCallback for play option and add custom "settings" option # Note: standard options like instructions, high_scores, etc. are handled automatically choice = sg.game_menu(self.game.sound.sounds, self.play_game, "settings") if choice == "settings": self.settings() # Run the game if __name__ == "__main__": game = MyGame() game.run() ``` ## Best Practices 1. **Sound Looping and Positioning**: - Use the `loop=True` parameter for sounds that need to loop - For moving sound sources, use `update_all_active_loops()` to efficiently update all sounds at once - Include vertical positioning (Y-axis) if you need 3D audio 2. **Modern vs Traditional Approach**: - New projects: Use the Game class for better organization - Existing projects: Continue with global functions for compatibility - Both approaches are fully supported 3. **Always clean up resources**: - Use `exit_game()` or `game.exit()` when exiting to ensure proper cleanup - Stop sounds that are no longer needed with `obj_stop()` 4. **Volume control**: - Implement the Alt+key volume controls in your game - Use volume services for better control 5. **Configuration**: - Save user preferences using the Config class - Load settings at startup 6. **Path initialization**: - Always initialize the framework with a proper game title - Game title is used to determine configuration directory paths - Services are interconnected, so proper initialization ensures correct operation ## Troubleshooting ### No Sound - Ensure pygame mixer is properly initialized - Check if sound files exist in the correct directory - Verify file formats (OGG and WAV are supported) ### No Speech - Make sure at least one speech provider is installed - Linux/Unix: `python-speechd` or `accessible-output2` - Windows/macOS: `accessible-output2` - Check if pygame display is initialized properly ### Input Issues - Ensure pygame is properly handling events - Check event loop for proper event handling - Remember to use pygame.event.pump() especially in the game loop ### Scoreboard/Configuration Issues - Check if path services are properly initialized with a game title - Verify write permissions in the configuration directory - For debugging, use the following code to view path configuration: ```python from libstormgames.services import PathService, ConfigService # Print path information path_service = PathService.get_instance() print(f"Game Name: {path_service.gameName}") print(f"Game Path: {path_service.gamePath}") # Check config service connection config_service = ConfigService.get_instance() print(f"Config connected to path: {hasattr(config_service, 'pathService')}") ``` ## Contributing Contributions are welcome! Please feel free to submit a Pull Request. ## License This project is licensed under the GPL v3 License - see the LICENSE file for details.