Skip to content

Commit d15e7c0

Browse files
Merge pull request #87 from Preponderous-Software/copilot/fix-23cdbfde-e934-442b-a458-2d5a2e1c718c
Implement comprehensive Options Menu Screen with enhanced button feedback and ESC key navigation
2 parents 24a0697 + c6ac12e commit d15e7c0

File tree

10 files changed

+1260
-66
lines changed

10 files changed

+1260
-66
lines changed

src/audio/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Audio package

src/audio/audio_manager.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import pygame
2+
import logging
3+
from typing import Optional
4+
5+
logger = logging.getLogger(__name__)
6+
7+
8+
class AudioManager:
9+
"""Manages audio playback and volume controls"""
10+
11+
def __init__(self, config):
12+
self.config = config
13+
self.initialized = False
14+
15+
try:
16+
# Initialize pygame mixer for audio
17+
pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512)
18+
self.initialized = True
19+
logger.info("Audio system initialized successfully")
20+
except pygame.error as e:
21+
logger.warning(f"Failed to initialize audio system: {e}")
22+
self.initialized = False
23+
24+
def play_sound_effect(self, sound_name: str = "default"):
25+
"""Play a sound effect with SFX volume"""
26+
if not self.initialized:
27+
return
28+
29+
try:
30+
# Create a simple beep sound programmatically since we don't have audio files
31+
# This is a placeholder that demonstrates volume control
32+
effective_volume = self.config.master_volume * self.config.sfx_volume
33+
34+
if effective_volume > 0:
35+
# Generate a simple tone for demonstration
36+
duration = 0.1 # seconds
37+
sample_rate = 22050
38+
frames = int(duration * sample_rate)
39+
40+
# Create a simple sine wave (beep)
41+
import numpy as np
42+
frequency = 440 # A note
43+
arr = np.sin(2 * np.pi * frequency * np.linspace(0, duration, frames))
44+
arr = (arr * 32767 * effective_volume).astype(np.int16)
45+
46+
# Convert to stereo
47+
stereo_arr = np.zeros((frames, 2), np.int16)
48+
stereo_arr[:, 0] = arr
49+
stereo_arr[:, 1] = arr
50+
51+
sound = pygame.sndarray.make_sound(stereo_arr)
52+
sound.play()
53+
logger.debug(f"Played sound effect '{sound_name}' at volume {effective_volume:.2f}")
54+
except Exception as e:
55+
logger.debug(f"Could not play sound effect: {e}")
56+
57+
def play_music(self, music_name: str = "background"):
58+
"""Play background music with music volume"""
59+
if not self.initialized:
60+
return
61+
62+
try:
63+
# Set music volume
64+
effective_volume = self.config.master_volume * self.config.music_volume
65+
pygame.mixer.music.set_volume(effective_volume)
66+
logger.debug(f"Set music volume to {effective_volume:.2f}")
67+
68+
# In a real implementation, you would load and play music files here
69+
# pygame.mixer.music.load("path/to/music/file.ogg")
70+
# pygame.mixer.music.play(-1) # Loop indefinitely
71+
72+
except Exception as e:
73+
logger.debug(f"Could not play music: {e}")
74+
75+
def stop_music(self):
76+
"""Stop background music"""
77+
if not self.initialized:
78+
return
79+
80+
try:
81+
pygame.mixer.music.stop()
82+
except Exception as e:
83+
logger.debug(f"Could not stop music: {e}")
84+
85+
def update_volumes(self):
86+
"""Update all audio volumes based on current config"""
87+
if not self.initialized:
88+
return
89+
90+
# Update music volume if music is playing
91+
try:
92+
effective_volume = self.config.master_volume * self.config.music_volume
93+
pygame.mixer.music.set_volume(effective_volume)
94+
logger.debug(f"Updated music volume to {effective_volume:.2f}")
95+
except Exception as e:
96+
logger.debug(f"Could not update music volume: {e}")
97+
98+
def cleanup(self):
99+
"""Clean up audio resources"""
100+
if self.initialized:
101+
try:
102+
pygame.mixer.quit()
103+
logger.info("Audio system cleaned up")
104+
except Exception as e:
105+
logger.warning(f"Error during audio cleanup: {e}")

src/config/config.py

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
# @author Daniel McCoy Stephenson
22
# @since August 6th, 2022
33

4+
import json
5+
import os
6+
import pygame
47

58
# @author Daniel McCoy Stephenson
69
# @since August 6th, 2022
@@ -15,16 +18,105 @@ def __init__(self):
1518
self.green = (0, 255, 0)
1619
self.red = (255, 0, 0)
1720
self.yellow = (255, 255, 0)
21+
self.blue = (0, 0, 255)
22+
self.gray = (128, 128, 128)
1823
self.text_size = 50
1924

25+
# audio settings
26+
self.master_volume = 0.7
27+
self.music_volume = 0.5
28+
self.sfx_volume = 0.8
29+
2030
# grid size
2131
self.initial_grid_size = 5
2232

