commit 26eb57c61dd91ba69e2ca9e2500630c24b4aa792 Author: Andrew Kesterson Date: Sat Dec 14 22:54:21 2013 -0500 Initial work, pong paddles move, lots of the groundwork for multiplayer or alternate displays is already done 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) + )