Added loci, and made dialog processing internal to the engine.

This commit is contained in:
Patrick Marsee 2019-10-11 14:35:39 -04:00
parent 5010042430
commit 9cda61a895
11 changed files with 1860 additions and 1269 deletions

File diff suppressed because it is too large Load diff

View file

@ -63,30 +63,41 @@ class NoOpEvent(GameEvent):
class GoEvent(GameEvent): class GoEvent(GameEvent):
def __init__(self, actor, x, y): def __init__(self, actor, path, speed, action = None, locus = None, timeTaken = 0):
super(GoEvent, self).__init__('go') super(GoEvent, self).__init__('go')
self.actor = actor self.actor = actor
self.x = x self.path = path
self.y = y self.speed = speed
self.action = action
self.locus = locus
self.timeTaken = timeTaken
@property
def pos(self):
return self.path[0]
class ArriveEvent(GameEvent): class ArriveEvent(GameEvent):
def __init__(self, actor, x, y, t): def __init__(self, actor, pos, speed, action = None, locus = None, timeTaken = 0):
super(ArriveEvent, self).__init__('arrive') super(ArriveEvent, self).__init__('arrive')
self.actor = actor self.actor = actor
self.x = x self.pos = pos
self.y = y self.speed = speed
self.t = t self.action = action
self.locus = locus
self.timeTaken = timeTaken
class UseEvent(GameEvent): class UseEvent(GameEvent):
def __init__(self, thing, args): def __init__(self, actor, thing, args):
super(UseEvent, self).__init__('use') super(UseEvent, self).__init__('use')
self.actor = actor
self.thing = thing self.thing = thing
self.args = args self.args = args
class UseOnEvent(GameEvent): class UseOnEvent(GameEvent):
def __init__(self, item, thing, args): def __init__(self, actor, item, thing, args):
super(UseOnEvent, self).__init__('useon') super(UseOnEvent, self).__init__('useon')
self.actor = actor
self.thing = thing # thing can be a coordinate pair? self.thing = thing # thing can be a coordinate pair?
self.item = item self.item = item
self.args = args self.args = args

336
gamelocus.py Normal file
View file

@ -0,0 +1,336 @@
# gamelocus.py
import math as _mt
class Locus(object):
"""Abstract base class for locus objects."""
def __init__(self, **kwargs):
"""Base locus constructor."""
pass
def __contains__(self, item):
"""Is this point within the locus?"""
return False
def __iter__(self):
return iter(())
def __repr__(self):
return "Locus()"
class PointLocus(Locus):
"""A locus that defines a single tile."""
def __init__(self, x, y, **kwargs):
"""Take the coordinates for a point."""
self.x = x
self.y = y
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
return item[0] == self.x and item[1] == self.y
def __iter__(self):
return iter(((self.x, self.y),)) # just an iterator of the one tile
def __repr__(self):
return "PointLocus({}, {})".format(self.x, self.y)
class LineLocus(Locus):
"""A locus that defines all square tiles that intersect a given line segment."""
def __init__(self, x1, y1, x2, y2, thick = False, **kwargs):
"""Take the coordinates for the two endpoints."""
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.thick = thick
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
# Is it outside the bounding box for this line?
if x > max(self.x1, self.x2) or x < min(self.x1, self.x2) or y > max(self.y1, self.y2) or y < min(self.y1, self.y2):
return False
# Is this line straight up and down, or left and right?
elif self.x1 == self.x2 or self.y1 == self.y2:
# In this case, the line takes up the whole bounding box, so it must
# be true since it passed the first step to get here.
return True
else:
slope = (self.y2 - self.y1) / (self.x2 - self.x1)
intercept = (self.y1 + 0.5) - (self.x1 + 0.5) * slope
f = lambda z: z * slope + intercept
if self.thick:
if slope > 0:
return _mt.ceil(f(x)-1) <= y and _mt.floor(f(x+1)+1) > y
else:
return _mt.floor(f(x)+1) > y and _mt.ceil(f(x+1)-1) <= y
else:
if slope > 0:
return _mt.floor(f(x)) <= y and _mt.ceil(f(x+1)) > y
else:
return _mt.ceil(f(x)) > y and _mt.floor(f(x+1)) <= y
def __iter__(self):
# avoid infinite slope case
if self.x1 == self.x2:
miny, maxy = min(self.y1, self.y2), max(self.y1, self.y2) + 1
ret = [(self.x1, y) for y in range(miny, maxy)]
return iter(ret)
# for convenience: it's easy to calculate if it's horizontal too.
elif self.y1 == self.y2:
minx, maxx = min(self.x1, self.x2), max(self.x1, self.x2) + 1
ret = [(x, self.y1) for x in range(minx, maxx)]
return iter(ret)
else:
slope = (self.y2 - self.y1) / (self.x2 - self.x1)
intercept = (self.y1 + 0.5) - (self.x1 + 0.5) * slope
f = lambda x: x * slope + intercept
lx, ly = min((self.x1, self.y1), (self.x2, self.y2))
rx, ry = max((self.x1, self.y1), (self.x2, self.y2))
ret = []
# edge case 1: the first half column
if slope > 0:
maxy = _mt.ceil(f(lx+1))
if self.thick:
maxy = _mt.floor(f(lx+1)+1)
for y in range(ly, maxy):
ret.append((lx, y))
else:
maxy = _mt.floor(f(lx+1))-1
if self.thick:
maxy = _mt.ceil(f(lx+1)-2)
for y in range(ly, maxy, -1):
ret.append((lx, y))
# Usual case: the line between the end points
for x in range(lx+1, rx):
if slope > 0:
miny = _mt.floor(f(x))
maxy = _mt.ceil(f(x+1))
if self.thick:
miny = _mt.ceil(f(x)-1)
maxy = _mt.floor(f(x+1)+1)
for y in range(miny, maxy):
ret.append((x, y))
else:
miny = _mt.ceil(f(x)-1)
maxy = _mt.floor(f(x+1))-1
if self.thick:
miny = _mt.floor(f(x))
maxy = _mt.ceil(f(x+1)-2)
for y in range(miny, maxy, -1):
ret.append((x, y))
# edge case 2: the last half column
if slope > 0:
miny = _mt.floor(f(rx))
if self.thick:
miny = _mt.ceil(f(rx)-1)
for y in range(miny, ry+1):
ret.append((rx, y))
else:
miny = _mt.ceil(f(rx)-1)
if self.thick:
miny = _mt.floor(f(rx))
for y in range(miny, ry-1, -1):
ret.append((rx, y))
return iter(ret)
def __repr__(self):
return "LineLocus({}, {}, {}, {}, thick={})".format(self.x1, self.y1, self.x2, self.y2, self.thick)
class RectLocus(Locus):
"""A locus that defines the outline of a rectangle."""
def __init__(self, x1, y1, x2, y2, **kwargs):
self.lx = min(x1, x2)
self.rx = max(x1, x2)
self.ly = min(y1, y2)
self.ry = max(y1, y2)
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
if x < self.lx or x > self.rx or y < self.ly or y > self.ry:
return False
elif x == self.lx or x == self.rx or y == self.ry or y == self.ly:
return True
else:
return False
def __iter__(self):
ret = [(x, self.ly) for x in range(self.lx, self.rx)]
ret.extend([(self.rx, y) for y in range(self.ly, self.ry)])
ret.extend([(x, self.ry) for x in range(self.rx, self.lx, -1)])
ret.extend([(self.lx, y) for y in range(self.ry, self.ly, -1)])
return iter(ret)
def __repr__(self):
return "RectLocus({}, {}, {}, {})".format(self.lx, self.ly, self.rx, self.ry)
class FilledRectLocus(Locus):
"""A locus that defines all points within a rectangle."""
def __init__(self, x1, y1, x2, y2, **kwargs):
self.lx = min(x1, x2)
self.rx = max(x1, x2)
self.ly = min(y1, y2)
self.ry = max(y1, y2)
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
return x >= self.lx and x <= self.rx and y >= self.ly and y <= self.ry
def __iter__(self):
return iter([(x, y) for y in range(self.ly, self.ry+1) for x in range(self.lx, self.rx+1)])
def __repr__(self):
return "FilledRectLocus({}, {}, {}, {})".format(self.lx, self.ly, self.rx, self.ry)
class CircleLocus(Locus):
"""A locus that defines the outline of a circle."""
def __init__(self, x, y, radius, **kwargs):
self.x = x + 0.5
self.y = y + 0.5
self.r = radius
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
if self.x == x + 0.5 and (y == _mt.floor(self.y + self.r) or y == _mt.floor(self.y - self.r)):
return True
elif self.y == y + 0.5 and (x == _mt.floor(self.x + self.r) or x == _mt.floor(self.x - self.r)):
return True
else:
# Edge case: very small circle
if self.r <= _mt.sqrt(2) * 0.5:
# In this case, the circle is small enough that the previous
# checks would have cought every possible tile other than the center.
return x == _mt.floor(self.x) and y == _mt.floor(self.y)
if y + 0.5 > self.y:
if x + 0.5 > self.x:
return _mt.sqrt((x - self.x)**2 + (y - self.y)**2) < self.r and _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) > self.r
else:
return _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) < self.r and _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) > self.r
else:
if x + 0.5 > self.x:
return _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) < self.r and _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) > self.r
else:
return _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) < self.r and _mt.sqrt((x - self.x)**2 + (y - self.y)**2) > self.r
def __iter__(self):
# Edge case: very small circle
if self.r <= _mt.sqrt(2) * 0.5:
if self.r > 0.5:
ret = ((_mt.floor(self.x), _mt.floor(self.y)),
(_mt.floor(self.x) + 1, _mt.floor(self.y)),
(_mt.floor(self.x), _mt.floor(self.y) + 1),
(_mt.floor(self.x) - 1, _mt.floor(self.y)),
(_mt.floor(self.x), _mt.floor(self.y) - 1))
return iter(ret)
else:
return iter(((_mt.floor(self.x), _mt.floor(self.y)),))
else:
f = lambda z: _mt.sqrt(self.r * self.r - (z - self.x)**2) + self.y
# The topmost square: the 'keystone' if you will.
ret = [(_mt.floor(self.x), _mt.floor(self.y + self.r))]
# All squares between the keystone and the rightmost column.
for x in range(_mt.ceil(self.x), _mt.floor(self.x + self.r)):
ly = _mt.floor(f(x))
ry = _mt.floor(f(x+1)) - 1
for y in range(ly, ry, -1):
ret.append((x, y))
# The last column, minus the bottom square
x = _mt.floor(self.x + self.r)
for y in range(_mt.floor(f(x)), _mt.floor(self.y), -1):
ret.append((x, y))
# Finish the circle by copying the coordinates we already have.
ret = ret + [(_mt.floor(self.x) + (y - _mt.floor(self.y)), _mt.floor(self.y) - (x - _mt.floor(self.x))) for x, y in ret]
return iter(ret + [(_mt.floor(self.x) - (x - _mt.floor(self.x)), _mt.floor(self.y) - (y - _mt.floor(self.y))) for x, y in ret])
def __repr__(self):
return "CircleLocus({}, {}, {})".format(_mt.floor(self.x), _mt.floor(self.y), self.r)
class FilledCircleLocus(Locus):
"""A locus that defines all points within a circle."""
def __init__(self, x, y, radius, **kwargs):
self.x = x + 0.5
self.y = y + 0.5
self.r = radius
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
if self.x == x + 0.5 and (y <= _mt.floor(self.y + self.r) and y >= _mt.floor(self.y - self.r)):
return True
elif self.y == y + 0.5 and (x <= _mt.floor(self.x + self.r) and x >= _mt.floor(self.x - self.r)):
return True
else:
# Edge case: very small circle
if self.r <= _mt.sqrt(2) * 0.5:
# In this case, the circle is small enough that the previous
# checks would have cought every possible tile other than the center.
return x == _mt.floor(self.x) and y == _mt.floor(self.y)
if y + 0.5 > self.y:
if x + 0.5 > self.x:
return _mt.sqrt((x - self.x)**2 + (y - self.y)**2) < self.r
else:
return _mt.sqrt((x+1 - self.x)**2 + (y - self.y)**2) < self.r
else:
if x + 0.5 > self.x:
return _mt.sqrt((x - self.x)**2 + (y+1 - self.y)**2) < self.r
else:
return _mt.sqrt((x+1 - self.x)**2 + (y+1 - self.y)**2) < self.r
def __iter__(self):
# Edge case: very small circle
if self.r <= _mt.sqrt(2) * 0.5:
if self.r > 0.5:
ret = ((_mt.floor(self.x), _mt.floor(self.y)),
(_mt.floor(self.x) + 1, _mt.floor(self.y)),
(_mt.floor(self.x), _mt.floor(self.y) + 1),
(_mt.floor(self.x) - 1, _mt.floor(self.y)),
(_mt.floor(self.x), _mt.floor(self.y) - 1))
return iter(ret)
else:
return iter(((_mt.floor(self.x), _mt.floor(self.y)),))
else:
f = lambda z: _mt.sqrt(self.r * self.r - (z - self.x)**2) + self.y
# The topmost square: the 'keystone' if you will.
ret = [(_mt.floor(self.x), y) for y in range(_mt.floor(self.y + self.r), _mt.floor(self.y), -1)]
# All squares between the keystone and the rightmost column.
ry = _mt.floor(self.y)
for x in range(_mt.ceil(self.x), _mt.floor(self.x + self.r)):
ly = _mt.floor(f(x))
for y in range(ly, ry, -1):
ret.append((x, y))
# The last column, minus the bottom square
x = _mt.floor(self.x + self.r)
for y in range(_mt.floor(f(x)), _mt.floor(self.y), -1):
ret.append((x, y))
# Finish the circle by copying the coordinates we already have.
ret = ret + [(_mt.floor(self.x) + (y - _mt.floor(self.y)), _mt.floor(self.y) - (x - _mt.floor(self.x))) for x, y in ret]
return iter(ret + [(_mt.floor(self.x) - (x - _mt.floor(self.x)), _mt.floor(self.y) - (y - _mt.floor(self.y))) for x, y in ret] + [(_mt.floor(self.x), _mt.floor(self.y))])
def __repr__(self):
return "FilledCircleLocus({}, {}, {})".format(_mt.floor(self.x), _mt.floor(self.y), self.r)
class SetLocus(set, Locus):
"""A locus that defines a set of given arbitrary points."""
def __init__(self, *args, **kwargs):
super(SetLocus, self).__init__(*args, **kwargs)
def __repr__(self):
return "SetLocus({})".format(tuple(self))

