feat: Add Sound/Music classes, keyboard state, version (#66, #160, #164)

Replace module-level audio functions with proper OOP API:
- mcrfpy.Sound: Wraps sf::SoundBuffer + sf::Sound for short effects
- mcrfpy.Music: Wraps sf::Music for streaming long tracks
- Both support: volume, loop, playing, duration, play/pause/stop
- Music adds position property for seeking

Add mcrfpy.keyboard singleton for real-time modifier state:
- shift, ctrl, alt, system properties (bool, read-only)
- Queries sf::Keyboard::isKeyPressed() directly

Add mcrfpy.__version__ = "1.0.0" for version identity

Remove old audio API entirely (no deprecation - unused in codebase):
- createSoundBuffer, loadMusic, playSound
- setMusicVolume, getMusicVolume, setSoundVolume, getSoundVolume

closes #66, closes #160, closes #164

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
John McCardle 2025-12-29 16:24:27 -05:00
commit c025cd7da3
11 changed files with 1110 additions and 201 deletions

View file

@ -0,0 +1,153 @@
#!/usr/bin/env python3
"""Test for Sound, Music, Keyboard classes and __version__ (#66, #160, #164)."""
import mcrfpy
import sys
def test_version():
"""Test that __version__ exists and is a valid semver string."""
assert hasattr(mcrfpy, '__version__'), "mcrfpy.__version__ not found"
version = mcrfpy.__version__
assert isinstance(version, str), f"__version__ should be str, got {type(version)}"
parts = version.split('.')
assert len(parts) == 3, f"Version should be MAJOR.MINOR.PATCH, got {version}"
print(f" Version: {version}")
def test_keyboard():
"""Test Keyboard singleton exists and has expected properties."""
assert hasattr(mcrfpy, 'keyboard'), "mcrfpy.keyboard not found"
kb = mcrfpy.keyboard
# Check all modifier properties exist and are bool
for prop in ['shift', 'ctrl', 'alt', 'system']:
assert hasattr(kb, prop), f"keyboard.{prop} not found"
val = getattr(kb, prop)
assert isinstance(val, bool), f"keyboard.{prop} should be bool, got {type(val)}"
print(f" Keyboard state: {kb}")
def test_sound_class():
"""Test Sound class creation and properties."""
# Test with a known good file
sound = mcrfpy.Sound("assets/sfx/splat1.ogg")
# Check repr works
repr_str = repr(sound)
assert 'Sound' in repr_str
assert 'splat1.ogg' in repr_str
print(f" Sound: {repr_str}")
# Check default values
assert sound.volume == 100.0, f"Default volume should be 100, got {sound.volume}"
assert sound.loop == False, f"Default loop should be False, got {sound.loop}"
assert sound.playing == False, f"Should not be playing initially"
assert sound.duration > 0, f"Duration should be positive, got {sound.duration}"
assert sound.source == "assets/sfx/splat1.ogg"
# Test setting properties (use tolerance for floating point)
sound.volume = 50.0
assert abs(sound.volume - 50.0) < 0.01, f"Volume should be ~50, got {sound.volume}"
sound.loop = True
assert sound.loop == True
# Test methods exist (don't actually play in headless)
assert callable(sound.play)
assert callable(sound.pause)
assert callable(sound.stop)
print(f" Duration: {sound.duration:.3f}s")
def test_sound_error_handling():
"""Test Sound raises on invalid file."""
try:
sound = mcrfpy.Sound("nonexistent_file.ogg")
print(" ERROR: Should have raised RuntimeError")
return False
except RuntimeError as e:
print(f" Correctly raised: {e}")
return True
def test_music_class():
"""Test Music class creation and properties."""
music = mcrfpy.Music("assets/sfx/splat1.ogg")
# Check repr works
repr_str = repr(music)
assert 'Music' in repr_str
print(f" Music: {repr_str}")
# Check default values
assert music.volume == 100.0, f"Default volume should be 100, got {music.volume}"
assert music.loop == False, f"Default loop should be False, got {music.loop}"
assert music.playing == False, f"Should not be playing initially, got {music.playing}"
assert music.duration > 0, f"Duration should be positive, got {music.duration}"
# Position comparison needs tolerance for floating point
assert abs(music.position) < 0.001, f"Position should be ~0, got {music.position}"
assert music.source == "assets/sfx/splat1.ogg", f"Source mismatch: {music.source}"
# Test setting properties (use tolerance for floating point)
music.volume = 30.0
assert abs(music.volume - 30.0) < 0.01, f"Volume should be ~30, got {music.volume}"
music.loop = True
assert music.loop == True, f"Loop should be True, got {music.loop}"
# Test position can be set (seek)
# Can't really test this without playing, but check it's writable
music.position = 0.1
print(f" Duration: {music.duration:.3f}s")
def test_music_error_handling():
"""Test Music raises on invalid file."""
try:
music = mcrfpy.Music("nonexistent_file.ogg")
print(" ERROR: Should have raised RuntimeError")
return False
except RuntimeError as e:
print(f" Correctly raised: {e}")
return True
def main():
print("Testing mcrfpy audio and keyboard features (#66, #160, #164)")
print()
tests = [
("__version__", test_version),
("keyboard singleton", test_keyboard),
("Sound class", test_sound_class),
("Sound error handling", test_sound_error_handling),
("Music class", test_music_class),
("Music error handling", test_music_error_handling),
]
passed = 0
failed = 0
for name, test_fn in tests:
print(f"Testing {name}...")
try:
result = test_fn()
if result is False:
failed += 1
print(f" FAILED")
else:
passed += 1
print(f" PASSED")
except Exception as e:
failed += 1
print(f" FAILED: {e}")
print()
print(f"Results: {passed} passed, {failed} failed")
if failed == 0:
print("PASS")
sys.exit(0)
else:
print("FAIL")
sys.exit(1)
if __name__ == "__main__":
main()