2019-12-02 08:45:03 -05:00
2025-09-15 20:54:45 -04: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
  • NEW: Statistics tracking with level/total separation
  • NEW: Save/load management with atomic operations
  • NEW: Combat systems with weapons and projectiles

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
  • stat_tracker: Statistics tracking system
  • save_manager: Save/load management
  • combat: Weapon and projectile systems

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.

StatTracker

Flexible statistics tracking with separate level and total counters:

# Initialize with default stats
stats = sg.StatTracker({
    "kills": 0, 
    "deaths": 0, 
    "score": 0, 
    "time_played": 0.0
})

# Update stats during gameplay
stats.update_stat("kills", 1)        # Increment kills
stats.update_stat("time_played", 1.5) # Add playtime

# Access current values
level_kills = stats.get_stat("kills")              # Current level kills
total_kills = stats.get_stat("kills", from_total=True) # All-time kills

# Reset level stats for new level (keeps totals)
stats.reset_level()

# Add new stats dynamically  
stats.add_stat("boss_kills", 0)

# Serialization for save/load
stats_data = stats.to_dict()
restored_stats = sg.StatTracker.from_dict(stats_data)

SaveManager

Atomic save/load operations with corruption detection:

# Initialize save manager
save_manager = sg.SaveManager("my-rpg-game")

# Create a comprehensive save
game_state = {
    "player": {
        "level": 5, 
        "hp": 80, 
        "inventory": ["sword", "health_potion"]
    },
    "world": {
        "current_area": "enchanted_forest",
        "completed_quests": ["tutorial", "first_boss"]
    },
    "stats": stats.to_dict()  # Include StatTracker data
}

# Add metadata for save selection screen
metadata = {
    "display_name": "Enchanted Forest - Level 5",
    "level": 5,
    "playtime": "2.5 hours"
}

# Create the save (atomic operation)
save_path = save_manager.create_save(game_state, metadata)

# List and load saves
save_files = save_manager.get_save_files()  # Newest first
if save_files:
    loaded_data, loaded_metadata = save_manager.load_save(save_files[0])
    restored_stats = sg.StatTracker.from_dict(loaded_data["stats"])

Combat Systems

Weapons

# Create weapons using factory methods
sword = sg.Weapon.create_sword("Iron Sword", damage=15)
dagger = sg.Weapon.create_dagger("Steel Dagger", damage=12)

# Custom weapon with stat bonuses
bow = sg.Weapon(
    name="Elvish Bow", damage=18, range_value=8,
    cooldown=600, stat_bonuses={"speed": 1.15}
)

# Combat usage
if sword.can_attack():  # Check cooldown
    damage = sword.attack()  # Start attack
    
    if sword.can_hit_target(enemy_pos, player_pos, facing_right, "enemy1"):
        actual_damage = sword.hit_target("enemy1")
        sg.speak(f"Hit for {actual_damage} damage!")

Projectiles

# Create projectiles
arrow = sg.Projectile.create_arrow(
    start_pos=(player_x, player_y), 
    direction=(1, 0)  # Moving right
)

# Game loop integration
active_projectiles = []

def update_projectiles():
    for projectile in active_projectiles[:]:
        if not projectile.update():  # Move and check range
            active_projectiles.remove(projectile)
            continue
        
        # Check enemy collisions
        for enemy in enemies:
            if projectile.check_collision(enemy.position, enemy.size):
                damage = projectile.hit()
                enemy.take_damage(damage)
                active_projectiles.remove(projectile)
                break

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 New Systems

import libstormgames as sg
import pygame
import random

