Compare commits

..

12 Commits

2 changed files with 940 additions and 513 deletions

942
__init__.py Normal file → Executable file
View File

@@ -1,4 +1,942 @@
#!/bin/python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Standard initializations and functions shared by all Storm Games.
from .libstormgames import *
This module provides core functionality for Storm Games including:
- Sound and speech handling
- Volume controls
- Configuration management
- Score tracking
- GUI initialization
- Game menu systems
"""
from sys import exit
import configparser
import os
from os import listdir
from os.path import isfile, join
from inspect import isfunction
from xdg import BaseDirectory
from setproctitle import setproctitle
import pygame
import pyperclip
import random
import re
import requests
import textwrap
import webbrowser
import math
import numpy as np
import time
import wx
# Global variable for speech provider
try:
import speechd
spd = speechd.Client()
speechProvider = "speechd"
except ImportError:
import accessible_output2.outputs.auto
s = accessible_output2.outputs.auto.Auto()
speechProvider = "accessible_output2"
except ImportError:
print("No other speech providers found.")
exit()
# Configuration objects
localConfig = configparser.ConfigParser()
globalConfig = configparser.ConfigParser()
# Volume control globals
bgmVolume = 0.75 # Default background music volume
sfxVolume = 1.0 # Default sound effects volume
masterVolume = 1.0 # Default master volume
class Scoreboard:
"""Handles score tracking and top 10 high scores for games.
This class manages the scoring system including:
- Score tracking
- High score storage
- Score updates and persistence
"""
def __init__(self, starting_score=0):
"""Initialize a new scoreboard with optional starting score.
Args:
starting_score (int): Initial score value (default: 0)
"""
read_config()
try:
localConfig.add_section("scoreboard")
except:
pass
self.score = starting_score
self.old_scores = []
for i in range(1, 11):
try:
self.old_scores.insert(i - 1, localConfig.getint("scoreboard", str(i)))
except:
self.old_scores.insert(i - 1, 0)
for i in range(1, 11):
if self.old_scores[i - 1] is None:
self.old_scores[i - 1] = 0
def __del__(self):
"""Save scores when object is destroyed."""
self.update_scores()
try:
write_config()
except:
pass
def decrease_score(self, points=1):
"""Decrease the current score.
Args:
points (int): Number of points to decrease (default: 1)
"""
self.score -= points
def get_high_score(self, position=1):
"""Get a high score at specified position.
Args:
position (int): Position in high score list (1-10, default: 1)
Returns:
int: Score at specified position
"""
return self.old_scores[position - 1]
def get_score(self):
"""Get current score.
Returns:
int: Current score
"""
return self.score
def increase_score(self, points=1):
"""Increase the current score.
Args:
points (int): Number of points to increase (default: 1)
"""
self.score += points
def new_high_score(self):
"""Check if current score qualifies as a new high score.
Returns:
int: Position of new high score (1-10), or None if not a high score
"""
for i, j in enumerate(self.old_scores):
if self.score > j:
return i + 1
return None
def update_scores(self):
"""Update the high score list with current score if qualified."""
# Update the scores
for i, j in enumerate(self.old_scores):
if self.score > j:
self.old_scores.insert(i, self.score)
break
# Only keep the top 10 scores
self.old_scores = self.old_scores[:10]
# Update the scoreboard section of the games config file
for i, j in enumerate(self.old_scores):
localConfig.set("scoreboard", str(i + 1), str(j))
def write_config(write_global=False):
"""Write configuration to file.
Args:
write_global (bool): If True, write to global config, otherwise local (default: False)
"""
if not write_global:
with open(gamePath + "/config.ini", 'w') as configfile:
localConfig.write(configfile)
else:
with open(globalPath + "/config.ini", 'w') as configfile:
globalConfig.write(configfile)
def read_config(read_global=False):
"""Read configuration from file.
Args:
read_global (bool): If True, read global config, otherwise local (default: False)
"""
if not read_global:
try:
with open(gamePath + "/config.ini", 'r') as configfile:
localConfig.read_file(configfile)
except:
pass
else:
try:
with open(globalPath + "/config.ini", 'r') as configfile:
globalConfig.read_file(configfile)
except:
pass
def initialize_gui(gameTitle):
"""Initialize the game GUI and sound system.
Args:
gameTitle (str): Title of the game
Returns:
dict: Dictionary of loaded sound objects
"""
# Check for, and possibly create, storm-games path
global globalPath
global gamePath
global gameName
globalPath = BaseDirectory.xdg_config_home + "/storm-games"
gamePath = globalPath + "/" + str.lower(str.replace(gameTitle, " ", "-"))
if not os.path.exists(gamePath):
os.makedirs(gamePath)
# Seed the random generator to the clock
random.seed()
# Set game's name
gameName = gameTitle
setproctitle(str.lower(str.replace(gameTitle, " ", "")))
# Initialize pygame
pygame.init()
pygame.display.set_mode((800, 600))
pygame.display.set_caption(gameTitle)
# Set up audio system
pygame.mixer.pre_init(44100, -16, 2, 1024)
pygame.mixer.init()
pygame.mixer.set_num_channels(32)
pygame.mixer.set_reserved(0) # Reserve channel for cut scenes
# Enable key repeat for volume controls
pygame.key.set_repeat(500, 100)
# Load sound files
try:
soundFiles = [f for f in listdir("sounds/")
if isfile(join("sounds/", f))
and (f.split('.')[1].lower() in ["ogg", "wav"])]
except Exception as e:
print("No sounds found.")
speak("No sounds found.", False)
soundFiles = []
# Create dictionary of sound objects
soundData = {}
for f in soundFiles:
soundData[f.split('.')[0]] = pygame.mixer.Sound("sounds/" + f)
# Play intro sound if available
if 'game-intro' in soundData:
soundData['game-intro'].play()
time.sleep(soundData['game-intro'].get_length())
return soundData
def adjust_master_volume(change):
"""Adjust the master volume for all sounds.
Args:
change (float): Amount to change volume by (positive or negative)
"""
global masterVolume
masterVolume = max(0.0, min(1.0, masterVolume + change))
# Update music volume
if pygame.mixer.music.get_busy():
pygame.mixer.music.set_volume(bgmVolume * masterVolume)
# Update all sound channels
for i in range(pygame.mixer.get_num_channels()):
channel = pygame.mixer.Channel(i)
if channel.get_busy():
current_volume = channel.get_volume()
if isinstance(current_volume, (int, float)):
# Mono audio
channel.set_volume(current_volume * masterVolume)
else:
# Stereo audio
left, right = current_volume
channel.set_volume(left * masterVolume, right * masterVolume)
def adjust_bgm_volume(change):
"""Adjust only the background music volume.
Args:
change (float): Amount to change volume by (positive or negative)
"""
global bgmVolume
bgmVolume = max(0.0, min(1.0, bgmVolume + change))
if pygame.mixer.music.get_busy():
pygame.mixer.music.set_volume(bgmVolume * masterVolume)
def adjust_sfx_volume(change):
"""Adjust volume for sound effects only.
Args:
change (float): Amount to change volume by (positive or negative)
"""
global sfxVolume
sfxVolume = max(0.0, min(1.0, sfxVolume + change))
# Update all sound channels except reserved ones
for i in range(pygame.mixer.get_num_channels()):
channel = pygame.mixer.Channel(i)
if channel.get_busy():
current_volume = channel.get_volume()
if isinstance(current_volume, (int, float)):
# Mono audio
channel.set_volume(current_volume * sfxVolume * masterVolume)
else:
# Stereo audio
left, right = current_volume
channel.set_volume(left * sfxVolume * masterVolume,
right * sfxVolume * masterVolume)
def adjust_bgm_volume(change):
"""Adjust only the background music volume.
Args:
change (float): Amount to change volume by (positive or negative)
"""
global bgmVolume
bgmVolume = max(0.0, min(1.0, bgmVolume + change))
if pygame.mixer.music.get_busy():
pygame.mixer.music.set_volume(bgmVolume * masterVolume)
def play_bgm(music_file):
"""Play background music with proper volume settings.
Args:
music_file (str): Path to the music file to play
"""
try:
pygame.mixer.music.stop()
pygame.mixer.music.load(music_file)
pygame.mixer.music.set_volume(bgmVolume * masterVolume)
pygame.mixer.music.play(-1) # Loop indefinitely
except Exception as e:
pass
def get_input(prompt="Enter text:", text=""):
"""Display a dialog box for text input.
Args:
prompt (str): Prompt text to display (default: "Enter text:")
text (str): Initial text in input box (default: "")
Returns:
str: User input text, or None if cancelled
"""
app = wx.App(False)
dialog = wx.TextEntryDialog(None, prompt, "Input", text)
dialog.SetValue(text)
if dialog.ShowModal() == wx.ID_OK:
userInput = dialog.GetValue()
else:
userInput = None
dialog.Destroy()
return userInput
def speak(text, interrupt=True):
"""Speak text using the configured speech provider and display on screen.
Args:
text (str): Text to speak and display
interrupt (bool): Whether to interrupt current speech (default: True)
"""
if speechProvider == "speechd":
if interrupt:
spd.cancel()
spd.say(text)
else:
if speechProvider == "accessible_output2":
s.speak(text, interrupt=interrupt)
# Display the text on screen
screen = pygame.display.get_surface()
font = pygame.font.Font(None, 36)
# Wrap the text
max_width = screen.get_width() - 40 # Leave a 20-pixel margin on each side
wrapped_text = textwrap.wrap(text, width=max_width // font.size('A')[0])
# Render each line
text_surfaces = [font.render(line, True, (255, 255, 255)) for line in wrapped_text]
screen.fill((0, 0, 0)) # Clear screen with black
# Calculate total height of text block
total_height = sum(surface.get_height() for surface in text_surfaces)
# Start y-position (centered vertically)
currentY = (screen.get_height() - total_height) // 2
# Blit each line of text
for surface in text_surfaces:
text_rect = surface.get_rect(center=(screen.get_width() // 2, currentY + surface.get_height() // 2))
screen.blit(surface, text_rect)
currentY += surface.get_height()
pygame.display.flip()
def check_for_exit():
"""Check if user has pressed escape key.
Returns:
bool: True if escape was pressed, False otherwise
"""
for event in pygame.event.get():
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
return True
return False
pygame.event.pump()
def exit_game():
"""Clean up and exit the game."""
if speechProvider == "speechd":
spd.close()
pygame.mixer.music.stop()
pygame.quit()
exit()
def calculate_volume_and_pan(player_pos, obj_pos):
"""Calculate volume and stereo panning based on relative positions.
Args:
player_pos (float): Player's position on x-axis
obj_pos (float): Object's position on x-axis
Returns:
tuple: (volume, left_vol, right_vol) values between 0 and 1
"""
distance = abs(player_pos - obj_pos)
max_distance = 12 # Maximum audible distance
if distance > max_distance:
return 0, 0, 0 # No sound if out of range
# Calculate volume (non-linear scaling for more noticeable changes)
# Apply masterVolume as the maximum possible volume
volume = (((max_distance - distance) / max_distance) ** 1.5) * masterVolume
# Determine left/right based on relative position
if player_pos < obj_pos:
# Object is to the right
left = max(0, 1 - (obj_pos - player_pos) / max_distance)
right = 1
elif player_pos > obj_pos:
# Object is to the left
left = 1
right = max(0, 1 - (player_pos - obj_pos) / max_distance)
else:
# Player is on the object
left = right = 1
return volume, left, right
def obj_play(sounds, soundName, player_pos, obj_pos, loop=True):
"""Play a sound with positional audio.
Args:
sounds (dict): Dictionary of sound objects
soundName (str): Name of sound to play
player_pos (float): Player's position for audio panning
obj_pos (float): Object's position for audio panning
loop (bool): Whether to loop the sound (default: True)
Returns:
pygame.mixer.Channel: Sound channel object, or None if out of range
"""
volume, left, right = calculate_volume_and_pan(player_pos, obj_pos)
if volume == 0:
return None # Don't play if out of range
# Play the sound on a new channel
channel = sounds[soundName].play(-1 if loop else 0)
if channel:
channel.set_volume(volume * left * sfxVolume,
volume * right * sfxVolume)
return channel
def obj_update(channel, player_pos, obj_pos):
"""Update positional audio for a playing sound.
Args:
channel: Sound channel to update
player_pos (float): New player position
obj_pos (float): New object position
Returns:
pygame.mixer.Channel: Updated channel, or None if sound should stop
"""
if channel is None:
return None
volume, left, right = calculate_volume_and_pan(player_pos, obj_pos)
if volume == 0:
channel.stop()
return None
# Apply the volume and pan
channel.set_volume(volume * left * sfxVolume,
volume * right * sfxVolume)
return channel
def obj_stop(channel):
"""Stop a playing sound channel.
Args:
channel: Sound channel to stop
Returns:
None if stopped successfully, otherwise returns original channel
"""
try:
channel.stop()
return None
except:
return channel
def play_sound(sound, volume=1.0):
"""Play a sound with current volume settings applied.
Args:
sound: pygame Sound object to play
volume: base volume for the sound (0.0-1.0, default: 1.0)
Returns:
pygame.mixer.Channel: The channel the sound is playing on
"""
channel = sound.play()
if channel:
channel.set_volume(volume * sfxVolume * masterVolume)
return channel
def play_ambiance(sounds, soundNames, probability, randomLocation=False):
"""Play random ambient sounds with optional positional audio.
Args:
sounds (dict): Dictionary of sound objects
soundNames (list): List of possible sound names to choose from
probability (int): Chance to play (1-100)
randomLocation (bool): Whether to randomize stereo position
Returns:
pygame.mixer.Channel: Sound channel if played, None otherwise
"""
# Check if any of the sounds in the list is already playing
for soundName in soundNames:
if pygame.mixer.find_channel(True) and pygame.mixer.find_channel(True).get_busy():
return None
if random.randint(1, 100) > probability:
return None
# Choose a random sound from the list
ambianceSound = random.choice(soundNames)
channel = sounds[ambianceSound].play()
if randomLocation and channel:
leftVolume = random.random() * sfxVolume * masterVolume
rightVolume = random.random() * sfxVolume * masterVolume
channel.set_volume(leftVolume, rightVolume)
return channel
def play_random(sounds, soundName, pause=False, interrupt=False):
"""Play a random variation of a sound.
Args:
sounds (dict): Dictionary of sound objects
soundName (str): Base name of sound (will match all starting with this)
pause (bool): Whether to pause execution until sound finishes
interrupt (bool): Whether to interrupt other sounds
"""
key = []
for i in sounds.keys():
if re.match("^" + soundName + ".*", i):
key.append(i)
if not key: # No matching sounds found
return
randomKey = random.choice(key)
if interrupt:
cut_scene(sounds, randomKey)
return
channel = sounds[randomKey].play()
if channel:
channel.set_volume(sfxVolume * masterVolume, sfxVolume * masterVolume)
if pause:
time.sleep(sounds[randomKey].get_length())
def play_random_positional(sounds, soundName, player_x, object_x):
"""Play a random variation of a sound with positional audio.
Args:
sounds (dict): Dictionary of sound objects
soundName (str): Base name of sound to match
player_x (float): Player's x position
object_x (float): Object's x position
Returns:
pygame.mixer.Channel: Sound channel if played, None otherwise
"""
keys = [k for k in sounds.keys() if k.startswith(soundName)]
if not keys:
return None
randomKey = random.choice(keys)
volume, left, right = calculate_volume_and_pan(player_x, object_x)
if volume == 0:
return None
channel = sounds[randomKey].play()
if channel:
channel.set_volume(volume * left * sfxVolume,
volume * right * sfxVolume)
return channel
def cut_scene(sounds, soundName):
"""Play a sound as a cut scene, stopping other sounds.
Args:
sounds (dict): Dictionary of sound objects
soundName (str): Name of sound to play
"""
pygame.event.clear()
pygame.mixer.stop()
channel = pygame.mixer.Channel(0)
channel.play(sounds[soundName])
while pygame.mixer.get_busy():
event = pygame.event.poll()
if event.type == pygame.KEYDOWN and event.key in [pygame.K_ESCAPE, pygame.K_RETURN, pygame.K_SPACE]:
pygame.mixer.stop()
pygame.event.pump()
def play_random_falling(sounds, soundName, player_x, object_x, start_y,
currentY=0, max_y=20, existing_channel=None):
"""Play or update a falling sound with positional audio and volume based on height.
Args:
sounds (dict): Dictionary of sound objects
soundName (str): Base name of sound to match
player_x (float): Player's x position
object_x (float): Object's x position
start_y (float): Starting Y position (0-20, higher = quieter start)
currentY (float): Current Y position (0 = ground level) (default: 0)
max_y (float): Maximum Y value (default: 20)
existing_channel: Existing sound channel to update (default: None)
Returns:
pygame.mixer.Channel: Sound channel for updating position/volume,
or None if sound should stop
"""
# Calculate horizontal positioning
volume, left, right = calculate_volume_and_pan(player_x, object_x)
# Calculate vertical fall volume multiplier (0 at max_y, 1 at y=0)
fallMultiplier = 1 - (currentY / max_y)
# Adjust final volumes
finalVolume = volume * fallMultiplier
finalLeft = left * finalVolume
finalRight = right * finalVolume
if existing_channel is not None:
if volume == 0: # Out of audible range
existing_channel.stop()
return None
existing_channel.set_volume(finalLeft * sfxVolume,
finalRight * sfxVolume)
return existing_channel
else: # Need to create new channel
if volume == 0: # Don't start if out of range
return None
# Find matching sound files
keys = [k for k in sounds.keys() if k.startswith(soundName)]
if not keys:
return None
randomKey = random.choice(keys)
channel = sounds[randomKey].play()
if channel:
channel.set_volume(finalLeft * sfxVolume,
finalRight * sfxVolume)
return channel
def display_text(text):
"""Display and speak text with navigation controls.
Allows users to:
- Navigate text line by line with arrow keys
- Listen to full text with space
- Copy current line or full text
- Exit with enter/escape
Args:
text (list): List of text lines to display
"""
currentIndex = 0
text.insert(0, "Press space to read the whole text. Use up and down arrows to navigate "
"the text line by line. Press c to copy the current line to the clipboard "
"or t to copy the entire text. Press enter or escape when you are done reading.")
text.append("End of text.")
speak(text[currentIndex])
while True:
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
return
if event.key == pygame.K_DOWN and currentIndex < len(text) - 1:
currentIndex += 1
speak(text[currentIndex])
if event.key == pygame.K_UP and currentIndex > 0:
currentIndex -= 1
speak(text[currentIndex])
if event.key == pygame.K_SPACE:
speak(' '.join(text[1:]))
if event.key == pygame.K_c:
try:
pyperclip.copy(text[currentIndex])
speak("Copied " + text[currentIndex] + " to the clipboard.")
except:
speak("Failed to copy the text to the clipboard.")
if event.key == pygame.K_t:
try:
pyperclip.copy(' '.join(text[1:-1]))
speak("Copied entire message to the clipboard.")
except:
speak("Failed to copy the text to the clipboard.")
event = pygame.event.clear()
time.sleep(0.001)
def messagebox(text):
"""Display a simple message box with text.
Shows a message that can be repeated until the user chooses to continue.
Args:
text (str): Message to display
"""
speak(text + "\nPress any key to repeat or enter to continue.")
while True:
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key in (pygame.K_ESCAPE, pygame.K_RETURN):
return
speak(text + "\nPress any key to repeat or enter to continue.")
def instructions():
"""Display game instructions from file.
Reads and displays instructions from 'files/instructions.txt'.
If file is missing, displays an error message.
"""
try:
with open('files/instructions.txt', 'r') as f:
info = f.readlines()
except:
info = ["Instructions file is missing."]
display_text(info)
def credits():
"""Display game credits from file.
Reads and displays credits from 'files/credits.txt'.
Adds game name header before displaying.
If file is missing, displays an error message.
"""
try:
with open('files/credits.txt', 'r') as f:
info = f.readlines()
# Add the header
info.insert(0, gameName + ": brought to you by Storm Dragon")
except:
info = ["Credits file is missing."]
display_text(info)
def learn_sounds(sounds):
"""Interactive menu for learning game sounds.
Allows users to:
- Navigate through available sounds
- Play selected sounds
- Return to menu with escape key
Args:
sounds (dict): Dictionary of available sound objects
Returns:
str: "menu" if user exits with escape
"""
loop = True
pygame.mixer.music.pause()
currentIndex = 0
# Get list of available sounds, excluding special sounds
soundFiles = [f for f in listdir("sounds/")
if isfile(join("sounds/", f))
and (f.split('.')[1].lower() in ["ogg", "wav"])
and (f.split('.')[0].lower() not in ["game-intro", "music_menu"])
and (not f.lower().startswith("_"))]
# Track last spoken index to avoid repetition
lastSpoken = -1
while loop:
if currentIndex != lastSpoken:
speak(soundFiles[currentIndex][:-4])
lastSpoken = currentIndex
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
return "menu"
if event.key == pygame.K_DOWN and currentIndex < len(soundFiles) - 1:
pygame.mixer.stop()
currentIndex += 1
if event.key == pygame.K_UP and currentIndex > 0:
pygame.mixer.stop()
currentIndex -= 1
if event.key == pygame.K_RETURN:
try:
soundName = soundFiles[currentIndex][:-4]
pygame.mixer.stop()
sounds[soundName].play()
except:
lastSpoken = -1
speak("Could not play sound.")
event = pygame.event.clear()
time.sleep(0.001)
def game_menu(sounds, *options):
"""Display and handle the main game menu.
Provides menu navigation with:
- Up/Down arrows for selection
- Home/End for first/last option
- Enter to select
- Escape to exit
- Volume controls for all options
Args:
sounds (dict): Dictionary of sound objects
*options: Variable list of menu options
Returns:
str: Selected menu option if not handled internally
"""
loop = True
pygame.mixer.stop()
if pygame.mixer.music.get_busy():
pygame.mixer.music.unpause()
else:
try:
play_bgm("sounds/music_menu.ogg")
except:
pass
currentIndex = 0
lastSpoken = -1 # Track last spoken index
while loop:
if currentIndex != lastSpoken:
speak(options[currentIndex])
lastSpoken = currentIndex
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
# Volume controls
if event.key == pygame.K_PAGEUP:
adjust_master_volume(0.1)
elif event.key == pygame.K_PAGEDOWN:
adjust_master_volume(-0.1)
elif event.key == pygame.K_HOME:
if currentIndex != 0:
currentIndex = 0
try:
sounds['menu-move'].play()
except:
pass
if options[currentIndex] != "donate":
pygame.mixer.music.unpause()
else:
adjust_bgm_volume(0.1)
elif event.key == pygame.K_END:
if currentIndex != len(options) - 1:
currentIndex = len(options) - 1
try:
sounds['menu-move'].play()
except:
pass
if options[currentIndex] != "donate":
pygame.mixer.music.unpause()
else:
adjust_bgm_volume(-0.1)
elif event.key == pygame.K_INSERT:
adjust_sfx_volume(0.1)
elif event.key == pygame.K_DELETE:
adjust_sfx_volume(-0.1)
# Menu navigation
elif event.key == pygame.K_ESCAPE:
exit_game()
elif event.key == pygame.K_DOWN and currentIndex < len(options) - 1:
currentIndex += 1
try:
sounds['menu-move'].play()
except:
pass
if options[currentIndex] != "donate":
pygame.mixer.music.unpause()
elif event.key == pygame.K_UP and currentIndex > 0:
currentIndex -= 1
try:
sounds['menu-move'].play()
except:
pass
if options[currentIndex] != "donate":
pygame.mixer.music.unpause()
elif event.key == pygame.K_RETURN:
try:
lastSpoken = -1
try:
sounds['menu-select'].play()
time.sleep(sounds['menu-select'].get_length())
except:
pass
eval(options[currentIndex] + "()")
except:
lastSpoken = -1
return options[currentIndex]
event = pygame.event.clear()
time.sleep(0.001)
def donate():
"""Open the donation webpage.
Pauses background music and opens the Ko-fi donation page.
"""
pygame.mixer.music.pause()
webbrowser.open('https://ko-fi.com/stormux')

View File

@@ -1,511 +0,0 @@
#!/bin/python
# -*- coding: utf-8 -*-
"""Standard initializations and functions shared by all games."""
from sys import exit
import configparser
import os
from os import listdir
from os.path import isfile, join
from inspect import isfunction
from xdg import BaseDirectory
from setproctitle import setproctitle
import pygame
import pyperclip
import random
import re
import requests
import textwrap
import webbrowser
# Global variable for speech provider
try:
import speechd
spd = speechd.Client()
speechProvider = "speechd"
except ImportError:
import accessible_output2.outputs.auto
s = accessible_output2.outputs.auto.Auto()
speechProvider = "accessible_output2"
except ImportError:
print("No other speech providers found.")
exit()
import math
import numpy as np
import time
localConfig = configparser.ConfigParser()
globalConfig = configparser.ConfigParser()
class scoreboard():
'Handles scores and top 10'
def __init__(self, startingScore = 0):
read_config()
try:
localConfig.add_section("scoreboard")
except:
pass
self.score = startingScore
self.oldScores = []
for i in range(1, 11):
try:
self.oldScores.insert(i - 1, localConfig.getint("scoreboard", str(i)))
except:
pass
self.oldScores.insert(i - 1, 0)
for i in range(1, 11):
if self.oldScores[i - 1] == None:
self.oldScores[i - 1] = 0
def __del__(self):
self.Update_Scores()
try:
write_config()
except:
pass
def Decrease_Score(self, points = 1):
self.score -= points
def Get_High_Score(self, position = 1):
return self.oldScores[position - 1]
def Get_Score(self):
return self.score
def Increase_Score(self, points = 1):
self.score += points
def New_High_Score(self):
for i, j in enumerate(self.oldScores):
if self.score > j: return i + 1
return None
def Update_Scores(self):
# Update the scores
for i, j in enumerate(self.oldScores):
if self.score > j:
self.oldScores.insert(i, self.score)
break
# Only keep the top 10 scores.
self.oldScores = self.oldScores[:10]
# Update the scoreboard section of the games config file.
for i, j in enumerate(self.oldScores):
localConfig.set("scoreboard", str(i + 1), str(j))
def write_config(writeGlobal = False):
if writeGlobal == False:
with open(gamePath + "/config.ini", 'w') as configfile:
localConfig.write(configfile)
else:
with open(globalPath + "/config.ini", 'w') as configfile:
globalConfig.write(configfile)
def read_config(readGlobal = False):
if readGlobal == False:
try:
with open(gamePath + "/config.ini", 'r') as configfile:
localConfig.read_file(configfile)
except:
pass
else:
try:
with open(globalPath + "/config.ini", 'r') as configfile:
globalConfig.read_file(configfile)
except:
pass
def speak(text, interupt=True):
if speechProvider == "speechd":
if interupt: spd.cancel()
spd.say(text)
else:
if speechProvider == "accessible_output2":
s.speak(text, interrupt=True)
# Display the text on screen
screen = pygame.display.get_surface()
font = pygame.font.Font(None, 36)
# Wrap the text
maxWidth = screen.get_width() - 40 # Leave a 20-pixel margin on each side
wrappedText = textwrap.wrap(text, width=maxWidth // font.size('A')[0])
# Render each line
textSurfaces = [font.render(line, True, (255, 255, 255)) for line in wrappedText]
screen.fill((0, 0, 0)) # Clear screen with black
# Calculate total height of text block
totalHeight = sum(surface.get_height() for surface in textSurfaces)
# Start y-position (centered vertically)
currentY = (screen.get_height() - totalHeight) // 2
# Blit each line of text
for surface in textSurfaces:
textRect = surface.get_rect(center=(screen.get_width() // 2, currentY + surface.get_height() // 2))
screen.blit(surface, textRect)
currentY += surface.get_height()
pygame.display.flip()
def check_for_exit():
for event in pygame.event.get():
if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
return True
return False
pygame.event.pump()
def exit_game():
if speechProvider == "speechd": spd.close()
pygame.mixer.music.stop()
pygame.quit()
exit()
def initialize_gui(gameTitle):
# Check for, and possibly create, storm-games path
global globalPath
global gamePath
globalPath = BaseDirectory.xdg_config_home + "/storm-games"
gamePath = globalPath + "/" + str.lower(str.replace(gameTitle, " ", "-"))
if not os.path.exists(gamePath): os.makedirs(gamePath)
# Seed the random generator to the clock
random.seed()
# Set game's name
global gameName
gameName = gameTitle
setproctitle(str.lower(str.replace(gameTitle, " ", "")))
# start pygame
pygame.init()
# start the display (required by the event loop)
pygame.display.set_mode((800, 600))
pygame.display.set_caption(gameTitle)
# Set 32 channels for sound by default
pygame.mixer.pre_init(44100, -16, 2, 1024)
pygame.mixer.init()
pygame.mixer.set_num_channels(32)
# Reserve the cut scene channel
pygame.mixer.set_reserved(0)
# Load sounds from the sound directory and creates a list like {'bottle': 'bottle.ogg'}
try:
soundFiles = [f for f in listdir("sounds/") if isfile(join("sounds/", f)) and (f.split('.')[1].lower() in ["ogg","wav"])]
except Exception as e:
print("No sounds found.")
speak("No sounds found.", False)
#lets make a dict with pygame.mixer.Sound() objects {'bottle':<soundobject>}
soundData = {}
for f in soundFiles:
soundData[f.split('.')[0]] = pygame.mixer.Sound("sounds/" + f)
soundData['game-intro'].play()
time.sleep(soundData['game-intro'].get_length())
return soundData
def generate_tone(frequency, duration=0.1, sample_rate=44100, volume = 0.2):
t = np.linspace(0, duration, int(sample_rate * duration), False)
tone = np.sin(2 * np.pi * frequency * t)
stereo_tone = np.vstack((tone, tone)).T # Create a 2D array for stereo
stereo_tone = (stereo_tone * 32767).astype(np.int16)
stereo_tone = (stereo_tone * 32767 * volume).astype(np.int16) # Apply volume
stereo_tone = np.ascontiguousarray(stereo_tone) # Ensure C-contiguous array
return pygame.sndarray.make_sound(stereo_tone)
def x_powerbar():
clock = pygame.time.Clock()
screen = pygame.display.get_surface()
position = -50 # Start from the leftmost position
direction = 1 # Move right initially
barHeight = 20
while True:
frequency = 440 # A4 note
leftVolume = (50 - position) / 100
rightVolume = (position + 50) / 100
tone = generate_tone(frequency)
channel = tone.play()
channel.set_volume(leftVolume, rightVolume)
# Visual representation
screen.fill((0, 0, 0))
barWidth = screen.get_width() - 40 # Leave 20px margin on each side
pygame.draw.rect(screen, (100, 100, 100), (20, screen.get_height() // 2 - barHeight // 2, barWidth, barHeight))
markerPos = int(20 + (position + 50) / 100 * barWidth)
pygame.draw.rect(screen, (255, 0, 0), (markerPos - 5, screen.get_height() // 2 - barHeight, 10, barHeight * 2))
pygame.display.flip()
for event in pygame.event.get():
check_for_exit()
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
channel.stop()
return position # This will return a value between -50 and 50
position += direction
if position > 50:
position = 50
direction = -1
elif position < -50:
position = -50
direction = 1
clock.tick(40) # Speed of bar
def y_powerbar():
clock = pygame.time.Clock()
screen = pygame.display.get_surface()
power = 0
direction = 1 # 1 for increasing, -1 for decreasing
barWidth = 20
while True:
frequency = 220 + (power * 5) # Adjust these values to change the pitch range
tone = generate_tone(frequency)
channel = tone.play()
# Visual representation
screen.fill((0, 0, 0))
barHeight = screen.get_height() - 40 # Leave 20px margin on top and bottom
pygame.draw.rect(screen, (100, 100, 100), (screen.get_width() // 2 - barWidth // 2, 20, barWidth, barHeight))
markerPos = int(20 + (100 - power) / 100 * barHeight)
pygame.draw.rect(screen, (255, 0, 0), (screen.get_width() // 2 - barWidth, markerPos - 5, barWidth * 2, 10))
pygame.display.flip()
for event in pygame.event.get():
check_for_exit()
if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
channel.stop()
return power
power += direction
if power >= 100 or power <= 0:
direction *= -1 # Reverse direction at limits
clock.tick(40)
def cut_scene(sounds, soundName):
pygame.event.clear()
pygame.mixer.stop()
c = pygame.mixer.Channel(0)
c.play(sounds[soundName])
while pygame.mixer.get_busy():
event = pygame.event.poll()
if event.type == pygame.KEYDOWN and event.key in [pygame.K_ESCAPE, pygame.K_RETURN, pygame.K_SPACE]:
pygame.mixer.stop()
pygame.event.pump()
def calculate_volume_and_pan(player_pos, obj_pos):
distance = abs(player_pos - obj_pos)
max_distance = 12 # Maximum audible distance
if distance > max_distance:
return 0, 0, 0 # No sound if out of range
# Calculate volume (non-linear scaling for more noticeable changes)
volume = ((max_distance - distance) / max_distance) ** 1.5
# Determine left/right based on relative position
if player_pos < obj_pos:
# Object is to the right
left = max(0, 1 - (obj_pos - player_pos) / max_distance)
right = 1
elif player_pos > obj_pos:
# Object is to the left
left = 1
right = max(0, 1 - (player_pos - obj_pos) / max_distance)
else:
# Player is on the object
left = right = 1
return volume, left, right
def obj_play(sounds, soundName, player_pos, obj_pos):
volume, left, right = calculate_volume_and_pan(player_pos, obj_pos)
if volume == 0:
return None # Don't play if out of range
# Play the sound on a new channel
x = sounds[soundName].play(-1)
# Apply the volume and pan
x.set_volume(volume * left, volume * right)
return x
def obj_update(x, player_pos, obj_pos):
if x is None:
return None
volume, left, right = calculate_volume_and_pan(player_pos, obj_pos)
if volume == 0:
x.stop()
return None
# Apply the volume and pan
x.set_volume(volume * left, volume * right)
return x
def obj_stop(x):
# Tries to stop a playing object channel
try:
x.stop()
return None
except:
return x
def play_ambiance(sounds, soundNames, probability, randomLocation = False):
# Check if any of the sounds in the list is already playing
for soundName in soundNames:
if pygame.mixer.find_channel(True) and pygame.mixer.find_channel(True).get_busy():
return
if random.randint(1, 100) > probability:
return
# Choose a random sound from the list
ambianceSound = random.choice(soundNames)
channel = sounds[ambianceSound].play()
if randomLocation and channel:
left_volume = random.random()
right_volume = random.random()
channel.set_volume(left_volume, right_volume)
return channel # Return the channel object for potential further manipulation
def play_random(sounds, soundName, pause = False, interrupt = False):
key = []
for i in sounds.keys():
if re.match("^" + soundName + ".*", i):
key.append(i)
randomKey = random.choice(key)
if interrupt == False:
sounds[randomKey].play()
else:
cut_scene(sounds, randomKey)
# Cut scenes override the pause option
return
if pause == True:
time.sleep(sounds[randomKey].get_length())
def instructions():
# Read in the instructions file
try:
with open('files/instructions.txt', 'r') as f:
info = f.readlines()
except:
info = ["Instructions file is missing."]
display_text(info)
def credits():
# Read in the credits file.
try:
with open('files/credits.txt', 'r') as f:
info = f.readlines()
# Add the header
info.insert(0, gameName + ": brought to you by Storm Dragon")
except:
info = ["Credits file is missing."]
display_text(info)
def display_text(text):
i = 0
text.insert(0, "Press space to read the whole text. Use up and down arrows to navigate the text line by line. Press c to copy the current line to the clipboard or t to copy the entire text. Press enter or escape when you are done reading.")
text.append("End of text.")
speak(text[i])
while True:
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE or event.key == pygame.K_RETURN: return
if event.key == pygame.K_DOWN and i < len(text) - 1: i = i + 1
if event.key == pygame.K_UP and i > 0: i = i - 1
if event.key == pygame.K_SPACE:
speak(' '.join(text[1:]))
else:
speak(text[i])
if event.key == pygame.K_c:
try:
pyperclip.copy(text[i])
speak("Copied " + text[i] + " to the clipboard.")
except:
speak("Failed to copy the text to the clipboard.")
if event.key == pygame.K_t:
try:
pyperclip.copy(' '.join(text[1:-1]))
speak("Copied entire message to the clipboard.")
except:
speak("Failed to copy the text to the clipboard.")
event = pygame.event.clear()
time.sleep(0.001)
def learn_sounds(sounds):
loop = True
pygame.mixer.music.pause()
i = 0
soundFiles = [f for f in listdir("sounds/") if isfile(join("sounds/", f)) and (f.split('.')[1].lower() in ["ogg","wav"]) and (f.split('.')[0].lower() not in ["game-intro", "music_menu"]) and (not f.lower().startswith("_"))]
# j keeps track of last spoken index so it isn't voiced on key up.
j = -1
while loop == True:
if i != j:
speak(soundFiles[i][:-4])
j = i
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: return "menu"
if event.key == pygame.K_DOWN and i < len(soundFiles) - 1:
pygame.mixer.stop()
i = i + 1
if event.key == pygame.K_UP and i > 0:
pygame.mixer.stop()
i = i - 1
if event.key == pygame.K_RETURN:
try:
soundName = soundFiles[i][:-4]
pygame.mixer.stop()
sounds[soundName].play()
continue
except:
j = -1
speak("Could not play sound.")
continue
event = pygame.event.clear()
time.sleep(0.001)
def game_menu(sounds, *options):
loop = True
pygame.mixer.stop()
if pygame.mixer.music.get_busy():
pygame.mixer.music.unpause()
else:
try:
pygame.mixer.music.load("sounds/music_menu.ogg")
pygame.mixer.music.set_volume(0.75)
pygame.mixer.music.play(-1)
except:
pass
i = 0
# j keeps track of last spoken index so it isn't voiced on key up.
j = -1
while loop == True:
if i != j:
speak(options[i])
j = i
event = pygame.event.wait()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE: exit_game()
if event.key == pygame.K_DOWN and i < len(options) - 1:
i = i + 1
try:
sounds['menu-move'].play()
except:
pass
if options[i] != "donate": pygame.mixer.music.unpause()
if event.key == pygame.K_UP and i > 0:
i = i - 1
try:
sounds['menu-move'].play()
except:
pass
if options[i] != "donate": pygame.mixer.music.unpause()
if event.key == pygame.K_HOME and i != 0:
i = 0
try:
sounds['menu-move'].play()
except:
pass
if options[i] != "donate": pygame.mixer.music.unpause()
if event.key == pygame.K_END and i != len(options) - 1:
i = len(options) -1
try:
sounds['menu-move'].play()
except:
pass
if options[i] != "donate": pygame.mixer.music.unpause()
if event.key == pygame.K_RETURN:
try:
j = -1
try:
sounds['menu-select'].play()
time.sleep(sounds['menu-select'].get_length())
except:
pass
eval(options[i] + "()")
continue
except:
j = -1
return options[i]
continue
event = pygame.event.clear()
time.sleep(0.001)
def donate():
pygame.mixer.music.pause()
webbrowser.open('https://ko-fi.com/stormux')