various tweaks and fixes

This commit is contained in:
Patrick Marsee 2019-06-08 13:42:00 -04:00
parent ed7fe60a6d
commit ed7d265b48
5 changed files with 524 additions and 212 deletions

2
.gitignore vendored
View file

@ -1,5 +1,7 @@
maps
saves
dialogs
__pycache__
gameshell.geany
gameexpr.py
game.py

View file

@ -8,13 +8,19 @@ import random as _ra
import sys as _sys
import pickle as _pi
import ruamel.yaml as _yaml
import textwrap as _tw
class GameError(RuntimeError):
pass
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"
@ -61,12 +67,19 @@ class GameBase(object):
self.registerUseFunc('container', self.container)
self.registerUseFunc('info', self.info)
self.registerUseFunc('wear', self.wear)
self.registerBehavior('stand', self.stand)
self.registerBehavior('wander', self.wander)
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('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)
# Helper functions
@ -112,12 +125,30 @@ to get input from stdin."""
def parseCoords(self, args, usePlayerCoords = True, allowInventory = True):
"""Takes an argument list, and figures out what it's talking about.
Returns (thing, x, y). "Thing" can be None."""
coordStr = args[0]
if len(args) > 1:
coordStr += ' ' + args[1]
#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:
for i in self.playerInv:
thingName = self.playerInv[i].name
if name == thingName:
return self.playerInv[i], -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())
@ -139,27 +170,7 @@ Returns (thing, x, y). "Thing" can be None."""
else:
return thing, x, y
else: # if a name was given
name = ' '.join(args)
thing = self.level.getThingByName(name)
if thing != None and usePlayerCoords:
return thing, thing.playerx, thing.playery
elif thing != None:
return thing, thing.x, thing.y
elif allowInventory:
for i in self.playerInv:
thingName = self.playerInv[i].name
if name == thingName:
thing = self.playerInv[i]
if thing != None:
return thing, -1, -1
else:
return None, -1, -1
#raise RuntimeError("'None' item named '{0}' in player inventory.".format(name))
return None, -1, -1
else: # nothing
return None, -1, -1
#raise ValueError("{0} cannot be reached.".format(name))
return None, x, y
def justifyText(self, text, width = 80):
ret = []
@ -181,10 +192,12 @@ Returns (thing, x, y). "Thing" can be None."""
return '\n'.join(ret)
def getValueFromString(self, arg: str):
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 '"\'': # assume it's a string
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:
@ -204,39 +217,49 @@ Returns (thing, x, y). "Thing" can be None."""
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]
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
if validIdent.end() < len(arg):
if arg[validIdent.end()] == '[':
openBracket = validIdent.end()
if openBracket < len(arg):
if arg[openBracket] == '[':
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))
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))
else:
raise GameError('Invalid argument to getValueFromString: {}'.format(arg))
return val
def compareValues(self, operator: str, left: str, right: str):
def compareValues(self, args: list):
"""Generalized comparisons, may eventually be extended to other operators"""
lval = self.getValueFromString(left)
rval = self.getValueFromString(right)
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 == '!=':
@ -249,6 +272,16 @@ Returns (thing, x, y). "Thing" can be None."""
return lval < rval
elif operator == '>':
return lval > rval
elif operator == 'in':
if args[2].casefold() == 'playerinv':
for i in rval:
if rval[i].name == lval:
return True
return False
else:
return lval in rval
else:
raise GameError("Condition cannot be evaluated: {}".format(' '.join(args)))
# commands
@ -278,6 +311,7 @@ The letter is not case-sensitive."""
# Now we have a heading! Let's see if we can get there...
if (x, y) == (self.playerx, self.playery):
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.ArriveEvent(self.playerName, x, y, 0.0)))
self.setEvent(0.0, _ge.ArriveEvent(self.playerName, self.playerx, self.playery, 0.0))
return
dist, path = self.level.path(x, y, self.playerx, self.playery)
if dist == -1:
@ -343,7 +377,7 @@ 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)
thing, x, y = self.parseCoords(args, usePlayerCoords = False, allowInventory = False)
if not self.level.lineOfSight(self.playerx, self.playery, x, y):
print("{} cannot talk to {}.".format(self.playerName, thing.name), file = self.outstream)
elif thing == None:
@ -423,15 +457,15 @@ the name of an item in the player's inventory."""
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])
item, x, y = self.parseCoords(args[:onIndex], usePlayerCoords = False, allowInventory = True)
if args[onIndex+1] == 'the':
onIndex += 1
thing, x, y = self.parseCoords(args[onIndex+1:])
thing, x, y = self.parseCoords(args[onIndex+1:], usePlayerCoords = True, allowInventory = True)
useArgs = []
if 'with' in args:
useArgs = args[args.index('with')+1:]
if item == None or item.thingID not in self.playerInv:
print("There is no {0} in the inventory.".format(item.name), file = self.outstream)
print("There is no such item in the inventory.", file = self.outstream)
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
return
if thing == None and x < 0 and y < 0:
@ -481,7 +515,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)
thing, x, y = self.parseCoords(args, allowInventory = False)
if thing == None:
print("There is nothing to take.", file = self.outstream)
return
@ -522,7 +556,7 @@ Object can be the name of the object, or its coordinates."""
for i in self.playerInv:
thingName = self.playerInv[i].name
if ' '.join(args) == thingName:
self.setEvent(0.0, _ge.DropEvent(self.playerInv[args[0]]))
self.setEvent(0.0, _ge.DropEvent(self.playerInv[i]))
return
print('{0} does not have a {1}.'.format(self.playerName, args[0]), file = self.outstream)
@ -547,11 +581,14 @@ Object can be the name of the object, or its coordinates."""
else:
self.level, self.nextThing = _gm.GameMap.read(args[0], None, preLoaded, self.nextThing)
if self.level == None:
raise GameError("Map could not be loaded.")
# get persistent things from it
if args[0] in self.persist:
persistedThings = tuple(self.persist[args[0]].keys())
for i in persistedThings:
self.level.addThing(self.persist[args[0]][i], True)
self.nextThing = self.level.addThing(self.persist[args[0]][i], self.nextThing, True) #nextThing shouldn't change
del self.persist[args[0]][i] # delete them from the persist dict to prevent item duplication
print(self.level.openingText, file = self.outstream)
@ -579,10 +616,10 @@ Object can be the name of the object, or its coordinates."""
if self.level.name not in self.persist:
self.persist[self.level.name] = {}
for i in self.level.persistent:
self.persist[self.level.name][i] = self.level.getThingByName(i)
self.persist[self.level.name][i] = self.level.getThingByID(i)
# build data object to be saved
data = (self.playerName, self.playerx, self.playery, self.playerInv, self.level.name, self.persist, self.eventQueue, self.gameTime, self.nextThing)
data = (self.playerName, self.playerx, self.playery, self.playerInv, self.level.name, self.persist, self.eventQueue, self.customValues, self.gameTime, self.nextThing)
# save it!
fileName = 'saves/' + args[0].replace(' ', '_') + '.dat'
@ -613,7 +650,7 @@ Object can be the name of the object, or its coordinates."""
fileName = args[0]
x, y, levelname = 1, 1, 'testing/test1.txt'
with open(fileName, 'rb') as f:
self.playerName, x, y, self.playerInv, levelname, self.persist, self.eventQueue, self.gameTime, self.nextThing = _pi.load(f)
self.playerName, x, y, self.playerInv, levelname, self.persist, self.eventQueue, self.customValues, self.gameTime, self.nextThing = _pi.load(f)
#print(levelname, x, y, file = self.outstream)
self.loadMap((levelname, x, y))
#_hq.heappush(self.eventQueue, (self.gameTime, _ge.NoOpEvent()))
@ -659,9 +696,14 @@ Object can be the name of the object, or its coordinates."""
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
@ -675,8 +717,8 @@ Object can be the name of the object, or its coordinates."""
elif instr[c] == '}': # close block
if len(ret) > 0:
script.append(ret)
script = stack.pop()
ret = []
script = stack.pop()
del literalStr[l]
l -= 1
argStart = l + 1
@ -684,20 +726,40 @@ Object can be the name of the object, or its coordinates."""
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'] = {}
self.runScript(script)
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 line[0].casefold() == 'playercmd':
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:
self.__scripts[line[0]](line[1:])
# conditional evaluation
ret = self.compareValues(line)
# We specifically want the return value of the last line executed.
return ret
def gameEventLoop(self):
#print(self.skipLoop)
@ -730,7 +792,8 @@ Object can be the name of the object, or its coordinates."""
self.prevx, self.prevy = self.playerx, self.playery
self.playerx, self.playery = e.x, e.y
else:
self.level.moveThing(e.actor, e.x, e.y)
actor = self.level.getThingByName(e.actor)
self.level.moveThing(actor, e.x, e.y)
return False
def handleArrive(self, e):
@ -741,6 +804,8 @@ Object can be the name of the object, or its coordinates."""
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))
@ -781,7 +846,7 @@ Object can be the name of the object, or its coordinates."""
def handleDrop(self, e):
e.item.x = self.playerx
e.item.y = self.playery
self.level.addThing(e.item, True)
self.nextThing = self.level.addThing(e.item, self.nextThing, True) # nextThing shouldn't change
del self.playerInv[e.item.thingID]
return True
@ -796,30 +861,30 @@ Object can be the name of the object, or its coordinates."""
raise ValueError("Non-examinable thing {0} examined.".format(thing.name))
if thing.customValues['pattern'] == 'single':
print(self.justifyText(thing.customValues['text']), file = self.outstream)
print(self.justifyText(str(thing.customValues['text'])), file = self.outstream)
elif thing.customValues['pattern'] == 'loop':
if not 'cursor' in thing.customValues:
thing.customValues['cursor'] = 0
cursor = thing.customValues['cursor']
print(self.justifyText(thing.customValues['text'][cursor]), file = self.outstream)
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
thing.customValues['cursor'] = (cursor + 1) % len(thing.customValues['text'])
elif thing.customValues['pattern'] == 'once':
if not 'cursor' in thing.customValues:
thing.customValues['cursor'] = 0
cursor = thing.customValues['cursor']
print(self.justifyText(thing.customValues['text'][cursor]), file = self.outstream)
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
if cursor < len(thing.customValues['text']) - 1:
thing.customValues['cursor'] += 1
elif thing.customValues['pattern'] == 'random':
cursor = _ra.randrange(len(thing.customValues['text']))
print(self.justifyText(thing.customValues['text'][cursor]), file = self.outstream)
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
thing.customValues['cursor'] = cursor
if 'set' in thing.customValues:
if 'cursor' in thing.customValues:
self.customVals[thing.customValues['set']] = thing.customValues[cursor]
self.customValues[thing.customValues['set']] = thing.customValues[cursor]
else:
self.customVals[thing.customValues['set']] = 0
self.customValues[thing.customValues['set']] = True
return 0.0
def container(self, thing, args):
@ -893,12 +958,13 @@ Object can be the name of the object, or its coordinates."""
raise GameError('Incomplete If statement: if {}'.format(' '.join(args)))
# evaluate condition
if args[1] in ('==', '!=', '<=', '>=', '<', '>'):
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[1], args[0], args[2])
ret = self.compareValues(args[:3])
args = args[3:]
else:
ret = bool(self.parseValue(args[0]))
ret = bool(self.getValueFromString(args[0]))
args = args[1:]
if inverse:
ret = not ret
@ -906,16 +972,33 @@ Object can be the name of the object, or its coordinates."""
# if condition is true, evaluate further
if ret:
if isinstance(args[-1], list):
self.runScript(args[-1])
return self.runScript(args[-1])
else:
self.runScript([args])
return 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)
ret = args[0].format(*[self.getValueFromString(i) for i in args[1:]])
print(ret, file = self.outstream)
return ret
def setCustomValue(self, args):
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:
@ -925,7 +1008,9 @@ Object can be the name of the object, or its coordinates."""
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])
# 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] != '=':
@ -975,6 +1060,15 @@ Object can be the name of the object, or its coordinates."""
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]"""
@ -989,35 +1083,33 @@ Object can be the name of the object, or its coordinates."""
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:9].casefold() == 'location=':
_, x, y = self.parseCoords(i[9:].split(), usePlayerCoords = False, allowInventory = False)
elif i[0:4].casefold() == 'fgc=':
if len(i[4:]) != 7 or i[0] != '#':
if len(i[4:]) != 7 or i[4] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
for j in i[4:]:
for j in i[5:]:
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] != '#':
if len(i[4:]) != 7 or i[4] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
for j in i[4:]:
for j in i[5:]:
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:]
if i[6] not in 'ox^ #|-':
raise GameError("Invalid shape: {}".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.
thing = None
if args[0].casefold() == 'item':
# spawn an item
description = 'A nondescript item.'
@ -1027,12 +1119,12 @@ Object can be the name of the object, or its coordinates."""
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']
bgc = _gm.Item.defaultGraphic[0]
if fgc == None:
fgc = _gm.Item.defaultGraphic[1]
if shape == None:
shape = _gm.Item.defaultGraphic['shape']
shape = _gm.Item.defaultGraphic[2]
for i in args[1:]:
if i[0:12].casefold() == 'description=':
description = i[12:]
@ -1045,9 +1137,8 @@ Object can be the name of the object, or its coordinates."""
elif i[0:3].casefold() == 'cv:':
equalsign = i.index('=')
cv = i[3:equalsign]
customValues[cv] = getValueFromString(i[equalsign+1:])
customValues[cv] = self.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.'
@ -1056,12 +1147,12 @@ Object can be the name of the object, or its coordinates."""
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']
bgc = _gm.Useable.defaultGraphic[0]
if fgc == None:
fgc = _gm.Useable.defaultGraphic[1]
if shape == None:
shape = _gm.Item.defaultGraphic['shape']
shape = _gm.Useable.defaultGraphic[2]
for i in args[1:]:
if i[0:12].casefold() == 'description=':
description = i[12:]
@ -1072,24 +1163,23 @@ Object can be the name of the object, or its coordinates."""
elif i[0:3].casefold() == 'cv:':
equalsign = i.index('=')
cv = i[3:equalsign]
customValues[cv] = getValueFromString(i[equalsign+1:])
customValues[cv] = self.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 = ''
behavior = 'stand'
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']
bgc = _gm.NPC.defaultGraphic[0]
if fgc == None:
fgc = _gm.NPC.defaultGraphic[1]
if shape == None:
shape = _gm.Item.defaultGraphic['shape']
shape = _gm.NPC.defaultGraphic[2]
for i in args[1:]:
if i[0:12].casefold() == 'description=':
description = i[12:]
@ -1100,9 +1190,8 @@ Object can be the name of the object, or its coordinates."""
elif i[0:3].casefold() == 'cv:':
equalsign = i.index('=')
cv = i[3:equalsign]
customValues[cv] = getValueFromString(i[equalsign+1:])
customValues[cv] = self.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.'
@ -1110,32 +1199,33 @@ Object can be the name of the object, or its coordinates."""
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']
bgc = _gm.Door.defaultGraphic[0]
if fgc == None:
fgc = _gm.Door.defaultGraphic[1]
if shape == None:
shape = _gm.Item.defaultGraphic['shape']
shape = _gm.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))
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
onUse = ''
key = True
if name == None:
name = 'somewhere'
if fgc == None:
fgc = _gm.Item.defaultGraphic['fgc']
if bgc == None:
bgc = _gm.Item.defaultGraphic['bgc']
bgc = _gm.MapExit.defaultGraphic[0]
if fgc == None:
fgc = _gm.MapExit.defaultGraphic[1]
if shape == None:
shape = _gm.Item.defaultGraphic['shape']
shape = _gm.MapExit.defaultGraphic[2]
for i in args[1:]:
if i[0:12].casefold() == 'destination=':
destination = i[12:]
@ -1143,8 +1233,11 @@ Object can be the name of the object, or its coordinates."""
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 i[0:6].casefold() == 'onuse=':
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))
elif args[0].casefold() == 'mapentrance':
# spawn a map entrance
exitid = 0
@ -1152,7 +1245,10 @@ Object can be the name of the object, or its coordinates."""
if i[0:7].casefold() == 'exitid=':
exitid = int(i[7:])
thing = _gm.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)
return thing
def giveToPlayer(self, args):
"""We get to assume it's an item."""
@ -1169,7 +1265,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())
_, x, y = self.parseCoords(i[10:].split(), usePlayerCoords = False, allowInventory = False)
elif i[0:4].casefold() == 'fgc=':
if len(i[4:]) != 7 or i[0] != '#':
raise GameError("Invalid color: {}".format(i[4:]))
@ -1206,10 +1302,49 @@ Object can be the name of the object, or its coordinates."""
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)
thing.thingID = self.nextThing
self.playerInv[thing.thingID] = thing
self.nextThing += 1
return thing
def moveThingScript(self, args):
colon = args.index(':')
thing, x, y = self.parseCoords(args[0:colon], usePlayerCoords = False, allowInventory = 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)
if thing != None and thing.thingType in 'ciu':
return self.getCustomValue(args[-1:], env = thing.customValues)
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)
if thing != None and thing.thingType in 'ciu':
return self.setCustomValue(args[-3:], env = thing.customValues)
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)
if thing != None and thing.thingType in 'ciu':
return self.setCustomValue(args[colon+1:], env = thing.customValues)
else:
raise GameError("Thing described in cvmod doesn't exist or isn't a character, item, or useable.")
# behaviors
def stand(self, actor):
pass
def wander(self, actor):
pass

View file

@ -358,7 +358,7 @@ 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, graphic = defaultGraphic):
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)
@ -366,6 +366,8 @@ class MapExit(Thing):
self.exitid = exitid
self.destination = destination
self.prefix = prefix
self.onUse = onUse
self.key = key
self.graphic = graphic
@classmethod
@ -384,6 +386,10 @@ class MapExit(Thing):
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
@ -392,6 +398,8 @@ class MapExit(Thing):
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]
@ -406,8 +414,12 @@ class MapExit(Thing):
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, graphic)
parts['id'], parts['destination'], prefix, onUse, key, graphic)
class MapEntrance(Thing):
yaml_flag = u'!MapEntrance'
@ -417,7 +429,7 @@ class MapEntrance(Thing):
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)
super(MapEntrance, self).__init__('a', name, x, y, '', 1)
self.exitid = exitid
@classmethod
@ -574,6 +586,7 @@ Entering a map through stdin will be obsolete once testing is over."""
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:
@ -582,7 +595,7 @@ Entering a map through stdin will be obsolete once testing is over."""
print("The file could not be read.")
return None, nextThing
else:
raise RuntimeError("No file was specified for loading.")
raise MapError("No file was specified for loading.")
# Now what we do with the data
mat = None
@ -593,10 +606,10 @@ Entering a map through stdin will be obsolete once testing is over."""
#print(layout.text)
match = GameMap.matrixRegex.match(layout.lstrip())
if match == None:
raise RuntimeError('Map read a file without a map matrix.')
raise MapError('Map read a file without a map matrix.')
mat = match.group()
else:
raise RuntimeError('Map read a file without a map matrix.')
raise MapError('Map read a file without a map matrix.')
# generate matrix and graph first
mapMatrix, mapGraph, dimensions = GameMap.parseMatrix(mat)
@ -632,7 +645,7 @@ list of lists of tuples."""
if x == 0:
x = len(mat[l])
elif x != len(mat[l]):
raise RuntimeError("Map matrix has jagged edges.")
raise MapError("Map matrix has jagged edges.")
l += 1
#x = 0
mat.append([])
@ -644,7 +657,7 @@ list of lists of tuples."""
else:
matrixStr = matrixStr[i:]
else: # This should happen when it finishes?
raise RuntimeError("Unexpected token in map matrix: '{0}'".format(matrixStr))
raise MapError("Unexpected token in map matrix: '{0}'".format(matrixStr))
y = len(mat) - 1
# Now for the graph
@ -995,19 +1008,19 @@ The closeEnough parameter will create a path that lands beside the source if nec
newPos = self.coordsToInt(x, y)
else:
x, y = self.intToCoords(x)
if thing:
if thing != None:
oldPos = self.coordsToInt(thing.x, thing.y)
if oldPos in self.thingPos:
self.thingPos[oldPos].remove(name)
self.thingPos[oldPos].remove(thing.thingID)
if len(self.thingPos[oldPos]) == 0:
del self.thingPos[oldPos]
if newPos not in self.thingPos:
self.thingPos[newPos] = [name]
self.thingPos[newPos] = [thing.thingID]
else:
self.thingPos[newPos].append(name)
self.thingPos[newPos].append(thing.thingID)
relPlayerx, relPlayery = thing.playerx - thing.x, thing.playery - thing.y
thing.x, thing.y = x, y
thing.playerx, thing.playery = thing.x + relPlayerx, thing.y + relPlayery
else:
raise RuntimeError("There is nothing to move.".format(name))
raise MapError("There is nothing to move.")

