From 8355dccb331ac8143d140e9072ade50cd4ed84bb Mon Sep 17 00:00:00 2001 From: Patrick Marsee Date: Tue, 28 May 2019 14:23:15 -0400 Subject: [PATCH] added "info" and "wear" use functions, and fixed many bugs related to storing things by ID. --- gamebase.py | 107 ++++++++++++++++++++++++++++++++++++-------------- gameevents.py | 6 ++- gamemap.py | 53 +++++++++++++++++++++++-- gameshell.py | 43 +++++++++++++++----- 4 files changed, 165 insertions(+), 44 deletions(-) diff --git a/gamebase.py b/gamebase.py index 6433ef6..a11febd 100644 --- a/gamebase.py +++ b/gamebase.py @@ -25,7 +25,7 @@ class GameBase(object): self.__behaviors = {} self.__gameEvents = {} self.__IOCalls = {} # {str : function} - self.customVals = {} # for setting flags and such + self.customValues = {} # for setting flags and such self.level = None self.persist = {} # {level : {thingName : thing}} self.ps2 = '? ' @@ -58,6 +58,8 @@ class GameBase(object): self.registerUseFunc('examine', self.examine) self.registerUseFunc('key', self.key) self.registerUseFunc('container', self.container) + self.registerUseFunc('info', self.info) + self.registerUseFunc('wear', self.wear) self.registerBehavior('wander', self.wander) # Helper functions @@ -138,16 +140,16 @@ Returns (thing, x, y). "Thing" can be None.""" 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)) + for i in self.playerInv: + thingName = self.playerInv[i].name + if name == thingName: + thing = self.playerInv[i] + if thing != None: + return thing, -1, -1 + else: + return None, -1, -1 + #raise RuntimeError("'None' item named '{0}' in player inventory.".format(name)) + return None, -1, -1 else: # nothing return None, -1, -1 #raise ValueError("{0} cannot be reached.".format(name)) @@ -295,6 +297,7 @@ 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 + useArgs = [] if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': speed = 0.3333333 args.pop(0) @@ -303,19 +306,21 @@ the name of an item in the player's inventory.""" if 'on' in args: self.useOn(args, speed) return + if 'with' in args: + useArgs = args[args.index('with')+1:] 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: + if thing.thingType != 'u' and thing.thingID 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)) + if (x, y) == (self.playerx, self.playery) or thing.thingID in self.playerInv: + self.setEvent(0.125, _ge.UseEvent(thing, useArgs)) return dist, path = self.level.path(x, y, self.playerx, self.playery) if dist == -1: @@ -335,7 +340,7 @@ the name of an item in the player's inventory.""" 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)) + self.setEvent(t * speed + 0.125, _ge.UseEvent(thing, useArgs)) return def useOn(self, args, speed): @@ -345,7 +350,10 @@ the name of an item in the player's inventory.""" 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: + useArgs = [] + if 'with' in args: + useArgs = args[args.index('with')+1:] + if item == None or item.thingID 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 @@ -356,7 +364,7 @@ the name of an item in the player's inventory.""" # Similar to go, but not quite the same. if (x, y) == (self.playerx, self.playery): - self.setEvent(0.125, _ge.UseOnEvent(item, thing)) + self.setEvent(0.125, _ge.UseOnEvent(item, thing, useArgs)) return dist, path = self.level.path(x, y, self.playerx, self.playery) if dist == -1: @@ -379,7 +387,7 @@ the name of an item in the player's inventory.""" 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)) + self.setEvent(t * speed + 0.125, _ge.UseOnEvent(item, thing, useArgs)) return def take(self, args): @@ -434,10 +442,12 @@ Object can be the name of the object, or its coordinates.""" """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) + for i in self.playerInv: + thingName = self.playerInv[i].name + if ' '.join(args) == thingName: + self.setEvent(0.0, _ge.DropEvent(self.playerInv[args[0]])) + return + print('{0} does not have a {1}.'.format(self.playerName, args[0]), file = self.outstream) def loadMap(self, args): # loadMap (fileName) @@ -602,26 +612,26 @@ Object can be the name of the object, or its coordinates.""" 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()) + self.setEvent(self.__useFuncs[e.thing.useFunc](e.thing, e.args), _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()) + self.setEvent(self.__useFuncs[e.item.useOnFunc](e.item, e.thing, e.args), _ge.NoOpEvent()) return False def handleTake(self, e): - self.level.removeThingByName(e.item.name) - self.playerInv[e.item.name] = e.item + self.level.removeThingByID(e.item.thingID) + self.playerInv[e.item.thingID] = 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] + del self.playerInv[e.item.thingID] return True def handleBehave(self, e): @@ -629,7 +639,7 @@ Object can be the name of the object, or its coordinates.""" # default useFuncs: take a list of arguments, return the time the use took - def examine(self, thing): + def examine(self, thing, args): """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)) @@ -661,13 +671,50 @@ Object can be the name of the object, or its coordinates.""" self.customVals[thing.customValues['set']] = 0 return 0.0 - def container(self, thing): + def container(self, thing, args): """Acts as a container. Items can be traded between the container and the player's inventory.""" items = list(thing.customValues['items']) self.playerInv, thing.customValues['items'], timeOpen = self.getIO('container')(self.playerInv, items) return timeOpen - def key(self, item, thing): + def info(self, thing, args): + """Acts as a bookshelf, filing cabinet, bulletin board, or anything that contains raw info.""" + items = dict(thing.customValues['items']) + return self.getIO('info')(items) + + def wear(self, item, args): + """Wear clotherg or otherwise equip a passive item.""" + # An item must be in the player's inventory in order to wear it. + inInv = False + for i in self.playerInv: + if i == item.thingID: + inInv = True + break + if not inInv: + print("You cannot wear what you are not carrying.", file = self.outstream) + return 0.0 + if 'wearing' not in self.customValues: + self.customValues['wearing'] = {} + if item.customValues['slot'] not in self.customValues['wearing']: + self.customValues['wearing'][item.customValues['slot']] = item + # This is so a player can't put on a garment, then drop it while + # still also wearing it. + del self.playerInv[item.thingID] + print("{} put on the {}.".format(self.playerName, item.name), file = self.outstream) + else: # the player must be wearing something that will get in the way + # put the old clothes back in the inventory + oldItem = self.customValues['wearing'][item.customValues['slot']] + self.playerInv[oldItem.thingID] = oldItem + self.customValues['wearing'][item.customValues['slot']] = item + # This is so a player can't put on a garment, then drop it while + # still also wearing it. + del self.playerInv[item.thingID] + print("{} took off the {} and put on the {}.".format(self.playerName, oldItem.name, item.name), file = self.outstream) + return 1.0 + + # item use-on functions + + def key(self, item, thing, args): """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) diff --git a/gameevents.py b/gameevents.py index fb61598..78b9ba4 100644 --- a/gameevents.py +++ b/gameevents.py @@ -79,15 +79,17 @@ class ArriveEvent(GameEvent): self.t = t class UseEvent(GameEvent): - def __init__(self, thing): + def __init__(self, thing, args): super(UseEvent, self).__init__('use') self.thing = thing + self.args = args class UseOnEvent(GameEvent): - def __init__(self, item, thing): + def __init__(self, item, thing, args): super(UseOnEvent, self).__init__('useon') self.thing = thing # thing can be a coordinate pair? self.item = item + self.args = args class TakeEvent(GameEvent): def __init__(self, item): diff --git a/gamemap.py b/gamemap.py index e715cd7..f79d0ea 100644 --- a/gamemap.py +++ b/gamemap.py @@ -409,6 +409,29 @@ class MapExit(Thing): return cls(parts['name'], parts['location'][0], parts['location'][1], parts['id'], parts['destination'], prefix, graphic) +class MapEntrance(Thing): + yaml_flag = u'!MapEntrance' + # no graphic - should not be drawn + + def __init__(self, x: int, y: int, exitid: int): + if prefix: + description = "{0} {1}".format(prefix, name) + super(MapEntrance, self).__init__('a', name, x, y, description, 1) + self.exitid = exitid + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'location': (node.x, node.y), 'id': node.exitid} + 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 + return cls(parts['location'][0], parts['location'][1], parts['id']) + class MapError(RuntimeError): pass @@ -575,7 +598,6 @@ Entering a map through stdin will be obsolete once testing is over.""" # generate matrix and graph first mapMatrix, mapGraph, dimensions = GameMap.parseMatrix(mat) - playerStart = (-1, -1) level = GameMap(infile, mapGraph, mapMatrix, dimensions) # Now, load other info @@ -664,6 +686,7 @@ list of lists of tuples.""" level.wallColors.append('#7F3F0F') # get things + hasKnownEntrance = False if 'loadOnce' in info and not preLoaded: for thing in info['loadOnce']: #print(type(thing)) @@ -672,8 +695,9 @@ list of lists of tuples.""" for thing in info['loadAlways']: #print(type(thing)) nextThing = level.addThing(thing, nextThing) - if thing.thingType == 'x' and prevMap == thing.exitid: + if ((thing.thingType == 'x' and not hasKnownEntrance) or thing.thingType == 'a') and prevMap == thing.exitid: level.playerStart = (thing.x, thing.y) + hasKnownEntrance = True return nextThing # stuff the gameshell itself might use @@ -686,6 +710,14 @@ list of lists of tuples.""" if thing.thingID == -1: # This is to ensure that we don't double up IDs. thing.thingID = nextThing nextThing += 1 + # Some things, like containers, have other things as custom values, + # so they need IDs as well. + if thing.thingType in 'iuc': + nextThing = self.addThingRecursive(thing.customValues, nextThing) + if thing.thingType == 'c': + for i in thing.inventory: + i.thingID = nextThing + nextThing += 1 pos = self.coordsToInt(thing.x, thing.y) if pos not in self.thingPos: self.thingPos[pos] = [thing.thingID] @@ -700,6 +732,21 @@ list of lists of tuples.""" self.persistent.append(thing.thingID) return nextThing + def addThingRecursive(self, container, nextThing = 0): + if isinstance(container, Thing): + container.thingID = nextThing + return nextThing + 1 + elif isinstance(container, dict): + for i in container: + nextThing = self.addThingRecursive(container[i], nextThing) + return nextThing + elif isinstance(container, list): + for i in container: + nextThing = self.addThingRecursive(i, nextThing) + return nextThing + else: + return nextThing + def getThing(self, **kwargs): if 'name' in kwargs: return self.getThingByName(kwargs['name']) @@ -885,7 +932,7 @@ The closeEnough parameter will create a path that lands beside the source if nec if len(self.thingPos[oldPos]) == 0: del self.thingPos[oldPos] oldName = thing.name - if oldName in self.thingPos: + if oldName in self.thingNames: self.thingNames[oldName].remove(thing.thingID) if len(self.thingNames[oldName]) == 0: del self.thingNames[oldName] diff --git a/gameshell.py b/gameshell.py index 7d3da4a..ee205af 100644 --- a/gameshell.py +++ b/gameshell.py @@ -56,6 +56,7 @@ class GameShell(Shell): self.registerAlias('run', ['go', '-r']) self.gameBase.registerIO('container', self.container) self.gameBase.registerIO('dialog', self.dialog) + self.gameBase.registerIO('info', self.info) # Helper functions @@ -212,6 +213,9 @@ If -l is given, a map legend will be printed under the map.""" 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))) + ret.append("Inventory:") + for i in self.gameBase.playerInv: + ret.append("{0:<8}: {1}".format(i, self.gameBase.playerInv[i].name)) ret.append("Things:\nID Name X Y") for i in self.gameBase.level.things: j = self.gameBase.level.things[i] @@ -224,7 +228,7 @@ If -l is given, a map legend will be printed under the map.""" return def inv(self, args): - print('\n'.join([i for i in self.gameBase.playerInv])) + print('\n'.join([self.gameBase.playerInv[i].name for i in self.gameBase.playerInv])) def container(self, inv, cont): """container IO""" @@ -236,7 +240,7 @@ If -l is given, a map legend will be printed under the map.""" longestLen = len(i) longestStr = i if longestLen > 0: - print('{{0:<{0}}}{1}'.format(longestLen+2, "Container:").format("Inv:")) + print('{{0:<{0}}}{1}'.format(max(6, longestLen+2), "Container:").format("Inv:")) i = 0 invKeys = tuple(inv.keys()) while i < len(invKeys) and i < len(cont): @@ -264,7 +268,7 @@ If -l is given, a map legend will be printed under the map.""" thing = ' '.join(instr[1:]) for i in range(len(cont)): if thing == cont[i].name: - inv[cont[i].name] = cont[i] + inv[cont[i].thingID] = cont[i] del cont[i] timeSpent += 0.5 print("{0} taken.".format(thing)) @@ -273,15 +277,36 @@ If -l is given, a map legend will be printed under the map.""" # store something in the container if instr[1] == "the": del instr[1] - thing = ' '.join(instr[1:]) - if thing in inv: - cont.append(inv[thing]) - del inv[thing] - print("{0} stored.".format(thing)) - timeSpent += 0.5 + thingName = ' '.join(instr[1:]) + for i in inv: + thing = inv[i].name + if thing == thingName: + cont.append(inv[i]) + del inv[i] + print("{0} stored.".format(thing)) + timeSpent += 0.5 + break # so that all things with the same name don't get stored instr = input("Take, store, or exit: ") return inv, cont, timeSpent + def info(self, items): + """IO for collections of information""" + charsRead = 0 + for i in items: + print(' ', i) + instr = input("Choose an item to view, or exit: ") + while instr != 'exit': + if instr in items: + print(_tw.fill(items[instr], width = TERM_SIZE)) + charsRead += len(items[instr]) + else: + print('{} not here.'.format(instr)) + input('') + for i in items: + print(' ', i) + instr = input("Choose an item to view, or exit: ") + return charsRead / 27 # based on average 250 words per minute, and word length of 5.5 + 1 for space. + def dialog(self, dialogObj): if 'opener' in dialogObj: print(_tw.fill(dialogObj['opener'], width = TERM_SIZE))