Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New subsystem API #221

Merged
merged 7 commits into from
Apr 8, 2019
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions ppb/engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ def __init__(self, first_scene: Type, *,
self.event_extensions = defaultdict(dict)
self.running = False
self.entered = False
self.last_idle_event = None
pathunstrom marked this conversation as resolved.
Show resolved Hide resolved

# Systems
self.systems_classes = systems
Expand Down Expand Up @@ -80,17 +81,18 @@ def run(self):

def start(self):
self.running = True
self.last_idle_event = time.monotonic()
self.activate({"scene_class": self.first_scene,
"kwargs": self.scene_kwargs})

def main_loop(self):
while self.running:
time.sleep(0)
for system in self.systems:
for event in system.activate(self):
self.signal(event)
while self.events:
self.publish()
now = time.monotonic()
self.signal(events.Idle(now - self.last_idle_event))
self.last_idle_event = now
while self.events:
self.publish()
self.manage_scene()

def activate(self, next_scene: dict):
Expand Down
9 changes: 9 additions & 0 deletions ppb/events.py
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,15 @@ class StopScene:
scene: Scene = None


@dataclass
pathunstrom marked this conversation as resolved.
Show resolved Hide resolved
class Idle:
"""
An engine plumbing event to pump timing information to subsystems.
"""
time_delta: float
scene: Scene = None


@dataclass
class Update:
"""
Expand Down
35 changes: 23 additions & 12 deletions ppb/systems/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,23 +20,24 @@ def __enter__(self):
def __exit__(self, exc_type, exc_val, exc_tb):
pass

def activate(self, engine):
return []


from ppb.systems.pg import EventPoller as PygameEventPoller # To not break old imports.


class Renderer(System):

def __init__(self, resolution=default_resolution, window_title: str="PursuedPyBear", **kwargs):
def __init__(self, resolution=default_resolution, window_title: str="PursuedPyBear", target_frame_rate: int=30, **kwargs):
self.resolution = resolution
self.resources = {}
self.window = None
self.window_title = window_title
self.pixel_ratio = None
self.resized_images = {}
self.old_resized_images = {}
self.render_clock = 0
self.render_ready = False
self.target_frame_rate = target_frame_rate
self.target_count = 1 / self.target_frame_rate

def __enter__(self):
pygame.init()
Expand All @@ -46,9 +47,18 @@ def __enter__(self):
def __exit__(self, exc_type, exc_val, exc_tb):
pygame.quit()

def activate(self, engine):
yield events.PreRender()
yield events.Render()
def on_idle(self, idle_event: events.Idle, signal):
self.render_clock += idle_event.time_delta
if self.render_ready:
self.render_ready = False
signal(events.Render())
elif self.render_clock >= self.target_count:
self.render_clock = 0
signal(events.PreRender())

def on_pre_render(self, pre_render_event, signal):
# Here to let the system flush responses to PreRender before rendering.
self.render_ready = True

def on_render(self, render_event, signal):
self.render_background(render_event.scene)
Expand Down Expand Up @@ -139,14 +149,15 @@ def __init__(self, time_step=0.016, **kwargs):
self.time_step = time_step

def __enter__(self):
self.start_time = time.time()
self.start_time = time.monotonic()

def activate(self, engine):
def on_idle(self, idle_event: events.Idle, signal):
if self.last_tick is None:
self.last_tick = time.time()
this_tick = time.time()
self.last_tick = time.monotonic()
this_tick = time.monotonic()
self.accumulated_time += this_tick - self.last_tick
self.last_tick = this_tick
while self.accumulated_time >= self.time_step:
# This might need to change for the Idle event system to signal _only_ once per idle event.
self.accumulated_time += -self.time_step
yield events.Update(self.time_step)
signal(events.Update(self.time_step))
4 changes: 2 additions & 2 deletions ppb/systems/pg.py
Original file line number Diff line number Diff line change
Expand Up @@ -155,11 +155,11 @@ def __enter__(self):
def __exit__(self, exc_type, exc_val, exc_tb):
pygame.quit()

def on_update(self, update, signal):
def on_idle(self, idle: events.Idle, signal):
for pygame_event in pygame.event.get():
methname = self.event_map.get(pygame_event.type)
if methname is not None: # Is there a handler for this pygame event?
ppbevent = getattr(self, methname)(pygame_event, update.scene)
ppbevent = getattr(self, methname)(pygame_event, idle.scene)
if ppbevent: # Did the handler actually produce a ppb event?
signal(ppbevent)

Expand Down
14 changes: 7 additions & 7 deletions ppb/testutils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,27 @@
from typing import Callable

from ppb.engine import GameEngine
from ppb.events import Idle
from ppb.events import Quit
from ppb.systems import System


class Failer(System):

def __init__(self, *, fail: Callable[[GameEngine], bool], message: str,
run_time: float=1, **kwargs):
run_time: float=1, engine, **kwargs):
super().__init__(**kwargs)
self.fail = fail
self.message = message
self.start = time.monotonic()
self.run_time = run_time
self.engine = engine

def activate(self, engine):
def on_idle(self, idle_event: Idle, signal):
if time.monotonic() - self.start > self.run_time:
raise AssertionError("Test ran too long.")
if self.fail(engine):
if self.fail(self.engine):
raise AssertionError(self.message)
return ()



class Quitter(System):
Expand All @@ -35,7 +35,7 @@ def __init__(self, loop_count=1, **kwargs):
self.counter = 0
self.loop_count = loop_count

def activate(self, engine):
def on_idle(self, idle_event: Idle, signal):
self.counter += 1
if self.counter >= self.loop_count:
yield Quit()
signal(Quit())
33 changes: 25 additions & 8 deletions tests/test_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,8 +98,9 @@ def fail(engine):
if parent.count > 0 and engine.current_scene != parent:
return True

failer = Failer(fail=fail, message="ParentScene should not be counting while a child exists.")
engine = GameEngine(ParentScene, systems=[Updater(time_step=0.001), failer])
engine = GameEngine(ParentScene,
systems=[Updater(time_step=0.001), Failer], fail=fail,
message="ParentScene should not be counting while a child exists.")
engine.run()


Expand All @@ -114,8 +115,8 @@ def __init__(self, *args, **kwargs):
def change(self):
return super().change()

failer = Failer(fail=lambda n: False, message="Will only time out.")
with GameEngine(Scene, systems=[Updater, failer]) as ge:
with GameEngine(Scene, systems=[Updater, Failer], fail=lambda n: False,
message="Will only time out.") as ge:
ge.run()


Expand Down Expand Up @@ -175,7 +176,8 @@ def on_scene_started(self, event, signal):
class Tester(System):
listening = False

def activate(self, engine):
def on_idle(self, idle: events.Idle, signal):
engine = idle.engine
if self.listening:
assert isinstance(engine.current_scene, SecondScene)
assert len(engine.scenes) == 2
Expand All @@ -185,6 +187,7 @@ def on_scene_paused(self, event, signal):
self.listening = True

with GameEngine(FirstScene, systems=[Updater, Tester]) as ge:
ge.register(events.Idle, "engine", ge)
ge.run()

pause_was_run.assert_called()
Expand All @@ -209,7 +212,7 @@ def on_scene_started(self, event, signal):
class TestFailer(Failer):

def __init__(self, engine):
super().__init__(fail=self.fail, message="Will not call")
super().__init__(fail=self.fail, message="Will not call", engine=engine)
self.first_scene_ended = False

def on_scene_stopped(self, event, signal):
Expand Down Expand Up @@ -240,8 +243,7 @@ def on_scene_stopped(self, event, signal):
assert event.scene is self
test_function()

failer = Failer(fail=lambda x: False, message="Will only time out.")
with GameEngine(TestScene, systems=[Updater, failer]) as ge:
with GameEngine(TestScene, systems=[Updater, Failer], fail=lambda x: False, message="Will only time out.") as ge:
ge.run()

test_function.assert_called()
Expand All @@ -258,3 +260,18 @@ def test_flush_events():
ge.flush_events()

assert len(ge.events) == 0


def test_idle():
"""This test confirms that Idle events work."""
was_called = False

class TestSystem(System):

def on_idle(self, event: events.Idle, signal):
global was_called
was_called = True
signal(events.Quit())

with GameEngine(BaseScene, systems=[TestSystem, Failer], fail=lambda x: False, message="Can only time out.") as ge:
ge.run()
22 changes: 13 additions & 9 deletions tests/test_testutil.py
Original file line number Diff line number Diff line change
@@ -1,37 +1,41 @@
from time import monotonic
from unittest.mock import Mock

from pytest import mark
from pytest import raises

import ppb.testutils as testutil
from ppb.events import Idle
from ppb.events import Quit


@mark.parametrize("loop_count", range(1, 6))
def test_quitter(loop_count):
quitter = testutil.Quitter(loop_count=loop_count)
for _ in range(loop_count):
for e in quitter.activate(None): # Quitter doesn't need access to the engine, so we can pass None here.
if isinstance(e, Quit):
return
raise AssertionError("Quitter did not raise a quit event.")
signal_mock = Mock()
for i in range(loop_count):
quitter.__event__(Idle(.01), signal_mock)
signal_mock.assert_called_once()
assert len(signal_mock.call_args[0]) == 1
assert len(signal_mock.call_args[1]) == 0
assert isinstance(signal_mock.call_args[0][0], Quit)


def test_failer_immediate():
failer = testutil.Failer(fail=lambda e: True, message="Expected failure.")
failer = testutil.Failer(fail=lambda e: True, message="Expected failure.", engine=None)

with raises(AssertionError):
failer.activate(None)
failer.__event__(Idle(0.0), lambda x: None)


def test_failer_timed():
failer = testutil.Failer(fail=lambda e: False, message="Should time out", run_time=0.1)
failer = testutil.Failer(fail=lambda e: False, message="Should time out", run_time=0.1, engine=None)

start_time = monotonic()

while True:
try:
failer.activate(None)
failer.__event__(Idle(0.0), lambda x: None)
except AssertionError as e:
if e.args[0] == "Test ran too long.":
end_time = monotonic()
Expand Down