View file

@ -1,511 +1,10 @@
#gamemap.py #gamemap.py
import re import re
import heapq import heapq
import xml.etree.ElementTree as ET
import ruamel.yaml import ruamel.yaml
import math as _mt import math as _mt
from ruamel.yaml.comments import CommentedMap # for loading classes import gamethings as _gt
import gamelocus as _gl
class Thing(object):
def __init__(self, thingType: str, name: str, x: int, y: int, description: str, flags: int, playerx = None, playery = None):
self.thingType = thingType
self.name = name
self.description = description
self.x = x
self.y = y
self.playerx = x
self.playery = y
self.prevx = x # if an area gets double-occupied, a thing can get pushed back.
self.prevy = y
if playerx:
self.playerx = playerx
if playery:
self.playery = playery
self.passable = bool(flags & 1)
self.talkable = bool(flags & 2)
self.lookable = bool(flags & 4)
self.takeable = bool(flags & 8)
self.useable = bool(flags & 16)
self.graphic = ('clear', '#7F7F7F', ' ')
self.thingID = -1 # ID not assigned
def __str__(self):
"""__str__ is used for look."""
return self.description
def __eq__(self, other):
if not isinstance(other, Thing):
return False
return self.name == other.name
class Item(Thing):
yaml_flag = u'!Item'
defaultGraphic = ('clear', '#00BF00', '^')
def __init__(self, name, x: int, y: int, description: str, useFunc: str, useOnFunc: str, customValues: dict, ranged: bool, graphic = defaultGraphic):
super(Item, self).__init__('i', name, x, y, description, 13)
self.useFunc = useFunc
self.useOnFunc = useOnFunc
self.customValues = customValues
self.ranged = ranged
self.graphic = graphic
def use(self):
pass
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description}
# save graphic
graphic = {}
if node.graphic[0] != Item.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != Item.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != Item.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if node.useFunc != '':
ret['useFunc'] = node.useFunc
if node.useOnFunc != '':
ret['useOnFunc'] = node.useOnFunc
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.ranged:
ret['ranged'] = node.ranged
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
useFunc = ''
useOnFunc = ''
customValues = {}
ranged = False
bgc = Item.defaultGraphic[0]
fgc = Item.defaultGraphic[1]
shape = Item.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load use functions
if 'useFunc' in parts:
useFunc = parts['useFunc']
if 'useOnFunc' in parts:
useOnFunc = parts['useOnFunc']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'ranged' in parts:
useOnFunc = parts['ranged']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], useFunc, useOnFunc, customValues, ranged, graphic)
class Useable(Thing):
yaml_flag = u'!Useable'
defaultGraphic = ('clear', '#0000FF', '#')
def __init__(self, name, x: int, y: int, description: str, useFunc: str, customValues: dict, playerx = None, playery = None, graphic = defaultGraphic):
super(Useable, self).__init__('u', name, x, y, description, 16, playerx, playery)
self.useFunc = useFunc
self.customValues = customValues
self.graphic = graphic
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description}
# save graphic
graphic = {}
if node.graphic[0] != Useable.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != Useable.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != Useable.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if node.useFunc != '':
ret['useFunc'] = node.useFunc
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.x != node.playerx or node.y != node.playery:
ret['useLocation'] = (node.playerx, node.playery)
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
useFunc = ''
customValues = {}
playerx, playery = parts['location']
bgc = Useable.defaultGraphic[0]
fgc = Useable.defaultGraphic[1]
shape = Useable.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load use functions
if 'useFunc' in parts:
useFunc = parts['useFunc']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], useFunc, customValues, playerx, playery, graphic)
class Character(Thing):
defaultGraphic = ('clear', '#000000', 'o')
def __init__(self, thingType: str, name: str, x: int, y: int,
description: str, inventory: dict, customValues: dict,
flags: int, playerx = None, playery = None, graphic = defaultGraphic):
super(Character, self).__init__(thingType, name, x, y, description, flags)
if inventory == None:
inventory = {} # create a new dict for the inventory.
# This couldn't be in the NPC constructor because
# then all characters would share a reference to
# the same empty inventory.
self.__inventory = inventory
self.customValues = customValues
self.graphic = graphic
self.thingNames = {}
# set up inventory shtuff
for i in self.__inventory:
if self.__inventory[i].name in self.thingNames:
self.thingNames[self.__inventory[i].name].append(i)
else:
self.thingNames[self.__inventory[i].name] = [i]
def addThing(self, thing):
if not isinstance(thing, Item):
raise TypeError("Only items can be added to a character's inventory.")
self.__inventory[thing.thingID] = thing
if thing.name in self.thingNames:
self.thingNames[thing.name].append(thing.thingID)
else:
self.thingNames[thing.name] = [thing.thingID]
def getThingByID(self, thingID):
return self.__inventory[thingID]
def getThingByName(self, name):
if name in self.thingNames:
return self.__inventory[self.thingNames[name][0]]
else:
return None
def removeThingByID(self, thingID):
ret = self.__inventory[thingID]
self.thingNames[ret.name].remove(thingID)
if len(self.thingNames[ret.name]) == 0:
del self.thingNames[ret.name]
del self.__inventory[thingID]
return ret
def removeThingByName(self, name):
ret = self.getThingByName(name)
self.thingNames[ret.name].remove(thingID)
if len(self.thingNames[ret.name]) == 0:
del self.thingNames[ret.name]
del self.__inventory[thingID]
return ret
def removeThing(self, ret):
self.thingNames[ret.name].remove(ret.thingID)
if len(self.thingNames[ret.name]) == 0:
del self.thingNames[ret.name]
del self.__inventory[ret.thingID]
return ret
@property
def inventory(self):
"""Get the inventory as a list."""
return list(self.__inventory.values())
class NPC(Character):
yaml_flag = u'!NPC'
defaultGraphic = ('clear', '#000000', 'o')
def __init__(self, name, x: int, y: int, description: str, behavior: str, inventory: list, customValues: dict, playerx = None, playery = None, graphic = defaultGraphic):
super(NPC, self).__init__('n', name, x, y, description, None, customValues, 6, playerx, playery, graphic)
self.behavior = behavior
self.behaveEvent = None
self.tempInventory = inventory # should be deleted once NPC is loaded
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y),
'description': node.description, 'behavior': node.behavior}
# save graphic
graphic = {}
if node.graphic[0] != NPC.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != NPC.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != NPC.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if len(node.inventory) > 0:
ret['inventory'] = node.inventory
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.x != node.playerx or node.y != node.playery:
ret['useLocation'] = (node.playerx, node.playery)
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
following = False
inventory = []
customValues = {}
playerx, playery = parts['location']
bgc = NPC.defaultGraphic[0]
fgc = NPC.defaultGraphic[1]
shape = NPC.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load use functions
if 'inventory' in parts:
inventory = parts['inventory']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], parts['behavior'], inventory, customValues,
playerx, playery, graphic)
class Door(Thing):
yaml_flag = u'!Door'
defaultGraphic = ('clear', '#7F3F00', '#')
def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = defaultGraphic):
self.descBase = description
if description == None:
if locked:
description = "The {0} is locked.".format(name)
else:
description = "The {0} is unlocked.".format(name)
else:
if locked:
description += " It is locked.".format(name)
else:
description += " It is unlocked.".format(name)
super(Door, self).__init__('d', name, x, y, description, 1)
self.passable = not locked
self.key = key
self.graphic = graphic
def lock(self, key = None):
if key == self.key:
self.passable = not self.passable
if self.descBase == None:
if self.passable:
self.description = "The {0} is unlocked.".format(self.name)
else:
self.description = "The {0} is locked.".format(self.name)
else:
if self.passable:
self.description += " It is unlocked.".format(self.name)
else:
self.description += " It is locked.".format(self.name)
return True
return False
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y)}
# save graphic
graphic = {}
if node.graphic[0] != Door.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != Door.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != Door.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save door state
if node.passable:
ret['locked'] = not node.passable
if node.descBase != None:
ret['description'] = node.descBase
if node.key != None:
ret['key'] = node.key
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
description = None
locked = False
key = None
bgc = Door.defaultGraphic[0]
fgc = Door.defaultGraphic[1]
shape = Door.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load door state
if 'description' in parts:
description = parts['description']
if 'locked' in parts:
locked = parts['locked']
if 'key' in parts:
key = parts['key']
return cls(parts['name'], parts['location'][0], parts['location'][1],
locked, description, key, graphic)
class MapExit(Thing):
yaml_flag = u'!MapExit'
defaultGraphic = ('clear', '#FF0000', 'x')
def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix = None, onUse = '', key = True, graphic = defaultGraphic):
description = name
if prefix:
description = "{0} {1}".format(prefix, name)
super(MapExit, self).__init__('x', name, x, y, description, 5)
self.exitid = exitid
self.destination = destination
self.prefix = prefix
self.onUse = onUse
self.key = key
self.graphic = graphic
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'id': node.exitid, 'destination': node.destination}
# save graphic
graphic = {}
if node.graphic[0] != MapExit.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != MapExit.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != MapExit.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
if node.prefix != None:
ret['prefix'] = node.prefix
if node.onUse != '':
ret['onUse'] = node.onUse
if node.key != True:
ret['key'] = node.key
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
prefix = None
onUse = ''
key = True
bgc = MapExit.defaultGraphic[0]
fgc = MapExit.defaultGraphic[1]
shape = MapExit.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
if 'prefix' in parts:
prefix = parts['prefix']
if 'onUse' in parts:
onUse = parts['onUse']
if 'key' in parts:
key = parts['key']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['id'], parts['destination'], prefix, onUse, key, graphic)
class MapEntrance(Thing):
yaml_flag = u'!MapEntrance'
defaultGraphic = ('clear', 'clear', 'x')
# no graphic - should not be drawn
def __init__(self, x: int, y: int, exitid: int, name = None):
if name == None:
name = 'entrance {}'.format(exitid)
super(MapEntrance, self).__init__('a', name, x, y, '', 1)
self.exitid = exitid
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'location': (node.x, node.y), 'id': node.exitid}
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
return cls(parts['location'][0], parts['location'][1], parts['id'])
class PlayerCharacter(Character):
"""Player object. Cannot be created with yaml."""
defaultGraphic = ('clear', '#0000FF', 'o')
def __init__(self, x: int, y: int, description: str, inventory: dict, customValues: dict, name = 'You', graphic = defaultGraphic):
super(PlayerCharacter, self).__init__('p', name, x, y, description, inventory, customValues, 5, graphic=graphic)
class MapError(RuntimeError): class MapError(RuntimeError):
pass pass
@ -518,6 +17,13 @@ class GameMap(object):
# regular expressions # regular expressions
tileRegex = re.compile(r'([a-z ])([0-9]+|[ ])') tileRegex = re.compile(r'([a-z ])([0-9]+|[ ])')
matrixRegex = re.compile(r'(?:[ \t]*(?:[a-z ](?:[0-9]+|[ ]))+(\n))+') matrixRegex = re.compile(r'(?:[ \t]*(?:[a-z ](?:[0-9]+|[ ]))+(\n))+')
yaml = ruamel.yaml.YAML()
yaml.register_class(_gt.Item)
yaml.register_class(_gt.Useable)
yaml.register_class(_gt.NPC)
yaml.register_class(_gt.Door)
yaml.register_class(_gt.MapExit)
yaml.register_class(_gt.MapEntrance)
def __init__(self, name, graph, matrix, dimensions): def __init__(self, name, graph, matrix, dimensions):
self.name = name self.name = name
@ -534,6 +40,7 @@ class GameMap(object):
self.wallColors = [] self.wallColors = []
self.persistent = [] self.persistent = []
self.enterScript = '' self.enterScript = ''
self.version = 'square 1'
@staticmethod @staticmethod
def __cleanStr(text: str, end = '\n'): def __cleanStr(text: str, end = '\n'):
@ -562,23 +69,24 @@ it will read from stdin. Otherwise, it should be a valid file name.
Entering a map through stdin will be obsolete once testing is over.""" Entering a map through stdin will be obsolete once testing is over."""
info = None info = None
tryToRead = True tryToRead = True
yaml = ruamel.yaml.YAML()
yaml.register_class(Item)
yaml.register_class(Useable)
yaml.register_class(NPC)
yaml.register_class(Door)
yaml.register_class(MapExit)
yaml.register_class(MapEntrance)
if infile != None: if infile != None:
try: try:
with open(infile, 'r') as f: with open(infile, 'r') as f:
info = yaml.load(f) info = GameMap.yaml.load(f)
except OSError as e: except OSError as e:
print("The file could not be read.") print("The file could not be read.")
return None, nextThing return None, nextThing
else: else:
raise MapError("No file was specified for loading.") raise MapError("No file was specified for loading.")
# Future feature: different map standards, with cool special features
# like hex tiles, or walls in-between tiles.
# For now just 'square 1': square tiles, version 1.
version = 'square 1'
if 'version' in info:
if info['version'] in ('square 1'):
version = info['version']
# Now what we do with the data # Now what we do with the data
mat = None mat = None
if 'layout' not in info: if 'layout' not in info:
@ -735,7 +243,7 @@ list of lists of tuples."""
return nextThing return nextThing
def addThingRecursive(self, container, nextThing = 0): def addThingRecursive(self, container, nextThing = 0):
if isinstance(container, Thing): if isinstance(container, _gt.Thing):
if container.thingID == -1: if container.thingID == -1:
container.thingID = nextThing container.thingID = nextThing
nextThing = self.addThingRecursive(container.customValues, nextThing) nextThing = self.addThingRecursive(container.customValues, nextThing)
@ -777,42 +285,55 @@ list of lists of tuples."""
else: else:
raise ValueError('Thing cannot be found by {}.'.format(str(kwargs))) raise ValueError('Thing cannot be found by {}.'.format(str(kwargs)))
def path(self, x1, y1, x2, y2, closeEnough = True): def path(self, x1, y1, loc, closeEnough = True):
startThing = self.getThingAtCoords(x1, y1) startThing = self.getThingAtCoords(x1, y1)
if not closeEnough: #if not closeEnough:
if startThing and not startThing.passable: # if startThing and not startThing.passable:
return -1, [] # meaning you can't get there # return -1, [], -1 # meaning you can't get there
dist, prev = self.dijkstra(x1, y1, closeEnough) dist, prev, endPoint = self.dijkstra(x1, y1, loc, closeEnough)
endPoint = self.coordsToInt(x2, y2) #endPoint = self.coordsToInt(x2, y2)
numVertex = self.dimensions[0] * self.dimensions[1] numVertex = self.dimensions[0] * self.dimensions[1]
if dist[endPoint] < numVertex + 1: if endPoint > -1 and dist[endPoint] < numVertex + 1:
return dist[endPoint], prev pathList = [endPoint]
nextPoint = prev[endPoint]
while nextPoint != -1:
pathList.append(nextPoint)
nextPoint = prev[nextPoint]
pathList.reverse()
return dist[endPoint], pathList[1:], endPoint
else: else:
return -1, [] # meaning you can't get there return -1, [], -1 # meaning you can't get there
def dijkstra(self, x1, y1, closeEnough = True): def dijkstra(self, x1, y1, loc = None, closeEnough = True):
"""Uses Dijkstra's Algorithm to find the shortest path from (x1, y1) to (x2, y2) """Uses Dijkstra's Algorithm to find the shortest path from (x1, y1) to (x2, y2)
The closeEnough parameter will create a path that lands beside the source if necessary.""" The closeEnough parameter will create a path that lands beside the source if
necessary. The loc parameter is an optional locus which will cause the function
to return once it finds a point that's in the locus."""
# first test to see that the start point is passable # first test to see that the start point is passable
startThing = self.getThingAtCoords(x1, y1) startThing = self.getThingAtCoords(x1, y1)
startPoint = self.coordsToInt(x1, y1) startPoint = self.coordsToInt(x1, y1)
#endPoint = self.coordsToInt(x2, y2) endPoint = -1 # until one matches the locus, which it might not.
numVertex = self.dimensions[0] * self.dimensions[1] 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)]
dist[startPoint] = 0 dist[startPoint] = 0
if closeEnough: #if closeEnough:
if startThing and not startThing.passable: # if startThing and not startThing.passable:
dist[startPoint] = -1 # 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:
return dist, prev, u
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:
continue if closeEnough and self.intToCoords(v) in loc:
return dist, prev, u
else:
continue
tempDist = dist[u] + 1 tempDist = dist[u] + 1
if tempDist < dist[v]: if tempDist < dist[v]:
dist[v] = tempDist dist[v] = tempDist
@ -820,7 +341,7 @@ The closeEnough parameter will create a path that lands beside the source if nec
prev[v] = u prev[v] = u
heapq.heappush(queue, (dist[v], v)) heapq.heappush(queue, (dist[v], v))
return dist, prev return dist, prev, endPoint
#if dist[endPoint] < numVertex + 1: #if dist[endPoint] < numVertex + 1:
# return dist[endPoint], prev # return dist[endPoint], prev
#else: #else:
@ -831,37 +352,13 @@ The closeEnough parameter will create a path that lands beside the source if nec
# 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
Dx = x2 - x1
Dy = y2 - y1 # Common case second:
y = y1 + 0.5 lst = list(_gl.LineLocus(x1, y1, x2, y2, False))[1:-1]
x = x1 + 0.5
lst = []
if abs(Dx) >= abs(Dy):
if Dx < 0:
x = x2 + 0.5
y = y2 + 0.5
dy = Dy / Dx
while(x < max(x2, x1) - 0.5):
x += 1
lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y)))
if _mt.floor(y) != _mt.floor(y + dy):
lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y + dy)))
y += dy
elif abs(Dx) < abs(Dy):
if Dy < 0:
x = x2 + 0.5
y = y2 + 0.5
dx = Dx / Dy
while(y < max(y2, y1) - 0.5):
y += 1
lst.append(self.coordsToInt(_mt.floor(x), _mt.floor(y)))
if _mt.floor(x) != _mt.floor(x + dx):
lst.append(self.coordsToInt(_mt.floor(x + dx), _mt.floor(y)))
x += dx
# Here is where we actually check: # Here is where we actually check:
for space in lst: for space in lst:
if not self.isPassable(space): if not self.isPassable(self.coordsToInt(*space)):
return False return False
return True return True
@ -999,6 +496,8 @@ The closeEnough parameter will create a path that lands beside the source if nec
else: else:
x, y = self.intToCoords(x) x, y = self.intToCoords(x)
if thing != None: if thing != None:
if thing.x == x and thing.y == y:
return # it's already there, so don't do anything.
oldPos = self.coordsToInt(thing.x, thing.y) oldPos = self.coordsToInt(thing.x, thing.y)
if oldPos in self.thingPos: if oldPos in self.thingPos:
self.thingPos[oldPos].remove(thing.thingID) self.thingPos[oldPos].remove(thing.thingID)
@ -1015,3 +514,75 @@ The closeEnough parameter will create a path that lands beside the source if nec
else: else:
raise MapError("There is nothing to move.") raise MapError("There is nothing to move.")
class LoSLocus(_gl.Locus):
"""A locus that defines all points within line-of-sight of a given points."""
def __init__(self, x, y, level):
self.x = x
self.y = y
self.level = level
def __contains__(self, item):
if not isinstance(item, tuple) and not isinstance(item, list):
raise ValueError("Item must be a tuple or a list.")
x, y = item
return self.level.lineOfSight(x, y, self.x, self.y)
def __iter__(self):
ret = [(self.x, self.y)]
mat = [[False for i in range(self.level.y)] for j in range(self.level.x)]
mat[self.x][self.y] = True
fringe = [(self.x, self.y + 1), (self.x + 1, self.y), (self.x, self.y - 1), (self.x - 1, self.y),
(self.x + 1, self.y + 1), (self.x + 1, self.y - 1), (self.x - 1, self.y - 1), (self.x - 1, self.y + 1)]
while len(fringe) > 0:
point = fringe.pop(0)
check = []
if abs(point[0] - self.x) > abs(point[1] - self.y):
if point[0] > self.x:
check.append((point[0] - 1, point[1]))
if point[1] > self.y:
check.append((point[0] - 1, point[1] - 1))
elif point[1] < self.y:
check.append((point[0] - 1, point[1] + 1))
else:
check.append((point[0] + 1, point[1]))
if point[1] > self.y:
check.append((point[0] + 1, point[1] - 1))
elif point[1] < self.y:
check.append((point[0] + 1, point[1] + 1))
elif abs(point[0] - self.x) < abs(point[1] - self.y):
if point[1] > self.y:
check.append((point[0], point[1] - 1))
if point[0] > self.x:
check.append((point[0] - 1, point[1] - 1))
elif point[0] < self.x:
check.append((point[0] + 1, point[1] - 1))
else:
check.append((point[0], point[1] + 1))
if point[0] > self.x:
check.append((point[0] - 1, point[1] + 1))
elif point[0] < self.x:
check.append((point[0] + 1, point[1] + 1))
else:
if point[0] > self.x:
if point[1] > self.y:
check.append((point[0] - 1, point[1] - 1))
else:
check.append((point[0] - 1, point[1] - 1))
else:
if point[1] > self.y:
check.append((point[0] + 1, point[1] - 1))
else:
check.append((point[0] + 1, point[1] - 1))
status = [mat[i[0]][i[1]] for i in check]
addIf = False
if True in status:
if False in status:
addIf = self.level.lineOfSight(point[0], point[1], self.x, self.y)
else:
addIf = True
if addIf:
mat[point[0]][point[1]] = self.level.isPassable(*point)
ret.append(point)
return iter(ret)