2333
# tick speed
2434
self.limit_tick_speed = True
2535
self.tick_speed = 0.1
2636

37+
# difficulty settings
38+
self.difficulty = "Normal" # Easy, Normal, Hard
39+
40+
# key bindings
41+
self.key_bindings = {
42+
'move_up': pygame.K_w,
43+
'move_down': pygame.K_s,
44+
'move_left': pygame.K_a,
45+
'move_right': pygame.K_d,
46+
'fullscreen': pygame.K_F11,
47+
'restart': pygame.K_r,
48+
'quit': pygame.K_q
49+
}
50+
2751
# misc
2852
self.debug = False
2953
self.restart_upon_collision = True
3054
self.level_progress_percentage_required = 0.25
55+
56+
# Load saved settings
57+
self.load_settings()
58+
59+
def get_available_resolutions(self):
60+
"""Get available display resolutions"""
61+
return [
62+
(500, 500),
63+
(800, 600),
64+
(1024, 768),
65+
(1280, 720),
66+
(1920, 1080)
67+
]
68+
69+
def get_difficulty_levels(self):
70+
"""Get available difficulty levels"""
71+
return ["Easy", "Normal", "Hard"]
72+
73+
def save_settings(self):
74+
"""Save current settings to file"""
75+
settings = {
76+
'display_width': self.display_width,
77+
'display_height': self.display_height,
78+
'fullscreen': self.fullscreen,
79+
'master_volume': self.master_volume,
80+
'music_volume': self.music_volume,
81+
'sfx_volume': self.sfx_volume,
82+
'limit_tick_speed': self.limit_tick_speed,
83+
'tick_speed': self.tick_speed,
84+
'difficulty': self.difficulty,
85+
'initial_grid_size': self.initial_grid_size,
86+
'level_progress_percentage_required': self.level_progress_percentage_required,
87+
'key_bindings': self.key_bindings
88+
}
89+
90+
try:
91+
os.makedirs('config', exist_ok=True)
92+
with open('config/settings.json', 'w') as f:
93+
json.dump(settings, f, indent=2)
94+
except Exception as e:
95+
print(f"Warning: Could not save settings: {e}")
96+
97+
def load_settings(self):
98+
"""Load settings from file"""
99+
try:
100+
with open('config/settings.json', 'r') as f:
101+
settings = json.load(f)
102+
103+
self.display_width = settings.get('display_width', self.display_width)
104+
self.display_height = settings.get('display_height', self.display_height)
105+
self.fullscreen = settings.get('fullscreen', self.fullscreen)
106+
self.master_volume = settings.get('master_volume', self.master_volume)
107+
self.music_volume = settings.get('music_volume', self.music_volume)
108+
self.sfx_volume = settings.get('sfx_volume', self.sfx_volume)
109+
self.limit_tick_speed = settings.get('limit_tick_speed', self.limit_tick_speed)
110+
self.tick_speed = settings.get('tick_speed', self.tick_speed)
111+
self.difficulty = settings.get('difficulty', self.difficulty)
112+
self.initial_grid_size = settings.get('initial_grid_size', self.initial_grid_size)
113+
self.level_progress_percentage_required = settings.get('level_progress_percentage_required', self.level_progress_percentage_required)
114+
115+
# Load key bindings, ensuring they are valid pygame key constants
116+
saved_bindings = settings.get('key_bindings', {})
117+
for key, value in saved_bindings.items():
118+
if key in self.key_bindings and isinstance(value, int):
119+
self.key_bindings[key] = value
120+
except (FileNotFoundError, json.JSONDecodeError):
121+
# File doesn't exist or is invalid, use defaults
122+
pass

src/environment/pyEnvLibEnvironmentRepositoryImpl.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,11 +25,24 @@ def __init__(self, level: int, config: Config, snake_part_repository: SnakePartR
2525
self.level = level
2626
self.config = config
2727
self.snake_part_repository = snake_part_repository
28+
29+
# Adjust game parameters based on difficulty
30+
base_grid_size = config.initial_grid_size
2831
if level == 1:
29-
grid_size = config.initial_grid_size
32+
grid_size = base_grid_size
3033
else:
31-
grid_size = config.initial_grid_size + level
32-
logging.info("Initializing environment repository for level " + str(level) + " with grid size " + str(grid_size))
34+
grid_size = base_grid_size + level
35+
36+
# Apply difficulty modifiers
37+
if config.difficulty == "Easy":
38+
# Larger grid = easier game
39+
grid_size = max(5, int(grid_size * 1.3))
40+
elif config.difficulty == "Hard":
41+
# Smaller grid = harder game (but ensure minimum of 4 for Hard)
42+
grid_size = max(4, int(grid_size * 0.7))
43+
# Normal difficulty uses default grid size
44+
45+
logging.info(f"Initializing environment repository for level {level} with grid size {grid_size} (difficulty: {config.difficulty})")
3346
self.environment = Environment(
3447
"Level " + str(level), grid_size
3548
)

0 commit comments

Comments
 (0)