# 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 - 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) if choice == "play": play_game() 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.get_sounds()) if choice == "play": play_game() 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 - **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. ### Sound Manages sound loading and playback with positional audio support. ```python # Create a sound manager sound_system = sg.Sound("sounds/") # Get the dictionary of loaded sounds sounds = sound_system.get_sounds() # Play a sound sg.play_sound(sounds["explosion"]) # Play a sound with positional audio (player at x=5, object at x=10) channel = sg.obj_play(sounds, "footsteps", 5, 10) # Update sound position as player or object moves channel = sg.obj_update(channel, 6, 10) # Player moved to x=6 # Stop the sound channel = sg.obj_stop(channel) # Play background music sg.play_bgm("sounds/background.ogg") # 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 ``` ### Speech Provides text-to-speech functionality using available speech providers. ```python # Create a speech manager (usually you'll use the global instance) speech = sg.Speech() # Speak text speech.speak("Hello, world!") # Or use the global function for convenience sg.speak("Hello, world!") # Speak without interrupting previous speech sg.speak("This won't interrupt", interrupt=False) # Clean up when done exit_game() or as the class, game.exit_game() ``` ### Scoreboard Tracks scores and manages high score tables. ```python # Create a scoreboard scoreboard = sg.Scoreboard() # Manipulate score scoreboard.increase_score(10) scoreboard.decrease_score(5) current_score = scoreboard.get_score() # Check for high score position = scoreboard.check_high_score() if position: print(f"You're in position {position}!") # Add high score (prompts for player name) scoreboard.add_high_score() # Get all high scores high_scores = scoreboard.get_high_scores() for entry in high_scores: print(f"{entry['name']}: {entry['score']}") ``` The Scoreboard system automatically manages high score data in the game's configuration directory: - Linux/Unix: `~/.config/storm-games/game-name/config.ini` - Windows: `%APPDATA%\storm-games\game-name\config.ini` - macOS: `~/Library/Application Support/storm-games/game-name/config.ini` Where `game-name` is the lowercase, hyphenated version of your game title. For example, "My Awesome Game" would use the directory `my-awesome-game`. ## Key Functions ### Game Initialization and Control ```python # Initialize game systems (traditional approach) sounds = sg.initialize_gui("Game Title") # Or use the Game class (modern approach) game = sg.Game("Game Title").initialize() # Pause the game (freezes until user presses backspace) sg.pause_game() # Check if user wants to exit if sg.check_for_exit(): sg.exit_game() # Exit the game properly sg.exit_game() ``` ### Menu System ```python # Basic menu example (returns option string) choice = sg.game_menu(sounds) if choice == "play": # Start the game play_game() elif choice == "instructions": sg.instructions() # etc... # Menu with play callback (callback is executed directly) def play_callback(): sg.speak("Game started with callback!") # Game code goes here sg.game_menu(sounds, play_callback) # Menu with custom options choice = sg.game_menu(sounds, None, "practice_mode") if choice == "practice_mode": # Handle practice mode practice_game() elif choice == "difficulty": # Handle difficulty settings set_difficulty() elif choice == "options": # Handle options screen show_options() # etc... ``` ### Complete Menu Example ```python import libstormgames as sg import pygame def main(): # Initialize the game sounds = sg.initialize_gui("Menu Example Game") # Play menu music sg.play_bgm("sounds/music_menu.ogg") # Define play function with callback def play_game(): sg.speak("Starting game!") # Game code here sg.speak("Game over!") return "menu" # Return to menu after game ends # Define custom menu option handlers def practice_mode(): sg.speak("Starting practice mode!") # Practice mode code here return "menu" def difficulty_settings(): # Show difficulty options options = ["easy", "normal", "hard", "back"] current = 0 while True: sg.speak(options[current]) 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 "menu" sg.speak(f"Difficulty set to {options[current]}") return "menu" elif event.key == pygame.K_ESCAPE: return "menu" # Main menu loop while True: # Display menu with play callback and custom options choice = sg.game_menu(sounds, play_game, "practice_mode", "difficulty") # Handle menu choices if choice == "practice_mode": practice_mode() elif choice == "difficulty": difficulty_settings() # Other options are handled automatically by game_menu if __name__ == "__main__": main() ``` ### Class-Based Menu Example ```python import libstormgames as sg import pygame class MyGame: def __init__(self): # Create and initialize game self.game = sg.Game("Class-Based Menu Example").initialize() self.sounds = self.game.sound.get_sounds() def play_game(self): self.game.speak("Starting game!") # Game code here self.game.speak("Game over!") return "menu" def practice_mode(self): self.game.speak("Starting practice mode!") # Practice mode code here return "menu" def settings(self): # A nested menu example submenu_options = ["graphics", "audio", "controls", "back"] current = 0 while True: self.game.speak(submenu_options[current]) 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(submenu_options) - 1: current += 1 elif event.key == pygame.K_RETURN: if submenu_options[current] == "back": return "menu" self.game.speak(f"Selected {submenu_options[current]} settings") return "menu" elif event.key == pygame.K_ESCAPE: return "menu" def run(self): # Main menu loop while True: # Use playCallback parameter to directly call play_game when "play" is selected choice = sg.game_menu(self.sounds, None, "practice_mode", "settings") # Handle other menu options if choice == "practice_mode": self.practice_mode() elif choice == "settings": self.settings() # Run the game if __name__ == "__main__": game = MyGame() game.run() ``` ### Text Display ```python # Display text with navigation sg.display_text([ "Line 1 of instructions", "Line 2 of instructions", "Line 3 of instructions" ]) # Display a simple message box sg.messagebox("Game Over! You scored 100 points.") # Get text input from user name = sg.get_input("Enter your name:", "Player") ``` ### Sound Effects ```python # Play a random variation of a sound sg.play_random(sounds, "explosion") # Play positional sound with distance-based volume sg.play_random_positional(sounds, "footsteps", player_x=5, object_x=10) # Play directional sound (simplified left/right positioning) sg.play_directional_sound(sounds, "voice", player_x=5, object_x=10) # Play a sound as a cutscene (interrupts other sounds, waits until complete) sg.cut_scene(sounds, "intro_speech") # Play or update a falling sound channel = sg.play_random_falling(sounds, "rock", player_x=5, object_x=8, start_y=10, currentY=5) ``` ### Utility Functions ```python # Check for game updates update_info = sg.check_for_updates("1.0.0", "My Game", "https://example.com/version.json") # Check compatibility with library version is_compatible = sg.check_compatibility("1.0.0", "1.2.3") # Sanitize a filename for any OS safe_name = sg.sanitize_filename("User's File.txt") # Calculate distance between points distance = sg.distance_2d(x1=5, y1=10, x2=8, y2=15) # Interpolation functions mid_value = sg.lerp(start=0, end=10, factor=0.5) # Returns 5.0 smooth_value = sg.smooth_step(edge0=0, edge1=10, x=5) # Smooth transition ``` ## Advanced Examples ### Using the Game Class (Modern Approach) ```python import libstormgames as sg import pygame import random def main(): # Create and initialize the game game = sg.Game("My Advanced Game").initialize() # Set up game environment game.play_bgm("sounds/background.ogg") # Main game loop running = True player_x = 5 while running: # Process events for event in pygame.event.get(): if event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False elif event.key == pygame.K_SPACE: # Score points game.scoreboard.increase_score(5) game.speak(f"Score: {game.scoreboard.get_score()}") # Update game state player_x += random.uniform(-0.2, 0.2) # Add random ambient sounds if random.random() < 0.05: sounds = game.sound.get_sounds() if "ambient" in sounds: sg.play_random_positional(sounds, "ambient", player_x, player_x + random.uniform(-5, 5)) pygame.time.delay(50) # Game over and cleanup game.speak("Game over!") game.exit() if __name__ == "__main__": main() ``` ### Complex Sound Environment ```python import libstormgames as sg import time import random def create_sound_environment(player_x, player_y): sounds = sg.initialize_gui("Sound Environment Demo") # Place sound sources water_x, water_y = 10, 5 fire_x, fire_y = 15, 8 wind_x, wind_y = 3, 12 # Play ambient sounds water_channel = sg.obj_play(sounds, "water", player_x, water_x) fire_channel = sg.obj_play(sounds, "fire", player_x, fire_x) wind_channel = sg.obj_play(sounds, "wind", player_x, wind_x) # Main loop running = True while running: # Simulate player movement player_x += random.uniform(-0.5, 0.5) player_y += random.uniform(-0.5, 0.5) # Update sound positions water_channel = sg.obj_update(water_channel, player_x, water_x) fire_channel = sg.obj_update(fire_channel, player_x, fire_x) wind_channel = sg.obj_update(wind_channel, player_x, wind_x) # Occasionally play random sound if random.random() < 0.1: sg.play_random_positional(sounds, "creature", player_x, player_x + random.uniform(-5, 5)) # Check for exit if sg.check_for_exit(): running = False time.sleep(0.1) # Clean up sg.obj_stop(water_channel) sg.obj_stop(fire_channel) sg.obj_stop(wind_channel) sg.exit_game() ``` ### 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 = 5 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 while running: # Update game state self.player_x += random.uniform(-0.2, 0.2) # 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_SPACE: self.game.scoreboard.increase_score() self.game.speak(f"Score: {self.game.scoreboard.get_score()}") # Add some random sounds if random.random() < 0.05: sounds = self.game.sound.get_sounds() if "ambient" in sounds: sg.play_random_positional(sounds, "ambient", self.player_x, self.player_x + random.uniform(-10, 10)) pygame.time.delay(50) # 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: sounds = self.game.sound.get_sounds() choice = sg.game_menu(sounds, self.play_game, "settings", "instructions", "credits", "donate", "exit_game") if choice == "settings": self.settings() elif choice == "instructions": sg.instructions() elif choice == "credits": sg.credits() elif choice == "donate": sg.donate() elif choice == "exit_game": self.game.exit() # Run the game if __name__ == "__main__": game = MyGame() game.run() ``` ## Best Practices 1. **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 2. **Always clean up resources**: - Use `exit_game()` or `game.exit()` when exiting to ensure proper cleanup - Stop sounds that are no longer needed 3. **Volume control**: - Implement the Alt+key volume controls in your game - Use volume services for better control 4. **Configuration**: - Save user preferences using the Config class - Load settings at startup 5. **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 ### 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.