diff --git a/gamebase.py b/gamebase.py index c992716..66debad 100644 --- a/gamebase.py +++ b/gamebase.py @@ -7,6 +7,10 @@ import gameevents as _ge import random as _ra import sys as _sys import pickle as _pi +import ruamel.yaml as _yaml + +class GameError(RuntimeError): + pass class GameBase(object): @@ -18,6 +22,7 @@ class GameBase(object): def __init__(self): self.outstream = _sys.stdout self.__useFuncs = {} + self.__behaviors = {} self.__gameEvents = {} self.__IOCalls = {} # {str : function} self.customVals = {} # for setting flags and such @@ -49,9 +54,11 @@ class GameBase(object): self.registerEvent('useon', self.handleUseOn) self.registerEvent('take', self.handleTake) self.registerEvent('drop', self.handleDrop) + self.registerEvent('behave', self.handleBehave) self.registerUseFunc('examine', self.examine) self.registerUseFunc('key', self.key) self.registerUseFunc('container', self.container) + self.registerBehavior('wander', self.wander) # Helper functions @@ -178,7 +185,7 @@ Destination can be a coordinate pair or object name. For instance, if one wanted to go to D6, one could say "go to D6", "go to d 6", or "go to 3 6". The letter is not case-sensitive.""" if self.level == None: - raise RuntimeError("Cannot move: No level has been loaded.") + raise GameError("Cannot move: No level has been loaded.") speed = 0.6666667 if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': speed = 0.3333333 @@ -236,13 +243,46 @@ Object can be the name of the object, or its coordinates.""" 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)) + print("{} cannot see that.".format(self.playerName), file = self.outstream) elif thing == None: print("There is nothing to see here.\n", file = self.outstream) else: print(self.justifyText(str(thing)), file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) + def talk(self, args): + """talk [to [the]] character +Talk to a character. +"to" and "the" do nothing, and are intended only to make certain commands +make more sense from a linguistic perspective (for instance, one could +say "talk to the guard" rather than just "talk guard"). +Character can be the name of the character, or their coordinates.""" + if len(args) == 0: + print(self.justifyText(self.level.description), file = self.outstream) + else: + if args[0] == 'to': + args.pop(0) + 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 talk to {}.".format(self.playerName, thing.name), file = self.outstream) + elif thing == None: + print("There is nobody here.\n", file = self.outstream) + else: + if 'dialogs' in thing.customValues: + if isinstance(thing.customValues['dialogs'], list): + if 'dialogPtr' in thing.customValues and isinstance(thiong.customValues['dialogPtr'], int): + self.dialog(thing.customValues['dialogs'][thing.customValues['dialogPtr']], thing) + else: + self.dialog(thing.customValues['dialogs'][0], thing) + elif isinstance(thing.customValues['dialogs'], str): + self.dialog(thing.customValues['dialogs'], thing) + else: + raise GameError("Character '{}' has dialog of an unexpected type.".format(thing.name)) + else: + print("{} has nothing to say to you.".format(thing.name), file = self.outstream) + def use(self, args): """use [-r] [the] object [on [the] object2] Use an object. If the player is not already close to it, they will go to it. @@ -491,6 +531,13 @@ Object can be the name of the object, or its coordinates.""" #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) return + def dialog(self, dialogName, conversant): + yaml = _yaml.YAML() + dialog = None + with open(dialogName, 'r') as f: + dialog = yaml.load(f) + self.getIO('dialog')(dialog) + def gameEventLoop(self): #print(self.skipLoop) if self.skipLoop: @@ -576,6 +623,9 @@ Object can be the name of the object, or its coordinates.""" self.level.addThing(e.item, True) del self.playerInv[e.item.name] return True + + def handleBehave(self, e): + self.__behaviors[e.actor.behavior](e.actor) # default useFuncs: take a list of arguments, return the time the use took @@ -629,12 +679,21 @@ Object can be the name of the object, or its coordinates.""" else: print("The key doesn't fit that lock.", file = self.outstream) return 0.125 + + # behaviors + + def wander(self, actor): + pass # stuff for extended classes to use def registerUseFunc(self, name, func): """Registers a function for use by things in the map, but not directly callable by the player.""" self.__useFuncs[name] = func + + def registerBehavior(self, name, func): + """Registers a function for use as an NPC's behavior.""" + self.__behaviors[name] = func def registerEvent(self, name, func): """Registers a function to handle an event in the event loop. @@ -648,4 +707,6 @@ always give the player a turn, False otherwise.""" def getIO(self, name): """This is so derived classes can access their IOCalls.""" + if name not in self.__IOCalls: + raise GameError("No IO call for {}.".format(name)) return self.__IOCalls[name] diff --git a/gamemap.py b/gamemap.py index 6529fdc..6b36195 100644 --- a/gamemap.py +++ b/gamemap.py @@ -190,6 +190,7 @@ class NPC(Thing): self.inventory = inventory self.customValues = customValues self.graphic = graphic + self.behaveEvent = None def give(self, item): self.inventory.append(item) @@ -545,6 +546,7 @@ Entering a map through stdin will be obsolete once testing is over.""" yaml = ruamel.yaml.YAML() yaml.register_class(Item) yaml.register_class(Useable) + yaml.register_class(NPC) yaml.register_class(Door) yaml.register_class(MapExit) if infile != None: @@ -553,14 +555,9 @@ Entering a map through stdin will be obsolete once testing is over.""" info = yaml.load(f) except OSError as e: print("The file could not be read.") - return None + return None, nextThing else: - - while tryToRead: - try: - data += input() - except EOFError as e: - tryToRead = False + raise RuntimeError("No file was specified for loading.") # Now what we do with the data mat = None diff --git a/gameshell.py b/gameshell.py index 3d286d8..38473ab 100644 --- a/gameshell.py +++ b/gameshell.py @@ -7,9 +7,12 @@ import sys as _sys import heapq #import gamemap import gameevents -#from os import get_terminal_size +import textwrap as _tw +from shutil import get_terminal_size as _gts #import random +TERM_SIZE = _gts()[0] + class GameShell(Shell): UP = 1 @@ -24,6 +27,7 @@ class GameShell(Shell): self.outstream = _sys.stdout self.gameBase = gameBase self.colorMode = 0 + self.ps2 = '?> ' # register functions @@ -33,6 +37,7 @@ class GameShell(Shell): self.registerCommand('move', self.gameBase.go) self.registerCommand('walk', self.gameBase.go) self.registerCommand('look', self.gameBase.look) + self.registerCommand('talk', self.gameBase.talk) self.registerCommand('use', self.gameBase.use) self.registerCommand('loadMap', self.gameBase.loadMap) self.registerCommand('man', self.man) @@ -50,6 +55,7 @@ class GameShell(Shell): self.registerCommand('colorTest', self.colorTest) self.registerAlias('run', ['go', '-r']) self.gameBase.registerIO('container', self.container) + self.gameBase.registerIO('dialog', self.dialog) # Helper functions @@ -111,6 +117,7 @@ If -l is given, a map legend will be printed under the map.""" rows = [] index = 0 exits = {} + characters = {} doors = {} useables = {} items = {} @@ -135,6 +142,9 @@ If -l is given, a map legend will be printed under the map.""" if 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 + characters[len(characters)+1] = (thing.name, thing.graphic[1]) + rows[-1].append('C{0}'.format(len(characters))) elif thing.thingType == 'd': # door doors[len(doors)+1] = (thing.name, thing.graphic[1]) rows[-1].append('D{0}'.format(len(doors))) @@ -173,6 +183,9 @@ If -l is given, a map legend will be printed under the map.""" "Xn - Exit to another area"] for i in exits: legend.append(' {0}X{1}{2} - {3}'.format(self.color(exits[i][1][1:]), i, self.clearColor(), exits[i][0])) + legend.append("Cn - Character") + for i in characters: + legend.append(' {0}U{1}{2} - {3}'.format(self.color(characters[i][1][1:]), i, self.clearColor(), characters[i][0])) legend.append("Un - Useable object") for i in useables: legend.append(' {0}U{1}{2} - {3}'.format(self.color(useables[i][1][1:]), i, self.clearColor(), useables[i][0])) @@ -262,6 +275,33 @@ If -l is given, a map legend will be printed under the map.""" instr = input("Take, store, or exit: ") return inv, cont, timeSpent + def dialog(self, dialogObj): + if 'opener' in dialogObj: + print(_tw.fill(dialogObj['opener'], width = TERM_SIZE)) + while isinstance(dialogObj, dict): + if 'action' in dialogObj: + action = dialogObj['action'] + if action == 'answer': + answer = 0 + if 'answers' in dialogObj and isinstance(dialogObj['answers'], list): + for i in range(len(dialogObj['answers'])): + print(_tw.fill('{}: {}'.format(i+1, dialogObj['answers'][i]), width = TERM_SIZE)) + answer = int(input(self.ps2)) - 1 + if 'replies' in dialogObj and isinstance(dialogObj['replies'], list): + ret = self.dialog(dialogObj['replies'][answer]) + if ret > 1: + return ret - 1 + elif ret == 0: + return 0 + # if ret == 1, then do this dialog again + + elif len(action) >= 4 and action[:4] == 'back': + if len(action) == 4: + return 1 + return int(action[4:]) + elif action == 'exit': + return 0 + def update(self): self.gameBase.gameEventLoop() diff --git a/testing/test1.yml b/testing/test1.yml index 0b57979..8aef392 100644 --- a/testing/test1.yml +++ b/testing/test1.yml @@ -42,4 +42,11 @@ loadAlways: location: [21, 23] destination: testing/test4.yml name: downstairs + - !NPC + name: guy + description: a guy + location: [4, 20] + behavior: wander + customValues: + dialogs: testing/testDialog.yml