From adfb6b83f497383d98fa07d80944793d04446ab0 Mon Sep 17 00:00:00 2001 From: Patrick Marsee Date: Sat, 9 Mar 2019 19:08:46 -0500 Subject: [PATCH] All map files converted to new YAML standard. --- .gitignore | 1 + gamebase.py | 1276 ++++++++++++++++---------------- gameevents.py | 200 ++--- gamegui.py | 878 +++++++++++----------- gamemap.py | 1769 +++++++++++++++++++++++++------------------- gameshell.py | 426 +++++------ shell.py | 568 +++++++------- testing/test1.json | 102 +-- testing/test1.xml | 70 +- testing/test1.yml | 90 +-- testing/test2.xml | 74 +- testing/test2.yml | 62 ++ testing/test3.xml | 52 +- testing/test3.yml | 46 ++ testing/test4.xml | 68 +- testing/test4.yml | 44 ++ tile.py | 88 +-- 17 files changed, 3123 insertions(+), 2691 deletions(-) create mode 100644 testing/test2.yml create mode 100644 testing/test3.yml create mode 100644 testing/test4.yml diff --git a/.gitignore b/.gitignore index 48bed84..9c2c34a 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ maps saves __pycache__ winsh.py +yamltest.py diff --git a/gamebase.py b/gamebase.py index a849dd3..d6477af 100644 --- a/gamebase.py +++ b/gamebase.py @@ -1,638 +1,638 @@ -# 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 - self.skipLoop = True - - # 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: - self.setEvent(0.0, _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: - self.setEvent(t * speed, _ge.GoEvent(self.playerName, newx, newy)) - else: - self.setEvent(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: - self.setEvent(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] - self.setEvent(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))) - self.setEvent(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): - self.setEvent(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] - self.setEvent(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))) - self.setEvent(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): - self.setEvent(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] - self.setEvent(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))) - self.setEvent(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: - self.setEvent(0.0, _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): - #print(self.skipLoop) - if self.skipLoop: - return - #print(self.skipLoop) - while len(self.eventQueue) > 0: - ev = _hq.heappop(self.eventQueue) - self.gameTime = ev[0] - e = ev[1] - if self.__gameEvents[e.eventType](e): - #print('break loop') - break - if len(self.eventQueue) == 0: - self.gameTime = 0.0 - _ge.resetEventNum() - self.skipLoop = True - - def setEvent(self, t, e, skip = False): - _hq.heappush(self.eventQueue, (self.gameTime + t, e)) - self.skipLoop = skip - - # 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 - self.setEvent(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 - self.setEvent(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 +# 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 + self.skipLoop = True + + # 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: + self.setEvent(0.0, _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: + self.setEvent(t * speed, _ge.GoEvent(self.playerName, newx, newy)) + else: + self.setEvent(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: + self.setEvent(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] + self.setEvent(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))) + self.setEvent(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): + self.setEvent(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] + self.setEvent(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))) + self.setEvent(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): + self.setEvent(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] + self.setEvent(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))) + self.setEvent(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: + self.setEvent(0.0, _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): + #print(self.skipLoop) + if self.skipLoop: + return + #print(self.skipLoop) + while len(self.eventQueue) > 0: + ev = _hq.heappop(self.eventQueue) + self.gameTime = ev[0] + e = ev[1] + if self.__gameEvents[e.eventType](e): + #print('break loop') + break + if len(self.eventQueue) == 0: + self.gameTime = 0.0 + _ge.resetEventNum() + self.skipLoop = True + + def setEvent(self, t, e, skip = False): + _hq.heappush(self.eventQueue, (self.gameTime + t, e)) + self.skipLoop = skip + + # 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 + self.setEvent(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 + self.setEvent(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 index ef711e4..9adeef6 100644 --- a/gameevents.py +++ b/gameevents.py @@ -1,100 +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 +#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 index d0e21dd..016b5f0 100644 --- a/gamegui.py +++ b/gamegui.py @@ -1,439 +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() +# 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 index f0b3568..2623c58 100644 --- a/gamemap.py +++ b/gamemap.py @@ -1,745 +1,1024 @@ -#gamemap.py -import re -import heapq -import xml.etree.ElementTree as ET -import ruamel.yaml -from ruamel.yaml.comments import CommentedMap - -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): - yaml_tag = u'!Item' - - 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 - - @classmethod - def to_yaml(cls, representer, node): - graphic = {} - if node.graphic[0] != 'clear': - graphic['bgc'] = node.graphic[0] - if node.graphic[1] != '#00BF00': - graphic['fgc'] = node.graphic[1] - if node.graphic[2] != '^': - graphic['shape'] = node.graphic[2] - ret = {'name': node.name, 'location': (node.x, node.y), - 'description': node.description, 'graphic': graphic, - 'useFunc': node.useFunc, 'useOnFunc': node.useOnFunc, - 'customValues': node.customValues, 'ranged': node.ranged} - return representer.represent_mapping(cls.yaml_flag, ret) - - @classmethod - def from_yaml(cls, constructor, node): - parts = CommentedMap() - constructor.construct_mapping(node, parts, True) - bgc = 'clear' - fgc = '#00BF00' - shape = '^' - if 'bgc' in parts['graphic']: - bgc = parts['graphic']['bgc'] - if 'fgc' in parts['graphic']: - fgc = parts['graphic']['fgc'] - if 'shape' in parts['graphic']: - shape = parts['graphic']['shape'] - graphic = (bgc, fgc, shape) - return cls(parts['name'], parts['location'][0], parts['location'][1], - parts['description'], parts['useFunc'], parts['useOnFunc'], - dict(parts['customValues']), parts['ranged'], graphic) - -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 - tileRegex = re.compile(r'([a-z ])([0-9]+|[ ])') - 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.") - return None - 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 - l = 0 - while len(matrixStr) > 0: - tile = GameMap.tileRegex.match(matrixStr) - if tile != None: - tileType = tile.group(1) - tileNum = tile.group(2) - if tileType == ' ': - tileType = 'e' - if tileNum == ' ': - tileNum = '0' - mat[l].append((tileType, int(tileNum))) - #x += 1 - matrixStr = matrixStr[len(tile.group()):] - elif matrixStr[0] == '\n': - if x == 0: - x = len(mat[l]) - elif x != len(mat[l]): - raise RuntimeError("Map matrix has jagged edges.") - l += 1 - #x = 0 - mat.append([]) - i = 1 - while i < len(matrixStr) and matrixStr[i] in ' \t\n': - i += 1 - if i == len(matrixStr): - matrixStr = '' - else: - matrixStr = matrixStr[i:] - else: # This shouldn't happen, so if it does, there was an error. - raise RuntimeError("Unexpected token in map matrix: '{0}'".format(matrixStr)) - 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): - startThing = self.getThingAtCoords(x1, y1) - if not closeEnough: - if startThing and not startThing.passable: - return -1, [] # meaning you can't get there - dist, prev = self.dijkstra(x1, y1, closeEnough) - endPoint = self.coordsToInt(x2, y2) - numVertex = self.dimensions[0] * self.dimensions[1] - if dist[endPoint] < numVertex + 1: - return dist[endPoint], prev - else: - return -1, [] # meaning you can't get there - - def dijkstra(self, x1, y1, 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) - 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)) - - return dist, prev - #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)) - +#gamemap.py +import re +import heapq +import xml.etree.ElementTree as ET +import ruamel.yaml +from ruamel.yaml.comments import CommentedMap # for loading classes + +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): + yaml_flag = u'!Item' + defaultGraphic = ('clear', '#00BF00', '^') + + def __init__(self, name, x: int, y: int, description: str, useFunc: str, useOnFunc: str, customValues: dict, ranged: bool, graphic = defaultGraphic): + 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 + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description} + # save graphic + graphic = {} + if node.graphic[0] != Item.defaultGraphic[0]: + graphic['bgc'] = node.graphic[0] + if node.graphic[1] != Item.defaultGraphic[1]: + graphic['fgc'] = node.graphic[1] + if node.graphic[2] != Item.defaultGraphic[2]: + graphic['shape'] = node.graphic[2] + if len(graphic) > 0: + ret['graphic'] = graphic + # save use functions + if node.useFunc != '': + ret['useFunc'] = node.useFunc + if node.useOnFunc != '': + ret['useOnFunc'] = node.useOnFunc + if len(node.customValues) > 0: + ret['customValues'] = node.customValues + if node.ranged: + ret['ranged'] = node.ranged + return representer.represent_mapping(cls.yaml_flag, ret) + + @classmethod + def from_yaml(cls, constructor, node): + parts = CommentedMap() + constructor.construct_mapping(node, parts, True) + # set default values for optional arguments + useFunc = '' + useOnFunc = '' + customValues = {} + ranged = False + bgc = Item.defaultGraphic[0] + fgc = Item.defaultGraphic[1] + shape = Item.defaultGraphic[2] + # load graphic + if 'graphic' in parts: + if 'bgc' in parts['graphic']: + bgc = parts['graphic']['bgc'] + if 'fgc' in parts['graphic']: + fgc = parts['graphic']['fgc'] + if 'shape' in parts['graphic']: + shape = parts['graphic']['shape'] + graphic = (bgc, fgc, shape) + # load use functions + if 'useFunc' in parts: + useFunc = parts['useFunc'] + if 'useOnFunc' in parts: + useOnFunc = parts['useOnFunc'] + if 'customValues' in parts: + customValues = dict(parts['customValues']) + for v in customValues: + if isinstance(customValues[v], tuple): + customValues[v] = list(customValues[v]) + if 'ranged' in parts: + useOnFunc = parts['ranged'] + return cls(parts['name'], parts['location'][0], parts['location'][1], + parts['description'], useFunc, useOnFunc, customValues, ranged, graphic) + +class Useable(Thing): + yaml_flag = u'!Useable' + defaultGraphic = ('clear', '#0000FF', '#') + + def __init__(self, name, x: int, y: int, description: str, useFunc: str, customValues: dict, playerx = None, playery = None, graphic = defaultGraphic): + 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 + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description} + # save graphic + graphic = {} + if node.graphic[0] != Useable.defaultGraphic[0]: + graphic['bgc'] = node.graphic[0] + if node.graphic[1] != Useable.defaultGraphic[1]: + graphic['fgc'] = node.graphic[1] + if node.graphic[2] != Useable.defaultGraphic[2]: + graphic['shape'] = node.graphic[2] + if len(graphic) > 0: + ret['graphic'] = graphic + # save use functions + if node.useFunc != '': + ret['useFunc'] = node.useFunc + if len(node.customValues) > 0: + ret['customValues'] = node.customValues + if node.x != node.playerx or node.y != node.playery: + ret['useLocation'] = (node.playerx, node.playery) + return representer.represent_mapping(cls.yaml_flag, ret) + + @classmethod + def from_yaml(cls, constructor, node): + parts = CommentedMap() + constructor.construct_mapping(node, parts, True) + # set default values for optional arguments + useFunc = '' + customValues = {} + playerx, playery = parts['location'] + bgc = Useable.defaultGraphic[0] + fgc = Useable.defaultGraphic[1] + shape = Useable.defaultGraphic[2] + # load graphic + if 'graphic' in parts: + if 'bgc' in parts['graphic']: + bgc = parts['graphic']['bgc'] + if 'fgc' in parts['graphic']: + fgc = parts['graphic']['fgc'] + if 'shape' in parts['graphic']: + shape = parts['graphic']['shape'] + graphic = (bgc, fgc, shape) + # load use functions + if 'useFunc' in parts: + useFunc = parts['useFunc'] + if 'customValues' in parts: + customValues = dict(parts['customValues']) + for v in customValues: + if isinstance(customValues[v], tuple): + customValues[v] = list(customValues[v]) + if 'useLocation' in parts: + playerx, playery = parts['useLocation'] + return cls(parts['name'], parts['location'][0], parts['location'][1], + parts['description'], useFunc, customValues, playerx, playery, graphic) + +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): + yaml_flag = u'!Door' + defaultGraphic = ('clear', '#7F3F00', '#') + + def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = defaultGraphic): + 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 + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'name': node.name, 'location': (node.x, node.y)} + # save graphic + graphic = {} + if node.graphic[0] != Door.defaultGraphic[0]: + graphic['bgc'] = node.graphic[0] + if node.graphic[1] != Door.defaultGraphic[1]: + graphic['fgc'] = node.graphic[1] + if node.graphic[2] != Door.defaultGraphic[2]: + graphic['shape'] = node.graphic[2] + if len(graphic) > 0: + ret['graphic'] = graphic + # save door state + if node.passable: + ret['locked'] = not node.passable + if node.descBase != None: + ret['description'] = node.descBase + if node.key != None: + ret['key'] = node.key + return representer.represent_mapping(cls.yaml_flag, ret) + + @classmethod + def from_yaml(cls, constructor, node): + parts = CommentedMap() + constructor.construct_mapping(node, parts, True) + # set default values for optional arguments + description = None + locked = False + key = None + bgc = Door.defaultGraphic[0] + fgc = Door.defaultGraphic[1] + shape = Door.defaultGraphic[2] + # load graphic + if 'graphic' in parts: + if 'bgc' in parts['graphic']: + bgc = parts['graphic']['bgc'] + if 'fgc' in parts['graphic']: + fgc = parts['graphic']['fgc'] + if 'shape' in parts['graphic']: + shape = parts['graphic']['shape'] + graphic = (bgc, fgc, shape) + # load door state + if 'description' in parts: + description = parts['description'] + if 'locked' in parts: + locked = parts['locked'] + if 'key' in parts: + key = parts['key'] + return cls(parts['name'], parts['location'][0], parts['location'][1], + locked, description, key, graphic) + +class MapExit(Thing): + yaml_flag = u'!Exit' + defaultGraphic = ('clear', '#FF0000', 'x') + + def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix: None, graphic = defaultGraphic): + 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 + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'name': node.name, 'location': (node.x, node.y), 'id': node.exitid, 'destination': node.destination} + # save graphic + graphic = {} + if node.graphic[0] != MapExit.defaultGraphic[0]: + graphic['bgc'] = node.graphic[0] + if node.graphic[1] != MapExit.defaultGraphic[1]: + graphic['fgc'] = node.graphic[1] + if node.graphic[2] != MapExit.defaultGraphic[2]: + graphic['shape'] = node.graphic[2] + if len(graphic) > 0: + ret['graphic'] = graphic + if node.prefix != None: + ret['prefix'] = node.prefix + return representer.represent_mapping(cls.yaml_flag, ret) + + @classmethod + def from_yaml(cls, constructor, node): + parts = CommentedMap() + constructor.construct_mapping(node, parts, True) + # set default values for optional arguments + prefix = None + bgc = Door.defaultGraphic[0] + fgc = Door.defaultGraphic[1] + shape = Door.defaultGraphic[2] + # load graphic + if 'graphic' in parts: + if 'bgc' in parts['graphic']: + bgc = parts['graphic']['bgc'] + if 'fgc' in parts['graphic']: + fgc = parts['graphic']['fgc'] + if 'shape' in parts['graphic']: + shape = parts['graphic']['shape'] + graphic = (bgc, fgc, shape) + if 'prefix' in parts: + prefix = parts['prefix'] + return cls(parts['name'], parts['location'][0], parts['location'][1], + parts['id'], parts['destination'], prefix, graphic) + +class MapError(RuntimeError): + pass + +class GameMap(object): + # Matrix tile codes: + # e: empty (0) + # w: wall (0) + + # regular expressions + tileRegex = re.compile(r'([a-z ])([0-9]+|[ ])') + 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 __cleanStr(text: str, end = '\n'): + if text == None: + return text + text = text.strip() + i = 0 + while True: + i = text.find('\n', i) + if i == -1: + break + j = i+1 + if len(text) > j: + while text[j] in ' \t': + j += 1 # just find the first non-space chartacter + text = text[:i+1] + text[j:] + i += 1 + else: + break + return text.replace('\n', end) + + @staticmethod + def convert(infile: str): + """Convert an XML map to a YAML one.""" + data = None + with open(infile, 'r') as f: + data = f.read() + info = ET.fromstring(data) + layout = info.find('layout') + ret = {} + if layout == None: + raise MapError('No layout in {0}.'.format(infile)) + + # "Layout" needs some work before we convert it. + ret['layout'] = GameMap.__cleanStr(layout.text.strip()) + + # now the rest of the things + if 'openingText' in info.attrib: + ret['openingText'] = info.attrib['openingText'] + else: + raise MapError('No opening text in {0}.'.format(infile)) + if 'playerStart' in info.attrib: + ps = info.attrib['playerStart'].split(',') + ret['playerStart'] = (int(ps[0]), int(ps[1])) + else: + raise MapError('No player start position in {0}.'.format(infile)) + if info.text != None: + ret['description'] = GameMap.__cleanStr(info.text, ' ') + else: + raise MapError('No description in {0}.'.format(infile)) + + # get map colors + floorColors = ['#9F7F5F'] + floorColorsStr = info.find('floorColors') + if floorColorsStr != None: + floorColors = floorColorsStr.text.lstrip().split() + if len(floorColors) == 0: + floorColors.append('#9F7F5F') + ret['floorColors'] = floorColors + wallColors = ['#7F3F0F'] + wallColorsStr = info.find('wallColors') + if wallColorsStr != None: + wallColors = wallColorsStr.text.lstrip().split() + if len(wallColors) == 0: + wallColors.append('#7F3F0F') + ret['wallColors'] = wallColors + + # get things + ret['loadAlways'] = [] + ret['loadOnce'] = [] + 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. + for node1 in node: + if node1.tag == 'door': + ret['loadOnce'].append(GameMap.__loadDoor(node1)) + elif node1.tag == 'useable': + ret['loadOnce'].append(GameMap.__loadUseable(node1)) + elif node1.tag == 'item': + ret['loadOnce'].append(GameMap.__loadItem(node1)) + elif node.tag == 'exit': + ret['loadAlways'].append(GameMap.__loadExit(None, node, -1)) # weird arguments: there is no actual level + elif node.tag == 'door': + ret['loadAlways'].append(GameMap.__loadDoor(node)) + elif node.tag == 'useable': + ret['loadAlways'].append(GameMap.__loadUseable(node)) + + #start saving + outfile = infile[:-3] + 'yml' + yaml = ruamel.yaml.YAML() + yaml.indent(mapping=4, sequence=4, offset=2) + yaml.register_class(Item) + yaml.register_class(Useable) + yaml.register_class(Door) + yaml.register_class(MapExit) + with open(outfile, 'w') as f: + f.write('%YAML 1.2\n---\n') + yaml.dump(ret, f) + + @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.") + return None + 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 + l = 0 + while len(matrixStr) > 0: + tile = GameMap.tileRegex.match(matrixStr) + if tile != None: + tileType = tile.group(1) + tileNum = tile.group(2) + if tileType == ' ': + tileType = 'e' + if tileNum == ' ': + tileNum = '0' + mat[l].append((tileType, int(tileNum))) + #x += 1 + matrixStr = matrixStr[len(tile.group()):] + elif matrixStr[0] == '\n': + if x == 0: + x = len(mat[l]) + elif x != len(mat[l]): + raise RuntimeError("Map matrix has jagged edges.") + l += 1 + #x = 0 + mat.append([]) + i = 1 + while i < len(matrixStr) and matrixStr[i] in ' \t\n': + i += 1 + if i == len(matrixStr): + matrixStr = '' + else: + matrixStr = matrixStr[i:] + else: # This shouldn't happen, so if it does, there was an error. + raise RuntimeError("Unexpected token in map matrix: '{0}'".format(matrixStr)) + 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 = GameMap.__cleanStr(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 = GameMap.__cleanStr(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 = GameMap.__cleanStr(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): + startThing = self.getThingAtCoords(x1, y1) + if not closeEnough: + if startThing and not startThing.passable: + return -1, [] # meaning you can't get there + dist, prev = self.dijkstra(x1, y1, closeEnough) + endPoint = self.coordsToInt(x2, y2) + numVertex = self.dimensions[0] * self.dimensions[1] + if dist[endPoint] < numVertex + 1: + return dist[endPoint], prev + else: + return -1, [] # meaning you can't get there + + def dijkstra(self, x1, y1, 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) + 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)) + + return dist, prev + #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 index 4dc78b2..dfd3dc9 100644 --- a/gameshell.py +++ b/gameshell.py @@ -1,213 +1,213 @@ -# 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 - self.colorMode = 0 - - # 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.registerCommand('options', self.options) - self.registerCommand('colorTest', self.colorTest) - 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 options(self, args): - i = 0 - while i < len(args): - if args[i] == 'color': - i += 1 - if args[i] in ('0', '3', '4', '8', '24'): - self.colorMode = int(args[i]) - i += 1 - - def __colorTestRoutine(self, lower, upper): - colors = [] - step = int((upper - lower) / 8) - #print(step) - for g in range(lower, upper, step): - colors.append(self.color(upper-1, g, lower, False)) - #print(len(colors)) - colors.append(self.color(upper-1, upper-1, lower, False)) - for r in range(upper-step, lower-1, -step): - colors.append(self.color(r, upper-1, lower, False)) - #print(len(colors)) - for b in range(lower+step, upper, step): - colors.append(self.color(lower, upper-1, b, False)) - #print(len(colors)) - colors.append(self.color(lower, upper-1, upper-1, False)) - for g in range(upper-step, lower-1, -step): - colors.append(self.color(lower, g, upper-1, False)) - #print(len(colors)) - for r in range(lower+step, upper, step): - colors.append(self.color(r, lower, upper-1, False)) - colors.append(self.color(upper-1, lower, upper-1, False)) - #print(len(colors)) - for b in range(upper-step, lower, -step): - colors.append(self.color(upper-1, lower, b, False)) - #print(len(colors)) - colors.append('\x1b[0m') - print(' '.join(colors)) - - def colorTest(self, args): - for i in (3, 4, 8, 24): - print(i) - self.colorMode = i - self.__colorTestRoutine(0, 128) # dark - self.__colorTestRoutine(0, 256) # medium - self.__colorTestRoutine(128, 256) # light - - 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 = {} - level = self.gameBase.level - textColor = level.wallColors[0] - floorColor = level.floorColors[0] - for y in range(level.dimensions[1]): - rows.append(['{0}{1:2} {2}{3}'.format(self.clearColor(), y, self.color(textColor[1:]), self.color(floorColor[1:], fg = False))]) - rows[-1].append(self.color(textColor[1:])) - for x in range(level.dimensions[0]): - pos = level.mapMatrix[y][x] - thing = level.getThingAtPos(index) - if x == self.gameBase.playerx and y == self.gameBase.playery: - if '#0000FF' != textColor: - textColor = '#0000FF' - rows[-1].append(self.color(textColor[1:])) - rows[-1].append('()') - elif thing: - if thing.graphic[1] != textColor: - textColor = thing.graphic[1] - rows[-1].append(self.color(textColor[1:])) - if thing.thingType == 'x': # exit - rows[-1].append('X{0}'.format(thing.exitid)) - exits[thing.exitid] = (thing.name, thing.graphic[1]) - elif thing.thingType == 'd': # door - doors[len(doors)+1] = (thing.name, thing.graphic[1]) - rows[-1].append('D{0}'.format(len(doors))) - elif thing.thingType == 'u': # useable - useables[len(useables)+1] = (thing.name, thing.graphic[1]) - rows[-1].append('U{0}'.format(len(useables))) - elif thing.thingType == 'i': # item - items[len(items)+1] = (thing.name, thing.graphic[1]) - rows[-1].append('I{0}'.format(len(items))) - elif pos[0] == 'w': - if level.wallColors[level.mapMatrix[y][x][1]] != textColor: - textColor = level.wallColors[level.mapMatrix[y][x][1]] - rows[-1].append(self.color(textColor[1:])) - sides = 0 - if y > 0 and level.mapMatrix[y-1][x][0] == 'w': - sides += GameShell.UP - if x < level.dimensions[0]-1 and level.mapMatrix[y][x+1][0] == 'w': - sides += GameShell.RIGHT - if y < level.dimensions[1]-1 and level.mapMatrix[y+1][x][0] == 'w': - sides += GameShell.DOWN - if x > 0 and 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('{0}\n'.format(self.clearColor(False)).join(rows) + self.clearColor()) - self.setClearColor() - - if len(args) > 0: - if args[0] == '-l' or args[0] == 'l' or args[0] == 'legend': - legend = ["\n---Legend---\n", "{0}(){1} - {2}".format(self.color('0000FF'), self.clearColor(), self.gameBase.playerName), - "Xn - Exit to another area"] - for i in exits: - legend.append(' {0}X{1}{2} - {3}'.format(self.color(exits[i][1][1:]), i, self.clearColor(), exits[i][0])) - legend.append("Un - Useable object") - for i in useables: - legend.append(' {0}U{1}{2} - {3}'.format(self.color(useables[i][1][1:]), i, self.clearColor(), useables[i][0])) - legend.append("In - Item") - for i in items: - legend.append(' {0}I{1}{2} - {3}'.format(self.color(items[i][1][1:]), i, self.clearColor(), items[i][0])) - legend.append("Dn - Door") - for i in doors: - legend.append(' {0}D{1}{2} - {3}'.format(self.color(doors[i][1][1:]), i, self.clearColor(), doors[i][0])) - 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() +# 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 + self.colorMode = 0 + + # 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.registerCommand('options', self.options) + self.registerCommand('colorTest', self.colorTest) + 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 options(self, args): + i = 0 + while i < len(args): + if args[i] == 'color': + i += 1 + if args[i] in ('0', '3', '4', '8', '24'): + self.colorMode = int(args[i]) + i += 1 + + def __colorTestRoutine(self, lower, upper): + colors = [] + step = int((upper - lower) / 8) + #print(step) + for g in range(lower, upper, step): + colors.append(self.color(upper-1, g, lower, False)) + #print(len(colors)) + colors.append(self.color(upper-1, upper-1, lower, False)) + for r in range(upper-step, lower-1, -step): + colors.append(self.color(r, upper-1, lower, False)) + #print(len(colors)) + for b in range(lower+step, upper, step): + colors.append(self.color(lower, upper-1, b, False)) + #print(len(colors)) + colors.append(self.color(lower, upper-1, upper-1, False)) + for g in range(upper-step, lower-1, -step): + colors.append(self.color(lower, g, upper-1, False)) + #print(len(colors)) + for r in range(lower+step, upper, step): + colors.append(self.color(r, lower, upper-1, False)) + colors.append(self.color(upper-1, lower, upper-1, False)) + #print(len(colors)) + for b in range(upper-step, lower, -step): + colors.append(self.color(upper-1, lower, b, False)) + #print(len(colors)) + colors.append('\x1b[0m') + print(' '.join(colors)) + + def colorTest(self, args): + for i in (3, 4, 8, 24): + print(i) + self.colorMode = i + self.__colorTestRoutine(0, 128) # dark + self.__colorTestRoutine(0, 256) # medium + self.__colorTestRoutine(128, 256) # light + + 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 = {} + level = self.gameBase.level + textColor = level.wallColors[0] + floorColor = level.floorColors[0] + for y in range(level.dimensions[1]): + rows.append(['{0}{1:2} {2}{3}'.format(self.clearColor(), y, self.color(textColor[1:]), self.color(floorColor[1:], fg = False))]) + rows[-1].append(self.color(textColor[1:])) + for x in range(level.dimensions[0]): + pos = level.mapMatrix[y][x] + thing = level.getThingAtPos(index) + if x == self.gameBase.playerx and y == self.gameBase.playery: + if '#0000FF' != textColor: + textColor = '#0000FF' + rows[-1].append(self.color(textColor[1:])) + rows[-1].append('()') + elif thing: + if thing.graphic[1] != textColor: + textColor = thing.graphic[1] + rows[-1].append(self.color(textColor[1:])) + if thing.thingType == 'x': # exit + rows[-1].append('X{0}'.format(thing.exitid)) + exits[thing.exitid] = (thing.name, thing.graphic[1]) + elif thing.thingType == 'd': # door + doors[len(doors)+1] = (thing.name, thing.graphic[1]) + rows[-1].append('D{0}'.format(len(doors))) + elif thing.thingType == 'u': # useable + useables[len(useables)+1] = (thing.name, thing.graphic[1]) + rows[-1].append('U{0}'.format(len(useables))) + elif thing.thingType == 'i': # item + items[len(items)+1] = (thing.name, thing.graphic[1]) + rows[-1].append('I{0}'.format(len(items))) + elif pos[0] == 'w': + if level.wallColors[level.mapMatrix[y][x][1]] != textColor: + textColor = level.wallColors[level.mapMatrix[y][x][1]] + rows[-1].append(self.color(textColor[1:])) + sides = 0 + if y > 0 and level.mapMatrix[y-1][x][0] == 'w': + sides += GameShell.UP + if x < level.dimensions[0]-1 and level.mapMatrix[y][x+1][0] == 'w': + sides += GameShell.RIGHT + if y < level.dimensions[1]-1 and level.mapMatrix[y+1][x][0] == 'w': + sides += GameShell.DOWN + if x > 0 and 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('{0}\n'.format(self.clearColor(False)).join(rows) + self.clearColor()) + self.setClearColor() + + if len(args) > 0: + if args[0] == '-l' or args[0] == 'l' or args[0] == 'legend': + legend = ["\n---Legend---\n", "{0}(){1} - {2}".format(self.color('0000FF'), self.clearColor(), self.gameBase.playerName), + "Xn - Exit to another area"] + for i in exits: + legend.append(' {0}X{1}{2} - {3}'.format(self.color(exits[i][1][1:]), i, self.clearColor(), exits[i][0])) + legend.append("Un - Useable object") + for i in useables: + legend.append(' {0}U{1}{2} - {3}'.format(self.color(useables[i][1][1:]), i, self.clearColor(), useables[i][0])) + legend.append("In - Item") + for i in items: + legend.append(' {0}I{1}{2} - {3}'.format(self.color(items[i][1][1:]), i, self.clearColor(), items[i][0])) + legend.append("Dn - Door") + for i in doors: + legend.append(' {0}D{1}{2} - {3}'.format(self.color(doors[i][1][1:]), i, self.clearColor(), doors[i][0])) + 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 index 8a683da..c414002 100644 --- a/shell.py +++ b/shell.py @@ -1,284 +1,284 @@ -#!/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.0033 - - -import types as _types -import traceback as _tb -import math as _math -import re as _re - -_CONV24TO8 = 6 / 256 - -class Shell(object): - - def __init__(self): - self.__commands = {'exit': self.exitShell, 'batch': self.batch, 'alias': self.makeAlias, 'def': self.defineBatch} - self.__aliases = {} - self.__batches = {} - self.ps1 = '> ' - self.ps2 = '. ' - self.colorMode = 0 # bits of color depth. Supports: 0, 3, 4, 8, 24 - self.prevColor = '\x1b[0m' - self.__exit = False - - def __color24(self, r, g, b, fg = True): - if fg: - return '\x1b[38;2;{0};{1};{2}m'.format(r, g, b) - else: - return '\x1b[48;2;{0};{1};{2}m'.format(r, g, b) - - def __color8(self, r, g, b, fg = True): - r = _math.floor(r * _CONV24TO8) - g = _math.floor(g * _CONV24TO8) - b = _math.floor(b * _CONV24TO8) - ret = 16 + b + 6 * g + 36 * r - if fg: - return '\x1b[38;5;{0}m'.format(ret) - else: - return '\x1b[48;5;{0}m'.format(ret) - - def __color4(self, r, g, b, fg = True): - color = _math.floor(r / 128) + 2 * _math.floor(g / 128) + 4 * _math.floor(b / 128) - r2, g2, b2 = r % 128, g % 128, b % 128 - if r2 + g2 + b2 >= 192: - color += 60 - if fg: - color += 30 - else: - color += 40 - return '\x1b[{0}m'.format(color) - - def __color3(self, r, g, b, fg = True): - color = _math.floor(r / 128) + 2 * _math.floor(g / 128) + 4 * _math.floor(b / 128) - if fg: - color += 30 - else: - color += 40 - return '\x1b[{0}m'.format(color) - - def colorFromHex(self, color): - """expects string formmatted like 3377DD""" - return int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16) - - def color(self, r, g = 0, b = 0, fg = True, setPrevColor = True): - if isinstance(r, str): - r, g, b = self.colorFromHex(r) - ret = '' - if self.colorMode == 0: # no color - ret = '' - elif self.colorMode == 3: - ret = self.__color3(r, g, b, fg) - elif self.colorMode == 4: - ret = self.__color4(r, g, b, fg) - elif self.colorMode == 8: - ret = self.__color8(r, g, b, fg) - elif self.colorMode == 24: - ret = self.__color24(r, g, b, fg) - else: - ret = '' - if ret == self.prevColor: - return '' - if setPrevColor: - self.prevColor = ret - return ret - - def setColor(self, r, g = 0, b = 0, fg = True): - """Set the text color.""" - print(color(r, g, b, fg), end = '') - return - - def clearColor(self, setPrevColor = True): - ret = '' - if self.colorMode > 0: - ret = '\x1b[0m' - if ret == self.prevColor: - return '' - if setPrevColor: - self.prevColor = ret - return ret - - def setClearColor(self): - print(self.clearColor()) - return - - 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) - self.handleCommand(command) - self.update() - self.__exit = False - - 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 registerBatch(self, name: str, commands: list): - self.__batches[name] = commands - - def getAlias(self, a: str): - if a in self.__aliases: - return self.__aliases[a] - else: - return None - - def getBatch(self, name: str): - if name in self.__batches: - return ['batch'] - else: - return None - - def scanLine(self, instr): - """Take a line of text and turn it into an argument list""" - if instr == '': - return [] - inQuotes = False - ret = [] - argStart = 0 - c = 0 - while c < len(instr): - if inQuotes: - if instr[c] == '"': - inQuotes = False - ret.append(instr[argStart:c]) - argStart = c+1 - else: - if instr[c] == '"': - inQuotes = True - if argStart != c: - ret.append(instr[argStart:c]) - argStart = c+1 - elif instr[c] in ' \t\n': - if argStart != c: - ret.append(instr[argStart:c]) - argStart = c + 1 - c += 1 - if argStart != c: - ret.append(instr[argStart:c]) - a = self.getAlias(ret[0]) - if a: - ret = a + ret[1:] - b = self.getBatch(ret[0]) - if b: - ret = b + ret - ret[0] = _re.sub(r'\A\$\((\w+)\)', r'\1', ret[0]) - return ret - - # Default functions - - def exitShell(self, args): - """The default exit command.""" - self.__exit = True - - def makeAlias(self, args): - """Make an alias.""" - self.registerAlias(args[0], args[1:]) - - def defineBatch(self, args): - """Define a batch file.""" - if len(args) < 1: - raise ValueError('def takes at least one argument') - ret = [] - command = input(self.ps2) - while command != 'end': - ret.append(command) - command = input(self.ps2) - for i in args: - self.registerBatch(i, ret) - - def batch(self, args): - """Run commands in batch mode -$0 is the name of the batch -$1 - $n are individual arguments -$* is all the arguments except 0 -$>0 - $>n is all arguments after the nth -$>=0 - $>=n is the nth and later arguments""" - script = self.__batches[args[0]] - var = _re.compile(r'\$((?:[>][=]?)?[0-9]+)') - u = False - for line in script: - if u: - self.update() - else: - u = True - newLine = line.replace(r'$*', ' '.join(args[1:])) - matches = var.finditer(newLine) - for m in matches: - n = m.group(1) - if n[0] == '>': - if n[1] == '=': - num = int(n[2:]) - else: - num = int(n[1:])+1 - newLine = newLine.replace(m.group(), ' '.join(args[num:])) - else: - newLine = newLine.replace(m.group(), args[int(n)]) - newLine = _re.sub(r'\A({0})'.format(args[0]), r'$(\1)', newLine) - #print(newLine) - self.handleCommand(self.scanLine(newLine)) - - # 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.""" - return self.scanLine(input()) - - def handleCommand(self, command): - if len(command) == 0: - return - 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) - - 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)) +#!/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.0033 + + +import types as _types +import traceback as _tb +import math as _math +import re as _re + +_CONV24TO8 = 6 / 256 + +class Shell(object): + + def __init__(self): + self.__commands = {'exit': self.exitShell, 'batch': self.batch, 'alias': self.makeAlias, 'def': self.defineBatch} + self.__aliases = {} + self.__batches = {} + self.ps1 = '> ' + self.ps2 = '. ' + self.colorMode = 0 # bits of color depth. Supports: 0, 3, 4, 8, 24 + self.prevColor = '\x1b[0m' + self.__exit = False + + def __color24(self, r, g, b, fg = True): + if fg: + return '\x1b[38;2;{0};{1};{2}m'.format(r, g, b) + else: + return '\x1b[48;2;{0};{1};{2}m'.format(r, g, b) + + def __color8(self, r, g, b, fg = True): + r = _math.floor(r * _CONV24TO8) + g = _math.floor(g * _CONV24TO8) + b = _math.floor(b * _CONV24TO8) + ret = 16 + b + 6 * g + 36 * r + if fg: + return '\x1b[38;5;{0}m'.format(ret) + else: + return '\x1b[48;5;{0}m'.format(ret) + + def __color4(self, r, g, b, fg = True): + color = _math.floor(r / 128) + 2 * _math.floor(g / 128) + 4 * _math.floor(b / 128) + r2, g2, b2 = r % 128, g % 128, b % 128 + if r2 + g2 + b2 >= 192: + color += 60 + if fg: + color += 30 + else: + color += 40 + return '\x1b[{0}m'.format(color) + + def __color3(self, r, g, b, fg = True): + color = _math.floor(r / 128) + 2 * _math.floor(g / 128) + 4 * _math.floor(b / 128) + if fg: + color += 30 + else: + color += 40 + return '\x1b[{0}m'.format(color) + + def colorFromHex(self, color): + """expects string formmatted like 3377DD""" + return int(color[0:2], 16), int(color[2:4], 16), int(color[4:6], 16) + + def color(self, r, g = 0, b = 0, fg = True, setPrevColor = True): + if isinstance(r, str): + r, g, b = self.colorFromHex(r) + ret = '' + if self.colorMode == 0: # no color + ret = '' + elif self.colorMode == 3: + ret = self.__color3(r, g, b, fg) + elif self.colorMode == 4: + ret = self.__color4(r, g, b, fg) + elif self.colorMode == 8: + ret = self.__color8(r, g, b, fg) + elif self.colorMode == 24: + ret = self.__color24(r, g, b, fg) + else: + ret = '' + if ret == self.prevColor: + return '' + if setPrevColor: + self.prevColor = ret + return ret + + def setColor(self, r, g = 0, b = 0, fg = True): + """Set the text color.""" + print(color(r, g, b, fg), end = '') + return + + def clearColor(self, setPrevColor = True): + ret = '' + if self.colorMode > 0: + ret = '\x1b[0m' + if ret == self.prevColor: + return '' + if setPrevColor: + self.prevColor = ret + return ret + + def setClearColor(self): + print(self.clearColor()) + return + + 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) + self.handleCommand(command) + self.update() + self.__exit = False + + 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 registerBatch(self, name: str, commands: list): + self.__batches[name] = commands + + def getAlias(self, a: str): + if a in self.__aliases: + return self.__aliases[a] + else: + return None + + def getBatch(self, name: str): + if name in self.__batches: + return ['batch'] + else: + return None + + def scanLine(self, instr): + """Take a line of text and turn it into an argument list""" + if instr == '': + return [] + inQuotes = False + ret = [] + argStart = 0 + c = 0 + while c < len(instr): + if inQuotes: + if instr[c] == '"': + inQuotes = False + ret.append(instr[argStart:c]) + argStart = c+1 + else: + if instr[c] == '"': + inQuotes = True + if argStart != c: + ret.append(instr[argStart:c]) + argStart = c+1 + elif instr[c] in ' \t\n': + if argStart != c: + ret.append(instr[argStart:c]) + argStart = c + 1 + c += 1 + if argStart != c: + ret.append(instr[argStart:c]) + a = self.getAlias(ret[0]) + if a: + ret = a + ret[1:] + b = self.getBatch(ret[0]) + if b: + ret = b + ret + ret[0] = _re.sub(r'\A\$\((\w+)\)', r'\1', ret[0]) + return ret + + # Default functions + + def exitShell(self, args): + """The default exit command.""" + self.__exit = True + + def makeAlias(self, args): + """Make an alias.""" + self.registerAlias(args[0], args[1:]) + + def defineBatch(self, args): + """Define a batch file.""" + if len(args) < 1: + raise ValueError('def takes at least one argument') + ret = [] + command = input(self.ps2) + while command != 'end': + ret.append(command) + command = input(self.ps2) + for i in args: + self.registerBatch(i, ret) + + def batch(self, args): + """Run commands in batch mode +$0 is the name of the batch +$1 - $n are individual arguments +$* is all the arguments except 0 +$>0 - $>n is all arguments after the nth +$>=0 - $>=n is the nth and later arguments""" + script = self.__batches[args[0]] + var = _re.compile(r'\$((?:[>][=]?)?[0-9]+)') + u = False + for line in script: + if u: + self.update() + else: + u = True + newLine = line.replace(r'$*', ' '.join(args[1:])) + matches = var.finditer(newLine) + for m in matches: + n = m.group(1) + if n[0] == '>': + if n[1] == '=': + num = int(n[2:]) + else: + num = int(n[1:])+1 + newLine = newLine.replace(m.group(), ' '.join(args[num:])) + else: + newLine = newLine.replace(m.group(), args[int(n)]) + newLine = _re.sub(r'\A({0})'.format(args[0]), r'$(\1)', newLine) + #print(newLine) + self.handleCommand(self.scanLine(newLine)) + + # 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.""" + return self.scanLine(input()) + + def handleCommand(self, command): + if len(command) == 0: + return + 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) + + 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.json b/testing/test1.json index f20a3af..0dcbaf6 100644 --- a/testing/test1.json +++ b/testing/test1.json @@ -1,51 +1,51 @@ -{ - "openingText": "Floor 1 map loaded successfully. Normally, this would describe the environment.", - "playerStart": [5, 26], - "layout": [ - "w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w ", - "w w ", - "w w w w w w w w w w w ", - "w w w w w w w ", - "w w w w w w w w w w w ", - "w w w w w w w w w ", - "w w w w w w ", - "w w w w w w w w w w w w ", - "w w w w w w w w w w w w w ", - "w w w w w w w w w w w w w w w w w w ", - "w w w w w w w w w w w ", - "w w w w w w w w w ", - "w w w w w w w ", - "w w w w w w w w w w w w w w w w w w ", - "w w w w w w w w w w ", - "w w w w w w ", - "w w w w w w ", - "w w w w w w w w w w ", - "w w w w w w w w w w ", - "w w w w w w w w w w w w ", - "w w w w w w ", - "w w w w ", - "w w w w w w ", - "w w w w w w w w w w ", - "w w w w w w w ", - "w w w w w ", - "w w w w w w ", - "w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w " - ], - "things": [ - { - "type": "exit", - "id": 1, - "location": [5, 10], - "destination": "testing/test2.xml", - "name": "upstairs" - }, - { - "type": "exit", - "id": 2, - "location": [21, 23], - "destination": "testing/test4.xml", - "name": "downstairs" - } - ] -} - +{ + "openingText": "Floor 1 map loaded successfully. Normally, this would describe the environment.", + "playerStart": [5, 26], + "layout": [ + "w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w ", + "w w ", + "w w w w w w w w w w w ", + "w w w w w w w ", + "w w w w w w w w w w w ", + "w w w w w w w w w ", + "w w w w w w ", + "w w w w w w w w w w w w ", + "w w w w w w w w w w w w w ", + "w w w w w w w w w w w w w w w w w w ", + "w w w w w w w w w w w ", + "w w w w w w w w w ", + "w w w w w w w ", + "w w w w w w w w w w w w w w w w w w ", + "w w w w w w w w w w ", + "w w w w w w ", + "w w w w w w ", + "w w w w w w w w w w ", + "w w w w w w w w w w ", + "w w w w w w w w w w w w ", + "w w w w w w ", + "w w w w ", + "w w w w w w ", + "w w w w w w w w w w ", + "w w w w w w w ", + "w w w w w ", + "w w w w w w ", + "w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w " + ], + "things": [ + { + "type": "exit", + "id": 1, + "location": [5, 10], + "destination": "testing/test2.xml", + "name": "upstairs" + }, + { + "type": "exit", + "id": 2, + "location": [21, 23], + "destination": "testing/test4.xml", + "name": "downstairs" + } + ] +} + diff --git a/testing/test1.xml b/testing/test1.xml index bf56fe4..943c181 100644 --- a/testing/test1.xml +++ b/testing/test1.xml @@ -1,35 +1,35 @@ - - - w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w - w w - w w w w w w w w w w w - w w w w w w w - w w w w w w w w w w w - w w w w w w w w w - w w w w w w - w w w w w w w w w w w w - w w w w w w w w w w w w w - w w w w w w w w w w w w w w w w w w - w w w w w w w w w w w - w w w w w w w w w - w w w w w w w - w w w w w w w w w w w w w w w w w w - w w w w w w w w w w - w w w w w w - w w w w w w - w w w w w w w w w w - w w w w w w w w w w - w w w w w w w w w w w w - w w w w w w - w w w w - w w w w w w - w w w w w w w w w w - w w w w w w w - w w w w w - w w w w w w - w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w - - - - - + + + w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w + w w + w w w w w w w w w w w + w w w w w w w + w w w w w w w w w w w + w w w w w w w w w + w w w w w w + w w w w w w w w w w w w + w w w w w w w w w w w w w + w w w w w w w w w w w w w w w w w w + w w w w w w w w w w w + w w w w w w w w w + w w w w w w w + w w w w w w w w w w w w w w w w w w + w w w w w w w w w w + w w w w w w + w w w w w w + w w w w w w w w w w + w w w w w w w w w w + w w w w w w w w w w w w + w w w w w w + w w w w + w w w w w w + w w w w w w w w w w + w w w w w w w + w w w w w + w w w w w w + w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w + + + + + diff --git a/testing/test1.yml b/testing/test1.yml index 431cc70..1816ee9 100644 --- a/testing/test1.yml +++ b/testing/test1.yml @@ -1,45 +1,45 @@ -%YAML 1.2 ---- -openingText: Floor 1 map loaded successfully. Normally, this would describe the environment. -playerStart: [5, 26] -layout: | - w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w0 - w w0 - w w w w w w w w w w w0 - w w w w w w w0 - w w w w w w w w w w w0 - w w w w w w w w w0 - w w w w w w0 - w w w w w w w w w w w w0 - w w w w w w w w w w w w w0 - W w w w w w w w w w w w w w w w w w0 - w w w w w w w w w w w0 - w w w w w w w w w0 - w w w w w w w0 - w w w w w w w w w w w w w w w w w w0 - w w w w w w w w w w0 - w w w w w w0 - w w w w w w0 - w w w w w w w w w w0 - w w w w w w w w w w0 - w w w w w w w w w w w w0 - w w w w w w0 - w w w w0 - w w w w w w0 - w w w w w w w w w w0 - w w w w w w w0 - w w w w w0 - w w w w w w0 - w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w0 -things: - - !Exit - id: 1 - location: [5, 10] - destination: testing/test2.xml - name: upstairs - - !Exit - id: 2 - location: [21, 23] - destination: testing/test4.xml - name: downstairs - +%YAML 1.2 +--- +openingText: Floor 1 map loaded successfully. Normally, this would describe the environment. +playerStart: [5, 26] +layout: | + w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w0 + w w0 + w w w w w w w w w w w0 + w w w w w w w0 + w w w w w w w w w w w0 + w w w w w w w w w0 + w w w w w w0 + w w w w w w w w w w w w0 + w w w w w w w w w w w w w0 + W w w w w w w w w w w w w w w w w w0 + w w w w w w w w w w w0 + w w w w w w w w w0 + w w w w w w w0 + w w w w w w w w w w w w w w w w w w0 + w w w w w w w w w w0 + w w w w w w0 + w w w w w w0 + w w w w w w w w w w0 + w w w w w w w w w w0 + w w w w w w w w w w w w0 + w w w w w w0 + w w w w0 + w w w w w w0 + w w w w w w w w w w0 + w w w w w w w0 + w w w w w0 + w w w w w w0 + w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w0 +loadAlways: + - !Exit + id: 1 + location: [5, 10] + destination: testing/test2.yml + name: upstairs + - !Exit + id: 2 + location: [21, 23] + destination: testing/test4.yml + name: downstairs + diff --git a/testing/test2.xml b/testing/test2.xml index ad4eded..9fa9e96 100644 --- a/testing/test2.xml +++ b/testing/test2.xml @@ -1,37 +1,37 @@ - - - w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w - w w w w w w x3 w w w w w w w w w - w w w w w w w w w w w w w - w w w w w w w w w w - w w w w w w w - w w w w w w w w w w w - w w w w w w w w w w - w w w w w w w w w w - w w w w w w w w w w w w w w w w - w w w w w w w w w w w w w w w w w w w - w x1 x2 w w w w - w w w w w w w - w w w w w w w w w - w w w w w w w w w w w w w w w w w - w w w w w w w w w w w w w w w w w x4 w - w w w w w w w w w w w w w w w w w - w w w w w w w w w w w w w w w w w w w w - w w w w w w w w - w w w w w w w w w w - w w w w w w w w w - w w w w - w w w w - w w w w w w - w w w w w w w - w w w w - w w w w - w w w w w w - w w w w w w w w w w w w - - - - - - - + + + w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w + w w w w w w x3 w w w w w w w w w + w w w w w w w w w w w w w + w w w w w w w w w w + w w w w w w w + w w w w w w w w w w w + w w w w w w w w w w + w w w w w w w w w w + w w w w w w w w w w w w w w w w + w w w w w w w w w w w w w w w w w w w + w x1 x2 w w w w + w w w w w w w + w w w w w w w w w + w w w w w w w w w w w w w w w w w + w w w w w w w w w w w w w w w w w x4 w + w w w w w w w w w w w w w w w w w + w w w w w w w w w w w w w w w w w w w w + w w w w w w w w + w w w w w w w w w w + w w w w w w w w w + w w w w + w w w w + w w w w w w + w w w w w w w + w w w w + w w w w + w w w w w w + w w w w w w w w w w w w + + + + + + + diff --git a/testing/test2.yml b/testing/test2.yml new file mode 100644 index 0000000..dfb911c --- /dev/null +++ b/testing/test2.yml @@ -0,0 +1,62 @@ +%YAML 1.2 +--- +layout: | + w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 + w0w0w0w0w0w0 w0 w0 w0 w0w0w0w0w0w0 + w0w0w0w0 w0 w0 w0w0 w0 w0w0w0w0 + w0w0w0w0 w0 w0 w0w0 w0w0 + w0w0 w0 w0w0w0 w0 + w0w0 w0w0w0 w0w0w0w0w0 w0 + w0 w0 w0 w0 w0w0w0w0w0 w0 + w0 w0 w0 w0 w0w0w0w0w0 w0 + w0 w0 w0w0w0w0 w0w0w0w0w0w0w0w0w0w0 + w0w0w0w0w0w0w0w0w0w0 w0w0w0 w0w0 w0w0w0w0 + w0 w0w0 w0 w0 + w0 w0 w0w0w0 w0 w0 + w0 w0w0 w0w0w0w0 w0 w0 + w0 w0w0 w0w0w0w0w0w0w0w0w0 w0 w0w0w0w0 + w0w0w0w0w0 w0w0w0 w0w0w0w0w0w0w0 w0w0 w0 + w0w0w0w0 w0w0w0 w0w0w0w0w0w0w0 w0w0 w0 + w0 w0w0w0w0w0 w0 w0 w0w0w0w0w0w0w0w0w0w0w0w0 + w0 w0w0w0w0w0 w0 w0 e0 + w0w0w0w0w0w0w0w0 w0 w0 e0 + w0w0w0w0w0w0 w0 w0 w0 e0 + w0 w0 w0 w0 e0 + w0 w0 w0 w0 e0 + w0 w0w0w0w0 w0 e0 + w0 w0w0w0w0 w0 w0 e0 + w0 w0 w0 w0 e0 + w0 w0 w0 w0 e0 + w0 w0 w0w0w0w0 e0 + w0w0w0w0w0w0w0w0w0w0w0w0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0e0 +openingText: Floor 2 +playerStart: [6, 10] +description: '' +floorColors: ['#9F7F5F'] +wallColors: ['#7F3F0F'] +loadAlways: + - !Exit + name: downstairs + location: [5, 10] + id: 1 + destination: testing/test1.yml + graphic: {fgc: '#7F7F7F', shape: '#'} + - !Exit + name: upstairs + location: [7, 10] + id: 2 + destination: testing/test3.yml + graphic: {fgc: '#7F7F7F', shape: '#'} + - !Exit + name: north + location: [6, 1] + id: 3 + destination: testing/test3.yml + graphic: {fgc: '#7F7F7F', shape: '#'} + - !Exit + name: east + location: [25, 14] + id: 4 + destination: testing/test3.yml + graphic: {fgc: '#7F7F7F', shape: '#'} +loadOnce: [] diff --git a/testing/test3.xml b/testing/test3.xml index 43e4932..34972eb 100644 --- a/testing/test3.xml +++ b/testing/test3.xml @@ -1,26 +1,26 @@ - - - w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w - w x3 w w w w w w w w w w w w w w w w w w - w w w w w w w w w - w w w w w w w w w - w w w w w w w w w - w w w w w w w w w w - w w w w w w w w w w w w w w - w w w w w w w w w w w w w w - w w w w w w w w w w w w w w w w w - w w w w w w w w w w w w w w w w w w w w w w w - w w w x2 w w w w w w - w w w w w w w w - w w w w w w w w w w w w - w w w w w w w w w w w w w w w w w w w w w w w - w w w w w w w w w x4 w - w w w w w w w w w w - w w w w w w w w w - w w w w w w w w w w w w w w w w w w w w - - - - - - + + + w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w + w x3 w w w w w w w w w w w w w w w w w w + w w w w w w w w w + w w w w w w w w w + w w w w w w w w w + w w w w w w w w w w + w w w w w w w w w w w w w w + w w w w w w w w w w w w w w + w w w w w w w w w w w w w w w w w + w w w w w w w w w w w w w w w w w w w w w w w + w w w x2 w w w w w w + w w w w w w w w + w w w w w w w w w w w w + w w w w w w w w w w w w w w w w w w w w w w w + w w w w w w w w w x4 w + w w w w w w w w w w + w w w w w w w w w + w w w w w w w w w w w w w w w w w w w w + + + + + + diff --git a/testing/test3.yml b/testing/test3.yml new file mode 100644 index 0000000..d09575d --- /dev/null +++ b/testing/test3.yml @@ -0,0 +1,46 @@ +%YAML 1.2 +--- +layout: | + w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 + w0 w0w0w0w0 w0w0w0w0w0w0w0w0 w0w0w0w0w0w0 + w0 w0w0 w0 w0 w0w0w0w0 + w0 w0w0 w0 w0 w0w0w0w0 + w0w0w0 w0 w0w0 w0w0w0 + w0w0w0 w0w0 w0w0 w0w0w0 + w0w0w0w0w0w0w0w0w0w0 w0 w0w0 w0 + w0w0w0w0w0w0w0w0w0w0 w0 w0w0 w0 + w0w0w0w0w0 w0w0w0w0 w0 w0w0w0w0w0w0 w0 + w0w0w0w0w0 w0w0w0 w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 + w0 w0w0 w0w0 w0w0 w0 w0 + w0 w0w0 w0w0w0 w0 w0 + w0 w0w0 w0w0 w0w0w0 w0w0 w0 w0 + w0 w0w0 w0w0w0w0w0w0w0w0w0w0w0w0w0w0 w0w0w0w0w0w0 + w0w0w0w0 w0w0w0w0 w0 w0 + w0w0w0w0 w0w0w0w0 w0 w0 + w0 w0w0w0w0w0w0 w0 w0 + w0w0w0w0w0w0w0w0w0w0w0w0e0e0e0e0e0e0e0e0e0e0w0w0w0w0w0w0w0w0 +openingText: Floor 3 +playerStart: [6, 10] +description: '' +floorColors: ['#9F7F5F'] +wallColors: ['#7F3F0F'] +loadAlways: + - !Exit + name: downstairs + location: [7, 10] + id: 2 + destination: testing/test2.yml + graphic: {fgc: '#7F7F7F', shape: '#'} + - !Exit + name: north + location: [6, 1] + id: 3 + destination: testing/test2.yml + graphic: {fgc: '#7F7F7F', shape: '#'} + - !Exit + name: east + location: [25, 14] + id: 4 + destination: testing/test2.yml + graphic: {fgc: '#7F7F7F', shape: '#'} +loadOnce: [] diff --git a/testing/test4.xml b/testing/test4.xml index 81c5b38..f8415af 100644 --- a/testing/test4.xml +++ b/testing/test4.xml @@ -1,34 +1,34 @@ - - - w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w - w w w w w w w w w w w w w w w w w w w w - w w w w w w w w w - w w w w w w w w w w - w w w w w w - w w w w w w w w w w w w w w w w w w w w - w w - w w - w w w w w w w w w w w w w w w w w w w w w w w w w - w w w w - w w w w - w w w w w w w w - w w w w - w w w w - w w w w - w w w w w w w w - w w w w w w w w w w - w w w w w w w w w w w w w w w w w w w - w w w w - w w w w - w w w w w w - w w w w w w - w w x2 w - w w w - w w w w - w w w w w - w w w w - w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w - - - - + + + w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w + w w w w w w w w w w w w w w w w w w w w + w w w w w w w w w + w w w w w w w w w w + w w w w w w + w w w w w w w w w w w w w w w w w w w w + w w + w w + w w w w w w w w w w w w w w w w w w w w w w w w w + w w w w + w w w w + w w w w w w w w + w w w w + w w w w + w w w w + w w w w w w w w + w w w w w w w w w w + w w w w w w w w w w w w w w w w w w w + w w w w + w w w w + w w w w w w + w w w w w w + w w x2 w + w w w + w w w w + w w w w w + w w w w + w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w + + + + diff --git a/testing/test4.yml b/testing/test4.yml new file mode 100644 index 0000000..f24b33b --- /dev/null +++ b/testing/test4.yml @@ -0,0 +1,44 @@ +%YAML 1.2 +--- +layout: | + w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 + w0w0w0w0w0w0w0w0 w0w0w0w0w0w0w0w0w0w0w0 w0 + w0w0 w0 w0w0 w0 w0 w0 w0 + w0w0 w0 w0w0w0 w0 w0 w0 w0 + w0 w0w0 w0 w0 w0 + w0 w0w0 w0w0w0 w0w0w0w0w0w0w0w0w0w0 w0w0w0w0 + w0 w0 + w0 w0 + w0w0w0w0 w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 w0 + w0 w0 w0 w0 + w0 w0 w0 w0 + w0 w0w0 w0w0 w0 w0 w0 + w0 w0 w0 w0 + w0 w0 w0 w0 + w0 w0 w0 w0 + w0 w0w0 w0w0 w0 w0 w0 + w0 w0 w0w0w0 w0w0w0 w0 w0 + w0w0w0w0w0w0w0w0w0w0 w0w0w0 w0w0w0 w0 w0w0 + w0 w0 w0 w0 + w0 w0 w0 w0 + w0 w0 w0w0 w0 w0 + w0 w0 w0w0 w0 w0 + w0 w0 w0 + w0 w0 w0 + w0 w0 w0 w0 + w0 w0 w0 w0 w0 + w0 w0 w0 w0 + w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0w0 +openingText: Basement +playerStart: [22, 22] +description: '' +floorColors: ['#9F7F5F'] +wallColors: ['#7F3F0F'] +loadAlways: + - !Exit + name: upstairs + location: [23, 22] + id: 2 + destination: testing/test1.xml + graphic: {fgc: '#7F7F7F', shape: '#'} +loadOnce: [] diff --git a/tile.py b/tile.py index 3d55305..08c81a8 100644 --- a/tile.py +++ b/tile.py @@ -1,44 +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 +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