Permalink
Cannot retrieve contributors at this time
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
flax/flax/component.py /
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
755 lines (556 sloc)
24.1 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| """Component infrastructure and definitions. | |
| Game world objects are called "entities", and are broken into parts that each | |
| implement some interface (and respond to some events). This allows different | |
| types of entities to share only some of their behavior, without making an | |
| unholy mess of mixins and inheritance. | |
| Each part is called a "component", which is what's defined here. See the | |
| `Component` base class for an explanation of how they work, or just read over | |
| some of the component classes to get a feel for what's going on. | |
| """ | |
| from collections import defaultdict | |
| import logging | |
| import zope.interface as zi | |
| from flax.event import PickUp | |
| from flax.event import MeleeAttack, Damage, Die | |
| from flax.event import Ascend, Descend, Walk | |
| from flax.event import Open, Unlock | |
| from flax.event import Equip | |
| from flax.event import Unequip | |
| from flax.relation import RelationSubject | |
| from flax.relation import RelationObject | |
| from flax.relation import Wearing | |
| log = logging.getLogger(__name__) | |
| class GameOver(Exception): | |
| def __init__(self, message, *, success): | |
| super().__init__(message, success) | |
| self.message = message | |
| self.success = success | |
| ############################################################################### | |
| # Crazy plumbing begins here! | |
| # ----------------------------------------------------------------------------- | |
| # Component definitions | |
| # TODO distinguish between those that should only be altered with modifiers | |
| # (like stats), and those that are expected to change (like /current/ health | |
| # and inventory)? | |
| def static_attribute(doc): | |
| attr = zi.Attribute(doc) | |
| attr.setTaggedValue('mode', 'static') | |
| return attr | |
| # TODO none of these yet, but we should assert that they /are/ given as | |
| # properties of the class | |
| def derived_attribute(doc): | |
| attr = zi.Attribute(doc) | |
| attr.setTaggedValue('mode', 'derived') | |
| return attr | |
| class IComponentFactory(zi.Interface): | |
| """An object that produces components. Usually these are component | |
| classes, but sometimes they're wrapped in a `ComponentInitializer`. | |
| """ | |
| interface = zi.Attribute("The interface this component implements.") | |
| component = zi.Attribute("The real underlying component class.") | |
| def init_entity_type(entity_type): | |
| """Run the component's `__typeinit__` on the given entity type.""" | |
| def init_entity(entity): | |
| """Run the component's `__init__` on the given entity.""" | |
| def adapt(entity): | |
| """Create a component that wraps the given entity. | |
| This is the actual component constructor, since calling is used for | |
| something else. | |
| """ | |
| @zi.implementer(IComponentFactory) | |
| class ComponentInitializer: | |
| """What you get when you call a component class. Used as a deferred init | |
| mechanism. | |
| """ | |
| def __init__(self, component, args, kwargs): | |
| self.component = component | |
| self.args = args | |
| self.kwargs = kwargs | |
| @property | |
| def interface(self): | |
| return self.component.interface | |
| def init_entity_type(self, entity): | |
| self.component.init_entity_type(entity, *self.args, **self.kwargs) | |
| def init_entity(self, entity): | |
| self.component.init_entity(entity, *self.args, **self.kwargs) | |
| def adapt(self, entity): | |
| return self.component.adapt(entity) | |
| @zi.implementer(IComponentFactory) | |
| class ComponentMeta(type): | |
| """Metaclass for components. Implements the slightly weird bits, like the | |
| ruination of object creation. See `Component` for most of it. | |
| Note that calling a component class does NOT produce component objects -- | |
| it produces objects used for initializing entities later. Component | |
| objects are created with `adapt`. | |
| """ | |
| def __new__(meta, name, bases, attrs, *, interface=None): | |
| # Prevent assigning to arbitrary attributes, cut down on storage space | |
| # a bit, and make object creation (which we do a lot!) a bit faster | |
| attrs.setdefault('__slots__', ()) | |
| return super().__new__(meta, name, bases, attrs) | |
| def __init__(cls, name, bases, attrs, *, interface=None): | |
| if interface is None: | |
| # Try to fetch it from a parent class | |
| interface = cls.interface | |
| zi.implementer(interface)(cls) | |
| cls.interface = interface | |
| # Slap on an attribute descriptor for every static attribute in the | |
| # interface. (Derived attributes promise that they're computed by the | |
| # class via @property or some other mechanism.) | |
| for key in interface: | |
| attr = interface[key] | |
| if not isinstance(attr, zi.Attribute): | |
| continue | |
| mode = attr.queryTaggedValue('mode') | |
| if mode == 'static': | |
| if key in cls.__dict__: | |
| # TODO i have seen the light: there is a good reason to do | |
| # this. see HealthRender | |
| continue | |
| raise TypeError( | |
| "Implementation {!r} " | |
| "defines static attribute {!r}" | |
| .format(cls, key) | |
| ) | |
| else: | |
| setattr(cls, key, ComponentAttribute(attr)) | |
| def __call__(cls, *args, **kwargs): | |
| """Override object construction. We don't want to make a component | |
| object; we want to make something that can be used to initialize an | |
| entity later. That something is a `ComponentInitializer`. | |
| """ | |
| return ComponentInitializer(cls, args, kwargs) | |
| def init_entity(cls, entity, *args, **kwargs): | |
| """Initialize an entity. Calls the class's ``__init__`` method.""" | |
| if cls.__init__ is Component.__init__: | |
| # Micro-optimization: if there's no __init__, don't even try to | |
| # call it | |
| return | |
| self = cls.adapt(entity) | |
| self.__init__(*args, **kwargs) | |
| def init_entity_type(cls, entity_type, *args, **kwargs): | |
| """Initialize an entity. Calls the class's ``__typeinit__`` method.""" | |
| # TODO self.entity will actually be the class here which seems mega | |
| # janky. | |
| if cls.__typeinit__ is Component.__typeinit__: | |
| # Micro-optimization: if there's no __typeinit__, don't even try to | |
| # call it | |
| return | |
| # This is a slightly naughty use of `adapt`, since we're not passing an | |
| # entity, but EntityType has the same plumbing so it all works. | |
| self = cls.adapt(entity_type) | |
| self.__typeinit__(*args, **kwargs) | |
| def adapt(cls, entity): | |
| """The actual constructor. Creates a new component that wraps the | |
| given entity. Does not call ``__init__``. | |
| """ | |
| self = object.__new__(cls) | |
| self.entity = entity | |
| return self | |
| @property | |
| def component(cls): | |
| return cls | |
| class ComponentAttribute: | |
| def __init__(desc, zope_attribute): | |
| desc.zope_attribute = zope_attribute | |
| def __get__(desc, self, cls): | |
| if self is None: | |
| return desc | |
| attr = desc.zope_attribute | |
| try: | |
| value = self.entity[attr] | |
| except KeyError: | |
| raise AttributeError | |
| # TODO this doesn't seem right really. i think modifiers should really | |
| # be tracked separately, and removed by the relation destructor | |
| for relation_set in self.entity.relates_to.values(): | |
| for relation in relation_set: | |
| # TODO lol yeah this definitely won't work | |
| for mod in IEquipment(relation.to_entity).modifiers: | |
| value = mod.modify(attr, value) | |
| return value | |
| def __set__(desc, self, value): | |
| # TODO seems like this doesn't make sense for something subject to | |
| # modifiers? | |
| self.entity.component_data[desc.zope_attribute] = value | |
| class IComponent(zi.Interface): | |
| """Dummy base class for all component interfaces. | |
| A component interface specifies some small set of data and behavior that an | |
| entity might like to have. For example, there's an `IActor` interface that | |
| has a method, ``act``, for deciding what an entity might want to do. There | |
| are two basic implementations of this interface: one for monsters where the | |
| ``act`` method implements an AI, and one for the player where the ``act`` | |
| method merely returns an action based on player input. | |
| By breaking functionality into discrete components, different entity types | |
| can share some behavior (such as collision detection) without having to | |
| share all of it (such as AI). | |
| Component interfaces also specify what data may be stored on the entity. | |
| Place a `static_attribute` in your interface's class body, and your | |
| components will be able to read to and write from an attribute of that | |
| name. You don't have to worry about name collisions between different | |
| interfaces, either. | |
| An entity type can only have at most one component per interface at a time. | |
| Interfaces are also used to access components. If you have an entity, and | |
| you want its implementation of ``IFoo``, calling ``IFoo(entity)`` will | |
| produce an appropriate component object. | |
| """ | |
| class Component(metaclass=ComponentMeta, interface=IComponent): | |
| """Base class for all components. Take note: some unorthodox things are | |
| happening here. | |
| A component class must implement exactly one interface, specified by the | |
| ``interface`` kwarg in the class statement. (The interface is inherited by | |
| subclasses.) | |
| A component object acts like a "view" of an entity, able to access only the | |
| data specified in its interface (via `static_attribute`). That is, within | |
| a component method, ``self.prop`` will read from and write to a value | |
| stored within the underlying entity. The entity itself is also available, | |
| as ``self.entity``. | |
| Components aren't created the traditional way. Instead, they're built to | |
| act like part of an entity as transparently as possible. Consider: | |
| class ICombatant(IComponent): | |
| strength = static_attribute("Raw power") | |
| class Combatant(Component, interface=ICombatant): | |
| def __init__(self, *, strength): | |
| self.strength = strength | |
| newt = EntityType(Combatant(strength=3)) | |
| mind_flayer = EntityType(Combatant(strength=100)) | |
| The ``__init__`` method will never actually be called by this code. It's | |
| only called when a ``newt`` or ``mind_flayer`` entity is created, to | |
| initialize the combat part of that entity. Creating an entity thus | |
| triggers the ``__init__`` for each of its components, as though the entity | |
| were all of those components simultaneously. | |
| """ | |
| # Note: the constructor is ComponentMeta.adapt, which also assigns the | |
| # `entity` attribute. | |
| __slots__ = ('entity',) | |
| def __typeinit__(self): | |
| pass | |
| def __init__(self): | |
| pass | |
| def __setattr__(self, key, value): | |
| # TODO this approach will break inheriting ad hoc attributes in | |
| # subclasses; is that a concern? | |
| cls = type(self) | |
| if hasattr(cls, key): | |
| super().__setattr__(key, value) | |
| else: | |
| self.entity[cls, key] = value | |
| def __getattr__(self, key): | |
| cls = type(self) | |
| try: | |
| return self.entity[cls, key] | |
| except KeyError: | |
| raise AttributeError | |
| ############################################################################### | |
| # Particular interfaces and components follow. | |
| # ----------------------------------------------------------------------------- | |
| # Rendering | |
| class IRender(IComponent): | |
| # TODO consolidate these into a single some-kind-of-object | |
| sprite = static_attribute("") | |
| color = static_attribute("") | |
| class Render(Component, interface=IRender): | |
| def __typeinit__(self, sprite, color): | |
| self.sprite = sprite | |
| self.color = color | |
| class OpenRender(Component, interface=IRender): | |
| def __typeinit__(self, *, open, closed, locked): | |
| self.open = open | |
| self.closed = closed | |
| self.locked = locked | |
| @property | |
| def sprite(self): | |
| # TODO what if it doesn't exist | |
| if ILockable(self.entity).locked: | |
| return self.locked[0] | |
| # TODO what if it doesn't exist | |
| elif IOpenable(self.entity).open: | |
| return self.open[0] | |
| else: | |
| return self.closed[0] | |
| @property | |
| def color(self): | |
| # TODO what if it doesn't exist | |
| if ILockable(self.entity).locked: | |
| return self.locked[1] | |
| # TODO what if it doesn't exist | |
| elif IOpenable(self.entity).open: | |
| return self.open[1] | |
| else: | |
| return self.closed[1] | |
| class HealthRender(Component, interface=IRender): | |
| def __typeinit__(self, *choices): | |
| self.choices = [] | |
| total_weight = sum(choice[0] for choice in choices) | |
| for weight, sprite, color in choices: | |
| self.choices.append((weight / total_weight, sprite, color)) | |
| def current_rendering(self): | |
| health = ( | |
| ICombatant(self.entity).current_health / | |
| ICombatant(self.entity).maximum_health) | |
| for weight, sprite, color in self.choices: | |
| if health <= weight: | |
| return sprite, color | |
| health -= weight | |
| @property | |
| def sprite(self): | |
| return self.current_rendering()[0] | |
| @property | |
| def color(self): | |
| return self.current_rendering()[1] | |
| # ----------------------------------------------------------------------------- | |
| # Physics | |
| class IPhysics(IComponent): | |
| def blocks(actor): | |
| """Return True iff this object won't allow `actor` to move on top of | |
| it. | |
| """ | |
| # TODO i'm starting to think it would be nice to eliminate the dummy base class | |
| # i have for like every goddamn component? but how? | |
| # TODO seems like i should /require/ that every entity type has a IPhysics, | |
| # maybe others... | |
| class Physics(Component, interface=IPhysics): | |
| pass | |
| class Solid(Physics): | |
| def blocks(self, actor): | |
| # TODO i have /zero/ idea how passwall works here -- perhaps objects | |
| # should be made of distinct "materials"... | |
| return True | |
| class Empty(Physics): | |
| def blocks(self, actor): | |
| return False | |
| class DoorPhysics(Physics): | |
| def blocks(self, actor): | |
| return not IOpenable(self.entity).open | |
| @Walk.check(Solid) | |
| def cant_walk_through_solid_objects(event, _): | |
| event.cancel() | |
| @Walk.check(DoorPhysics) | |
| def cant_walk_through_closed_doors(event, door): | |
| if not IOpenable(door.entity).open: | |
| event.cancel() | |
| @Walk.perform(Physics) | |
| def do_walk(event, _): | |
| event.world.current_map.move(event.actor, event.target.position) | |
| # ----------------------------------------------------------------------------- | |
| # Map portal | |
| class IPortal(IComponent): | |
| destination = static_attribute("Name of the destination map.") | |
| class Portal(Component, interface=IPortal): | |
| def __init__(self, *, destination): | |
| self.destination = destination | |
| class PortalDownstairs(Portal): | |
| pass | |
| @Descend.perform(PortalDownstairs) | |
| def do_descend_stairs(event, portal): | |
| event.world.change_map(portal.destination) | |
| class PortalUpstairs(Portal): | |
| pass | |
| @Ascend.perform(PortalUpstairs) | |
| def do_ascend_stairs(event, portal): | |
| event.world.change_map(portal.destination) | |
| # ----------------------------------------------------------------------------- | |
| # Doors | |
| class IOpenable(IComponent): | |
| open = static_attribute("""Whether I'm currently open.""") | |
| class Openable(Component, interface=IOpenable): | |
| def __init__(self, *, open=False): | |
| self.open = open | |
| # TODO maybe this merits a check rule? maybe EVERYTHING does. | |
| # TODO only if closed | |
| @Open.perform(Openable) | |
| def do_open(event, openable): | |
| openable.open = True | |
| class ILockable(IComponent): | |
| locked = static_attribute("""Whether I'm currently locked.""") | |
| class Lockable(Component, interface=ILockable): | |
| def __init__(self, *, locked=False): | |
| self.locked = locked | |
| # TODO maybe this merits a check rule? maybe EVERYTHING does. | |
| # TODO only if closed | |
| @Unlock.perform(Lockable) | |
| def do_unlock(event, lockable): | |
| # TODO check that the key is a key, player holds it, etc. (inform has | |
| # touchability rules for all this...) | |
| lockable.locked = False | |
| # Destroy the key. TODO: need to be able to tell an entity that i'm taking | |
| # it away from whatever owns it, whatever that may mean! inform's "now" | |
| # does this | |
| IContainer(event.actor).inventory.remove(event.agent) | |
| @Open.check(Lockable) | |
| def cant_open_locked_things(event, lockable): | |
| if lockable.locked: | |
| log.info("it's locked") | |
| event.cancel() | |
| # ----------------------------------------------------------------------------- | |
| # Containment | |
| class IContainer(IComponent): | |
| inventory = static_attribute("Items contained by this container.") | |
| class Container(Component, interface=IContainer): | |
| # TODO surely this isn't called when something is polymorphed. right? | |
| # or... maybe it is, if the entity didn't have an IContainer before? | |
| def __init__(self): | |
| self.inventory = [] | |
| # ----------------------------------------------------------------------------- | |
| # Combat | |
| class ICombatant(IComponent): | |
| """Implements an entity's ability to fight and take damage.""" | |
| maximum_health = static_attribute("Entity's maximum possible health.") | |
| current_health = static_attribute("Current amount of health.") | |
| strength = static_attribute("Generic placeholder stat.") | |
| class Combatant(Component, interface=ICombatant): | |
| """Regular creature that has stats, fights with weapons, etc.""" | |
| def __typeinit__(self, *, health, strength): | |
| self.maximum_health = health | |
| self.current_health = health | |
| self.strength = strength | |
| # TODO need several things to happen with attributes here | |
| # 1. need to be able to pass them to Entity constructor | |
| # 2. not all attributes have or want a default | |
| # 3. some attributes are computed based on others | |
| # 4. some attributes want to be randomized in a range, i.e. need some sort | |
| # of constructor arguments that are used to compute an attribute but not | |
| # stored anywhere | |
| def lose_health(self, event): | |
| self.current_health -= event.amount | |
| # TODO i feel like this doesn't belong here (this definitely shouldn't | |
| # need to take an event object), but then where should it go? | |
| if self.current_health <= 0: | |
| event.world.queue_immediate_event(Die(self.entity)) | |
| @MeleeAttack.perform(Combatant) | |
| def do_melee_attack(event, combatant): | |
| opponent = ICombatant(event.actor) | |
| event.world.queue_immediate_event( | |
| Damage(combatant.entity, opponent.strength)) | |
| @MeleeAttack.announce(Combatant) | |
| def announce_melee_attack(event, combatant): | |
| log.info("{0} hits {1}".format( | |
| event.actor.type.name, combatant.entity.type.name)) | |
| @Damage.perform(Combatant) | |
| def do_damage(event, combatant): | |
| combatant.lose_health(event) | |
| @Die.perform(Combatant) | |
| def do_die(event, combatant): | |
| # TODO player death is a little different. should be a separate component, | |
| # probably, instead of special-casing here (yikes) | |
| # TODO i am not actually sure if using an exception here is a good idea | |
| from flax.entity import Player | |
| if combatant.entity.isa(Player): | |
| raise GameOver("you died :(", success=False) | |
| event.world.current_map.remove(combatant.entity) | |
| # TODO and drop inventory, and/or a corpse | |
| @Die.announce(Combatant) | |
| def announce_die(event, combatant): | |
| log.info("{} has died".format(combatant.entity.type.name)) | |
| class Breakable(Component, interface=ICombatant): | |
| def __typeinit__(self, *, health): | |
| self.maximum_health = health | |
| self.current_health = health | |
| # TODO breakables don't /have/ strength. is this separate? | |
| # TODO should interfaces/components be able to say they can only exist | |
| # for entities that also support some other interface? | |
| self.strength = 0 | |
| def __init__(self, health_fraction): | |
| self.current_health = health_fraction * self.maximum_health | |
| # ----------------------------------------------------------------------------- | |
| # AI | |
| class IActor(IComponent): | |
| """Implements an entity's active thought process. An entity with an | |
| `IActor` component can decide to perform actions on its own, and has a | |
| sense of speed and time. | |
| """ | |
| def act(world): | |
| """Return an action to be performed (i.e., an `Event` to be fired), or | |
| `None` to do nothing. | |
| it. | |
| """ | |
| class GenericAI(Component, interface=IActor): | |
| def act(self, world): | |
| from flax.geometry import Direction | |
| from flax.event import Walk | |
| from flax.event import MeleeAttack | |
| import random | |
| pos = world.current_map.find(self.entity).position | |
| player_pos = world.current_map.find(world.player).position | |
| for direction in Direction: | |
| if pos + direction == player_pos: | |
| world.queue_event(MeleeAttack(self.entity, direction)) | |
| return | |
| # TODO try to walk towards player | |
| world.queue_event(Walk(self.entity, random.choice(list(Direction)))) | |
| class PlayerIntelligence(Component, interface=IActor): | |
| def act(self, world): | |
| if world.player_action_queue: | |
| world.queue_immediate_event(world.player_action_queue.popleft()) | |
| # ----------------------------------------------------------------------------- | |
| # Items | |
| class IPortable(IComponent): | |
| """Entity can be picked up and placed in containers.""" | |
| class Portable(Component, interface=IPortable): | |
| pass | |
| # TODO maybe "actor" could just be an event target, and we'd need fewer | |
| # duplicate events for the source vs the target? | |
| @PickUp.perform(Portable) | |
| def do_pick_up(event, portable): | |
| from flax.entity import Layer | |
| assert portable.entity.type.layer is Layer.item | |
| event.world.current_map.remove(portable.entity) | |
| IContainer(event.actor).inventory.append(portable.entity) | |
| @PickUp.announce(Portable) | |
| def announce_pick_up(event, portable): | |
| log.info("ooh picking up {}".format(portable.entity.type.name)) | |
| # ----------------------------------------------------------------------------- | |
| # Equipment | |
| class IBodied(IComponent): | |
| wearing = derived_attribute("") | |
| # TODO this direly wants, like, a list of limbs and how many | |
| class Bodied(Component, interface=IBodied): | |
| wearing = RelationSubject(Wearing) | |
| class IEquipment(IComponent): | |
| # worn_by? | |
| modifiers = static_attribute("Stat modifiers granted by this equipment.") | |
| class Equipment(Component, interface=IEquipment): | |
| # TODO i think this should live on Bodied as a simple dict of body part to | |
| # equipment | |
| # TODO problem is that if the player loses the item /for any reason | |
| # whatsoever/, the item needs to vanish from the dict. ALSO, the existence | |
| # of the item in the dict can block some other actions. | |
| worn_by = RelationObject(Wearing) | |
| def __typeinit__(self, *, modifiers=None): | |
| self.modifiers = modifiers or () | |
| # TODO recurring problems with events: | |
| # - what happens if something changes and the check is no | |
| # longer valid by the time the handler runs? :S | |
| # - similarly, what happens to events when an actor vanishes before | |
| # they get to fire? that's an ongoing problem -- maybe should be | |
| # using weak properties for all the bound entities, and invalidating | |
| # the event when any entity vanishes | |
| # TODO must be holding the armor... or standing on it? | |
| @Equip.check(Equipment) | |
| def equipper_must_have_body_part(event, equipment): | |
| # TODO need to implement slots | |
| if IBodied not in event.actor: | |
| log.info("you can't wear that") | |
| event.cancel() | |
| @Equip.check(Equipment) | |
| def equipment_must_not_be_worn(event, equipment): | |
| if equipment.worn_by: | |
| log.info("that's already being worn") | |
| event.cancel() | |
| @Equip.perform(Equipment) | |
| def put_on_equipment(event, equipment): | |
| equipment.worn_by.add(event.actor) | |
| @Equip.announce(Equipment) | |
| def equipment_success(event, equipment): | |
| log.info("you put on the armor") | |
| @Unequip.check(Equipment) | |
| def can_only_equip_whats_equipped(event, equipment): | |
| if event.actor not in equipment.worn_by: | |
| log.info("you're not wearing the armor!") | |
| event.cancel() | |
| @Unequip.perform(Equipment) | |
| def take_off_equipment(event, equipment): | |
| self.worn_by.remove(event.actor) | |
| @Unequip.announce(Equipment) | |
| def unequip_success(event, equipment): | |
| log.info("you take off the armor") |