From 26eb57c61dd91ba69e2ca9e2500630c24b4aa792 Mon Sep 17 00:00:00 2001 From: Andrew Kesterson Date: Sat, 14 Dec 2013 22:54:21 -0500 Subject: [PATCH] Initial work, pong paddles move, lots of the groundwork for multiplayer or alternate displays is already done --- actor.py | 54 ++++++++++++++++++++++++ common.py | 11 +++++ display.py | 115 +++++++++++++++++++++++++++++++++++++++++++++++++++ event.py | 21 ++++++++++ game.py | 111 +++++++++++++++++++++++++++++++++++++++++++++++++ harnesses.py | 9 ++++ multipong.py | 51 +++++++++++++++++++++++ player.py | 10 +++++ registry.py | 63 ++++++++++++++++++++++++++++ 9 files changed, 445 insertions(+) create mode 100755 actor.py create mode 100755 common.py create mode 100755 display.py create mode 100755 event.py create mode 100755 game.py create mode 100755 harnesses.py create mode 100755 multipong.py create mode 100755 player.py create mode 100755 registry.py diff --git a/actor.py b/actor.py new file mode 100755 index 0000000..5c9f6cc --- /dev/null +++ b/actor.py @@ -0,0 +1,54 @@ +from common import * +import event +import display +import uuid +import registry +import dpath.util +import logging +logger = logging.getLogger() + +class Actor(event.EventHandler, registry.Registerable): + def __init__(self, *args, **kwargs): + registry.Registerable.__init__(self, *args, **kwargs) + event.EventHandler.__init__(self, *args, **kwargs) + self.x = 0 + self.y = 0 + self.vx = 0 + self.vy = 0 + dpath.util.merge( + self.__eventHandlers__, + { + 'moveUp': self._event_moveUp, + 'moveDown': self._event_moveDown + } + ) + + def _event_moveUp(self, evt): + self.y -= 1 + + def _event_moveDown(self, evt): + self.y += 1 + + def frame(self, display): + return getattr( + self, + "frameFor{}".format(display.__class__.__name__) + )() + +class Text(Actor): + def __init__(self, *args, **kwargs): + Actor.__init__(self, *args, **kwargs) + self.__text__ = kwargs.get('text', "") + + def setText(self, text): + self.__text__ = text + + def frameForCursesDisplay(self): + def drawForCurses(disp): + disp.__screen__.addstr(self.y, self.x, self.__text__) + return drawForCurses + +class Paddle(Text): + def __init__(self, *args, **kwargs): + Text.__init__(self, *args, **kwargs) + self.__text__ = "|" diff --git a/common.py b/common.py new file mode 100755 index 0000000..ace7f0d --- /dev/null +++ b/common.py @@ -0,0 +1,11 @@ +import curses +import Queue +import argparse +import sys +import logging +import traceback +import json +import threading + +FRAMERATE=1.0/60.0 + diff --git a/display.py b/display.py new file mode 100755 index 0000000..cd76556 --- /dev/null +++ b/display.py @@ -0,0 +1,115 @@ +from common import * +import event +import game +import registry + +class Display(event.EventHandler, registry.Registerable): + # --- Static methods + + @staticmethod + def CurrentDisplay(): + return getattr(Display, '__currentDisplay__') + + @staticmethod + def NewDisplay(displayType): + display = globals()["{}Display".format(displayType.title())]() + setattr(Display, '__currentDisplay__', display) + return display + + @staticmethod + def StaticRefresh(): + display = Display.CurrentDisplay() + display.refresh() + + # --- Member methods + def __init__(self, *args, **kwargs): + registry.Registerable.__init__(self, *args, **kwargs) + event.EventHandler.__init__(self, *args, **kwargs) + self.__lock_actors__ = threading.RLock() + self.__lock_timer__ = threading.RLock() + self.__lock_drawing__ = threading.RLock() + self.__actors__ = {} + self.__setDrawTimer__() + + def __setDrawTimer__(self): + self.__drawTimer__ = threading.Timer(FRAMERATE, Display.StaticRefresh) + self.__drawTimer__.start() + + def cleanup(self): + self.__drawTimer__.cancel() + + # --- + + def checkInput(self): + raise Exception("lol implement me") + + def refresh(self): + try: + with self.__lock_drawing__: + self.__screen__.clear() + for actor in self.__actors__.values(): + self.drawActor(actor) + self.__setDrawTimer__() + except Exception, e: + game.Game.CurrentGame().addEvent(('endGame', traceback.format_exc())) + + def addActor(self, actor): + logger = logging.getLogger() + with self.__lock_actors__: + if not actor.uuid in self.__actors__.keys(): + logger.debug("Added actor {}".format(actor.uuid)) + self.__actors__[actor.uuid] = actor + + def delActor(self, actor): + with self.__lock_actors__: + try: + del(self.__actors__, actor.uuid) + except KeyError, e: + return + +class CursesDisplay(Display): + def __init__(self, *args, **kwargs): + self.logger = logging.getLogger() + self.logger.info("CursesDisplay starting") + Display.__init__(self, *args, **kwargs) + self.__screen__ = curses.initscr() + self.__screen__.keypad(1) + try: + curses.curs_set(0) + except: + logger.warn("I couldn't make the cursor invisible - I'm sorry") + curses.noecho() + curses.cbreak() + + def cleanup(self): + self.logger.info("CursesDisplay cleaning up") + curses.nocbreak() + self.__screen__.keypad(0) + curses.echo() + curses.endwin() + Display.cleanup(self) + + def refresh(self): + Display.refresh(self) + self.__screen__.refresh() + + def checkInput(self): + key = self.__screen__.getch() + if key == ord('q'): + raise event.EventBubble(('endGame', + 'User terminated the game', + '/Game/uuid/*')) + elif key == ord('w'): + raise event.EventBubble(('moveUp', '', '/Player/cn/player1')) + elif key == ord('s'): + raise event.EventBubble(('moveDown', '', '/Player/cn/player1')) + elif key == curses.KEY_UP: + raise event.EventBubble(('moveUp', '', '/Player/cn/player2')) + elif key == curses.KEY_DOWN: + raise event.EventBubble(('moveDown', '', '/Player/cn/player2')) + + def drawActor(self, actor): + # Curses displays have it easy; actors return us a function to call + # with a reference to ourselves, and they draw on us. + actor.frame(self)(self) + diff --git a/event.py b/event.py new file mode 100755 index 0000000..22b23f5 --- /dev/null +++ b/event.py @@ -0,0 +1,21 @@ +from common import * + +# Raise an EventBubble with a tuple(eventName, eventMessage) to post a new +# message to the queue. If you bubble a list of tuples - [(), (), ()] - then +# each tuple in the list of events is added to the queue. +class EventBubble(Exception): + pass + +# Events are just tuples of (eventName, eventMessage), where eventMessage +# can be any object appropriate for eventName +class EventHandler: + def __init__(self, *args, **kwargs): + self.__eventHandlers__ = {} + + def handleEvent(self, event): + self.__eventHandlers__[event[0]](event[1]) + + def bindEvent(self, eventName, function): + self.__eventHandlers__ = getattr(self, "__eventHandlers__", {}) + self.__eventHandlers__[eventName] = function + diff --git a/game.py b/game.py new file mode 100755 index 0000000..e506866 --- /dev/null +++ b/game.py @@ -0,0 +1,111 @@ +from common import * +import event +import display +import threading +import actor +import player +import registry +import dpath.util + +class Game(event.EventHandler, registry.Registerable): + + # ---- Static methods + + # ---- This is largely unnecessary now that events are handled via exception bubbling + @staticmethod + def CurrentGame(): + return Game.__currentGame__ + + @staticmethod + def NewGame(flags): + if hasattr(Game, '__currentGame__'): + raise AttributeError("Attempted to create a new game while one was already in progress") + if flags['type'] == 'server': + game = ServerHost(display=flags['display']) + elif flags['hostname']: + game = ServerGame(display=flags['display'], hostname=flags['hostname']) + else: + game = LocalGame(display=flags['display']) + setattr(Game, '__currentGame__', game) + return game + + # ---- Member methods + + def __init__(self, *args, **kwargs): + registry.Registerable.__init__(self, *args, **kwargs) + event.EventHandler.__init__(self, *args, **kwargs) + self.__eventQueue__ = Queue.Queue() + self.__display__ = display.Display.NewDisplay(kwargs['display']) + self.__players__ = [] + self.gameRunning = True + logger = logging.getLogger() + logger.info("Display is: {}".format(str(self.__display__))) + + self.__players__.append(player.Player(registryKey='player1')) + self.__players__.append(player.Player(registryKey='player2')) + self.__players__[1].x = 25 + logger = logging.getLogger() + logger.debug("Adding actors {} and {}".format(self.__players__[0].uuid, self.__players__[1].uuid)) + self.__display__.addActor(self.__players__[0]) + self.__display__.addActor(self.__players__[1]) + + def eventLoop(self): + reg = registry.Registry.GetRegistry() + logger = logging.getLogger() + logger.info("gameRunning: {}".format(self.gameRunning)) + while self.gameRunning: + evlist = [] + try: + try: + evlist = self.__eventQueue__.get_nowait() + except Queue.Empty, e: + pass + self.__display__.checkInput() + except event.EventBubble, e: + evlist = e.message + if not evlist: + continue + if not isinstance(evlist, list): + evlist = [evlist] + for ev in evlist: + if len(ev) < 2: + logger.error("Malformed event : {}".format(ev)) + target = "/*/uuid/*" + if len(ev) >= 3: + target = ev[2] + logger.debug("Event: {} => {}".format(ev[0], target)) + for line in str(ev[1]).split('\n'): + logger.debug(" {}".format(line)) + # Targeted event? + for pair in dpath.util.search(reg, target, yielded=True): + logger.debug(" processed by {}".format(pair[0])) + pair[1].handleEvent(ev) + + def handleEvent(self, ev): + if ev[0] == 'endGame': + self.gameRunning = False + + def addEvent(self, event): + self.__eventQueue__.put(event) + + def cleanup(self): + self.__display__.cleanup() + for player in self.__players__: + player.cleanup() + +class LocalGame(Game): + def __init__(self, *args, **kwargs): + Game.__init__(self, *args, **kwargs) + + def handleEvent(self, ev): + Game.handleEvent(self, ev) + + +class ServerHost(Game): + def __init__(self, *args, **kwargs): + Game.__init__(self, *args, **kwargs) + +class ServerGame(Game): + def __init__(self, *args, **kwargs): + Game.__init__(self, *args, **kwargs) + diff --git a/harnesses.py b/harnesses.py new file mode 100755 index 0000000..5903d02 --- /dev/null +++ b/harnesses.py @@ -0,0 +1,9 @@ +from common import * +import actor +import display + +def textActor(): + ta = actor.Text(text="lolzors") + ta.x = 10 + ta.y = 10 + display.Display.CurrentDisplay().addActor(ta) diff --git a/multipong.py b/multipong.py new file mode 100755 index 0000000..57847cb --- /dev/null +++ b/multipong.py @@ -0,0 +1,51 @@ +from common import * +import event +import display +import player +import game + +def parse(): + parser = argparse.ArgumentParser(description='A multiplayer pong clone') + parser.add_argument('-H', '--hostname', help='The hostname of a server to connect to', required=False) + parser.add_argument('-s', '--server', help='Start the game in server mode for clients to connect', required=False) + parser.add_argument('-S', '--speed', help='The starting speed of the ping pong ball', required=False) + parser.add_argument('-d', '--display', help='The display type to use (curses)', required=False, default='curses') + parser.add_argument('-l', '--loglevel', help='The loglevel (DEBUG|INFO|WARNING|ERROR|CRITICAL)', required=False, default='INFO') + return parser.parse_args() + +def main(): + args = parse() + flags = {'type': 'local', 'hostname': '', 'display': args.display} + if args.server: + flags['type'] = 'server' + else: + flags['type'] = 'local' + if args.hostname: + flags['type'] = 'remote' + flags['hostname'] = str(args.hostname) + FORMAT = '%(asctime)-15s %(message)s' + logging.basicConfig(filename="logfile.txt", + format="%(asctime)s [%(levelname)s] %(message)s", + level=getattr(logging, args.loglevel)) + logger = logging.getLogger() + logger.info("Starting:") + for line in json.dumps(flags, indent=4, sort_keys=True).split('\n'): + logger.info(line) + + pong = None + try: + pong = game.Game.NewGame(flags) + pong.eventLoop() + except Exception, e: + for line in traceback.format_exc().split('\n'): + logger.error(line) + logger.info("Cleaning up ...") + try: + pong.cleanup() + except Exception, e: + for line in traceback.format_exc().split('\n'): + logger.error(line) + return 0 + +if __name__ == "__main__": + sys.exit(main()) diff --git a/player.py b/player.py new file mode 100755 index 0000000..1c28f88 --- /dev/null +++ b/player.py @@ -0,0 +1,10 @@ +from common import * +import event +import actor + +class Player(actor.Paddle, event.EventHandler): + def __init__(self, *args, **kwargs): + actor.Paddle.__init__(self, *args, **kwargs) + + def cleanup(self): + raise Exception("lol implement me") diff --git a/registry.py b/registry.py new file mode 100755 index 0000000..072d36c --- /dev/null +++ b/registry.py @@ -0,0 +1,63 @@ +from common import * +import dpath.util +import uuid +import pprint + +class Registry(dict): + @staticmethod + def GetRegistry(): + try: + ret = getattr(Registry, '__curRegistry__') + except AttributeError, e: + ret = Registry() + setattr(Registry, '__curRegistry__', ret) + return ret + + def __init__(self, *args, **kwargs): + dict.__init__(self, *args, **kwargs) + # Circular dependencies, ye are my bane + import event + import display + import actor + import game + import player + # -- + self.__validClasses__ = ( + event.EventHandler, + actor.Actor, + player.Player, + game.Game, + display.Display + ) + + def search(self, path): + res = [] + for item in dpath.util.search(self, path, yielded=True): + res.append(item) + return res + + def register(self, value, key = None): + logger = logging.getLogger() + if not issubclass(value.__class__, self.__validClasses__): + raise TypeError("Registry only accepts objects of type: {}" + "".format(self.__validClasses__)) + for baseClass in self.__validClasses__: + if issubclass(value.__class__, baseClass): + cn = baseClass.__name__ + ouuid = uuid.uuid4() + uuidpath = "/{}/uuid/{}".format(cn, ouuid) + cnpath = "/{}/cn/{}".format(cn, key) + dpath.util.new(self, uuidpath, value) + if key: + dpath.util.new(self, cnpath, value) + logger.debug("Registered {} as {} | {}".format(value, uuidpath, cnpath)) + for line in pprint.pformat(self, indent=4).split('\n'): + logger.debug("registry: {}".format(line)) + return ouuid + +class Registerable: + def __init__(self, *args, **kwargs): + self.uuid = Registry.GetRegistry().register( + self, + kwargs.get('registryKey', None) + )