diff --git a/gamebase.py b/gamebase.py index 2d41c90..04d58a6 100644 --- a/gamebase.py +++ b/gamebase.py @@ -42,12 +42,9 @@ class GameBase(object): self.nextThing = 0 # player info - self.playerx = -1 - self.playery = -1 - self.prevx = -1 # prevx and prevy are for moving the player out of the way - self.prevy = -1 # when the tile they're standing on becomes blocked. self.playerName = 'You' - self.playerInv = {} + self.playerDescription = 'The main character.' + self.player = None # reference to the player's 'thing' # function deligates self.onLevelLoad = None # str : level name? -> None @@ -125,6 +122,8 @@ to get input from stdin.""" 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.""" + if self.level == None: + raise GameError('Coordinates cannot be parsed because there is no map.') #print(coordStr) x = -1 y = -1 @@ -140,10 +139,8 @@ Returns (thing, x, y). "Thing" can be None.""" # Second, try by name for objects in the inventory if allowInventory: - for i in self.playerInv: - thingName = self.playerInv[i].name - if name == thingName: - return self.playerInv[i], -1, -1 + if name in self.player.thingNames: + return self.player.getThingByName(name), -1, -1 # Third, try by location coordStr = args[0] @@ -208,14 +205,16 @@ Returns (thing, x, y). "Thing" can be None.""" val = True elif arg.casefold() == 'false': val = False - elif arg.casefold() == 'playerx': - val = self.playerx - elif arg.casefold() == 'playery': - val = self.playery - elif arg.casefold() == 'playername': - val = self.playerName - elif arg.casefold() == 'playerinv': - val = self.playerInv + elif arg.casefold() == 'playerx' and self.player != None: + val = self.player.x + elif arg.casefold() == 'playery' and self.player != None: + val = self.player.y + elif arg.casefold() == 'playername' and self.player != None: + val = self.player.name + elif arg.casefold() == 'playerinv' and self.player != None: + val = self.player.inventory + elif arg.casefold() == 'playerdesc' and self.player != None: + val = self.player.description elif validIdent != None: group = validIdent.group() if 'scriptLocal' in self.customValues and group in self.customValues['scriptLocal']: @@ -274,10 +273,7 @@ Returns (thing, x, y). "Thing" can be None.""" return lval > rval elif operator == 'in': if args[2].casefold() == 'playerinv': - for i in rval: - if rval[i].name == lval: - return True - return False + return lval in self.player.thingNames else: return lval in rval else: @@ -298,6 +294,8 @@ 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 GameError("Cannot move: No level has been loaded.") + if self.player == None: + raise GameError("Cannot move: Player character doesn't exist.") speed = 0.6666667 if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': speed = 0.3333333 @@ -309,20 +307,20 @@ The letter is not case-sensitive.""" 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): + if (x, y) == (self.player.x, self.player.y): #_hq.heappush(self.eventQueue, (self.gameTime, _ge.ArriveEvent(self.playerName, x, y, 0.0))) - self.setEvent(0.0, _ge.ArriveEvent(self.playerName, self.playerx, self.playery, 0.0)) + self.setEvent(0.0, _ge.ArriveEvent(self.player, self.player.x, self.player.y, 0.0)) return - dist, path = self.level.path(x, y, self.playerx, self.playery) + dist, path = self.level.path(x, y, self.player.x, self.player.y) if dist == -1: - print('{0} cannot reach {1}{2}.'.format(self.playerName, self.numberToLetter(x), y), file = self.outstream) + print('{0} cannot reach {1}{2}.'.format(self.player.name, 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) + pos = self.level.coordsToInt(self.player.x, self.player.y) space = path[pos] if space == -1: - self.setEvent(0.0, _ge.ArriveEvent(self.playerName, self.playerx, self.playery, 0.0)) + self.setEvent(0.0, _ge.ArriveEvent(self.player, self.player.x, self.player.y, 0.0)) return #target = self.level.coordsToInt(x, y) t = 1 @@ -331,13 +329,13 @@ The letter is not case-sensitive.""" newx, newy = self.level.intToCoords(space) space = path[space] if space != -1: - self.setEvent(t * speed, _ge.GoEvent(self.playerName, newx, newy)) + self.setEvent(t * speed, _ge.GoEvent(self.player, newx, newy)) else: - self.setEvent(t * speed, _ge.ArriveEvent(self.playerName, newx, newy, t * speed)) + self.setEvent(t * speed, _ge.ArriveEvent(self.player, 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))) + #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.ArriveEvent(self.player, newx, newy, t * speed))) return def look(self, args): @@ -347,6 +345,10 @@ Describe an object. 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 self.level == None: + raise GameError("Cannot look: No level has been loaded.") + if self.player == None: + raise GameError("Cannot look: Player character doesn't exist.") if len(args) == 0: print(self.justifyText(self.level.description), file = self.outstream) else: @@ -355,8 +357,8 @@ Object can be the name of the object, or its coordinates.""" if args[0] == 'the': args.pop(0) thing, x, y = self.parseCoords(args, usePlayerCoords = False) - if not self.level.lineOfSight(self.playerx, self.playery, x, y): - print("{} cannot see that.".format(self.playerName), file = self.outstream) + if not self.level.lineOfSight(self.player.x, self.player.y, x, y): + print("{} cannot see that.".format(self.player.name), file = self.outstream) elif thing == None: print("There is nothing to see here.\n", file = self.outstream) else: @@ -370,6 +372,10 @@ Talk to a character. make more sense from a linguistic perspective (for instance, one could say "talk to the guard" rather than just "talk guard"). Character can be the name of the character, or their coordinates.""" + if self.level == None: + raise GameError("Cannot talk: No level has been loaded.") + if self.player == None: + raise GameError("Cannot talk: Player character doesn't exist.") if len(args) == 0: print(self.justifyText(self.level.description), file = self.outstream) else: @@ -378,8 +384,8 @@ Character can be the name of the character, or their coordinates.""" if args[0] == 'the': args.pop(0) thing, x, y = self.parseCoords(args, usePlayerCoords = False, allowInventory = False) - if not self.level.lineOfSight(self.playerx, self.playery, x, y): - print("{} cannot talk to {}.".format(self.playerName, thing.name), file = self.outstream) + if not self.level.lineOfSight(self.player.x, self.player.y, x, y): + print("{} cannot talk to {}.".format(self.player.name, thing.name), file = self.outstream) elif thing == None: print("There is nobody here.\n", file = self.outstream) else: @@ -407,6 +413,10 @@ 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.""" + if self.level == None: + raise GameError("Cannot use: No level has been loaded.") + if self.player == None: + raise GameError("Cannot use: Player character doesn't exist.") speed = 0.6666667 useArgs = [] if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': @@ -424,22 +434,22 @@ the name of an item in the player's inventory.""" print("There is nothing to use.", file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return - if thing.thingType != 'u' and thing.thingID not in self.playerInv: + if thing.thingType != 'u' and thing not in self.player.inventory: 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.thingID in self.playerInv: + if (x, y) == (self.player.x, self.player.y) or thing in self.player.inventory: self.setEvent(0.125, _ge.UseEvent(thing, useArgs)) return - dist, path = self.level.path(x, y, self.playerx, self.playery) + dist, path = self.level.path(x, y, self.player.x, self.player.y) if dist == -1: - print('{0} cannot reach the {1}.'.format(self.playerName, thing.name), file = self.outstream) + print('{0} cannot reach the {1}.'.format(self.player.name, thing.name), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return else: - pos = self.level.coordsToInt(self.playerx, self.playery) + pos = self.level.coordsToInt(self.player.x, self.player.y) space = path[pos] #target = self.level.coordsToInt(x, y) t = 1 @@ -447,7 +457,7 @@ the name of an item in the player's inventory.""" while space != -1: newx, newy = self.level.intToCoords(space) space = path[space] - self.setEvent(t * speed, _ge.GoEvent(self.playerName, newx, newy)) + self.setEvent(t * speed, _ge.GoEvent(self.player, newx, newy)) t += 1 #newx, newy = self.level.intToCoords(space) #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.playerName, newx, newy))) @@ -464,7 +474,7 @@ the name of an item in the player's inventory.""" useArgs = [] if 'with' in args: useArgs = args[args.index('with')+1:] - if item == None or item.thingID not in self.playerInv: + if item == None or item not in self.player.inventory: print("There is no such item in the inventory.", file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return @@ -472,33 +482,41 @@ the name of an item in the player's inventory.""" 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, useArgs)) - 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) + + if not item.ranged: + # Similar to go, but not quite the same. + if (x, y) == (self.player.x, self.player.y): + self.setEvent(0.125, _ge.UseOnEvent(item, thing, useArgs)) + return + dist, path = self.level.path(x, y, self.player.x, self.player.y) + if dist == -1: + if thing != None: + print('{0} cannot reach the {1}.'.format(self.player.name, thing.name), file = self.outstream) + else: + print('{0} cannot reach {1}{2}.'.format(self.player.name, self.numberToLetter(x), y), file = self.outstream) + #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + return 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 + pos = self.level.coordsToInt(self.player.x, self.player.y) + 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.player, 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, useArgs)) 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, useArgs)) + if not self.level.lineOfSight(self.player.x, self.player.y, x, y): + print("{} cannot use the {} on that from here.".format(self.player.name, item.name), file = self.outstream) + elif thing == None: + print("There is nothing to use the {} on here.".format(item.name), file = self.outstream) + else: + self.setEvent(0, _ge.UseOnEvent(item, thing, useArgs)) return def take(self, args): @@ -509,6 +527,10 @@ If the player is not already close to it, they will go to it. 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.""" + if self.level == None: + raise GameError("Cannot take: No level has been loaded.") + if self.player == None: + raise GameError("Cannot take: Player character doesn't exist.") speed = 0.6666667 if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': speed = 0.3333333 @@ -525,16 +547,16 @@ Object can be the name of the object, or its coordinates.""" return # Similar to go, but not quite the same. - if (x, y) == (self.playerx, self.playery): - self.setEvent(0.125, _ge.TakeEvent(thing)) + if (x, y) == (self.player.x, self.player.y): + self.setEvent(0.125, _ge.TakeEvent(self.player, thing)) return - dist, path = self.level.path(x, y, self.playerx, self.playery) + dist, path = self.level.path(x, y, self.player.x, self.player.y) if dist == -1: - print('{0} cannot reach the {1}.'.format(self.playerName, thing.name), file = self.outstream) + print('{0} cannot reach the {1}.'.format(self.player.name, thing.name), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return else: - pos = self.level.coordsToInt(self.playerx, self.playery) + pos = self.level.coordsToInt(self.player.x, self.player.y) space = path[pos] #target = self.level.coordsToInt(x, y) t = 1 @@ -542,23 +564,31 @@ Object can be the name of the object, or its coordinates.""" while space != -1: newx, newy = self.level.intToCoords(space) space = path[space] - self.setEvent(t * speed, _ge.GoEvent(self.playerName, newx, newy)) + self.setEvent(t * speed, _ge.GoEvent(self.player, 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)) + #_hq.heappush(self.eventQueue, (self.gameTime + t * speed, _ge.GoEvent(self.player.name, newx, newy))) + self.setEvent(t * speed + 0.125, _ge.TakeEvent(self.player, thing)) return def drop(self, args): """drop [the] item""" + if self.level == None: + raise GameError("Cannot drop: No level has been loaded.") + if self.player == None: + raise GameError("Cannot drop: Player character doesn't exist.") if args[0] == 'the': args.pop(0) - for i in self.playerInv: - thingName = self.playerInv[i].name - if ' '.join(args) == thingName: - self.setEvent(0.0, _ge.DropEvent(self.playerInv[i])) - return - print('{0} does not have a {1}.'.format(self.playerName, args[0]), file = self.outstream) + #for i in self.player.inventory: + # thingName = self.player.inventory[i].name + # if ' '.join(args) == thingName: + # self.setEvent(0.0, _ge.DropEvent(self.player.inventory[i])) + # return + thingName = ' '.join(args) + if thingName in self.player.thingNames: + self.setEvent(0.0, _ge.DropEvent(self.player.getThingByName(thingName))) + else: + print('{0} does not have a {1}.'.format(self.player.name, args[0]), file = self.outstream) def loadMap(self, args): # loadMap (fileName) @@ -593,14 +623,26 @@ Object can be the name of the object, or its coordinates.""" print(self.level.openingText, file = self.outstream) #print(self.outstream.getvalue()) - if len(args) <= 2: - self.playerx, self.playery = self.level.playerStart + x, y = self.level.playerStart + if len(args) == 3: + x, y = int(args[1]), int(args[2]) + if self.player == None: + # create a player to put in the level. + self.player = _gm.PlayerCharacter(x, y, self.playerDescription, {}, {}, self.playerName) + print("Player created.") else: - self.playerx, self.playery = int(args[1]), int(args[2]) - self.prevx, self.prevy = self.playerx, self.playery + self.player.x, self.player.y = x, y + print("Player moved.") + self.player.prevx, self.player.prevy = self.player.x, self.player.y + self.nextThing = self.level.addThing(self.player, self.nextThing) # The player needs to be added to the new level. if self.onLevelLoad != None: self.onLevelLoad() self.parseScript(self.level.enterScript) + + def unloadMap(self, args): + """Args is only there just in case.""" + self.level = None + self.player = None def saveGame(self, args): if len(args) < 1: @@ -619,7 +661,7 @@ Object can be the name of the object, or its coordinates.""" self.persist[self.level.name][i] = self.level.getThingByID(i) # build data object to be saved - data = (self.playerName, self.playerx, self.playery, self.playerInv, self.level.name, self.persist, self.eventQueue, self.customValues, self.gameTime, self.nextThing) + data = (self.player, self.level.name, self.persist, self.eventQueue, self.customValues, self.gameTime, self.nextThing) # save it! fileName = 'saves/' + args[0].replace(' ', '_') + '.dat' @@ -650,9 +692,9 @@ Object can be the name of the object, or its coordinates.""" 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.customValues, self.gameTime, self.nextThing = _pi.load(f) + self.player, levelname, self.persist, self.eventQueue, self.customValues, self.gameTime, self.nextThing = _pi.load(f) #print(levelname, x, y, file = self.outstream) - self.loadMap((levelname, x, y)) + self.loadMap((levelname, self.player.x, self.player.y)) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return @@ -788,41 +830,59 @@ Object can be the name of the object, or its coordinates.""" 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: - actor = self.level.getThingByName(e.actor) - self.level.moveThing(actor, e.x, e.y) + #if e.actor == self.playerName: + # self.prevx, self.prevy = self.playerx, self.playery + # self.playerx, self.playery = e.x, e.y + #else: + if e.actor == None: + raise GameError("'Go' event raised for no object.") + 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': + #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': + # self.parseScript(thing.onUse) + # if (isinstance(thing.key, bool) and thing.key == True) or (isinstance(thing.key, str) and self.parseScript(thing.key)): + # 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 + if e.actor == None: + raise GameError("'Go' event raised for no object.") + thing = self.level.getThingAtCoords(e.x, e.y) + self.level.moveThing(e.actor, e.x, e.y) + print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(e.actor.name, self.numberToLetter(e.x), e.y, e.t), file = self.outstream) + if thing: + if thing.thingType == 'x': + if e.actor == self.player: self.parseScript(thing.onUse) if (isinstance(thing.key, bool) and thing.key == True) or (isinstance(thing.key, str) and self.parseScript(thing.key)): 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 + else: + print('{0} went {1}.'.format(actor.name, str(thing))) + self.level.removeThing(actor.name) + return True def handleUse(self, e): if e.thing.useFunc == '': @@ -839,15 +899,19 @@ Object can be the name of the object, or its coordinates.""" return False def handleTake(self, e): + if e.actor == None or e.actor.thingType not in 'np' or e.item == None: + raise GameError("'Take' event cannot be handled.") self.level.removeThingByID(e.item.thingID) - self.playerInv[e.item.thingID] = e.item + e.actor.addThing(e.item) return True def handleDrop(self, e): - e.item.x = self.playerx - e.item.y = self.playery + if e.actor == None or e.actor.thingType not in 'np': + raise GameError("'Drop' event cannot be handled.") + e.item.x = e.actor.x + e.item.y = e.actor.y self.nextThing = self.level.addThing(e.item, self.nextThing, True) # nextThing shouldn't change - del self.playerInv[e.item.thingID] + self.actor.removeThing(e.item) return True def handleBehave(self, e): @@ -890,7 +954,7 @@ Object can be the name of the object, or its coordinates.""" 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) + thing.customValues['items'], timeOpen = self.getIO('container')(self.player, items) return timeOpen def info(self, thing, args): @@ -901,31 +965,24 @@ Object can be the name of the object, or its coordinates.""" 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 + inInv = item in self.player.inventory 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 + self.customValues['wearing'][item.customValues['slot']] = self.player.removeThing(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) + print("{} put on the {}.".format(self.player.name, 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 + self.player.addThing(self.customValues['wearing'][item.customValues['slot']]) + self.customValues['wearing'][item.customValues['slot']] = self.player.removeThing(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) + print("{} took off the {} and put on the {}.".format(self.player.name, oldItem.name, item.name), file = self.outstream) return 1.0 # item use-on functions @@ -937,8 +994,8 @@ Object can be the name of the object, or its coordinates.""" 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 + if not thing.passable and self.player.x == thing.x and self.player.y == thing.y: + self.player.x, self.player.y = self.player.prevx, self.player.prevy else: print("The key doesn't fit that lock.", file = self.outstream) return 0.125 @@ -1191,7 +1248,7 @@ Object can be the name of the object, or its coordinates.""" equalsign = i.index('=') cv = i[3:equalsign] customValues[cv] = self.getValueFromString(i[equalsign+1:]) - thing = _gm.NPC(name, x, y, description, behavior, inv, customValues, playerx, playery, False, (bgc, fgc, shape)) + thing = _gm.NPC(name, x, y, description, behavior, inv, customValues, playerx, playery, (bgc, fgc, shape)) elif args[0].casefold() == 'door': # spawn a door description = 'a nondescript door.' @@ -1303,7 +1360,7 @@ Object can be the name of the object, or its coordinates.""" customValues[cv] = getValueFromString(i[equalsign+1:]) thing = _gm.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape)) thing.thingID = self.nextThing - self.playerInv[thing.thingID] = thing + self.player.addThing(thing) self.nextThing += 1 return thing diff --git a/gameevents.py b/gameevents.py index 78b9ba4..8286a4f 100644 --- a/gameevents.py +++ b/gameevents.py @@ -92,13 +92,15 @@ class UseOnEvent(GameEvent): self.args = args class TakeEvent(GameEvent): - def __init__(self, item): + def __init__(self, actor, item): super(TakeEvent, self).__init__('take') + self.actor = actor self.item = item class DropEvent(GameEvent): - def __init__(self, item): + def __init__(self, actor, item): super(DropEvent, self).__init__('drop') + self.actor = actor self.item = item class BehaveEvent(GameEvent): diff --git a/gamemap.py b/gamemap.py index e7cd443..1205225 100644 --- a/gamemap.py +++ b/gamemap.py @@ -16,6 +16,8 @@ class Thing(object): self.y = y self.playerx = x self.playery = y + self.prevx = x # if an area gets double-occupied, a thing can get pushed back. + self.prevy = y if playerx: self.playerx = playerx if playery: @@ -179,27 +181,84 @@ class Useable(Thing): return cls(parts['name'], parts['location'][0], parts['location'][1], parts['description'], useFunc, customValues, playerx, playery, graphic) -class NPC(Thing): +class Character(Thing): + defaultGraphic = ('clear', '#000000', 'o') + + def __init__(self, thingType: str, name: str, x: int, y: int, + description: str, inventory: dict, customValues: dict, + flags: int, playerx = None, playery = None, graphic = defaultGraphic): + super(Character, self).__init__(thingType, name, x, y, description, flags) + if inventory == None: + inventory = {} # create a new dict for the inventory. + # This couldn't be in the NPC constructor because + # then all characters would share a reference to + # the same empty inventory. + self.__inventory = inventory + self.customValues = customValues + self.graphic = graphic + self.thingNames = {} + # set up inventory shtuff + for i in self.__inventory: + if self.__inventory[i].name in self.thingNames: + self.thingNames[self.__inventory[i].name].append(i) + else: + self.thingNames[self.__inventory[i].name] = [i] + + def addThing(self, thing): + if not isinstance(thing, Item): + raise TypeError("Only items can be added to a character's inventory.") + self.__inventory[thing.thingID] = thing + if thing.name in self.thingNames: + self.thingNames[thing.name].append(thing.thingID) + else: + self.thingNames[thing.name] = [thing.thingID] + + def getThingByID(self, thingID): + return self.__inventory[thingID] + + def getThingByName(self, name): + if name in self.thingNames: + return self.__inventory[self.thingNames[name][0]] + else: + return None + + def removeThingByID(self, thingID): + ret = self.__inventory[thingID] + self.thingNames[ret.name].remove(thingID) + if len(self.thingNames[ret.name]) == 0: + del self.thingNames[ret.name] + del self.__inventory[thingID] + return ret + + def removeThingByName(self, name): + ret = self.getThingByName(name) + self.thingNames[ret.name].remove(thingID) + if len(self.thingNames[ret.name]) == 0: + del self.thingNames[ret.name] + del self.__inventory[thingID] + return ret + + def removeThing(self, ret): + self.thingNames[ret.name].remove(ret.thingID) + if len(self.thingNames[ret.name]) == 0: + del self.thingNames[ret.name] + del self.__inventory[ret.thingID] + return ret + + @property + def inventory(self): + """Get the inventory as a list.""" + return list(self.__inventory.values()) + +class NPC(Character): yaml_flag = u'!NPC' defaultGraphic = ('clear', '#000000', 'o') - def __init__(self, name, x: int, y: int, description: str, behavior: str, inventory: list, customValues: dict, playerx = None, playery = None, following = False, graphic = defaultGraphic): - super(NPC, self).__init__('c', name, x, y, description, 6, playerx, playery) + def __init__(self, name, x: int, y: int, description: str, behavior: str, inventory: list, customValues: dict, playerx = None, playery = None, graphic = defaultGraphic): + super(NPC, self).__init__('n', name, x, y, description, None, customValues, 6, playerx, playery, graphic) self.behavior = behavior - self.following = following - self.inventory = inventory - self.customValues = customValues - self.graphic = graphic self.behaveEvent = None - - def give(self, item): - self.inventory.append(item) - - def drop(self, index): - return self.inventory.pop(index) - - def dialog(self): - pass + self.tempInventory = inventory # should be deleted once NPC is loaded @classmethod def to_yaml(cls, representer, node): @@ -217,8 +276,6 @@ class NPC(Thing): if len(graphic) > 0: ret['graphic'] = graphic # save use functions - if node.following: - ret['following'] = node.following if len(node.inventory) > 0: ret['inventory'] = node.inventory if len(node.customValues) > 0: @@ -249,8 +306,6 @@ class NPC(Thing): shape = parts['graphic']['shape'] graphic = (bgc, fgc, shape) # load use functions - if 'following' in parts: - useFunc = parts['following'] if 'inventory' in parts: inventory = parts['inventory'] if 'customValues' in parts: @@ -262,7 +317,7 @@ class NPC(Thing): playerx, playery = parts['useLocation'] return cls(parts['name'], parts['location'][0], parts['location'][1], parts['description'], parts['behavior'], inventory, customValues, - playerx, playery, following, graphic) + playerx, playery, graphic) class Door(Thing): yaml_flag = u'!Door' @@ -444,6 +499,13 @@ class MapEntrance(Thing): constructor.construct_mapping(node, parts, True) # set default values for optional arguments return cls(parts['location'][0], parts['location'][1], parts['id']) + +class PlayerCharacter(Character): + """Player object. Cannot be created with yaml.""" + defaultGraphic = ('clear', '#0000FF', 'o') + + def __init__(self, x: int, y: int, description: str, inventory: dict, customValues: dict, name = 'You', graphic = defaultGraphic): + super(PlayerCharacter, self).__init__('p', name, x, y, description, inventory, customValues, 5, graphic=graphic) class MapError(RuntimeError): pass @@ -492,86 +554,6 @@ class GameMap(object): 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, nextThing = 0): @@ -729,12 +711,15 @@ list of lists of tuples.""" nextThing += 1 # Some things, like containers, have other things as custom values, # so they need IDs as well. - if thing.thingType in 'iuc': + if thing.thingType in 'iun': nextThing = self.addThingRecursive(thing.customValues, nextThing) - if thing.thingType == 'c': - for i in thing.inventory: - i.thingID = nextThing - nextThing += 1 + if thing.thingType == 'n': + for i in thing.tempInventory: + if i.thingID == -1: + i.thingID = nextThing + nextThing = self.addThingRecursive(i.customValues, nextThing + 1) + thing.addThing(i) + del thing.tempInventory pos = self.coordsToInt(thing.x, thing.y) if pos not in self.thingPos: self.thingPos[pos] = [thing.thingID] @@ -751,8 +736,12 @@ list of lists of tuples.""" def addThingRecursive(self, container, nextThing = 0): if isinstance(container, Thing): - container.thingID = nextThing - return nextThing + 1 + if container.thingID == -1: + container.thingID = nextThing + nextThing = self.addThingRecursive(container.customValues, nextThing) + return nextThing + 1 + else: + return nextThing elif isinstance(container, dict): for i in container: nextThing = self.addThingRecursive(container[i], nextThing) @@ -838,6 +827,10 @@ The closeEnough parameter will create a path that lands beside the source if nec # return -1, [] # meaning you can't get there def lineOfSight(self, x1, y1, x2, y2): + """Test for line of signt from one tile to another.""" + # Trivial case first: + if abs(x1 - x2) <= 1 and abs(y1 - y2) <= 1: + return True Dx = x2 - x1 Dy = y2 - y1 y = y1 + 0.5 @@ -913,10 +906,7 @@ The closeEnough parameter will create a path that lands beside the source if nec def getThingsAtPos(self, pos): if pos in self.thingPos: - ret = [] - for i in self.thingPos[pos]: - ret.append(self.things[i]) - return ret + return [self.things[i] for i in self.thingPos[pos]] else: return [] @@ -1019,6 +1009,7 @@ The closeEnough parameter will create a path that lands beside the source if nec else: self.thingPos[newPos].append(thing.thingID) relPlayerx, relPlayery = thing.playerx - thing.x, thing.playery - thing.y + thing.prevx, thing.prevy = thing.x, thing.y thing.x, thing.y = x, y thing.playerx, thing.playery = thing.x + relPlayerx, thing.y + relPlayery else: diff --git a/gameshell.py b/gameshell.py index 43a7969..a804acf 100644 --- a/gameshell.py +++ b/gameshell.py @@ -46,7 +46,6 @@ class GameShell(Shell): self.gameBase.registerIO('dialog', self.dialog) self.gameBase.registerIO('info', self.info) self.gameBase.registerIO('playercmd', self.playercmd) - self.menuMode() # Helper functions @@ -148,25 +147,33 @@ If -l is given, a map legend will be printed under the map.""" level = self.gameBase.level textColor = level.wallColors[0] floorColor = level.floorColors[0] + priorities = {'p': 1, 'n': 2, 'i': 3, 'u': 4, 'd': 5, 'x': 6, 'a': 7} 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: + things = level.getThingsAtPos(index) + #if x == self.gameBase.playerx and y == self.gameBase.playery: + # if self.gameBase.player != textColor: + # textColor = '#0000FF' + # rows[-1].append(self.color(textColor[1:])) + # rows[-1].append('()') + if len(things) > 0: + # Prioritize types: p, n, i, u, d, x, a + thing = things[0] + for i in things[1:]: + if priorities[i.thingType] < priorities[thing.thingType]: + thing = i if thing.graphic[1] != textColor: textColor = thing.graphic[1] rows[-1].append(self.color(textColor[1:])) - if thing.thingType == 'x': # exit + if thing.thingType == 'p': # player + rows[-1].append('()') + elif thing.thingType == 'x': # exit rows[-1].append('X{0}'.format(thing.exitid)) exits[thing.exitid] = (thing.name, thing.graphic[1]) - elif thing.thingType == 'c': # useable + elif thing.thingType == 'n': # NPC characters[len(characters)+1] = (thing.name, thing.graphic[1]) rows[-1].append('C{0}'.format(len(characters))) elif thing.thingType == 'd': # door @@ -178,7 +185,7 @@ If -l is given, a map legend will be printed under the map.""" elif thing.thingType == 'i': # item items[len(items)+1] = (thing.name, thing.graphic[1]) rows[-1].append('I{0}'.format(len(items))) - elif thing.thingType == 'a': # entrance + else: # entrance rows[-1].append(' ') elif pos[0] == 'w': if level.wallColors[level.mapMatrix[y][x][1]] != textColor: @@ -245,12 +252,12 @@ If -l is given, a map legend will be printed under the map.""" ret.append("custom values:") for i in self.gameBase.customValues: ret.append("{0:<22}: {1}".format(i, self.gameBase.customValues[i])) - 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("Player name:{0:.>68}".format(self.gameBase.player.name)) + ret.append("Player position:{0:.>64}".format("{0}{1}".format(self.gameBase.numberToLetter(self.gameBase.player.x), self.gameBase.player.y))) + ret.append("Prev. position:{0:.>65}".format("{0}{1}".format(self.gameBase.numberToLetter(self.gameBase.player.prevx), self.gameBase.player.prevy))) ret.append("Inventory:") - for i in self.gameBase.playerInv: - ret.append("{0:<8}: {1}".format(i, self.gameBase.playerInv[i].name)) + for i in self.gameBase.player.inventory: + ret.append("{0:<8}: {1}".format(i.thingID, i.name)) ret.append("Things:\nID Name X Y") for i in self.gameBase.level.things: j = self.gameBase.level.things[i] @@ -263,7 +270,7 @@ If -l is given, a map legend will be printed under the map.""" return def inv(self, args): - print('\n'.join([self.gameBase.playerInv[i].name for i in self.gameBase.playerInv])) + print('\n'.join([i for i in self.gameBase.player.thingNames])) def newGame(self, args): if self.__inGame: @@ -351,22 +358,23 @@ If -l is given, a map legend will be printed under the map.""" # IO calls - def container(self, inv, cont): - """container IO""" + def container(self, player, cont): + """container IO +Player is modified through side-effect.""" # Pretty print: get length of the longest inventory item's name longestLen = 0 - for i in inv: - if len(inv[i].name) > longestLen: - longestLen = len(inv[i].name) + for i in player.thingNames: + if len(i) > longestLen: + longestLen = len(i) if longestLen > 0: + inv = player.inventory # do this assignment because player.inventory is O(n) 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): - print('{{0:<{0}}}{1}'.format(longestLen+2, cont[i].name).format(inv[invKeys[i]].name)) + while i < len(inv) and i < len(cont): + print('{{0:<{0}}}{1}'.format(longestLen+2, cont[i].name).format(inv[i].name)) i += 1 - while i < len(invKeys): - print(inv[invKeys[i]].name) + while i < len(inv): + print(inv[i].name) i += 1 while i < len(cont): print(' '*(longestLen+2) + cont[i].name) @@ -387,26 +395,27 @@ 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].thingID] = cont[i] + player.addThing(cont[i]) del cont[i] timeSpent += 0.5 print("{0} taken.".format(thing)) break + else: + # If it got here, it didn't find it. + print("No {0} in container.".format(thing)) elif instr[0] == "store": # store something in the container if instr[1] == "the": del instr[1] 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 + if thingName in player.thingNames: + cont.append(player.removeThingByName(thingName)) + print("{0} stored.".format(thingName)) + timeSpent += 0.5 + else: + print("No {0} in inventory.".format(thingName)) instr = input("Take, store, or exit: ") - return inv, cont, timeSpent + return cont, timeSpent def info(self, items): """IO for collections of information""" @@ -532,4 +541,5 @@ If -l is given, a map legend will be printed under the map.""" if __name__ == '__main__': sh = GameShell(GameBase()) + sh.menuMode() sh.run()