View file

@ -27,36 +27,26 @@ class GameShell(Shell):
self.outstream = _sys.stdout
self.gameBase = gameBase
self.colorMode = 0
self.gameTitle = 'Game Shell' # should be changed for actual games
self.startLevel = 'testing/test1.yml' # should be changed for actual games
self.openingText = '{}\nIn Development'
self.ps2 = '?> '
self.__inGame = False
# register functions
self.registerCommand('map', self.showMap)
self.registerCommand('ls', self.showMap)
self.registerCommand('go', self.gameBase.go)
self.registerCommand('move', self.gameBase.go)
self.registerCommand('walk', self.gameBase.go)
self.registerCommand('look', self.gameBase.look)
self.registerCommand('talk', self.gameBase.talk)
self.registerCommand('use', self.gameBase.use)
self.registerCommand('loadMap', self.gameBase.loadMap)
self.registerCommand('man', self.man)
self.registerCommand('help', self.man)
self.registerCommand('save', self.gameBase.saveGame)
self.registerCommand('load', self.gameBase.loadGame)
self.registerCommand('take', self.gameBase.take)
self.registerCommand('get', self.gameBase.take)
self.registerCommand('drop', self.gameBase.drop)
self.registerCommand('inv', self.inv)
self.registerCommand('bag', self.inv)
self.registerCommand('items', self.inv)
self.registerCommand('status', self.status)
self.registerCommand('load', self.gameBase.loadGame) # should always be available
self.registerCommand('flippetywick', self.devMode)
self.registerCommand('options', self.options)
self.registerCommand('colorTest', self.colorTest)
self.registerAlias('run', ['go', '-r'])
self.registerCommand('man', self.man)
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('info', self.info)
self.gameBase.registerIO('playercmd', self.playercmd)
self.menuMode()
# Helper functions
@ -109,6 +99,39 @@ class GameShell(Shell):
self.__colorTestRoutine(0, 256) # medium
self.__colorTestRoutine(128, 256) # light
def __getAdjFloors(self, level, x, y):
"""Get the floor colors of the 8 nearest tiles."""
adjFloors = []
for i in range(y-1, y+2):
if i < 0 or i >= level.dimensions[1]:
continue
for j in range(x-1, x+2):
if j < 0 or j >= level.dimensions[0]:
continue
if level.mapMatrix[i][j][0] == 'e':
adjFloors.append(level.mapMatrix[i][j][1])
return adjFloors
def __getUnderWallColor(self, level, x, y):
adjFloors = self.__getAdjFloors(level, x, y)
if len(adjFloors) > 0:
counts = {}
highestCount = 0
ret = -1
for i in adjFloors:
if i in counts:
counts[i] += 1
else:
counts[i] = 1
if counts[i] > highestCount:
ret = i
highestCount = counts[i]
elif counts[i] == highestCount and i < ret:
ret = i
return ret
else:
return -1
def showMap(self, args):
"""map [-l]
See a map of the local area. "ls" is an alias of map.
@ -170,8 +193,15 @@ If -l is given, a map legend will be printed under the map."""
sides += GameShell.DOWN
if x > 0 and level.mapMatrix[y][x-1][0] == 'w':
sides += GameShell.LEFT
underWallColor = self.__getUnderWallColor(level, x, y)
if level.floorColors[underWallColor] != floorColor and underWallColor != -1:
floorColor = level.floorColors[underWallColor]
rows[-1].append(self.color(floorColor[1:], fg = False))
rows[-1].append(GameShell.WALLS[sides])
else:
if level.floorColors[level.mapMatrix[y][x][1]] != floorColor:
floorColor = level.floorColors[level.mapMatrix[y][x][1]]
rows[-1].append(self.color(floorColor[1:], fg = False))
rows[-1].append(' ')
index += 1
rows[-1] = ''.join(rows[-1])
@ -235,26 +265,108 @@ 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]))
def newGame(self, args):
if self.__inGame:
response = input("Do you want to save before exiting (Y/n/x)? ")
if len(response) > 0:
if response[0] in 'Nn':
super(GameShell, self).exitShell(args)
elif response[0] in 'Xx': # cancel
return
else:
sf = input('Save file: ')
if len(sf) > 0:
gameBase.saveGame([sf])
else:
print('No save file given, cancelling exit.')
else:
sf = input('Save file: ')
if len(sf) > 0:
gameBase.saveGame([sf])
else:
print('No save file given, cancelling exit.')
try:
self.gameBase.loadMap([self.startLevel])
except RuntimeError as e:
print(e)
return
self.gameMode()
def gameMode(self):
"""Mode for in-game."""
if not self.__inGame:
self.registerCommand('map', self.showMap)
self.registerCommand('ls', self.showMap)
self.registerCommand('go', self.gameBase.go)
self.registerCommand('move', self.gameBase.go)
self.registerCommand('walk', self.gameBase.go)
self.registerCommand('look', self.gameBase.look)
self.registerCommand('talk', self.gameBase.talk)
self.registerCommand('use', self.gameBase.use)
self.registerCommand('save', self.gameBase.saveGame)
self.registerCommand('take', self.gameBase.take)
self.registerCommand('get', self.gameBase.take)
self.registerCommand('drop', self.gameBase.drop)
self.registerCommand('inv', self.inv)
self.registerCommand('bag', self.inv)
self.registerCommand('items', self.inv)
self.registerAlias('run', ['go', '-r'])
self.__inGame = True
def menuMode(self, text = None):
"""Mode for main menus and the like."""
if self.__inGame:
self.registerCommand('map', self.showMap)
self.unRegisterCommand('ls', self.showMap)
self.unRegisterCommand('go', self.gameBase.go)
self.unRegisterCommand('move', self.gameBase.go)
self.unRegisterCommand('walk', self.gameBase.go)
self.unRegisterCommand('look', self.gameBase.look)
self.unRegisterCommand('talk', self.gameBase.talk)
self.unRegisterCommand('use', self.gameBase.use)
self.unRegisterCommand('save', self.gameBase.saveGame)
self.unRegisterCommand('take', self.gameBase.take)
self.unRegisterCommand('get', self.gameBase.take)
self.unRegisterCommand('drop', self.gameBase.drop)
self.unRegisterCommand('inv', self.inv)
self.unRegisterCommand('bag', self.inv)
self.unRegisterCommand('items', self.inv)
self.unRegisterAlias('run', ['go', '-r'])
self.__inGame = False
if text == None:
print(self.openingText.format(self.gameTitle))
else:
print(text.format(self.gameTitle))
def devMode(self, args):
print('Dev mode activated. Please dev responsibly.')
self.gameMode()
self.registerCommand('dev', self.runScript)
self.registerCommand('status', self.status)
self.registerCommand('loadMap', self.gameBase.loadMap)
def runScript(self, args):
"""simple wrapper for gameBase.runscript."""
self.gameBase.parseScript(' '.join(args))
# IO calls
def container(self, inv, cont):
"""container IO"""
# Pretty print: get length of the longest inventory item's name
longestLen = 0
longestStr = ""
for i in inv:
if len(i) > longestLen:
longestLen = len(i)
longestStr = i
if len(inv[i].name) > longestLen:
longestLen = len(inv[i].name)
if longestLen > 0:
print('{{0:<{0}}}{1}'.format(max(6, longestLen+2), "Container:").format("Inv:"))
i = 0
invKeys = tuple(inv.keys())
while i < len(invKeys) and i < len(cont):
print('{{0:<{0}}}{1}'.format(longestLen+2, cont[i].name).format(invKeys[i]))
print('{{0:<{0}}}{1}'.format(longestLen+2, cont[i].name).format(inv[invKeys[i]].name))
i += 1
while i < len(invKeys):
print(invKeys[i])
print(inv[invKeys[i]].name)
i += 1
while i < len(cont):
print(' '*(longestLen+2) + cont[i].name)
@ -308,7 +420,7 @@ If -l is given, a map legend will be printed under the map."""
charsRead += len(items[instr])
else:
print('{} not here.'.format(instr))
input('<strike RETURN>')
input('<ENTER>')
for i in items:
print(' ', i)
instr = input("Choose an item to view, or exit: ")
@ -336,7 +448,10 @@ If -l is given, a map legend will be printed under the map."""
return ret
# if ret == 1 or ret == 0, go back again.
elif 'opener' in dialogObj:
print(_tw.fill(dialogObj['opener'], width = TERM_SIZE))
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('<ENTER>')
while isinstance(dialogObj, dict):
if 'action' in dialogObj:
action = dialogObj['action']
@ -350,8 +465,8 @@ If -l is given, a map legend will be printed under the map."""
if ans[0] == '?':
condEnd = ans.index(':')
cond = ans[1:condEnd].strip().split()
if self.gameBase.compareValues(cond[1], cond[0], cond[2]):
print(_tw.fill('{}: {}'.format(j+1, ans[condEnd+1:]), width = TERM_SIZE))
if self.gameBase.compareValues(cond):
print(_tw.fill('{}: {}'.format(j+1, ans[condEnd+1:].strip()), width = TERM_SIZE))
j += 1
else:
skips.append(i)
@ -390,6 +505,31 @@ If -l is given, a map legend will be printed under the map."""
def update(self):
self.gameBase.gameEventLoop()
def exitShell(self, args):
if self.__inGame:
response = input("Do you want to save before exiting (Y/n/x)? ")
if len(response) > 0:
if response[0] in 'Nn':
super(GameShell, self).exitShell(args)
elif response[0] in 'Xx': # cancel
return
else:
sf = input('Save file: ')
if len(sf) > 0:
gameBase.saveGame([sf])
super(GameShell, self).exitShell(args)
else:
print('No save file given, cancelling exit.')
else:
sf = input('Save file: ')
if len(sf) > 0:
gameBase.saveGame([sf])
super(GameShell, self).exitShell(args)
else:
print('No save file given, cancelling exit.')
else:
super(GameShell, self).exitShell(args)
if __name__ == '__main__':
sh = GameShell(GameBase())
sh.run()

