diff --git a/gamebase.py b/gamebase.py index 04d58a6..5d249d8 100644 --- a/gamebase.py +++ b/gamebase.py @@ -9,18 +9,22 @@ import sys as _sys import pickle as _pi import ruamel.yaml as _yaml import textwrap as _tw +import gamethings as _gt +import gamelocus as _gl +import gamesequence as _gs +import gameutil as _gu class GameError(RuntimeError): pass -class _ScriptBreak(object): - - def __init__(self, value): - self.value = value +#class _ScriptBreak(object): +# +# def __init__(self, value): +# self.value = value class GameBase(object): - coordRegex = _re.compile(r'(?:(-?[a-zA-Z]+) ?(-?[0-9]+))|(?:(-?[0-9]+),? (-?[0-9]+))|(?:\(([0-9]+), ([0-9]+)\))') + #coordRegex = _re.compile(r'(?:(-?[a-zA-Z]+) ?(-?[0-9]+))|(?:(-?[0-9]+),? (-?[0-9]+))|(?:\(([0-9]+), ([0-9]+)\))') #coordRegex1 = _re.compile(r'(-?[a-zA-Z]+) ?(-?[0-9]+)') # "B12" or "B 12" #coordRegex2 = _re.compile(r'\(([0-9]+), ([0-9]+)\)') # "(2, 12)" #coordRegex3 = _re.compile(r'(-?[0-9]+),? (-?[0-9]+)') # "2 12" or "2, 12" @@ -35,6 +39,7 @@ class GameBase(object): self.customValues = {} # for setting flags and such self.level = None self.persist = {} # {level : {thingName : thing}} + self.singletons = {} # {thingID : thing} self.ps2 = '? ' self.eventQueue = [] self.gameTime = 0.0 @@ -45,10 +50,10 @@ class GameBase(object): self.playerName = 'You' self.playerDescription = 'The main character.' self.player = None # reference to the player's 'thing' + self.autoMove = True # function deligates self.onLevelLoad = None # str : level name? -> None - #self.onContainer = None # list : contents -> list : newContents, float : timeOpen # default events and useFuncs self.registerEvent('noop', self.handleNoOp) @@ -66,17 +71,19 @@ class GameBase(object): self.registerUseFunc('wear', self.wear) self.registerBehavior('stand', self.stand) self.registerBehavior('wander', self.wander) - self.registerScript('if', self.ifScript) + self.registerBehavior('follow', self.follow) + #self.registerScript('if', self.ifScript) self.registerScript('printf', self.printfScript) - self.registerScript('get', self.getCustomValue) - self.registerScript('set', self.setCustomValue) - self.registerScript('del', self.delCustomValue) + #self.registerScript('get', self.getCustomValue) + #self.registerScript('set', self.setCustomValue) + #self.registerScript('del', self.delCustomValue) self.registerScript('spawn', self.spawnThing) self.registerScript('give', self.giveToPlayer) self.registerScript('move', self.moveThingScript) self.registerScript('cvget', self.cvget) self.registerScript('cvmod', self.cvmod) self.registerScript('cvdel', self.cvdel) + self.registerScript('playercmd', self.playerCmdScript) # Helper functions @@ -85,90 +92,6 @@ class GameBase(object): to get input from stdin.""" return input(prompt) - def letterToNumber(self, letter): - if letter.isdecimal(): - return int(letter) - ret = 0 - sign = 1 - start = 0 - for i in range(len(letter)): - if letter[i] == '-': - sign = -sign - start -= 1 - elif letter[i].isalpha(): - if letter[i].isupper(): - ret += (ord(letter[i]) - ord('A')) * (26**(i+start)) - else: - ret += (ord(letter[i]) - ord('a')) * (26**(i+start)) - else: - return ret * sign - return ret * sign - - def numberToLetter(self, number): - if isinstance(number, str): - return number - ret = '' - sign = '' - if number == 0: - return 'A' - elif number < 0: - sign = '-' - number = -number - while number > 0: - ret += chr(ord('A') + number % 26) - number = int(number / 26) - return sign + ret - - def parseCoords(self, args, usePlayerCoords = True, allowInventory = True): - """Takes an argument list, and figures out what it's talking about. -Returns (thing, x, y). "Thing" can be None.""" - if self.level == None: - raise GameError('Coordinates cannot be parsed because there is no map.') - #print(coordStr) - x = -1 - y = -1 - - # first, try by name for objects in the map - name = ' '.join(args) - thing = self.level.getThingByName(name) - if thing != None: - if usePlayerCoords: - return thing, thing.playerx, thing.playery - else: - return thing, thing.x, thing.y - - # Second, try by name for objects in the inventory - if allowInventory: - if name in self.player.thingNames: - return self.player.getThingByName(name), -1, -1 - - # Third, try by location - coordStr = args[0] - if len(args) > 1: - coordStr += ' ' + args[1] - match = GameBase.coordRegex.match(coordStr) - if match != None: # if coordinates were given - #print(match.group()) - groups = match.groups() - ind = 0 - for i in range(len(groups)): - if groups[i] == None or groups[i] == match.group(): - continue - else: - ind = i - break - #print(groups[ind], groups[ind+1]) - x = self.letterToNumber(groups[ind]) - y = self.letterToNumber(groups[ind+1]) - #print(x, y) - thing = self.level.getThingAtCoords(x, y) - if thing != None and usePlayerCoords: - return thing, thing.playerx, thing.playery - else: - return thing, x, y - else: # if a name was given - return None, -1, -1 - def justifyText(self, text, width = 80): ret = [] text = text.lstrip().rstrip() @@ -189,97 +112,33 @@ Returns (thing, x, y). "Thing" can be None.""" return '\n'.join(ret) - def getValueFromString(self, arg: str, env = None): - if env == None: - env = self.customValues - val = None - 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 - val = arg[1:-1] - elif _re.match(r'[+-]?(?:[0-9]*[.])?[0-9]+', arg) != None: - if '.' in arg: - val = float(arg) - else: - val = int(arg) - elif arg.casefold() == 'true': - val = True - elif arg.casefold() == '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: - group = validIdent.group() - if 'scriptLocal' in self.customValues and group in self.customValues['scriptLocal']: - val = self.customValues['scriptLocal'][group] - elif group in env: - val = env[group] - else: - return False # for if statements; if a variable doesn't exist, should evaluate to False - # evaluate all values of all indecies - openBracket = validIdent.end() - if openBracket < len(arg): - if arg[openBracket] == '[': - ptr = openBracket - depth = 0 - while ptr < len(arg): - if depth == 0 and arg[ptr] != '[': - raise GameError('Invalid value syntax: {}'.format(arg)) - if arg[ptr] == '[': - depth += 1 - elif arg[ptr] == ']': - depth -= 1 - - if depth == 0: - index = self.getValueFromString(arg[openBracket+1:ptr]) - if index in val: - val = val[index] - else: - return False - openBracket = ptr + 1 - ptr += 1 - else: - raise GameError('Invalid value syntax: {}'.format(arg)) + def smoothMove(self, actor, loc, speed, action = None, immediate = False): + """Move a thing smoothly at a constant speed.""" + 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) + #print(path) + if dist == -1: + print('{0} cannot reach there.'.format(actor.name), file = self.outstream) + return + elif dist == 1: + self.setEvent(speed, _ge.ArriveEvent(actor, path[0], speed, action, loc, timeTaken = speed)) + return speed + elif immediate: + self.setEvent(0, _ge.GoEvent(actor, path, speed, action, loc)) + return speed * (dist - 1) else: - raise GameError('Invalid argument to getValueFromString: {}'.format(arg)) - return val - - def compareValues(self, args: list): - """Generalized comparisons, may eventually be extended to other operators""" - if len(args) == 1: - return bool(self.getValueFromString(args[0])) - elif len(args) == 3 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'): - lval = self.getValueFromString(args[0]) - operator = args[1] - rval = self.getValueFromString(args[2]) - if operator == '==': - return lval == rval - elif operator == '!=': - return lval != rval - elif operator == '<=': - return lval <= rval - elif operator == '>=': - return lval >= rval - elif operator == '<': - return lval < rval - 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 GameError("Condition cannot be evaluated: {}".format(' '.join(args))) + self.setEvent(speed, _ge.GoEvent(actor, path, speed, action, loc, timeTaken = speed)) + return speed * dist # commands + + def wait(self, args): + """wait seconds""" + if len(args) < 1: + return + self.setEvent(float(args[0]), _ge.NoOpEvent()) def go(self, args): """go [-r] [to [the]] destination [additional arguments...] @@ -304,38 +163,10 @@ The letter is not case-sensitive.""" args.pop(0) if args[0] == 'the': args.pop(0) - thing, x, y = self.parseCoords(args, allowInventory = False) + thing, x, y = _gu.parseCoords(self.level, args) # Now we have a heading! Let's see if we can get there... - 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.player, self.player.x, self.player.y, 0.0)) - return - dist, path = self.level.path(x, y, self.player.x, self.player.y) - if dist == -1: - 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.player.x, self.player.y) - space = path[pos] - if space == -1: - 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 - #while space != target: - while space != -1: - newx, newy = self.level.intToCoords(space) - space = path[space] - if space != -1: - self.setEvent(t * speed, _ge.GoEvent(self.player, newx, newy)) - else: - 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.player, newx, newy, t * speed))) + self.smoothMove(self.player, _gl.PointLocus(x, y), speed) return def look(self, args): @@ -356,14 +187,18 @@ Object can be the name of the object, or its coordinates.""" args.pop(0) if args[0] == 'the': args.pop(0) - thing, x, y = self.parseCoords(args, usePlayerCoords = False) + 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): - print("{} cannot see that.".format(self.player.name), file = self.outstream) + if self.autoMove: + self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: print(self.justifyText(str(thing)), file = self.outstream)) + 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) - #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) def talk(self, args): """talk [to [the]] character @@ -383,24 +218,18 @@ Character can be the name of the character, or their coordinates.""" args.pop(0) if args[0] == 'the': args.pop(0) - thing, x, y = self.parseCoords(args, usePlayerCoords = False, allowInventory = False) + thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False) 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) + if self.autoMove: + self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: _gu.startDialog(thing, self.outstream, self.dialog)) + 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: - 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) + _gu.startDialog(thing, self.outstream, self.dialog) def use(self, args): """use [-r] [the] object [on [the] object2] @@ -429,7 +258,7 @@ the name of an item in the player's inventory.""" return if 'with' in args: useArgs = args[args.index('with')+1:] - thing, x, y = self.parseCoords(args) + thing, x, y = _gu.parseCoords(self.level, args, player = self.player) if thing == None: print("There is nothing to use.", file = self.outstream) #_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent())) @@ -440,37 +269,16 @@ the name of an item in the player's inventory.""" return # Similar to go, but not quite the same. - 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.player.x, self.player.y) - if dist == -1: - 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.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.UseEvent(thing, useArgs)) + self.smoothMove(self.player, ((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') - item, x, y = self.parseCoords(args[:onIndex], usePlayerCoords = False, allowInventory = True) + item, x, y = _gu.parseCoords(self.level, args[:onIndex], usePlayerCoords = False, player = self.player) if args[onIndex+1] == 'the': onIndex += 1 - thing, x, y = self.parseCoords(args[onIndex+1:], usePlayerCoords = True, allowInventory = True) + thing, x, y = _gu.parseCoords(self.level, args[onIndex+1:], usePlayerCoords = True, player = self.player) useArgs = [] if 'with' in args: useArgs = args[args.index('with')+1:] @@ -485,39 +293,14 @@ the name of an item in the player's inventory.""" 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: - 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)) + self.smoothMove(self.player, ((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): - 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 + 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))) + else: + print("{} cannot see that.".format(self.player.name), file = self.outstream) + return def take(self, args): """take [the] item @@ -537,7 +320,7 @@ Object can be the name of the object, or its coordinates.""" args.pop(0) if args[0] == 'the': args.pop(0) - thing, x, y = self.parseCoords(args, allowInventory = False) + thing, x, y = _gu.parseCoords(self.level, args) if thing == None: print("There is nothing to take.", file = self.outstream) return @@ -547,29 +330,7 @@ Object can be the name of the object, or its coordinates.""" return # Similar to go, but not quite the same. - 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.player.x, self.player.y) - if dist == -1: - 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.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.player.name, newx, newy))) - self.setEvent(t * speed + 0.125, _ge.TakeEvent(self.player, thing)) - return + self.smoothMove(self.player, ((x, y),), speed, lambda: self.setEvent(0.125, _ge.TakeEvent(self.player, thing))) def drop(self, args): """drop [the] item""" @@ -579,11 +340,6 @@ Object can be the name of the object, or its coordinates.""" raise GameError("Cannot drop: Player character doesn't exist.") if args[0] == 'the': args.pop(0) - #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))) @@ -623,16 +379,17 @@ Object can be the name of the object, or its coordinates.""" print(self.level.openingText, file = self.outstream) #print(self.outstream.getvalue()) - x, y = self.level.playerStart + mx, my = self.level.playerStart if len(args) == 3: - x, y = int(args[1]), int(args[2]) + mx, my = 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.") + self.player = _gt.PlayerCharacter(x = mx, y = my, description = self.playerDescription, + inventory = {}, customValues = {}, name = self.playerName) + #print("Player created.") else: self.player.x, self.player.y = x, y - print("Player moved.") + #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: @@ -703,105 +460,119 @@ Object can be the name of the object, or its coordinates.""" dialog = None with open(dialogName, 'r') as f: dialog = yaml.load(f) - self.getIO('dialog')(dialog) + self.getIO('startDialog')() + self.runDialog(dialog) + self.getIO('endDialog')() + + def runDialog(self, dialogObj): + #print(dialogObj) + if 'script' in dialogObj: + self.parseScript(dialogObj['script']) + if 'cond' in dialogObj: + cases = dialogObj['cond'] + 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})): + ret = self.runDialog(case) + break + elif len(cond) == 3 and _gs.compareValues(cond, {'scriptLocal': {}, 'global': self.customValues}): + ret = self.runDialog(case) + break + else: + raise GameError("All routes are false: {}".format(testValue)) + if ret > 1: + return ret - 1 + else: + return ret + # if ret == 1 or ret == 0, go back again. + elif 'opener' in dialogObj: + self.getIO('openDialog')(dialogObj['opener'].split('\n')) # split by lines so that fill doesn't erase newlines + while isinstance(dialogObj, dict): + if 'action' in dialogObj: + action = dialogObj['action'] + if action == 'answer': + answer = 0 + skips = [] + j = 0 # follower to i + options = [] + if 'answers' in dialogObj and isinstance(dialogObj['answers'], list): + for i in range(len(dialogObj['answers'])): + ans = dialogObj['answers'][i] + if ans[0] == '?': + condEnd = ans.index(':') + cond = ans[1:condEnd].strip().split() + if _gs.compareValues(cond, {'scriptLocal': {}, 'global': 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: + answer += 1 + else: + break + if 'replies' in dialogObj and isinstance(dialogObj['replies'], list): + ret = self.runDialog(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 + else: + raise RuntimeError('Malformed action: {}'.format(action)) + else: + raise RuntimeError("Dialog branch with neither switch nor openner.") def parseScript(self, instr: str): - """parses then runs a script.""" - #print('parseScript called with {}.'.format(instr)) - if instr == '': - return # nothing to be done. - literalStr = list(instr) - inQuotes = False - script = [] - stack = [] - ret = [] - argStart = 0 - c = 0 - l = 0 - while c < len(instr): - if inQuotes: - if instr[c] == '"': - if instr[c-1] == '\\': - del literalStr[l-1] - l -= 1 - else: - inQuotes = False - del literalStr[l] - l -= 1 - else: - if instr[c] == '"': # quoted string - if l > 0 and instr[c-1] == '\\': - del literalStr[l-1] - l -= 1 - else: - inQuotes = True - del literalStr[l] - l -= 1 - elif instr[c] in ' \t\n;}{': - if l > 0 and instr[c-1] == '\\': - del literalStr[l-1] - l -= 1 - else: - if argStart != l: - ret.append(''.join(literalStr[argStart:l])) - if instr[c] == ';': - if len(ret) > 0: - script.append(ret) - ret = [] - elif instr[c] == '{': # block - stack.append(script) - script.append(ret) - script = [] - ret.append(script) - ret = [] - del literalStr[l] - l -= 1 - elif instr[c] == '}': # close block - if len(ret) > 0: - script.append(ret) - ret = [] - script = stack.pop() - del literalStr[l] - l -= 1 - argStart = l + 1 - c += 1 - l += 1 - if argStart != l: - ret.append(''.join(literalStr[argStart:l])) - if len(ret) > 0: - script.append(ret) - #print('After parsing: {}'.format(script)) - self.customValues['scriptLocal'] = {} - ret = self.runScript(script) - if 'scriptLocal' in self.customValues: - del self.customValues['scriptLocal'] - if isinstance(ret, _ScriptBreak): - ret = ret.value - return ret - - def runScript(self, script: list): - """run a script""" - ret = False - for line in script: - if len(line) == 0: - # empty line - continue - elif line[0].casefold() == 'playercmd': - # run a player command - self.getIO('playercmd')(line[1:]) - elif line[0].casefold() == 'break': - # exit early - return _ScriptBreak(ret) - elif line[0] in self.__scripts: - # run a script - ret = self.__scripts[line[0]](line[1:]) - if isinstance(ret, _ScriptBreak): - return ret - else: - # conditional evaluation - ret = self.compareValues(line) - # We specifically want the return value of the last line executed. - return ret + """A thin wrapper for _gs.parseScript.""" + return _gs.parseScript(instr, self.customValues, self.__scripts) + + def loadGameData(self, dataFile): + yaml = _yaml.YAML() + yaml.register_class(_gt.Item) + yaml.register_class(_gt.Useable) + yaml.register_class(_gt.NPC) + yaml.register_class(_gt.Door) + yaml.register_class(_gt.MapExit) + yaml.register_class(_gt.MapEntrance) + data = None + with open(dataFile, 'r') as f: + data = yaml.load(f) + if 'singletons' in data: + for datum in data['singletons']: + thing = data['singletons'][datum] + if not isinstance(thing, _gt.Thing): + continue + thing.thingID = self.nextThing + self.nextThing += 1 + if thing.thingType in 'iun': + nextThing = self.addThingRecursive(thing.customValues, nextThing) + 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 + self.singletons = dict(data[singletons]) + return data def gameEventLoop(self): #print(self.skipLoop) @@ -812,8 +583,13 @@ Object can be the name of the object, or its coordinates.""" ev = _hq.heappop(self.eventQueue) self.gameTime = ev[0] e = ev[1] - if self.__gameEvents[e.eventType](e): - #print('break loop') + 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 ret: break if len(self.eventQueue) == 0: self.gameTime = 0.0 @@ -823,6 +599,18 @@ Object can be the name of the object, or its coordinates.""" def setEvent(self, t, e, skip = False): _hq.heappush(self.eventQueue, (self.gameTime + t, e)) self.skipLoop = skip + + def clearEvents(self, actor = None): + if isinstance(actor, _gt.Thing): + i = 0 + while i < len(self.eventQueue): + if actor is self.eventQueue[i].actor: + del self.eventQueue[i] + else: + i += 1 + _hq.heapify(self.eventQueue) + else: + self.eventQueue.clear() # default event handlers @@ -830,59 +618,51 @@ 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: if e.actor == None: raise GameError("'Go' event raised for no object.") - self.level.moveThing(e.actor, e.x, e.y) + if self.level.isPassable(e.path[0]): + #print(e.actor.name, e.actor.x, e.actor.y) + self.level.moveThing(e.actor, e.path[0]) + #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)) + 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) + else: + print('{0} cannot !reach there.'.format(e.actor.name), file = self.outstream) 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': - # 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)) - else: - print('{0} went {1}.'.format(actor.name, str(thing))) - self.level.removeThing(actor.name) - return True + #print(e.actor, "arriving at", e.pos, self.level.isPassable(e.pos)) + if self.level.isPassable(e.pos) or (e.actor.x, e.actor.y) == self.level.intToCoords(e.pos): + thing = self.level.getThingAtPos(e.pos) + 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) + 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)) + else: + print('{0} went {1}.'.format(actor.name, str(thing))) + self.level.removeThing(actor.name) + else: + e.action() + #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) + return e.actor == self.player def handleUse(self, e): if e.thing.useFunc == '': @@ -915,7 +695,16 @@ Object can be the name of the object, or its coordinates.""" return True def handleBehave(self, e): - self.__behaviors[e.actor.behavior](e.actor) + """Allow an NPC to make another move.""" + if not isinstance(e.actor, _gt.Observer): + raise GameError("Non-observer is trying to behave.") + if len(e.actor.behaviorQueue) > 0: + nextBehavior = _hq.heappop(e.actor.behaviorQueue) + timeTaken = self.__behaviors[e.actor.behaviors[nextBehavior[1].eventType][1]](e.actor, nextBehavior[1]) + self.setEvent(timeTaken, _ge.BehaveEvent(e.actor)) + else: + e.actor.busy = False + return False # default useFuncs: take a list of arguments, return the time the use took @@ -1001,131 +790,12 @@ Object can be the name of the object, or its coordinates.""" return 0.125 # default scripts - - def ifScript(self, args): - """If statement: if [not] value [op value] script""" - if len(args) < 2: - raise GameError('Incomplete If statement: if {}'.format(' '.join(args))) - inverse = False - ret = False - if args[0] == 'not': - inverse = True - args.pop(0) - if len(args) < 2: - raise GameError('Incomplete If statement: if {}'.format(' '.join(args))) - - # evaluate condition - if len(args) > 1 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'): - if len(args) < 4: - raise GameError('Incomplete If statement: if {}'.format(' '.join(args))) - ret = self.compareValues(args[:3]) - args = args[3:] - else: - ret = bool(self.getValueFromString(args[0])) - args = args[1:] - if inverse: - ret = not ret - - # if condition is true, evaluate further - if ret: - if isinstance(args[-1], list): - return self.runScript(args[-1]) - else: - return self.runScript([args]) def printfScript(self, args): """Assume the first argument is a stringable object, and format all others.""" ret = args[0].format(*[self.getValueFromString(i) for i in args[1:]]) print(ret, file = self.outstream) return ret - - def getCustomValue(self, args, env = None): - norm = True - if env == None: - env = self.customValues - else: - norm = False - if len(args) > 0 and args[0] == 'local': - if 'scriptLocal' in self.customValues: - env = self.customValues['scriptLocal'] - else: - raise GameError('Attempted to set a local variable without local scope.') - args.pop(0) - return self.getValueFromString(args[0], env) - - def setCustomValue(self, args, env = None): - """takes [customValue, op, value]""" - if env == None: - env = self.customValues - if len(args) > 0 and args[0] == 'local': - if 'scriptLocal' in self.customValues: - env = self.customValues['scriptLocal'] - else: - raise GameError('Attempted to set a local variable without local scope.') - args.pop(0) - if len(args) < 3 or args[1] not in ('=', '+=', '-=', '*=', '/=', '%=', '//=', '**=', 'b=', '!=', '|=', '&=', '^='): - raise GameError('Arguments are not fit for the setCustomValue script.') - # This next line allows cvmod to use values in the thing's customValues dict, - # but doing this means that accessing a game CV requires setting it to a local var. - val = self.getValueFromString(args[2], env) - - # set the value to a default value (0, 0.0, '', etc.) if not yet assigned - if args[0] not in env and args[1] != '=': - if isinstance(val, int): - env[args[0]] = 0 - elif isinstance(val, float): - env[args[0]] = 0.0 - elif isinstance(val, str): - env[args[0]] = '' - elif isinstance(val, bool): - env[args[0]] = False - # Beyond this point are types that can only be found from other customValues - elif isinstance(val, tuple): - env[args[0]] = () - elif isinstance(val, list): - env[args[0]] = [] - elif isinstance(val, dict): - env[args[0]] = {} - else: - raise GameError("Operation not supported for unassigned custom values of type {}: {}" - .format(type(val), args[1])) - - # done parsing, evaluate - if args[1] == '=': - env[args[0]] = val - elif args[1] == '+=': - env[args[0]] += val - elif args[1] == '-=': - env[args[0]] -= val - elif args[1] == '*=': - env[args[0]] *= val - elif args[1] == '/=': - env[args[0]] /= val - elif args[1] == '%=': - env[args[0]] %= val - elif args[1] == '//=': - env[args[0]] //= val - elif args[1] == '**=': - env[args[0]] **= val - elif args[1] == 'b=': - env[args[0]] = bool(val) - elif args[1] == '!=': - env[args[0]] = not bool(val) - elif args[1] == '|=': - env[args[0]] |= val - elif args[1] == '&=': - env[args[0]] &= val - elif args[1] == '^=': - env[args[0]] ^= val - return env[args[0]] - - def delCustomValue(self, args, env = None): - """To clean up after a map.""" - if env == None: - env = self.customValues - for i in args: - if i in env: - del(env[i]) def spawnThing(self, args): """Spawns a thing. [type name=... location=... etc]""" @@ -1141,7 +811,7 @@ Object can be the name of the object, or its coordinates.""" if i[0:5].casefold() == 'name=': name = i[5:] elif i[0:9].casefold() == 'location=': - _, x, y = self.parseCoords(i[9:].split(), usePlayerCoords = False, allowInventory = False) + _, x, y = _gu.parseCoords(self.level, i[9:].split(), usePlayerCoords = False) elif i[0:4].casefold() == 'fgc=': if len(i[4:]) != 7 or i[4] != '#': raise GameError("Invalid color: {}".format(i[4:])) @@ -1177,11 +847,11 @@ Object can be the name of the object, or its coordinates.""" if name == None: name = 'item {}'.format(self.nextThing) if bgc == None: - bgc = _gm.Item.defaultGraphic[0] + bgc = _gt.Item.defaultGraphic[0] if fgc == None: - fgc = _gm.Item.defaultGraphic[1] + fgc = _gt.Item.defaultGraphic[1] if shape == None: - shape = _gm.Item.defaultGraphic[2] + shape = _gt.Item.defaultGraphic[2] for i in args[1:]: if i[0:12].casefold() == 'description=': description = i[12:] @@ -1195,7 +865,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.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape)) + thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape)) elif args[0].casefold() == 'useable': # spawn a useable thing description = 'A nondescript useable thing.' @@ -1205,23 +875,23 @@ Object can be the name of the object, or its coordinates.""" if name == None: name = 'useable {}'.format(self.nextThing) if bgc == None: - bgc = _gm.Useable.defaultGraphic[0] + bgc = _gt.Useable.defaultGraphic[0] if fgc == None: - fgc = _gm.Useable.defaultGraphic[1] + fgc = _gt.Useable.defaultGraphic[1] if shape == None: - shape = _gm.Useable.defaultGraphic[2] + shape = _gt.Useable.defaultGraphic[2] for i in args[1:]: if i[0:12].casefold() == 'description=': description = i[12:] elif i[0:8].casefold() == 'usefunc=': useFunc = i[8:] elif i[0:10].casefold() == 'playerpos=': - _, playerx, playery = self.parseCoords(i[10:].split()) + _, playerx, playery = _gu.parseCoords(self.level, i[10:].split()) elif i[0:3].casefold() == 'cv:': equalsign = i.index('=') cv = i[3:equalsign] customValues[cv] = self.getValueFromString(i[equalsign+1:]) - thing = _gm.Useable(name, x, y, description, useFunc, customValues, playerx, playery, (bgc, fgc, shape)) + thing = _gt.Useable(name, x, y, description, useFunc, customValues, playerx, playery, (bgc, fgc, shape)) elif args[0].casefold() == 'npc': # spawn an NPC description = 'A nondescript character.' @@ -1232,23 +902,23 @@ Object can be the name of the object, or its coordinates.""" if name == None: name = 'character {}'.format(self.nextThing) if bgc == None: - bgc = _gm.NPC.defaultGraphic[0] + bgc = _gt.NPC.defaultGraphic[0] if fgc == None: - fgc = _gm.NPC.defaultGraphic[1] + fgc = _gt.NPC.defaultGraphic[1] if shape == None: - shape = _gm.NPC.defaultGraphic[2] + shape = _gt.NPC.defaultGraphic[2] for i in args[1:]: if i[0:12].casefold() == 'description=': description = i[12:] elif i[0:9].casefold() == 'behavior=': useFunc = i[9:] elif i[0:10].casefold() == 'playerpos=': - _, playerx, playery = self.parseCoords(i[10:].split()) + _, playerx, playery = _gu.parseCoords(self.level, i[10:].split()) elif i[0:3].casefold() == 'cv:': 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, (bgc, fgc, shape)) + thing = _gt.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.' @@ -1257,17 +927,17 @@ Object can be the name of the object, or its coordinates.""" if name == None: name = 'door {}'.format(self.nextThing) if bgc == None: - bgc = _gm.Door.defaultGraphic[0] + bgc = _gt.Door.defaultGraphic[0] if fgc == None: - fgc = _gm.Door.defaultGraphic[1] + fgc = _gt.Door.defaultGraphic[1] if shape == None: - shape = _gm.Door.defaultGraphic[2] + shape = _gt.Door.defaultGraphic[2] for i in args[1:]: if i[0:12].casefold() == 'description=': description = i[12:] elif i.casefold() == 'locked': locked = True - thing = _gm.Door(name, x, y, locked, description, key, (bgc, fgc, shape)) + thing = _gt.Door(name, x, y, locked, description, key, (bgc, fgc, shape)) elif args[0].casefold() == 'mapexit': # spawn an exit to another map (use with EXTREME caution!) destination = '' @@ -1278,11 +948,11 @@ Object can be the name of the object, or its coordinates.""" if name == None: name = 'somewhere' if bgc == None: - bgc = _gm.MapExit.defaultGraphic[0] + bgc = _gt.MapExit.defaultGraphic[0] if fgc == None: - fgc = _gm.MapExit.defaultGraphic[1] + fgc = _gt.MapExit.defaultGraphic[1] if shape == None: - shape = _gm.MapExit.defaultGraphic[2] + shape = _gt.MapExit.defaultGraphic[2] for i in args[1:]: if i[0:12].casefold() == 'destination=': destination = i[12:] @@ -1294,14 +964,14 @@ Object can be the name of the object, or its coordinates.""" onUse = i[6:] elif i[0:4].casefold() == 'key=': key = i[4:] - thing = _gm.MapExit(name, x, y, exitid, destination, prefix, onUse, key, (bgc, fgc, shape)) + thing = _gt.MapExit(name, x, y, exitid, destination, prefix, onUse, key, (bgc, fgc, shape)) elif args[0].casefold() == 'mapentrance': # spawn a map entrance exitid = 0 for i in args[1:]: if i[0:7].casefold() == 'exitid=': exitid = int(i[7:]) - thing = _gm.MapEntrance(x, y, exitid, name) + thing = _gt.MapEntrance(x, y, exitid, name) else: raise GameError("{} not a valid thing type.".format(args[0])) self.nextThing = self.level.addThing(thing, self.nextThing, persist) @@ -1311,7 +981,7 @@ Object can be the name of the object, or its coordinates.""" """We get to assume it's an item.""" name = 'item {}'.format(self.nextThing) x, y = self.playerx, self.playery - fgc, bgc, shape = _gm.Item.defaultGraphic['fgc'], _gm.Item.defaultGraphic['bgc'], _gm.Item.defaultGraphic['shape'] + fgc, bgc, shape = _gt.Item.defaultGraphic['fgc'], _gt.Item.defaultGraphic['bgc'], _gt.Item.defaultGraphic['shape'] description = 'A nondescript item.' persist = True useFunc = '' @@ -1322,7 +992,7 @@ Object can be the name of the object, or its coordinates.""" if i[0:5].casefold() == 'name=': name = i[5:] elif i[0:10].casefold() == 'location=': - _, x, y = self.parseCoords(i[10:].split(), usePlayerCoords = False, allowInventory = False) + _, x, y = _gu.parseCoords(self.level, i[10:].split(), usePlayerCoords = False) elif i[0:4].casefold() == 'fgc=': if len(i[4:]) != 7 or i[0] != '#': raise GameError("Invalid color: {}".format(i[4:])) @@ -1358,7 +1028,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 = _gm.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape)) + thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape)) thing.thingID = self.nextThing self.player.addThing(thing) self.nextThing += 1 @@ -1366,36 +1036,64 @@ Object can be the name of the object, or its coordinates.""" def moveThingScript(self, args): colon = args.index(':') - thing, x, y = self.parseCoords(args[0:colon], usePlayerCoords = False, allowInventory = False) + thing, x, y = _gu.parseCoords(self.level, args[0:colon], usePlayerCoords = False) if thing == None: return False _, newX, newY = self.parseCoords(args[colon+1:], usePlayerCoords = True, allowInventory = False) self.level.moveThing(thing, newX, newY) def cvget(self, args): - thing, x, y = self.parseCoords(args[0:-1], usePlayerCoords = False, allowInventory = True) + thing, x, y = _gu.parseCoords(self.level, args[0:-1], usePlayerCoords = False, player = self.player) if thing != None and thing.thingType in 'ciu': - return self.getCustomValue(args[-1:], env = thing.customValues) + return _gs.getCustomValue(args[-1:], {'scriptLocal': thing.customValues, 'global': {}}) else: raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.") def cvmod(self, args): """Modify a custom value in a thing. args = thing identifier operator value""" - thing, x, y = self.parseCoords(args[0:-3], usePlayerCoords = False, allowInventory = True) + thing, x, y = _gu.parseCoords(args[0:-3], usePlayerCoords = False, player = self.player) if thing != None and thing.thingType in 'ciu': - return self.setCustomValue(args[-3:], env = thing.customValues) + return _gs.setCustomValue(args[-3:], {'scriptLocal': thing.customValues, 'global': {}}) else: raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.") def cvdel(self, args): """Delete custom values.""" colon = args.index(':') - thing, x, y = self.parseCoords(args[0:colon], usePlayerCoords = False, allowInventory = True) + thing, x, y = _gu.parseCoords(args[0:colon], usePlayerCoords = False, player = self.player) if thing != None and thing.thingType in 'ciu': - return self.setCustomValue(args[colon+1:], env = thing.customValues) + return _gs.delCustomValue(args[colon+1:], {'scriptLocal': {}, 'global': thing.customValues}) else: raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.") + + def playerCmdScript(self, args): + """Run a player command from a script.""" + self.getIO('playercmd')(line[1:]) + return False + + # Observer + + def observe(self, e): + if self.level == None: + # Maybe not an error? Not sure yet. + return + for thingID in self.level.things: + thing = self.level.things[thingID] + if isinstance(thing, _gt.Observer): + if e.eventType in thing.behaviors: + if not thing.busy: + thing.busy = True + timeTaken = self.__behaviors[thing.behaviors[e.eventType][1]](thing, e) + if timeTaken >= 0: + self.setEvent(timeTaken, _ge.BehaveEvent(thing)) + # We assume that otherwise, the behavior itself decides when it's done. + else: + # negative priority means just forget it; + # otherwise, lower number means higher priority + if thing.behaviors[e.eventType][0] >= 0: + #print("'{}' event added to {}'s queue".format(e.eventType, thing.name)) + _hq.heappush(thing.behaviorQueue, (thing.behaviors[e.eventType][0], e)) # behaviors @@ -1404,6 +1102,21 @@ Object can be the name of the object, or its coordinates.""" def wander(self, actor): pass + + def follow(self, actor, event): + # make sure we only follow who we want to + if event.actor.name != actor.customValues["follow"]["target"]: + 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))) + return -1 # stuff for extended classes to use def registerUseFunc(self, name, func): @@ -1419,7 +1132,11 @@ callable by the player.""" """Registers a function to handle an event in the event loop. They should take the event as an argument, and return True if it should always give the player a turn, False otherwise.""" - self.__gameEvents[name] = func + if name in self.__gameEvents: + self.__gameEvents[name].add(func) + else: + self.__gameEvents[name] = set((func,)) + #self.__gameEvents[name] = func def registerIO(self, name, func): """Registers a function for useFuncs and such to use.""" diff --git a/gameevents.py b/gameevents.py index 8286a4f..5c366a4 100644 --- a/gameevents.py +++ b/gameevents.py @@ -63,30 +63,41 @@ class NoOpEvent(GameEvent): class GoEvent(GameEvent): - def __init__(self, actor, x, y): + def __init__(self, actor, path, speed, action = None, locus = None, timeTaken = 0): super(GoEvent, self).__init__('go') self.actor = actor - self.x = x - self.y = y + self.path = path + self.speed = speed + self.action = action + self.locus = locus + self.timeTaken = timeTaken + + @property + def pos(self): + return self.path[0] class ArriveEvent(GameEvent): - def __init__(self, actor, x, y, t): + def __init__(self, actor, pos, speed, action = None, locus = None, timeTaken = 0): super(ArriveEvent, self).__init__('arrive') self.actor = actor - self.x = x - self.y = y - self.t = t + self.pos = pos + self.speed = speed + self.action = action + self.locus = locus + self.timeTaken = timeTaken class UseEvent(GameEvent): - def __init__(self, thing, args): + def __init__(self, actor, thing, args): super(UseEvent, self).__init__('use') + self.actor = actor self.thing = thing self.args = args class UseOnEvent(GameEvent): - def __init__(self, item, thing, args): + def __init__(self, actor, item, thing, args): super(UseOnEvent, self).__init__('useon') + self.actor = actor self.thing = thing # thing can be a coordinate pair? self.item = item self.args = args diff --git a/gamelocus.py b/gamelocus.py new file mode 100644 index 0000000..5995799 --- /dev/null +++ b/gamelocus.py @@ -0,0 +1,336 @@ +# gamelocus.py + +import math as _mt + +class Locus(object): + """Abstract base class for locus objects.""" + + def __init__(self, **kwargs): + """Base locus constructor.""" + pass + + def __contains__(self, item): + """Is this point within the locus?""" + return False + + def __iter__(self): + return iter(()) + + def __repr__(self): + return "Locus()" + +class PointLocus(Locus): + """A locus that defines a single tile.""" + + def __init__(self, x, y, **kwargs): + """Take the coordinates for a point.""" + self.x = x + self.y = y + + def __contains__(self, item): + if not isinstance(item, tuple) and not isinstance(item, list): + raise ValueError("Item must be a tuple or a list.") + return item[0] == self.x and item[1] == self.y + + def __iter__(self): + return iter(((self.x, self.y),)) # just an iterator of the one tile + + def __repr__(self): + return "PointLocus({}, {})".format(self.x, self.y) + +class LineLocus(Locus): + """A locus that defines all square tiles that intersect a given line segment.""" + + def __init__(self, x1, y1, x2, y2, thick = False, **kwargs): + """Take the coordinates for the two endpoints.""" + self.x1 = x1 + self.y1 = y1 + self.x2 = x2 + self.y2 = y2 + self.thick = thick + + def __contains__(self, item): + if not isinstance(item, tuple) and not isinstance(item, list): + raise ValueError("Item must be a tuple or a list.") + x, y = item + # Is it outside the bounding box for this line? + if x > max(self.x1, self.x2) or x < min(self.x1, self.x2) or y > max(self.y1, self.y2) or y < min(self.y1, self.y2): + return False + # Is this line straight up and down, or left and right? + elif self.x1 == self.x2 or self.y1 == self.y2: + # In this case, the line takes up the whole bounding box, so it must + # be true since it passed the first step to get here. + return True + else: + slope = (self.y2 - self.y1) / (self.x2 - self.x1) + intercept = (self.y1 + 0.5) - (self.x1 + 0.5) * slope + f = lambda z: z * slope + intercept + if self.thick: + if slope > 0: + return _mt.ceil(f(x)-1) <= y and _mt.floor(f(x+1)+1) > y + else: + return _mt.floor(f(x)+1) > y and _mt.ceil(f(x+1)-1) <= y + else: + if slope > 0: + return _mt.floor(f(x)) <= y and _mt.ceil(f(x+1)) > y + else: + return _mt.ceil(f(x)) > y and _mt.floor(f(x+1)) <= y + + def __iter__(self): + # avoid infinite slope case + if self.x1 == self.x2: + miny, maxy = min(self.y1, self.y2), max(self.y1, self.y2) + 1 + ret = [(self.x1, y) for y in range(miny, maxy)] + return iter(ret) + # for convenience: it's easy to calculate if it's horizontal too. + elif self.y1 == self.y2: + minx, maxx = min(self.x1, self.x2), max(self.x1, self.x2) + 1 + ret = [(x, self.y1) for x in range(minx, maxx)] + return iter(ret) + else: + slope = (self.y2 - self.y1) / (self.x2 - self.x1) + intercept = (self.y1 + 0.5) - (self.x1 + 0.5) * slope + f = lambda x: x * slope + intercept + lx, ly = min((self.x1, self.y1), (self.x2, self.y2)) + rx, ry = max((self.x1, self.y1), (self.x2, self.y2)) + ret = [] + # edge case 1: the first half column + if slope > 0: + maxy = _mt.ceil(f(lx+1)) + if self.thick: + maxy = _mt.floor(f(lx+1)+1) + for y in range(ly, maxy): + ret.append((lx, y)) + else: + maxy = _mt.floor(f(lx+1))-1 + if self.thick: + maxy = _mt.ceil(f(lx+1)-2) + for y in range(ly, maxy, -1): + ret.append((lx, y)) + # Usual case: the line between the end points + for x in range(lx+1, rx): + if slope > 0: + miny = _mt.floor(f(x)) + maxy = _mt.ceil(f(x+1)) + if self.thick: + miny = _mt.ceil(f(x)-1) + maxy = _mt.floor(f(x+1)+1) + for y in range(miny, maxy): + ret.append((x, y)) + else: + miny = _mt.ceil(f(x)-1) + maxy = _mt.floor(f(x+1))-1 + if self.thick: + miny = _mt.floor(f(x)) + maxy = _mt.ceil(f(x+1)-2) + for y in range(miny, maxy, -1): + ret.append((x, y)) + # edge case 2: the last half column + if slope > 0: + miny = _mt.floor(f(rx)) + if self.thick: + miny = _mt.ceil(f(rx)-1) + for y in range(miny, ry+1): + ret.append((rx, y)) + else: + miny = _mt.ceil(f(rx)-1) + if self.thick: + miny = _mt.floor(f(rx)) + for y in range(miny, ry-1, -1): + ret.append((rx, y)) + return iter(ret) + + def __repr__(self): + return "LineLocus({}, {}, {}, {}, thick={})".format(self.x1, self.y1, self.x2, self.y2, self.thick) + +class RectLocus(Locus): + """A locus that defines the outline of a rectangle.""" + + def __init__(self, x1, y1, x2, y2, **kwargs): + self.lx = min(x1, x2) + self.rx = max(x1, x2) + self.ly = min(y1, y2) + self.ry = max(y1, y2) + + def __contains__(self, item): + if not isinstance(item, tuple) and not isinstance(item, list): + raise ValueError("Item must be a tuple or a list.") + x, y = item + if x < self.lx or x > self.rx or y < self.ly or y > self.ry: + return False + elif x == self.lx or x == self.rx or y == self.ry or y == self.ly: + return True + else: + return False + + def __iter__(self): + ret = [(x, self.ly) for x in range(self.lx, self.rx)] + ret.extend([(self.rx, y) for y in range(self.ly, self.ry)]) + ret.extend([(x, self.ry) for x in range(self.rx, self.lx, -1)]) + ret.extend([(self.lx, y) for y in range(self.ry, self.ly, -1)]) + return iter(ret) + + def __repr__(self): + return "RectLocus({}, {}, {}, {})".format(self.lx, self.ly, self.rx, self.ry) + +class FilledRectLocus(Locus): + """A locus that defines all points within a rectangle.""" + + def __init__(self, x1, y1, x2, y2, **kwargs): + self.lx = min(x1, x2) + self.rx = max(x1, x2) + self.ly = min(y1, y2) + self.ry = max(y1, y2) + + def __contains__(self, item): + if not isinstance(item, tuple) and not isinstance(item, list): + raise ValueError("Item must be a tuple or a list.") + x, y = item + return x >= self.lx and x <= self.rx and y >= self.ly and y <= self.ry + + def __iter__(self): + return iter([(x, y) for y in range(self.ly, self.ry+1) for x in range(self.lx, self.rx+1)]) + + def __repr__(self): + return "FilledRectLocus({}, {}, {}, {})".format(self.lx, self.ly, self.rx, self.ry) + +class CircleLocus(Locus): + """A locus that defines the outline of a circle.""" + + def __init__(self, x, y, radius, **kwargs): + self.x = x + 0.5 + self.y = y + 0.5 + self.r = radius + + def __contains__(self, item): + if not isinstance(item, tuple) and not isinstance(item, list): + raise ValueError("Item must be a tuple or a list.") + x, y = item + if self.x == x + 0.5 and (y == _mt.floor(self.y + self.r) or y == _mt.floor(self.y - self.r)): + return True + elif self.y == y + 0.5 and (x == _mt.floor(self.x + self.r) or x == _mt.floor(self.x - self.r)): + return True + else: + # Edge case: very small circle + if self.r <= _mt.sqrt(2) * 0.5: + # In this case, the circle is small enough that the previous + # checks would have cought every possible tile other than the center. + return x == _mt.floor(self.x) and y == _mt.floor(self.y) + if y + 0.5 > self.y: + if x + 0.5 > self.x: + return _mt.sqrt((x - self.x)**2 + (y - self.y)**2) < self.r and _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) > self.r + else: + return _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) < self.r and _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) > self.r + else: + if x + 0.5 > self.x: + return _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) < self.r and _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) > self.r + else: + return _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) < self.r and _mt.sqrt((x - self.x)**2 + (y - self.y)**2) > self.r + + def __iter__(self): + # Edge case: very small circle + if self.r <= _mt.sqrt(2) * 0.5: + if self.r > 0.5: + ret = ((_mt.floor(self.x), _mt.floor(self.y)), + (_mt.floor(self.x) + 1, _mt.floor(self.y)), + (_mt.floor(self.x), _mt.floor(self.y) + 1), + (_mt.floor(self.x) - 1, _mt.floor(self.y)), + (_mt.floor(self.x), _mt.floor(self.y) - 1)) + return iter(ret) + else: + return iter(((_mt.floor(self.x), _mt.floor(self.y)),)) + else: + f = lambda z: _mt.sqrt(self.r * self.r - (z - self.x)**2) + self.y + # The topmost square: the 'keystone' if you will. + ret = [(_mt.floor(self.x), _mt.floor(self.y + self.r))] + # All squares between the keystone and the rightmost column. + for x in range(_mt.ceil(self.x), _mt.floor(self.x + self.r)): + ly = _mt.floor(f(x)) + ry = _mt.floor(f(x+1)) - 1 + for y in range(ly, ry, -1): + ret.append((x, y)) + # The last column, minus the bottom square + x = _mt.floor(self.x + self.r) + for y in range(_mt.floor(f(x)), _mt.floor(self.y), -1): + ret.append((x, y)) + # Finish the circle by copying the coordinates we already have. + ret = ret + [(_mt.floor(self.x) + (y - _mt.floor(self.y)), _mt.floor(self.y) - (x - _mt.floor(self.x))) for x, y in ret] + return iter(ret + [(_mt.floor(self.x) - (x - _mt.floor(self.x)), _mt.floor(self.y) - (y - _mt.floor(self.y))) for x, y in ret]) + + def __repr__(self): + return "CircleLocus({}, {}, {})".format(_mt.floor(self.x), _mt.floor(self.y), self.r) + +class FilledCircleLocus(Locus): + """A locus that defines all points within a circle.""" + + def __init__(self, x, y, radius, **kwargs): + self.x = x + 0.5 + self.y = y + 0.5 + self.r = radius + + def __contains__(self, item): + if not isinstance(item, tuple) and not isinstance(item, list): + raise ValueError("Item must be a tuple or a list.") + x, y = item + if self.x == x + 0.5 and (y <= _mt.floor(self.y + self.r) and y >= _mt.floor(self.y - self.r)): + return True + elif self.y == y + 0.5 and (x <= _mt.floor(self.x + self.r) and x >= _mt.floor(self.x - self.r)): + return True + else: + # Edge case: very small circle + if self.r <= _mt.sqrt(2) * 0.5: + # In this case, the circle is small enough that the previous + # checks would have cought every possible tile other than the center. + return x == _mt.floor(self.x) and y == _mt.floor(self.y) + if y + 0.5 > self.y: + if x + 0.5 > self.x: + return _mt.sqrt((x - self.x)**2 + (y - self.y)**2) < self.r + else: + return _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) < self.r + else: + if x + 0.5 > self.x: + return _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) < self.r + else: + return _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) < self.r + + def __iter__(self): + # Edge case: very small circle + if self.r <= _mt.sqrt(2) * 0.5: + if self.r > 0.5: + ret = ((_mt.floor(self.x), _mt.floor(self.y)), + (_mt.floor(self.x) + 1, _mt.floor(self.y)), + (_mt.floor(self.x), _mt.floor(self.y) + 1), + (_mt.floor(self.x) - 1, _mt.floor(self.y)), + (_mt.floor(self.x), _mt.floor(self.y) - 1)) + return iter(ret) + else: + return iter(((_mt.floor(self.x), _mt.floor(self.y)),)) + else: + f = lambda z: _mt.sqrt(self.r * self.r - (z - self.x)**2) + self.y + # The topmost square: the 'keystone' if you will. + ret = [(_mt.floor(self.x), y) for y in range(_mt.floor(self.y + self.r), _mt.floor(self.y), -1)] + # All squares between the keystone and the rightmost column. + ry = _mt.floor(self.y) + for x in range(_mt.ceil(self.x), _mt.floor(self.x + self.r)): + ly = _mt.floor(f(x)) + for y in range(ly, ry, -1): + ret.append((x, y)) + # The last column, minus the bottom square + x = _mt.floor(self.x + self.r) + for y in range(_mt.floor(f(x)), _mt.floor(self.y), -1): + ret.append((x, y)) + # Finish the circle by copying the coordinates we already have. + ret = ret + [(_mt.floor(self.x) + (y - _mt.floor(self.y)), _mt.floor(self.y) - (x - _mt.floor(self.x))) for x, y in ret] + return iter(ret + [(_mt.floor(self.x) - (x - _mt.floor(self.x)), _mt.floor(self.y) - (y - _mt.floor(self.y))) for x, y in ret] + [(_mt.floor(self.x), _mt.floor(self.y))]) + + def __repr__(self): + return "FilledCircleLocus({}, {}, {})".format(_mt.floor(self.x), _mt.floor(self.y), self.r) + +class SetLocus(set, Locus): + """A locus that defines a set of given arbitrary points.""" + + def __init__(self, *args, **kwargs): + super(SetLocus, self).__init__(*args, **kwargs) + + def __repr__(self): + return "SetLocus({})".format(tuple(self)) diff --git a/gamemap.py b/gamemap.py index 1205225..fcb2605 100644 --- a/gamemap.py +++ b/gamemap.py @@ -1,511 +1,10 @@ #gamemap.py import re import heapq -import xml.etree.ElementTree as ET import ruamel.yaml import math as _mt -from ruamel.yaml.comments import CommentedMap # for loading classes - -class Thing(object): - - def __init__(self, thingType: str, name: str, x: int, y: int, description: str, flags: int, playerx = None, playery = None): - self.thingType = thingType - self.name = name - self.description = description - self.x = x - self.y = y - self.playerx = x - self.playery = y - 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: - self.playery = playery - self.passable = bool(flags & 1) - self.talkable = bool(flags & 2) - self.lookable = bool(flags & 4) - self.takeable = bool(flags & 8) - self.useable = bool(flags & 16) - self.graphic = ('clear', '#7F7F7F', ' ') - self.thingID = -1 # ID not assigned - - def __str__(self): - """__str__ is used for look.""" - return self.description - - def __eq__(self, other): - if not isinstance(other, Thing): - return False - return self.name == other.name - -class Item(Thing): - yaml_flag = u'!Item' - defaultGraphic = ('clear', '#00BF00', '^') - - def __init__(self, name, x: int, y: int, description: str, useFunc: str, useOnFunc: str, customValues: dict, ranged: bool, graphic = defaultGraphic): - super(Item, self).__init__('i', name, x, y, description, 13) - self.useFunc = useFunc - self.useOnFunc = useOnFunc - self.customValues = customValues - self.ranged = ranged - self.graphic = graphic - - def use(self): - pass - - @classmethod - def to_yaml(cls, representer, node): - # save usual things - ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description} - # save graphic - graphic = {} - if node.graphic[0] != Item.defaultGraphic[0]: - graphic['bgc'] = node.graphic[0] - if node.graphic[1] != Item.defaultGraphic[1]: - graphic['fgc'] = node.graphic[1] - if node.graphic[2] != Item.defaultGraphic[2]: - graphic['shape'] = node.graphic[2] - if len(graphic) > 0: - ret['graphic'] = graphic - # save use functions - if node.useFunc != '': - ret['useFunc'] = node.useFunc - if node.useOnFunc != '': - ret['useOnFunc'] = node.useOnFunc - if len(node.customValues) > 0: - ret['customValues'] = node.customValues - if node.ranged: - ret['ranged'] = node.ranged - return representer.represent_mapping(cls.yaml_flag, ret) - - @classmethod - def from_yaml(cls, constructor, node): - parts = CommentedMap() - constructor.construct_mapping(node, parts, True) - # set default values for optional arguments - useFunc = '' - useOnFunc = '' - customValues = {} - ranged = False - bgc = Item.defaultGraphic[0] - fgc = Item.defaultGraphic[1] - shape = Item.defaultGraphic[2] - # load graphic - if 'graphic' in parts: - if 'bgc' in parts['graphic']: - bgc = parts['graphic']['bgc'] - if 'fgc' in parts['graphic']: - fgc = parts['graphic']['fgc'] - if 'shape' in parts['graphic']: - shape = parts['graphic']['shape'] - graphic = (bgc, fgc, shape) - # load use functions - if 'useFunc' in parts: - useFunc = parts['useFunc'] - if 'useOnFunc' in parts: - useOnFunc = parts['useOnFunc'] - if 'customValues' in parts: - customValues = dict(parts['customValues']) - for v in customValues: - if isinstance(customValues[v], tuple): - customValues[v] = list(customValues[v]) - if 'ranged' in parts: - useOnFunc = parts['ranged'] - return cls(parts['name'], parts['location'][0], parts['location'][1], - parts['description'], useFunc, useOnFunc, customValues, ranged, graphic) - -class Useable(Thing): - yaml_flag = u'!Useable' - defaultGraphic = ('clear', '#0000FF', '#') - - def __init__(self, name, x: int, y: int, description: str, useFunc: str, customValues: dict, playerx = None, playery = None, graphic = defaultGraphic): - super(Useable, self).__init__('u', name, x, y, description, 16, playerx, playery) - self.useFunc = useFunc - self.customValues = customValues - self.graphic = graphic - - @classmethod - def to_yaml(cls, representer, node): - # save usual things - ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description} - # save graphic - graphic = {} - if node.graphic[0] != Useable.defaultGraphic[0]: - graphic['bgc'] = node.graphic[0] - if node.graphic[1] != Useable.defaultGraphic[1]: - graphic['fgc'] = node.graphic[1] - if node.graphic[2] != Useable.defaultGraphic[2]: - graphic['shape'] = node.graphic[2] - if len(graphic) > 0: - ret['graphic'] = graphic - # save use functions - if node.useFunc != '': - ret['useFunc'] = node.useFunc - if len(node.customValues) > 0: - ret['customValues'] = node.customValues - if node.x != node.playerx or node.y != node.playery: - ret['useLocation'] = (node.playerx, node.playery) - return representer.represent_mapping(cls.yaml_flag, ret) - - @classmethod - def from_yaml(cls, constructor, node): - parts = CommentedMap() - constructor.construct_mapping(node, parts, True) - # set default values for optional arguments - useFunc = '' - customValues = {} - playerx, playery = parts['location'] - bgc = Useable.defaultGraphic[0] - fgc = Useable.defaultGraphic[1] - shape = Useable.defaultGraphic[2] - # load graphic - if 'graphic' in parts: - if 'bgc' in parts['graphic']: - bgc = parts['graphic']['bgc'] - if 'fgc' in parts['graphic']: - fgc = parts['graphic']['fgc'] - if 'shape' in parts['graphic']: - shape = parts['graphic']['shape'] - graphic = (bgc, fgc, shape) - # load use functions - if 'useFunc' in parts: - useFunc = parts['useFunc'] - if 'customValues' in parts: - customValues = dict(parts['customValues']) - for v in customValues: - if isinstance(customValues[v], tuple): - customValues[v] = list(customValues[v]) - if 'useLocation' in parts: - playerx, playery = parts['useLocation'] - return cls(parts['name'], parts['location'][0], parts['location'][1], - parts['description'], useFunc, customValues, playerx, playery, graphic) - -class 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, graphic = defaultGraphic): - super(NPC, self).__init__('n', name, x, y, description, None, customValues, 6, playerx, playery, graphic) - self.behavior = behavior - self.behaveEvent = None - self.tempInventory = inventory # should be deleted once NPC is loaded - - @classmethod - def to_yaml(cls, representer, node): - # save usual things - ret = {'name': node.name, 'location': (node.x, node.y), - 'description': node.description, 'behavior': node.behavior} - # 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 len(graphic) > 0: - ret['graphic'] = graphic - # save use functions - if len(node.inventory) > 0: - ret['inventory'] = node.inventory - if len(node.customValues) > 0: - ret['customValues'] = node.customValues - if node.x != node.playerx or node.y != node.playery: - ret['useLocation'] = (node.playerx, node.playery) - return representer.represent_mapping(cls.yaml_flag, ret) - - @classmethod - def from_yaml(cls, constructor, node): - parts = CommentedMap() - constructor.construct_mapping(node, parts, True) - # set default values for optional arguments - following = False - inventory = [] - customValues = {} - playerx, playery = parts['location'] - bgc = NPC.defaultGraphic[0] - fgc = NPC.defaultGraphic[1] - shape = NPC.defaultGraphic[2] - # load graphic - if 'graphic' in parts: - if 'bgc' in parts['graphic']: - bgc = parts['graphic']['bgc'] - if 'fgc' in parts['graphic']: - fgc = parts['graphic']['fgc'] - if 'shape' in parts['graphic']: - shape = parts['graphic']['shape'] - graphic = (bgc, fgc, shape) - # load use functions - if 'inventory' in parts: - inventory = parts['inventory'] - if 'customValues' in parts: - customValues = dict(parts['customValues']) - for v in customValues: - if isinstance(customValues[v], tuple): - customValues[v] = list(customValues[v]) - if 'useLocation' in parts: - playerx, playery = parts['useLocation'] - return cls(parts['name'], parts['location'][0], parts['location'][1], - parts['description'], parts['behavior'], inventory, customValues, - playerx, playery, graphic) - -class Door(Thing): - yaml_flag = u'!Door' - defaultGraphic = ('clear', '#7F3F00', '#') - - def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = defaultGraphic): - self.descBase = description - if description == None: - if locked: - description = "The {0} is locked.".format(name) - else: - description = "The {0} is unlocked.".format(name) - else: - if locked: - description += " It is locked.".format(name) - else: - description += " It is unlocked.".format(name) - super(Door, self).__init__('d', name, x, y, description, 1) - self.passable = not locked - self.key = key - self.graphic = graphic - - def lock(self, key = None): - if key == self.key: - self.passable = not self.passable - if self.descBase == None: - if self.passable: - self.description = "The {0} is unlocked.".format(self.name) - else: - self.description = "The {0} is locked.".format(self.name) - else: - if self.passable: - self.description += " It is unlocked.".format(self.name) - else: - self.description += " It is locked.".format(self.name) - return True - return False - - @classmethod - def to_yaml(cls, representer, node): - # save usual things - ret = {'name': node.name, 'location': (node.x, node.y)} - # save graphic - graphic = {} - if node.graphic[0] != Door.defaultGraphic[0]: - graphic['bgc'] = node.graphic[0] - if node.graphic[1] != Door.defaultGraphic[1]: - graphic['fgc'] = node.graphic[1] - if node.graphic[2] != Door.defaultGraphic[2]: - graphic['shape'] = node.graphic[2] - if len(graphic) > 0: - ret['graphic'] = graphic - # save door state - if node.passable: - ret['locked'] = not node.passable - if node.descBase != None: - ret['description'] = node.descBase - if node.key != None: - ret['key'] = node.key - return representer.represent_mapping(cls.yaml_flag, ret) - - @classmethod - def from_yaml(cls, constructor, node): - parts = CommentedMap() - constructor.construct_mapping(node, parts, True) - # set default values for optional arguments - description = None - locked = False - key = None - bgc = Door.defaultGraphic[0] - fgc = Door.defaultGraphic[1] - shape = Door.defaultGraphic[2] - # load graphic - if 'graphic' in parts: - if 'bgc' in parts['graphic']: - bgc = parts['graphic']['bgc'] - if 'fgc' in parts['graphic']: - fgc = parts['graphic']['fgc'] - if 'shape' in parts['graphic']: - shape = parts['graphic']['shape'] - graphic = (bgc, fgc, shape) - # load door state - if 'description' in parts: - description = parts['description'] - if 'locked' in parts: - locked = parts['locked'] - if 'key' in parts: - key = parts['key'] - return cls(parts['name'], parts['location'][0], parts['location'][1], - locked, description, key, graphic) - -class MapExit(Thing): - yaml_flag = u'!MapExit' - defaultGraphic = ('clear', '#FF0000', 'x') - - def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix = None, onUse = '', key = True, graphic = defaultGraphic): - description = name - if prefix: - description = "{0} {1}".format(prefix, name) - super(MapExit, self).__init__('x', name, x, y, description, 5) - self.exitid = exitid - self.destination = destination - self.prefix = prefix - self.onUse = onUse - self.key = key - self.graphic = graphic - - @classmethod - def to_yaml(cls, representer, node): - # save usual things - ret = {'name': node.name, 'location': (node.x, node.y), 'id': node.exitid, 'destination': node.destination} - # save graphic - graphic = {} - if node.graphic[0] != MapExit.defaultGraphic[0]: - graphic['bgc'] = node.graphic[0] - if node.graphic[1] != MapExit.defaultGraphic[1]: - graphic['fgc'] = node.graphic[1] - if node.graphic[2] != MapExit.defaultGraphic[2]: - graphic['shape'] = node.graphic[2] - if len(graphic) > 0: - ret['graphic'] = graphic - if node.prefix != None: - ret['prefix'] = node.prefix - if node.onUse != '': - ret['onUse'] = node.onUse - if node.key != True: - ret['key'] = node.key - return representer.represent_mapping(cls.yaml_flag, ret) - - @classmethod - def from_yaml(cls, constructor, node): - parts = CommentedMap() - constructor.construct_mapping(node, parts, True) - # set default values for optional arguments - prefix = None - onUse = '' - key = True - bgc = MapExit.defaultGraphic[0] - fgc = MapExit.defaultGraphic[1] - shape = MapExit.defaultGraphic[2] - # load graphic - if 'graphic' in parts: - if 'bgc' in parts['graphic']: - bgc = parts['graphic']['bgc'] - if 'fgc' in parts['graphic']: - fgc = parts['graphic']['fgc'] - if 'shape' in parts['graphic']: - shape = parts['graphic']['shape'] - graphic = (bgc, fgc, shape) - if 'prefix' in parts: - prefix = parts['prefix'] - if 'onUse' in parts: - onUse = parts['onUse'] - if 'key' in parts: - key = parts['key'] - return cls(parts['name'], parts['location'][0], parts['location'][1], - parts['id'], parts['destination'], prefix, onUse, key, graphic) - -class MapEntrance(Thing): - yaml_flag = u'!MapEntrance' - defaultGraphic = ('clear', 'clear', 'x') - # no graphic - should not be drawn - - def __init__(self, x: int, y: int, exitid: int, name = None): - if name == None: - name = 'entrance {}'.format(exitid) - super(MapEntrance, self).__init__('a', name, x, y, '', 1) - self.exitid = exitid - - @classmethod - def to_yaml(cls, representer, node): - # save usual things - ret = {'location': (node.x, node.y), 'id': node.exitid} - return representer.represent_mapping(cls.yaml_flag, ret) - - @classmethod - def from_yaml(cls, constructor, node): - parts = CommentedMap() - constructor.construct_mapping(node, parts, True) - # set default values for optional arguments - return cls(parts['location'][0], parts['location'][1], parts['id']) - -class 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) +import gamethings as _gt +import gamelocus as _gl class MapError(RuntimeError): pass @@ -518,6 +17,13 @@ class GameMap(object): # regular expressions tileRegex = re.compile(r'([a-z ])([0-9]+|[ ])') matrixRegex = re.compile(r'(?:[ \t]*(?:[a-z ](?:[0-9]+|[ ]))+(\n))+') + yaml = ruamel.yaml.YAML() + yaml.register_class(_gt.Item) + yaml.register_class(_gt.Useable) + yaml.register_class(_gt.NPC) + yaml.register_class(_gt.Door) + yaml.register_class(_gt.MapExit) + yaml.register_class(_gt.MapEntrance) def __init__(self, name, graph, matrix, dimensions): self.name = name @@ -534,6 +40,7 @@ class GameMap(object): self.wallColors = [] self.persistent = [] self.enterScript = '' + self.version = 'square 1' @staticmethod def __cleanStr(text: str, end = '\n'): @@ -562,22 +69,23 @@ it will read from stdin. Otherwise, it should be a valid file name. Entering a map through stdin will be obsolete once testing is over.""" info = None tryToRead = True - yaml = ruamel.yaml.YAML() - yaml.register_class(Item) - yaml.register_class(Useable) - yaml.register_class(NPC) - yaml.register_class(Door) - yaml.register_class(MapExit) - yaml.register_class(MapEntrance) if infile != None: try: with open(infile, 'r') as f: - info = yaml.load(f) + info = GameMap.yaml.load(f) except OSError as e: print("The file could not be read.") return None, nextThing else: raise MapError("No file was specified for loading.") + + # Future feature: different map standards, with cool special features + # like hex tiles, or walls in-between tiles. + # For now just 'square 1': square tiles, version 1. + version = 'square 1' + if 'version' in info: + if info['version'] in ('square 1'): + version = info['version'] # Now what we do with the data mat = None @@ -735,7 +243,7 @@ list of lists of tuples.""" return nextThing def addThingRecursive(self, container, nextThing = 0): - if isinstance(container, Thing): + if isinstance(container, _gt.Thing): if container.thingID == -1: container.thingID = nextThing nextThing = self.addThingRecursive(container.customValues, nextThing) @@ -777,42 +285,55 @@ list of lists of tuples.""" else: raise ValueError('Thing cannot be found by {}.'.format(str(kwargs))) - def path(self, x1, y1, x2, y2, closeEnough = True): + def path(self, x1, y1, loc, closeEnough = True): startThing = self.getThingAtCoords(x1, y1) - if not closeEnough: - if startThing and not startThing.passable: - return -1, [] # meaning you can't get there - dist, prev = self.dijkstra(x1, y1, closeEnough) - endPoint = self.coordsToInt(x2, y2) + #if not closeEnough: + # 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 dist[endPoint] < numVertex + 1: - return dist[endPoint], prev + if endPoint > -1 and dist[endPoint] < numVertex + 1: + pathList = [endPoint] + nextPoint = prev[endPoint] + while nextPoint != -1: + pathList.append(nextPoint) + nextPoint = prev[nextPoint] + pathList.reverse() + return dist[endPoint], pathList[1:], endPoint else: - return -1, [] # meaning you can't get there + return -1, [], -1 # meaning you can't get there - def dijkstra(self, x1, y1, closeEnough = True): + def dijkstra(self, x1, y1, loc = None, closeEnough = True): """Uses Dijkstra's Algorithm to find the shortest path from (x1, y1) to (x2, y2) -The closeEnough parameter will create a path that lands beside the source if necessary.""" +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 = self.coordsToInt(x2, y2) + 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)] dist[startPoint] = 0 - if closeEnough: - if startThing and not startThing.passable: - dist[startPoint] = -1 + #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 for v in self.mapGraph[u]: thing = self.getThingAtPos(v) if thing and not thing.passable: - continue + if closeEnough and self.intToCoords(v) in loc: + return dist, prev, u + else: + continue tempDist = dist[u] + 1 if tempDist < dist[v]: dist[v] = tempDist @@ -820,7 +341,7 @@ The closeEnough parameter will create a path that lands beside the source if nec prev[v] = u heapq.heappush(queue, (dist[v], v)) - return dist, prev + return dist, prev, endPoint #if dist[endPoint] < numVertex + 1: # return dist[endPoint], prev #else: @@ -831,37 +352,13 @@ The closeEnough parameter will create a path that lands beside the source if nec # 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 - x = x1 + 0.5 - lst = [] - if abs(Dx) >= abs(Dy): - if Dx < 0: - x = x2 + 0.5 - y = y2 + 0.5 - dy = Dy / Dx - while(x < max(x2, x1) - 0.5): - x += 1 - lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y))) - if _mt.floor(y) != _mt.floor(y + dy): - lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y + dy))) - y += dy - elif abs(Dx) < abs(Dy): - if Dy < 0: - x = x2 + 0.5 - y = y2 + 0.5 - dx = Dx / Dy - while(y < max(y2, y1) - 0.5): - y += 1 - lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y))) - if _mt.floor(x) != _mt.floor(x + dx): - lst.append(self.coordsToInt(_mt.floor(x + dx), _mt.floor(y))) - x += dx + + # Common case second: + lst = list(_gl.LineLocus(x1, y1, x2, y2, False))[1:-1] # Here is where we actually check: for space in lst: - if not self.isPassable(space): + if not self.isPassable(self.coordsToInt(*space)): return False return True @@ -999,6 +496,8 @@ The closeEnough parameter will create a path that lands beside the source if nec else: x, y = self.intToCoords(x) if thing != None: + if thing.x == x and thing.y == y: + return # it's already there, so don't do anything. oldPos = self.coordsToInt(thing.x, thing.y) if oldPos in self.thingPos: self.thingPos[oldPos].remove(thing.thingID) @@ -1015,3 +514,75 @@ The closeEnough parameter will create a path that lands beside the source if nec else: raise MapError("There is nothing to move.") +class LoSLocus(_gl.Locus): + """A locus that defines all points within line-of-sight of a given points.""" + + def __init__(self, x, y, level): + self.x = x + self.y = y + self.level = level + + def __contains__(self, item): + if not isinstance(item, tuple) and not isinstance(item, list): + raise ValueError("Item must be a tuple or a list.") + x, y = item + return self.level.lineOfSight(x, y, self.x, self.y) + + def __iter__(self): + ret = [(self.x, self.y)] + mat = [[False for i in range(self.level.y)] for j in range(self.level.x)] + mat[self.x][self.y] = True + fringe = [(self.x, self.y + 1), (self.x + 1, self.y), (self.x, self.y - 1), (self.x - 1, self.y), + (self.x + 1, self.y + 1), (self.x + 1, self.y - 1), (self.x - 1, self.y - 1), (self.x - 1, self.y + 1)] + while len(fringe) > 0: + point = fringe.pop(0) + check = [] + if abs(point[0] - self.x) > abs(point[1] - self.y): + if point[0] > self.x: + check.append((point[0] - 1, point[1])) + if point[1] > self.y: + check.append((point[0] - 1, point[1] - 1)) + elif point[1] < self.y: + check.append((point[0] - 1, point[1] + 1)) + else: + check.append((point[0] + 1, point[1])) + if point[1] > self.y: + check.append((point[0] + 1, point[1] - 1)) + elif point[1] < self.y: + check.append((point[0] + 1, point[1] + 1)) + elif abs(point[0] - self.x) < abs(point[1] - self.y): + if point[1] > self.y: + check.append((point[0], point[1] - 1)) + if point[0] > self.x: + check.append((point[0] - 1, point[1] - 1)) + elif point[0] < self.x: + check.append((point[0] + 1, point[1] - 1)) + else: + check.append((point[0], point[1] + 1)) + if point[0] > self.x: + check.append((point[0] - 1, point[1] + 1)) + elif point[0] < self.x: + check.append((point[0] + 1, point[1] + 1)) + else: + if point[0] > self.x: + if point[1] > self.y: + check.append((point[0] - 1, point[1] - 1)) + else: + check.append((point[0] - 1, point[1] - 1)) + else: + if point[1] > self.y: + check.append((point[0] + 1, point[1] - 1)) + else: + check.append((point[0] + 1, point[1] - 1)) + status = [mat[i[0]][i[1]] for i in check] + addIf = False + if True in status: + if False in status: + addIf = self.level.lineOfSight(point[0], point[1], self.x, self.y) + else: + addIf = True + if addIf: + mat[point[0]][point[1]] = self.level.isPassable(*point) + ret.append(point) + return iter(ret) + diff --git a/gamesequence.py b/gamesequence.py new file mode 100644 index 0000000..950d8e1 --- /dev/null +++ b/gamesequence.py @@ -0,0 +1,315 @@ +# gamesequence.py + +import re as _re + +class SequenceError(RuntimeError): + pass + +class ScriptBreak(object): + + def __init__(self, value): + self.value = value + + +def getValueFromString(arg: str, env: dict): + #if env == None: + #env = self.customValues + val = None + 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 + val = arg[1:-1] + elif _re.match(r'[+-]?(?:[0-9]*[.])?[0-9]+', arg) != None: + if '.' in arg: + val = float(arg) + else: + val = int(arg) + elif arg.casefold() == 'true': + val = True + elif arg.casefold() == '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: + 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] + else: + return False # for if statements; if a variable doesn't exist, should evaluate to False + # evaluate all values of all indecies + openBracket = validIdent.end() + if openBracket < len(arg): + if arg[openBracket] == '[': + ptr = openBracket + depth = 0 + while ptr < len(arg): + if depth == 0 and arg[ptr] != '[': + raise SequenceError('Invalid value syntax: {}'.format(arg)) + if arg[ptr] == '[': + depth += 1 + elif arg[ptr] == ']': + depth -= 1 + + if depth == 0: + index = getValueFromString(arg[openBracket+1:ptr], env) + if index in val: + val = val[index] + else: + return False + openBracket = ptr + 1 + ptr += 1 + 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): + """Generalized comparisons, may eventually be extended to other operators""" + if len(args) == 1: + return bool(getValueFromString(args[0]), env) + elif len(args) == 3 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'): + lval = getValueFromString(args[0], env) + operator = args[1] + rval = getValueFromString(args[2], env) + if operator == '==': + return lval == rval + elif operator == '!=': + return lval != rval + elif operator == '<=': + return lval <= rval + elif operator == '>=': + return lval >= rval + elif operator == '<': + return lval < rval + 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): + """If statement: if [not] value [op value] script""" + if len(args) < 2: + raise GameError('Incomplete If statement: if {}'.format(' '.join(args))) + inverse = False + ret = False + if args[0] == 'not': + inverse = True + args.pop(0) + if len(args) < 2: + raise GameError('Incomplete If statement: if {}'.format(' '.join(args))) + + # evaluate condition + if len(args) > 1 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'): + if len(args) < 4: + raise SequenceError('Incomplete If statement: if {}'.format(' '.join(args))) + ret = compareValues(args[:3], env) + args = args[3:] + else: + ret = bool(getValueFromString(args[0]), env) + args = args[1:] + if inverse: + ret = not ret + + # if condition is true, evaluate further + if ret: + if isinstance(args[-1], list): + return runScript(args[-1], env, externalScripts) + else: + return runScript([args], env, externalScripts) + +def getCustomValue(args, env: dict): + if env == None: + raise SequenceError("Cannot get a value from an empty environment.") + return getValueFromString(args[0], env) + +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'] + if len(args) > 0 and args[0] == 'global': + scope = env['global'] + args.pop(0) + if len(args) < 3 or args[1] not in ('=', '+=', '-=', '*=', '/=', '%=', '//=', '**=', 'b=', '!=', '|=', '&=', '^='): + raise SequenceError('Arguments are not fit for the setCustomValue script.') + # This next line allows cvmod to use values in the thing's customValues dict, + # but doing this means that accessing a game CV requires setting it to a local var. + val = getValueFromString(args[2], env) + + # set the value to a default value (0, 0.0, '', etc.) if not yet assigned + if args[0] not in scope and args[1] != '=': + if isinstance(val, int): + scope[args[0]] = 0 + elif isinstance(val, float): + scope[args[0]] = 0.0 + elif isinstance(val, str): + scope[args[0]] = '' + elif isinstance(val, bool): + scope[args[0]] = False + # Beyond this point are types that can only be found from other customValues + elif isinstance(val, tuple): + scope[args[0]] = () + elif isinstance(val, list): + scope[args[0]] = [] + elif isinstance(val, dict): + scope[args[0]] = {} + else: + raise SequenceError("Operation not supported for unassigned custom values of type {}: {}" + .format(type(val), args[1])) + + # done parsing, evaluate + if args[1] == '=': + scope[args[0]] = val + elif args[1] == '+=': + scope[args[0]] += val + elif args[1] == '-=': + scope[args[0]] -= val + elif args[1] == '*=': + scope[args[0]] *= val + elif args[1] == '/=': + scope[args[0]] /= val + elif args[1] == '%=': + scope[args[0]] %= val + elif args[1] == '//=': + scope[args[0]] //= val + elif args[1] == '**=': + scope[args[0]] **= val + elif args[1] == 'b=': + scope[args[0]] = bool(val) + elif args[1] == '!=': + scope[args[0]] = not bool(val) + elif args[1] == '|=': + scope[args[0]] |= val + elif args[1] == '&=': + scope[args[0]] &= val + elif args[1] == '^=': + scope[args[0]] ^= val + return scope[args[0]] + +def delCustomValue(args, env: dict): + """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]) + +_requireEnv = {'get' : getCustomValue, 'set' : setCustomValue, 'del' : delCustomValue} +_requireScripts = {'if' : ifScript} + +def runScript(script: list, env: dict, externalScripts: dict): + """run a script""" + ret = False + for line in script: + if len(line) == 0: + # empty line + continue + elif line[0].casefold() == 'playercmd': + # run a player command + self.getIO('playercmd')(line[1:]) + elif line[0].casefold() in _requireScripts: + ret = _requireScripts[line[0]](line[1:], env, externalScripts) + elif line[0].casefold() in _requireEnv: + ret = _requireEnv[line[0]](line[1:], env) + elif line[0].casefold() == 'break': + # exit early + return _ScriptBreak(ret) + elif line[0] in externalScripts: + # run a script + ret = externalScripts[line[0]](line[1:]) + else: + # conditional evaluation + compareValues(line, env) + + if isinstance(ret, ScriptBreak): + return ret + # We specifically want the return value of the last line executed. + return ret + +def parseScript(instr: str, envGlobal: dict, externalScripts: dict): + """parses then runs a script.""" + #print('parseScript called with {}.'.format(instr)) + if instr == '': + return # nothing to be done. + literalStr = list(instr) + inQuotes = False + script = [] + stack = [] + ret = [] + argStart = 0 + c = 0 + l = 0 + while c < len(instr): + if inQuotes: + if instr[c] == '"': + if instr[c-1] == '\\': + del literalStr[l-1] + l -= 1 + else: + inQuotes = False + del literalStr[l] + l -= 1 + else: + if instr[c] == '"': # quoted string + if l > 0 and instr[c-1] == '\\': + del literalStr[l-1] + l -= 1 + else: + inQuotes = True + del literalStr[l] + l -= 1 + elif instr[c] in ' \t\n;}{': + if l > 0 and instr[c-1] == '\\': + del literalStr[l-1] + l -= 1 + else: + if argStart != l: + ret.append(''.join(literalStr[argStart:l])) + if instr[c] == ';': + if len(ret) > 0: + script.append(ret) + ret = [] + elif instr[c] == '{': # block + stack.append(script) + script.append(ret) + script = [] + ret.append(script) + ret = [] + del literalStr[l] + l -= 1 + elif instr[c] == '}': # close block + if len(ret) > 0: + script.append(ret) + ret = [] + script = stack.pop() + del literalStr[l] + l -= 1 + argStart = l + 1 + c += 1 + l += 1 + if argStart != l: + 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'] = {} + ret = runScript(script, env, externalScripts) + if isinstance(ret, ScriptBreak): + ret = ret.value + return ret diff --git a/gameshell.py b/gameshell.py index a804acf..4a2fd74 100644 --- a/gameshell.py +++ b/gameshell.py @@ -9,6 +9,7 @@ import heapq import gameevents import textwrap as _tw from shutil import get_terminal_size as _gts +import gameutil as _gu #import random TERM_SIZE = _gts()[0] @@ -27,9 +28,16 @@ class GameShell(Shell): self.outstream = _sys.stdout self.gameBase = gameBase self.colorMode = 0 + data = self.gameBase.loadGameData('testing/testdata.yml') self.gameTitle = 'Game Shell' # should be changed for actual games + if 'title' in data: + self.gameTitle = data['title'] self.startLevel = 'testing/test1.yml' # should be changed for actual games + if 'startLevel' in data: + self.gameTitle = data['startLevel'] self.openingText = '{}\nIn Development' + if 'openingText' in data: + self.gameTitle = data['openingText'] self.ps2 = '?> ' self.__inGame = False @@ -43,7 +51,10 @@ class GameShell(Shell): self.registerCommand('help', self.man) self.registerCommand('new', self.newGame) self.gameBase.registerIO('container', self.container) - self.gameBase.registerIO('dialog', self.dialog) + self.gameBase.registerIO('startDialog', self.startDialog) + self.gameBase.registerIO('openDialog', self.openDialog) + self.gameBase.registerIO('respondDialog', self.respondDialog) + self.gameBase.registerIO('endDialog', self.endDialog) self.gameBase.registerIO('info', self.info) self.gameBase.registerIO('playercmd', self.playercmd) @@ -136,7 +147,7 @@ class GameShell(Shell): See a map of the local area. "ls" is an alias of map. "l" and "legend" are aliases of -l. If -l is given, a map legend will be printed under the map.""" - xAxis = ' ' + ''.join([self.gameBase.numberToLetter(i).ljust(2) for i in range(self.gameBase.level.dimensions[0])]) + '\n' + xAxis = ' ' + ''.join([_gu.numberToLetter(i).ljust(2) for i in range(self.gameBase.level.dimensions[0])]) + '\n' rows = [] index = 0 exits = {} @@ -154,11 +165,6 @@ If -l is given, a map legend will be printed under the map.""" for x in range(level.dimensions[0]): pos = level.mapMatrix[y][x] 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] @@ -253,8 +259,8 @@ If -l is given, a map legend will be printed under the map.""" 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.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("Player position:{0:.>64}".format("{0}{1}".format(_gu.numberToLetter(self.gameBase.player.x), self.gameBase.player.y))) + ret.append("Prev. position:{0:.>65}".format("{0}{1}".format(_gu.numberToLetter(self.gameBase.player.prevx), self.gameBase.player.prevy))) ret.append("Inventory:") for i in self.gameBase.player.inventory: ret.append("{0:<8}: {1}".format(i.thingID, i.name)) @@ -303,6 +309,7 @@ If -l is given, a map legend will be printed under the map.""" """Mode for in-game.""" if not self.__inGame: self.registerCommand('map', self.showMap) + self.registerCommand('wait', self.gameBase.wait) self.registerCommand('ls', self.showMap) self.registerCommand('go', self.gameBase.go) self.registerCommand('move', self.gameBase.go) @@ -324,6 +331,7 @@ If -l is given, a map legend will be printed under the map.""" """Mode for main menus and the like.""" if self.__inGame: self.registerCommand('map', self.showMap) + self.unregisterCommand('wait', self.gameBase.wait) self.unRegisterCommand('ls', self.showMap) self.unRegisterCommand('go', self.gameBase.go) self.unRegisterCommand('move', self.gameBase.go) @@ -435,78 +443,24 @@ Player is modified through side-effect.""" instr = input("Choose an item to view, or exit: ") return charsRead / 27 # based on average 250 words per minute, and word length of 5.5 + 1 for space. - def dialog(self, dialogObj): - if 'script' in dialogObj: - self.gameBase.parseScript(dialogObj['script']) - if 'cond' in dialogObj: - cases = dialogObj['cond'] - 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 self.gameBase.getValueFromString(cond[0])): - ret = self.dialog(case) - break - elif len(cond) == 3 and self.gameBase.compareValues(cond[1], cond[0], cond[2]): - ret = self.dialog(case) - break - else: - raise RuntimeError("All routes are false: {}".format(testValue)) - if ret > 1: - return ret - 1 - else: - return ret - # if ret == 1 or ret == 0, go back again. - elif 'opener' in dialogObj: - toPrint = dialogObj['opener'].split('\n') # split by lines so that fill doesn't erase newlines - for line in toPrint: - print(_tw.fill(line, width = TERM_SIZE)) - input('') - while isinstance(dialogObj, dict): - if 'action' in dialogObj: - action = dialogObj['action'] - if action == 'answer': - answer = 0 - skips = [] - j = 0 # follower to i - if 'answers' in dialogObj and isinstance(dialogObj['answers'], list): - for i in range(len(dialogObj['answers'])): - ans = dialogObj['answers'][i] - if ans[0] == '?': - condEnd = ans.index(':') - cond = ans[1:condEnd].strip().split() - if self.gameBase.compareValues(cond): - print(_tw.fill('{}: {}'.format(j+1, ans[condEnd+1:].strip()), width = TERM_SIZE)) - j += 1 - else: - skips.append(i) - else: - print(_tw.fill('{}: {}'.format(j+1, ans), width = TERM_SIZE)) - j += 1 - 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: - answer += 1 - else: - break - 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 - else: - raise RuntimeError('Malformed action: {}'.format(action)) - else: - raise RuntimeError("Dialog branch with neither switch nor openner.") + def startDialog(self): + pass + + def openDialog(self, opener): + for line in opener: + print(_tw.fill(line, width = TERM_SIZE)) + input("...") + + def respondDialog(self, options): + for lineNo in range(len(options)): + 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 + return answer + + def endDialog(self): + pass def playercmd(self, args): self.handleCommand(args) diff --git a/gamethings.py b/gamethings.py new file mode 100644 index 0000000..a411c66 --- /dev/null +++ b/gamethings.py @@ -0,0 +1,529 @@ +#gamethings.py + +import ruamel.yaml +from ruamel.yaml.comments import CommentedMap + +class Thing(object): + + def __init__(self, thingType: str, name: str, x: int, y: int, description: str, flags: int, playerx = None, playery = None, **kwargs): + self.thingType = thingType + self.name = name + self.description = description + self.x = x + 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: + self.playery = playery + self.passable = bool(flags & 1) + self.talkable = bool(flags & 2) + self.lookable = bool(flags & 4) + self.takeable = bool(flags & 8) + self.useable = bool(flags & 16) + self.graphic = ('clear', '#7F7F7F', ' ') + self.thingID = -1 # ID not assigned + + def __str__(self): + """__str__ is used for look.""" + return self.description + + def __eq__(self, other): + if not isinstance(other, Thing): + return False + return self.name == other.name + +class Observer(Thing): + """ABC for things that have a dict of events that they should listen to.""" + + def __init__(self, **kwargs): + super().__init__(**kwargs) + self.behaviors = {} # {the name of the event : (behaviorQueue priority, the name of the behavior)} + self.busy = False # Prevents a new behavior until the current one is finished + self.behaviorQueue = [] # If you can't make it perfect, then make it adjustable. + # The behavior queue is for behaviors that need to execute once the current action is finished. + # If it's not considered important enough (behaviors[event][0] < 0), it's dropped. + +class Item(Thing): + yaml_flag = u'!Item' + defaultGraphic = ('clear', '#00BF00', '^') + + def __init__(self, name, x: int, y: int, description: str, useFunc: str, useOnFunc: str, customValues: dict, ranged: bool, graphic = defaultGraphic): + super(Item, self).__init__('i', name, x, y, description, 13) + self.useFunc = useFunc + self.useOnFunc = useOnFunc + self.customValues = customValues + self.ranged = ranged + self.graphic = graphic + + def use(self): + pass + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description} + # save graphic + graphic = {} + if node.graphic[0] != Item.defaultGraphic[0]: + graphic['bgc'] = node.graphic[0] + if node.graphic[1] != Item.defaultGraphic[1]: + graphic['fgc'] = node.graphic[1] + if node.graphic[2] != Item.defaultGraphic[2]: + graphic['shape'] = node.graphic[2] + if len(graphic) > 0: + ret['graphic'] = graphic + # save use functions + if node.useFunc != '': + ret['useFunc'] = node.useFunc + if node.useOnFunc != '': + ret['useOnFunc'] = node.useOnFunc + if len(node.customValues) > 0: + ret['customValues'] = node.customValues + if node.ranged: + ret['ranged'] = node.ranged + return representer.represent_mapping(cls.yaml_flag, ret) + + @classmethod + def from_yaml(cls, constructor, node): + parts = CommentedMap() + constructor.construct_mapping(node, parts, True) + # set default values for optional arguments + useFunc = '' + useOnFunc = '' + customValues = {} + ranged = False + bgc = Item.defaultGraphic[0] + fgc = Item.defaultGraphic[1] + shape = Item.defaultGraphic[2] + # load graphic + if 'graphic' in parts: + if 'bgc' in parts['graphic']: + bgc = parts['graphic']['bgc'] + if 'fgc' in parts['graphic']: + fgc = parts['graphic']['fgc'] + if 'shape' in parts['graphic']: + shape = parts['graphic']['shape'] + graphic = (bgc, fgc, shape) + # load use functions + if 'useFunc' in parts: + useFunc = parts['useFunc'] + if 'useOnFunc' in parts: + useOnFunc = parts['useOnFunc'] + if 'customValues' in parts: + customValues = dict(parts['customValues']) + for v in customValues: + if isinstance(customValues[v], tuple): + customValues[v] = list(customValues[v]) + if 'ranged' in parts: + useOnFunc = parts['ranged'] + return cls(parts['name'], parts['location'][0], parts['location'][1], + parts['description'], useFunc, useOnFunc, customValues, ranged, graphic) + + def use(self, user, gameBase, args) -> float: + pass + + def useOn(self, user, gameBase, thing, args) -> float: + pass + +class Useable(Thing): + yaml_flag = u'!Useable' + defaultGraphic = ('clear', '#0000FF', '#') + + def __init__(self, name, x: int, y: int, description: str, useFunc: str, customValues: dict, playerx = None, playery = None, graphic = defaultGraphic): + super(Useable, self).__init__('u', name, x, y, description, 16, playerx, playery) + self.useFunc = useFunc + self.customValues = customValues + self.graphic = graphic + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description} + # save graphic + graphic = {} + if node.graphic[0] != Useable.defaultGraphic[0]: + graphic['bgc'] = node.graphic[0] + if node.graphic[1] != Useable.defaultGraphic[1]: + graphic['fgc'] = node.graphic[1] + if node.graphic[2] != Useable.defaultGraphic[2]: + graphic['shape'] = node.graphic[2] + if len(graphic) > 0: + ret['graphic'] = graphic + # save use functions + if node.useFunc != '': + ret['useFunc'] = node.useFunc + if len(node.customValues) > 0: + ret['customValues'] = node.customValues + if node.x != node.playerx or node.y != node.playery: + ret['useLocation'] = (node.playerx, node.playery) + return representer.represent_mapping(cls.yaml_flag, ret) + + @classmethod + def from_yaml(cls, constructor, node): + parts = CommentedMap() + constructor.construct_mapping(node, parts, True) + # set default values for optional arguments + useFunc = '' + customValues = {} + playerx, playery = parts['location'] + bgc = Useable.defaultGraphic[0] + fgc = Useable.defaultGraphic[1] + shape = Useable.defaultGraphic[2] + # load graphic + if 'graphic' in parts: + if 'bgc' in parts['graphic']: + bgc = parts['graphic']['bgc'] + if 'fgc' in parts['graphic']: + fgc = parts['graphic']['fgc'] + if 'shape' in parts['graphic']: + shape = parts['graphic']['shape'] + graphic = (bgc, fgc, shape) + # load use functions + if 'useFunc' in parts: + useFunc = parts['useFunc'] + if 'customValues' in parts: + customValues = dict(parts['customValues']) + for v in customValues: + if isinstance(customValues[v], tuple): + customValues[v] = list(customValues[v]) + if 'useLocation' in parts: + playerx, playery = parts['useLocation'] + return cls(parts['name'], parts['location'][0], parts['location'][1], + parts['description'], useFunc, customValues, playerx, playery, graphic) + + def use(self, user, gameBase, args) -> float: + pass + +class Character(Thing): + defaultGraphic = ('clear', '#000000', 'o') + + def __init__(self, inventory: dict, customValues: dict, graphic = defaultGraphic, **kwargs): + super(Character, self).__init__(**kwargs) + 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, Observer): + yaml_flag = u'!NPC' + defaultGraphic = ('clear', '#000000', 'o') + + def __init__(self, behaviors: dict, tempInventory: list, **kwargs): + if 'graphic' not in kwargs: + kwargs['graphic'] = PlayerCharacter.defaultGraphic + super(NPC, self).__init__(thingType = 'n', inventory = {}, flags = 6, **kwargs) + self.behaviors = behaviors + self.behaveEvent = None + self.tempInventory = tempInventory # should be deleted once NPC is loaded + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'name': node.name, 'location': (node.x, node.y), + '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 len(graphic) > 0: + ret['graphic'] = graphic + # save use functions + if len(node.inventory) > 0: + ret['inventory'] = node.inventory + if len(node.customValues) > 0: + ret['customValues'] = node.customValues + if node.x != node.playerx or node.y != node.playery: + ret['useLocation'] = (node.playerx, node.playery) + return representer.represent_mapping(cls.yaml_flag, ret) + + @classmethod + def from_yaml(cls, constructor, node): + parts = CommentedMap() + constructor.construct_mapping(node, parts, True) + # set default values for optional arguments + following = False + minventory = [] + mcustomValues = {} + mplayerx, mplayery = parts['location'] + bgc = NPC.defaultGraphic[0] + fgc = NPC.defaultGraphic[1] + shape = NPC.defaultGraphic[2] + # load graphic + if 'graphic' in parts: + if 'bgc' in parts['graphic']: + bgc = parts['graphic']['bgc'] + if 'fgc' in parts['graphic']: + fgc = parts['graphic']['fgc'] + if 'shape' in parts['graphic']: + shape = parts['graphic']['shape'] + mgraphic = (bgc, fgc, shape) + # load use functions + if 'inventory' in parts: + inventory = parts['inventory'] + if 'customValues' in parts: + mcustomValues = dict(parts['customValues']) + for v in mcustomValues: + if isinstance(mcustomValues[v], tuple): + mcustomValues[v] = list(mcustomValues[v]) + if 'useLocation' in parts: + playerx, playery = parts['useLocation'] + return cls(name = parts['name'], x = parts['location'][0], y = parts['location'][1], + description = parts['description'], behaviors = parts['behaviors'], tempInventory = minventory, customValues = mcustomValues, + playerx = mplayerx, plyery = mplayery, graphic = mgraphic) + +class Door(Thing): + yaml_flag = u'!Door' + defaultGraphic = ('clear', '#7F3F00', '#') + + def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = defaultGraphic): + self.descBase = description + if description == None: + if locked: + description = "The {0} is locked.".format(name) + else: + description = "The {0} is unlocked.".format(name) + else: + if locked: + description += " It is locked.".format(name) + else: + description += " It is unlocked.".format(name) + super(Door, self).__init__('d', name, x, y, description, 1) + self.passable = not locked + self.key = key + self.graphic = graphic + + def lock(self, key = None): + if key == self.key: + self.passable = not self.passable + if self.descBase == None: + if self.passable: + self.description = "The {0} is unlocked.".format(self.name) + else: + self.description = "The {0} is locked.".format(self.name) + else: + if self.passable: + self.description += " It is unlocked.".format(self.name) + else: + self.description += " It is locked.".format(self.name) + return True + return False + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'name': node.name, 'location': (node.x, node.y)} + # save graphic + graphic = {} + if node.graphic[0] != Door.defaultGraphic[0]: + graphic['bgc'] = node.graphic[0] + if node.graphic[1] != Door.defaultGraphic[1]: + graphic['fgc'] = node.graphic[1] + if node.graphic[2] != Door.defaultGraphic[2]: + graphic['shape'] = node.graphic[2] + if len(graphic) > 0: + ret['graphic'] = graphic + # save door state + if node.passable: + ret['locked'] = not node.passable + if node.descBase != None: + ret['description'] = node.descBase + if node.key != None: + ret['key'] = node.key + return representer.represent_mapping(cls.yaml_flag, ret) + + @classmethod + def from_yaml(cls, constructor, node): + parts = CommentedMap() + constructor.construct_mapping(node, parts, True) + # set default values for optional arguments + description = None + locked = False + key = None + bgc = Door.defaultGraphic[0] + fgc = Door.defaultGraphic[1] + shape = Door.defaultGraphic[2] + # load graphic + if 'graphic' in parts: + if 'bgc' in parts['graphic']: + bgc = parts['graphic']['bgc'] + if 'fgc' in parts['graphic']: + fgc = parts['graphic']['fgc'] + if 'shape' in parts['graphic']: + shape = parts['graphic']['shape'] + graphic = (bgc, fgc, shape) + # load door state + if 'description' in parts: + description = parts['description'] + if 'locked' in parts: + locked = parts['locked'] + if 'key' in parts: + key = parts['key'] + return cls(parts['name'], parts['location'][0], parts['location'][1], + locked, description, key, graphic) + +class MapExit(Thing): + yaml_flag = u'!MapExit' + defaultGraphic = ('clear', '#FF0000', 'x') + + def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix = None, onUse = '', key = True, graphic = defaultGraphic): + description = name + if prefix: + description = "{0} {1}".format(prefix, name) + super(MapExit, self).__init__('x', name, x, y, description, 5) + self.exitid = exitid + self.destination = destination + self.prefix = prefix + self.onUse = onUse + self.key = key + self.graphic = graphic + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'name': node.name, 'location': (node.x, node.y), 'id': node.exitid, 'destination': node.destination} + # save graphic + graphic = {} + if node.graphic[0] != MapExit.defaultGraphic[0]: + graphic['bgc'] = node.graphic[0] + if node.graphic[1] != MapExit.defaultGraphic[1]: + graphic['fgc'] = node.graphic[1] + if node.graphic[2] != MapExit.defaultGraphic[2]: + graphic['shape'] = node.graphic[2] + if len(graphic) > 0: + ret['graphic'] = graphic + if node.prefix != None: + ret['prefix'] = node.prefix + if node.onUse != '': + ret['onUse'] = node.onUse + if node.key != True: + ret['key'] = node.key + return representer.represent_mapping(cls.yaml_flag, ret) + + @classmethod + def from_yaml(cls, constructor, node): + parts = CommentedMap() + constructor.construct_mapping(node, parts, True) + # set default values for optional arguments + prefix = None + onUse = '' + key = True + bgc = MapExit.defaultGraphic[0] + fgc = MapExit.defaultGraphic[1] + shape = MapExit.defaultGraphic[2] + # load graphic + if 'graphic' in parts: + if 'bgc' in parts['graphic']: + bgc = parts['graphic']['bgc'] + if 'fgc' in parts['graphic']: + fgc = parts['graphic']['fgc'] + if 'shape' in parts['graphic']: + shape = parts['graphic']['shape'] + graphic = (bgc, fgc, shape) + if 'prefix' in parts: + prefix = parts['prefix'] + if 'onUse' in parts: + onUse = parts['onUse'] + if 'key' in parts: + key = parts['key'] + return cls(parts['name'], parts['location'][0], parts['location'][1], + parts['id'], parts['destination'], prefix, onUse, key, graphic) + +class MapEntrance(Thing): + yaml_flag = u'!MapEntrance' + defaultGraphic = ('clear', 'clear', 'x') + # no graphic - should not be drawn + + def __init__(self, x: int, y: int, exitid: int, name = None): + if name == None: + name = 'entrance {}'.format(exitid) + super(MapEntrance, self).__init__('a', name, x, y, '', 1) + self.exitid = exitid + + @classmethod + def to_yaml(cls, representer, node): + # save usual things + ret = {'location': (node.x, node.y), 'id': node.exitid} + return representer.represent_mapping(cls.yaml_flag, ret) + + @classmethod + def from_yaml(cls, constructor, node): + parts = CommentedMap() + constructor.construct_mapping(node, parts, True) + # set default values for optional arguments + return cls(parts['location'][0], parts['location'][1], parts['id']) + +class PlayerCharacter(Character): + """Player object. Cannot be created with yaml.""" + defaultGraphic = ('clear', '#0000FF', 'o') + + def __init__(self, **kwargs): + if 'name' not in kwargs: + kwargs['name'] == 'You' + if 'graphic' not in kwargs: + kwargs['graphic'] = PlayerCharacter.defaultGraphic + super(PlayerCharacter, self).__init__(thingType = 'p', flags = 5, **kwargs) diff --git a/gameutil.py b/gameutil.py new file mode 100644 index 0000000..ed8f8a9 --- /dev/null +++ b/gameutil.py @@ -0,0 +1,102 @@ +# gameutil.py + +# This is a home for nice helper functions that were useful, but also cluttered +# the GameBase class. + +import re as _re + +coordRegex = _re.compile(r'(?:(-?[a-zA-Z]+) ?(-?[0-9]+))|(?:(-?[0-9]+),? (-?[0-9]+))|(?:\(([0-9]+), ([0-9]+)\))') + +def letterToNumber(letter): + if letter.isdecimal(): + return int(letter) + ret = 0 + sign = 1 + start = 0 + for i in range(len(letter)): + if letter[i] == '-': + sign = -sign + start -= 1 + elif letter[i].isalpha(): + if letter[i].isupper(): + ret += (ord(letter[i]) - ord('A')) * (26**(i+start)) + else: + ret += (ord(letter[i]) - ord('a')) * (26**(i+start)) + else: + return ret * sign + return ret * sign + +def numberToLetter(number): + if isinstance(number, str): + return number + ret = '' + sign = '' + if number == 0: + return 'A' + elif number < 0: + sign = '-' + number = -number + while number > 0: + ret += chr(ord('A') + number % 26) + number = int(number / 26) + return sign + ret + +def parseCoords(level, args, usePlayerCoords = True, player = None): + """Takes an argument list, and figures out what it's talking about. +Returns (thing, x, y). "Thing" can be None.""" + if level == None: + raise ValueError('Coordinates cannot be parsed because there is no map.') + x = -1 + y = -1 + + # first, try by name for objects in the map + name = ' '.join(args) + thing = level.getThingByName(name) + if thing != None: + if usePlayerCoords: + return thing, thing.playerx, thing.playery + else: + return thing, thing.x, thing.y + + # Second, try by name for objects in the inventory + if player != None: + if name in player.thingNames: + return player.getThingByName(name), -1, -1 + + # Third, try by location + coordStr = args[0] + if len(args) > 1: + coordStr += ' ' + args[1] + match = coordRegex.match(coordStr) + if match != None: # if coordinates were given + groups = match.groups() + ind = 0 + for i in range(len(groups)): + if groups[i] == None or groups[i] == match.group(): + continue + else: + ind = i + break + x = letterToNumber(groups[ind]) + y = letterToNumber(groups[ind+1]) + thing = level.getThingAtCoords(x, y) + if thing != None and usePlayerCoords: + return thing, thing.playerx, thing.playery + else: + return thing, x, y + else: # if a name was given + return None, -1, -1 + +def startDialog(thing, outstream, dialog): + if 'dialogs' in thing.customValues: + if isinstance(thing.customValues['dialogs'], list): + if 'dialogPtr' in thing.customValues and isinstance(thing.customValues['dialogPtr'], int): + dialog(thing.customValues['dialogs'][thing.customValues['dialogPtr']], thing) + else: + dialog(thing.customValues['dialogs'][0], thing) + elif isinstance(thing.customValues['dialogs'], str): + 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 = outstream) diff --git a/locusdemo.py b/locusdemo.py new file mode 100644 index 0000000..ab4ffa9 --- /dev/null +++ b/locusdemo.py @@ -0,0 +1,44 @@ +from gamelocus import * + +def demo(loc): + print(loc) + allpoints = [i for i in loc] + print("Contains: Iter: ") + for row in range(10): + for col in range(10): + if (col, 9 - row) in loc: + print("##", end = "") + else: + print("..", end = "") + for col in range(10): + if (col, 9 - row) in allpoints: + print("##", end = "") + else: + print("..", end = "") + print() + +demo(LineLocus(1, 1, 8, 8)) +demo(LineLocus(1, 1, 8, 8, True)) +demo(LineLocus(1, 8, 8, 1)) +demo(LineLocus(1, 8, 8, 1, True)) +demo(LineLocus(1, 3, 8, 6)) +demo(LineLocus(1, 3, 8, 6, True)) +demo(LineLocus(1, 6, 8, 3)) +demo(LineLocus(1, 6, 8, 3, True)) +demo(LineLocus(3, 1, 6, 8)) +demo(LineLocus(3, 1, 6, 8, True)) +demo(LineLocus(1, 5, 8, 5)) +demo(LineLocus(1, 5, 8, 5, True)) +demo(RectLocus(1, 1, 8, 8)) +demo(FilledRectLocus(1, 1, 8, 8)) +demo(RectLocus(1, 3, 8, 6)) +demo(FilledRectLocus(1, 3, 8, 6)) +demo(CircleLocus(4, 4, 4)) +demo(CircleLocus(4, 4, 1)) +demo(CircleLocus(4, 4, 0.6)) +demo(CircleLocus(4, 4, 0.4)) +demo(FilledCircleLocus(4, 4, 4)) +demo(FilledCircleLocus(4, 4, 1)) +demo(FilledCircleLocus(4, 4, 0.6)) +demo(FilledCircleLocus(4, 4, 0.4)) +demo(SetLocus(((1, 1), (1, 8), (8, 8), (8, 1)))) diff --git a/testing/test1.yml b/testing/test1.yml index 0a3a2dd..0fa649f 100644 --- a/testing/test1.yml +++ b/testing/test1.yml @@ -47,7 +47,19 @@ loadAlways: name: guy description: a guy location: [4, 20] - behavior: wander + behaviors: + none: none # might this work to prevent this character from doing anything? customValues: dialogs: testing/testDialog.yml - + - !NPC + name: follower + description: a follower + location: [6, 26] + behaviors: + go: [-1, follow] + arrive: [-1, follow] + customValues: + follow: + distance: 2 + isFollowing: True + target: You # yes, YOU! diff --git a/testing/testDialog.yml b/testing/testDialog.yml index 1d76367..07fb3fa 100644 --- a/testing/testDialog.yml +++ b/testing/testDialog.yml @@ -15,7 +15,7 @@ replies: opener: I would, but there's still a bunch of foreshadowing and stuff we have to get through first. action: back - script: set tellmore += 1 + script: set global tellmore += 1 - case: else opener: You really want to know more, don't you? action: back