Added simple scripting for yaml files.

This commit is contained in:
Patrick Marsee 2019-05-30 15:44:29 -04:00
parent 8355dccb33
commit b332d579e9
6 changed files with 506 additions and 4 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@ maps
saves
__pycache__
gameshell.geany
gameexpr.py

View file

@ -25,6 +25,7 @@ class GameBase(object):
self.__behaviors = {}
self.__gameEvents = {}
self.__IOCalls = {} # {str : function}
self.__scripts = {} # functions with the same signature, but not callable by the user
self.customValues = {} # for setting flags and such
self.level = None
self.persist = {} # {level : {thingName : thing}}
@ -61,6 +62,11 @@ class GameBase(object):
self.registerUseFunc('info', self.info)
self.registerUseFunc('wear', self.wear)
self.registerBehavior('wander', self.wander)
self.registerScript('if', self.ifScript)
self.registerScript('printf', self.printfScript)
self.registerScript('set', self.setCustomValue)
self.registerScript('spawn', self.spawnThing)
self.registerScript('give', self.giveToPlayer)
# Helper functions
@ -175,6 +181,60 @@ Returns (thing, x, y). "Thing" can be None."""
return '\n'.join(ret)
def getValueFromString(self, arg: str):
val = None
validIdent = _re.match(r'[_A-Za-z][_0-9A-Za-z]*', arg)
if arg[0] in '"\'': # 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':
val = self.playerx
elif arg.casefold() == 'playery':
val = self.playery
elif arg.casefold() == 'playername':
val = self.playerName
elif arg.casefold() == 'playerinv':
val = self.playerInv
elif validIdent != None:
if 'scriptLocal' in self.customValues and validIdent.group() in self.customValues['scriptLocal']:
val = self.customValues['scriptLocal'][arg]
elif validIdent.group() in self.customValues:
val = self.customValues[arg]
else:
return False # for if statements; if a variable doesn't exist, should evaluate to False
# evaluate all values of all indecies
if validIdent.end() < len(arg):
if arg[validIdent.end()] == '[':
openBracket = validIdent.end()
ptr = openBracket
depth = 0
while ptr < len(arg):
if ptr == '[':
depth += 1
elif ptr == ']':
depth -= 1
if depth == 0:
var = var[self.getValueFromString(arg[openBracket+1:ptr])]
openBracket = ptr + 1
ptr += 1
if depth == 0 and arg[ptr] != '[':
raise GameError('Invalid value syntax: {}'.format(arg))
else:
raise GameError('Invalid value syntax: {}'.format(arg))
else:
raise GameError('Invalid argument to getValueFromString: {}'.format(arg))
return val
# commands
def go(self, args):
"""go [-r] [to [the]] destination [additional arguments...]
Go to a location. "walk" and "move" are aliases of go.
@ -486,6 +546,7 @@ Object can be the name of the object, or its coordinates."""
self.prevx, self.prevy = self.playerx, self.playery
if self.onLevelLoad != None:
self.onLevelLoad()
self.parseScript(self.level.enterScript)
def saveGame(self, args):
if len(args) < 1:
@ -541,13 +602,86 @@ Object can be the name of the object, or its coordinates."""
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
return
def dialog(self, dialogName, conversant):
def dialog(self, dialogName, conversant = None):
yaml = _yaml.YAML()
dialog = None
with open(dialogName, 'r') as f:
dialog = yaml.load(f)
self.getIO('dialog')(dialog)
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 argStart != l:
ret.append(''.join(literalStr[argStart:l]))
if instr[c] == ';':
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)
script = stack.pop()
ret = []
del literalStr[l]
l -= 1
argStart = l + 1
c += 1
l += 1
if argStart != l:
ret.append(''.join(literalStr[argStart:l]))
script.append(ret)
#print('After parsing: {}'.format(script))
self.customValues['scriptLocal'] = {}
self.runScript(script)
del self.customValues['scriptLocal']
def runScript(self, script: list):
"""run a script"""
for line in script:
if line[0].casefold() == 'playercmd':
# run a player command
self.getIO('playercmd')(line[1:])
else:
self.__scripts[line[0]](line[1:])
def gameEventLoop(self):
#print(self.skipLoop)
if self.skipLoop:
@ -727,6 +861,350 @@ Object can be the name of the object, or its coordinates."""
print("The key doesn't fit that lock.", file = self.outstream)
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
val = self.parseValue(args[0])
if args[1] in ('==', '!=', '<=', '>=', '<', '>'):
if len(args) < 4:
raise GameError('Incomplete If statement: if {}'.format(' '.join(args)))
val2 = self.parseValue(args[2])
if args[1] == '==':
ret = val == val2
elif args[1] == '!=':
ret = val != val2
elif args[1] == '<=':
ret = val <= val2
elif args[1] == '>=':
ret = val >= val2
elif args[1] == '<':
ret = val < val2
elif args[1] == '>':
ret = val > val2
args = args[3:]
else:
ret = bool(val)
args = args[1:]
if inverse:
ret = not ret
# if condition is true, evaluate further
if ret:
if isinstance(args[-1], list):
self.runScript(args[-1])
else:
self.runScript([args])
def printfScript(self, args):
"""Assume the first argument is a stringable object, and format all others."""
print(args[0].format(*[self.getValueFromString(i) for i in args[1:]]), file = self.outstream)
def setCustomValue(self, args):
"""takes [customValue, op, value]"""
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.')
val = self.getValueFromString(args[2])
# 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
def spawnThing(self, args):
"""Spawns a thing. [type name=... location=... etc]"""
if args[0].casefold() not in ('item', 'useable', 'npc', 'door', 'mapexit', 'mapentrance'):
raise GameError("{} not a spawnable thing type.".format(args[0]))
# Get the standard fields that most Things have in common: name, coordinates, graphic.
name = None
x, y = -1, -1
fgc, bgc, shape = None, None, None
persist = False
for i in args[1:]:
if i[0:5].casefold() == 'name=':
name = i[5:]
elif i[0:10].casefold() == 'location=':
_, x, y = self.parseCoords(i[10:].split())
elif i[0:4].casefold() == 'fgc=':
if len(i[4:]) != 7 or i[0] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
for j in i[4:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[4:]))
fgc = i[4:]
elif i[0:4].casefold() == 'bgc=':
if len(i[4:]) != 7 or i[0] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
for j in i[4:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[4:]))
bgc = i[4:]
elif i[0:6].casefold() == 'shape=':
if len(i[6:]) != 7 or i[0] != '#':
raise GameError("Invalid color: {}".format(i[6:]))
for j in i[6:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[6:]))
shape = i[6:]
elif i == 'persist':
persist = True
if x < 0 or y < 0:
raise GameError("Cannot spawn thing: bad location")
# Unfortunately, there needs to be a case for each type.
if args[0].casefold() == 'item':
# spawn an item
description = 'A nondescript item.'
useFunc = ''
useOnFunc = ''
customValues = {}
ranged = False
if name == None:
name = 'item {}'.format(self.nextThing)
if fgc == None:
fgc = _gm.Item.defaultGraphic['fgc']
if bgc == None:
bgc = _gm.Item.defaultGraphic['bgc']
if shape == None:
shape = _gm.Item.defaultGraphic['shape']
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() == 'useonfunc=':
useOnFunc = i[10:]
elif i.casefold() == 'ranged':
ranged = True
elif i[0:3].casefold() == 'cv:':
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))
self.nextThing = self.level.addThing(thing, self.nextThing, persist)
elif args[0].casefold() == 'useable':
# spawn a useable thing
description = 'A nondescript useable thing.'
useFunc = ''
customValues = {}
playerx, playery = x, y
if name == None:
name = 'useable {}'.format(self.nextThing)
if fgc == None:
fgc = _gm.Item.defaultGraphic['fgc']
if bgc == None:
bgc = _gm.Item.defaultGraphic['bgc']
if shape == None:
shape = _gm.Item.defaultGraphic['shape']
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())
elif i[0:3].casefold() == 'cv:':
equalsign = i.index('=')
cv = i[3:equalsign]
customValues[cv] = getValueFromString(i[equalsign+1:])
thing = _gm.Useable(name, x, y, description, useFunc, customValues, playerx, playery, (bgc, fgc, shape))
self.nextThing = self.level.addThing(thing, self.nextThing, persist)
elif args[0].casefold() == 'npc':
# spawn an NPC
description = 'A nondescript character.'
behavior = ''
inv = []
customValues = {}
playerx, playery = x, y
if name == None:
name = 'character {}'.format(self.nextThing)
if fgc == None:
fgc = _gm.Item.defaultGraphic['fgc']
if bgc == None:
bgc = _gm.Item.defaultGraphic['bgc']
if shape == None:
shape = _gm.Item.defaultGraphic['shape']
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())
elif i[0:3].casefold() == 'cv:':
equalsign = i.index('=')
cv = i[3:equalsign]
customValues[cv] = getValueFromString(i[equalsign+1:])
thing = _gm.NPC(name, x, y, description, behavior, inv, customValues, playerx, playery, False, (bgc, fgc, shape))
self.nextThing = self.level.addThing(thing, self.nextThing, persist)
elif args[0].casefold() == 'door':
# spawn a door
description = 'a nondescript door.'
locked = False
key = None
if name == None:
name = 'door {}'.format(self.nextThing)
if fgc == None:
fgc = _gm.Item.defaultGraphic['fgc']
if bgc == None:
bgc = _gm.Item.defaultGraphic['bgc']
if shape == None:
shape = _gm.Item.defaultGraphic['shape']
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))
self.nextThing = self.level.addThing(thing, self.nextThing, persist)
elif args[0].casefold() == 'mapexit':
# spawn an exit to another map (use with EXTREME caution!)
destination = ''
prefix = None
exitid = 0
if name == None:
name = 'somewhere'
if fgc == None:
fgc = _gm.Item.defaultGraphic['fgc']
if bgc == None:
bgc = _gm.Item.defaultGraphic['bgc']
if shape == None:
shape = _gm.Item.defaultGraphic['shape']
for i in args[1:]:
if i[0:12].casefold() == 'destination=':
destination = i[12:]
elif i[0:7].casefold() == 'prefix=':
prefix = i[7:]
elif i[0:7].casefold() == 'exitid=':
exitid = int(i[7:])
thing = _gm.MapExit(name, x, y, exitid, destination, prefix, (bgc, fgc, shape))
self.nextThing = self.level.addThing(thing, self.nextThing, persist)
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)
self.nextThing = self.level.addThing(thing, self.nextThing, persist)
def giveToPlayer(self, args):
"""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']
description = 'A nondescript item.'
persist = True
useFunc = ''
useOnFunc = ''
customValues = {}
ranged = False
for i in args[1:]:
if i[0:5].casefold() == 'name=':
name = i[5:]
elif i[0:10].casefold() == 'location=':
_, x, y = self.parseCoords(i[10:].split())
elif i[0:4].casefold() == 'fgc=':
if len(i[4:]) != 7 or i[0] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
for j in i[4:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[4:]))
fgc = i[4:]
elif i[0:4].casefold() == 'bgc=':
if len(i[4:]) != 7 or i[0] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
for j in i[4:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[4:]))
bgc = i[4:]
elif i[0:6].casefold() == 'shape=':
if len(i[6:]) != 7 or i[0] != '#':
raise GameError("Invalid color: {}".format(i[6:]))
for j in i[6:]:
if j not in '0123456789abcdefABCDEF':
raise GameError("Invalid color: {}".format(i[6:]))
shape = i[6:]
elif i == 'persist':
persist = True
elif i[0:12].casefold() == 'description=':
description = i[12:]
elif i[0:8].casefold() == 'usefunc=':
useFunc = i[8:]
elif i[0:10].casefold() == 'useonfunc=':
useOnFunc = i[10:]
elif i.casefold() == 'ranged':
ranged = True
elif i[0:3].casefold() == 'cv:':
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))
self.nextThing = self.level.addThing(thing, self.nextThing, persist)
# behaviors
def wander(self, actor):
@ -752,6 +1230,10 @@ always give the player a turn, False otherwise."""
"""Registers a function for useFuncs and such to use."""
self.__IOCalls[name] = func
def registerScript(self, name, func):
"""Registers a function as a script callable from yaml files."""
self.__scripts[name] = func
def getIO(self, name):
"""This is so derived classes can access their IOCalls."""
if name not in self.__IOCalls:

View file

@ -411,11 +411,12 @@ class MapExit(Thing):
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):
if prefix:
description = "{0} {1}".format(prefix, name)
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, description, 1)
self.exitid = exitid
@ -458,6 +459,7 @@ class GameMap(object):
self.floorColors = []
self.wallColors = []
self.persistent = []
self.enterScript = ''
@staticmethod
def __cleanStr(text: str, end = '\n'):
@ -674,6 +676,8 @@ list of lists of tuples."""
level.playerStart = info['playerStart']
if 'description' in info:
level.description = info['description']
if 'enterScript' in info:
level.enterScript = info['enterScript']
# get map colors
if 'floorColors' in info:

View file

@ -155,6 +155,8 @@ If -l is given, a map legend will be printed under the map."""
elif thing.thingType == 'i': # item
items[len(items)+1] = (thing.name, thing.graphic[1])
rows[-1].append('I{0}'.format(len(items)))
elif thing.thingType == 'a': # entrance
rows[-1].append(' ')
elif pos[0] == 'w':
if level.wallColors[level.mapMatrix[y][x][1]] != textColor:
textColor = level.wallColors[level.mapMatrix[y][x][1]]
@ -210,6 +212,9 @@ If -l is given, a map legend will be printed under the map."""
ret.append("Event queue:")
for i in sorted(self.gameBase.eventQueue):
ret.append("{0:.<8.3}:{1:.>72}".format(i[0], str(i[1])))
ret.append("custom values:")
for i in self.gameBase.customValues:
ret.append("{0:<22}: {1}".format(i, self.gameBase.customValues[i]))
ret.append("Player name:{0:.>68}".format(self.gameBase.playerName))
ret.append("Player position:{0:.>64}".format("{0}{1}".format(self.gameBase.numberToLetter(self.gameBase.playerx), self.gameBase.playery)))
ret.append("Prev. position:{0:.>65}".format("{0}{1}".format(self.gameBase.numberToLetter(self.gameBase.prevx), self.gameBase.prevy)))
@ -230,6 +235,8 @@ If -l is given, a map legend will be printed under the map."""
def inv(self, args):
print('\n'.join([self.gameBase.playerInv[i].name for i in self.gameBase.playerInv]))
# IO calls
def container(self, inv, cont):
"""container IO"""
# Pretty print: get length of the longest inventory item's name
@ -310,6 +317,8 @@ If -l is given, a map legend will be printed under the map."""
def dialog(self, dialogObj):
if 'opener' in dialogObj:
print(_tw.fill(dialogObj['opener'], width = TERM_SIZE))
if 'script' in dialogObj:
self.gameBase.parseScript(dialogObj['script'])
while isinstance(dialogObj, dict):
if 'action' in dialogObj:
action = dialogObj['action']
@ -334,6 +343,9 @@ If -l is given, a map legend will be printed under the map."""
elif action == 'exit':
return 0
def playercmd(self, args):
self.handleCommand(args)
def update(self):
self.gameBase.gameEventLoop()

View file

@ -2,6 +2,7 @@
---
openingText: Floor 1 map loaded successfully. Normally, this would describe the environment.
playerStart: [5, 26]
enterScript: printf "This script ran when you entered the level."
layout: |
w w w w w w w w w w w w w w w w w w w w w w w w w w w w w w0
w w0

View file

@ -13,3 +13,5 @@ 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;
printf "You have seen this text {} times." tellmore