# 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

```bash
git clone https://git.stormux.org/storm/libstormgames
cd libstormgames
pip install -e .
```

## Getting Started

You can use libstormgames in two ways: the traditional function-based approach or the new class-based approach.

### Traditional Function-Based Approach

```python
#!/usr/bin/env python3
import libstormgames as sg

def main():
    # Initialize the game
    sounds = sg.initialize_gui("My First Audio Game")
    
    # Welcome message
    sg.speak("Welcome to My First Audio Game!")
    
    # Create a scoreboard
    scoreboard = sg.Scoreboard()
    
    # Main game loop
    def play_game():
        sg.speak("Game started!")
        scoreboard.increase_score(10)
        sg.speak(f"Your score is {scoreboard.get_score()}")
        scoreboard.add_high_score()
        return "menu"  # Return to menu after game ends
    
    # Define menu options
    while True:
        choice = sg.game_menu(sounds, "play_game", "instructions", "credits", "donate", "exit_game")
        if choice == "play_game":
            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 (using fluent API)
    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(), "play_game", "instructions", "credits", "donate", "exit_game")
        if choice == "play_game":
            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.ogg")

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

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

### 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
speech.close()
```

### 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']}")
```

## 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
# Display a menu with options (functions should exist with these names)
choice = sg.game_menu(sounds, "play_game", "high_scores", "instructions", "credits", "exit_game")

# Display built-in instructions
sg.instructions()

# Display built-in credits
sg.credits()

# Open donation page
sg.donate()

# Interactive menu to learn available sounds
sg.learn_sounds(sounds)
```

### 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, "play_game", "settings", 
                                "instructions", "credits", "donate", "exit_game")
            
            if choice == "play_game":
                self.play_game()
            elif 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. **Speech feedback**:
   - Provide clear speech feedback for all actions
   - Use the `interrupt` parameter to control speech priority

5. **Sound positioning**:
   - Use positional audio to create an immersive environment
   - Update object positions as the game state changes

6. **Configuration**:
   - Save user preferences using the Config class
   - Load settings at startup

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

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