Skip to content

Commit 8179b43

Browse files
committed
First pass event loop implementation.
1 parent c6d16bd commit 8179b43

File tree

21 files changed

+753
-274
lines changed

21 files changed

+753
-274
lines changed

sc2reader/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import sys
66

77
# import submodules
8-
from sc2reader import plugins, data, scripts
8+
from sc2reader import engine
99
from sc2reader import factories, log_utils
1010

1111
# setup the library logging

sc2reader/engine/__init__.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from __future__ import absolute_import
2+
3+
import sys
4+
from sc2reader.engine.engine import GameEngine
5+
from sc2reader.engine.plugins.apm import APMTracker
6+
from sc2reader.engine.plugins.selection import SelectionTracker
7+
from sc2reader.engine.plugins.context import ContextLoader
8+
9+
def setGameEngine(engine):
10+
module = sys.modules[__name__]
11+
module.run = engine.run
12+
module.register_plugin = engine.register_plugin
13+
module.register_plugins = engine.register_plugins
14+
15+
_default_engine = GameEngine()
16+
_default_engine.register_plugin(ContextLoader())
17+
setGameEngine(_default_engine)

sc2reader/engine/engine.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
from __future__ import absolute_import
2+
3+
import collections
4+
from sc2reader.events import *
5+
6+
class InitGameEvent(object):
7+
name = 'InitGame'
8+
9+
class EndGameEvent(object):
10+
name = 'EndGame'
11+
12+
class GameEngine(object):
13+
""" GameEngine Specification
14+
--------------------------
15+
16+
The game engine runs through all the events for a given replay in
17+
chronological order. For each event, event handlers from registered
18+
plugins are executed in order of plugin registration from most general
19+
to most specific.
20+
21+
Example Usage::
22+
23+
class Plugin1():
24+
def handleAbilityEvent(self, event, replay):
25+
pass
26+
27+
class Plugin2():
28+
def handleEvent(self, event, replay):
29+
pass
30+
31+
def handleTargetAbilityEvent(self, event, replay):
32+
pass
33+
34+
...
35+
36+
engine = GameEngine(plugins=[Plugin1(), Plugin2()], **options)
37+
engine.register_plugins(Plugin3(), Plugin(4))
38+
engine.reigster_plugin(Plugin(5))
39+
engine.run(replay)
40+
41+
Calls functions in the following order for a ``TargetUnitEvent``::
42+
43+
Plugin1.handleAbilityEvent(replay, event)
44+
Plugin2.handleEvent(replay, event)
45+
Plugin2.handleTargetAbilityEvent(replay, event)
46+
47+
48+
Plugin Specification
49+
-------------------------
50+
51+
Plugins can opt in to handle events with methods in the format:
52+
53+
def handleEventName(self, event, replay)
54+
55+
In addition to handling specific event types, plugins can also
56+
handle events more generally by handling built-in parent classes
57+
from the list below::
58+
59+
* handleEvent - called for every single event of all types
60+
* handleMessageEvent - called for events in replay.message.events
61+
* handleGameEvent - called for events in replay.game.events
62+
* handleTrackerEvent - called for events in replay.tracker.events
63+
* handlePlayerActionEvent - called for all game events indicating player actions
64+
* handleAbilityEvent - called for all types of ability events
65+
* handleHotkeyEvent - called for all player hotkey events
66+
67+
Plugins may also handle optional InitGame and EndGame events generated
68+
by the GameEngine before and after processing all the events:
69+
70+
* handleInitGame - is called prior to processing a new replay to provide
71+
an opportunity for the plugin to clear internal state and set up any
72+
replay state necessary.
73+
74+
* handleEndGame - is called after all events have been processed and
75+
can be used to perform post processing on aggrated data or clean up
76+
intermediate data caches.
77+
78+
Event handlers can choose to ``yield`` additional events which will be injected
79+
into the event stream directly after the event currently being processed. This is
80+
a great way to send messages to downstream plugins.
81+
"""
82+
def __init__(self, plugins=[]):
83+
self._plugins = list()
84+
self.register_plugins(*plugins)
85+
86+
def register_plugin(self, plugin):
87+
self._plugins.append(plugin)
88+
89+
def register_plugins(self, *plugins):
90+
for plugin in plugins:
91+
self.register_plugin(plugin)
92+
93+
def run(self, replay):
94+
# A map of [event.name] => event handlers in plugin registration order
95+
# ranked from most generic to most specific
96+
handlers = dict()
97+
98+
# Fill event event queue with the replay events, bookmarked by Init and End events.
99+
event_queue = collections.deque()
100+
event_queue.append(InitGameEvent())
101+
event_queue.extend(replay.events)
102+
event_queue.append(EndGameEvent())
103+
104+
# Work through the events in the queue, pushing newly emitted events to
105+
# the front of the line for immediate processing.
106+
while len(event_queue) > 0:
107+
event = event_queue.popleft()
108+
109+
# If we haven't compiled a list of handlers for this event yet, do so!
110+
if event.name not in handlers:
111+
event_handlers = self._get_event_handlers(event)
112+
handlers[event.name] = event_handlers
113+
else:
114+
event_handlers = handlers[event.name]
115+
116+
# Events have the option of yielding one or more additional events
117+
# which get processed after the current event finishes.
118+
new_events = list()
119+
for event_handler in event_handlers:
120+
new_events.extend(event_handler(event, replay) or [])
121+
122+
# extendleft does a series of appendlefts and reverses the order so we
123+
# need to reverse the list first to have them added in order.
124+
event_queue.extendleft(new_events)
125+
126+
127+
def _get_event_handlers(self, event):
128+
return sum([self._get_plugin_event_handlers(plugin, event) for plugin in self._plugins],[])
129+
130+
def _get_plugin_event_handlers(self, plugin, event):
131+
handlers = list()
132+
if isinstance(event, Event) and self._has_event_handler(plugin, Event):
133+
handlers.append(self._get_event_handler(plugin,Event))
134+
if isinstance(event, MessageEvent) and self._has_event_handler(plugin, MessageEvent):
135+
handlers.append(self._get_event_handler(plugin,MessageEvent))
136+
if isinstance(event, GameEvent) and self._has_event_handler(plugin, GameEvent):
137+
handlers.append(self._get_event_handler(plugin,GameEvent))
138+
if isinstance(event, TrackerEvent) and self._has_event_handler(plugin, TrackerEvent):
139+
handlers.append(self._get_event_handler(plugin,TrackerEvent))
140+
if isinstance(event, PlayerActionEvent) and self._has_event_handler(plugin, PlayerActionEvent):
141+
handlers.append(self._get_event_handler(plugin,PlayerActionEvent))
142+
if isinstance(event, AbilityEvent) and self._has_event_handler(plugin, AbilityEvent):
143+
handlers.append(self._get_event_handler(plugin,AbilityEvent))
144+
if isinstance(event, HotkeyEvent) and self._has_event_handler(plugin, HotkeyEvent):
145+
handlers.append(self._get_event_handler(plugin,HotkeyEvent))
146+
if self._has_event_handler(plugin, event):
147+
handlers.append(self._get_event_handler(plugin,event))
148+
return handlers
149+
150+
def _has_event_handler(self, plugin, event):
151+
return hasattr(plugin, 'handle'+event.name)
152+
153+
def _get_event_handler(self, plugin, event):
154+
return getattr(plugin, 'handle'+event.name, None)
155+
156+
157+
if __name__ == '__main__':
158+
from sc2reader.events import UserOptionsEvent, GameStartEvent, PlayerLeaveEvent
159+
class TestEvent(object):
160+
name = 'TestEvent'
161+
def __init__(self, source):
162+
self.source = source
163+
164+
class TestPlugin(object):
165+
yields = TestEvent
166+
167+
def handleInitGame(self, event, replay,):
168+
yield TestEvent(event.name)
169+
170+
def handleTestEvent(self, event, replay):
171+
print event.source
172+
173+
def handleGameStartEvent(self, event, replay):
174+
yield TestEvent(event.name)
175+
176+
def handleEndGame(self, event, replay):
177+
yield TestEvent(event.name)
178+
179+
class TestReplay(object):
180+
events = [UserOptionsEvent, UserOptionsEvent, GameStartEvent, PlayerLeaveEvent]
181+
182+
engine = GameEngine()
183+
engine.register_plugin(TestPlugin())
184+
events = engine.run(TestReplay)

sc2reader/engine/plugins/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from __future__ import absolute_import
2+
3+
from sc2reader.engine.plugins.apm import APMTracker
4+
from sc2reader.engine.plugins.selection import SelectionTracker
5+
from sc2reader.engine.plugins.context import ContextLoader

sc2reader/engine/plugins/apm.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
from collections import Counter
2+
3+
class APMTracker(object):
4+
5+
def handleInitGame(self, event, replay):
6+
for player in replay.players:
7+
player.apm = Counter()
8+
player.aps = Counter()
9+
player.seconds_played = replay.length.seconds
10+
11+
def handlePlayerActionEvent(self, event, replay):
12+
event.player.aps[event.second] += 1
13+
event.player.apm[event.second/60] += 1
14+
15+
def handlePlayerLeaveEvent(self, event, replay):
16+
event.player.seconds_played = event.second
17+
18+
def handleEndGame(self, event, replay):
19+
print "Handling End Game"
20+
for player in replay.players:
21+
if len(player.apm.keys()) > 0:
22+
player.avg_apm = sum(player.apm.values())/float(player.seconds_played)*60
23+
else:
24+
player.avg_apm = 0

0 commit comments

Comments
 (0)