315
gamesequence.py Normal file
View file

@ -0,0 +1,315 @@
# gamesequence.py
import re as _re
class SequenceError(RuntimeError):
pass
class ScriptBreak(object):
def __init__(self, value):
self.value = value
def getValueFromString(arg: str, env: dict):
#if env == None:
#env = self.customValues
val = None
validIdent = _re.match(r'[_A-Za-z][_0-9A-Za-z]*', arg)
if arg[0] in '"\'' and arg[-1] == arg[0]: # assume it's a string
val = arg[1:-1]
elif _re.match(r'[+-]?(?:[0-9]*[.])?[0-9]+', arg) != None:
if '.' in arg:
val = float(arg)
else:
val = int(arg)
elif arg.casefold() == 'true':
val = True
elif arg.casefold() == 'false':
val = False
#elif arg.casefold() == 'playerx' and self.player != None:
# val = self.player.x
#elif arg.casefold() == 'playery' and self.player != None:
# val = self.player.y
#elif arg.casefold() == 'playername' and self.player != None:
# val = self.player.name
#elif arg.casefold() == 'playerinv' and self.player != None:
# val = self.player.inventory
#elif arg.casefold() == 'playerdesc' and self.player != None:
# val = self.player.description
elif validIdent != None and env != None:
group = validIdent.group()
if 'scriptLocal' in env and group in env['scriptLocal']:
val = env['scriptLocal'][group]
elif group in env['global']:
val = env['global'][group]
else:
return False # for if statements; if a variable doesn't exist, should evaluate to False
# evaluate all values of all indecies
openBracket = validIdent.end()
if openBracket < len(arg):
if arg[openBracket] == '[':
ptr = openBracket
depth = 0
while ptr < len(arg):
if depth == 0 and arg[ptr] != '[':
raise SequenceError('Invalid value syntax: {}'.format(arg))
if arg[ptr] == '[':
depth += 1
elif arg[ptr] == ']':
depth -= 1
if depth == 0:
index = getValueFromString(arg[openBracket+1:ptr], env)
if index in val:
val = val[index]
else:
return False
openBracket = ptr + 1
ptr += 1
else:
raise SequenceError('Invalid value syntax: {}'.format(arg))
else:
raise SequenceError('Invalid argument to getValueFromString: {}'.format(arg))
return val
def compareValues(args: list, env: dict):
"""Generalized comparisons, may eventually be extended to other operators"""
if len(args) == 1:
return bool(getValueFromString(args[0]), env)
elif len(args) == 3 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'):
lval = getValueFromString(args[0], env)
operator = args[1]
rval = getValueFromString(args[2], env)
if operator == '==':
return lval == rval
elif operator == '!=':
return lval != rval
elif operator == '<=':
return lval <= rval
elif operator == '>=':
return lval >= rval
elif operator == '<':
return lval < rval
elif operator == '>':
return lval > rval
elif operator == 'in':
#if args[2].casefold() == 'playerinv':
# return lval in self.player.thingNames
#else:
return lval in rval
else:
raise SequenceError("Condition cannot be evaluated: {}".format(' '.join(args)))
def ifScript(args, env: dict, externalScripts: dict):
"""If statement: if [not] value [op value] script"""
if len(args) < 2:
raise GameError('Incomplete If statement: if {}'.format(' '.join(args)))
inverse = False
ret = False
if args[0] == 'not':
inverse = True
args.pop(0)
if len(args) < 2:
raise GameError('Incomplete If statement: if {}'.format(' '.join(args)))
# evaluate condition
if len(args) > 1 and args[1] in ('==', '!=', '<=', '>=', '<', '>', 'in'):
if len(args) < 4:
raise SequenceError('Incomplete If statement: if {}'.format(' '.join(args)))
ret = compareValues(args[:3], env)
args = args[3:]
else:
ret = bool(getValueFromString(args[0]), env)
args = args[1:]
if inverse:
ret = not ret
# if condition is true, evaluate further
if ret:
if isinstance(args[-1], list):
return runScript(args[-1], env, externalScripts)
else:
return runScript([args], env, externalScripts)
def getCustomValue(args, env: dict):
if env == None:
raise SequenceError("Cannot get a value from an empty environment.")
return getValueFromString(args[0], env)
def setCustomValue(args, env: dict):
"""takes [customValue, op, value]"""
if env == None:
raise SequenceError("Cannot set a value from an empty environment.")
scope = env['scriptLocal']
if len(args) > 0 and args[0] == 'global':
scope = env['global']
args.pop(0)
if len(args) < 3 or args[1] not in ('=', '+=', '-=', '*=', '/=', '%=', '//=', '**=', 'b=', '!=', '|=', '&=', '^='):
raise SequenceError('Arguments are not fit for the setCustomValue script.')
# This next line allows cvmod to use values in the thing's customValues dict,
# but doing this means that accessing a game CV requires setting it to a local var.
val = getValueFromString(args[2], env)
# set the value to a default value (0, 0.0, '', etc.) if not yet assigned
if args[0] not in scope and args[1] != '=':
if isinstance(val, int):
scope[args[0]] = 0
elif isinstance(val, float):
scope[args[0]] = 0.0
elif isinstance(val, str):
scope[args[0]] = ''
elif isinstance(val, bool):
scope[args[0]] = False
# Beyond this point are types that can only be found from other customValues
elif isinstance(val, tuple):
scope[args[0]] = ()
elif isinstance(val, list):
scope[args[0]] = []
elif isinstance(val, dict):
scope[args[0]] = {}
else:
raise SequenceError("Operation not supported for unassigned custom values of type {}: {}"
.format(type(val), args[1]))
# done parsing, evaluate
if args[1] == '=':
scope[args[0]] = val
elif args[1] == '+=':
scope[args[0]] += val
elif args[1] == '-=':
scope[args[0]] -= val
elif args[1] == '*=':
scope[args[0]] *= val
elif args[1] == '/=':
scope[args[0]] /= val
elif args[1] == '%=':
scope[args[0]] %= val
elif args[1] == '//=':
scope[args[0]] //= val
elif args[1] == '**=':
scope[args[0]] **= val
elif args[1] == 'b=':
scope[args[0]] = bool(val)
elif args[1] == '!=':
scope[args[0]] = not bool(val)
elif args[1] == '|=':
scope[args[0]] |= val
elif args[1] == '&=':
scope[args[0]] &= val
elif args[1] == '^=':
scope[args[0]] ^= val
return scope[args[0]]
def delCustomValue(args, env: dict):
"""To clean up after a map."""
if env == None:
raise SequenceError("Cannot delete a value from an empty environment.")
for i in args:
if i in env['global']:
del(env['global'][i])
_requireEnv = {'get' : getCustomValue, 'set' : setCustomValue, 'del' : delCustomValue}
_requireScripts = {'if' : ifScript}
def runScript(script: list, env: dict, externalScripts: dict):
"""run a script"""
ret = False
for line in script:
if len(line) == 0:
# empty line
continue
elif line[0].casefold() == 'playercmd':
# run a player command
self.getIO('playercmd')(line[1:])
elif line[0].casefold() in _requireScripts:
ret = _requireScripts[line[0]](line[1:], env, externalScripts)
elif line[0].casefold() in _requireEnv:
ret = _requireEnv[line[0]](line[1:], env)
elif line[0].casefold() == 'break':
# exit early
return _ScriptBreak(ret)
elif line[0] in externalScripts:
# run a script
ret = externalScripts[line[0]](line[1:])
else:
# conditional evaluation
compareValues(line, env)
if isinstance(ret, ScriptBreak):
return ret
# We specifically want the return value of the last line executed.
return ret
def parseScript(instr: str, envGlobal: dict, externalScripts: dict):
"""parses then runs a script."""
#print('parseScript called with {}.'.format(instr))
if instr == '':
return # nothing to be done.
literalStr = list(instr)
inQuotes = False
script = []
stack = []
ret = []
argStart = 0
c = 0
l = 0
while c < len(instr):
if inQuotes:
if instr[c] == '"':
if instr[c-1] == '\\':
del literalStr[l-1]
l -= 1
else:
inQuotes = False
del literalStr[l]
l -= 1
else:
if instr[c] == '"': # quoted string
if l > 0 and instr[c-1] == '\\':
del literalStr[l-1]
l -= 1
else:
inQuotes = True
del literalStr[l]
l -= 1
elif instr[c] in ' \t\n;}{':
if l > 0 and instr[c-1] == '\\':
del literalStr[l-1]
l -= 1
else:
if argStart != l:
ret.append(''.join(literalStr[argStart:l]))
if instr[c] == ';':
if len(ret) > 0:
script.append(ret)
ret = []
elif instr[c] == '{': # block
stack.append(script)
script.append(ret)
script = []
ret.append(script)
ret = []
del literalStr[l]
l -= 1
elif instr[c] == '}': # close block
if len(ret) > 0:
script.append(ret)
ret = []
script = stack.pop()
del literalStr[l]
l -= 1
argStart = l + 1
c += 1
l += 1
if argStart != l:
ret.append(''.join(literalStr[argStart:l]))
if len(ret) > 0:
script.append(ret)
#print('After parsing: {}'.format(script))
env = {'global' : envGlobal, 'scriptLocal' : {}}
#self.customValues['scriptLocal'] = {}
ret = runScript(script, env, externalScripts)
if isinstance(ret, ScriptBreak):
ret = ret.value
return ret