class ModernRPGGame:
    def __init__(self):
        # Create a Game instance that manages all subsystems
        self.game = sg.Game("Modern RPG Demo").initialize()
        
        # Initialize new game systems
        self.player_stats = sg.StatTracker({
            "level": 1, "exp": 0, "hp": 100, "mp": 50,
            "kills": 0, "deaths": 0, "playtime": 0.0,
            "items_found": 0, "gold": 0
        })
        self.save_manager = sg.SaveManager("modern-rpg-demo")
        
        # Combat system
        self.player_weapon = sg.Weapon.create_sword("Starting Sword", damage=10)
        self.projectiles = []
        self.enemies = []
        
        # Game state
        self.player_x = 5
        self.player_y = 5
        self.current_area = "village"
        self.difficulty = "normal"
        
        # Load settings
        try:
            self.difficulty = self.game.configService.localConfig.get("settings", "difficulty")
        except:
            self.game.configService.localConfig.add_section("settings")
            self.game.configService.localConfig.set("settings", "difficulty", "normal")
            self.game.configService.write_local_config()
    
    def play_game(self):
        """Main game loop demonstrating new systems."""
        self.game.speak(f"Starting game on {self.difficulty} difficulty")
        self.game.play_bgm("sounds/game_music.ogg")
        
        start_time = pygame.time.get_ticks()
        
        # Game loop
        running = True
        while running:
            current_time = pygame.time.get_ticks()
            
            # Update playtime stats
            playtime_hours = (current_time - start_time) / 3600000.0  # Convert to hours
            self.player_stats.set_stat("playtime", playtime_hours, level_only=True)
            
            # 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:
                        # Combat example
                        self.player_attack()
                    elif event.key == pygame.K_s:
                        # Quick save
                        self.quick_save()
                    elif event.key == pygame.K_l:
                        # Quick load
                        self.quick_load()
            
            # Update game systems
            self.update_combat()
            self.update_player_stats()
            
            # Random events
            if random.random() < 0.01:
                self.random_encounter()
            
            pygame.time.delay(50)
        
        # Game over - update total stats
        total_playtime = self.player_stats.get_stat("playtime", from_total=True)
        self.game.speak(f"Session ended. Total playtime: {total_playtime:.1f} hours")
        
        return "menu"
    
    def player_attack(self):
        """Handle player combat."""
        if self.player_weapon.can_attack():
            damage = self.player_weapon.attack()
            self.game.speak("Attack!")
            
            # Simulate hitting enemies
            if self.enemies and self.player_weapon.is_attack_active():
                enemy = self.enemies[0]  # Attack first enemy
                if self.player_weapon.can_hit_target(
                    enemy.position, (self.player_x, self.player_y), 
                    facing_right=True, target_id=enemy.id
                ):
                    actual_damage = self.player_weapon.hit_target(enemy.id)
                    self.player_stats.update_stat("damage_dealt", actual_damage)
                    
                    # Remove enemy if defeated
                    if enemy.take_damage(actual_damage):
                        self.enemies.remove(enemy)
                        self.player_stats.update_stat("kills", 1)
                        self.player_stats.update_stat("exp", 25)
                        self.game.speak("Enemy defeated!")
    
    def update_combat(self):
        """Update combat systems."""
        # Update projectiles
        for projectile in self.projectiles[:]:
            if not projectile.update():
                self.projectiles.remove(projectile)
                continue
            
            # Check enemy collisions
            for enemy in self.enemies[:]:
                if projectile.check_collision(enemy.position, enemy.size):
                    damage = projectile.hit()
                    if enemy.take_damage(damage):
                        self.enemies.remove(enemy)
                        self.player_stats.update_stat("kills", 1)
                    self.projectiles.remove(projectile)
                    break
    
    def update_player_stats(self):
        """Handle player progression."""
        exp = self.player_stats.get_stat("exp")
        level = self.player_stats.get_stat("level")
        
        # Level up check
        exp_needed = level * 100
        if exp >= exp_needed:
            self.player_stats.set_stat("level", level + 1)
            self.player_stats.set_stat("exp", exp - exp_needed)
            self.player_stats.set_stat("hp", 100)  # Full heal on level up
            self.game.speak(f"Level up! Now level {level + 1}!")
    
    def random_encounter(self):
        """Create random encounters."""
        self.game.speak("An enemy appears!")
        # Add enemy logic here
        self.player_stats.update_stat("encounters", 1)
    
    def quick_save(self):
        """Create a quick save."""
        try:
            save_name = f"Quick Save - Level {self.player_stats.get_stat('level')}"
            self.create_complete_save(save_name)
            self.game.speak("Game saved!")
        except Exception as e:
            self.game.speak(f"Save failed: {e}")
    
    def quick_load(self):
        """Load the most recent save."""
        try:
            saves = self.save_manager.get_save_files()
            if saves:
                self.load_complete_save(saves[0])
                self.game.speak("Game loaded!")
            else:
                self.game.speak("No saves found!")
        except Exception as e:
            self.game.speak(f"Load failed: {e}")
    
    def create_complete_save(self, save_name):
        """Create comprehensive save with all systems."""
        complete_state = {
            "player_stats": self.player_stats.to_dict(),
            "weapon": self.player_weapon.to_dict(),
            "player_position": (self.player_x, self.player_y),
            "current_area": self.current_area,
            "difficulty": self.difficulty,
            "enemies": [enemy.to_dict() for enemy in self.enemies],
            "projectiles": [proj.to_dict() for proj in self.projectiles]
        }
        
        metadata = {
            "display_name": save_name,
            "level": self.player_stats.get_stat("level"),
            "location": self.current_area,
            "playtime": f"{self.player_stats.get_stat('playtime', from_total=True):.1f}h"
        }
        
        return self.save_manager.create_save(complete_state, metadata)
    
    def load_complete_save(self, save_path):
        """Load comprehensive save restoring all systems."""
        data, metadata = self.save_manager.load_save(save_path)
        
        # Restore all systems
        self.player_stats = sg.StatTracker.from_dict(data["player_stats"])
        self.player_weapon = sg.Weapon.from_dict(data["weapon"])
        self.player_x, self.player_y = data["player_position"]
        self.current_area = data["current_area"]
        self.difficulty = data["difficulty"]
        
        # Restore dynamic objects (implementation depends on your enemy/projectile classes)
        # self.enemies = [Enemy.from_dict(e) for e in data["enemies"]]
        # self.projectiles = [sg.Projectile.from_dict(p) for p in data["projectiles"]]
    
    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
  6. New game systems:

    • Use StatTracker for comprehensive statistics with level/total separation
    • Implement SaveManager for reliable save/load with metadata
    • Leverage Combat systems for professional weapon and projectile mechanics
    • Combine all systems for rich, full-featured games
  7. Performance considerations:

    • Reset level stats regularly to prevent memory bloat
    • Clean up old saves periodically using SaveManager methods
    • Remove inactive projectiles from update loops
    • Use weapon cooldowns to prevent spam attacks

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.

Description
Library to make writing audiogames easier.
Readme GPL-3.0 298 KiB
Languages
Python 100%