Fixed most input validation bugs, and did too many improvements to remember. Did I mention that I am typing these words with my hands?

This commit is contained in:
Patrick Marsee 2020-05-10 23:26:21 -04:00
parent ee5c4da549
commit 4a398cc2a3
11 changed files with 546 additions and 253 deletions

2
.gitignore vendored
View file

@ -5,3 +5,5 @@ __pycache__
gameshell.geany gameshell.geany
gameexpr.py gameexpr.py
game.py game.py
solution.txt

View file

@ -38,8 +38,8 @@ class GameBase(object):
self.__scripts = {} # functions with the same signature, but not callable by the user self.__scripts = {} # functions with the same signature, but not callable by the user
self.customValues = {} # for setting flags and such self.customValues = {} # for setting flags and such
self.level = None self.level = None
self.persist = {} # {level : {thingName : thing}} self.persist = {} # {level : {thingID : thing}}
self.singletons = {} # {thingID : thing} self.singletons = {} # {thingName : thing}
self.ps2 = '? ' self.ps2 = '? '
self.eventQueue = [] self.eventQueue = []
self.gameTime = 0.0 self.gameTime = 0.0
@ -112,12 +112,23 @@ to get input from stdin."""
return '\n'.join(ret) return '\n'.join(ret)
def smoothMove(self, actor, loc, speed, action = None, immediate = False): def smoothMove(self, actor: _gt.Thing, loc: _gl.Locus, speed: float, action = None, immediate = False, closeEnough = True):
"""Move a thing smoothly at a constant speed.""" """Move a thing smoothly at a constant speed.
actor should be a Thing.
loc should be a locus.
speed should be a number.
action should be a callable with 0 arguments. A lambda would be good here.
If immediate is True, the go event would happen immediately.
closeEnough should be True only if the path is to a non-passable point locus."""
if not isinstance(actor, _gt.Thing):
raise TypeError("The actor passed to smoothMove must be a Thing.")
if not isinstance(loc, _gl.Locus):
raise TypeError("The locus passed to smoothMove must be a Locus.")
if (actor.x, actor.y) in loc: 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)) self.setEvent(0.0, _ge.ArriveEvent(actor, self.level.coordsToInt(actor.x, actor.y), speed, action, loc, timeTaken = speed))
return return
dist, path, endPoint = self.level.path(actor.x, actor.y, loc) dist, path, endPoint = self.level.path(actor.x, actor.y, loc, closeEnough)
#print(path) #print(path)
if dist == -1: if dist == -1:
print('{0} cannot reach there.'.format(actor.name), file = self.outstream) print('{0} cannot reach there.'.format(actor.name), file = self.outstream)
@ -125,11 +136,15 @@ to get input from stdin."""
elif dist == 1: elif dist == 1:
self.setEvent(speed, _ge.ArriveEvent(actor, path[0], speed, action, loc, timeTaken = speed)) self.setEvent(speed, _ge.ArriveEvent(actor, path[0], speed, action, loc, timeTaken = speed))
return speed return speed
elif dist == 0:
# We should try to avoid this branch, because it means an error happened.
self.setEvent(0.0, _ge.ArriveEvent(actor, self.level.coordsToInt(actor.x, actor.y), speed, action, loc, timeTaken = speed))
return
elif immediate: elif immediate:
self.setEvent(0, _ge.GoEvent(actor, path, speed, action, loc)) self.setEvent(0, _ge.GoEvent(actor, path, speed, action, loc, closeEnough))
return speed * (dist - 1) return speed * (dist - 1)
else: else:
self.setEvent(speed, _ge.GoEvent(actor, path, speed, action, loc, timeTaken = speed)) self.setEvent(speed, _ge.GoEvent(actor, path, speed, action, loc, closeEnough, timeTaken = speed))
return speed * dist return speed * dist
# commands # commands
@ -155,6 +170,9 @@ The letter is not case-sensitive."""
raise GameError("Cannot move: No level has been loaded.") raise GameError("Cannot move: No level has been loaded.")
if self.player == None: if self.player == None:
raise GameError("Cannot move: Player character doesn't exist.") raise GameError("Cannot move: Player character doesn't exist.")
if len(args) == 0:
print(f"{self.player.name} goes nowhere.", file = self.outstream)
return
speed = 0.6666667 speed = 0.6666667
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
speed = 0.3333333 speed = 0.3333333
@ -181,6 +199,7 @@ Object can be the name of the object, or its coordinates."""
if self.player == None: if self.player == None:
raise GameError("Cannot look: Player character doesn't exist.") raise GameError("Cannot look: Player character doesn't exist.")
if len(args) == 0: if len(args) == 0:
# no arguments: print the level description
print(self.justifyText(self.level.description), file = self.outstream) print(self.justifyText(self.level.description), file = self.outstream)
else: else:
if args[0] == 'at': if args[0] == 'at':
@ -188,15 +207,18 @@ Object can be the name of the object, or its coordinates."""
if args[0] == 'the': if args[0] == 'the':
args.pop(0) args.pop(0)
thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False, player = self.player) thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False, player = self.player)
if not self.level.lineOfSight(self.player.x, self.player.y, x, y): if thing == None:
print("There is nothing to see here.", file = self.outstream)
return
elif thing in self.player.inventory:
print(self.justifyText(str(thing)), file = self.outstream)
return
elif not self.level.lineOfSight(self.player.x, self.player.y, x, y):
if self.autoMove: if self.autoMove:
self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: print(self.justifyText(str(thing)), file = self.outstream)) self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: print(self.justifyText(str(thing)), file = self.outstream), closeEnough = False)
else: else:
print("{} cannot see that.".format(self.player.name), file = self.outstream) print("{} cannot see that.".format(self.player.name), file = self.outstream)
return return
elif thing == None:
print("There is nothing to see here.\n", file = self.outstream)
return
else: else:
print(self.justifyText(str(thing)), file = self.outstream) print(self.justifyText(str(thing)), file = self.outstream)
@ -212,22 +234,26 @@ Character can be the name of the character, or their coordinates."""
if self.player == None: if self.player == None:
raise GameError("Cannot talk: Player character doesn't exist.") raise GameError("Cannot talk: Player character doesn't exist.")
if len(args) == 0: if len(args) == 0:
print(self.justifyText(self.level.description), file = self.outstream) print(f'{self.player.name} says, "Hello!" Nobody responds.', file = self.outstream)
return
else: else:
if args[0] == 'to': if args[0] == 'to':
args.pop(0) args.pop(0)
if args[0] == 'the': if args[0] == 'the':
args.pop(0) args.pop(0)
thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False) thing, x, y = _gu.parseCoords(self.level, args, usePlayerCoords = False)
if not self.level.lineOfSight(self.player.x, self.player.y, x, y): if thing == None:
if len(args) > 0:
print(f"There is nobody named {' '.join(args)}.", file = self.outstream)
else:
print(f"There is nobody there.", file = self.outstream)
return
elif not self.level.lineOfSight(self.player.x, self.player.y, x, y):
if self.autoMove: if self.autoMove:
self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: _gu.startDialog(thing, self.outstream, self.dialog)) self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: _gu.startDialog(thing, self.outstream, self.dialog), closeEnough = False)
else: else:
print("{} cannot talk to {}.".format(self.player.name, thing.name), file = self.outstream) print("{} cannot talk to {}.".format(self.player.name, thing.name), file = self.outstream)
return return
elif thing == None:
print("There is nobody here.\n", file = self.outstream)
return
else: else:
_gu.startDialog(thing, self.outstream, self.dialog) _gu.startDialog(thing, self.outstream, self.dialog)
@ -246,6 +272,9 @@ the name of an item in the player's inventory."""
raise GameError("Cannot use: No level has been loaded.") raise GameError("Cannot use: No level has been loaded.")
if self.player == None: if self.player == None:
raise GameError("Cannot use: Player character doesn't exist.") raise GameError("Cannot use: Player character doesn't exist.")
if len(args) == 0:
print(f"{self.player.name} pokes the air as if pressing a button.", file = self.outstream)
return
speed = 0.6666667 speed = 0.6666667
useArgs = [] useArgs = []
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
@ -269,12 +298,23 @@ the name of an item in the player's inventory."""
return return
# Similar to go, but not quite the same. # Similar to go, but not quite the same.
self.smoothMove(self.player, ((x, y),), speed, lambda: self.setEvent(0.125, _ge.UseEvent(self.player, thing, useArgs))) if thing.thingType == 'i': # it must be in inventory to pass the last check.
self.setEvent(0.125, _ge.UseEvent(self.player, thing, useArgs))
else:
self.smoothMove(self.player, _gl.PointLocus(x, y), speed, lambda: self.setEvent(0.125, _ge.UseEvent(self.player, thing, useArgs)))
return return
def useOn(self, args, speed): def useOn(self, args, speed):
"""Called by use when there is an 'on' clause""" """Called by use when there is an 'on' clause"""
onIndex = args.index('on') onIndex = args.index('on')
if len(args[:onIndex]) == 0:
# just starts with 'use on ...'
print(f"{self.player.name} tries to use nothing, but nothing happens.", file = self.outstream)
return
if len(args[onIndex + 1:]) == 0:
# just ends with '... on'
print(f"{self.player.name} tries to use the {' '.join(args[:onIndex])} on nothing, but nothing happens.", file = self.outstream)
return
item, x, y = _gu.parseCoords(self.level, args[:onIndex], usePlayerCoords = False, player = self.player) item, x, y = _gu.parseCoords(self.level, args[:onIndex], usePlayerCoords = False, player = self.player)
if args[onIndex+1] == 'the': if args[onIndex+1] == 'the':
onIndex += 1 onIndex += 1
@ -293,11 +333,11 @@ the name of an item in the player's inventory."""
if not item.ranged: if not item.ranged:
# Similar to go, but not quite the same. # Similar to go, but not quite the same.
self.smoothMove(self.player, ((x, y),), speed, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs))) self.smoothMove(self.player, _gl.PointLocus(x, y), speed, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs)))
else: else:
if not self.level.lineOfSight(self.player.x, self.player.y, x, y): if not self.level.lineOfSight(self.player.x, self.player.y, x, y):
if self.autoMove: if self.autoMove:
self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs))) self.smoothMove(self.player, _gm.LoSLocus(x, y, self.level), 0.3333333, lambda: self.setEvent(0.125, _ge.UseOnEvent(self.player, item, thing, useArgs)), closeEnough = False)
else: else:
print("{} cannot see that.".format(self.player.name), file = self.outstream) print("{} cannot see that.".format(self.player.name), file = self.outstream)
return return
@ -315,6 +355,9 @@ Object can be the name of the object, or its coordinates."""
if self.player == None: if self.player == None:
raise GameError("Cannot take: Player character doesn't exist.") raise GameError("Cannot take: Player character doesn't exist.")
speed = 0.6666667 speed = 0.6666667
if len(args) == 0:
print(f"{self.player.name} reaches out and grasps at nothing.", file = self.outstream)
return
if args[0] == '-r' or args[0] == 'r' or args[0] == 'run': if args[0] == '-r' or args[0] == 'r' or args[0] == 'run':
speed = 0.3333333 speed = 0.3333333
args.pop(0) args.pop(0)
@ -330,7 +373,7 @@ Object can be the name of the object, or its coordinates."""
return return
# Similar to go, but not quite the same. # Similar to go, but not quite the same.
self.smoothMove(self.player, ((x, y),), speed, lambda: self.setEvent(0.125, _ge.TakeEvent(self.player, thing))) self.smoothMove(self.player, _gl.PointLocus(x, y), speed, lambda: self.setEvent(0.125, _ge.TakeEvent(self.player, thing)))
def drop(self, args): def drop(self, args):
"""drop [the] item""" """drop [the] item"""
@ -338,11 +381,14 @@ Object can be the name of the object, or its coordinates."""
raise GameError("Cannot drop: No level has been loaded.") raise GameError("Cannot drop: No level has been loaded.")
if self.player == None: if self.player == None:
raise GameError("Cannot drop: Player character doesn't exist.") raise GameError("Cannot drop: Player character doesn't exist.")
if len(args) == 0:
print(f"{self.player.name} falls over.", file = self.outstream)
return
if args[0] == 'the': if args[0] == 'the':
args.pop(0) args.pop(0)
thingName = ' '.join(args) thingName = ' '.join(args)
if thingName in self.player.thingNames: if thingName in self.player.thingNames:
self.setEvent(0.0, _ge.DropEvent(self.player.getThingByName(thingName))) self.setEvent(0.0, _ge.DropEvent(self.player, self.player.getThingByName(thingName)))
else: else:
print('{0} does not have a {1}.'.format(self.player.name, args[0]), file = self.outstream) print('{0} does not have a {1}.'.format(self.player.name, args[0]), file = self.outstream)
@ -443,9 +489,8 @@ Object can be the name of the object, or its coordinates."""
print("Save file must have a name!", file = self.outstream) print("Save file must have a name!", file = self.outstream)
return return
# choose pickle protocol depending on python version: # pickle protocol 4 for Python 3.4.0 to 3.7.x
# 3 for Python 3.0.0 to 3.3.x, 4 for Python 3.4.0 to 3.7.x prot = 4
prot = _pi.HIGHEST_PROTOCOL
fileName = 'saves/' + args[0].replace(' ', '_') + '.dat' fileName = 'saves/' + args[0].replace(' ', '_') + '.dat'
if args[0].endswith('.dat'): if args[0].endswith('.dat'):
fileName = args[0] fileName = args[0]
@ -475,10 +520,10 @@ Object can be the name of the object, or its coordinates."""
ret = 0 ret = 0
for case in cases: for case in cases:
cond = case['case'].split() # should be list like ["value", "==", 1] cond = case['case'].split() # should be list like ["value", "==", 1]
if len(cond) == 1 and (cond[0] == 'else' or _gs.getValueFromString(cond[0], {'scriptLocal': {}, 'global': self.customValues})): if len(cond) == 1 and (cond[0] == 'else' or _gs.getValueFromString(cond[0], _gs.ScriptEnvironment(self.customValues, {}))):
ret = self.runDialog(case) ret = self.runDialog(case)
break break
elif len(cond) == 3 and _gs.compareValues(cond, {'scriptLocal': {}, 'global': self.customValues}): elif len(cond) == 3 and _gs.compareValues(cond, _gs.ScriptEnvironment(self.customValues, {})):
ret = self.runDialog(case) ret = self.runDialog(case)
break break
else: else:
@ -504,19 +549,15 @@ Object can be the name of the object, or its coordinates."""
if ans[0] == '?': if ans[0] == '?':
condEnd = ans.index(':') condEnd = ans.index(':')
cond = ans[1:condEnd].strip().split() cond = ans[1:condEnd].strip().split()
if _gs.compareValues(cond, {'scriptLocal': {}, 'global': self.customValues}): if _gs.compareValues(cond, _gs.ScriptEnvironment(self.customValues, {})):
options.append(ans[condEnd+1:].strip()) options.append(ans[condEnd+1:].strip())
#print(_tw.fill('{}: {}'.format(j+1, ans[condEnd+1:].strip()), width = TERM_SIZE))
j += 1 j += 1
else: else:
skips.append(i) skips.append(i)
else: else:
options.append(ans) options.append(ans)
#print(_tw.fill('{}: {}'.format(j+1, ans), width = TERM_SIZE))
j += 1 j += 1
answer = self.getIO('respondDialog')(options) 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 # account for offset if there were answer options that didn't meet conditions
for i in skips: for i in skips:
if i <= answer: if i <= answer:
@ -534,7 +575,7 @@ Object can be the name of the object, or its coordinates."""
elif len(action) >= 4 and action[:4] == 'back': elif len(action) >= 4 and action[:4] == 'back':
if len(action) == 4: if len(action) == 4:
return 1 return 1
return int(action[4:]) return int(action[5:])
elif action == 'exit': elif action == 'exit':
return 0 return 0
else: else:
@ -561,6 +602,7 @@ Object can be the name of the object, or its coordinates."""
# any map. This is useful for when you have major characters who will # any map. This is useful for when you have major characters who will
# show up in many scenes, and their inventory must stay the same, for # show up in many scenes, and their inventory must stay the same, for
# example. # example.
#print(data)
self.singletons = {} self.singletons = {}
if 'singletons' in data: if 'singletons' in data:
for thing in data['singletons']: for thing in data['singletons']:
@ -580,6 +622,7 @@ Object can be the name of the object, or its coordinates."""
thing.addThing(i) thing.addThing(i)
del thing.tempInventory del thing.tempInventory
self.singletons[thing.name] = thing self.singletons[thing.name] = thing
#print(self.singletons)
return data return data
def gameEventLoop(self): def gameEventLoop(self):
@ -591,12 +634,11 @@ Object can be the name of the object, or its coordinates."""
ev = _hq.heappop(self.eventQueue) ev = _hq.heappop(self.eventQueue)
self.gameTime = ev[0] self.gameTime = ev[0]
e = ev[1] e = ev[1]
if e.eventType not in self.__gameEvents:
raise GameError("Unhandled event.")
ret = False ret = False
for i in self.__gameEvents[e.eventType]: if e.eventType in self.__gameEvents:
ret = ret or i(e) for i in self.__gameEvents[e.eventType]:
self.observe(e) ret = ret or i(e)
self.observe(e) # An event can still be observed even if it has no callback.
if ret: if ret:
break break
if len(self.eventQueue) == 0: if len(self.eventQueue) == 0:
@ -604,8 +646,8 @@ Object can be the name of the object, or its coordinates."""
_ge.resetEventNum() _ge.resetEventNum()
self.skipLoop = True self.skipLoop = True
def setEvent(self, t, e, skip = False): def setEvent(self, timeFromNow: float, event: _ge.GameEvent, skip = False):
_hq.heappush(self.eventQueue, (self.gameTime + t, e)) _hq.heappush(self.eventQueue, (self.gameTime + timeFromNow, event))
self.skipLoop = skip self.skipLoop = skip
def clearEvents(self, actor = None): def clearEvents(self, actor = None):
@ -634,13 +676,13 @@ Object can be the name of the object, or its coordinates."""
#print(e.actor.x, e.actor.y) #print(e.actor.x, e.actor.y)
#print(e.action) #print(e.action)
if len(e.path) > 1: if len(e.path) > 1:
self.setEvent(e.speed, _ge.GoEvent(e.actor, e.path[1:], e.speed, e.action, e.locus, e.timeTaken + e.speed)) self.setEvent(e.speed, _ge.GoEvent(e.actor, e.path[1:], e.speed, e.action, e.locus, e.closeEnough, e.timeTaken + e.speed))
else: else:
self.setEvent(e.speed, _ge.ArriveEvent(e.actor, e.path[0], e.speed, e.action, e.locus, e.timeTaken + e.speed)) 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): elif isinstance(e.locus, _gl.Locus):
self.smoothMove(e.actor, e.locus, e.speed, e.action, immediate = True) self.smoothMove(e.actor, e.locus, e.speed, e.action, True, e.closeEnough)
else: else:
print('{0} cannot !reach there.'.format(e.actor.name), file = self.outstream) print('{0} cannot reach there.'.format(e.actor.name), file = self.outstream)
return False return False
def handleArrive(self, e): def handleArrive(self, e):
@ -652,11 +694,16 @@ Object can be the name of the object, or its coordinates."""
self.level.moveThing(e.actor, e.pos) self.level.moveThing(e.actor, e.pos)
if e.action == None: if e.action == None:
#print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(e.actor.name, _gu.numberToLetter(e.x), e.y, e.t), file = self.outstream) #print('{0} arrived at {1}{2} after {3:.1f} seconds.'.format(e.actor.name, _gu.numberToLetter(e.x), e.y, e.t), file = self.outstream)
#print ("action = None")
if thing: if thing:
#print("thing != None")
if thing.thingType == 'x': if thing.thingType == 'x':
#print("thing is exit")
if e.actor == self.player: if e.actor == self.player:
#print("actor = player")
self.parseScript(thing.onUse) self.parseScript(thing.onUse)
if (isinstance(thing.key, bool) and thing.key == True) or (isinstance(thing.key, str) and self.parseScript(thing.key)): if (isinstance(thing.key, bool) and thing.key == True) or (isinstance(thing.key, str) and self.parseScript(thing.key)):
#print("key")
a = self.requestInput('Do you want to go {0}? (Y/n)'.format(str(thing))) a = self.requestInput('Do you want to go {0}? (Y/n)'.format(str(thing)))
if a != 'n' and a != 'N': if a != 'n' and a != 'N':
self.loadMap((thing.destination, thing.exitid)) self.loadMap((thing.destination, thing.exitid))
@ -665,25 +712,29 @@ Object can be the name of the object, or its coordinates."""
self.level.removeThing(actor.name) self.level.removeThing(actor.name)
else: else:
e.action() e.action()
return False # so that use works.
#elif isinstance(e.locus, _gl.Locus): #elif isinstance(e.locus, _gl.Locus):
# self.smoothMove(e.actor, e.locus, e.speed, e.action, immediate = True) # self.smoothMove(e.actor, e.locus, e.speed, e.action, immediate = True)
else: else:
# checking the locus again would inevitably fail, so we just give up. # checking the locus again would inevitably fail, so we just give up.
print('{0} cannot @reach there.'.format(e.actor.name), file = self.outstream) print('{0} cannot reach there.'.format(e.actor.name), file = self.outstream)
return e.actor == self.player return e.actor == self.player
def handleUse(self, e): def handleUse(self, e: _ge.UseEvent):
"""Called when a UseEvent needs processed."""
if e.thing.useFunc == '': if e.thing.useFunc == '':
print('The {0} cannot be used by itself.'.format(e.thing.name), file = self.outstream) print('The {0} cannot be used by itself.'.format(e.thing.name), file = self.outstream)
return True return True
self.setEvent(self.__useFuncs[e.thing.useFunc](e.thing, e.args), _ge.NoOpEvent()) self.setEvent(self.__useFuncs[e.thing.useFunc](e.thing, e.args), _ge.NoOpEvent())
return False return False
def handleUseOn(self, e): def handleUseOn(self, e: _ge.UseOnEvent):
"""Called when a UseOnEvent needs processed.
It calls the item's useOnFunc in the useFuncs map with the item, target, and arguments."""
if e.item.useOnFunc == '': if e.item.useOnFunc == '':
print('The {0} cannot be used on other objects.'.format(e.item.name), file = self.outstream) print('The {0} cannot be used on other objects.'.format(e.item.name), file = self.outstream)
return True return True
self.setEvent(self.__useFuncs[e.item.useOnFunc](e.item, e.thing, e.args), _ge.NoOpEvent()) self.setEvent(self.__useFuncs[e.item.useOnFunc](e.item, e.target, e.args), _ge.NoOpEvent())
return False return False
def handleTake(self, e): def handleTake(self, e):
@ -699,7 +750,7 @@ Object can be the name of the object, or its coordinates."""
e.item.x = e.actor.x e.item.x = e.actor.x
e.item.y = e.actor.y e.item.y = e.actor.y
self.nextThing = self.level.addThing(e.item, self.nextThing, True) # nextThing shouldn't change self.nextThing = self.level.addThing(e.item, self.nextThing, True) # nextThing shouldn't change
self.actor.removeThing(e.item) e.actor.removeThing(e.item)
return True return True
def handleBehave(self, e): def handleBehave(self, e):
@ -720,15 +771,24 @@ Object can be the name of the object, or its coordinates."""
"""Just prints the given text.""" """Just prints the given text."""
if 'pattern' not in thing.customValues or 'text' not in thing.customValues: if 'pattern' not in thing.customValues or 'text' not in thing.customValues:
raise ValueError("Non-examinable thing {0} examined.".format(thing.name)) raise ValueError("Non-examinable thing {0} examined.".format(thing.name))
# Start out with a cursor at 0.
if not 'cursor' in thing.customValues:
thing.customValues['cursor'] = 0
# With 'single', the cursor will not be modified.
if thing.customValues['pattern'] == 'single': if thing.customValues['pattern'] == 'single':
print(self.justifyText(str(thing.customValues['text'])), file = self.outstream) if isinstance(thing.customValues['text'], str):
print(self.justifyText(str(thing.customValues['text'])), file = self.outstream)
elif isinstance(thing.customValues['text'], list):
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
# With 'loop', the given strings will keep looping.
elif thing.customValues['pattern'] == 'loop': elif thing.customValues['pattern'] == 'loop':
if not 'cursor' in thing.customValues:
thing.customValues['cursor'] = 0
cursor = thing.customValues['cursor'] cursor = thing.customValues['cursor']
print(self.justifyText(str(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']) thing.customValues['cursor'] = (cursor + 1) % len(thing.customValues['text'])
# With 'once', the strings will be used one by one, until it gets to the end. The last one is then repeated.
elif thing.customValues['pattern'] == 'once': elif thing.customValues['pattern'] == 'once':
if not 'cursor' in thing.customValues: if not 'cursor' in thing.customValues:
thing.customValues['cursor'] = 0 thing.customValues['cursor'] = 0
@ -736,6 +796,8 @@ Object can be the name of the object, or its coordinates."""
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream) print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
if cursor < len(thing.customValues['text']) - 1: if cursor < len(thing.customValues['text']) - 1:
thing.customValues['cursor'] += 1 thing.customValues['cursor'] += 1
elif thing.customValues['pattern'] == 'random': elif thing.customValues['pattern'] == 'random':
cursor = _ra.randrange(len(thing.customValues['text'])) cursor = _ra.randrange(len(thing.customValues['text']))
print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream) print(self.justifyText(str(thing.customValues['text'][cursor])), file = self.outstream)
@ -784,14 +846,14 @@ Object can be the name of the object, or its coordinates."""
# item use-on functions # item use-on functions
def key(self, item, thing, args): def key(self, key : _gt.Item, door : _gt.Door, args):
"""Item is a key, which unlocks a door. This may be implemented for containers later.""" """Item is a key, which unlocks a door. This may be implemented for containers later."""
if isinstance(thing, tuple) or thing.thingType != 'd': if isinstance(door, tuple) or door.thingType != 'd':
print("That is not a door.", file = self.outstream) print("That is not a door.", file = self.outstream)
return 0.0 return 0.0
if thing.lock(item.name): if door.lock(key.name):
print("The key fits the lock.", file = self.outstream) print("The key fits the lock.", file = self.outstream)
if not thing.passable and self.player.x == thing.x and self.player.y == thing.y: if not door.passable and self.player.x == door.x and self.player.y == door.y:
self.player.x, self.player.y = self.player.prevx, self.player.prevy self.player.x, self.player.y = self.player.prevx, self.player.prevy
else: else:
print("The key doesn't fit that lock.", file = self.outstream) print("The key doesn't fit that lock.", file = self.outstream)
@ -870,10 +932,15 @@ Object can be the name of the object, or its coordinates."""
elif i.casefold() == 'ranged': elif i.casefold() == 'ranged':
ranged = True ranged = True
elif i[0:3].casefold() == 'cv:': elif i[0:3].casefold() == 'cv:':
cvEnv = customValues
equalsign = i.index('=') equalsign = i.index('=')
cv = i[3:equalsign] cv = i[3:equalsign]
customValues[cv] = self.getValueFromString(i[equalsign+1:]) ns = cv.find(':')
thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape)) while ns != -1:
cvEnv[cv[1:ns]] = {}
cvEnv = cvEnv[cv[1:ns]]
cvEnv[cv] = self.getValueFromString(i[equalsign+1:])
thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, _gt.ThingGraphic(bgc, fgc, shape))
elif args[0].casefold() == 'useable': elif args[0].casefold() == 'useable':
# spawn a useable thing # spawn a useable thing
description = 'A nondescript useable thing.' description = 'A nondescript useable thing.'
@ -899,7 +966,7 @@ Object can be the name of the object, or its coordinates."""
equalsign = i.index('=') equalsign = i.index('=')
cv = i[3:equalsign] cv = i[3:equalsign]
customValues[cv] = self.getValueFromString(i[equalsign+1:]) customValues[cv] = self.getValueFromString(i[equalsign+1:])
thing = _gt.Useable(name, x, y, description, useFunc, customValues, playerx, playery, (bgc, fgc, shape)) thing = _gt.Useable(name, x, y, description, useFunc, customValues, playerx, playery, _gt.ThingGraphic(bgc, fgc, shape))
elif args[0].casefold() == 'npc': elif args[0].casefold() == 'npc':
# spawn an NPC # spawn an NPC
description = 'A nondescript character.' description = 'A nondescript character.'
@ -926,7 +993,7 @@ Object can be the name of the object, or its coordinates."""
equalsign = i.index('=') equalsign = i.index('=')
cv = i[3:equalsign] cv = i[3:equalsign]
customValues[cv] = self.getValueFromString(i[equalsign+1:]) customValues[cv] = self.getValueFromString(i[equalsign+1:])
thing = _gt.NPC(name, x, y, description, behavior, inv, customValues, playerx, playery, (bgc, fgc, shape)) thing = _gt.NPC(name, x, y, description, behavior, inv, customValues, playerx, playery, _gt.ThingGraphic(bgc, fgc, shape))
elif args[0].casefold() == 'door': elif args[0].casefold() == 'door':
# spawn a door # spawn a door
description = 'a nondescript door.' description = 'a nondescript door.'
@ -945,7 +1012,7 @@ Object can be the name of the object, or its coordinates."""
description = i[12:] description = i[12:]
elif i.casefold() == 'locked': elif i.casefold() == 'locked':
locked = True locked = True
thing = _gt.Door(name, x, y, locked, description, key, (bgc, fgc, shape)) thing = _gt.Door(name, x, y, locked, description, key, _gt.ThingGraphic(bgc, fgc, shape))
elif args[0].casefold() == 'mapexit': elif args[0].casefold() == 'mapexit':
# spawn an exit to another map (use with EXTREME caution!) # spawn an exit to another map (use with EXTREME caution!)
destination = '' destination = ''
@ -972,7 +1039,7 @@ Object can be the name of the object, or its coordinates."""
onUse = i[6:] onUse = i[6:]
elif i[0:4].casefold() == 'key=': elif i[0:4].casefold() == 'key=':
key = i[4:] key = i[4:]
thing = _gt.MapExit(name, x, y, exitid, destination, prefix, onUse, key, (bgc, fgc, shape)) thing = _gt.MapExit(name, x, y, exitid, destination, prefix, onUse, key, _gt.ThingGraphic(bgc, fgc, shape))
elif args[0].casefold() == 'mapentrance': elif args[0].casefold() == 'mapentrance':
# spawn a map entrance # spawn a map entrance
exitid = 0 exitid = 0
@ -980,6 +1047,11 @@ Object can be the name of the object, or its coordinates."""
if i[0:7].casefold() == 'exitid=': if i[0:7].casefold() == 'exitid=':
exitid = int(i[7:]) exitid = int(i[7:])
thing = _gt.MapEntrance(x, y, exitid, name) thing = _gt.MapEntrance(x, y, exitid, name)
elif args[0].casefold() == 'singleton':
if name in self.singletons:
single = self.singletons[name]
single.x, single.y = x, y
single.prevx, single.prevy = x, y
else: else:
raise GameError("{} not a valid thing type.".format(args[0])) raise GameError("{} not a valid thing type.".format(args[0]))
self.nextThing = self.level.addThing(thing, self.nextThing, persist) self.nextThing = self.level.addThing(thing, self.nextThing, persist)
@ -1036,7 +1108,7 @@ Object can be the name of the object, or its coordinates."""
equalsign = i.index('=') equalsign = i.index('=')
cv = i[3:equalsign] cv = i[3:equalsign]
customValues[cv] = getValueFromString(i[equalsign+1:]) customValues[cv] = getValueFromString(i[equalsign+1:])
thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, (bgc, fgc, shape)) thing = _gt.Item(name, x, y, description, useFunc, useOnFunc, customValues, ranged, _gt.ThingGraphic(bgc, fgc, shape))
thing.thingID = self.nextThing thing.thingID = self.nextThing
self.player.addThing(thing) self.player.addThing(thing)
self.nextThing += 1 self.nextThing += 1
@ -1106,25 +1178,35 @@ Object can be the name of the object, or its coordinates."""
# behaviors # behaviors
def stand(self, actor): def stand(self, actor):
pass return 0
def wander(self, actor): def wander(self, actor):
pass return 0
def follow(self, actor, event): def follow(self, actor, event):
# make sure we only follow who we want to # make sure we only follow who we want to
if event.actor.name != actor.customValues["follow"]["target"]: if "follow" not in actor.customValues:
return 0 return 0
if "isFollowing" not in actor.customValues["follow"]:
return 0
if actor.customValues["follow"]["isFollowing"]:
if event.actor.name != actor.customValues["follow"]["target"]:
if actor.customValues["follow"]["target"] == '':
# This is a quick-and-dirty fix for an issue I was having with followers.
actor.customValues["follow"]["target"] = actor
return 0
else:
dest = 0
#print(event.eventType)
if event.eventType == 'go':
dest = event.path[-1]
elif event.eventType == 'arrive':
dest = event.pos
x, y = self.level.intToCoords(dest)
self.smoothMove(actor, _gl.CircleLocus(x, y, 2), event.speed, action = lambda: self.setEvent(0, _ge.BehaveEvent(actor)), closeEnough = False)
return -1
else: else:
dest = 0 return 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 # stuff for extended classes to use
def registerUseFunc(self, name, func): def registerUseFunc(self, name, func):
@ -1159,3 +1241,7 @@ always give the player a turn, False otherwise."""
if name not in self.__IOCalls: if name not in self.__IOCalls:
raise GameError("No IO call for {}.".format(name)) raise GameError("No IO call for {}.".format(name))
return self.__IOCalls[name] return self.__IOCalls[name]
# |\_/|
# /0 0\
# \o/

View file

@ -63,13 +63,14 @@ class NoOpEvent(GameEvent):
class GoEvent(GameEvent): class GoEvent(GameEvent):
def __init__(self, actor, path, speed, action = None, locus = None, timeTaken = 0): def __init__(self, actor, path, speed, action = None, locus = None, closeEnough = True, timeTaken = 0):
super(GoEvent, self).__init__('go') super(GoEvent, self).__init__('go')
self.actor = actor self.actor = actor
self.path = path self.path = path
self.speed = speed self.speed = speed
self.action = action self.action = action
self.locus = locus self.locus = locus
self.closeEnough = closeEnough
self.timeTaken = timeTaken self.timeTaken = timeTaken
@property @property
@ -95,10 +96,10 @@ class UseEvent(GameEvent):
self.args = args self.args = args
class UseOnEvent(GameEvent): class UseOnEvent(GameEvent):
def __init__(self, actor, item, thing, args): def __init__(self, actor, item, target, args):
super(UseOnEvent, self).__init__('useon') super(UseOnEvent, self).__init__('useon')
self.actor = actor self.actor = actor
self.thing = thing # thing can be a coordinate pair? self.target = target # thing can be a coordinate pair?
self.item = item self.item = item
self.args = args self.args = args
@ -118,3 +119,7 @@ class BehaveEvent(GameEvent):
def __init__(self, actor): def __init__(self, actor):
super(BehaveEvent, self).__init__('behave') super(BehaveEvent, self).__init__('behave')
self.actor = actor self.actor = actor
# |\_/|
# /0 0\
# \o/

View file

@ -338,7 +338,6 @@ list of lists of tuples."""
# if startThing and not startThing.passable: # if startThing and not startThing.passable:
# return -1, [], -1 # meaning you can't get there # return -1, [], -1 # meaning you can't get there
dist, prev, endPoint = self.dijkstra(x1, y1, loc, closeEnough) dist, prev, endPoint = self.dijkstra(x1, y1, loc, closeEnough)
#endPoint = self.coordsToInt(x2, y2)
numVertex = self.dimensions[0] * self.dimensions[1] numVertex = self.dimensions[0] * self.dimensions[1]
if endPoint > -1 and dist[endPoint] < numVertex + 1: if endPoint > -1 and dist[endPoint] < numVertex + 1:
pathList = [endPoint] pathList = [endPoint]
@ -356,35 +355,39 @@ list of lists of tuples."""
The closeEnough parameter will create a path that lands beside the source if 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 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.""" to return once it finds a point that's in the locus."""
# first test to see that the start point is passable
startThing = self.getThingAtCoords(x1, y1)
startPoint = self.coordsToInt(x1, y1)
endPoint = -1 # until one matches the locus, which it might not. endPoint = -1 # until one matches the locus, which it might not.
numVertex = self.dimensions[0] * self.dimensions[1] numVertex = self.dimensions[0] * self.dimensions[1]
dist = [numVertex + 1 for i in range(numVertex)] dist = [numVertex + 1 for i in range(numVertex)]
prev = [-1 for i in range(numVertex)] prev = [-1 for i in range(numVertex)]
# validation
if loc == None or not self.__validateCoords(x1, y1):
# start point is out-of-bounds or there is no destination
return dist, prev, endPoint
# first test to see that the start point is passable
startThing = self.getThingAtCoords(x1, y1)
startPoint = self.coordsToInt(x1, y1)
dist[startPoint] = 0 dist[startPoint] = 0
#if closeEnough:
# if startThing and not startThing.passable:
# dist[startPoint] = -1 # This is so it doesn't path into a non-passable end point.
queue = [] queue = []
heapq.heappush(queue, (dist[startPoint], startPoint)) heapq.heappush(queue, (dist[startPoint], startPoint))
while len(queue) > 0: while len(queue) > 0:
u = heapq.heappop(queue)[1] u = heapq.heappop(queue)[1]
if loc != None and self.intToCoords(u) in loc: if loc != None and self.intToCoords(u) in loc:
return dist, prev, u if endPoint == -1 or dist[u] < dist[endPoint]:
endPoint = u
#print(f"endPoint: {endPoint}; Reason: in locus")
for v in self.mapGraph[u]: for v in self.mapGraph[u]:
thing = self.getThingAtPos(v) thing = self.getThingAtPos(v)
if thing and not thing.passable: if thing and not thing.passable:
if closeEnough and self.intToCoords(v) in loc: if closeEnough and self.intToCoords(v) in loc and (endPoint == -1 or dist[u] < dist[endPoint]):
return dist, prev, u endPoint = u
#print(f"endPoint: {endPoint}; Reason: good enough")
else: else:
continue continue
tempDist = dist[u] + 1 tempDist = dist[u] + 1
if tempDist < dist[v]: if tempDist < dist[v]:
dist[v] = tempDist dist[v] = tempDist
if dist[u] != -1: if dist[u] != numVertex + 1:
prev[v] = u prev[v] = u
heapq.heappush(queue, (dist[v], v)) heapq.heappush(queue, (dist[v], v))
@ -396,6 +399,8 @@ to return once it finds a point that's in the locus."""
def lineOfSight(self, x1, y1, x2, y2): def lineOfSight(self, x1, y1, x2, y2):
"""Test for line of signt from one tile to another.""" """Test for line of signt from one tile to another."""
if not (self.__validateCoords(x1, y1) and self.__validateCoords(x2, y2)):
return False
# Trivial case first: # Trivial case first:
if abs(x1 - x2) <= 1 and abs(y1 - y2) <= 1: if abs(x1 - x2) <= 1 and abs(y1 - y2) <= 1:
return True return True
@ -410,10 +415,14 @@ to return once it finds a point that's in the locus."""
return True return True
def isPassable(self, x, y = -1): def isPassable(self, x, y = -1):
pos = x
if y == -1: if y == -1:
if not self.__validatePos(x):
return False
pos = x
x, y = self.intToCoords(x) x, y = self.intToCoords(x)
else: else:
if not self.__validateCoords(x, y):
return False
pos = self.coordsToInt(x, y) pos = self.coordsToInt(x, y)
if self.mapMatrix[y][x][0] == 'w': if self.mapMatrix[y][x][0] == 'w':
return False return False
@ -423,6 +432,12 @@ to return once it finds a point that's in the locus."""
return False return False
return True return True
def __validateCoords(self, x: int, y: int) -> bool:
return x >= 0 and x < self.dimensions[0] and y >= 0 and y < self.dimensions[1]
def __validatePos(self, pos: int) -> bool:
return pos >= 0 and pos < self.dimensions[0] * self.dimensions[1]
@staticmethod @staticmethod
def __coordsToInt(x, y, width): def __coordsToInt(x, y, width):
return x + y * width return x + y * width
@ -633,3 +648,6 @@ class LoSLocus(_gl.Locus):
ret.append(point) ret.append(point)
return iter(ret) return iter(ret)
# |\_/|
# /0 0\
# \o/

View file

@ -1,48 +1,92 @@
# gamesequence.py # gamesequence.py
"""This contains the functions and classes necessary to parse and execute the scripting language.
Classes:
- SequenceError: Derived from RuntimeError.
- ScriptBreak: Represent a 'break' statement.
- ScriptEnvironment: Represent the environment of a script.
Functions:
- getValueFromString: Translate a literal or variable to a value.
- compareValues: compare values for a conditional statement.
- ifScript: Execute an 'if' statement.
- getCustomValue: Get the value of a global variable.
- setCustomValue: Set the value of a global variable.
- delCustomValue: Delete a global variable.
- runScript: Execute a script.
- parseScript: Parse a script.
"""
import re as _re import re as _re
class SequenceError(RuntimeError): class SequenceError(RuntimeError):
"""Derived from RuntimeError.
Raise whenever there is a runtime execution problem while parsing or executing a script.
"""
pass pass
class ScriptBreak(object): class ScriptBreak(object):
"""Class created when a 'break' statement is read."""
def __init__(self, value): def __init__(self, value):
"""Create a script break object with the value returned from the script broken from."""
self.value = value self.value = value
class ScriptEnvironment(object):
"""Represent the environment of a script.
def getValueFromString(arg: str, env: dict): This contains a dictionary of all global variables, as well as local variables."""
#if env == None:
#env = self.customValues def __init__(self, globalVars: dict, localVars: dict):
"""Create a script environment.
Global vars will survive beyond the end of the script.
Local vars are all dropped at the end of a script."""
if not isinstance(globalVars, dict):
raise TypeError("Global variables must be in a dictionary.")
if not isinstance(globalVars, dict):
raise TypeError("Local variables must be in a dictionary.")
self.globalVars = globalVars
self.localVars = localVars
def getValueFromString(arg: str, env: ScriptEnvironment):
"""Translate a literal or variable name into a value.
arg should be a string representing a literal or identifier.
env should be a ScriptEnvironment.
Return the value described by the first literal or variable name."""
val = None val = None
# We test for a valid identifier here, before all the if-elif starts.
validIdent = _re.match(r'[_A-Za-z][_0-9A-Za-z]*', arg) validIdent = _re.match(r'[_A-Za-z][_0-9A-Za-z]*', arg)
if arg[0] in '"\'' and arg[-1] == arg[0]: # assume it's a string if arg[0] in '"\'' and arg[-1] == arg[0]:
# The argument is a string literal.
val = arg[1:-1] val = arg[1:-1]
elif _re.match(r'[+-]?(?:[0-9]*[.])?[0-9]+', arg) != None: elif _re.match(r'[+-]?(?:[0-9]*[.])?[0-9]+', arg) != None:
# The argument is a number.
if '.' in arg: if '.' in arg:
# The argument is a float.
val = float(arg) val = float(arg)
else: else:
# The argument is an int.
val = int(arg) val = int(arg)
elif arg.casefold() == 'true': elif arg.casefold() == 'true':
# The argument is the boolean value 'true'.
val = True val = True
elif arg.casefold() == 'false': elif arg.casefold() == 'false':
# The argument is the boolean value 'false'.
val = 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: elif validIdent != None and env != None:
# The argument is a variable name.
group = validIdent.group() group = validIdent.group()
if 'scriptLocal' in env and group in env['scriptLocal']: if group in env.localVars:
val = env['scriptLocal'][group] # The variable is local.
elif group in env['global']: val = env.localVars[group]
val = env['global'][group] elif group in env.globalVars:
# The variable is global.
val = env.globalVars[group]
else: else:
return False # for if statements; if a variable doesn't exist, should evaluate to False return False # for if statements; if a variable doesn't exist, should evaluate to False
# evaluate all values of all indecies # evaluate all values of all indecies
@ -67,16 +111,19 @@ def getValueFromString(arg: str, env: dict):
return False return False
openBracket = ptr + 1 openBracket = ptr + 1
ptr += 1 ptr += 1
if depth != 0:
#depth should always == 0 at this point
raise SequenceError('Mismatched number of open and close brackets: {}'.format(arg))
else: else:
raise SequenceError('Invalid value syntax: {}'.format(arg)) raise SequenceError('Invalid value syntax: {}'.format(arg))
else: else:
raise SequenceError('Invalid argument to getValueFromString: {}'.format(arg)) raise SequenceError('Invalid argument to getValueFromString: {}'.format(arg))
return val return val
def compareValues(args: list, env: dict): def compareValues(args: list, env: ScriptEnvironment):
"""Generalized comparisons, may eventually be extended to other operators""" """Generalized comparisons, may eventually be extended to other operators"""
if len(args) == 1: if len(args) == 1:
return bool(getValueFromString(args[0]), env) return bool(getValueFromString(args[0], env))
elif len(args) == 3 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'): elif len(args) == 3 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'):
lval = getValueFromString(args[0], env) lval = getValueFromString(args[0], env)
operator = args[1] operator = args[1]
@ -94,14 +141,11 @@ def compareValues(args: list, env: dict):
elif operator == '>': elif operator == '>':
return lval > rval return lval > rval
elif operator == 'in': elif operator == 'in':
#if args[2].casefold() == 'playerinv':
# return lval in self.player.thingNames
#else:
return lval in rval return lval in rval
else: else:
raise SequenceError("Condition cannot be evaluated: {}".format(' '.join(args))) raise SequenceError("Condition cannot be evaluated: {}".format(' '.join(args)))
def ifScript(args, env: dict, externalScripts: dict): def ifScript(args, env: ScriptEnvironment, externalScripts: dict):
"""If statement: if [not] value [op value] script""" """If statement: if [not] value [op value] script"""
if len(args) < 2: if len(args) < 2:
raise GameError('Incomplete If statement: if {}'.format(' '.join(args))) raise GameError('Incomplete If statement: if {}'.format(' '.join(args)))
@ -120,7 +164,7 @@ def ifScript(args, env: dict, externalScripts: dict):
ret = compareValues(args[:3], env) ret = compareValues(args[:3], env)
args = args[3:] args = args[3:]
else: else:
ret = bool(getValueFromString(args[0]), env) ret = bool(getValueFromString(args[0], env))
args = args[1:] args = args[1:]
if inverse: if inverse:
ret = not ret ret = not ret
@ -141,9 +185,9 @@ def setCustomValue(args, env: dict):
"""takes [customValue, op, value]""" """takes [customValue, op, value]"""
if env == None: if env == None:
raise SequenceError("Cannot set a value from an empty environment.") raise SequenceError("Cannot set a value from an empty environment.")
scope = env['scriptLocal'] scope = env.localVars
if len(args) > 0 and args[0] == 'global': if len(args) > 0 and args[0] == 'global':
scope = env['global'] scope = env.globalVars
args.pop(0) args.pop(0)
if len(args) < 3 or args[1] not in ('=', '+=', '-=', '*=', '/=', '%=', '//=', '**=', 'b=', '!=', '|=', '&=', '^='): if len(args) < 3 or args[1] not in ('=', '+=', '-=', '*=', '/=', '%=', '//=', '**=', 'b=', '!=', '|=', '&=', '^='):
raise SequenceError('Arguments are not fit for the setCustomValue script.') raise SequenceError('Arguments are not fit for the setCustomValue script.')
@ -201,18 +245,18 @@ def setCustomValue(args, env: dict):
scope[args[0]] ^= val scope[args[0]] ^= val
return scope[args[0]] return scope[args[0]]
def delCustomValue(args, env: dict): def delCustomValue(args, env: ScriptEnvironment):
"""To clean up after a map.""" """To clean up after a map."""
if env == None: if env == None:
raise SequenceError("Cannot delete a value from an empty environment.") raise SequenceError("Cannot delete a value from an empty environment.")
for i in args: for i in args:
if i in env['global']: if i in env.globalVars:
del(env['global'][i]) del(env.globalVars[i])
_requireEnv = {'get' : getCustomValue, 'set' : setCustomValue, 'del' : delCustomValue} _requireEnv = {'get' : getCustomValue, 'set' : setCustomValue, 'del' : delCustomValue}
_requireScripts = {'if' : ifScript} _requireScripts = {'if' : ifScript}
def runScript(script: list, env: dict, externalScripts: dict): def runScript(script: list, env: ScriptEnvironment, externalScripts: dict):
"""run a script""" """run a script"""
ret = False ret = False
for line in script: for line in script:
@ -228,13 +272,13 @@ def runScript(script: list, env: dict, externalScripts: dict):
ret = _requireEnv[line[0]](line[1:], env) ret = _requireEnv[line[0]](line[1:], env)
elif line[0].casefold() == 'break': elif line[0].casefold() == 'break':
# exit early # exit early
return _ScriptBreak(ret) return ScriptBreak(ret)
elif line[0] in externalScripts: elif line[0] in externalScripts:
# run a script # run a script
ret = externalScripts[line[0]](line[1:]) ret = externalScripts[line[0]](line[1:])
else: else:
# conditional evaluation # conditional evaluation
compareValues(line, env) return compareValues(line, env)
if isinstance(ret, ScriptBreak): if isinstance(ret, ScriptBreak):
return ret return ret
@ -306,9 +350,7 @@ def parseScript(instr: str, envGlobal: dict, externalScripts: dict):
ret.append(''.join(literalStr[argStart:l])) ret.append(''.join(literalStr[argStart:l]))
if len(ret) > 0: if len(ret) > 0:
script.append(ret) script.append(ret)
#print('After parsing: {}'.format(script)) env = ScriptEnvironment(envGlobal, {})
env = {'global' : envGlobal, 'scriptLocal' : {}}
#self.customValues['scriptLocal'] = {}
ret = runScript(script, env, externalScripts) ret = runScript(script, env, externalScripts)
if isinstance(ret, ScriptBreak): if isinstance(ret, ScriptBreak):
ret = ret.value ret = ret.value

View file

@ -3,6 +3,7 @@
from shell import Shell from shell import Shell
from gamebase import GameBase from gamebase import GameBase
import sys as _sys import sys as _sys
import os as _os
#import re #import re
import heapq import heapq
#import gamemap #import gamemap
@ -23,12 +24,12 @@ class GameShell(Shell):
WALLS = ('++', '++', ' +', '++', '++', '||', '++', '|+', '+ ', '++', WALLS = ('++', '++', ' +', '++', '++', '||', '++', '|+', '+ ', '++',
'==', '++', '++', '+|', '++', '++') '==', '++', '++', '+|', '++', '++')
def __init__(self, gameBase): def __init__(self, gameBase, gameData = 'testing/testdata.yml'):
super(GameShell, self).__init__() super(GameShell, self).__init__()
self.outstream = _sys.stdout self.outstream = _sys.stdout
self.gameBase = gameBase self.gameBase = gameBase
self.colorMode = 0 self.colorMode = 0
data = self.gameBase.loadGameData('testing/testdata.yml') data = self.gameBase.loadGameData(gameData)
self.gameTitle = 'Game Shell' # should be changed for actual games self.gameTitle = 'Game Shell' # should be changed for actual games
if 'title' in data: if 'title' in data:
self.gameTitle = data['title'] self.gameTitle = data['title']
@ -38,12 +39,16 @@ class GameShell(Shell):
self.openingText = '{}\nIn Development' self.openingText = '{}\nIn Development'
if 'openingText' in data: if 'openingText' in data:
self.openingText = data['openingText'] self.openingText = data['openingText']
if 'playerName' in data:
self.gameBase.playerName = data['playerName']
if 'playerDescription' in data:
self.gameBase.playerDescription = data['playerDescription']
self.ps2 = '?> ' self.ps2 = '?> '
self.__inGame = False self.__inGame = False
# register functions # register functions
self.registerCommand('load', self.gameBase.loadGame) # should always be available self.registerCommand('load', self.loadGame) # should always be available
self.registerCommand('flippetywick', self.devMode) self.registerCommand('flippetywick', self.devMode)
self.registerCommand('options', self.options) self.registerCommand('options', self.options)
self.registerCommand('colorTest', self.colorTest) self.registerCommand('colorTest', self.colorTest)
@ -62,7 +67,6 @@ class GameShell(Shell):
def man(self, args): def man(self, args):
super(GameShell, self).man(args) super(GameShell, self).man(args)
#heapq.heappush(self.gameBase.eventQueue, (self.gameBase.gameTime, gameevents.NoOpEvent()))
def options(self, args): def options(self, args):
i = 0 i = 0
@ -171,25 +175,26 @@ If -l is given, a map legend will be printed under the map."""
for i in things[1:]: for i in things[1:]:
if priorities[i.thingType] < priorities[thing.thingType]: if priorities[i.thingType] < priorities[thing.thingType]:
thing = i thing = i
if thing.graphic[1] != textColor: if thing.graphic.foreground != textColor:
textColor = thing.graphic[1] textColor = thing.graphic.foreground
#print(textColor)
rows[-1].append(self.color(textColor[1:])) rows[-1].append(self.color(textColor[1:]))
if thing.thingType == 'p': # player if thing.thingType == 'p': # player
rows[-1].append('()') rows[-1].append('()')
elif thing.thingType == 'x': # exit elif thing.thingType == 'x': # exit
rows[-1].append('X{0}'.format(thing.exitid)) rows[-1].append('X{0}'.format(thing.exitid))
exits[thing.exitid] = (thing.name, thing.graphic[1]) exits[thing.exitid] = (thing.name, thing.graphic.foreground)
elif thing.thingType == 'n': # NPC elif thing.thingType == 'n': # NPC
characters[len(characters)+1] = (thing.name, thing.graphic[1]) characters[len(characters)+1] = (thing.name, thing.graphic.foreground)
rows[-1].append('C{0}'.format(len(characters))) rows[-1].append('C{0}'.format(len(characters)))
elif thing.thingType == 'd': # door elif thing.thingType == 'd': # door
doors[len(doors)+1] = (thing.name, thing.graphic[1]) doors[len(doors)+1] = (thing.name, thing.graphic.foreground)
rows[-1].append('D{0}'.format(len(doors))) rows[-1].append('D{0}'.format(len(doors)))
elif thing.thingType == 'u': # useable elif thing.thingType == 'u': # useable
useables[len(useables)+1] = (thing.name, thing.graphic[1]) useables[len(useables)+1] = (thing.name, thing.graphic.foreground)
rows[-1].append('U{0}'.format(len(useables))) rows[-1].append('U{0}'.format(len(useables)))
elif thing.thingType == 'i': # item elif thing.thingType == 'i': # item
items[len(items)+1] = (thing.name, thing.graphic[1]) items[len(items)+1] = (thing.name, thing.graphic.foreground)
rows[-1].append('I{0}'.format(len(items))) rows[-1].append('I{0}'.format(len(items)))
else: # entrance else: # entrance
rows[-1].append(' ') rows[-1].append(' ')
@ -254,7 +259,7 @@ If -l is given, a map legend will be printed under the map."""
ret.append("Gametime:{0:.>71.3}".format(self.gameBase.gameTime)) ret.append("Gametime:{0:.>71.3}".format(self.gameBase.gameTime))
ret.append("Event queue:") ret.append("Event queue:")
for i in sorted(self.gameBase.eventQueue): for i in sorted(self.gameBase.eventQueue):
ret.append("{0:.<8.3}:{1:.>72}".format(i[0], str(i[1]))) ret.append("{0:.<8.3}:{1:.>71}".format(i[0], str(i[1])))
ret.append("custom values:") ret.append("custom values:")
for i in self.gameBase.customValues: for i in self.gameBase.customValues:
ret.append("{0:<22}: {1}".format(i, self.gameBase.customValues[i])) ret.append("{0:<22}: {1}".format(i, self.gameBase.customValues[i]))
@ -283,19 +288,19 @@ If -l is given, a map legend will be printed under the map."""
response = input("Do you want to save before exiting (Y/n/x)? ") response = input("Do you want to save before exiting (Y/n/x)? ")
if len(response) > 0: if len(response) > 0:
if response[0] in 'Nn': if response[0] in 'Nn':
super(GameShell, self).exitShell(args) pass
elif response[0] in 'Xx': # cancel elif response[0] in 'Xx': # cancel
return return
else: else:
sf = input('Save file: ') sf = input('Save file: ')
if len(sf) > 0: if len(sf) > 0:
gameBase.saveGame([sf]) self.gameBase.saveGame([sf])
else: else:
print('No save file given, cancelling exit.') print('No save file given, cancelling exit.')
else: else:
sf = input('Save file: ') sf = input('Save file: ')
if len(sf) > 0: if len(sf) > 0:
gameBase.saveGame([sf]) self.gameBase.saveGame([sf])
else: else:
print('No save file given, cancelling exit.') print('No save file given, cancelling exit.')
try: try:
@ -305,6 +310,43 @@ If -l is given, a map legend will be printed under the map."""
return return
self.gameMode() self.gameMode()
def loadGame(self, args):
# Check if there's a name in args. If not, just present a list of save files.
if len(args) == 0:
import pickle as _pi
for fileName in _os.listdir('saves'):
with open(f'saves/{fileName}', 'rb') as f:
player, levelname, persist, eventQueue, customValues, gameTime, nextThing = _pi.load(f)
print(f'{fileName[:-4]}\n - {levelname[5:-4]}')
return
if self.__inGame:
response = input("Do you want to save before exiting (Y/n/x)? ")
if len(response) > 0:
if response[0] in 'Nn':
pass
elif response[0] in 'Xx': # cancel
return
else:
sf = input('Save file: ')
if len(sf) > 0:
self.gameBase.saveGame([sf])
else:
print('No save file given, cancelling exit.')
return
else:
sf = input('Save file: ')
if len(sf) > 0:
self.gameBase.saveGame([sf])
else:
print('No save file given, cancelling exit.')
return
try:
self.gameBase.loadGame(args)
except RuntimeError as e:
print(e)
return
self.gameMode()
def gameMode(self): def gameMode(self):
"""Mode for in-game.""" """Mode for in-game."""
if not self.__inGame: if not self.__inGame:
@ -366,7 +408,7 @@ If -l is given, a map legend will be printed under the map."""
# IO calls # IO calls
def container(self, player, cont): def container(self, player, cont: list):
"""container IO """container IO
Player is modified through side-effect.""" Player is modified through side-effect."""
# Pretty print: get length of the longest inventory item's name # Pretty print: get length of the longest inventory item's name
@ -393,36 +435,64 @@ Player is modified through side-effect."""
print(i.name) print(i.name)
# Now, actually interacting with the container # Now, actually interacting with the container
timeSpent = 0.5 # using a container always takes at least 1/2 second, even just opening and closing it again. timeSpent = 0.5 # using a container always takes at least 1/2 second, even just opening and closing it again.
instr = input("Take, store, or exit: ") instr = input("Look, take, store, or exit: ")
while instr != "exit": while instr != "exit":
instr = instr.split() instr = instr.split()
if instr[0] == "take": if len(instr) != 0:
# take something out of the container if instr[0] == "take":
if instr[1] == "the": # take something out of the container
del instr[1] if len(instr) > 1 and instr[1] == "the":
thing = ' '.join(instr[1:]) del instr[1]
for i in range(len(cont)): thingName = ' '.join(instr[1:])
if thing == cont[i].name: if len(thingName) == 0:
player.addThing(cont[i]) print(f"{self.gameBase.player.name} takes nothing.")
del cont[i] else:
for i in range(len(cont)):
if thingName == cont[i].name:
player.addThing(cont[i])
del cont[i]
timeSpent += 0.5
print(f"{thingName} taken.")
break
else:
# If it got here, it didn't find it.
print(f"No {thingName} in container.")
elif instr[0] == "store":
# store something in the container
if len(instr) > 1 and instr[1] == "the":
del instr[1]
thingName = ' '.join(instr[1:])
if len(thingName) == 0:
print(f"{self.gameBase.player.name} stores nothing.")
elif thingName in player.thingNames:
cont.append(player.removeThingByName(thingName))
print(f"{thingName} stored.")
timeSpent += 0.5 timeSpent += 0.5
print("{0} taken.".format(thing)) else:
break print(f"No {thingName} in inventory.")
else: elif instr[0] == "look":
# If it got here, it didn't find it. # look at something in the container
print("No {0} in container.".format(thing)) if len(instr) > 1 and instr[1] == "at":
elif instr[0] == "store": del instr[1]
# store something in the container if len(instr) > 1 and instr[1] == "the":
if instr[1] == "the": del instr[1]
del instr[1] thingName = ' '.join(instr[1:])
thingName = ' '.join(instr[1:]) if len(thingName) == 0:
if thingName in player.thingNames: print(f"{self.gameBase.player.name} looks at nothing.")
cont.append(player.removeThingByName(thingName)) else:
print("{0} stored.".format(thingName)) # Check the container first.
timeSpent += 0.5 for i in range(len(cont)):
else: if thingName == cont[i].name:
print("No {0} in inventory.".format(thingName)) print(self.gameBase.justifyText(str(cont[i])))
instr = input("Take, store, or exit: ") break
else:
# If it wasn't there, try the player's inventory.
if thingName in player.thingNames:
print(self.gameBase.justifyText(str(player.getThingByName(thingName))))
else:
# If we get here, it just isn't there.
print(f"There is no {thingName} to look at.")
instr = input("Look, take, store, or exit: ")
return cont, timeSpent return cont, timeSpent
def info(self, items): def info(self, items):
@ -456,7 +526,11 @@ Player is modified through side-effect."""
print(_tw.fill('{}: {}'.format(lineNo+1, options[lineNo]), width = TERM_SIZE)) print(_tw.fill('{}: {}'.format(lineNo+1, options[lineNo]), width = TERM_SIZE))
answer = -1 answer = -1
while answer < 0 or answer >= len(options): while answer < 0 or answer >= len(options):
answer = int(input(self.ps2)) - 1 answerString = input(self.ps2)
if not answerString.isdigit():
# If the player inputs a non-integer, just prompt again.
continue
answer = int(answerString) - 1
return answer return answer
def endDialog(self): def endDialog(self):
@ -479,14 +553,14 @@ Player is modified through side-effect."""
else: else:
sf = input('Save file: ') sf = input('Save file: ')
if len(sf) > 0: if len(sf) > 0:
gameBase.saveGame([sf]) self.gameBase.saveGame([sf])
super(GameShell, self).exitShell(args) super(GameShell, self).exitShell(args)
else: else:
print('No save file given, cancelling exit.') print('No save file given, cancelling exit.')
else: else:
sf = input('Save file: ') sf = input('Save file: ')
if len(sf) > 0: if len(sf) > 0:
gameBase.saveGame([sf]) self.gameBase.saveGame([sf])
super(GameShell, self).exitShell(args) super(GameShell, self).exitShell(args)
else: else:
print('No save file given, cancelling exit.') print('No save file given, cancelling exit.')
@ -497,3 +571,7 @@ if __name__ == '__main__':
sh = GameShell(GameBase()) sh = GameShell(GameBase())
sh.menuMode() sh.menuMode()
sh.run() sh.run()
# |\_/|
# /0 0\
# \o/

View file

@ -1,8 +1,51 @@
#gamethings.py #gamethings.py
"""Standard thing classes.
Classes:
- ThingGraphic: Represents how thing is to be visualized.
- Thing: The abstract base class for every thing.
- Observer: The abstract base class for things that listen to events.
- Item: A thing that can exist in a character's inventory.
- Useable: A thing that can be used by a character.
- Character: A thing that represents a character.
- NPC: A character that is not controlled by the player.
- Door: A thing that sometimes blocks paths.
- MapExit: A technical thing that marks a map transition.
"""
import ruamel.yaml import ruamel.yaml
from ruamel.yaml.comments import CommentedMap from ruamel.yaml.comments import CommentedMap
class ThingGraphic(object):
"""Represent how a thing is to be visualized.
Background color, foreground color, and shape are represented.
This could hypothetically be extended to hold sprites, textures or models."""
def __init__(self, background: str, foreground: str, shape: str):
"""Create a graphic for a thing.
The background and foregrond must be strings with color hex representations.
For instance: '#00FF00' means bright green.
The shape must be a string containing exactly one of the following shapes:
o: circle
x: cross
-: horizontal line
|: vertical line
#: square
^: triangle
A graphic may be invalid.
"""
if not isinstance(background, str):
raise TypeError("Background must be a string of form '#[0-9A-Fa-f]{6}'.")
if not isinstance(foreground, str):
raise TypeError("Foreground must be a string of form '#[0-9A-Fa-f]{6}'.")
if not isinstance(shape, str):
raise TypeError("Shape must be a string of form '[-|ox#^]'.")
self.background = background
self.foreground = foreground
self.shape = shape
class Thing(object): class Thing(object):
def __init__(self, thingType: str, name: str, x: int, y: int, description: str, flags: int, playerx = None, playery = None, **kwargs): def __init__(self, thingType: str, name: str, x: int, y: int, description: str, flags: int, playerx = None, playery = None, **kwargs):
@ -24,7 +67,7 @@ class Thing(object):
self.lookable = bool(flags & 4) self.lookable = bool(flags & 4)
self.takeable = bool(flags & 8) self.takeable = bool(flags & 8)
self.useable = bool(flags & 16) self.useable = bool(flags & 16)
self.graphic = ('clear', '#7F7F7F', ' ') self.graphic = ThingGraphic('clear', '#7F7F7F', ' ')
self.thingID = -1 # ID not assigned self.thingID = -1 # ID not assigned
def __str__(self): def __str__(self):
@ -49,7 +92,7 @@ class Observer(Thing):
class Item(Thing): class Item(Thing):
yaml_flag = u'!Item' yaml_flag = u'!Item'
defaultGraphic = ('clear', '#00BF00', '^') defaultGraphic = ThingGraphic('clear', '#00BF00', '^')
def __init__(self, name, x: int, y: int, description: str, useFunc: str, useOnFunc: str, customValues: dict, ranged: bool, graphic = defaultGraphic): 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) super(Item, self).__init__('i', name, x, y, description, 13)
@ -68,12 +111,12 @@ class Item(Thing):
ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description} ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description}
# save graphic # save graphic
graphic = {} graphic = {}
if node.graphic[0] != Item.defaultGraphic[0]: if node.graphic.background != Item.defaultGraphic.background:
graphic['bgc'] = node.graphic[0] graphic['bgc'] = node.graphic.background
if node.graphic[1] != Item.defaultGraphic[1]: if node.graphic.foreground != Item.defaultGraphic.forground:
graphic['fgc'] = node.graphic[1] graphic['fgc'] = node.graphic.foreground
if node.graphic[2] != Item.defaultGraphic[2]: if node.graphic.shape != Item.defaultGraphic.shape:
graphic['shape'] = node.graphic[2] graphic['shape'] = node.graphicshape
if len(graphic) > 0: if len(graphic) > 0:
ret['graphic'] = graphic ret['graphic'] = graphic
# save use functions # save use functions
@ -96,9 +139,9 @@ class Item(Thing):
useOnFunc = '' useOnFunc = ''
customValues = {} customValues = {}
ranged = False ranged = False
bgc = Item.defaultGraphic[0] bgc = Item.defaultGraphic.background
fgc = Item.defaultGraphic[1] fgc = Item.defaultGraphic.foreground
shape = Item.defaultGraphic[2] shape = Item.defaultGraphic.shape
# load graphic # load graphic
if 'graphic' in parts: if 'graphic' in parts:
if 'bgc' in parts['graphic']: if 'bgc' in parts['graphic']:
@ -107,7 +150,7 @@ class Item(Thing):
fgc = parts['graphic']['fgc'] fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']: if 'shape' in parts['graphic']:
shape = parts['graphic']['shape'] shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape) graphic = ThingGraphic(bgc, fgc, shape)
# load use functions # load use functions
if 'useFunc' in parts: if 'useFunc' in parts:
useFunc = parts['useFunc'] useFunc = parts['useFunc']
@ -131,7 +174,7 @@ class Item(Thing):
class Useable(Thing): class Useable(Thing):
yaml_flag = u'!Useable' yaml_flag = u'!Useable'
defaultGraphic = ('clear', '#0000FF', '#') defaultGraphic = ThingGraphic('clear', '#0000FF', '#')
def __init__(self, name, x: int, y: int, description: str, useFunc: str, customValues: dict, playerx = None, playery = None, graphic = defaultGraphic): 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) super(Useable, self).__init__('u', name, x, y, description, 16, playerx, playery)
@ -145,12 +188,12 @@ class Useable(Thing):
ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description} ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description}
# save graphic # save graphic
graphic = {} graphic = {}
if node.graphic[0] != Useable.defaultGraphic[0]: if node.graphic.background != Useable.defaultGraphic.background:
graphic['bgc'] = node.graphic[0] graphic['bgc'] = node.graphic.background
if node.graphic[1] != Useable.defaultGraphic[1]: if node.graphic.foreground != Useable.defaultGraphic.foreground:
graphic['fgc'] = node.graphic[1] graphic['fgc'] = node.graphic.foreground
if node.graphic[2] != Useable.defaultGraphic[2]: if node.graphic.shape != Useable.defaultGraphic.shape:
graphic['shape'] = node.graphic[2] graphic['shape'] = node.graphic.shape
if len(graphic) > 0: if len(graphic) > 0:
ret['graphic'] = graphic ret['graphic'] = graphic
# save use functions # save use functions
@ -170,9 +213,9 @@ class Useable(Thing):
useFunc = '' useFunc = ''
customValues = {} customValues = {}
playerx, playery = parts['location'] playerx, playery = parts['location']
bgc = Useable.defaultGraphic[0] bgc = Useable.defaultGraphic.background
fgc = Useable.defaultGraphic[1] fgc = Useable.defaultGraphic.foreground
shape = Useable.defaultGraphic[2] shape = Useable.defaultGraphic.shape
# load graphic # load graphic
if 'graphic' in parts: if 'graphic' in parts:
if 'bgc' in parts['graphic']: if 'bgc' in parts['graphic']:
@ -181,7 +224,7 @@ class Useable(Thing):
fgc = parts['graphic']['fgc'] fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']: if 'shape' in parts['graphic']:
shape = parts['graphic']['shape'] shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape) graphic = ThingGraphic(bgc, fgc, shape)
# load use functions # load use functions
if 'useFunc' in parts: if 'useFunc' in parts:
useFunc = parts['useFunc'] useFunc = parts['useFunc']
@ -199,7 +242,7 @@ class Useable(Thing):
pass pass
class Character(Thing): class Character(Thing):
defaultGraphic = ('clear', '#000000', 'o') defaultGraphic = ThingGraphic('clear', '#000000', 'o')
def __init__(self, inventory: dict, customValues: dict, graphic = defaultGraphic, **kwargs): def __init__(self, inventory: dict, customValues: dict, graphic = defaultGraphic, **kwargs):
super(Character, self).__init__(**kwargs) super(Character, self).__init__(**kwargs)
@ -211,7 +254,7 @@ class Character(Thing):
self.__inventory = inventory self.__inventory = inventory
self.customValues = customValues self.customValues = customValues
self.graphic = graphic self.graphic = graphic
self.thingNames = {} self.thingNames = {} #{str: int}
# set up inventory shtuff # set up inventory shtuff
for i in self.__inventory: for i in self.__inventory:
if self.__inventory[i].name in self.thingNames: if self.__inventory[i].name in self.thingNames:
@ -229,7 +272,10 @@ class Character(Thing):
self.thingNames[thing.name] = [thing.thingID] self.thingNames[thing.name] = [thing.thingID]
def getThingByID(self, thingID): def getThingByID(self, thingID):
return self.__inventory[thingID] if thingID in self.__inventory:
return self.__inventory[thingID]
else:
return None
def getThingByName(self, name): def getThingByName(self, name):
if name in self.thingNames: if name in self.thingNames:
@ -247,10 +293,10 @@ class Character(Thing):
def removeThingByName(self, name): def removeThingByName(self, name):
ret = self.getThingByName(name) ret = self.getThingByName(name)
self.thingNames[ret.name].remove(thingID) self.thingNames[ret.name].remove(ret.thingID)
if len(self.thingNames[ret.name]) == 0: if len(self.thingNames[ret.name]) == 0:
del self.thingNames[ret.name] del self.thingNames[ret.name]
del self.__inventory[thingID] del self.__inventory[ret.thingID]
return ret return ret
def removeThing(self, ret): def removeThing(self, ret):
@ -267,7 +313,7 @@ class Character(Thing):
class NPC(Character, Observer): class NPC(Character, Observer):
yaml_flag = u'!NPC' yaml_flag = u'!NPC'
defaultGraphic = ('clear', '#000000', 'o') defaultGraphic = ThingGraphic('clear', '#000000', 'o')
def __init__(self, behaviors: dict, tempInventory: list, **kwargs): def __init__(self, behaviors: dict, tempInventory: list, **kwargs):
if 'graphic' not in kwargs: if 'graphic' not in kwargs:
@ -284,12 +330,12 @@ class NPC(Character, Observer):
'description': node.description, 'behaviors': node.behaviors} 'description': node.description, 'behaviors': node.behaviors}
# save graphic # save graphic
graphic = {} graphic = {}
if node.graphic[0] != NPC.defaultGraphic[0]: if node.graphic.background != Useable.defaultGraphic.background:
graphic['bgc'] = node.graphic[0] graphic['bgc'] = node.graphic.background
if node.graphic[1] != NPC.defaultGraphic[1]: if node.graphic.foreground != Useable.defaultGraphic.foreground:
graphic['fgc'] = node.graphic[1] graphic['fgc'] = node.graphic.foreground
if node.graphic[2] != NPC.defaultGraphic[2]: if node.graphic.shape != Useable.defaultGraphic.shape:
graphic['shape'] = node.graphic[2] graphic['shape'] = node.graphic.shape
if len(graphic) > 0: if len(graphic) > 0:
ret['graphic'] = graphic ret['graphic'] = graphic
# save use functions # save use functions
@ -310,9 +356,9 @@ class NPC(Character, Observer):
minventory = [] minventory = []
mcustomValues = {} mcustomValues = {}
mplayerx, mplayery = parts['location'] mplayerx, mplayery = parts['location']
bgc = NPC.defaultGraphic[0] bgc = NPC.defaultGraphic.background
fgc = NPC.defaultGraphic[1] fgc = NPC.defaultGraphic.foreground
shape = NPC.defaultGraphic[2] shape = NPC.defaultGraphic.shape
# load graphic # load graphic
if 'graphic' in parts: if 'graphic' in parts:
if 'bgc' in parts['graphic']: if 'bgc' in parts['graphic']:
@ -321,7 +367,7 @@ class NPC(Character, Observer):
fgc = parts['graphic']['fgc'] fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']: if 'shape' in parts['graphic']:
shape = parts['graphic']['shape'] shape = parts['graphic']['shape']
mgraphic = (bgc, fgc, shape) mgraphic = ThingGraphic(bgc, fgc, shape)
# load use functions # load use functions
if 'inventory' in parts: if 'inventory' in parts:
inventory = parts['inventory'] inventory = parts['inventory']
@ -338,7 +384,7 @@ class NPC(Character, Observer):
class Door(Thing): class Door(Thing):
yaml_flag = u'!Door' yaml_flag = u'!Door'
defaultGraphic = ('clear', '#7F3F00', '#') defaultGraphic = ThingGraphic('clear', '#7F3F00', '#')
def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = defaultGraphic): def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = defaultGraphic):
self.descBase = description self.descBase = description
@ -379,12 +425,12 @@ class Door(Thing):
ret = {'name': node.name, 'location': (node.x, node.y)} ret = {'name': node.name, 'location': (node.x, node.y)}
# save graphic # save graphic
graphic = {} graphic = {}
if node.graphic[0] != Door.defaultGraphic[0]: if node.graphic.background != Useable.defaultGraphic.background:
graphic['bgc'] = node.graphic[0] graphic['bgc'] = node.graphic.background
if node.graphic[1] != Door.defaultGraphic[1]: if node.graphic.foreground != Useable.defaultGraphic.foreground:
graphic['fgc'] = node.graphic[1] graphic['fgc'] = node.graphic.foreground
if node.graphic[2] != Door.defaultGraphic[2]: if node.graphic.shape != Useable.defaultGraphic.shape:
graphic['shape'] = node.graphic[2] graphic['shape'] = node.graphic.shape
if len(graphic) > 0: if len(graphic) > 0:
ret['graphic'] = graphic ret['graphic'] = graphic
# save door state # save door state
@ -404,9 +450,9 @@ class Door(Thing):
description = None description = None
locked = False locked = False
key = None key = None
bgc = Door.defaultGraphic[0] bgc = Door.defaultGraphic.background
fgc = Door.defaultGraphic[1] fgc = Door.defaultGraphic.foreground
shape = Door.defaultGraphic[2] shape = Door.defaultGraphic.shape
# load graphic # load graphic
if 'graphic' in parts: if 'graphic' in parts:
if 'bgc' in parts['graphic']: if 'bgc' in parts['graphic']:
@ -415,7 +461,7 @@ class Door(Thing):
fgc = parts['graphic']['fgc'] fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']: if 'shape' in parts['graphic']:
shape = parts['graphic']['shape'] shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape) graphic = ThingGraphic(bgc, fgc, shape)
# load door state # load door state
if 'description' in parts: if 'description' in parts:
description = parts['description'] description = parts['description']
@ -428,7 +474,7 @@ class Door(Thing):
class MapExit(Thing): class MapExit(Thing):
yaml_flag = u'!MapExit' yaml_flag = u'!MapExit'
defaultGraphic = ('clear', '#FF0000', 'x') defaultGraphic = ThingGraphic('clear', '#FF0000', 'x')
def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix = None, onUse = '', key = True, graphic = defaultGraphic): def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix = None, onUse = '', key = True, graphic = defaultGraphic):
description = name description = name
@ -448,12 +494,12 @@ class MapExit(Thing):
ret = {'name': node.name, 'location': (node.x, node.y), 'id': node.exitid, 'destination': node.destination} ret = {'name': node.name, 'location': (node.x, node.y), 'id': node.exitid, 'destination': node.destination}
# save graphic # save graphic
graphic = {} graphic = {}
if node.graphic[0] != MapExit.defaultGraphic[0]: if node.graphic.background != Useable.defaultGraphic.background:
graphic['bgc'] = node.graphic[0] graphic['bgc'] = node.graphic.background
if node.graphic[1] != MapExit.defaultGraphic[1]: if node.graphic.foreground != Useable.defaultGraphic.foreground:
graphic['fgc'] = node.graphic[1] graphic['fgc'] = node.graphic.foreground
if node.graphic[2] != MapExit.defaultGraphic[2]: if node.graphic.shape != Useable.defaultGraphic.shape:
graphic['shape'] = node.graphic[2] graphic['shape'] = node.graphic.shape
if len(graphic) > 0: if len(graphic) > 0:
ret['graphic'] = graphic ret['graphic'] = graphic
if node.prefix != None: if node.prefix != None:
@ -472,9 +518,9 @@ class MapExit(Thing):
prefix = None prefix = None
onUse = '' onUse = ''
key = True key = True
bgc = MapExit.defaultGraphic[0] bgc = MapExit.defaultGraphic.background
fgc = MapExit.defaultGraphic[1] fgc = MapExit.defaultGraphic.foreground
shape = MapExit.defaultGraphic[2] shape = MapExit.defaultGraphic.shape
# load graphic # load graphic
if 'graphic' in parts: if 'graphic' in parts:
if 'bgc' in parts['graphic']: if 'bgc' in parts['graphic']:
@ -483,7 +529,7 @@ class MapExit(Thing):
fgc = parts['graphic']['fgc'] fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']: if 'shape' in parts['graphic']:
shape = parts['graphic']['shape'] shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape) graphic = ThingGraphic(bgc, fgc, shape)
if 'prefix' in parts: if 'prefix' in parts:
prefix = parts['prefix'] prefix = parts['prefix']
if 'onUse' in parts: if 'onUse' in parts:
@ -495,7 +541,7 @@ class MapExit(Thing):
class MapEntrance(Thing): class MapEntrance(Thing):
yaml_flag = u'!MapEntrance' yaml_flag = u'!MapEntrance'
defaultGraphic = ('clear', 'clear', 'x') defaultGraphic = ThingGraphic('clear', 'clear', 'x')
# no graphic - should not be drawn # no graphic - should not be drawn
def __init__(self, x: int, y: int, exitid: int, name = None): def __init__(self, x: int, y: int, exitid: int, name = None):
@ -503,6 +549,7 @@ class MapEntrance(Thing):
name = 'entrance {}'.format(exitid) name = 'entrance {}'.format(exitid)
super(MapEntrance, self).__init__('a', name, x, y, '', 1) super(MapEntrance, self).__init__('a', name, x, y, '', 1)
self.exitid = exitid self.exitid = exitid
#self.graphic = MapEntrance.defaultGraphic
@classmethod @classmethod
def to_yaml(cls, representer, node): def to_yaml(cls, representer, node):
@ -519,7 +566,7 @@ class MapEntrance(Thing):
class PlayerCharacter(Character): class PlayerCharacter(Character):
"""Player object. Cannot be created with yaml.""" """Player object. Cannot be created with yaml."""
defaultGraphic = ('clear', '#0000FF', 'o') defaultGraphic = ThingGraphic('clear', '#0000FF', 'o')
def __init__(self, **kwargs): def __init__(self, **kwargs):
if 'name' not in kwargs: if 'name' not in kwargs:

View file

@ -46,6 +46,8 @@ def parseCoords(level, args, usePlayerCoords = True, player = None):
Returns (thing, x, y). "Thing" can be None.""" Returns (thing, x, y). "Thing" can be None."""
if level == None: if level == None:
raise ValueError('Coordinates cannot be parsed because there is no map.') raise ValueError('Coordinates cannot be parsed because there is no map.')
if len(args) == 0:
return None, -1, -1
x = -1 x = -1
y = -1 y = -1

View file

@ -133,7 +133,10 @@ class Shell(object):
self.__exit = False self.__exit = False
def man(self, args): def man(self, args):
help(self.__commands[args[0]]) if (len(args)):
help(self.__commands[args[0]])
else:
print("Usage: help <commamd>\nCommands:\n\t{}".format('\n\t'.join(list(self.__commands.keys()))))
def registerCommand(self, commandName: str, command: _types.FunctionType): def registerCommand(self, commandName: str, command: _types.FunctionType):
"""command must be a function that takes one argument: a list of strings, """command must be a function that takes one argument: a list of strings,
@ -288,10 +291,10 @@ $>=0 - $>=n is the nth and later arguments"""
else: else:
self.handleUnknownCommand(command) self.handleUnknownCommand(command)
def handleUnknownCommand(self, command): def handleUnknownCommand(self, command: list):
"""Handle commands that aren't registered. Override this if you want to do """Handle commands that aren't registered. Override this if you want to do
something with those commands.""" something with those commands."""
print("Bad command.") print(f"Unknown command: {' '.join(command)}")
def update(self): def update(self):
"""Runs at the end of each loop. Does nothing by default. Override this if """Runs at the end of each loop. Does nothing by default. Override this if

View file

@ -51,6 +51,16 @@ loadAlways:
none: none # might this work to prevent this character from doing anything? none: none # might this work to prevent this character from doing anything?
customValues: customValues:
dialogs: testing/testDialog.yml dialogs: testing/testDialog.yml
- !Singleton loadOnce:
- !NPC
name: follower name: follower
description: a follower
location: [6, 26] location: [6, 26]
behaviors:
go: [-1, follow]
arrive: [-1, follow]
customValues:
follow:
distance: 2
isFollowing: True
target: You

View file

@ -3,17 +3,17 @@
title: Sample Text title: Sample Text
openingText: "{}\nsample text" openingText: "{}\nsample text"
startLevel: testing/test1.yml startLevel: testing/test1.yml
singletons: #singletons:
- !NPC # - !NPC
name: follower # name: follower
description: a follower # description: a follower
location: [0, 0] # location: [0, 0]
behaviors: # behaviors:
go: [-1, follow] # go: [-1, follow]
arrive: [-1, follow] # arrive: [-1, follow]
customValues: # customValues:
follow: # follow:
distance: 2 # distance: 2
isFollowing: True # isFollowing: True
target: You # target: You