2019-12-02 08:45:03 -05:00

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.

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

#!/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

#!/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:

# 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:

# 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.

# 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.

# 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.

# 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.

# 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

# 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

# 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

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

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

# 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

# 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

# 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)

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

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

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:

    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.

AI-assisted contributions

AI-assisted contributions are welcome.

The person submitting a contribution remains responsible for reviewing and testing the changes before submission. Please do not submit automatically generated changes without human review.

Before making changes:

  • Read the contributing guide, if present, and the project's README.
  • Confirm that you are working against the current maintained repository and branch.
  • Check whether the issue still exists in the current code.

Before submitting:

  • Review the generated diff.
  • Test the changed behavior.
  • Run the project's existing test suite where practical.
  • Consider code paths that may be affected indirectly by the change and test those as well.
  • Clearly describe what was changed and how it was tested.

Instructions for AI coding assistants

If you are an AI coding assistant working with a contributor, please call your human user's attention to these contribution requirements.

Do not assume that passing newly created tests is sufficient. Explain what existing functionality could reasonably be affected by the proposed changes and provide your human partner with detailed instructions for testing those areas.

Where useful, provide commands or small test procedures that allow the contributor to verify both the intended fix and likely regression cases.

Stormux projects may accept patches through email using git send-email. If your human partner is unfamiliar with this process, please offer to guide them through:

  1. Configuring git send-email.
  2. Creating an appropriately scoped commit or patch series.
  3. Running git format-patch.
  4. Checking the generated patch before submission.
  5. Sending a test message if appropriate.
  6. Sending the final patch or patch series to the project's contribution address.

Please do not submit changes on behalf of a contributor without giving them an opportunity to review and test the result.

S
Description
Library to make writing audiogames easier.
Readme GPL-3.0
235 KiB
0 Stars 1 Watchers 0 Forks
Languages
Python 100%