#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 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): 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.flags = flags self.graphic = ThingGraphic('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 @property def passable(self): return bool(self.flags & 1) @property def talkable(self): return bool(self.flags & 2) @property def lookable(self): return bool(self.flags & 4) @property def takeable(self): return bool(self.flags & 8) @property def useable(self): return bool(self.flags & 16) def fromPrefab(self, prefab): """This only exists as a catch-all in case an unsupported thing type is prefabed.""" parts = prefab.details # set default values for optional arguments name = self.name description = self.description playerx = prefab.x + (self.playerx - self.x) playery = prefab.y + (self.playery - self.y) bgc = self.graphic.background fgc = self.graphic.foreground shape = self.graphic.shape if parts != None: if 'name' in parts: name = parts['name'] if 'description' in parts: description = parts['description'] # 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'] if 'useLocation' in parts: playerx, playery = parts['useLocation'] graphic = ThingGraphic(bgc, fgc, shape) return Thing(self.thingType, name, prefab.x, prefab.y, description, self.flags, playerx, playery) class Prefab(Thing): """Basically a placeholder for a predefined prefab. This is for reading in YAML.""" yaml_flag = u'!Prefab' defaultGraphic = ThingGraphic('clear', 'clear', 'x') def __init__(self, name: str, x: int, y: int, **kwargs): super(Prefab, self).__init__('z', name, x, y, '', 1) # 'p' is already taken by the player. self.details = None # This is so that custom overrides can be made for specific instances. if "details" in kwargs: self.details = kwargs["details"] @classmethod def to_yaml(cls, representer, node): # save usual things ret = {'name': node.name, 'location': (node.x, node.y), 'details': self.details} 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 mdetails = None if 'details' in parts: mdetails = dict(parts['details']) return cls(parts['name'], parts['location'][0], parts['location'][1], details = mdetails) 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 = ThingGraphic('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.background != Item.defaultGraphic.background: graphic['bgc'] = node.graphic.background if node.graphic.foreground != Item.defaultGraphic.forground: graphic['fgc'] = node.graphic.foreground if node.graphic.shape != Item.defaultGraphic.shape: graphic['shape'] = node.graphicshape 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.background fgc = Item.defaultGraphic.foreground shape = Item.defaultGraphic.shape # 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 = ThingGraphic(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: ranged = 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 def fromPrefab(self, prefab): parts = prefab.details # set default values for optional arguments name = self.name useFunc = self.useFunc useOnFunc = self.useOnFunc customValues = dict(self.customValues) ranged = self.ranged bgc = self.graphic.background fgc = self.graphic.foreground shape = self.graphic.shape description = self.description if parts: if 'name' in parts: name = parts['name'] if 'description' in parts: description = parts['description'] # 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'] # 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: ranged = parts['ranged'] graphic = ThingGraphic(bgc, fgc, shape) return Item(name, prefab.x, prefab.y, description, useFunc, useOnFunc, customValues, ranged, graphic) class Useable(Thing): yaml_flag = u'!Useable' defaultGraphic = ThingGraphic('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.background != Useable.defaultGraphic.background: graphic['bgc'] = node.graphic.background if node.graphic.foreground != Useable.defaultGraphic.foreground: graphic['fgc'] = node.graphic.foreground if node.graphic.shape != Useable.defaultGraphic.shape: graphic['shape'] = node.graphic.shape 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.background fgc = Useable.defaultGraphic.foreground shape = Useable.defaultGraphic.shape # 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 = ThingGraphic(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 def fromPrefab(self, prefab): parts = prefab.details # set default values for optional arguments name = self.name description = self.description useFunc = self.useFunc customValues = dict(self.customValues) playerx = prefab.x + (self.playerx - self.x) playery = prefab.y + (self.playery - self.y) bgc = self.graphic.background fgc = self.graphic.foreground shape = self.graphic.shape if parts != None: if 'name' in parts: name = parts['name'] if 'description' in parts: description = parts['description'] # 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'] # 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'] graphic = ThingGraphic(bgc, fgc, shape) return Useable(name, prefab.x, prefab.y, description, useFunc, customValues, playerx, playery, graphic) class Character(Thing): defaultGraphic = ThingGraphic('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 = {} #{str: int} # 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): if thingID in self.__inventory: return self.__inventory[thingID] else: return None 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(ret.thingID) if len(self.thingNames[ret.name]) == 0: del self.thingNames[ret.name] del self.__inventory[ret.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 = ThingGraphic('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.background != Useable.defaultGraphic.background: graphic['bgc'] = node.graphic.background if node.graphic.foreground != Useable.defaultGraphic.foreground: graphic['fgc'] = node.graphic.foreground if node.graphic.shape != Useable.defaultGraphic.shape: graphic['shape'] = node.graphic.shape 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 minventory = [] mcustomValues = {} mplayerx, mplayery = parts['location'] bgc = NPC.defaultGraphic.background fgc = NPC.defaultGraphic.foreground shape = NPC.defaultGraphic.shape # 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 = ThingGraphic(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) def fromPrefab(self, prefab): parts = prefab.details # set default values for optional arguments mname = self.name mdescription = self.description minventory = list(self.tempInventory) mcustomValues = dict(self.customValues) mbehaviors = dict(self.behaviors) mplayerx = prefab.x + (self.playerx - self.x) mplayery = prefab.y + (self.playery - self.y) bgc = self.graphic.background fgc = self.graphic.foreground shape = self.graphic.shape if prefab != None: if 'name' in parts: mname = parts['name'] if 'description' in parts: mdescription = parts['description'] # 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'] # 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'] if 'behaviors' in parts: mbehaviors = dict(parts['behaviors']) mgraphic = ThingGraphic(bgc, fgc, shape) return NPC(name = mname, x = prefab.x, y = prefab.y, description = mdescription, behaviors = mbehaviors, tempInventory = minventory, customValues = mcustomValues, playerx = mplayerx, plyery = mplayery, graphic = mgraphic) class Door(Thing): yaml_flag = u'!Door' defaultGraphic = ThingGraphic('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.background != Useable.defaultGraphic.background: graphic['bgc'] = node.graphic.background if node.graphic.foreground != Useable.defaultGraphic.foreground: graphic['fgc'] = node.graphic.foreground if node.graphic.shape != Useable.defaultGraphic.shape: graphic['shape'] = node.graphic.shape 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.background fgc = Door.defaultGraphic.foreground shape = Door.defaultGraphic.shape # 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 = ThingGraphic(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 = ThingGraphic('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.background != Useable.defaultGraphic.background: graphic['bgc'] = node.graphic.background if node.graphic.foreground != Useable.defaultGraphic.foreground: graphic['fgc'] = node.graphic.foreground if node.graphic.shape != Useable.defaultGraphic.shape: graphic['shape'] = node.graphic.shape 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.background fgc = MapExit.defaultGraphic.foreground shape = MapExit.defaultGraphic.shape # 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 = ThingGraphic(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 = ThingGraphic('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 #self.graphic = MapEntrance.defaultGraphic @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 = ThingGraphic('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)