View file

@ -140,14 +140,26 @@ class Shell(object):
conventionally called args or argv"""
self.__commands[commandName] = command
def unRegisterCommand(self, commandName: str):
"""Remove a command. May be useful for inheriting classes and the like."""
del self.__commands[commandName]
def registerAlias(self, a: str, original: list):
"""makes 'a' an alias for original.
'a' must be one token, but original can be multiple."""
self.__aliases[a] = original
def unRegisterAlias(self, commandName: str):
"""Remove an alias. May be useful for inheriting classes and the like."""
del self.__aliases[commandName]
def registerBatch(self, name: str, commands: list):
self.__batches[name] = commands
def unRegisterBatch(self, commandName: str):
"""Remove a command. May be useful for inheriting classes and the like."""
del self.__batches[commandName]
def getAlias(self, a: str):
if a in self.__aliases:
return self.__aliases[a]
@ -164,29 +176,39 @@ conventionally called args or argv"""
"""Take a line of text and turn it into an argument list"""
if instr == '':
return []
literalStr = list(instr)
inQuotes = False
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
ret.append(instr[argStart:c])
argStart = c+1
del literalStr[l]
l -= 1
else:
if instr[c] == '"':
if l > 0 and instr[c-1] == '\\':
del literalStr[l-1]
l -= 1
else:
inQuotes = True
if argStart != c:
ret.append(instr[argStart:c])
argStart = c+1
del literalStr[l]
l -= 1
elif instr[c] in ' \t\n':
if argStart != c:
ret.append(instr[argStart:c])
argStart = c + 1
if argStart != l:
ret.append(''.join(literalStr[argStart:l]))
argStart = l + 1
c += 1
if argStart != c:
ret.append(instr[argStart:c])
l += 1
if argStart != l:
ret.append(''.join(literalStr[argStart:l]))
a = self.getAlias(ret[0])
if a:
ret = a + ret[1:]