Huge refactor of the libstormgames library. It is hopefully mostly backwards compatible. Still lots of testing to do, and probably some fixes needed, but this is a good start.
This commit is contained in:
629
README.md
629
README.md
@ -1,8 +1,627 @@
|
||||
# libstormgames
|
||||
|
||||
Library to make writing audiogames easier.
|
||||
A Python library to make creating audio games easier.
|
||||
|
||||
## Requirements
|
||||
configparser pyxdg pygame pyperclip requests setproctitle
|
||||
### For mac os X and windows
|
||||
accessible_output2
|
||||
## 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.
|
||||
|
Reference in New Issue
Block a user