From 565b6ba903f7fe2e07ebe32076739839900845db Mon Sep 17 00:00:00 2001 From: Patrick Marsee Date: Fri, 18 Jan 2019 14:56:51 -0500 Subject: [PATCH] Initial commit --- .gitignore | 4 + gamebase.py | 628 ++++++++++++++++++++++++++++++++++++++++++ gameevents.py | 100 +++++++ gamegui.py | 439 +++++++++++++++++++++++++++++ gamemap.py | 688 ++++++++++++++++++++++++++++++++++++++++++++++ gameshell.py | 151 ++++++++++ shell.py | 106 +++++++ testing/test1.txt | 33 +++ testing/test2.txt | 35 +++ testing/test3.txt | 24 ++ testing/test4.txt | 32 +++ tile.py | 44 +++ 12 files changed, 2284 insertions(+) create mode 100644 .gitignore create mode 100644 gamebase.py create mode 100644 gameevents.py create mode 100644 gamegui.py create mode 100644 gamemap.py create mode 100644 gameshell.py create mode 100644 shell.py create mode 100644 testing/test1.txt create mode 100644 testing/test2.txt create mode 100644 testing/test3.txt create mode 100644 testing/test4.txt create mode 100644 tile.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..48bed84 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +maps +saves +__pycache__ +winsh.py diff --git a/gamebase.py b/gamebase.py new file mode 100644 index 0000000..28d63d3 --- /dev/null +++ b/gamebase.py @@ -0,0 +1,628 @@ +# gamebase.py + +import re as _re +import heapq as _hq +import gamemap as _gm +import gameevents as _ge +import random as _ra +import sys as _sys +import pickle as _pi + +class GameBase(object): + + coordRegex = _re.compile(r'((-?[a-zA-Z]+) ?(-?[0-9]+))|((-?[0-9]+),? (-?[0-9]+))|(\(([0-9]+), ([0-9]+)\))') + #coordRegex1 = _re.compile(r'(-?[a-zA-Z]+) ?(-?[0-9]+)') # "B12" or "B 12" + #coordRegex2 = _re.compile(r'\(([0-9]+), ([0-9]+)\)') # "(2, 12)" + #coordRegex3 = _re.compile(r'(-?[0-9]+),? (-?[0-9]+)') # "2 12" or "2, 12" + + def __init__(self): + self.outstream = _sys.stdout + self.__useFuncs = {} + self.__gameEvents = {} + self.customVals = {} # for setting flags and such + self.level = None + self.persist = {} # {level : {thingName : thing}} + self.ps2 = '? ' + self.eventQueue = [] + self.gameTime = 0.0 + + # player info + self.playerx = -1 + self.playery = -1 + self.prevx = -1 # prevx and prevy are for moving the player out of the way + self.prevy = -1 # when the tile they're standing on becomes blocked. + self.playerName = 'You' + self.playerInv = {} + + # function deligates + self.onLevelLoad = None # str : level name? -> None + self.onContainer = None # list : contents -> list : newContents, float : timeOpen + + # default events and useFuncs + self.registerEvent('noop', self.handleNoOp) + self.registerEvent('go', self.handleGo) + self.registerEvent('arrive', self.handleArrive) + self.registerEvent('use', self.handleUse) + self.registerEvent('useon', self.handleUseOn) + self.registerEvent('take', self.handleTake) + self.registerEvent('drop', self.handleDrop) + self.registerUseFunc('examine', self.examine) + self.registerUseFunc('key', self.key) + + # Helper functions + + def requestInput(self, prompt = ''): + """Like input by default, but should be overridden if you don't want +to get input from stdin.""" + return input(prompt) + + def letterToNumber(self, letter): + if letter.isdecimal(): + return int(letter) + ret = 0 + sign = 1 + start = 0 + for i in range(len(letter)): + if letter[i] == '-': + sign = -sign + start -= 1 + elif letter[i].isalpha(): + if letter[i].isupper(): + ret += (ord(letter[i]) - ord('A')) * (26**(i+start)) + else: + ret += (ord(letter[i]) - ord('a')) * (26**(i+start)) + else: + return ret * sign + return ret * sign + + def numberToLetter(self, number): + if isinstance(number, str): + return number + ret = '' + sign = '' + if number == 0: + return 'A' + elif number < 0: + sign = '-' + number = -number + while number > 0: + ret += chr(ord('A') + number % 26) + number = int(number / 26) + return sign + ret + + def parseCoords(self, args, usePlayerCoords = True, allowInventory = True): + """Takes an argument list, and figures out what it's talking about. +Returns (thing, x, y). "Thing" can be None.""" + coordStr = args[0] + if len(args) > 1: + coordStr += ' ' + args[1] + #print(coordStr) + x = -1 + y = -1 + match = GameBase.coordRegex.match(coordStr) + if match != None: # if coordinates were given + #print(match.group()) + groups = match.groups() + ind = 0 + for i in range(len(groups)): + if groups[i] == None or groups[i] == match.group(): + continue + else: + ind = i + break + #print(groups[ind], groups[ind+1]) + x = self.letterToNumber(groups[ind]) + y = self.letterToNumber(groups[ind+1]) + #print(x, y) + thing = self.level.getThingAtCoords(x, y) + if thing != None and usePlayerCoords: + return thing, thing.playerx, thing.playery + else: + return thing, x, y + else: # if a name was given + name = ' '.join(args) + thing = self.level.getThingByName(name) + if thing != None and usePlayerCoords: + return thing, thing.playerx, thing.playery + elif thing != None: + return thing, thing.x, thing.y + elif allowInventory: + if name in self.playerInv: + thing = self.playerInv[name] + if thing != None: + return thing, -1, -1 + else: + return None, -1, -1 + #raise RuntimeError("'None' item named '{0}' in player inventory.".format(name)) + else: + return None, -1, -1 + #raise ValueError("{0} cannot be reached.".format(name)) + else: # nothing + return None, -1, -1 + #raise ValueError("{0} cannot be reached.".format(name)) + return None, x, y + + def justifyText(self, text, width = 80): + ret = [] + text = text.lstrip().rstrip() + + # start by making it all one long line. + text = _re.sub(r'\s{2,}', r' ', text) + + # then add newlines as needed. + #i = 80 + #i = get_terminal_size()[0] + i = width + while i < len(text): + while text[i] != ' ': + i -= 1 + ret.append(text[:i]) + text = text[i+1:] + ret.append(text) + + return '\n'.join(ret) + + def go(self, args): + """go [-r] [to [the]] destination [additional arguments...] +Go to a location. "walk" and "move" are aliases of go. +-r: run to the location. "r" and "run" are aliases of -r. +The "run" command is an alias of "go -r". +"to" and "the" do nothing, and are intended only to make certain commands +make more sense from a linguistic perspective (for instance, one could +say "go to the balcony" rather than just "go balcony"). +Destination can be a coordinate pair or object name. For instance, if one +wanted to go to D6, one could say "go to D6", "go to d 6", or "go to 3 6". +The letter is not case-sensitive.""" + if self.level == None: + raise RuntimeError("Cannot move: No level has been loaded.") + speed = 0.6666667 + if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': + speed = 0.3333333 + args.pop(0) + if args[0] == 'to': + args.pop(0) + if args[0] == 'the': + args.pop(0) + thing, x, y = self.parseCoords(args, allowInventory = False) + + # Now we have a heading! Let's see if we can get there... + if (x, y) == (self.playerx, self.playery): + _hq.heappush(self.eventQueue, (self.gameTime, _ge.ArriveEvent(self.playerName, x, y, 0.0))) + return + dist, path = self.level.path(x, y, self.playerx, self.playery) + if dist == -1: + print('{0} cannot reach {1}{2}.'.format(self.playerName, self.numberToLetter(x), y), file = self.outstream) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + else: + pos = self.level.coordsToInt(self.playerx, self.playery) + space = path[pos] + if space == -1: + _hq.heappush(self.eventQueue, (self.gameTime, _ge.ArriveEvent(self.playerName, self.playerx, self.playery, 0.0))) + return + #target = self.level.coordsToInt(x, y) + t = 1 + #while space != target: + while space != -1: + newx, newy = self.level.intToCoords(space) + space = path[space] + if space != -1: + _hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.playerName, newx, newy))) + else: + _hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.ArriveEvent(self.playerName, newx, newy, t * speed))) + break + t += 1 + + #newx, newy = self.level.intToCoords(space) + #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.ArriveEvent(self.playerName, newx, newy, t * speed))) + return + + def look(self, args): + """look [at [the]] object +Describe an object. +"at" and "the" do nothing, and are intended only to make certain commands +make more sense from a linguistic perspective (for instance, one could +say "look at the balcony" rather than just "look balcony"). +Object can be the name of the object, or its coordinates.""" + if len(args) == 0: + print(self.justifyText(self.level.description), file = self.outstream) + else: + if args[0] == 'at': + args.pop(0) + if args[0] == 'the': + args.pop(0) + thing, x, y = self.parseCoords(args, usePlayerCoords = False) + if thing: + print(self.justifyText(str(thing)), file = self.outstream) + else: + print("There is nothing to see here.\n", file = self.outstream) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + + def use(self, args): + """use [-r] [the] object [on [the] object2] +Use an object. If the player is not already close to it, they will go to it. +use [-r] [the] item on [the] object +Use an item from the player's inventory on an object. +-r: run to the location. "r" and "run" are aliases of -r. +"the" does nothing, and is intended only to make certain commands +make more sense from a linguistic perspective (for instance, one could +say "use the lever" rather than just "use lever"). +Object can be the name of the object, or its coordinates. It can also be +the name of an item in the player's inventory.""" + speed = 0.6666667 + if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': + speed = 0.3333333 + args.pop(0) + if args[0] == 'the': + args.pop(0) + if 'on' in args: + self.useOn(args, speed) + return + thing, x, y = self.parseCoords(args) + if thing == None: + print("There is nothing to use.", file = self.outstream) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + if thing.thingType != 'u' and thing.name not in self.playerInv: + print("The {0} cannot be used.".format(thing.name), file = self.outstream) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + + # Similar to go, but not quite the same. + if (x, y) == (self.playerx, self.playery) or thing.name in self.playerInv: + _hq.heappush(self.eventQueue, (self.gameTime + 0.125, _ge.UseEvent(thing))) + return + dist, path = self.level.path(x, y, self.playerx, self.playery) + if dist == -1: + print('{0} cannot reach the {1}.'.format(self.playerName, thing.name), file = self.outstream) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + else: + pos = self.level.coordsToInt(self.playerx, self.playery) + space = path[pos] + #target = self.level.coordsToInt(x, y) + t = 1 + #while space != target: + while space != -1: + newx, newy = self.level.intToCoords(space) + space = path[space] + _hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.playerName, newx, newy))) + t += 1 + #newx, newy = self.level.intToCoords(space) + #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.playerName, newx, newy))) + _hq.heappush(self.eventQueue, (self.gameTime + t * speed + 0.125, _ge.UseEvent(thing))) + return + + def useOn(self, args, speed): + """Called by use when there is an 'on' clause""" + onIndex = args.index('on') + item, x, y = self.parseCoords(args[:onIndex]) + if args[onIndex+1] == 'the': + onIndex += 1 + thing, x, y = self.parseCoords(args[onIndex+1:]) + if item == None or item.name not in self.playerInv: + print("There is no {0} in the inventory.".format(item.name), file = self.outstream) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + if thing == None and x < 0 and y < 0: + print("Argument contains 'to' but with no real predicate.", file = self.outstream) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + + # Similar to go, but not quite the same. + if (x, y) == (self.playerx, self.playery): + _hq.heappush(self.eventQueue, (self.gameTime + 0.125, _ge.UseOnEvent(item, thing))) + return + dist, path = self.level.path(x, y, self.playerx, self.playery) + if dist == -1: + if thing != None: + print('{0} cannot reach the {1}.'.format(self.playerName, thing.name), file = self.outstream) + else: + print('{0} cannot reach {1}{2}.'.format(self.playerName, self.numberToLetter(x), y), file = self.outstream) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + else: + pos = self.level.coordsToInt(self.playerx, self.playery) + space = path[pos] + #target = self.level.coordsToInt(x, y) + t = 1 + #while space != target: + while space != -1: + newx, newy = self.level.intToCoords(space) + space = path[space] + _hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.playerName, newx, newy))) + t += 1 + #newx, newy = self.level.intToCoords(space) + #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.playerName, newx, newy))) + _hq.heappush(self.eventQueue, (self.gameTime + t * speed + 0.125, _ge.UseOnEvent(item, thing))) + return + + def take(self, args): + """take [the] item +Take an item. 'get' is an alias of take. +If the player is not already close to it, they will go to it. +"the" does nothing, and is intended only to make certain commands +make more sense from a linguistic perspective (for instance, one could +say "take the flask" rather than just "take flask"). +Object can be the name of the object, or its coordinates.""" + speed = 0.6666667 + if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': + speed = 0.3333333 + args.pop(0) + if args[0] == 'the': + args.pop(0) + thing, x, y = self.parseCoords(args) + if thing == None: + print("There is nothing to take.", file = self.outstream) + return + if thing.thingType != 'i': + print("The {0} cannot be taken.".format(thing.name), file = self.outstream) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + + # Similar to go, but not quite the same. + if (x, y) == (self.playerx, self.playery): + _hq.heappush(self.eventQueue, (self.gameTime + 0.125, _ge.TakeEvent(thing))) + return + dist, path = self.level.path(x, y, self.playerx, self.playery) + if dist == -1: + print('{0} cannot reach the {1}.'.format(self.playerName, thing.name), file = self.outstream) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + else: + pos = self.level.coordsToInt(self.playerx, self.playery) + space = path[pos] + #target = self.level.coordsToInt(x, y) + t = 1 + #while space != target: + while space != -1: + newx, newy = self.level.intToCoords(space) + space = path[space] + _hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.playerName, newx, newy))) + t += 1 + #newx, newy = self.level.intToCoords(space) + #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.playerName, newx, newy))) + _hq.heappush(self.eventQueue, (self.gameTime + t * speed + 0.125, _ge.TakeEvent(thing))) + return + + def drop(self, args): + """drop [the] item""" + if args[0] == 'the': + args.pop(0) + if args[0] in self.playerInv: + _hq.heappush(self.eventQueue, (self.gameTime, _ge.DropEvent(self.playerInv[args[0]]))) + else: + print('{0} do not have a {1}.'.format(self.playerName, args[0]), file = self.outstream) + + def loadMap(self, args): + # loadMap (fileName) + # loadMap (fileName, entrance) + # loadMap (fileName, x, y) + # save persistent things in previous level + if self.level != None: + if self.level.name not in self.persist: + self.persist[self.level.name] = {} + for i in self.level.persistent: + self.persist[self.level.name][i] = self.level.getThingByName(i) + + preLoaded = False + if args[0] in self.persist: + preLoaded = True + + # load the new level + if len(args) == 2: + self.level = _gm.GameMap.read(args[0], int(args[1]), preLoaded) + else: + self.level = _gm.GameMap.read(args[0], preLoaded) + + # get persistent things from it + if args[0] in self.persist: + persistedThings = tuple(self.persist[args[0]].keys()) + for i in persistedThings: + self.level.addThing(self.persist[args[0]][i], True) + del self.persist[args[0]][i] # delete them from the persist dict to prevent item duplication + + print(self.level.openingText, file = self.outstream) + #print(self.outstream.getvalue()) + if len(args) <= 2: + self.playerx, self.playery = self.level.playerStart + else: + self.playerx, self.playery = int(args[1]), int(args[2]) + self.prevx, self.prevy = self.playerx, self.playery + if self.onLevelLoad != None: + self.onLevelLoad() + + def saveGame(self, args): + if len(args) < 1: + print("Save file must have a name!", file = self.outstream) + return + + # choose pickle protocol depending on python version: + # 3 for Python 3.0.0 to 3.3.x, 4 for Python 3.4.0 to 3.7.x + prot = _pi.HIGHEST_PROTOCOL + + # save persistent things so that the current level can be recalled as-is + if self.level != None: + if self.level.name not in self.persist: + self.persist[self.level.name] = {} + for i in self.level.persistent: + self.persist[self.level.name][i] = self.level.getThingByName(i) + + # build data object to be saved + data = (self.playerName, self.playerx, self.playery, self.playerInv, self.level.name, self.persist, self.eventQueue, self.gameTime) + + # save it! + fileName = 'saves/' + args[0].replace(' ', '_') + '.dat' + if args[0].endswith('.dat'): # This is really for absolute paths, but doesn't really check for that. + fileName = args[0] + with open(fileName, 'wb') as f: + _pi.dump(data, f, protocol=prot) + + # delete things in the current map from the persist dict to prevent item duplication + persistedThings = tuple(self.persist[self.level.name].keys()) + for i in persistedThings: + del self.persist[self.level.name][i] + + # push a no-op event so that saving doesn't cost player characters time + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + + def loadGame(self, args): + if len(args) < 1: + print("Save file must have a name!", file = self.outstream) + return + + # choose pickle protocol depending on python version: + # 3 for Python 3.0.0 to 3.3.x, 4 for Python 3.4.0 to 3.7.x + prot = _pi.HIGHEST_PROTOCOL + fileName = 'saves/' + args[0].replace(' ', '_') + '.dat' + if args[0].endswith('.dat'): + fileName = args[0] + x, y, levelname = 1, 1, 'testing/test1.txt' + with open(fileName, 'rb') as f: + self.playerName, x, y, self.playerInv, levelname, self.persist, self.eventQueue, self.gameTime = _pi.load(f) + #print(levelname, x, y, file = self.outstream) + self.loadMap((levelname, x, y)) + _hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return + + def gameEventLoop(self): + while len(self.eventQueue) > 0: + ev = _hq.heappop(self.eventQueue) + self.gameTime = ev[0] + e = ev[1] + if self.__gameEvents[e.eventType](e): + break + if len(self.eventQueue) == 0: + self.gameTime = 0.0 + _ge.resetEventNum() + + # default event handlers + + def handleNoOp(self, e): + return True + + def handleGo(self, e): + if e.actor == self.playerName: + self.prevx, self.prevy = self.playerx, self.playery + self.playerx, self.playery = e.x, e.y + else: + self.level.moveThing(e.actor, e.x, e.y) + return False + + def handleArrive(self, e): + if e.actor == self.playerName: + self.prevx, self.prevy = self.playerx, self.playery + self.playerx, self.playery = e.x, e.y + print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(self.playerName, self.numberToLetter(e.x), e.y, e.t), file = self.outstream) + thing = self.level.getThingAtCoords(self.playerx, self.playery) + if thing: + if thing.thingType == 'x': + a = self.requestInput('Do you want to go {0}? (Y/n)'.format(str(thing))) + if a != 'n' and a != 'N': + self.loadMap((thing.destination, thing.exitid)) + return True + else: + actor = self.level.getThingByName(e.actor) + if actor: + self.level.moveThing(e.actor, e.x, e.y) + print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(actor.name, self.numberToLetter(e.x), e.y, e.t), file = self.outstream) + thing = self.level.getThingAtCoords(actor.x, actor.y) + if thing and thing != actor: + if thing.thingType == 'x': + print('{0} went {1}.'.format(actor.name, str(thing))) + self.level.removeThing(actor.name) + else: + print('There is nothing by the name of {0}.'.format(e.actor), file = self.outstream) + return False + + def handleUse(self, e): + if e.thing.useFunc == '': + print('The {0} cannot be used by itself.'.format(e.thing.name), file = self.outstream) + return True + _hq.heappush(self.eventQueue, (self.gameTime + self.__useFuncs[e.thing.useFunc](e.thing), _ge.NoOpEvent())) + return False + + def handleUseOn(self, e): + if e.item.useOnFunc == '': + print('The {0} cannot be used on other objects.'.format(e.item.name), file = self.outstream) + return True + _hq.heappush(self.eventQueue, (self.gameTime + self.__useFuncs[e.item.useOnFunc](e.item, e.thing), _ge.NoOpEvent())) + return False + + def handleTake(self, e): + self.level.removeThing(e.item.name) + self.playerInv[e.item.name] = e.item + return True + + def handleDrop(self, e): + e.item.x = self.playerx + e.item.y = self.playery + self.level.addThing(e.item, True) + del self.playerInv[e.item.name] + return True + + # default useFuncs: take a list of arguments, return the time the use took + + def examine(self, thing): + """Just prints the given text.""" + if 'pattern' not in thing.customValues or 'text' not in thing.customValues: + raise ValueError("Non-examinable thing {0} examined.".format(thing.name)) + + if thing.customValues['pattern'] == 'single': + print(self.justifyText(thing.customValues['text']), file = self.outstream) + elif thing.customValues['pattern'] == 'loop': + if not 'cursor' in thing.customValues: + thing.customValues['cursor'] = 0 + cursor = thing.customValues['cursor'] + print(self.justifyText(thing.customValues['text'][cursor]), file = self.outstream) + thing.customValues['cursor'] = (cursor + 1) % len(thing.customValues['text']) + elif thing.customValues['pattern'] == 'once': + if not 'cursor' in thing.customValues: + thing.customValues['cursor'] = 0 + cursor = thing.customValues['cursor'] + print(self.justifyText(thing.customValues['text'][cursor]), file = self.outstream) + if cursor < len(thing.customValues['text']) - 1: + thing.customValues['cursor'] += 1 + elif thing.customValues['pattern'] == 'random': + cursor = _ra.randrange(len(thing.customValues['text'])) + print(self.justifyText(thing.customValues['text'][cursor]), file = self.outstream) + thing.customValues['cursor'] = cursor + + if 'set' in thing.customValues: + if 'cursor' in thing.customValues: + self.customVals[thing.customValues['set']] = thing.customValues[cursor] + else: + self.customVals[thing.customValues['set']] = 0 + return 0.0 + + def container(self, thing): + """Acts as a container. Items can be traded between the container and the player's inventory.""" + items = thing.customValues['items'] + thing.customValues['items'], timeOpen = self.onContainer(items) + return timeOpen + + def key(self, item, thing): + """Item is a key, which unlocks a door. This may be implemented for containers later.""" + if isinstance(thing, tuple) or thing.thingType != 'd': + print("That is not a door.", file = self.outstream) + return 0.0 + if thing.lock(item.name): + print("The key fits the lock.", file = self.outstream) + if not thing.passable and self.playerx == thing.x and self.playery == thing.y: + self.playerx, self.playery = self.prevx, self.prevy + else: + print("The key doesn't fit that lock.", file = self.outstream) + return 0.125 + + # stuff for extended classes to use + def registerUseFunc(self, name, func): + """Registers a function for use by things in the map, but not directly +callable by the player.""" + self.__useFuncs[name] = func + + def registerEvent(self, name, func): + """Registers a function to handle an event in the event loop. +They should take the event as an argument, and return True if it should +always give the player a turn, False otherwise.""" + self.__gameEvents[name] = func diff --git a/gameevents.py b/gameevents.py new file mode 100644 index 0000000..ef711e4 --- /dev/null +++ b/gameevents.py @@ -0,0 +1,100 @@ +#gameevents.py + +# Experimental: events are ordered in the order that they were created, +# to resolve issues where two events happen at the same time. +_eventNum = 0 + +def resetEventNum(): + global _eventNum + _eventNum = 0 + +class GameEvent(object): + + def __init__(self, eventType: str): + global _eventNum + self.eventType = eventType + # Experimental, also NOT THREAD SAFE! + self.eventNum = _eventNum + _eventNum += 1 + + def __str__(self): + return "{0} ({1})".format(self.eventType, self.eventNum) + + def __eq__(self, other): + if isinstance(other, GameEvent): + return self.eventNum == other.eventNum + else: + return self.eventNum == other + + def __ne__(self, other): + if isinstance(other, GameEvent): + return self.eventNum != other.eventNum + else: + return self.eventNum != other + + def __lt__(self, other): + if isinstance(other, GameEvent): + return self.eventNum < other.eventNum + else: + return self.eventNum < other + + def __le__(self, other): + if isinstance(other, GameEvent): + return self.eventNum <= other.eventNum + else: + return self.eventNum <= other + + def __gt__(self, other): + if isinstance(other, GameEvent): + return self.eventNum > other.eventNum + else: + return self.eventNum > other + + def __ge__(self, other): + if isinstance(other, GameEvent): + return self.eventNum >= other.eventNum + else: + return self.eventNum >= other + +class NoOpEvent(GameEvent): + + def __init__(self): + super(NoOpEvent, self).__init__('noop') + +class GoEvent(GameEvent): + + def __init__(self, actor, x, y): + super(GoEvent, self).__init__('go') + self.actor = actor + self.x = x + self.y = y + +class ArriveEvent(GameEvent): + + def __init__(self, actor, x, y, t): + super(ArriveEvent, self).__init__('arrive') + self.actor = actor + self.x = x + self.y = y + self.t = t + +class UseEvent(GameEvent): + def __init__(self, thing): + super(UseEvent, self).__init__('use') + self.thing = thing + +class UseOnEvent(GameEvent): + def __init__(self, item, thing): + super(UseOnEvent, self).__init__('useon') + self.thing = thing # thing can be a coordinate pair? + self.item = item + +class TakeEvent(GameEvent): + def __init__(self, item): + super(TakeEvent, self).__init__('take') + self.item = item + +class DropEvent(GameEvent): + def __init__(self, item): + super(DropEvent, self).__init__('drop') + self.item = item diff --git a/gamegui.py b/gamegui.py new file mode 100644 index 0000000..d0e21dd --- /dev/null +++ b/gamegui.py @@ -0,0 +1,439 @@ +# gamegui.py + +from tkinter import * +from tkinter import ttk +from tkinter import filedialog +from tkinter import messagebox +from tile import Tile +from gamebase import GameBase +import io + +class App(ttk.Frame): + + def __init__(self, master, gameBase): + #print('Initializing...') + super(App, self).__init__(master) + self.gameBase = gameBase + self.gameBase.outstream = io.StringIO() + self.gameBase.requestInput = self.requestInput + self.gameBase.onLevelLoad = self.getGraphics + self.selected = (-1, -1) + self.parent = master + self.mapSize = (800, 450) + self.hasChanged = False + self.fileName = 'Untitled' + self.invTuple = tuple(self.gameBase.playerInv.keys()) + self.invNames = StringVar(value=self.invTuple) + #self.project = Project() + self.tool = StringVar() + self.brush = StringVar() + self.history = [] + self.redos = [] + #self.selectedArea = (0, 0, self.project.x-1, self.project.y-1) + self.parent.clipboard_clear() + self.parent.clipboard_append('') + self.grid() + self.createWidgets() + self.makeTheDisplayWork() + self.bindKeys() + + #print('Done.') + + def createWidgets(self): + #print('Creating Widgets...') + self.initWidth, self.initHeight = 0, 0 + + # create menu bar + self.createMenuBar() + + # create map + self.createMapView() + + # create text out + self.createTextOut() + + # create inventory list + self.createInventoryView() + + #self.tool.set('setone') + #brushes = ttk.Combobox(self, textvariable = self.brush, values = tuple(sorted(Project.tileID.keys()))) + #brushes.grid(column = 0, row = 6, sticky = (E, W)) + #brushes.state(['readonly']) + #brushes.set('Dirt') + #ttk.Separator(self, orient = VERTICAL).grid(column = 1, row = 0, rowspan = 9, sticky = (N, S)) + + ttk.Sizegrip(self).grid(column = 1, row = 1, sticky = (S, E)) + self.columnconfigure(0, weight = 1) + self.rowconfigure(0, weight = 1) + #print('Done.') + + def createMenuBar(self): + #print('Creating the menubar...') + menubar = Menu(self.parent) + menuFile = Menu(menubar) + #menuEdit = Menu(menubar) + menuHelp = Menu(menubar) + + menubar.add_cascade(menu = menuFile, label = 'File') + menuFile.add_command(label = 'New', command = lambda: self.loadLevel('maps/apartment.xml'), accelerator = 'Ctrl+N') + menuFile.add_command(label = 'Open...', command = self.fileOpen, accelerator = 'Ctrl+O') + menuFile.add_command(label = 'Save', command = self.fileSave, accelerator = 'Ctrl+S') + menuFile.add_command(label = 'Save As...', command = self.fileSaveAs) + menuFile.add_separator() + menuFile.add_command(label = 'Exit', command = self.onQuit, accelerator = 'Ctrl+Q') + + #menubar.add_cascade(menu = menuEdit, label = 'Edit') + #menuEdit.add_command(label = 'Undo', command = self.undo, accelerator = 'Ctrl+Z') + #menuEdit.add_command(label = 'Redo', command = self.redo, accelerator = 'Ctrl+Y') + #menuEdit.add_separator() + #menuEdit.add_command(label = 'Cut', command = self.cut, accelerator = 'Ctrl+X') + #menuEdit.add_command(label = 'Copy', command = self.copy, accelerator = 'Ctrl+C') + #menuEdit.add_command(label = 'Paste', command = self.paste, accelerator = 'Ctrl+V') + + menubar.add_cascade(menu = menuHelp, label = 'Help') + menuHelp.add_command(label = 'About', command = self.about) + menuHelp.add_separator() + menuHelp.add_command(label = 'Read Me', command = self.readme) + self.parent.configure(menu = menubar) + #print('Done.') + + def createMapView(self): + mapFrame = ttk.LabelFrame(self, text = "map") + mapFrame.grid(column = 0, row = 0) + self.levelDisplay = Canvas(mapFrame, width = self.mapSize[0], height = self.mapSize[1], scrollregion = '0 0 2048 2048') + self.levelDisplay.grid(column = 0, row = 0) + vbar = ttk.Scrollbar(mapFrame, orient = VERTICAL, command = self.levelDisplay.yview) + self.levelDisplay.configure(yscrollcommand = vbar.set) + vbar.grid(column = 1, row = 0, sticky = (N, S)) + hbar = ttk.Scrollbar(mapFrame, orient = HORIZONTAL, command = self.levelDisplay.xview) + self.levelDisplay.configure(xscrollcommand = hbar.set) + hbar.grid(column = 0, row = 1, sticky = (E, W)) + self.bind('', self.onResize) + + def createTextOut(self): + textFrame = ttk.LabelFrame(self, text = "Info") + textFrame.grid(column = 0, row = 1) + self.infoDisplay = Text(textFrame, width = 80, height = 8, state = 'disabled') + self.infoDisplay.grid(column = 0, row = 0) + vbar = ttk.Scrollbar(textFrame, orient = VERTICAL, command = self.infoDisplay.yview) + self.infoDisplay.configure(yscrollcommand = vbar.set) + vbar.grid(column = 1, row = 0, sticky = (N, S)) + self.infoCursor = 0 + + def createInventoryView(self): + invFrame = ttk.LabelFrame(self, text = "Inventory") + invFrame.grid(column = 1, row = 0) + self.invDisplay = Listbox(invFrame, height = 24, listvariable = self.invNames) + self.invDisplay.grid(column = 0, row = 0) + vbar = ttk.Scrollbar(invFrame, orient = VERTICAL, command = self.invDisplay.yview) + self.invDisplay.configure(yscrollcommand = vbar.set) + vbar.grid(column = 1, row = 0, sticky = (N, S)) + + def getGraphics(self): + self.tiles = {} + for i in range(len(self.gameBase.level.floorColors)): + self.tiles['e{0}'.format(i)] = Tile(self.gameBase.level.floorColors[i]) + for i in range(len(self.gameBase.level.wallColors)): + self.tiles['w{0}'.format(i)] = Tile(self.gameBase.level.wallColors[i]) + self.tiles['player'] = Tile('clear', 'blue', 'o') + for thing in self.gameBase.level.thingNames: + graphic = self.gameBase.level.thingNames[thing].graphic + self.tiles[thing] = Tile(graphic[0], graphic[1], graphic[2]) + + def makeTheDisplayWork(self): + #print('Making the display work...') + # level display + self.context = Menu(self.levelDisplay) + self.context.add_command(label = 'Load Map', command = lambda: self.loadLevel('maps/apartment.xml')) + self.context.add_command(label = 'Go', command = lambda: self.go(self.selected[0], self.selected[1])) + self.context.add_command(label = 'Run', command = lambda: self.go(self.selected[0], self.selected[1], True)) + self.context.add_command(label = 'Look', command = lambda: self.look(self.selected[0], self.selected[1])) + self.context.add_command(label = 'Use', command = lambda: self.use(self.selected[0], self.selected[1])) + self.context.add_command(label = 'Take', command = lambda: self.take(self.selected[0], self.selected[1])) + #self.context.add_command(label = 'Cut', command = self.cut, accelerator = 'Ctrl+X') + #self.context.add_command(label = 'Goblin...') + #self.context.entryconfigure('Goblin...', command = self.configureGoblin, state = DISABLED) + #self.refreshDisplay() + self.levelDisplay.bind('<1>', self.handleClick) + self.levelDisplay.bind('', self.handleDoubleClick) + #self.levelDisplay.bind('', self.handleDragEvent) + #self.levelDisplay.bind('', self.handleReleaseEvent) + self.levelDisplay.bind('<3>', self.handleRightClick) + + # inventory list + self.invContext = Menu(self.invDisplay) + self.invContext.add_command(label = 'Look', command = lambda: self.look(self.invTuple[self.invDisplay.curselection()[0]])) + self.invContext.add_command(label = 'Use', command = lambda: self.use(self.invTuple[self.invDisplay.curselection()[0]])) + self.invContext.add_command(label = 'Drop', command = lambda: self.drop(self.invTuple[self.invDisplay.curselection()[0]])) + self.invDisplay.bind('<>', lambda e: self.look(self.invTuple[self.invDisplay.curselection()[0]])) + self.invDisplay.bind('<3>', lambda e: self.invContext.post(e.x_root, e.y_root)) + #print('Done.') + pass + + def bindKeys(self): + self.parent.bind('', lambda e: self.fileNew()) + self.parent.bind('', lambda e: self.fileOpen()) + self.parent.bind('', lambda e: self.fileSave()) + #self.parent.bind('', lambda e: self.undo()) + #self.parent.bind('', lambda e: self.redo()) + #self.parent.bind('', lambda e: self.cut()) + #self.parent.bind('', lambda e: self.copy()) + #self.parent.bind('', lambda e: self.paste()) + self.parent.bind('', lambda e: self.onQuit()) + + def refreshDisplay(self): + #print('Refreshing the display...') + # refresh the map + self.levelDisplay.delete('all') + for y in range(self.gameBase.level.dimensions[1]): + for x in range(self.gameBase.level.dimensions[0]): + pos = self.gameBase.level.mapMatrix[y][x] + if pos[0] == 'w': + self.tiles['w{0}'.format(pos[1])].paint(self.levelDisplay, x, y) + else: + self.tiles['e{0}'.format(pos[1])].paint(self.levelDisplay, x, y) + for name in self.gameBase.level.thingNames: + thing = self.gameBase.level.getThingByName(name) + self.tiles[name].paint(self.levelDisplay, thing.x, thing.y) + self.tiles['player'].paint(self.levelDisplay, self.gameBase.playerx, self.gameBase.playery) + # refresh the info box + self.gameBase.outstream.seek(self.infoCursor) + #print(self.gameBase.outstream.tell()) + #print(self.gameBase.outstream.read()) + self.infoDisplay['state'] = 'normal' + self.infoDisplay.insert('end', self.gameBase.outstream.read()) + self.infoDisplay.see('end -1 chars') + self.infoDisplay['state'] = 'disabled' + self.infoCursor = self.gameBase.outstream.tell() + #print(self.infoCursor) + #print('Done.') + + def handleClick(self, event): + x = int(self.levelDisplay.canvasx(event.x) / 32) + y = int(self.levelDisplay.canvasy(event.y) / 32) + if (x, y) != self.selected: + self.selected = (x, y) + self.look(x, y) + + def handleDoubleClick(self, event): + x = int(self.levelDisplay.canvasx(event.x) / 32) + y = int(self.levelDisplay.canvasy(event.y) / 32) + thing = self.gameBase.level.getThingAtCoords(x, y) + if thing != None and thing.useable: + self.use(x, y) + else: + self.go(x, y) + + def handleRightClick(self, event): + x = int(self.levelDisplay.canvasx(event.x) / 32) + y = int(self.levelDisplay.canvasy(event.y) / 32) + self.selected = (x, y) + self.context.post(event.x_root, event.y_root) + + #def openInvContext(self, event): + # self.invContext.post(event.x_root, event.y_root) + + def handleDragEvent(self, event): + x = min(int(self.levelDisplay.canvasx(event.x) / 32), self.project.x-1) + y = min(int(self.levelDisplay.canvasy(event.y) / 32), self.project.y-1) + + if self.tool.get() == 'select': + rect = self.levelDisplay.find_withtag('selectArea') + self.selectedArea = (min(x, self.firstX), min(y, self.firstY), + max(x, self.firstX), max(y, self.firstY)) + self.levelDisplay.coords(rect, + (self.selectedArea[0]*32, self.selectedArea[1]*32, + self.selectedArea[2]*32+31, self.selectedArea[3]*32+31)) + + def handleReleaseEvent(self, event): + x = int(self.levelDisplay.canvasx(event.x) / 32) + y = int(self.levelDisplay.canvasy(event.y) / 32) + + if self.tool.get() == 'select': + rect = self.levelDisplay.find_withtag('selectArea') + self.selectedArea = (min(x, self.firstX), min(y, self.firstY), + max(x, self.firstX), max(y, self.firstY)) + self.levelDisplay.coords(rect, + (self.selectedArea[0]*32, self.selectedArea[1]*32, + self.selectedArea[2]*32+31, self.selectedArea[3]*32+31)) + tiles = self.project.getTiles(self.selectedArea) + for y in range(len(tiles)): + for x in range(len(tiles[0])): + if type(tiles[y][x]) == Project.GoblinSettings: + self.context.entryconfigure('Goblin...', state = NORMAL) + break + break + else: pass + + def loadLevel(self, fileName): + self.gameBase.loadMap([fileName]) + #self.getGraphics() + self.refreshDisplay() + + def go(self, x, y, running = False): + #print("go called") + #print(x, y) + if running: + self.gameBase.go(['-r', str(x), str(y)]) + else: + self.gameBase.go([str(x), str(y)]) + self.gameBase.gameEventLoop() + self.refreshDisplay() #inefficient, but will work for now. + + def look(self, x, y = 1): + #print("look called") + #if x == self.gameBase.playerx and y == self.gameBase.playery: + # self.gameBase.look([ + if isinstance(x, int): + thing = self.gameBase.level.getThingAtCoords(x, y) + if thing != None: + self.gameBase.look([thing.name]) + else: + self.gameBase.look([]) + else: + self.gameBase.look([x]) + self.gameBase.gameEventLoop() + self.refreshDisplay() #inefficient, but will work for now. + + def use(self, x, y = 1): + if isinstance(x, int): + self.gameBase.use([str(x), str(y)]) + else: + self.gameBase.use([x]) + self.gameBase.gameEventLoop() + self.refreshDisplay() #inefficient, but will work for now. + + def take(self, x, y): + self.gameBase.take([str(x), str(y)]) + self.gameBase.gameEventLoop() + self.refreshDisplay() #inefficient, but will work for now. + self.invTuple = tuple(self.gameBase.playerInv.keys()) + self.invNames.set(self.invTuple) + + def drop(self, item): + self.gameBase.drop([item]) + self.gameBase.gameEventLoop() + self.refreshDisplay() + self.invTuple = tuple(self.gameBase.playerInv.keys()) + self.invNames.set(self.invTuple) + + def requestInput(self, prompt = ''): + answer = messagebox.askyesno(message=prompt, icon='question', title='Prompt') + if answer: + return 'y' + else: + return 'n' + + def fileNew(self): + #print('Creating a new project...') + if self.askToSave(): + self.newDialog = Toplevel(self.parent) + self.newDialog.title('New Project') + newFrame = Frame(self.newDialog) + newFrame.grid() + self.hei = StringVar() + self.wid = StringVar() + ttk.Label(newFrame, text = 'Width:').grid(column = 0, row = 0, sticky = W) + ttk.Label(newFrame, text = 'Height:').grid(column = 0, row = 1, sticky = W) + Spinbox(newFrame, from_ = 16, to = 255, increment = 1, textvariable = self.wid + ).grid(column = 1, row = 0, sticky = W) + Spinbox(newFrame, from_ = 16, to = 255, increment = 1, textvariable = self.hei + ).grid(column = 1, row = 1, sticky = W) + ttk.Button(newFrame, text = 'Create', command = self.__confirmNewDimensions, + default = 'active').grid(column = 0, row = 2) + ttk.Button(newFrame, text = 'Cancel', command = self.newDialog.destroy + ).grid(column = 1, row = 2) + + #print('Done.') + + def fileOpen(self): + #print('Opening a project...') + if self.askToSave(): + newName = filedialog.askopenfilename(defaultextension = '.dat', initialdir = 'saves') + if newName != '': + self.fileName = newName + self.gameBase.loadGame((self.fileName,)) + self.gameBase.gameEventLoop() + self.refreshDisplay() + self.invTuple = tuple(self.gameBase.playerInv.keys()) + self.invNames.set(self.invTuple) + #print('Done.') + + def fileSave(self): + #print('Saving a project...') + newName = self.fileName + if self.fileName == 'Untitled': + self.fileSaveAs() + elif newName != '': + self.fileName = newName + self.gameBase.saveGame((self.fileName,)) + self.hasChanged = False + #print('Done.') + + def fileSaveAs(self): + newName = filedialog.asksaveasfilename(defaultextension = '.dat', initialdir = 'saves') + if newName != '': + self.fileName = newName + self.gameBase.saveGame((self.fileName,)) + self.hasChanged = False + + def onQuit(self): + if self.askToSave(): + exit() + + def askToSave(self): + if self.hasChanged: + insecure = messagebox.askyesnocancel( + message = 'Do you want to save ' + self.fileName + ' before continuing?', + icon = 'warning', title = 'New File') + print(type(insecure)) + if insecure == None: return False + elif insecure == True: + self.fileSave() + return True + else: return True + else: return True + + def about(self): + self.newDialog = Toplevel(self.parent) + self.newDialog.title('About') + newFrame = Frame(self.newDialog) + newFrame.grid() + ttk.Label(newFrame, text = 'I Am Gnome Level Editor v.0.9.0013').grid() + ttk.Button(newFrame, text = 'Okay', command = self.newDialog.destroy).grid() + + def readme(self): + self.newDialog = Toplevel(self.parent) + self.newDialog.title('About') + newFrame = Frame(self.newDialog) + newFrame.grid() + text = Text(newFrame, width=80, height=40, wrap = 'word') + text.grid(column = 0, row = 0) + sbar = ttk.Scrollbar(newFrame, orient = VERTICAL, command = text.yview) + sbar.grid(column = 1, row = 0, sticky = (N, S, W)) + text.configure(yscrollcommand = sbar.set) + text.state(['disabled']) + file = open('iag_readme.txt', 'r') + text.insert('1.0', file.read()) + + def onResize(self, event): + if self.initWidth == 0 and self.initHeight == 0: + self.initWidth = event.width + self.initHeight = event.height + else: + wDelta = event.width - int(self.initWidth) + hDelta = event.height - int(self.initHeight) + self.levelDisplay.configure(width = self.mapSize[0] + wDelta, height = self.mapSize[1] + hDelta) + +# main +if __name__ == '__main__': + root = Tk() + root.title('Game Gui (debug)') + #root.geometry("1024x768") + #root.resizable(FALSE, FALSE) + root.option_add('*tearOff', FALSE) + root.columnconfigure(0, weight = 1) + root.rowconfigure(0, weight = 1) + newApp = App(root, GameBase()) + newApp.grid(sticky = (N, S, E, W)) + root.mainloop() diff --git a/gamemap.py b/gamemap.py new file mode 100644 index 0000000..e436a98 --- /dev/null +++ b/gamemap.py @@ -0,0 +1,688 @@ +#gamemap.py +import re +import heapq +import xml.etree.ElementTree as ET + +class Thing(object): + + def __init__(self, thingType: str, name: str, x: int, y: int, description: str, flags: int, playerx = None, playery = None): + self.thingType = thingType + self.name = name + self.description = description + self.x = x + self.y = y + self.playerx = x + self.playery = y + if playerx: + self.playerx = playerx + if playery: + self.playery = playery + self.passable = bool(flags & 1) + self.talkable = bool(flags & 2) + self.lookable = bool(flags & 4) + self.takeable = bool(flags & 8) + self.useable = bool(flags & 16) + self.graphic = ('clear', '#7F7F7F', ' ') + + def __str__(self): + """__str__ is used for look.""" + return self.description + + def __eq__(self, other): + if not isinstance(other, Thing): + return False + return self.name == other.name + +class Item(Thing): + + def __init__(self, name, x: int, y: int, description: str, useFunc: str, useOnFunc: str, customValues: dict, ranged: bool, graphic = ('clear', '#00BF00', '^')): + super(Item, self).__init__('i', name, x, y, description, 13) + self.useFunc = useFunc + self.useOnFunc = useOnFunc + self.customValues = customValues + self.ranged = ranged + self.graphic = graphic + + def use(self): + pass + +class Useable(Thing): + + def __init__(self, name, x: int, y: int, description: str, useFunc: str, customValues: dict, playerx = None, playery = None, graphic = ('clear', '#0000FF', '#')): + super(Useable, self).__init__('u', name, x, y, description, 16, playerx, playery) + self.useFunc = useFunc + self.customValues = customValues + self.graphic = graphic + + def use(self): + pass + +class NPC(Thing): + + def __init__(self, name, x: int, y: int, description: str, friendly = True, following = False, graphic = ('clear', '#000000', 'o')): + super(NPC, self).__init__('c', name, x, y, description, 6) + self.following = following + self.friendly = friendly + self.inventory = [] + self.graphic = graphic + + def give(self, item): + self.inventory.append(item) + + def drop(self, index): + return self.inventory.pop(index) + + def dialog(self): + pass + +class Door(Thing): + + def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = ('clear', '#7F3F00', '#')): + self.descBase = description + if description == None: + if locked: + description = "The {0} is locked.".format(name) + else: + description = "The {0} is unlocked.".format(name) + else: + if locked: + description += " It is locked.".format(name) + else: + description += " It is unlocked.".format(name) + super(Door, self).__init__('d', name, x, y, description, 1) + self.passable = not locked + self.key = key + self.graphic = graphic + + def lock(self, key = None): + if key == self.key: + self.passable = not self.passable + if self.descBase == None: + if self.passable: + self.description = "The {0} is unlocked.".format(self.name) + else: + self.description = "The {0} is locked.".format(self.name) + else: + if self.passable: + self.description += " It is unlocked.".format(self.name) + else: + self.description += " It is locked.".format(self.name) + return True + return False + +class MapExit(Thing): + + def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix: None, graphic = ('clear', '#FF0000', 'x')): + description = name + if prefix: + description = "{0} {1}".format(prefix, name) + super(MapExit, self).__init__('x', name, x, y, description, 5) + self.exitid = exitid + self.destination = destination + self.prefix = prefix + self.graphic = graphic + +class GameMap(object): + + + + # Matrix tile codes: + # e: empty (0) + # d: door + # x: map change + # w: wall (0) + # c: NPC (should this be a subclass of interactable?) + # i: item + # u: useable + # p: player start (up to one per level) + + # regular expressions + matrixRegex = re.compile(r'([ \t]*([a-z][0-9]*)+(\n))+') + + def __init__(self, name, graph, matrix, dimensions): + self.name = name + self.openingText = "" + self.mapGraph = graph + self.mapMatrix = matrix + self.dimensions = dimensions + self.thingPos = {} # int location : list of names + self.thingNames = {} # Things can be looked up by name. + self.playerStart = (1, 1) + self.description = "The area is completely blank." + self.floorColors = [] + self.wallColors = [] + self.persistent = [] + + @staticmethod + def read(infile = None, prevMap = None, preLoaded = False): + """Read map data and return a Map object. If infile is not provided, then +it will read from stdin. Otherwise, it should be a valid file name. +Entering a map through stdin will be obsolete once testing is over.""" + data = None + tryToRead = True + if infile != None: + try: + with open(infile, 'r') as f: + data = f.read() + except OSError as e: + print("The file could not be read. Falling back to stdin...") + else: + + while tryToRead: + try: + data += input() + except EOFError as e: + tryToRead = False + + # Now what we do with the data + info = ET.fromstring(data) + mat = None + layout = info.find('layout') + if layout != None: + #print(layout.text) + match = GameMap.matrixRegex.match(layout.text.lstrip()) + if match == None: + raise RuntimeError('Map read a file without a map matrix.') + mat = match.group() + else: + raise RuntimeError('Map read a file without a map matrix.') + + # generate matrix and graph first + mapMatrix, mapGraph, dimensions = GameMap.parseMatrix(mat) + playerStart = (-1, -1) + level = GameMap(infile, mapGraph, mapMatrix, dimensions) + + # Now, load other info + GameMap.loadThings(level, info, prevMap, preLoaded) + + return level + + @staticmethod + def parseMatrix(matrixStr): + """Returns a map graph as an adjacency list, as well as the matrix as a +list of lists of tuples.""" + # Make the matrix first + mat = [[]] + x = 0 + y = 0 + i = 0 + l = 0 + while i < len(matrixStr): + if matrixStr[i].isalpha(): + j = i+1 + while j < len(matrixStr) and matrixStr[j].isdecimal(): + j += 1 + if j == i+1: # no number + mat[l].append((matrixStr[i], 0)) + else: + mat[l].append((matrixStr[i], int(matrixStr[i+1:j]))) + i = j + elif matrixStr[i] == '\n': + if x == 0: + x = len(mat[l]) + elif x != len(mat[l]): + raise RuntimeError('Map matrix has jagged edges.') + l += 1 + i += 1 + mat.append([]) + else: # assume it was a whitespace character. + i += 1 + y = len(mat) - 1 + + # Now for the graph + numTiles = x * y + dim = (x, y) + graph = [[] for j in range(numTiles)] + passable = ('e', 'd', 'x', 'p', 'i') + for j in range(y): + for i in range(x): + if mat[j][i][0] in passable: + here = GameMap.__coordsToInt(i, j, x) + if i > 0 and mat[j][i-1][0] in passable: + there = GameMap.__coordsToInt(i-1, j, x) + graph[here].append(there) + graph[there].append(here) + if j > 0 and mat[j-1][i][0] in passable: + there = GameMap.__coordsToInt(i, j-1, x) + graph[here].append(there) + graph[there].append(here) + + return mat, graph, dim + + @staticmethod + def loadThings(level, info, prevMap = None, preLoaded = False): + """load the things from the xml part of the map file.""" + if 'openingText' in info.attrib: + level.openingText = info.attrib['openingText'] + if 'playerStart' in info.attrib: + ps = info.attrib['playerStart'].split(',') + level.playerStart = (int(ps[0]), int(ps[1])) + if info.text != None: + level.description = info.text + + # get map colors + floorColors = info.find('floorColors') + if floorColors != None: + level.floorColors = floorColors.text.lstrip().split() + if len(level.floorColors) == 0: + level.floorColors.append('#9F7F5F') + wallColors = info.find('wallColors') + if wallColors != None: + level.wallColors = wallColors.text.lstrip().split() + if len(level.wallColors) == 0: + level.wallColors.append('#7F3F0F') + + # get things + for node in info: + if node.tag == 'loadOnce': + # Things in the load-once section are only loaded the first + # time that the map is loaded, and saved in the player's + # save file. + if preLoaded: + continue + for node1 in node: + if node1.tag == 'door': + level.addThing(GameMap.__loadDoor(node1), True) + elif node1.tag == 'useable': + level.addThing(GameMap.__loadUseable(node1), True) + elif node1.tag == 'item': + level.addThing(GameMap.__loadItem(node1), True) + elif node.tag == 'exit': + level.addThing(GameMap.__loadExit(level, node, prevMap)) + elif node.tag == 'door': + level.addThing(GameMap.__loadDoor(node)) + elif node.tag == 'useable': + level.addThing(GameMap.__loadUseable(node)) + + @staticmethod + def __loadExit(level, node, prevMap): + exitid = 0 + x, y = 0, 0 + destination = '' + name = '' + prefix = None + if 'name' in node.attrib: + name = node.attrib['name'] + else: + print("Nameless exit, ommitting.") + return None + if 'id' in node.attrib: + exitid = int(node.attrib['id']) + else: + print("Exit '{0}' with no id, ommitting.".format(name)) + return None + if 'location' in node.attrib: + loc = node.attrib['location'].split(',') + x, y = int(loc[0]), int(loc[1]) + else: + print("Exit '{0}' without a location, ommitting.".format(name)) + return None + if 'destination' in node.attrib: + destination = node.attrib['destination'] + else: + print("Exit '{0}' with no destination, ommitting.".format(name)) + return None + if 'prefix' in node.attrib: + prefix = node.attrib['prefix'] + graphic = GameMap.__loadGraphic(node) + #level.addThing(MapExit(name, x, y, exitid, destination, prefix)) + if prevMap == exitid: + level.playerStart = (x, y) + return MapExit(name, x, y, exitid, destination, prefix, graphic) + + @staticmethod + def __loadDoor(node): + x, y = 0, 0 + name = '' + locked = False + doorOpen = False + key = None + if 'name' in node.attrib: + name = node.attrib['name'] + else: + print("Nameless door, ommitting.") + return None + if 'location' in node.attrib: + pos = node.attrib['location'].split(',') + x, y = int(pos[0]), int(pos[1]) + else: + print("Door '{0}' without a location, ommitting.".format(name)) + return None + if 'locked' in node.attrib: + if node.attrib['locked'] == 'True': + locked = True + if 'open' in node.attrib: + if node.attrib['open'] == 'True': + doorOpen = True + if 'key' in node.attrib: + key = node.attrib['key'] + graphic = GameMap.__loadGraphic(node) + description = node.text + return Door(name, x, y, locked, description, key, graphic) + + @staticmethod + def __loadUseable(node): + x, y = 0, 0 + playerx, playery = 0, 0 + name = '' + useFunc = '' + description = '' + if 'name' in node.attrib: + name = node.attrib['name'] + else: + print("Nameless useable, ommitting.") + return None + if 'location' in node.attrib: + pos = node.attrib['location'].split(',') + x, y = int(pos[0]), int(pos[1]) + else: + print("Useable '{0}' without a location, ommitting.".format(name)) + return None + if 'useLocation' in node.attrib: + loc = node.attrib['useLocation'].split(',') + playerx, playery = int(loc[0]), int(loc[1]) + else: + playerx, playery = x, y + if 'useFunc' in node.attrib: + useFunc = node.attrib['useFunc'] + else: + print("Unuseable useable '{0}', ommitting.".format(name)) + return None + graphic = GameMap.__loadGraphic(node) + description = node.text + + # get custom values + customVals = {} + for val in node: + if val.tag == 'graphic': + continue # skip it + value = None + valName = '' + if 'name' in val.attrib: + valName = val.attrib['name'] + else: + print("Nameless custom value in {0}, ommitting.".format(name)) + continue + if 'value' in val.attrib: + if val.tag == 'int': + value = int(val.attrib['value']) + elif val.tag == 'float': + value = float(val.attrib['value']) + elif val.tag == 'str': + value = str(val.attrib['value']) + elif val.tag == 'item': + value = GameMap.__loadItem(val) + elif val.tag == 'intList': + value = [int(i) for i in val.attrib['value'].split(',')] + elif val.tag == 'floatList': + value = [float(i) for i in val.attrib['value'].split(',')] + elif val.tag == 'strList': + value = [i.lstrip() for i in val.attrib['value'].split(',')] # depricated, use 'list' of strings instead so you can use commas. + else: + print("Value {0} in {1} is of unknown type {2}, ommitting.".format(valName, name, val.tag)) + continue + elif val.tag == 'list': + value = [] + for i in val: + if 'value' in i.attrib: + if i.tag == 'int': + value.append(int(i.attrib['value'])) + elif i.tag == 'float': + value.append(float(i.attrib['value'])) + elif i.tag == 'str': + value.append(str(i.attrib['value'])) + elif i.tag == 'item': + value.append(GameMap.__loadItem(i)) + else: + print("Value {0} in {1} has an entry of unknown type {2}, ommitting.".format(valName, name, i.tag)) + continue + else: + print("Value {0} in {1} has an entry with no value, ommitting.".format(valName, name)) + continue + else: + print("Value {0} in {1} has no value, ommitting.".format(valName, name)) + continue + customVals[valName] = value + + return Useable(name, x, y, description, useFunc, customVals, playerx, playery, graphic) + + @staticmethod + def __loadItem(node): + x, y = 0, 0 + playerx, playery = 0, 0 + name = '' + useFunc = '' + useOnFunc = '' + description = '' + ranged = False + if 'name' in node.attrib: + name = node.attrib['name'] + else: + print("Nameless item, ommitting.") + return None + if 'location' in node.attrib: + pos = node.attrib['location'].split(',') + x, y = int(pos[0]), int(pos[1]) + else: + print("Item '{0}' without a location, ommitting.".format(name)) + return None + if 'useFunc' in node.attrib: + useFunc = node.attrib['useFunc'] + if 'useOnFunc' in node.attrib: + useOnFunc = node.attrib['useOnFunc'] + if 'ranged' in node.attrib: + ranged = (node.attrib['ranged'].casefold() == 'true') + graphic = GameMap.__loadGraphic(node) + description = node.text + + # get custom values + customVals = {} + for val in node: + if val.tag == 'graphic': + continue # skip it + value = None + valName = '' + if 'name' in val.attrib: + valName = val.attrib['name'] + else: + print("Nameless custom value in {0}, ommitting.".format(name)) + continue + if 'value' in val.attrib: + if val.tag == 'int': + value = int(val.attrib['value']) + elif val.tag == 'float': + value = float(val.attrib['value']) + elif val.tag == 'str': + value = str(val.attrib['value']) + elif val.tag == 'item': + value = GameMap.__loadItem(val) + elif val.tag == 'intList': + value = [int(i) for i in val.attrib['value'].split(',')] + elif val.tag == 'floatList': + value = [float(i) for i in val.attrib['value'].split(',')] + elif val.tag == 'strList': + value = [i.lstrip() for i in val.attrib['value'].split(',')] # depricated, use 'list' of strings instead so you can use commas. + else: + print("Value {0} in {1} is of unknown type {2}, ommitting.".format(valName, name, val.tag)) + continue + elif val.tag == 'list': + value = [] + for i in val: + if 'value' in i.attrib: + if i.tag == 'int': + value.append(int(i.attrib['value'])) + elif i.tag == 'float': + value.append(float(i.attrib['value'])) + elif i.tag == 'str': + value.append(str(i.attrib['value'])) + elif i.tag == 'item': + value.append(GameMap.__loadItem(i)) + else: + print("Value {0} in {1} has an entry of unknown type {2}, ommitting.".format(valName, name, i.tag)) + continue + else: + print("Value {0} in {1} has an entry with no value, ommitting.".format(valName, name)) + continue + else: + print("Value {0} in {1} has no value, ommitting.".format(valName, name)) + continue + customVals[valName] = value + + return Item(name, x, y, description, useFunc, useOnFunc, customVals, ranged, graphic) + + @staticmethod + def __loadGraphic(node): + bgc = 'clear' + fgc = '#7F7F7F' + shape = '#' + graphicInDecl = False + if 'bgc' in node.attrib: + bgc = node.attrib['bgc'] + graphicInDecl = True + if 'fgc' in node.attrib: + fgc = node.attrib['fgc'] + graphicInDecl = True + if 'shape' in node.attrib: + shape = node.attrib['shape'] + graphicInDecl = True + + if not graphicInDecl: + #check for graphic child node + graphic = node.find('graphic') + if graphic != None: + if 'bgc' in graphic.attrib: + bgc = graphic.attrib['bgc'] + if 'fgc' in graphic.attrib: + fgc = graphic.attrib['fgc'] + if 'shape' in graphic.attrib: + shape = graphic.attrib['shape'] + return (bgc, fgc, shape) + + # stuff the gameshell itself might use + + def addThing(self, thing, persist = False): + if thing == None: + return + if thing.name in self.thingNames: + raise ValueError("Cannot have two objects named {0}.".format(thing.name)) + pos = self.coordsToInt(thing.x, thing.y) + if pos not in self.thingPos: + self.thingPos[pos] = [thing.name] + else: + self.thingPos[pos].append(thing.name) + self.thingNames[thing.name] = thing + if persist: + self.persistent.append(thing.name) + + def removeThing(self, name): + thing = self.getThingByName(name) + if thing: + oldPos = self.coordsToInt(thing.x, thing.y) + if oldPos in self.thingPos: + self.thingPos[oldPos].remove(name) + if len(self.thingPos[oldPos]) == 0: + del self.thingPos[oldPos] + del self.thingNames[name] + + def path(self, x1, y1, x2, y2, closeEnough = True): + """Uses Dijkstra's Algorithm to find the shortest path from (x1, y1) to (x2, y2) +The closeEnough parameter will create a path that lands beside the source if necessary.""" + # first test to see that the start point is passable + startThing = self.getThingAtCoords(x1, y1) + if not closeEnough: + if startThing and not startThing.passable: + return -1, [] # meaning you can't get there + startPoint = self.coordsToInt(x1, y1) + endPoint = self.coordsToInt(x2, y2) + numVertex = self.dimensions[0] * self.dimensions[1] + dist = [numVertex + 1 for i in range(numVertex)] + prev = [-1 for i in range(numVertex)] + dist[startPoint] = 0 + if closeEnough: + if startThing and not startThing.passable: + dist[startPoint] = -1 + queue = [] + heapq.heappush(queue, (dist[startPoint], startPoint)) + + while len(queue) > 0: + u = heapq.heappop(queue)[1] + for v in self.mapGraph[u]: + thing = self.getThingAtPos(v) + if thing and not thing.passable: + continue + tempDist = dist[u] + 1 + if tempDist < dist[v]: + dist[v] = tempDist + if dist[u] != -1: + prev[v] = u + heapq.heappush(queue, (dist[v], v)) + + if dist[endPoint] < numVertex + 1: + return dist[endPoint], prev + else: + return -1, [] # meaning you can't get there + + def lineOfSight(self, x1, y1, x2, y2): + pass + + @staticmethod + def __coordsToInt(x, y, width): + return x + y * width + + def coordsToInt(self, x, y, width = -1): + if width < 0: + return x + y * self.dimensions[0] + else: + return x + y * width + + def intToCoords(self, pos): + return pos % self.dimensions[0], int(pos / self.dimensions[0]) + + def getThingAtCoords(self, x, y): + return self.getThingAtPos(self.coordsToInt(x, y)) + + def getThingsAtCoords(self, x, y): + return self.getThingsAtPos(self.coordsToInt(x, y)) + + def getThingAtPos(self, pos): + if pos in self.thingPos: + return self.thingNames[self.thingPos[pos][0]] + else: + return None + + def getThingsAtPos(self, pos): + if pos in self.thingPos: + ret = [] + for i in self.thingPos[pos]: + ret.append(self.thingNames[i]) + return ret + else: + return None + + def getThingByName(self, name): + if name in self.thingNames: + return self.thingNames[name] + else: + return None + + def moveThing(self, name, x, y = -1): + newPos = x + if y != -1: + newPos = self.coordsToInt(x, y) + else: + x, y = self.intToCoords(x) + thing = self.getThingByName(name) + if thing: + oldPos = self.coordsToInt(thing.x, thing.y) + if oldPos in self.thingPos: + self.thingPos[oldPos].remove(name) + if len(self.thingPos[oldPos]) == 0: + del self.thingPos[oldPos] + if newPos not in self.thingPos: + self.thingPos[newPos] = [name] + else: + self.thingPos[newPos].append(name) + thing.x, thing.y = x, y + else: + raise RuntimeError("There is nothing by the name of {0}.".format(name)) + diff --git a/gameshell.py b/gameshell.py new file mode 100644 index 0000000..e5e8683 --- /dev/null +++ b/gameshell.py @@ -0,0 +1,151 @@ +# gameshell.py + +from shell import Shell +from gamebase import GameBase +import sys as _sys +#import re +import heapq +#import gamemap +import gameevents +#from os import get_terminal_size +#import random + +class GameShell(Shell): + + UP = 1 + RIGHT = 2 + DOWN = 4 + LEFT = 8 + WALLS = ('++', '++', ' +', '++', '++', '||', '++', '|+', '+ ', '++', + '==', '++', '++', '+|', '++', '++') + + def __init__(self, gameBase): + super(GameShell, self).__init__() + self.outstream = _sys.stdout + self.gameBase = gameBase + + # register functions + + self.registerCommand('map', self.showMap) + self.registerCommand('ls', self.showMap) + self.registerCommand('go', self.gameBase.go) + self.registerCommand('move', self.gameBase.go) + self.registerCommand('walk', self.gameBase.go) + self.registerCommand('look', self.gameBase.look) + self.registerCommand('use', self.gameBase.use) + self.registerCommand('loadMap', self.gameBase.loadMap) + self.registerCommand('man', self.man) + self.registerCommand('help', self.man) + self.registerCommand('save', self.gameBase.saveGame) + self.registerCommand('load', self.gameBase.loadGame) + self.registerCommand('take', self.gameBase.take) + self.registerCommand('get', self.gameBase.take) + self.registerCommand('drop', self.gameBase.drop) + self.registerCommand('inv', self.inv) + self.registerCommand('bag', self.inv) + self.registerCommand('items', self.inv) + self.registerCommand('status', self.status) + self.registerAlias('run', ['go', '-r']) + + # Helper functions + + def man(self, args): + super(GameShell, self).man(args) + heapq.heappush(self.gameBase.eventQueue, (self.gameBase.gameTime, gameevents.NoOpEvent())) + + def showMap(self, args): + """map [-l] +See a map of the local area. "ls" is an alias of map. +"l" and "legend" are aliases of -l. +If -l is given, a map legend will be printed under the map.""" + xAxis = ' ' + ''.join([self.gameBase.numberToLetter(i).ljust(2) for i in range(self.gameBase.level.dimensions[0])]) + '\n' + rows = [] + index = 0 + exits = {} + doors = {} + useables = {} + items = {} + for y in range(self.gameBase.level.dimensions[1]): + rows.append(['{0:2} '.format(y)]) + for x in range(self.gameBase.level.dimensions[0]): + pos = self.gameBase.level.mapMatrix[y][x] + thing = self.gameBase.level.getThingAtPos(index) + if x == self.gameBase.playerx and y == self.gameBase.playery: + rows[-1].append('()') + elif thing: + if thing.thingType == 'x': # exit + rows[-1].append('X{0}'.format(thing.exitid)) + exits[thing.exitid] = thing.name + elif thing.thingType == 'd': # door + doors[len(doors)+1] = thing.name + rows[-1].append('D{0}'.format(len(doors))) + elif thing.thingType == 'u': # useable + useables[len(useables)+1] = thing.name + rows[-1].append('U{0}'.format(len(useables))) + elif thing.thingType == 'i': # item + items[len(items)+1] = thing.name + rows[-1].append('I{0}'.format(len(items))) + elif pos[0] == 'w': + sides = 0 + if y > 0 and self.gameBase.level.mapMatrix[y-1][x][0] == 'w': + sides += GameShell.UP + if x < self.gameBase.level.dimensions[0]-1 and self.gameBase.level.mapMatrix[y][x+1][0] == 'w': + sides += GameShell.RIGHT + if y < self.gameBase.level.dimensions[1]-1 and self.gameBase.level.mapMatrix[y+1][x][0] == 'w': + sides += GameShell.DOWN + if x > 0 and self.gameBase.level.mapMatrix[y][x-1][0] == 'w': + sides += GameShell.LEFT + rows[-1].append(GameShell.WALLS[sides]) + else: + rows[-1].append(' ') + index += 1 + rows[-1] = ''.join(rows[-1]) + + print(xAxis) + print('\n'.join(rows)) + + if len(args) > 0: + if args[0] == '-l' or args[0] == 'l' or args[0] == 'legend': + legend = ["\n---Legend---\n", "() - {0}".format(self.gameBase.playerName), + "Xn - Exit to another area"] + for i in exits: + legend.append(' X{0} - {1}'.format(i, exits[i])) + legend.append("Un - Useable object") + for i in useables: + legend.append(' U{0} - {1}'.format(i, useables[i])) + legend.append("In - Item") + for i in items: + legend.append(' I{0} - {1}'.format(i, items[i])) + legend.append("Dn - Door") + for i in doors: + legend.append(' D{0} - {1}'.format(i, doors[i])) + print('\n'.join(legend)) + heapq.heappush(self.gameBase.eventQueue, (self.gameBase.gameTime, gameevents.NoOpEvent())) + return + + def status(self, args): + ret = [] + if self.gameBase.level != None: + ret.append("Level:{0:.>74}".format(self.gameBase.level.name)) + else: + ret.append("Level:{0:.>74}".format("None")) + ret.append("Gametime:{0:.>71.3}".format(self.gameBase.gameTime)) + ret.append("Event queue:") + for i in sorted(self.gameBase.eventQueue): + ret.append("{0:.<8.3}:{1:.>72}".format(i[0], str(i[1]))) + ret.append("Player name:{0:.>68}".format(self.gameBase.playerName)) + ret.append("Player position:{0:.>64}".format("{0}{1}".format(self.gameBase.numberToLetter(self.gameBase.playerx), self.gameBase.playery))) + ret.append("Prev. position:{0:.>65}".format("{0}{1}".format(self.gameBase.numberToLetter(self.gameBase.prevx), self.gameBase.prevy))) + print('\n'.join(ret)) + heapq.heappush(self.gameBase.eventQueue, (self.gameBase.gameTime, gameevents.NoOpEvent())) + return + + def inv(self, args): + print('\n'.join([i for i in self.gameBase.playerInv])) + + def update(self): + self.gameBase.gameEventLoop() + +if __name__ == '__main__': + sh = GameShell(GameBase()) + sh.run() diff --git a/shell.py b/shell.py new file mode 100644 index 0000000..c09d550 --- /dev/null +++ b/shell.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# gameshell.py +# +# Copyright 2018 chees +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, +# MA 02110-1301, USA. +# +# Ver. 0.1.0017 + + +import types as _types +import traceback as _tb + +class Shell(object): + + def __init__(self): + self.__commands = {'exit': self.exitShell} + self.__aliases = {} + self.ps1 = '> ' + self.ps2 = '> ' + self.__exit = False + + def run(self): + """The main game/shell loop""" + while not self.__exit: + print(self.ps1, end = '') + command = self.scanInput() + # we have to handle shell built-ins first (when we get some) + if len(command) == 0: + continue + if command[0] in self.__commands: + try: + self.__commands[command[0]](command[1:]) + except Exception as e: + _tb.print_exc() + print(e) + else: + self.handleUnknownCommand(command) + self.update() + + def man(self, args): + help(self.__commands[args[0]]) + + def registerCommand(self, commandName: str, command: _types.FunctionType): + """command must be a function that takes one argument: a list of strings, +conventionally called args or argv""" + self.__commands[commandName] = command + + def registerAlias(self, a: str, original: list): + """makes 'a' an alias for original. +'a' must be one token, but original can be multiple.""" + self.__aliases[a] = original + + def getAlias(self, a: str): + if a in self.__aliases: + return self.__aliases[a] + else: + return None + + def exitShell(self, args): + """The default exit command.""" + self.__exit = True + + # Beyond this point are functions that are called within the main loop. + def scanInput(self): + """Parses input. Override this for custom input parsing, or input source.""" + ret = input() + if ret == '': + return [] + ret = ret.split() + a = self.getAlias(ret[0]) + if a: + ret = a[:] + ret[1:] + return ret + + def handleUnknownCommand(self, command): + """Handle commands that aren't registered. Override this if you want to do +something with those commands.""" + print("Bad command.") + + def update(self): + """Runs at the end of each loop. Does nothing by default. Override this if +there is something you want the shell to do between every command.""" + pass + +def main(args): + return 0 + +if __name__ == '__main__': + import sys + sys.exit(main(sys.argv)) diff --git a/testing/test1.txt b/testing/test1.txt new file mode 100644 index 0000000..09ee937 --- /dev/null +++ b/testing/test1.txt @@ -0,0 +1,33 @@ +w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0 +w0w0w0w0e0e0e0e0e0e0e0e0e0w0e0w0e0w0e0w0e0w0e0e0e0e0e0e0w0w0 +w0w0w0w0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0w0 +w0w0e0e0e0e0w0w0e0w0e0e0e0e0e0e0e0e0e0e0e0e0w0w0w0w0e0e0w0w0 +w0w0w0e0e0e0w0w0e0w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0w0w0 +w0e0e0e0w0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0w0w0 +w0e0e0e0w0e0e0e0e0w0e0e0e0w0w0w0e0e0w0w0w0w0w0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0w0w0w0w0e0w0e0e0e0e0e0e0w0w0w0e0w0w0e0e0w0w0 +w0w0w0w0w0w0w0w0w0w0e0e0e0w0e0e0e0e0e0e0w0w0w0e0w0w0e0e0w0w0 +w0e0e0w0e0x1e0e0w0e0e0e0e0w0e0e0w0e0w0e0e0w0w0e0e0e0e0w0w0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0w0e0w0e0e0w0w0e0e0e0e0w0w0w0 +w0e0e0w0e0e0e0e0w0e0e0e0e0w0e0e0e0e0e0e0e0e0w0w0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0w0w0w0w0w0w0w0w0w0w0w0w0w0e0e0w0w0w0w0 +w0e0e0w0e0e0e0e0w0e0e0w0e0e0e0e0e0e0w0w0w0w0e0e0e0e0e0e0w0w0 +w0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0w0w0e0e0e0e0e0e0e0e0w0w0 +w0e0e0w0e0e0e0e0w0e0e0w0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0w0w0e0e0w0w0e0e0w0w0w0w0 +w0e0e0w0e0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0w0w0e0e0w0w0w0w0 +w0e0e0e0e0e0e0e0e0e0e0w0e0e0w0w0w0w0w0w0e0e0w0w0e0e0e0e0w0w0 +w0e0e0w0e0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0w0 +w0e0e0w0e0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0w0e0e0w0w0w0w0w0w0e0x2e0e0w0e0e0e0e0w0 +w0e0e0w0e0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0w0w0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0w0w0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0w0w0e0e0w0w0 +w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 + + + + + diff --git a/testing/test2.txt b/testing/test2.txt new file mode 100644 index 0000000..59dd7d7 --- /dev/null +++ b/testing/test2.txt @@ -0,0 +1,35 @@ +w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 +w0w0w0w0w0w0x3e0e0w0e0e0e0e0e0w0e0e0e0e0e0e0w0e0w0w0w0w0w0w0 +w0w0w0w0e0e0e0e0e0w0e0e0e0e0e0w0e0e0w0w0e0e0w0e0e0e0w0w0w0w0 +w0w0w0w0e0e0e0e0e0w0e0e0e0e0e0w0e0e0w0w0e0e0e0e0e0e0e0e0w0w0 +w0w0e0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0w0w0w0e0e0e0e0w0 +w0w0e0e0e0e0e0e0e0e0e0e0e0e0e0w0w0w0e0e0w0w0w0w0w0e0e0e0e0w0 +w0e0e0e0w0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0w0w0w0w0w0e0w0 +w0e0e0e0w0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0w0w0w0w0w0e0w0 +w0e0e0e0e0e0e0e0e0w0e0e0e0e0w0w0w0w0e0e0w0w0w0w0w0w0w0w0w0w0 +w0w0w0w0w0w0w0w0w0w0e0e0e0w0w0w0e0e0e0e0e0e0w0w0e0e0w0w0w0w0 +w0e0e0e0e0x1e0x2e0e0e0e0e0e0w0w0e0e0e0e0e0e0w0e0e0e0e0e0e0w0 +w0e0w0e0e0e0e0e0e0e0e0e0e0w0w0w0e0e0e0e0e0e0w0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0w0w0e0e0e0e0e0w0w0w0w0e0e0e0e0w0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0w0w0e0e0w0w0w0w0w0w0w0w0w0e0e0w0e0e0e0w0w0w0w0 +w0w0w0w0w0e0w0w0w0e0e0w0w0w0w0w0w0w0e0e0e0e0w0w0e0x4e0e0e0w0 +w0w0w0w0e0e0w0w0w0e0e0w0w0w0w0w0w0w0e0e0e0e0w0w0e0e0e0e0e0w0 +w0e0e0e0e0w0w0w0w0w0e0w0e0e0w0e0e0e0w0w0w0w0w0w0w0w0w0w0w0w0 +w0e0e0e0e0w0w0w0w0w0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +w0w0w0w0w0w0w0w0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +w0w0w0w0w0w0e0w0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +w0e0e0e0e0e0e0w0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +w0e0e0e0e0e0e0w0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +w0e0w0w0w0w0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +w0e0w0w0w0w0e0e0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +w0e0e0e0e0e0e0w0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +w0e0e0e0e0e0e0w0e0e0e0w0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +w0e0e0e0e0e0e0w0e0e0e0w0w0w0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +w0w0w0w0w0w0w0w0w0w0w0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 + + + + + + + diff --git a/testing/test3.txt b/testing/test3.txt new file mode 100644 index 0000000..245733e --- /dev/null +++ b/testing/test3.txt @@ -0,0 +1,24 @@ +w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 +w0e0e0e0e0e0x3e0e0e0w0w0w0w0e0w0w0w0w0w0w0w0w0e0w0w0w0w0w0w0 +w0e0e0e0e0e0e0e0e0e0e0e0w0w0e0w0e0e0e0e0e0e0w0e0e0e0w0w0w0w0 +w0e0e0e0e0e0e0e0e0e0e0e0w0w0e0w0e0e0e0e0e0e0w0e0e0e0w0w0w0w0 +w0w0w0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0w0w0e0e0e0e0e0e0e0w0w0w0 +w0w0w0e0e0e0e0e0e0w0w0e0e0e0e0e0e0e0w0w0e0e0e0e0e0e0e0w0w0w0 +w0w0w0w0w0w0w0w0w0w0e0e0e0e0e0w0e0e0e0e0e0w0w0e0e0e0e0e0e0w0 +w0w0w0w0w0w0w0w0w0w0e0e0e0e0e0w0e0e0e0e0e0w0w0e0e0e0e0e0e0w0 +w0w0w0w0w0e0e0e0w0w0w0w0e0e0e0w0e0e0w0w0w0w0w0w0e0e0e0e0e0w0 +w0w0w0w0w0e0e0e0e0e0w0w0w0e0e0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 +w0e0w0w0e0e0e0x2e0e0w0w0e0e0e0e0w0w0e0e0e0e0w0e0e0e0e0e0e0w0 +w0e0w0w0e0e0e0e0e0e0w0w0w0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0w0 +w0e0w0w0e0e0w0w0e0w0w0w0e0e0w0w0e0e0w0e0e0e0e0e0e0e0e0e0e0w0 +w0e0e0e0e0e0w0w0e0w0w0w0w0w0w0w0w0w0w0w0w0w0w0e0w0w0w0w0w0w0 +w0w0w0w0e0e0e0e0w0w0w0w0e0e0e0e0e0e0e0e0e0e0w0e0e0x4e0e0e0w0 +w0w0w0w0e0e0e0e0w0w0w0w0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0w0 +w0e0e0e0e0e0w0w0w0w0w0w0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0w0 +w0w0w0w0w0w0w0w0w0w0w0w0e0e0e0e0e0e0e0e0e0e0w0w0w0w0w0w0w0w0 + + + + + + diff --git a/testing/test4.txt b/testing/test4.txt new file mode 100644 index 0000000..f9590c7 --- /dev/null +++ b/testing/test4.txt @@ -0,0 +1,32 @@ +w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 +w0w0w0w0w0w0w0w0e0w0w0w0w0w0w0w0w0w0w0w0e0e0e0e0e0e0e0e0e0w0 +w0w0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0w0w0e0e0w0e0w0e0w0e0e0w0 +w0w0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0w0w0w0e0w0e0w0e0w0e0e0w0 +w0e0e0e0w0w0e0e0e0w0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0w0 +w0e0e0e0w0w0e0e0e0w0w0w0e0e0w0w0w0w0w0w0w0w0w0w0e0e0w0w0w0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0 +w0w0w0w0e0e0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0e0e0e0w0 +w0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0w0 +w0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0w0 +w0e0w0w0e0e0w0w0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0w0 +w0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0w0 +w0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0w0 +w0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0w0 +w0e0w0w0e0e0w0w0e0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0w0 +w0e0e0e0e0e0e0e0e0w0e0e0e0w0w0w0e0e0w0w0w0e0e0e0e0w0e0e0e0w0 +w0w0w0w0w0w0w0w0w0w0e0e0e0w0w0w0e0e0w0w0w0e0e0e0e0w0e0e0w0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0w0w0e0e0w0e0e0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0w0w0e0e0w0e0e0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0x2e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0w0e0w0e0e0e0e0e0e0e0e0w0 +w0e0e0e0e0e0e0e0e0e0e0e0e0w0e0e0e0e0e0e0w0e0e0e0e0e0e0e0e0w0 +w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 + + + + diff --git a/tile.py b/tile.py new file mode 100644 index 0000000..3d55305 --- /dev/null +++ b/tile.py @@ -0,0 +1,44 @@ +from tkinter import * + +class Tile: + """A representation of a tile on the display""" + + shapes = {'circle' : 'o', 'cross' : 'x', 'triangle' : '^', 'none' : ' ', + 'square' : '#', 'vertical' : '|', 'horizontal' : '-'} + + def __init__(self, bgroundColor, fgroundColor = 'white', fgroundShape = ' '): + self.bgc = bgroundColor + self.fgc = fgroundColor + self.fgs = fgroundShape + + def paint(self, display, x, y): #display being a canvas + if type(display) != Canvas: + raise TypeError('Display must be a tkinter.Canvas.') + else: + tag = '(' + str(int(x)) + ', ' + str(int(y)) + ')' + display.delete(tag) #delete the old tile before creating a new one. + if self.bgc != 'clear': + display.create_rectangle((x*32, y*32, x*32+32, y*32+32), + fill = self.bgc, width = 0, tags = (tag)) + if self.fgs == Tile.shapes['triangle']: + display.create_polygon((x*32+15, y*32+2, x*32+2, y*32+30, + x*32+30, y*32+30, x*32+16, y*32+2), + fill = self.fgc, width = 0, tags = (tag)) + elif self.fgs == Tile.shapes['circle']: + display.create_oval((x*32+2, y*32+2, x*32+30, y*32+30), + fill = self.fgc, width = 0, tags = (tag)) + elif self.fgs == Tile.shapes['cross']: + display.create_line((x*32+2, y*32+2, x*32+30, y*32+30), + fill = self.fgc, width = 3, tags = (tag)) + display.create_line((x*32+30, y*32+2, x*32+2, y*32+30), + fill = self.fgc, width = 3, tags = (tag)) + elif self.fgs == Tile.shapes['square']: + display.create_rectangle((x*32+2, y*32+2, x*32+30, y*32+30), + fill = self.fgc, width = 0, tags = (tag)) + elif self.fgs == Tile.shapes['vertical']: + display.create_line((x*32+16, y*32, x*32+16, y*32+32), + fill = self.fgc, width = 3, tags = (tag)) + elif self.fgs == Tile.shapes['horizontal']: + display.create_line((x*32, y*32+16, x*32+32, y*32+16), + fill = self.fgc, width = 3, tags = (tag)) + else: pass