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-speechdoraccessible-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
-
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
-
Always clean up resources:
- Use
exit_game()orgame.exit()when exiting to ensure proper cleanup - Stop sounds that are no longer needed
- Use
-
Volume control:
- Implement the Alt+key volume controls in your game
- Use volume services for better control
-
Configuration:
- Save user preferences using the Config class
- Load settings at startup
-
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-speechdoraccessible-output2 - Windows/macOS:
accessible-output2
- Linux/Unix:
- 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:
- Configuring
git send-email. - Creating an appropriately scoped commit or patch series.
- Running
git format-patch. - Checking the generated patch before submission.
- Sending a test message if appropriate.
- 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.