diff --git a/.gitignore b/.gitignore index dceb081..5e33ec1 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,5 @@ __pycache__ gameshell.geany gameexpr.py game.py +solution.txt + diff --git a/gamebase.py b/gamebase.py index 8e17475..374798f 100644 --- a/gamebase.py +++ b/gamebase.py @@ -38,8 +38,8 @@ class GameBase(object): self.__scripts = {} # functions with the same signature, but not callable by the user self.customValues = {} # for setting flags and such self.level = None - self.persist = {} # {level : {thingName : thing}} - self.singletons = {} # {thingID : thing} + self.persist = {} # {level : {thingID : thing}} + self.singletons = {} # {thingName : thing} self.ps2 = '? ' self.eventQueue = [] self.gameTime = 0.0 @@ -112,12 +112,23 @@ to get input from stdin.""" return '\n'.join(ret) - def smoothMove(self, actor, loc, speed, action = None, immediate = False): - """Move a thing smoothly at a constant speed.""" + def smoothMove(self, actor: _gt.Thing, loc: _gl.Locus, speed: float, action = None, immediate = False, closeEnough = True): + """Move a thing smoothly at a constant speed. + + actor should be a Thing. + loc should be a locus. + speed should be a number. + action should be a callable with 0 arguments. A lambda would be good here. + If immediate is True, the go event would happen immediately. + closeEnough should be True only if the path is to a non-passable point locus.""" + if not isinstance(actor, _gt.Thing): + raise TypeError("The actor passed to smoothMove must be a Thing.") + if not isinstance(loc, _gl.Locus): + raise TypeError("The locus passed to smoothMove must be a Locus.") if (actor.x, actor.y) in loc: self.setEvent(0.0, _ge.ArriveEvent(actor, self.level.coordsToInt(actor.x, actor.y), speed, action, loc, timeTaken = speed)) return - dist, path, endPoint = self.level.path(actor.x, actor.y, loc) + dist, path, endPoint = self.level.path(actor.x, actor.y, loc, closeEnough) #print(path) if dist == -1: print('{0} cannot reach there.'.format(actor.name), file = self.outstream) @@ -125,11 +136,15 @@ to get input from stdin.""" elif dist == 1: self.setEvent(speed, _ge.ArriveEvent(actor, path[0], speed, action, loc, timeTaken = speed)) return speed + elif dist == 0: + # We should try to avoid this branch, because it means an error happened. + self.setEvent(0.0, _ge.ArriveEvent(actor, self.level.coordsToInt(actor.x, actor.y), speed, action, loc, timeTaken = speed)) + return elif immediate: - self.setEvent(0, _ge.GoEvent(actor, path, speed, action, loc)) + self.setEvent(0, _ge.GoEvent(actor, path, speed, action, loc, closeEnough)) return speed * (dist - 1) else: - self.setEvent(speed, _ge.GoEvent(actor, path, speed, action, loc, timeTaken = speed)) + self.setEvent(speed, _ge.GoEvent(actor, path, speed, action, loc, closeEnough, timeTaken = speed)) return speed * dist # commands @@ -155,6 +170,9 @@ The letter is not case-sensitive.""" raise GameError("Cannot move: No level has been loaded.") if self.player == None: raise GameError("Cannot move: Player character doesn't exist.") + if len(args) == 0: + print(f"{self.player.name} goes nowhere.", file = self.outstream) + return speed = 0.6666667 if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': speed = 0.3333333 @@ -181,6 +199,7 @@ Object can be the name of the object, or its coordinates.""" if self.player == None: raise GameError("Cannot look: Player character doesn't exist.") if len(args) == 0: + # no arguments: print the level description print(self.justifyText(self.level.description), file = self.outstream) else: if args[0] == 'at': @@ -188,15 +207,18 @@ Object can be the name of the object, or its coordinates.""" if args[0] == 'the': args.pop(0) thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False, player = self.player) - if not self.level.lineOfSight(self.player.x, self.player.y, x, y): + if thing == None: + print("There is nothing to see here.", file = self.outstream) + return + elif thing in self.player.inventory: + print(self.justifyText(str(thing)), file = self.outstream) + return + elif not self.level.lineOfSight(self.player.x, self.player.y, x, y): if self.autoMove: - self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: print(self.justifyText(str(thing)), file = self.outstream)) + self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: print(self.justifyText(str(thing)), file = self.outstream), closeEnough = False) else: print("{} cannot see that.".format(self.player.name), file = self.outstream) return - elif thing == None: - print("There is nothing to see here.\n", file = self.outstream) - return else: print(self.justifyText(str(thing)), file = self.outstream) @@ -212,22 +234,26 @@ Character can be the name of the character, or their coordinates.""" 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) + print(f'{self.player.name} says, "Hello!" Nobody responds.', file = self.outstream) + return else: if args[0] == 'to': args.pop(0) if args[0] == 'the': args.pop(0) thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False) - if not self.level.lineOfSight(self.player.x, self.player.y, x, y): + if thing == None: + if len(args) > 0: + print(f"There is nobody named {' '.join(args)}.", file = self.outstream) + else: + print(f"There is nobody there.", file = self.outstream) + return + elif not self.level.lineOfSight(self.player.x, self.player.y, x, y): if self.autoMove: - self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: _gu.startDialog(thing, self.outstream, self.dialog)) + self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: _gu.startDialog(thing, self.outstream, self.dialog), closeEnough = False) else: print("{} cannot talk to {}.".format(self.player.name, thing.name), file = self.outstream) return - elif thing == None: - print("There is nobody here.\n", file = self.outstream) - return else: _gu.startDialog(thing, self.outstream, self.dialog) @@ -246,6 +272,9 @@ the name of an item in the player's inventory.""" raise GameError("Cannot use: No level has been loaded.") if self.player == None: raise GameError("Cannot use: Player character doesn't exist.") + if len(args) == 0: + print(f"{self.player.name} pokes the air as if pressing a button.", file = self.outstream) + return speed = 0.6666667 useArgs = [] if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': @@ -269,12 +298,23 @@ the name of an item in the player's inventory.""" return # Similar to go, but not quite the same. - self.smoothMove(self.player, ((x, y),), speed, lambda: self.setEvent(0.125, _ge.UseEvent(self.player, thing, useArgs))) + if thing.thingType == 'i': # it must be in inventory to pass the last check. + self.setEvent(0.125, _ge.UseEvent(self.player, thing, useArgs)) + else: + self.smoothMove(self.player, _gl.PointLocus(x, y), speed, lambda: self.setEvent(0.125, _ge.UseEvent(self.player, thing, useArgs))) return def useOn(self, args, speed): """Called by use when there is an 'on' clause""" onIndex = args.index('on') + if len(args[:onIndex]) == 0: + # just starts with 'use on ...' + print(f"{self.player.name} tries to use nothing, but nothing happens.", file = self.outstream) + return + if len(args[onIndex + 1:]) == 0: + # just ends with '... on' + print(f"{self.player.name} tries to use the {' '.join(args[:onIndex])} on nothing, but nothing happens.", file = self.outstream) + return item, x, y = _gu.parseCoords(self.level, args[:onIndex], usePlayerCoords = False, player = self.player) if args[onIndex+1] == 'the': onIndex += 1 @@ -293,11 +333,11 @@ the name of an item in the player's inventory.""" if not item.ranged: # Similar to go, but not quite the same. - self.smoothMove(self.player, ((x, y),), speed, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs))) + self.smoothMove(self.player, _gl.PointLocus(x, y), speed, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs))) else: if not self.level.lineOfSight(self.player.x, self.player.y, x, y): if self.autoMove: - self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs))) + self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs)), closeEnough = False) else: print("{} cannot see that.".format(self.player.name), file = self.outstream) return @@ -315,6 +355,9 @@ Object can be the name of the object, or its coordinates.""" if self.player == None: raise GameError("Cannot take: Player character doesn't exist.") speed = 0.6666667 + if len(args) == 0: + print(f"{self.player.name} reaches out and grasps at nothing.", file = self.outstream) + return if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': speed = 0.3333333 args.pop(0) @@ -330,7 +373,7 @@ Object can be the name of the object, or its coordinates.""" return # Similar to go, but not quite the same. - self.smoothMove(self.player, ((x, y),), speed, lambda: self.setEvent(0.125, _ge.TakeEvent(self.player, thing))) + self.smoothMove(self.player, _gl.PointLocus(x, y), speed, lambda: self.setEvent(0.125, _ge.TakeEvent(self.player, thing))) def drop(self, args): """drop [the] item""" @@ -338,11 +381,14 @@ Object can be the name of the object, or its coordinates.""" raise GameError("Cannot drop: No level has been loaded.") if self.player == None: raise GameError("Cannot drop: Player character doesn't exist.") + if len(args) == 0: + print(f"{self.player.name} falls over.", file = self.outstream) + return if args[0] == 'the': args.pop(0) thingName = ' '.join(args) if thingName in self.player.thingNames: - self.setEvent(0.0, _ge.DropEvent(self.player.getThingByName(thingName))) + self.setEvent(0.0, _ge.DropEvent(self.player, self.player.getThingByName(thingName))) else: print('{0} does not have a {1}.'.format(self.player.name, args[0]), file = self.outstream) @@ -443,9 +489,8 @@ Object can be the name of the object, or its coordinates.""" 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 + # pickle protocol 4 for Python 3.4.0 to 3.7.x + prot = 4 fileName = 'saves/' + args[0].replace(' ', '_') + '.dat' if args[0].endswith('.dat'): fileName = args[0] @@ -475,10 +520,10 @@ Object can be the name of the object, or its coordinates.""" ret = 0 for case in cases: cond = case['case'].split() # should be list like ["value", "==", 1] - if len(cond) == 1 and (cond[0] == 'else' or _gs.getValueFromString(cond[0], {'scriptLocal': {}, 'global': self.customValues})): + if len(cond) == 1 and (cond[0] == 'else' or _gs.getValueFromString(cond[0], _gs.ScriptEnvironment(self.customValues, {}))): ret = self.runDialog(case) break - elif len(cond) == 3 and _gs.compareValues(cond, {'scriptLocal': {}, 'global': self.customValues}): + elif len(cond) == 3 and _gs.compareValues(cond, _gs.ScriptEnvironment(self.customValues, {})): ret = self.runDialog(case) break else: @@ -504,19 +549,15 @@ Object can be the name of the object, or its coordinates.""" if ans[0] == '?': condEnd = ans.index(':') cond = ans[1:condEnd].strip().split() - if _gs.compareValues(cond, {'scriptLocal': {}, 'global': self.customValues}): + if _gs.compareValues(cond, _gs.ScriptEnvironment(self.customValues, {})): options.append(ans[condEnd+1:].strip()) - #print(_tw.fill('{}: {}'.format(j+1, ans[condEnd+1:].strip()), width = TERM_SIZE)) j += 1 else: skips.append(i) else: options.append(ans) - #print(_tw.fill('{}: {}'.format(j+1, ans), width = TERM_SIZE)) j += 1 answer = self.getIO('respondDialog')(options) - #print(answer) - #answer = int(input(self.ps2)) - 1 # account for offset if there were answer options that didn't meet conditions for i in skips: if i <= answer: @@ -534,7 +575,7 @@ Object can be the name of the object, or its coordinates.""" elif len(action) >= 4 and action[:4] == 'back': if len(action) == 4: return 1 - return int(action[4:]) + return int(action[5:]) elif action == 'exit': return 0 else: @@ -561,6 +602,7 @@ Object can be the name of the object, or its coordinates.""" # any map. This is useful for when you have major characters who will # show up in many scenes, and their inventory must stay the same, for # example. + #print(data) self.singletons = {} if 'singletons' in data: for thing in data['singletons']: @@ -580,6 +622,7 @@ Object can be the name of the object, or its coordinates.""" thing.addThing(i) del thing.tempInventory self.singletons[thing.name] = thing + #print(self.singletons) return data def gameEventLoop(self): @@ -591,12 +634,11 @@ Object can be the name of the object, or its coordinates.""" ev = _hq.heappop(self.eventQueue) self.gameTime = ev[0] e = ev[1] - if e.eventType not in self.__gameEvents: - raise GameError("Unhandled event.") ret = False - for i in self.__gameEvents[e.eventType]: - ret = ret or i(e) - self.observe(e) + if e.eventType in self.__gameEvents: + for i in self.__gameEvents[e.eventType]: + ret = ret or i(e) + self.observe(e) # An event can still be observed even if it has no callback. if ret: break if len(self.eventQueue) == 0: @@ -604,8 +646,8 @@ Object can be the name of the object, or its coordinates.""" _ge.resetEventNum() self.skipLoop = True - def setEvent(self, t, e, skip = False): - _hq.heappush(self.eventQueue, (self.gameTime + t, e)) + def setEvent(self, timeFromNow: float, event: _ge.GameEvent, skip = False): + _hq.heappush(self.eventQueue, (self.gameTime + timeFromNow, event)) self.skipLoop = skip def clearEvents(self, actor = None): @@ -634,13 +676,13 @@ Object can be the name of the object, or its coordinates.""" #print(e.actor.x, e.actor.y) #print(e.action) if len(e.path) > 1: - self.setEvent(e.speed, _ge.GoEvent(e.actor, e.path[1:], e.speed, e.action, e.locus, e.timeTaken + e.speed)) + self.setEvent(e.speed, _ge.GoEvent(e.actor, e.path[1:], e.speed, e.action, e.locus, e.closeEnough, e.timeTaken + e.speed)) else: self.setEvent(e.speed, _ge.ArriveEvent(e.actor, e.path[0], e.speed, e.action, e.locus, e.timeTaken + e.speed)) elif isinstance(e.locus, _gl.Locus): - self.smoothMove(e.actor, e.locus, e.speed, e.action, immediate = True) + self.smoothMove(e.actor, e.locus, e.speed, e.action, True, e.closeEnough) else: - print('{0} cannot !reach there.'.format(e.actor.name), file = self.outstream) + print('{0} cannot reach there.'.format(e.actor.name), file = self.outstream) return False def handleArrive(self, e): @@ -652,11 +694,16 @@ Object can be the name of the object, or its coordinates.""" self.level.moveThing(e.actor, e.pos) if e.action == None: #print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(e.actor.name, _gu.numberToLetter(e.x), e.y, e.t), file = self.outstream) + #print ("action = None") if thing: + #print("thing != None") if thing.thingType == 'x': + #print("thing is exit") if e.actor == self.player: + #print("actor = player") self.parseScript(thing.onUse) if (isinstance(thing.key, bool) and thing.key == True) or (isinstance(thing.key, str) and self.parseScript(thing.key)): + #print("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)) @@ -665,25 +712,29 @@ Object can be the name of the object, or its coordinates.""" self.level.removeThing(actor.name) else: e.action() + return False # so that use works. #elif isinstance(e.locus, _gl.Locus): # self.smoothMove(e.actor, e.locus, e.speed, e.action, immediate = True) else: # checking the locus again would inevitably fail, so we just give up. - print('{0} cannot @reach there.'.format(e.actor.name), file = self.outstream) + print('{0} cannot reach there.'.format(e.actor.name), file = self.outstream) return e.actor == self.player - def handleUse(self, e): + def handleUse(self, e: _ge.UseEvent): + """Called when a UseEvent needs processed.""" 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, e.args), _ge.NoOpEvent()) return False - def handleUseOn(self, e): + def handleUseOn(self, e: _ge.UseOnEvent): + """Called when a UseOnEvent needs processed. + It calls the item's useOnFunc in the useFuncs map with the item, target, and arguments.""" 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, e.args), _ge.NoOpEvent()) + self.setEvent(self.__useFuncs[e.item.useOnFunc](e.item, e.target, e.args), _ge.NoOpEvent()) return False def handleTake(self, e): @@ -699,7 +750,7 @@ Object can be the name of the object, or its coordinates.""" 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 - self.actor.removeThing(e.item) + e.actor.removeThing(e.item) return True def handleBehave(self, e): @@ -720,15 +771,24 @@ Object can be the name of the object, or its coordinates.""" """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)) + # Start out with a cursor at 0. + if not 'cursor' in thing.customValues: + thing.customValues['cursor'] = 0 + # With 'single', the cursor will not be modified. if thing.customValues['pattern'] == 'single': - print(self.justifyText(str(thing.customValues['text'])), file = self.outstream) + if isinstance(thing.customValues['text'], str): + print(self.justifyText(str(thing.customValues['text'])), file = self.outstream) + elif isinstance(thing.customValues['text'], list): + print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream) + + # With 'loop', the given strings will keep looping. elif thing.customValues['pattern'] == 'loop': - if not 'cursor' in thing.customValues: - thing.customValues['cursor'] = 0 cursor = thing.customValues['cursor'] print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream) thing.customValues['cursor'] = (cursor + 1) % len(thing.customValues['text']) + + # With 'once', the strings will be used one by one, until it gets to the end. The last one is then repeated. elif thing.customValues['pattern'] == 'once': if not 'cursor' in thing.customValues: thing.customValues['cursor'] = 0 @@ -736,6 +796,8 @@ Object can be the name of the object, or its coordinates.""" print(self.justifyText(str(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(str(thing.customValues['text'][cursor])), file = self.outstream) @@ -784,14 +846,14 @@ Object can be the name of the object, or its coordinates.""" # item use-on functions - def key(self, item, thing, args): + def key(self, key : _gt.Item, door : _gt.Door, args): """Item is a key, which unlocks a door. This may be implemented for containers later.""" - if isinstance(thing, tuple) or thing.thingType != 'd': + if isinstance(door, tuple) or door.thingType != 'd': print("That is not a door.", file = self.outstream) return 0.0 - if thing.lock(item.name): + if door.lock(key.name): print("The key fits the lock.", file = self.outstream) - if not thing.passable and self.player.x == thing.x and self.player.y == thing.y: + if not door.passable and self.player.x == door.x and self.player.y == door.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) @@ -870,10 +932,15 @@ Object can be the name of the object, or its coordinates.""" elif i.casefold() == 'ranged': ranged = True elif i[0:3].casefold() == 'cv:': + cvEnv = customValues equalsign = i.index('=') cv = i[3:equalsign] - customValues[cv] = self.getValueFromString(i[equalsign+1:]) - thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape)) + ns = cv.find(':') + while ns != -1: + cvEnv[cv[1:ns]] = {} + cvEnv = cvEnv[cv[1:ns]] + cvEnv[cv] = self.getValueFromString(i[equalsign+1:]) + thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, _gt.ThingGraphic(bgc, fgc, shape)) elif args[0].casefold() == 'useable': # spawn a useable thing description = 'A nondescript useable thing.' @@ -899,7 +966,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 = _gt.Useable(name, x, y, description, useFunc, customValues, playerx, playery, (bgc, fgc, shape)) + thing = _gt.Useable(name, x, y, description, useFunc, customValues, playerx, playery, _gt.ThingGraphic(bgc, fgc, shape)) elif args[0].casefold() == 'npc': # spawn an NPC description = 'A nondescript character.' @@ -926,7 +993,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 = _gt.NPC(name, x, y, description, behavior, inv, customValues, playerx, playery, (bgc, fgc, shape)) + thing = _gt.NPC(name, x, y, description, behavior, inv, customValues, playerx, playery, _gt.ThingGraphic(bgc, fgc, shape)) elif args[0].casefold() == 'door': # spawn a door description = 'a nondescript door.' @@ -945,7 +1012,7 @@ Object can be the name of the object, or its coordinates.""" description = i[12:] elif i.casefold() == 'locked': locked = True - thing = _gt.Door(name, x, y, locked, description, key, (bgc, fgc, shape)) + thing = _gt.Door(name, x, y, locked, description, key, _gt.ThingGraphic(bgc, fgc, shape)) elif args[0].casefold() == 'mapexit': # spawn an exit to another map (use with EXTREME caution!) destination = '' @@ -972,7 +1039,7 @@ Object can be the name of the object, or its coordinates.""" onUse = i[6:] elif i[0:4].casefold() == 'key=': key = i[4:] - thing = _gt.MapExit(name, x, y, exitid, destination, prefix, onUse, key, (bgc, fgc, shape)) + thing = _gt.MapExit(name, x, y, exitid, destination, prefix, onUse, key, _gt.ThingGraphic(bgc, fgc, shape)) elif args[0].casefold() == 'mapentrance': # spawn a map entrance exitid = 0 @@ -980,6 +1047,11 @@ Object can be the name of the object, or its coordinates.""" if i[0:7].casefold() == 'exitid=': exitid = int(i[7:]) thing = _gt.MapEntrance(x, y, exitid, name) + elif args[0].casefold() == 'singleton': + if name in self.singletons: + single = self.singletons[name] + single.x, single.y = x, y + single.prevx, single.prevy = x, y else: raise GameError("{} not a valid thing type.".format(args[0])) self.nextThing = self.level.addThing(thing, self.nextThing, persist) @@ -1036,7 +1108,7 @@ Object can be the name of the object, or its coordinates.""" equalsign = i.index('=') cv = i[3:equalsign] customValues[cv] = getValueFromString(i[equalsign+1:]) - thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape)) + thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, _gt.ThingGraphic(bgc, fgc, shape)) thing.thingID = self.nextThing self.player.addThing(thing) self.nextThing += 1 @@ -1106,25 +1178,35 @@ Object can be the name of the object, or its coordinates.""" # behaviors def stand(self, actor): - pass + return 0 def wander(self, actor): - pass + return 0 def follow(self, actor, event): # make sure we only follow who we want to - if event.actor.name != actor.customValues["follow"]["target"]: + if "follow" not in actor.customValues: return 0 + if "isFollowing" not in actor.customValues["follow"]: + return 0 + if actor.customValues["follow"]["isFollowing"]: + if event.actor.name != actor.customValues["follow"]["target"]: + if actor.customValues["follow"]["target"] == '': + # This is a quick-and-dirty fix for an issue I was having with followers. + actor.customValues["follow"]["target"] = actor + return 0 + else: + dest = 0 + #print(event.eventType) + if event.eventType == 'go': + dest = event.path[-1] + elif event.eventType == 'arrive': + dest = event.pos + x, y = self.level.intToCoords(dest) + self.smoothMove(actor, _gl.CircleLocus(x, y, 2), event.speed, action = lambda: self.setEvent(0, _ge.BehaveEvent(actor)), closeEnough = False) + return -1 else: - dest = 0 - #print(event.eventType) - if event.eventType == 'go': - dest = event.path[-1] - elif event.eventType == 'arrive': - dest = event.pos - x, y = self.level.intToCoords(dest) - self.smoothMove(actor, _gl.CircleLocus(x, y, 2), event.speed, action = lambda: self.setEvent(0, _ge.BehaveEvent(actor))) - return -1 + return 0 # stuff for extended classes to use def registerUseFunc(self, name, func): @@ -1159,3 +1241,7 @@ always give the player a turn, False otherwise.""" if name not in self.__IOCalls: raise GameError("No IO call for {}.".format(name)) return self.__IOCalls[name] + +# |\_/| +# /0 0\ +# \o/ diff --git a/gameevents.py b/gameevents.py index 5c366a4..322bf1f 100644 --- a/gameevents.py +++ b/gameevents.py @@ -63,13 +63,14 @@ class NoOpEvent(GameEvent): class GoEvent(GameEvent): - def __init__(self, actor, path, speed, action = None, locus = None, timeTaken = 0): + def __init__(self, actor, path, speed, action = None, locus = None, closeEnough = True, timeTaken = 0): super(GoEvent, self).__init__('go') self.actor = actor self.path = path self.speed = speed self.action = action self.locus = locus + self.closeEnough = closeEnough self.timeTaken = timeTaken @property @@ -95,10 +96,10 @@ class UseEvent(GameEvent): self.args = args class UseOnEvent(GameEvent): - def __init__(self, actor, item, thing, args): + def __init__(self, actor, item, target, args): super(UseOnEvent, self).__init__('useon') self.actor = actor - self.thing = thing # thing can be a coordinate pair? + self.target = target # thing can be a coordinate pair? self.item = item self.args = args @@ -118,3 +119,7 @@ class BehaveEvent(GameEvent): def __init__(self, actor): super(BehaveEvent, self).__init__('behave') self.actor = actor + +# |\_/| +# /0 0\ +# \o/ diff --git a/gamemap.py b/gamemap.py index 9da2f47..5db855f 100644 --- a/gamemap.py +++ b/gamemap.py @@ -338,7 +338,6 @@ list of lists of tuples.""" # if startThing and not startThing.passable: # return -1, [], -1 # meaning you can't get there dist, prev, endPoint = self.dijkstra(x1, y1, loc, closeEnough) - #endPoint = self.coordsToInt(x2, y2) numVertex = self.dimensions[0] * self.dimensions[1] if endPoint > -1 and dist[endPoint] < numVertex + 1: pathList = [endPoint] @@ -356,35 +355,39 @@ list of lists of tuples.""" The closeEnough parameter will create a path that lands beside the source if necessary. The loc parameter is an optional locus which will cause the function to return once it finds a point that's in the locus.""" - # first test to see that the start point is passable - startThing = self.getThingAtCoords(x1, y1) - startPoint = self.coordsToInt(x1, y1) endPoint = -1 # until one matches the locus, which it might not. numVertex = self.dimensions[0] * self.dimensions[1] dist = [numVertex + 1 for i in range(numVertex)] prev = [-1 for i in range(numVertex)] + # validation + if loc == None or not self.__validateCoords(x1, y1): + # start point is out-of-bounds or there is no destination + return dist, prev, endPoint + # first test to see that the start point is passable + startThing = self.getThingAtCoords(x1, y1) + startPoint = self.coordsToInt(x1, y1) dist[startPoint] = 0 - #if closeEnough: - # if startThing and not startThing.passable: - # dist[startPoint] = -1 # This is so it doesn't path into a non-passable end point. queue = [] heapq.heappush(queue, (dist[startPoint], startPoint)) while len(queue) > 0: u = heapq.heappop(queue)[1] if loc != None and self.intToCoords(u) in loc: - return dist, prev, u + if endPoint == -1 or dist[u] < dist[endPoint]: + endPoint = u + #print(f"endPoint: {endPoint}; Reason: in locus") for v in self.mapGraph[u]: thing = self.getThingAtPos(v) if thing and not thing.passable: - if closeEnough and self.intToCoords(v) in loc: - return dist, prev, u + if closeEnough and self.intToCoords(v) in loc and (endPoint == -1 or dist[u] < dist[endPoint]): + endPoint = u + #print(f"endPoint: {endPoint}; Reason: good enough") else: continue tempDist = dist[u] + 1 if tempDist < dist[v]: dist[v] = tempDist - if dist[u] != -1: + if dist[u] != numVertex + 1: prev[v] = u heapq.heappush(queue, (dist[v], v)) @@ -396,6 +399,8 @@ to return once it finds a point that's in the locus.""" def lineOfSight(self, x1, y1, x2, y2): """Test for line of signt from one tile to another.""" + if not (self.__validateCoords(x1, y1) and self.__validateCoords(x2, y2)): + return False # Trivial case first: if abs(x1 - x2) <= 1 and abs(y1 - y2) <= 1: return True @@ -410,10 +415,14 @@ to return once it finds a point that's in the locus.""" return True def isPassable(self, x, y = -1): - pos = x if y == -1: + if not self.__validatePos(x): + return False + pos = x x, y = self.intToCoords(x) else: + if not self.__validateCoords(x, y): + return False pos = self.coordsToInt(x, y) if self.mapMatrix[y][x][0] == 'w': return False @@ -422,6 +431,12 @@ to return once it finds a point that's in the locus.""" if not thing.passable: return False return True + + def __validateCoords(self, x: int, y: int) -> bool: + return x >= 0 and x < self.dimensions[0] and y >= 0 and y < self.dimensions[1] + + def __validatePos(self, pos: int) -> bool: + return pos >= 0 and pos < self.dimensions[0] * self.dimensions[1] @staticmethod def __coordsToInt(x, y, width): @@ -633,3 +648,6 @@ class LoSLocus(_gl.Locus): ret.append(point) return iter(ret) +# |\_/| +# /0 0\ +# \o/ diff --git a/gamesequence.py b/gamesequence.py index 950d8e1..ffc8b4e 100644 --- a/gamesequence.py +++ b/gamesequence.py @@ -1,48 +1,92 @@ # gamesequence.py +"""This contains the functions and classes necessary to parse and execute the scripting language. + +Classes: + - SequenceError: Derived from RuntimeError. + - ScriptBreak: Represent a 'break' statement. + - ScriptEnvironment: Represent the environment of a script. + +Functions: + - getValueFromString: Translate a literal or variable to a value. + - compareValues: compare values for a conditional statement. + - ifScript: Execute an 'if' statement. + - getCustomValue: Get the value of a global variable. + - setCustomValue: Set the value of a global variable. + - delCustomValue: Delete a global variable. + - runScript: Execute a script. + - parseScript: Parse a script. +""" import re as _re class SequenceError(RuntimeError): + """Derived from RuntimeError. + + Raise whenever there is a runtime execution problem while parsing or executing a script. + """ + pass class ScriptBreak(object): + """Class created when a 'break' statement is read.""" def __init__(self, value): + """Create a script break object with the value returned from the script broken from.""" self.value = value - -def getValueFromString(arg: str, env: dict): - #if env == None: - #env = self.customValues +class ScriptEnvironment(object): + """Represent the environment of a script. + + This contains a dictionary of all global variables, as well as local variables.""" + + def __init__(self, globalVars: dict, localVars: dict): + """Create a script environment. + + Global vars will survive beyond the end of the script. + Local vars are all dropped at the end of a script.""" + + if not isinstance(globalVars, dict): + raise TypeError("Global variables must be in a dictionary.") + if not isinstance(globalVars, dict): + raise TypeError("Local variables must be in a dictionary.") + self.globalVars = globalVars + self.localVars = localVars + +def getValueFromString(arg: str, env: ScriptEnvironment): + """Translate a literal or variable name into a value. + + arg should be a string representing a literal or identifier. + env should be a ScriptEnvironment. + Return the value described by the first literal or variable name.""" val = None + # We test for a valid identifier here, before all the if-elif starts. validIdent = _re.match(r'[_A-Za-z][_0-9A-Za-z]*', arg) - if arg[0] in '"\'' and arg[-1] == arg[0]: # assume it's a string + if arg[0] in '"\'' and arg[-1] == arg[0]: + # The argument is a string literal. val = arg[1:-1] elif _re.match(r'[+-]?(?:[0-9]*[.])?[0-9]+', arg) != None: + # The argument is a number. if '.' in arg: + # The argument is a float. val = float(arg) else: + # The argument is an int. val = int(arg) elif arg.casefold() == 'true': + # The argument is the boolean value 'true'. val = True elif arg.casefold() == 'false': + # The argument is the boolean value 'false'. val = False - #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 and env != None: + # The argument is a variable name. group = validIdent.group() - if 'scriptLocal' in env and group in env['scriptLocal']: - val = env['scriptLocal'][group] - elif group in env['global']: - val = env['global'][group] + if group in env.localVars: + # The variable is local. + val = env.localVars[group] + elif group in env.globalVars: + # The variable is global. + val = env.globalVars[group] else: return False # for if statements; if a variable doesn't exist, should evaluate to False # evaluate all values of all indecies @@ -67,16 +111,19 @@ def getValueFromString(arg: str, env: dict): return False openBracket = ptr + 1 ptr += 1 + if depth != 0: + #depth should always == 0 at this point + raise SequenceError('Mismatched number of open and close brackets: {}'.format(arg)) else: raise SequenceError('Invalid value syntax: {}'.format(arg)) else: raise SequenceError('Invalid argument to getValueFromString: {}'.format(arg)) return val -def compareValues(args: list, env: dict): +def compareValues(args: list, env: ScriptEnvironment): """Generalized comparisons, may eventually be extended to other operators""" if len(args) == 1: - return bool(getValueFromString(args[0]), env) + return bool(getValueFromString(args[0], env)) elif len(args) == 3 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'): lval = getValueFromString(args[0], env) operator = args[1] @@ -94,14 +141,11 @@ def compareValues(args: list, env: dict): elif operator == '>': return lval > rval elif operator == 'in': - #if args[2].casefold() == 'playerinv': - # return lval in self.player.thingNames - #else: return lval in rval else: raise SequenceError("Condition cannot be evaluated: {}".format(' '.join(args))) -def ifScript(args, env: dict, externalScripts: dict): +def ifScript(args, env: ScriptEnvironment, externalScripts: dict): """If statement: if [not] value [op value] script""" if len(args) < 2: raise GameError('Incomplete If statement: if {}'.format(' '.join(args))) @@ -120,7 +164,7 @@ def ifScript(args, env: dict, externalScripts: dict): ret = compareValues(args[:3], env) args = args[3:] else: - ret = bool(getValueFromString(args[0]), env) + ret = bool(getValueFromString(args[0], env)) args = args[1:] if inverse: ret = not ret @@ -141,9 +185,9 @@ def setCustomValue(args, env: dict): """takes [customValue, op, value]""" if env == None: raise SequenceError("Cannot set a value from an empty environment.") - scope = env['scriptLocal'] + scope = env.localVars if len(args) > 0 and args[0] == 'global': - scope = env['global'] + scope = env.globalVars args.pop(0) if len(args) < 3 or args[1] not in ('=', '+=', '-=', '*=', '/=', '%=', '//=', '**=', 'b=', '!=', '|=', '&=', '^='): raise SequenceError('Arguments are not fit for the setCustomValue script.') @@ -201,18 +245,18 @@ def setCustomValue(args, env: dict): scope[args[0]] ^= val return scope[args[0]] -def delCustomValue(args, env: dict): +def delCustomValue(args, env: ScriptEnvironment): """To clean up after a map.""" if env == None: raise SequenceError("Cannot delete a value from an empty environment.") for i in args: - if i in env['global']: - del(env['global'][i]) + if i in env.globalVars: + del(env.globalVars[i]) _requireEnv = {'get' : getCustomValue, 'set' : setCustomValue, 'del' : delCustomValue} _requireScripts = {'if' : ifScript} -def runScript(script: list, env: dict, externalScripts: dict): +def runScript(script: list, env: ScriptEnvironment, externalScripts: dict): """run a script""" ret = False for line in script: @@ -228,13 +272,13 @@ def runScript(script: list, env: dict, externalScripts: dict): ret = _requireEnv[line[0]](line[1:], env) elif line[0].casefold() == 'break': # exit early - return _ScriptBreak(ret) + return ScriptBreak(ret) elif line[0] in externalScripts: # run a script ret = externalScripts[line[0]](line[1:]) else: # conditional evaluation - compareValues(line, env) + return compareValues(line, env) if isinstance(ret, ScriptBreak): return ret @@ -306,9 +350,7 @@ def parseScript(instr: str, envGlobal: dict, externalScripts: dict): ret.append(''.join(literalStr[argStart:l])) if len(ret) > 0: script.append(ret) - #print('After parsing: {}'.format(script)) - env = {'global' : envGlobal, 'scriptLocal' : {}} - #self.customValues['scriptLocal'] = {} + env = ScriptEnvironment(envGlobal, {}) ret = runScript(script, env, externalScripts) if isinstance(ret, ScriptBreak): ret = ret.value diff --git a/gameshell.py b/gameshell.py index 6f1e610..45c7e51 100644 --- a/gameshell.py +++ b/gameshell.py @@ -3,6 +3,7 @@ from shell import Shell from gamebase import GameBase import sys as _sys +import os as _os #import re import heapq #import gamemap @@ -23,12 +24,12 @@ class GameShell(Shell): WALLS = ('++', '++', ' +', '++', '++', '||', '++', '|+', '+ ', '++', '==', '++', '++', '+|', '++', '++') - def __init__(self, gameBase): + def __init__(self, gameBase, gameData = 'testing/testdata.yml'): super(GameShell, self).__init__() self.outstream = _sys.stdout self.gameBase = gameBase self.colorMode = 0 - data = self.gameBase.loadGameData('testing/testdata.yml') + data = self.gameBase.loadGameData(gameData) self.gameTitle = 'Game Shell' # should be changed for actual games if 'title' in data: self.gameTitle = data['title'] @@ -38,12 +39,16 @@ class GameShell(Shell): self.openingText = '{}\nIn Development' if 'openingText' in data: self.openingText = data['openingText'] + if 'playerName' in data: + self.gameBase.playerName = data['playerName'] + if 'playerDescription' in data: + self.gameBase.playerDescription = data['playerDescription'] self.ps2 = '?> ' self.__inGame = False # register functions - self.registerCommand('load', self.gameBase.loadGame) # should always be available + self.registerCommand('load', self.loadGame) # should always be available self.registerCommand('flippetywick', self.devMode) self.registerCommand('options', self.options) self.registerCommand('colorTest', self.colorTest) @@ -62,7 +67,6 @@ class GameShell(Shell): 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 @@ -171,25 +175,26 @@ If -l is given, a map legend will be printed under the map.""" for i in things[1:]: if priorities[i.thingType] < priorities[thing.thingType]: thing = i - if thing.graphic[1] != textColor: - textColor = thing.graphic[1] + if thing.graphic.foreground != textColor: + textColor = thing.graphic.foreground + #print(textColor) rows[-1].append(self.color(textColor[1:])) 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]) + exits[thing.exitid] = (thing.name, thing.graphic.foreground) elif thing.thingType == 'n': # NPC - characters[len(characters)+1] = (thing.name, thing.graphic[1]) + characters[len(characters)+1] = (thing.name, thing.graphic.foreground) rows[-1].append('C{0}'.format(len(characters))) elif thing.thingType == 'd': # door - doors[len(doors)+1] = (thing.name, thing.graphic[1]) + doors[len(doors)+1] = (thing.name, thing.graphic.foreground) rows[-1].append('D{0}'.format(len(doors))) elif thing.thingType == 'u': # useable - useables[len(useables)+1] = (thing.name, thing.graphic[1]) + useables[len(useables)+1] = (thing.name, thing.graphic.foreground) rows[-1].append('U{0}'.format(len(useables))) elif thing.thingType == 'i': # item - items[len(items)+1] = (thing.name, thing.graphic[1]) + items[len(items)+1] = (thing.name, thing.graphic.foreground) rows[-1].append('I{0}'.format(len(items))) else: # entrance rows[-1].append(' ') @@ -254,7 +259,7 @@ If -l is given, a map legend will be printed under the map.""" 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("{0:.<8.3}:{1:.>71}".format(i[0], str(i[1]))) ret.append("custom values:") for i in self.gameBase.customValues: ret.append("{0:<22}: {1}".format(i, self.gameBase.customValues[i])) @@ -283,19 +288,19 @@ If -l is given, a map legend will be printed under the map.""" response = input("Do you want to save before exiting (Y/n/x)? ") if len(response) > 0: if response[0] in 'Nn': - super(GameShell, self).exitShell(args) + pass elif response[0] in 'Xx': # cancel return else: sf = input('Save file: ') if len(sf) > 0: - gameBase.saveGame([sf]) + self.gameBase.saveGame([sf]) else: print('No save file given, cancelling exit.') else: sf = input('Save file: ') if len(sf) > 0: - gameBase.saveGame([sf]) + self.gameBase.saveGame([sf]) else: print('No save file given, cancelling exit.') try: @@ -305,6 +310,43 @@ If -l is given, a map legend will be printed under the map.""" return self.gameMode() + def loadGame(self, args): + # Check if there's a name in args. If not, just present a list of save files. + if len(args) == 0: + import pickle as _pi + for fileName in _os.listdir('saves'): + with open(f'saves/{fileName}', 'rb') as f: + player, levelname, persist, eventQueue, customValues, gameTime, nextThing = _pi.load(f) + print(f'{fileName[:-4]}\n - {levelname[5:-4]}') + return + if self.__inGame: + response = input("Do you want to save before exiting (Y/n/x)? ") + if len(response) > 0: + if response[0] in 'Nn': + pass + elif response[0] in 'Xx': # cancel + return + else: + sf = input('Save file: ') + if len(sf) > 0: + self.gameBase.saveGame([sf]) + else: + print('No save file given, cancelling exit.') + return + else: + sf = input('Save file: ') + if len(sf) > 0: + self.gameBase.saveGame([sf]) + else: + print('No save file given, cancelling exit.') + return + try: + self.gameBase.loadGame(args) + except RuntimeError as e: + print(e) + return + self.gameMode() + def gameMode(self): """Mode for in-game.""" if not self.__inGame: @@ -366,7 +408,7 @@ If -l is given, a map legend will be printed under the map.""" # IO calls - def container(self, player, cont): + def container(self, player, cont: list): """container IO Player is modified through side-effect.""" # Pretty print: get length of the longest inventory item's name @@ -393,36 +435,64 @@ Player is modified through side-effect.""" print(i.name) # Now, actually interacting with the container timeSpent = 0.5 # using a container always takes at least 1/2 second, even just opening and closing it again. - instr = input("Take, store, or exit: ") + instr = input("Look, take, store, or exit: ") while instr != "exit": instr = instr.split() - if instr[0] == "take": - # take something out of the container - if instr[1] == "the": - del instr[1] - thing = ' '.join(instr[1:]) - for i in range(len(cont)): - if thing == cont[i].name: - player.addThing(cont[i]) - del cont[i] + if len(instr) != 0: + if instr[0] == "take": + # take something out of the container + if len(instr) > 1 and instr[1] == "the": + del instr[1] + thingName = ' '.join(instr[1:]) + if len(thingName) == 0: + print(f"{self.gameBase.player.name} takes nothing.") + else: + for i in range(len(cont)): + if thingName == cont[i].name: + player.addThing(cont[i]) + del cont[i] + timeSpent += 0.5 + print(f"{thingName} taken.") + break + else: + # If it got here, it didn't find it. + print(f"No {thingName} in container.") + elif instr[0] == "store": + # store something in the container + if len(instr) > 1 and instr[1] == "the": + del instr[1] + thingName = ' '.join(instr[1:]) + if len(thingName) == 0: + print(f"{self.gameBase.player.name} stores nothing.") + elif thingName in player.thingNames: + cont.append(player.removeThingByName(thingName)) + print(f"{thingName} stored.") 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:]) - 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: ") + else: + print(f"No {thingName} in inventory.") + elif instr[0] == "look": + # look at something in the container + if len(instr) > 1 and instr[1] == "at": + del instr[1] + if len(instr) > 1 and instr[1] == "the": + del instr[1] + thingName = ' '.join(instr[1:]) + if len(thingName) == 0: + print(f"{self.gameBase.player.name} looks at nothing.") + else: + # Check the container first. + for i in range(len(cont)): + if thingName == cont[i].name: + print(self.gameBase.justifyText(str(cont[i]))) + break + else: + # If it wasn't there, try the player's inventory. + if thingName in player.thingNames: + print(self.gameBase.justifyText(str(player.getThingByName(thingName)))) + else: + # If we get here, it just isn't there. + print(f"There is no {thingName} to look at.") + instr = input("Look, take, store, or exit: ") return cont, timeSpent def info(self, items): @@ -456,7 +526,11 @@ Player is modified through side-effect.""" print(_tw.fill('{}: {}'.format(lineNo+1, options[lineNo]), width = TERM_SIZE)) answer = -1 while answer < 0 or answer >= len(options): - answer = int(input(self.ps2)) - 1 + answerString = input(self.ps2) + if not answerString.isdigit(): + # If the player inputs a non-integer, just prompt again. + continue + answer = int(answerString) - 1 return answer def endDialog(self): @@ -479,14 +553,14 @@ Player is modified through side-effect.""" else: sf = input('Save file: ') if len(sf) > 0: - gameBase.saveGame([sf]) + self.gameBase.saveGame([sf]) super(GameShell, self).exitShell(args) else: print('No save file given, cancelling exit.') else: sf = input('Save file: ') if len(sf) > 0: - gameBase.saveGame([sf]) + self.gameBase.saveGame([sf]) super(GameShell, self).exitShell(args) else: print('No save file given, cancelling exit.') @@ -497,3 +571,7 @@ if __name__ == '__main__': sh = GameShell(GameBase()) sh.menuMode() sh.run() + +# |\_/| +# /0 0\ +# \o/ diff --git a/gamethings.py b/gamethings.py index a411c66..8909bbf 100644 --- a/gamethings.py +++ b/gamethings.py @@ -1,8 +1,51 @@ #gamethings.py +"""Standard thing classes. + +Classes: + - ThingGraphic: Represents how thing is to be visualized. + - Thing: The abstract base class for every thing. + - Observer: The abstract base class for things that listen to events. + - Item: A thing that can exist in a character's inventory. + - Useable: A thing that can be used by a character. + - Character: A thing that represents a character. + - NPC: A character that is not controlled by the player. + - Door: A thing that sometimes blocks paths. + - MapExit: A technical thing that marks a map transition. +""" import ruamel.yaml from ruamel.yaml.comments import CommentedMap +class ThingGraphic(object): + """Represent how a thing is to be visualized. + + Background color, foreground color, and shape are represented. + This could hypothetically be extended to hold sprites, textures or models.""" + + def __init__(self, background: str, foreground: str, shape: str): + """Create a graphic for a thing. + + The background and foregrond must be strings with color hex representations. + For instance: '#00FF00' means bright green. + The shape must be a string containing exactly one of the following shapes: + o: circle + x: cross + -: horizontal line + |: vertical line + #: square + ^: triangle + A graphic may be invalid. + """ + if not isinstance(background, str): + raise TypeError("Background must be a string of form '#[0-9A-Fa-f]{6}'.") + if not isinstance(foreground, str): + raise TypeError("Foreground must be a string of form '#[0-9A-Fa-f]{6}'.") + if not isinstance(shape, str): + raise TypeError("Shape must be a string of form '[-|ox#^]'.") + self.background = background + self.foreground = foreground + self.shape = shape + class Thing(object): def __init__(self, thingType: str, name: str, x: int, y: int, description: str, flags: int, playerx = None, playery = None, **kwargs): @@ -24,7 +67,7 @@ class Thing(object): self.lookable = bool(flags & 4) self.takeable = bool(flags & 8) self.useable = bool(flags & 16) - self.graphic = ('clear', '#7F7F7F', ' ') + self.graphic = ThingGraphic('clear', '#7F7F7F', ' ') self.thingID = -1 # ID not assigned def __str__(self): @@ -49,7 +92,7 @@ class Observer(Thing): class Item(Thing): yaml_flag = u'!Item' - defaultGraphic = ('clear', '#00BF00', '^') + defaultGraphic = ThingGraphic('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) @@ -68,12 +111,12 @@ class Item(Thing): 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 node.graphic.background != Item.defaultGraphic.background: + graphic['bgc'] = node.graphic.background + if node.graphic.foreground != Item.defaultGraphic.forground: + graphic['fgc'] = node.graphic.foreground + if node.graphic.shape != Item.defaultGraphic.shape: + graphic['shape'] = node.graphicshape if len(graphic) > 0: ret['graphic'] = graphic # save use functions @@ -96,9 +139,9 @@ class Item(Thing): useOnFunc = '' customValues = {} ranged = False - bgc = Item.defaultGraphic[0] - fgc = Item.defaultGraphic[1] - shape = Item.defaultGraphic[2] + bgc = Item.defaultGraphic.background + fgc = Item.defaultGraphic.foreground + shape = Item.defaultGraphic.shape # load graphic if 'graphic' in parts: if 'bgc' in parts['graphic']: @@ -107,7 +150,7 @@ class Item(Thing): fgc = parts['graphic']['fgc'] if 'shape' in parts['graphic']: shape = parts['graphic']['shape'] - graphic = (bgc, fgc, shape) + graphic = ThingGraphic(bgc, fgc, shape) # load use functions if 'useFunc' in parts: useFunc = parts['useFunc'] @@ -131,7 +174,7 @@ class Item(Thing): class Useable(Thing): yaml_flag = u'!Useable' - defaultGraphic = ('clear', '#0000FF', '#') + defaultGraphic = ThingGraphic('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) @@ -145,12 +188,12 @@ class Useable(Thing): 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 node.graphic.background != Useable.defaultGraphic.background: + graphic['bgc'] = node.graphic.background + if node.graphic.foreground != Useable.defaultGraphic.foreground: + graphic['fgc'] = node.graphic.foreground + if node.graphic.shape != Useable.defaultGraphic.shape: + graphic['shape'] = node.graphic.shape if len(graphic) > 0: ret['graphic'] = graphic # save use functions @@ -170,9 +213,9 @@ class Useable(Thing): useFunc = '' customValues = {} playerx, playery = parts['location'] - bgc = Useable.defaultGraphic[0] - fgc = Useable.defaultGraphic[1] - shape = Useable.defaultGraphic[2] + bgc = Useable.defaultGraphic.background + fgc = Useable.defaultGraphic.foreground + shape = Useable.defaultGraphic.shape # load graphic if 'graphic' in parts: if 'bgc' in parts['graphic']: @@ -181,7 +224,7 @@ class Useable(Thing): fgc = parts['graphic']['fgc'] if 'shape' in parts['graphic']: shape = parts['graphic']['shape'] - graphic = (bgc, fgc, shape) + graphic = ThingGraphic(bgc, fgc, shape) # load use functions if 'useFunc' in parts: useFunc = parts['useFunc'] @@ -199,7 +242,7 @@ class Useable(Thing): pass class Character(Thing): - defaultGraphic = ('clear', '#000000', 'o') + defaultGraphic = ThingGraphic('clear', '#000000', 'o') def __init__(self, inventory: dict, customValues: dict, graphic = defaultGraphic, **kwargs): super(Character, self).__init__(**kwargs) @@ -211,7 +254,7 @@ class Character(Thing): self.__inventory = inventory self.customValues = customValues self.graphic = graphic - self.thingNames = {} + self.thingNames = {} #{str: int} # set up inventory shtuff for i in self.__inventory: if self.__inventory[i].name in self.thingNames: @@ -229,7 +272,10 @@ class Character(Thing): self.thingNames[thing.name] = [thing.thingID] def getThingByID(self, thingID): - return self.__inventory[thingID] + if thingID in self.__inventory: + return self.__inventory[thingID] + else: + return None def getThingByName(self, name): if name in self.thingNames: @@ -247,10 +293,10 @@ class Character(Thing): def removeThingByName(self, name): ret = self.getThingByName(name) - self.thingNames[ret.name].remove(thingID) + self.thingNames[ret.name].remove(ret.thingID) if len(self.thingNames[ret.name]) == 0: del self.thingNames[ret.name] - del self.__inventory[thingID] + del self.__inventory[ret.thingID] return ret def removeThing(self, ret): @@ -267,7 +313,7 @@ class Character(Thing): class NPC(Character, Observer): yaml_flag = u'!NPC' - defaultGraphic = ('clear', '#000000', 'o') + defaultGraphic = ThingGraphic('clear', '#000000', 'o') def __init__(self, behaviors: dict, tempInventory: list, **kwargs): if 'graphic' not in kwargs: @@ -284,12 +330,12 @@ class NPC(Character, Observer): 'description': node.description, 'behaviors': node.behaviors} # save graphic graphic = {} - if node.graphic[0] != NPC.defaultGraphic[0]: - graphic['bgc'] = node.graphic[0] - if node.graphic[1] != NPC.defaultGraphic[1]: - graphic['fgc'] = node.graphic[1] - if node.graphic[2] != NPC.defaultGraphic[2]: - graphic['shape'] = node.graphic[2] + if node.graphic.background != Useable.defaultGraphic.background: + graphic['bgc'] = node.graphic.background + if node.graphic.foreground != Useable.defaultGraphic.foreground: + graphic['fgc'] = node.graphic.foreground + if node.graphic.shape != Useable.defaultGraphic.shape: + graphic['shape'] = node.graphic.shape if len(graphic) > 0: ret['graphic'] = graphic # save use functions @@ -310,9 +356,9 @@ class NPC(Character, Observer): minventory = [] mcustomValues = {} mplayerx, mplayery = parts['location'] - bgc = NPC.defaultGraphic[0] - fgc = NPC.defaultGraphic[1] - shape = NPC.defaultGraphic[2] + bgc = NPC.defaultGraphic.background + fgc = NPC.defaultGraphic.foreground + shape = NPC.defaultGraphic.shape # load graphic if 'graphic' in parts: if 'bgc' in parts['graphic']: @@ -321,7 +367,7 @@ class NPC(Character, Observer): fgc = parts['graphic']['fgc'] if 'shape' in parts['graphic']: shape = parts['graphic']['shape'] - mgraphic = (bgc, fgc, shape) + mgraphic = ThingGraphic(bgc, fgc, shape) # load use functions if 'inventory' in parts: inventory = parts['inventory'] @@ -338,7 +384,7 @@ class NPC(Character, Observer): class Door(Thing): yaml_flag = u'!Door' - defaultGraphic = ('clear', '#7F3F00', '#') + defaultGraphic = ThingGraphic('clear', '#7F3F00', '#') def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = defaultGraphic): self.descBase = description @@ -379,12 +425,12 @@ class Door(Thing): 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 node.graphic.background != Useable.defaultGraphic.background: + graphic['bgc'] = node.graphic.background + if node.graphic.foreground != Useable.defaultGraphic.foreground: + graphic['fgc'] = node.graphic.foreground + if node.graphic.shape != Useable.defaultGraphic.shape: + graphic['shape'] = node.graphic.shape if len(graphic) > 0: ret['graphic'] = graphic # save door state @@ -404,9 +450,9 @@ class Door(Thing): description = None locked = False key = None - bgc = Door.defaultGraphic[0] - fgc = Door.defaultGraphic[1] - shape = Door.defaultGraphic[2] + bgc = Door.defaultGraphic.background + fgc = Door.defaultGraphic.foreground + shape = Door.defaultGraphic.shape # load graphic if 'graphic' in parts: if 'bgc' in parts['graphic']: @@ -415,7 +461,7 @@ class Door(Thing): fgc = parts['graphic']['fgc'] if 'shape' in parts['graphic']: shape = parts['graphic']['shape'] - graphic = (bgc, fgc, shape) + graphic = ThingGraphic(bgc, fgc, shape) # load door state if 'description' in parts: description = parts['description'] @@ -428,7 +474,7 @@ class Door(Thing): class MapExit(Thing): yaml_flag = u'!MapExit' - defaultGraphic = ('clear', '#FF0000', 'x') + defaultGraphic = ThingGraphic('clear', '#FF0000', 'x') def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix = None, onUse = '', key = True, graphic = defaultGraphic): description = name @@ -448,12 +494,12 @@ class MapExit(Thing): 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 node.graphic.background != Useable.defaultGraphic.background: + graphic['bgc'] = node.graphic.background + if node.graphic.foreground != Useable.defaultGraphic.foreground: + graphic['fgc'] = node.graphic.foreground + if node.graphic.shape != Useable.defaultGraphic.shape: + graphic['shape'] = node.graphic.shape if len(graphic) > 0: ret['graphic'] = graphic if node.prefix != None: @@ -472,9 +518,9 @@ class MapExit(Thing): prefix = None onUse = '' key = True - bgc = MapExit.defaultGraphic[0] - fgc = MapExit.defaultGraphic[1] - shape = MapExit.defaultGraphic[2] + bgc = MapExit.defaultGraphic.background + fgc = MapExit.defaultGraphic.foreground + shape = MapExit.defaultGraphic.shape # load graphic if 'graphic' in parts: if 'bgc' in parts['graphic']: @@ -483,7 +529,7 @@ class MapExit(Thing): fgc = parts['graphic']['fgc'] if 'shape' in parts['graphic']: shape = parts['graphic']['shape'] - graphic = (bgc, fgc, shape) + graphic = ThingGraphic(bgc, fgc, shape) if 'prefix' in parts: prefix = parts['prefix'] if 'onUse' in parts: @@ -495,7 +541,7 @@ class MapExit(Thing): class MapEntrance(Thing): yaml_flag = u'!MapEntrance' - defaultGraphic = ('clear', 'clear', 'x') + defaultGraphic = ThingGraphic('clear', 'clear', 'x') # no graphic - should not be drawn def __init__(self, x: int, y: int, exitid: int, name = None): @@ -503,6 +549,7 @@ class MapEntrance(Thing): name = 'entrance {}'.format(exitid) super(MapEntrance, self).__init__('a', name, x, y, '', 1) self.exitid = exitid + #self.graphic = MapEntrance.defaultGraphic @classmethod def to_yaml(cls, representer, node): @@ -519,7 +566,7 @@ class MapEntrance(Thing): class PlayerCharacter(Character): """Player object. Cannot be created with yaml.""" - defaultGraphic = ('clear', '#0000FF', 'o') + defaultGraphic = ThingGraphic('clear', '#0000FF', 'o') def __init__(self, **kwargs): if 'name' not in kwargs: diff --git a/gameutil.py b/gameutil.py index ed8f8a9..935f847 100644 --- a/gameutil.py +++ b/gameutil.py @@ -46,6 +46,8 @@ def parseCoords(level, args, usePlayerCoords = True, player = None): Returns (thing, x, y). "Thing" can be None.""" if level == None: raise ValueError('Coordinates cannot be parsed because there is no map.') + if len(args) == 0: + return None, -1, -1 x = -1 y = -1 diff --git a/shell.py b/shell.py index aca4dd0..a826468 100644 --- a/shell.py +++ b/shell.py @@ -133,7 +133,10 @@ class Shell(object): self.__exit = False def man(self, args): - help(self.__commands[args[0]]) + if (len(args)): + help(self.__commands[args[0]]) + else: + print("Usage: help \nCommands:\n\t{}".format('\n\t'.join(list(self.__commands.keys())))) def registerCommand(self, commandName: str, command: _types.FunctionType): """command must be a function that takes one argument: a list of strings, @@ -288,10 +291,10 @@ $>=0 - $>=n is the nth and later arguments""" else: self.handleUnknownCommand(command) - def handleUnknownCommand(self, command): + def handleUnknownCommand(self, command: list): """Handle commands that aren't registered. Override this if you want to do something with those commands.""" - print("Bad command.") + print(f"Unknown command: {' '.join(command)}") def update(self): """Runs at the end of each loop. Does nothing by default. Override this if diff --git a/testing/test1.yml b/testing/test1.yml index 6c8fdaa..9214145 100644 --- a/testing/test1.yml +++ b/testing/test1.yml @@ -51,6 +51,16 @@ loadAlways: none: none # might this work to prevent this character from doing anything? customValues: dialogs: testing/testDialog.yml - - !Singleton +loadOnce: + - !NPC name: follower + description: a follower location: [6, 26] + behaviors: + go: [-1, follow] + arrive: [-1, follow] + customValues: + follow: + distance: 2 + isFollowing: True + target: You diff --git a/testing/testdata.yml b/testing/testdata.yml index b15ade8..ca5d8c0 100644 --- a/testing/testdata.yml +++ b/testing/testdata.yml @@ -3,17 +3,17 @@ title: Sample Text openingText: "{}\nsample text" startLevel: testing/test1.yml -singletons: - - !NPC - name: follower - description: a follower - location: [0, 0] - behaviors: - go: [-1, follow] - arrive: [-1, follow] - customValues: - follow: - distance: 2 - isFollowing: True - target: You +#singletons: +# - !NPC +# name: follower +# description: a follower +# location: [0, 0] +# behaviors: +# go: [-1, follow] +# arrive: [-1, follow] +# customValues: +# follow: +# distance: 2 +# isFollowing: True +# target: You