guitarHeroButBetter/py/engine/framework.py
James Whiteman 3f7b409302 Python code
2026-01-30 21:16:42 -08:00

630 lines
17 KiB
Python

from __future__ import annotations
from typing import Any, Dict, List, Optional, Tuple, Type, TypeVar
import pyray as rl
T = TypeVar("T")
class Component:
"""Base class for all game object components.
Attributes:
owner: The GameObject that owns this component, or None if unassigned.
"""
def __init__(self) -> None:
self.owner: Optional[GameObject] = None
def init(self) -> None:
"""Lifecycle hook called when the component is initialized.
Returns:
None
"""
pass
def update(self, delta_time: float) -> None:
"""Lifecycle hook called every frame to update the component.
Args:
delta_time: Seconds since the last frame.
Returns:
None
"""
pass
def draw(self) -> None:
"""Lifecycle hook called every frame to draw the component.
Returns:
None
"""
pass
class GameObject:
"""Base class for all game objects (entities) in a scene.
Attributes:
scene: The Scene this object belongs to.
components: Mapping of component type to component instance.
tags: Set of string tags for lookup/filtering.
is_active: If False, update/draw are skipped.
"""
def __init__(self) -> None:
self.scene: Optional[Scene] = None
self.components: Dict[Type[Any], Component] = {}
self.tags: set[str] = set()
self.is_active: bool = True
def init(self) -> None:
"""Lifecycle hook called when the object is initialized.
Returns:
None
"""
pass
def update(self, delta_time: float) -> None:
"""Lifecycle hook called every frame to update the object.
Args:
delta_time: Seconds since the last frame.
Returns:
None
"""
pass
def draw(self) -> None:
"""Lifecycle hook called every frame to draw the object.
Returns:
None
"""
pass
def init_object(self) -> None:
"""Initialize the object and its components.
Returns:
None
"""
self.init()
for component in list(self.components.values()):
component.init()
def update_object(self, delta_time: float) -> None:
"""Update the object and its components if active.
Args:
delta_time: Seconds since the last frame.
Returns:
None
"""
if not self.is_active:
return
self.update(delta_time)
for component in list(self.components.values()):
component.update(delta_time)
def draw_object(self) -> None:
"""Draw the object and its components if active.
Returns:
None
"""
if not self.is_active:
return
self.draw()
for component in list(self.components.values()):
component.draw()
def add_component(self, component_or_cls: Any, *args: Any, **kwargs: Any) -> Component:
"""Add a component instance or construct one from a class.
Args:
component_or_cls: A Component instance or a Component class.
*args: Positional args forwarded to the component constructor.
**kwargs: Keyword args forwarded to the component constructor.
Returns:
The component instance added.
"""
if isinstance(component_or_cls, Component):
component = component_or_cls
else:
component = component_or_cls(*args, **kwargs)
component.owner = self
key = component.__class__
if key in self.components:
print(f"Duplicate component added: {key.__name__}")
self.components[key] = component
return component
def get_component(self, cls: Type[T]) -> Optional[T]:
"""Get a component by type, if present.
Args:
cls: Component class to look up.
Returns:
The component instance if found, otherwise None.
"""
component = self.components.get(cls)
return component if component is None else component # type: ignore[return-value]
def add_tag(self, tag: str) -> None:
"""Add a tag to this object.
Args:
tag: Tag string to add.
Returns:
None
"""
self.tags.add(tag)
def remove_tag(self, tag: str) -> None:
"""Remove a tag from this object.
Args:
tag: Tag string to remove.
Returns:
None
"""
self.tags.discard(tag)
def has_tag(self, tag: str) -> bool:
"""Check if a tag is present.
Args:
tag: Tag string to check.
Returns:
True if the tag is present, otherwise False.
"""
return tag in self.tags
class Service:
"""Base class for scene-level services.
Attributes:
scene: The Scene this service is attached to.
is_init: True once init_service has been run.
is_visible: If False, draw_service is skipped.
"""
def __init__(self) -> None:
self.scene: Optional[Scene] = None
self.is_init: bool = False
self.is_visible: bool = True
def init(self) -> None:
"""Lifecycle hook called when the service is initialized.
Returns:
None
"""
pass
def update(self, delta_time: float) -> None:
"""Lifecycle hook called every frame to update the service.
Args:
delta_time: Seconds since the last frame.
Returns:
None
"""
pass
def draw(self) -> None:
"""Lifecycle hook called every frame to draw the service.
Returns:
None
"""
pass
def init_service(self) -> None:
"""Initialize the service once.
Returns:
None
"""
if self.is_init:
return
self.init()
self.is_init = True
def draw_service(self) -> None:
"""Draw the service if visible.
Returns:
None
"""
if self.is_visible:
self.draw()
class Manager:
"""Base class for global managers.
Attributes:
is_init: True once init_manager has been run.
"""
def __init__(self) -> None:
self.is_init: bool = False
def init(self) -> None:
"""Lifecycle hook called when the manager is initialized.
Returns:
None
"""
pass
def init_manager(self) -> None:
"""Initialize the manager once.
Returns:
None
"""
if self.is_init:
return
self.init()
self.is_init = True
class Scene:
"""Base class for scenes that contain objects and services.
Attributes:
game_objects: List of GameObjects in the scene.
services: List of (type, Service) pairs.
game: Owning Game instance.
is_init: True once init_scene has been run.
"""
def __init__(self) -> None:
self.game_objects: List[GameObject] = []
self.services: List[Tuple[Type[Any], Service]] = []
self.game: Optional[Game] = None
self.is_init: bool = False
def init_services(self) -> None:
"""Hook to add services before scene init.
Returns:
None
"""
pass
def init(self) -> None:
"""Lifecycle hook called when the scene initializes.
Returns:
None
"""
pass
def update(self, delta_time: float) -> None:
"""Lifecycle hook called every frame to update the scene.
Args:
delta_time: Seconds since the last frame.
Returns:
None
"""
pass
def draw(self) -> None:
"""Lifecycle hook called every frame to draw the scene.
Returns:
None
"""
pass
def init_scene(self) -> None:
"""Initialize services, scene, and objects.
Returns:
None
"""
if self.is_init:
return
self.init_services()
for _, service in self.services:
service.init_service()
self.init()
for game_object in list(self.game_objects):
game_object.init_object()
self.is_init = True
def update_scene(self, delta_time: float) -> None:
"""Update the scene, services, and objects.
Args:
delta_time: Seconds since the last frame.
Returns:
None
"""
self.update(delta_time)
for _, service in self.services:
service.update(delta_time)
for game_object in list(self.game_objects):
game_object.update_object(delta_time)
def draw_scene(self) -> None:
"""Draw the scene, services, and objects.
Returns:
None
"""
self.draw()
for _, service in self.services:
service.draw_service()
for game_object in list(self.game_objects):
game_object.draw_object()
def on_enter(self) -> None:
"""Hook called when the scene becomes active.
Returns:
None
"""
pass
def on_exit(self) -> None:
"""Hook called when the scene is exited.
Returns:
None
"""
pass
def add_game_object(self, game_object: GameObject) -> GameObject:
"""Add an existing object to this scene.
Args:
game_object: The object to add.
Returns:
The same object, after being attached to the scene.
"""
game_object.scene = self
self.game_objects.append(game_object)
return game_object
def add_game_object_type(self, cls: Type[T], *args: Any, **kwargs: Any) -> T:
"""Create and add a new object of a given type.
Args:
cls: GameObject class to instantiate.
*args: Positional args forwarded to the constructor.
**kwargs: Keyword args forwarded to the constructor.
Returns:
The newly created object.
"""
game_object = cls(*args, **kwargs)
self.add_game_object(game_object)
return game_object
def add_service(self, service_or_cls: Any, *args: Any, **kwargs: Any) -> Service:
"""Add a service instance or construct one from a class.
Args:
service_or_cls: A Service instance or a Service class.
*args: Positional args forwarded to the constructor.
**kwargs: Keyword args forwarded to the constructor.
Returns:
The service instance added.
"""
if isinstance(service_or_cls, Service):
service = service_or_cls
else:
service = service_or_cls(*args, **kwargs)
service.scene = self
key = service.__class__
for svc_key, _ in self.services:
if svc_key == key:
print(f"Duplicate service added: {key.__name__}")
return service
self.services.append((key, service))
return service
def get_service(self, cls: Type[T]) -> T:
"""Get a service by type.
Args:
cls: Service class to look up.
Returns:
The service instance.
Raises:
RuntimeError: If no matching service exists.
"""
for svc_key, svc in self.services:
if svc_key == cls:
if not svc.is_init:
print(f"Service not initialized: {cls.__name__}")
return svc # type: ignore[return-value]
print(f"Service of requested type not found in scene: {cls.__name__}")
raise RuntimeError(f"Service not found: {cls.__name__}")
def get_game_objects_with_tag(self, tag: str) -> List[GameObject]:
"""Get all objects that contain a tag.
Args:
tag: Tag string to match.
Returns:
List of matching game objects.
"""
return [obj for obj in self.game_objects if obj.has_tag(tag)]
class Game:
"""Main game class that owns managers and scenes.
Attributes:
managers: Mapping of manager type to instance.
scenes: Mapping of scene name to instance.
scene_order: Ordered list of scene names.
current_scene: Active scene.
next_scene: Scene queued for transition.
"""
def __init__(self) -> None:
self.managers: Dict[Type[Any], Manager] = {}
self.scenes: Dict[str, Scene] = {}
self.scene_order: List[str] = []
self.current_scene: Optional[Scene] = None
self.next_scene: Optional[Scene] = None
def init(self) -> None:
"""Initialize all managers.
Returns:
None
"""
for manager in self.managers.values():
manager.init_manager()
def update(self, delta_time: float) -> None:
"""Update the active scene and render it.
Args:
delta_time: Seconds since the last frame.
Returns:
None
"""
if self.current_scene:
self.current_scene.init_scene()
self.current_scene.update_scene(delta_time)
rl.begin_drawing()
rl.clear_background(rl.RAYWHITE)
self.current_scene.draw_scene()
rl.end_drawing()
if self.next_scene:
if self.current_scene:
self.current_scene.on_exit()
self.current_scene = self.next_scene
self.current_scene.on_enter()
self.next_scene = None
def add_manager(self, manager_or_cls: Any, *args: Any, **kwargs: Any) -> Manager:
"""Add a manager instance or construct one from a class.
Args:
manager_or_cls: A Manager instance or a Manager class.
*args: Positional args forwarded to the constructor.
**kwargs: Keyword args forwarded to the constructor.
Returns:
The manager instance added.
"""
if isinstance(manager_or_cls, Manager):
manager = manager_or_cls
else:
manager = manager_or_cls(*args, **kwargs)
key = manager.__class__
if key in self.managers:
print(f"Duplicate manager added: {key.__name__}")
self.managers[key] = manager
return manager
def get_manager(self, cls: Type[T]) -> T:
"""Get a manager by type.
Args:
cls: Manager class to look up.
Returns:
The manager instance.
Raises:
RuntimeError: If no matching manager exists.
"""
manager = self.managers.get(cls)
if manager is None:
print(f"Manager of requested type not found: {cls.__name__}")
raise RuntimeError(f"Manager not found: {cls.__name__}")
if not manager.is_init:
print(f"Manager not initialized: {cls.__name__}")
return manager # type: ignore[return-value]
def add_scene(self, name: str, scene_or_cls: Any, *args: Any, **kwargs: Any) -> Scene:
"""Add a scene instance or construct one from a class.
Args:
name: Name to register the scene under.
scene_or_cls: A Scene instance or a Scene class.
*args: Positional args forwarded to the constructor.
**kwargs: Keyword args forwarded to the constructor.
Returns:
The scene instance added.
"""
if isinstance(scene_or_cls, Scene):
scene = scene_or_cls
else:
scene = scene_or_cls(*args, **kwargs)
self.scenes[name] = scene
scene.game = self
self.scene_order.append(name)
if not self.current_scene:
self.current_scene = scene
return scene
def go_to_scene(self, name: str) -> Optional[Scene]:
"""Queue a transition to a named scene.
Args:
name: Registered name of the scene.
Returns:
The target scene if found, otherwise None.
"""
scene = self.scenes.get(name)
if not scene:
print(f"Scene not found: {name}")
return None
self.next_scene = scene
return scene
def go_to_scene_next(self) -> Optional[Scene]:
"""Queue a transition to the next scene in order.
Returns:
The next scene if one is available, otherwise None.
"""
if not self.current_scene:
return None
current_name = None
for name, scene in self.scenes.items():
if scene == self.current_scene:
current_name = name
break
if current_name is None:
return None
if current_name in self.scene_order:
idx = self.scene_order.index(current_name)
if idx + 1 < len(self.scene_order):
next_name = self.scene_order[idx + 1]
else:
next_name = self.scene_order[0]
self.next_scene = self.scenes[next_name]
return self.next_scene