View file

@ -9,6 +9,7 @@ import heapq
import gameevents import gameevents
import textwrap as _tw import textwrap as _tw
from shutil import get_terminal_size as _gts from shutil import get_terminal_size as _gts
import gameutil as _gu
#import random #import random
TERM_SIZE = _gts()[0] TERM_SIZE = _gts()[0]
@ -27,9 +28,16 @@ class GameShell(Shell):
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')
self.gameTitle = 'Game Shell' # should be changed for actual games self.gameTitle = 'Game Shell' # should be changed for actual games
if 'title' in data:
self.gameTitle = data['title']
self.startLevel = 'testing/test1.yml' # should be changed for actual games self.startLevel = 'testing/test1.yml' # should be changed for actual games
if 'startLevel' in data:
self.gameTitle = data['startLevel']
self.openingText = '{}\nIn Development' self.openingText = '{}\nIn Development'
if 'openingText' in data:
self.gameTitle = data['openingText']
self.ps2 = '?> ' self.ps2 = '?> '
self.__inGame = False self.__inGame = False
@ -43,7 +51,10 @@ class GameShell(Shell):
self.registerCommand('help', self.man) self.registerCommand('help', self.man)
self.registerCommand('new', self.newGame) self.registerCommand('new', self.newGame)
self.gameBase.registerIO('container', self.container) self.gameBase.registerIO('container', self.container)
self.gameBase.registerIO('dialog', self.dialog) self.gameBase.registerIO('startDialog', self.startDialog)
self.gameBase.registerIO('openDialog', self.openDialog)
self.gameBase.registerIO('respondDialog', self.respondDialog)
self.gameBase.registerIO('endDialog', self.endDialog)
self.gameBase.registerIO('info', self.info) self.gameBase.registerIO('info', self.info)
self.gameBase.registerIO('playercmd', self.playercmd) self.gameBase.registerIO('playercmd', self.playercmd)
@ -136,7 +147,7 @@ class GameShell(Shell):
See a map of the local area. "ls" is an alias of map. See a map of the local area. "ls" is an alias of map.
"l" and "legend" are aliases of -l. "l" and "legend" are aliases of -l.
If -l is given, a map legend will be printed under the map.""" If -l is given, a map legend will be printed under the map."""
xAxis = ' ' + ''.join([self.gameBase.numberToLetter(i).ljust(2) for i in range(self.gameBase.level.dimensions[0])]) + '\n' xAxis = ' ' + ''.join([_gu.numberToLetter(i).ljust(2) for i in range(self.gameBase.level.dimensions[0])]) + '\n'
rows = [] rows = []
index = 0 index = 0
exits = {} exits = {}
@ -154,11 +165,6 @@ If -l is given, a map legend will be printed under the map."""
for x in range(level.dimensions[0]): for x in range(level.dimensions[0]):
pos = level.mapMatrix[y][x] pos = level.mapMatrix[y][x]
things = level.getThingsAtPos(index) things = level.getThingsAtPos(index)
#if x == self.gameBase.playerx and y == self.gameBase.playery:
# if self.gameBase.player != textColor:
# textColor = '#0000FF'
# rows[-1].append(self.color(textColor[1:]))
# rows[-1].append('()')
if len(things) > 0: if len(things) > 0:
# Prioritize types: p, n, i, u, d, x, a # Prioritize types: p, n, i, u, d, x, a
thing = things[0] thing = things[0]
@ -253,8 +259,8 @@ If -l is given, a map legend will be printed under the map."""
for i in self.gameBase.customValues: 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]))
ret.append("Player name:{0:.>68}".format(self.gameBase.player.name)) ret.append("Player name:{0:.>68}".format(self.gameBase.player.name))
ret.append("Player position:{0:.>64}".format("{0}{1}".format(self.gameBase.numberToLetter(self.gameBase.player.x), self.gameBase.player.y))) ret.append("Player position:{0:.>64}".format("{0}{1}".format(_gu.numberToLetter(self.gameBase.player.x), self.gameBase.player.y)))
ret.append("Prev. position:{0:.>65}".format("{0}{1}".format(self.gameBase.numberToLetter(self.gameBase.player.prevx), self.gameBase.player.prevy))) ret.append("Prev. position:{0:.>65}".format("{0}{1}".format(_gu.numberToLetter(self.gameBase.player.prevx), self.gameBase.player.prevy)))
ret.append("Inventory:") ret.append("Inventory:")
for i in self.gameBase.player.inventory: for i in self.gameBase.player.inventory:
ret.append("{0:<8}: {1}".format(i.thingID, i.name)) ret.append("{0:<8}: {1}".format(i.thingID, i.name))
@ -303,6 +309,7 @@ If -l is given, a map legend will be printed under the map."""
"""Mode for in-game.""" """Mode for in-game."""
if not self.__inGame: if not self.__inGame:
self.registerCommand('map', self.showMap) self.registerCommand('map', self.showMap)
self.registerCommand('wait', self.gameBase.wait)
self.registerCommand('ls', self.showMap) self.registerCommand('ls', self.showMap)
self.registerCommand('go', self.gameBase.go) self.registerCommand('go', self.gameBase.go)
self.registerCommand('move', self.gameBase.go) self.registerCommand('move', self.gameBase.go)
@ -324,6 +331,7 @@ If -l is given, a map legend will be printed under the map."""
"""Mode for main menus and the like.""" """Mode for main menus and the like."""
if self.__inGame: if self.__inGame:
self.registerCommand('map', self.showMap) self.registerCommand('map', self.showMap)
self.unregisterCommand('wait', self.gameBase.wait)
self.unRegisterCommand('ls', self.showMap) self.unRegisterCommand('ls', self.showMap)
self.unRegisterCommand('go', self.gameBase.go) self.unRegisterCommand('go', self.gameBase.go)
self.unRegisterCommand('move', self.gameBase.go) self.unRegisterCommand('move', self.gameBase.go)
@ -435,78 +443,24 @@ Player is modified through side-effect."""
instr = input("Choose an item to view, or exit: ") instr = input("Choose an item to view, or exit: ")
return charsRead / 27 # based on average 250 words per minute, and word length of 5.5 + 1 for space. return charsRead / 27 # based on average 250 words per minute, and word length of 5.5 + 1 for space.
def dialog(self, dialogObj): def startDialog(self):
if 'script' in dialogObj: pass
self.gameBase.parseScript(dialogObj['script'])
if 'cond' in dialogObj:
cases = dialogObj['cond']
ret = 0
for case in cases:
cond = case['case'].split() # should be list like ["value", "==", 1]
if len(cond) == 1 and (cond[0] == 'else' or self.gameBase.getValueFromString(cond[0])):
ret = self.dialog(case)
break
elif len(cond) == 3 and self.gameBase.compareValues(cond[1], cond[0], cond[2]):
ret = self.dialog(case)
break
else:
raise RuntimeError("All routes are false: {}".format(testValue))
if ret > 1:
return ret - 1
else:
return ret
# if ret == 1 or ret == 0, go back again.
elif 'opener' in dialogObj:
toPrint = dialogObj['opener'].split('\n') # split by lines so that fill doesn't erase newlines
for line in toPrint:
print(_tw.fill(line, width = TERM_SIZE))
input('<ENTER>')
while isinstance(dialogObj, dict):
if 'action' in dialogObj:
action = dialogObj['action']
if action == 'answer':
answer = 0
skips = []
j = 0 # follower to i
if 'answers' in dialogObj and isinstance(dialogObj['answers'], list):
for i in range(len(dialogObj['answers'])):
ans = dialogObj['answers'][i]
if ans[0] == '?':
condEnd = ans.index(':')
cond = ans[1:condEnd].strip().split()
if self.gameBase.compareValues(cond):
print(_tw.fill('{}: {}'.format(j+1, ans[condEnd+1:].strip()), width = TERM_SIZE))
j += 1
else:
skips.append(i)
else:
print(_tw.fill('{}: {}'.format(j+1, ans), width = TERM_SIZE))
j += 1
answer = int(input(self.ps2)) - 1
# account for offset if there were answer options that didn't meet conditions
for i in skips:
if i <= answer:
answer += 1
else:
break
if 'replies' in dialogObj and isinstance(dialogObj['replies'], list):
ret = self.dialog(dialogObj['replies'][answer])
if ret > 1:
return ret - 1
elif ret == 0:
return 0
# if ret == 1, then do this dialog again
elif len(action) >= 4 and action[:4] == 'back': def openDialog(self, opener):
if len(action) == 4: for line in opener:
return 1 print(_tw.fill(line, width = TERM_SIZE))
return int(action[4:]) input("...")
elif action == 'exit':
return 0 def respondDialog(self, options):
else: for lineNo in range(len(options)):
raise RuntimeError('Malformed action: {}'.format(action)) print(_tw.fill('{}: {}'.format(lineNo+1, options[lineNo]), width = TERM_SIZE))
else: answer = -1
raise RuntimeError("Dialog branch with neither switch nor openner.") while answer < 0 or answer >= len(options):
answer = int(input(self.ps2)) - 1
return answer
def endDialog(self):
pass
def playercmd(self, args): def playercmd(self, args):
self.handleCommand(args) self.handleCommand(args)

529
gamethings.py Normal file
View file

@ -0,0 +1,529 @@
#gamethings.py
import ruamel.yaml
from ruamel.yaml.comments import CommentedMap
class Thing(object):
def __init__(self, thingType: str, name: str, x: int, y: int, description: str, flags: int, playerx = None, playery = None, **kwargs):
self.thingType = thingType
self.name = name
self.description = description
self.x = x
self.y = y
self.playerx = x
self.playery = y
self.prevx = x # if an area gets double-occupied, a thing can get pushed back.
self.prevy = y
if playerx:
self.playerx = playerx
if playery:
self.playery = playery
self.passable = bool(flags & 1)
self.talkable = bool(flags & 2)
self.lookable = bool(flags & 4)
self.takeable = bool(flags & 8)
self.useable = bool(flags & 16)
self.graphic = ('clear', '#7F7F7F', ' ')
self.thingID = -1 # ID not assigned
def __str__(self):
"""__str__ is used for look."""
return self.description
def __eq__(self, other):
if not isinstance(other, Thing):
return False
return self.name == other.name
class Observer(Thing):
"""ABC for things that have a dict of events that they should listen to."""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self.behaviors = {} # {the name of the event : (behaviorQueue priority, the name of the behavior)}
self.busy = False # Prevents a new behavior until the current one is finished
self.behaviorQueue = [] # If you can't make it perfect, then make it adjustable.
# The behavior queue is for behaviors that need to execute once the current action is finished.
# If it's not considered important enough (behaviors[event][0] < 0), it's dropped.
class Item(Thing):
yaml_flag = u'!Item'
defaultGraphic = ('clear', '#00BF00', '^')
def __init__(self, name, x: int, y: int, description: str, useFunc: str, useOnFunc: str, customValues: dict, ranged: bool, graphic = defaultGraphic):
super(Item, self).__init__('i', name, x, y, description, 13)
self.useFunc = useFunc
self.useOnFunc = useOnFunc
self.customValues = customValues
self.ranged = ranged
self.graphic = graphic
def use(self):
pass
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description}
# save graphic
graphic = {}
if node.graphic[0] != Item.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != Item.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != Item.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if node.useFunc != '':
ret['useFunc'] = node.useFunc
if node.useOnFunc != '':
ret['useOnFunc'] = node.useOnFunc
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.ranged:
ret['ranged'] = node.ranged
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
useFunc = ''
useOnFunc = ''
customValues = {}
ranged = False
bgc = Item.defaultGraphic[0]
fgc = Item.defaultGraphic[1]
shape = Item.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load use functions
if 'useFunc' in parts:
useFunc = parts['useFunc']
if 'useOnFunc' in parts:
useOnFunc = parts['useOnFunc']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'ranged' in parts:
useOnFunc = parts['ranged']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], useFunc, useOnFunc, customValues, ranged, graphic)
def use(self, user, gameBase, args) -> float:
pass
def useOn(self, user, gameBase, thing, args) -> float:
pass
class Useable(Thing):
yaml_flag = u'!Useable'
defaultGraphic = ('clear', '#0000FF', '#')
def __init__(self, name, x: int, y: int, description: str, useFunc: str, customValues: dict, playerx = None, playery = None, graphic = defaultGraphic):
super(Useable, self).__init__('u', name, x, y, description, 16, playerx, playery)
self.useFunc = useFunc
self.customValues = customValues
self.graphic = graphic
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'description': node.description}
# save graphic
graphic = {}
if node.graphic[0] != Useable.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != Useable.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != Useable.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if node.useFunc != '':
ret['useFunc'] = node.useFunc
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.x != node.playerx or node.y != node.playery:
ret['useLocation'] = (node.playerx, node.playery)
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
useFunc = ''
customValues = {}
playerx, playery = parts['location']
bgc = Useable.defaultGraphic[0]
fgc = Useable.defaultGraphic[1]
shape = Useable.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load use functions
if 'useFunc' in parts:
useFunc = parts['useFunc']
if 'customValues' in parts:
customValues = dict(parts['customValues'])
for v in customValues:
if isinstance(customValues[v], tuple):
customValues[v] = list(customValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['description'], useFunc, customValues, playerx, playery, graphic)
def use(self, user, gameBase, args) -> float:
pass
class Character(Thing):
defaultGraphic = ('clear', '#000000', 'o')
def __init__(self, inventory: dict, customValues: dict, graphic = defaultGraphic, **kwargs):
super(Character, self).__init__(**kwargs)
if inventory == None:
inventory = {} # create a new dict for the inventory.
# This couldn't be in the NPC constructor because
# then all characters would share a reference to
# the same empty inventory.
self.__inventory = inventory
self.customValues = customValues
self.graphic = graphic
self.thingNames = {}
# set up inventory shtuff
for i in self.__inventory:
if self.__inventory[i].name in self.thingNames:
self.thingNames[self.__inventory[i].name].append(i)
else:
self.thingNames[self.__inventory[i].name] = [i]
def addThing(self, thing):
if not isinstance(thing, Item):
raise TypeError("Only items can be added to a character's inventory.")
self.__inventory[thing.thingID] = thing
if thing.name in self.thingNames:
self.thingNames[thing.name].append(thing.thingID)
else:
self.thingNames[thing.name] = [thing.thingID]
def getThingByID(self, thingID):
return self.__inventory[thingID]
def getThingByName(self, name):
if name in self.thingNames:
return self.__inventory[self.thingNames[name][0]]
else:
return None
def removeThingByID(self, thingID):
ret = self.__inventory[thingID]
self.thingNames[ret.name].remove(thingID)
if len(self.thingNames[ret.name]) == 0:
del self.thingNames[ret.name]
del self.__inventory[thingID]
return ret
def removeThingByName(self, name):
ret = self.getThingByName(name)
self.thingNames[ret.name].remove(thingID)
if len(self.thingNames[ret.name]) == 0:
del self.thingNames[ret.name]
del self.__inventory[thingID]
return ret
def removeThing(self, ret):
self.thingNames[ret.name].remove(ret.thingID)
if len(self.thingNames[ret.name]) == 0:
del self.thingNames[ret.name]
del self.__inventory[ret.thingID]
return ret
@property
def inventory(self):
"""Get the inventory as a list."""
return list(self.__inventory.values())
class NPC(Character, Observer):
yaml_flag = u'!NPC'
defaultGraphic = ('clear', '#000000', 'o')
def __init__(self, behaviors: dict, tempInventory: list, **kwargs):
if 'graphic' not in kwargs:
kwargs['graphic'] = PlayerCharacter.defaultGraphic
super(NPC, self).__init__(thingType = 'n', inventory = {}, flags = 6, **kwargs)
self.behaviors = behaviors
self.behaveEvent = None
self.tempInventory = tempInventory # should be deleted once NPC is loaded
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y),
'description': node.description, 'behaviors': node.behaviors}
# save graphic
graphic = {}
if node.graphic[0] != NPC.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != NPC.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != NPC.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save use functions
if len(node.inventory) > 0:
ret['inventory'] = node.inventory
if len(node.customValues) > 0:
ret['customValues'] = node.customValues
if node.x != node.playerx or node.y != node.playery:
ret['useLocation'] = (node.playerx, node.playery)
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
following = False
minventory = []
mcustomValues = {}
mplayerx, mplayery = parts['location']
bgc = NPC.defaultGraphic[0]
fgc = NPC.defaultGraphic[1]
shape = NPC.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
mgraphic = (bgc, fgc, shape)
# load use functions
if 'inventory' in parts:
inventory = parts['inventory']
if 'customValues' in parts:
mcustomValues = dict(parts['customValues'])
for v in mcustomValues:
if isinstance(mcustomValues[v], tuple):
mcustomValues[v] = list(mcustomValues[v])
if 'useLocation' in parts:
playerx, playery = parts['useLocation']
return cls(name = parts['name'], x = parts['location'][0], y = parts['location'][1],
description = parts['description'], behaviors = parts['behaviors'], tempInventory = minventory, customValues = mcustomValues,
playerx = mplayerx, plyery = mplayery, graphic = mgraphic)
class Door(Thing):
yaml_flag = u'!Door'
defaultGraphic = ('clear', '#7F3F00', '#')
def __init__(self, name, x: int, y: int, locked: bool, description = None, key = None, graphic = defaultGraphic):
self.descBase = description
if description == None:
if locked:
description = "The {0} is locked.".format(name)
else:
description = "The {0} is unlocked.".format(name)
else:
if locked:
description += " It is locked.".format(name)
else:
description += " It is unlocked.".format(name)
super(Door, self).__init__('d', name, x, y, description, 1)
self.passable = not locked
self.key = key
self.graphic = graphic
def lock(self, key = None):
if key == self.key:
self.passable = not self.passable
if self.descBase == None:
if self.passable:
self.description = "The {0} is unlocked.".format(self.name)
else:
self.description = "The {0} is locked.".format(self.name)
else:
if self.passable:
self.description += " It is unlocked.".format(self.name)
else:
self.description += " It is locked.".format(self.name)
return True
return False
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y)}
# save graphic
graphic = {}
if node.graphic[0] != Door.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != Door.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != Door.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
# save door state
if node.passable:
ret['locked'] = not node.passable
if node.descBase != None:
ret['description'] = node.descBase
if node.key != None:
ret['key'] = node.key
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
description = None
locked = False
key = None
bgc = Door.defaultGraphic[0]
fgc = Door.defaultGraphic[1]
shape = Door.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
# load door state
if 'description' in parts:
description = parts['description']
if 'locked' in parts:
locked = parts['locked']
if 'key' in parts:
key = parts['key']
return cls(parts['name'], parts['location'][0], parts['location'][1],
locked, description, key, graphic)
class MapExit(Thing):
yaml_flag = u'!MapExit'
defaultGraphic = ('clear', '#FF0000', 'x')
def __init__(self, name, x: int, y: int, exitid: int, destination: str, prefix = None, onUse = '', key = True, graphic = defaultGraphic):
description = name
if prefix:
description = "{0} {1}".format(prefix, name)
super(MapExit, self).__init__('x', name, x, y, description, 5)
self.exitid = exitid
self.destination = destination
self.prefix = prefix
self.onUse = onUse
self.key = key
self.graphic = graphic
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'name': node.name, 'location': (node.x, node.y), 'id': node.exitid, 'destination': node.destination}
# save graphic
graphic = {}
if node.graphic[0] != MapExit.defaultGraphic[0]:
graphic['bgc'] = node.graphic[0]
if node.graphic[1] != MapExit.defaultGraphic[1]:
graphic['fgc'] = node.graphic[1]
if node.graphic[2] != MapExit.defaultGraphic[2]:
graphic['shape'] = node.graphic[2]
if len(graphic) > 0:
ret['graphic'] = graphic
if node.prefix != None:
ret['prefix'] = node.prefix
if node.onUse != '':
ret['onUse'] = node.onUse
if node.key != True:
ret['key'] = node.key
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
prefix = None
onUse = ''
key = True
bgc = MapExit.defaultGraphic[0]
fgc = MapExit.defaultGraphic[1]
shape = MapExit.defaultGraphic[2]
# load graphic
if 'graphic' in parts:
if 'bgc' in parts['graphic']:
bgc = parts['graphic']['bgc']
if 'fgc' in parts['graphic']:
fgc = parts['graphic']['fgc']
if 'shape' in parts['graphic']:
shape = parts['graphic']['shape']
graphic = (bgc, fgc, shape)
if 'prefix' in parts:
prefix = parts['prefix']
if 'onUse' in parts:
onUse = parts['onUse']
if 'key' in parts:
key = parts['key']
return cls(parts['name'], parts['location'][0], parts['location'][1],
parts['id'], parts['destination'], prefix, onUse, key, graphic)
class MapEntrance(Thing):
yaml_flag = u'!MapEntrance'
defaultGraphic = ('clear', 'clear', 'x')
# no graphic - should not be drawn
def __init__(self, x: int, y: int, exitid: int, name = None):
if name == None:
name = 'entrance {}'.format(exitid)
super(MapEntrance, self).__init__('a', name, x, y, '', 1)
self.exitid = exitid
@classmethod
def to_yaml(cls, representer, node):
# save usual things
ret = {'location': (node.x, node.y), 'id': node.exitid}
return representer.represent_mapping(cls.yaml_flag, ret)
@classmethod
def from_yaml(cls, constructor, node):
parts = CommentedMap()
constructor.construct_mapping(node, parts, True)
# set default values for optional arguments
return cls(parts['location'][0], parts['location'][1], parts['id'])
class PlayerCharacter(Character):
"""Player object. Cannot be created with yaml."""
defaultGraphic = ('clear', '#0000FF', 'o')
def __init__(self, **kwargs):
if 'name' not in kwargs:
kwargs['name'] == 'You'
if 'graphic' not in kwargs:
kwargs['graphic'] = PlayerCharacter.defaultGraphic
super(PlayerCharacter, self).__init__(thingType = 'p', flags = 5, **kwargs)

102
gameutil.py Normal file
View file

@ -0,0 +1,102 @@
# gameutil.py
# This is a home for nice helper functions that were useful, but also cluttered
# the GameBase class.
import re as _re
coordRegex = _re.compile(r'(?:(-?[a-zA-Z]+) ?(-?[0-9]+))|(?:(-?[0-9]+),? (-?[0-9]+))|(?:\(([0-9]+), ([0-9]+)\))')
def letterToNumber(letter):
if letter.isdecimal():
return int(letter)
ret = 0
sign = 1
start = 0
for i in range(len(letter)):
if letter[i] == '-':
sign = -sign
start -= 1
elif letter[i].isalpha():
if letter[i].isupper():
ret += (ord(letter[i]) - ord('A')) * (26**(i+start))
else:
ret += (ord(letter[i]) - ord('a')) * (26**(i+start))
else:
return ret * sign
return ret * sign
def numberToLetter(number):
if isinstance(number, str):
return number
ret = ''
sign = ''
if number == 0:
return 'A'
elif number < 0:
sign = '-'
number = -number
while number > 0:
ret += chr(ord('A') + number % 26)
number = int(number / 26)
return sign + ret
def parseCoords(level, args, usePlayerCoords = True, player = None):
"""Takes an argument list, and figures out what it's talking about.
Returns (thing, x, y). "Thing" can be None."""
if level == None:
raise ValueError('Coordinates cannot be parsed because there is no map.')
x = -1
y = -1
# first, try by name for objects in the map
name = ' '.join(args)
thing = level.getThingByName(name)
if thing != None:
if usePlayerCoords:
return thing, thing.playerx, thing.playery
else:
return thing, thing.x, thing.y
# Second, try by name for objects in the inventory
if player != None:
if name in player.thingNames:
return player.getThingByName(name), -1, -1
# Third, try by location
coordStr = args[0]
if len(args) > 1:
coordStr += ' ' + args[1]
match = coordRegex.match(coordStr)
if match != None: # if coordinates were given
groups = match.groups()
ind = 0
for i in range(len(groups)):
if groups[i] == None or groups[i] == match.group():
continue
else:
ind = i
break
x = letterToNumber(groups[ind])
y = letterToNumber(groups[ind+1])
thing = level.getThingAtCoords(x, y)
if thing != None and usePlayerCoords:
return thing, thing.playerx, thing.playery
else:
return thing, x, y
else: # if a name was given
return None, -1, -1
def startDialog(thing, outstream, dialog):
if 'dialogs' in thing.customValues:
if isinstance(thing.customValues['dialogs'], list):
if 'dialogPtr' in thing.customValues and isinstance(thing.customValues['dialogPtr'], int):
dialog(thing.customValues['dialogs'][thing.customValues['dialogPtr']], thing)
else:
dialog(thing.customValues['dialogs'][0], thing)
elif isinstance(thing.customValues['dialogs'], str):
dialog(thing.customValues['dialogs'], thing)
else:
raise GameError("Character '{}' has dialog of an unexpected type.".format(thing.name))
else:
print("{} has nothing to say to you.".format(thing.name), file = outstream)

44
locusdemo.py Normal file
View file

@ -0,0 +1,44 @@
from gamelocus import *
def demo(loc):
print(loc)
allpoints = [i for i in loc]
print("Contains: Iter: ")
for row in range(10):
for col in range(10):
if (col, 9 - row) in loc:
print("##", end = "")
else:
print("..", end = "")
for col in range(10):
if (col, 9 - row) in allpoints:
print("##", end = "")
else:
print("..", end = "")
print()
demo(LineLocus(1, 1, 8, 8))
demo(LineLocus(1, 1, 8, 8, True))
demo(LineLocus(1, 8, 8, 1))
demo(LineLocus(1, 8, 8, 1, True))
demo(LineLocus(1, 3, 8, 6))
demo(LineLocus(1, 3, 8, 6, True))
demo(LineLocus(1, 6, 8, 3))
demo(LineLocus(1, 6, 8, 3, True))
demo(LineLocus(3, 1, 6, 8))
demo(LineLocus(3, 1, 6, 8, True))
demo(LineLocus(1, 5, 8, 5))
demo(LineLocus(1, 5, 8, 5, True))
demo(RectLocus(1, 1, 8, 8))
demo(FilledRectLocus(1, 1, 8, 8))
demo(RectLocus(1, 3, 8, 6))
demo(FilledRectLocus(1, 3, 8, 6))
demo(CircleLocus(4, 4, 4))
demo(CircleLocus(4, 4, 1))
demo(CircleLocus(4, 4, 0.6))
demo(CircleLocus(4, 4, 0.4))
demo(FilledCircleLocus(4, 4, 4))
demo(FilledCircleLocus(4, 4, 1))
demo(FilledCircleLocus(4, 4, 0.6))
demo(FilledCircleLocus(4, 4, 0.4))
demo(SetLocus(((1, 1), (1, 8), (8, 8), (8, 1))))

View file

@ -47,7 +47,19 @@ loadAlways:
name: guy name: guy
description: a guy description: a guy
location: [4, 20] location: [4, 20]
behavior: wander behaviors:
none: none # might this work to prevent this character from doing anything?
customValues: customValues:
dialogs: testing/testDialog.yml dialogs: testing/testDialog.yml
- !NPC
name: follower
description: a follower
location: [6, 26]
behaviors:
go: [-1, follow]
arrive: [-1, follow]
customValues:
follow:
distance: 2
isFollowing: True
target: You # yes, YOU!

View file

@ -15,7 +15,7 @@ replies:
opener: I would, but there's still a bunch of foreshadowing and stuff we opener: I would, but there's still a bunch of foreshadowing and stuff we
have to get through first. have to get through first.
action: back action: back
script: set tellmore += 1 script: set global tellmore += 1
- case: else - case: else
opener: You really want to know more, don't you? opener: You really want to know more, don't you?
action: back action: back