Skip to content

Core Classes

This is the code reference for the Open Mafia Engine's core classes.

open_mafia_engine.core.game

Game

Defines the state of an entire game, including the execution context.

Parameters

gen_phases : Callable[[Game], AbstractPhaseSystem] A function that creates a phase system, given a Game. Use AbstractPhaseSystem.gen(*args, **kwargs) to create this function. The default is SimplePhaseCycle.gen(), which creates a day-night cycle.

Attributes

event_engine : EventEngine The event engine, which handles all subscription and broadcasting. action_queue : ActionQueue The base action queue, which holds all core-level actions & all history. phase_system : AbstractPhaseSystem System that defines phases and their transitions. actors : List[Actor] All the actors (players) of the game. factions : List[Faction] All the factions (teams) of the game. aux : AuxHelper Helper object for auxiliary game objects.

add(self, obj: GameObject)

Adds the object to this game.

This is automatically called during obj.__init__()

Source code in core/game.py
def add(self, obj: GameObject):
    """Adds the object to this game.

    This is automatically called during `obj.__init__()`
    """
    if isinstance(obj, Actor):
        if obj not in self._actors:
            self._actors.append(obj)
    elif isinstance(obj, Faction):
        if obj not in self._factions:
            self._factions.append(obj)
    elif isinstance(obj, AuxObject):
        self._aux.add(obj)

change_phase(self, new_phase: Optional[Phase] = None)

Changes the phase to the given one (or bumps it). This causes events.

Source code in core/game.py
def change_phase(self, new_phase: Optional[Phase] = None):
    """Changes the phase to the given one (or bumps it). This causes events."""
    # NOTE: You can pass strings here, because ETryPhaseChange.__init__ converts
    self.process_event(ETryPhaseChange(self, new_phase=new_phase))

load(file: Union[Path, str, IO[bytes]]) -> Game classmethod

Cloudpickle-based loading

Source code in core/game.py
@classmethod
def load(cls, file: Union[Path, str, IO[bytes]]) -> Game:
    """Cloudpickle-based loading"""

    if isinstance(file, Path):
        with file.open(mode="rb") as f:
            return cls.load(f)
    elif isinstance(file, str):
        return cls.load(Path(file))
    else:
        res = cloudpickle.load(file)
        if not isinstance(res, cls):
            raise TypeError(f"Wrong object was pickled: {file!r}")
        return res

process_event(self, event: Event, *, process_now: bool = False)

Processes the action.

Source code in core/game.py
def process_event(self, event: Event, *, process_now: bool = False):
    """Processes the action."""
    responses: List[Action] = self.event_engine.broadcast(event)
    for resp in responses:
        self.action_queue.enqueue(resp)

    process_now = (
        process_now
        or self.current_phase.action_resolution == ActionResolutionType.instant
        or isinstance(event, ETryPhaseChange)
    )
    if process_now:
        self.action_queue.process_all()

save(self, file: Union[Path, str, IO[bytes]])

Cloudpickle-based saving

Source code in core/game.py
def save(self, file: Union[Path, str, IO[bytes]]):
    """Cloudpickle-based saving"""
    if isinstance(file, Path):
        with file.open(mode="wb") as f:
            self.save(f)
    elif isinstance(file, str):
        return self.save(Path(file))
    else:
        cloudpickle.dump(self, file)

open_mafia_engine.core.game_object

GameObject

Base class for game objects.

GameObjectMeta

Metaclass for game objects.

inject_converters(func: Callable) -> Callable

Decorator that adds converters for all possible types.

Source code in core/game_object.py
def inject_converters(func: Callable) -> Callable:
    """Decorator that adds converters for all possible types."""

    if hasattr(func, "__is_converting__"):
        return func

    @wraps(func)
    def wrapper(*args, **kwargs):
        # Work with signature
        sig = inspect.signature(func)
        sb = sig.bind(*args, **kwargs)
        sb.apply_defaults()  # we want to convert the default,s too!
        # Get type hints
        # FIXME: Unsure whether this will work for external subclasses.
        type_hints = get_type_hints(func, localns=_get_ns())

        game_param = sig.parameters.get("game")

        if game_param is None:
            self_param = sig.parameters.get("self")
            if self_param is None:
                raise TypeError(f"`game` or `self` are required, got {sig!r}")
            self: GameObject = sb.arguments["self"]
            if not isinstance(self, GameObject):
                raise TypeError(f"`self` must be GameObject, got {self!r}")
            game: Game = self.game
        else:
            game: Game = sb.arguments["game"]
        params = list(sig.parameters.values())

        nargs = []
        nkw = {}
        for p in params:
            val = sb.arguments[p.name]
            if p.kind == inspect.Parameter.VAR_POSITIONAL:
                # TODO: special behavior for sequence of params
                # e.g. f(game, *actors: List[Actor])
                nargs.extend(val)
            elif p.kind == inspect.Parameter.VAR_KEYWORD:
                # TODO: special behavior for mapping of params
                # e.g. f(game, **maps: Dict[str, Actor])
                nkw.update(val)
            else:
                th = type_hints.get(p.name, _BAD_HINT)
                if converter.can_convert_to(th):
                    val = converter.convert(game, th, val)
                    # except MafiaConverterError:
                    #     # TODO: Pre-check instead?
                    #     pass

                if p.kind == inspect.Parameter.POSITIONAL_ONLY:
                    nargs.append(val)
                elif p.name in sb.kwargs:
                    nkw[p.name] = val
                else:
                    nargs.append(val)

        res = func(*nargs, **nkw)
        return res

    setattr(wrapper, "__is_converting__", True)  # FIXME
    return wrapper

open_mafia_engine.core.event_system

Action

Core action object.

Attributes

game : Game source : GameObject The object that generated this action. priority : float canceled : bool

Abstract Methods

doit(self) -> None Performs the action.

Class Attributes

Pre : Type[EPreAction] Post : Type[EPostAction] Pre- and post-action event classes to use with action.pre and action.post. You may override these with your own when subclassing.

Post

Post-action event.

Pre

Pre-action event.

doit(self)

Performs the action.

Source code in core/event_system.py
@abstractmethod
def doit(self):
    """Performs the action."""

generate(func: Callable, name: str = None, doc: str = None) -> Type[Action] classmethod

Create an Action subtype from a function.

Parameters

func : Callable Function that does something. :) name : str What name to use. By default, will auto-generate a name from the func name. doc : str Docstring. By default, will use func doc, with prepended Action info.

Source code in core/event_system.py
@classmethod
def generate(
    cls, func: Callable, name: str = None, doc: str = None
) -> Type[Action]:
    """Create an Action subtype from a function.

    Parameters
    ----------
    func : Callable
        Function that does something. :)
    name : str
        What name to use. By default, will auto-generate a name from the func name.
    doc : str
        Docstring. By default, will use func doc, with prepended Action info.
    """

    if name is None:
        # NOTE: We can add random bits at the end to avoid conflicts
        # But this might mess up serialization?
        # from uuid import uuid4
        # rand_ = str(uuid4()).replace("-", "")[-8:]
        name = f"{cls.__name__}_{func.__name__}"

    if doc is None:
        doc = "(GENERATED ACTION) " + (func.__doc__ or "")

    # Get all signatures, parameters to merge
    sig_core = inspect.signature(cls.__init__)
    params_core = list(sig_core.parameters.values())
    sig_func = inspect.signature(func)
    params_func = list(sig_func.parameters.values())

    # Make sure the function has "self" as 0th arg
    if (len(params_func) < 1) or (params_func[0].name != "self"):
        raise TypeError(f"Function requires `self` argument to be first.")
    # Give a hint via warnings :)
    if params_func[0].annotation is None:
        warnings.warn(
            "We suggest you annotate like this to improve code editor experience:\n"
            f"  {func.__name__}(self: Action, ...)"
        )

    # Generate the list of params by merging
    DEFAULTS = {k: v.default for k, v in sig_core.parameters.items()}
    DEFAULTS.update({k: v.default for k, v in sig_func.parameters.items()})
    params_res: List[inspect.Parameter] = []
    attr_names = []
    kinds = [
        inspect.Parameter.POSITIONAL_ONLY,
        inspect.Parameter.POSITIONAL_OR_KEYWORD,
        inspect.Parameter.VAR_POSITIONAL,
        inspect.Parameter.KEYWORD_ONLY,
        inspect.Parameter.VAR_KEYWORD,
    ]
    i_core = 0
    i_func = 0
    for kind in kinds:
        while (i_core < len(params_core)) and (params_core[i_core].kind == kind):
            params_res.append(params_core[i_core])
            i_core += 1
        while (i_func < len(params_func)) and (params_func[i_func].kind == kind):
            pfi = params_func[i_func]
            conflict_params = [p for p in params_res if p.name == pfi.name]
            if len(conflict_params) == 0:
                params_res.append(pfi)
                attr_names.append(pfi.name)
            elif len(conflict_params) == 1:
                # conflict_params[0].default = pfi.default, except can't set param!
                conf = conflict_params[0]
                replacement = conf.replace(default=pfi.default)
                params_res[params_res.index(conf)] = replacement
                # don't add it - will conflict
            i_func += 1
    # Note: we make sure that we don't have duplicate *args, **kwargs
    sig_res = inspect.Signature(params_res)

    @wraps(cls.__init__, new_sig=sig_res)
    def __init__(
        self,
        game,
        /,
        *args,
        priority: float = DEFAULTS.get("priority", 0.0),
        canceled: bool = DEFAULTS.get("canceled", False),
        **kwargs,
    ):
        super(type(self), self).__init__(game, priority=priority, canceled=canceled)

        # Set attributes
        bs = sig_res.bind(
            self, game, *args, priority=priority, canceled=canceled, **kwargs
        )  # FIXME: Should we pass in `game`?
        bs.apply_defaults()
        for attr_name in attr_names:
            setattr(self, attr_name, bs.arguments[attr_name])
        # NOTE: We can't keep the signature, because someone might change
        # the arguments on the class instance; we have to "re-parse" instead

        # Housekeeping
        self._func = func
        self._sig = sig_res
        self._attr_names = tuple(attr_names)

    def doit(self):
        """Performs the action (generated from function)."""

        func = self._func
        sig = self._sig
        attr_names = self._attr_names

        # "re-parse" the signature
        args = []
        kwargs = {}
        for k, p in sig.parameters.items():
            if k not in attr_names:
                continue
            v = getattr(self, k)
            if p.kind in [
                inspect.Parameter.POSITIONAL_ONLY,
                inspect.Parameter.POSITIONAL_OR_KEYWORD,
            ]:
                args += v
            elif p.kind == inspect.Parameter.VAR_POSITIONAL:
                args.extend(v)
            elif p.kind == inspect.Parameter.KEYWORD_ONLY:
                kwargs[k] = v
            elif p.kind == inspect.Parameter.VAR_KEYWORD:
                kwargs.update(v)

        func(self, *args, **kwargs)

    GeneratedAction = type(
        name, (cls,), {"__init__": __init__, "doit": doit, "__doc__": doc}
    )
    return GeneratedAction

post(self) -> EPostAction

Create a post-event for this action.

Source code in core/event_system.py
def post(self) -> EPostAction:
    """Create a post-event for this action."""
    return self.Post(self.game, self)

pre(self) -> EPreAction

Create a pre-event for this action.

Source code in core/event_system.py
def pre(self) -> EPreAction:
    """Create a pre-event for this action."""
    return self.Pre(self.game, self)

ActionInspector

Helper to inspect Action objects.

ignored_args: List[str] property readonly

Arguments that are ignored

param_names: List[str] property readonly

Parameter names.

type_hints: Dict[str, Type] property readonly

Type hints, without ignored arguments.

extract_value(self, param: str) -> Any

Gets value for the parameter.

Source code in core/event_system.py
def extract_value(self, param: str) -> Any:
    """Gets value for the parameter."""
    # TODO: Make this smarter? :)
    return getattr(self.action, param)

params_of_type(self, T: Type) -> List[str]

Returns parameter names that have the given type.

Source code in core/event_system.py
def params_of_type(self, T: Type) -> List[str]:
    """Returns parameter names that have the given type."""

    def chk(v):
        try:
            return issubclass(v, T)
        except Exception:
            return False

    return [k for k, v in self.type_hints.items() if chk(v)]

set_value(self, param: str, obj: Any)

Sets value for the parameter.

Source code in core/event_system.py
def set_value(self, param: str, obj: Any):
    """Sets value for the parameter."""
    # TODO: Make this smarter? :)
    if not hasattr(self.action, param):
        warnings.warn(f"Action has no parameter {param!r}, setting anyways.")
    return setattr(self.action, param, obj)

ActionQueue

Action queue.

queue : List[Action] Actions are sorted by decreasing priority. Ties are broken by insertion order. history : List[Action] Actions are stored in the order they were processed in, including sub-queues.

add_history(self, actions: List[Action])

Adds the actions to history.

Source code in core/event_system.py
def add_history(self, actions: List[Action]):
    """Adds the actions to history."""
    self._history.extend(actions)

enqueue(self, action: Action)

Add an action to the queue.

Source code in core/event_system.py
def enqueue(self, action: Action):
    """Add an action to the queue."""
    if not isinstance(action, Action):
        raise TypeError(f"Expected Action, got {action!r}")
    self._queue.add(action)

peek_batch(self) -> List[Action]

Peek at the next batch of actions, without removing them.

If there are no more actions, this returns an empty list.

Source code in core/event_system.py
def peek_batch(self) -> List[Action]:
    """Peek at the next batch of actions, without removing them.

    If there are no more actions, this returns an empty list.
    """
    if len(self._queue) == 0:
        return []
    res = []
    i = 0
    priority = self._queue[0].priority
    while (i < len(self._queue)) and (self._queue[i].priority == priority):
        res.append(self._queue[i])
    return res

pop_batch(self) -> List[Action]

Gets the next batch of actions, removing them from the queue.

If there are no more actions, this returns an empty list.

Source code in core/event_system.py
def pop_batch(self) -> List[Action]:
    """Gets the next batch of actions, removing them from the queue.

    If there are no more actions, this returns an empty list.
    """
    if len(self._queue) == 0:
        return []

    res = []
    priority = self._queue[0].priority
    while (len(self._queue) > 0) and (self._queue[0].priority == priority):
        action: Action = self._queue.pop(0)
        res.append(action)
    return res

CancelAction

Action that cancels other actions.

doit(self)

Performs the action.

Source code in core/event_system.py
def doit(self):
    self.target.canceled = True

ConditionalCancelAction

Cancels the action, but checks condition just in case again.

If condition(action), actually does cancel the action.

doit(self)

Performs the action.

Source code in core/event_system.py
def doit(self):
    if self.condition(self.target):
        super().doit()

Constraint

Base class for constraints.

Override check and return a self.Violation("Description") or None.

Note that each Constraint is itself a Subscriber, so it technically can have its own constraints, but by default they're not used.

prefix_tags: List[str] property readonly

These are tags used for descriptions, as a prefix.

Violation

Constraint was violated.

check(self, action: Action) -> Optional[Constraint.Violation]

Checks whether the action is allowed.

If allowed, return None. If not allowed, return a Constraint.Violation

Source code in core/event_system.py
@abstractmethod
def check(self, action: Action) -> Optional[Constraint.Violation]:
    """Checks whether the `action` is allowed.

    If allowed, return None.
    If not allowed, return a `Constraint.Violation`
    """

handler_post(event: EPostAction) -> Optional[List[Action]]

handler_pre(event: EPreAction) -> Optional[List[Action]]

hook_post_action(self, action: Action) -> Optional[List[Action]]

Hook called when parent successfully actioned.

Source code in core/event_system.py
def hook_post_action(self, action: Action) -> Optional[List[Action]]:
    """Hook called when parent successfully actioned."""

hook_pre_action(self, action: Action) -> Optional[List[Action]]

Hook called when parent is trying to action & no violation for self.

Source code in core/event_system.py
def hook_pre_action(self, action: Action) -> Optional[List[Action]]:
    """Hook called when parent is trying to action & no violation for self."""

EPostAction

Post-action event.

EPreAction

Pre-action event.

Event

Core event object.

EventEngine

Subscription and broadcasting engine.

add_handler(self, handler: EventHandler, parent: Subscriber) -> _HandlerFunc

Adds the handler, with given parent, to own subscribers.

Source code in core/event_system.py
def add_handler(self, handler: EventHandler, parent: Subscriber) -> _HandlerFunc:
    """Adds the handler, with given parent, to own subscribers."""
    f = partial(handler.func, parent)
    for etype in handler.etypes:
        self._handlers[etype].append(f)

    if parent not in self._handlers[etype]:
        self._subscribers[etype].append(parent)
    return f

broadcast(self, event: Event) -> List[Action]

Broadcasts event to all handlers.

Source code in core/event_system.py
def broadcast(self, event: Event) -> List[Action]:
    """Broadcasts event to all handlers."""

    # Loop over superclasses, but make sure you don't repeat handlers
    funcs = []  # NOTE: not using a set, because we want deterministic sorting
    ET = type(event)
    for T in ET.mro():
        if issubclass(T, Event):
            funcs += [h for h in self._handlers[T] if h not in funcs]

    # Call each of the functions
    res = []
    for f in funcs:
        x = f(event)
        if x is None:
            x = []
        res.extend(x)
    return res

remove_subscriber(self, sub: Subscriber)

Removes all subscriptions from the subscriber.

FIXME: This operation is very hackish and iterates over EVERYTHING. This can probably be fixed by adding back-references, somehow.

Source code in core/event_system.py
def remove_subscriber(self, sub: Subscriber):
    """Removes all subscriptions from the subscriber.

    FIXME: This operation is very hackish and iterates over EVERYTHING.
    This can probably be fixed by adding back-references, somehow.
    """
    hfs = sub.handler_funcs
    for etype in self._subscribers.keys():
        try:
            self._subscribers[etype].remove(sub)
            for hf in hfs:
                try:
                    self._handlers[etype].remove(hf)
                except ValueError:
                    pass
        except ValueError:
            pass
    sub._handler_funcs = []

EventHandler

Descriptor that implements event handling logic.

Subscriber

Base class for objects that listen to events.

Creating Event Handlers

Subclass from this and use a handler or handles decorator. The following result in the same calls behavior:

1
2
3
4
5
6
7
8
class MySub(Subscriber):
    @handles(EPreAction)
    def handler_1(self, event) -> Optional[List[Action]]:
        return None

    @handler
    def handler_2(self, event: EPreAction):
        return []
Adding Constraints

Constraints are added after the Subscriber is created.

use_default_constraints: bool property readonly

Whether this object is using default constraints for this class.

add_default_constraints(self)

Adds default constraints for this type. Override for your own types.

Source code in core/event_system.py
def add_default_constraints(self):
    """Adds default constraints for this type. Override for your own types."""

check_constraints(self, action: Action) -> List[Constraint.Violation]

Checks all constraints. All violations are returned.

Source code in core/event_system.py
def check_constraints(self, action: Action) -> List[Constraint.Violation]:
    """Checks all constraints. All violations are returned."""
    res = []
    for con in self._constraints:
        r = con.check(action)
        if r is not None:
            res.append(r)
    return res

get_handlers() -> List[EventHandler] classmethod

Returns all event handlers for this class.

Source code in core/event_system.py
@classmethod
def get_handlers(cls) -> List[EventHandler]:
    """Returns all event handlers for this class."""
    res = []
    for T in cls.mro():
        for x in T.__dict__.values():
            if isinstance(x, EventHandler):
                res.append(x)
    return res

handler(func: _HandlerFunc) -> EventHandler

Decorator to automatically infer event handler.

Usage

Use this as a decorator with a mandatory type hint for event:

1
2
3
4
class A(Subscriber):
    @handler
    def f(self, event: Union[EPreAction, EPostAction]):
        return []
Source code in core/event_system.py
def handler(func: _HandlerFunc) -> EventHandler:
    """Decorator to automatically infer event handler.

    Usage
    -----
    Use this as a decorator with a mandatory type hint for `event`:

        class A(Subscriber):
            @handler
            def f(self, event: Union[EPreAction, EPostAction]):
                return []
    """

    type_hints = get_type_hints(func)
    th = type_hints.get("event")
    if th is None:
        raise TypeError("Type hint for 'event' is required; otherwise, use `handles()`")
    if get_origin(th) is None:
        if issubclass(th, Event):
            return EventHandler(func, th)
    elif get_origin(th) is Union:
        etypes = get_args(th)
        for a in etypes:
            if not issubclass(a, Event):
                raise TypeError(f"One of Union types is not Event: {th!r}")
        return EventHandler(func, *etypes)
    raise NotImplementedError(f"Unsupported type hint: {th!r}")

handles(*etypes: List[Event]) -> Callable[[_HandlerFunc], EventHandler]

Decorator factory, to handle events.

Usage

Use this as a decorator factory with the event types as arguments:

1
2
3
4
class A(Subscriber):
    @handles(EPreAction, EPostAction)
    def f(self, event) -> Optional[List[Action]]:
        return None
Source code in core/event_system.py
def handles(*etypes: List[Event]) -> Callable[[_HandlerFunc], EventHandler]:
    """Decorator factory, to handle events.

    Usage
    -----
    Use this as a decorator factory with the event types as arguments:

        class A(Subscriber):
            @handles(EPreAction, EPostAction)
            def f(self, event) -> Optional[List[Action]]:
                return None
    """

    def _inner(func: _HandlerFunc) -> EventHandler:
        return EventHandler(func, *etypes)

    return _inner

open_mafia_engine.core.state

Ability

Basic Ability object.

Attributes

game owner : Actor name : str The Ability's name. desc : str Description. Default is "".

argument_names: List[str] property readonly

Names of arguments. User-facing.

activate(self, *args, **kwargs) -> Optional[List[Action]]

Activate this ability with some arguments.

Make the signature be the same as the Action's. If generated, it should match already.

Source code in core/state.py
@abstractmethod
def activate(self, *args, **kwargs) -> Optional[List[Action]]:
    """Activate this ability with some arguments.

    Make the signature be the same as the Action's.
    If generated, it should match already.
    """

generate(action_or_func: Union[Type[Action], Callable], params: List[str] = None, name: str = None, doc: str = None, desc: str = None) -> Type[Ability] classmethod

Create an Ability subtype from an Action or function.

Parameters

action_or_func : Type[Action] or Callable If an Action subclass, uses it directly. If a callable (function), generates an action type and uses it. params : List[str] The names of parameters to leave as activation parameters. The rest will be taken as arguments for the Ability itself.

Source code in core/state.py
@classmethod
def generate(
    cls,
    action_or_func: Union[Type[Action], Callable],
    params: List[str] = None,
    name: str = None,
    doc: str = None,
    desc: str = None,
) -> Type[Ability]:
    """Create an Ability subtype from an Action or function.

    Parameters
    ----------
    action_or_func : Type[Action] or Callable
        If an Action subclass, uses it directly.
        If a callable (function), generates an action type and uses it.
    params : List[str]
        The names of parameters to leave as activation parameters.
        The rest will be taken as arguments for the Ability itself.
    """

    # Create an action type
    if isinstance(action_or_func, type) and issubclass(action_or_func, Action):
        TAction = action_or_func
    elif callable(action_or_func):
        gen_name: str = name if name else f"{action_or_func.__name__}_Action"
        TAction = Action.generate(action_or_func, name=gen_name, doc=doc)
    else:
        raise TypeError(f"Expected Action or function, got {action_or_func!r}")
    TAction: Type[Action]

    # Fix input arguments
    if params is None:
        params = []
    if name is None:
        name = f"{cls.__name__}_{TAction.__name__}"
        # TODO - add random bits to avoid conflict?
    if doc is None:
        doc = "(GENERATED ABILITY) " + (TAction.__doc__ or "")

    # Split the signature into __init__() and activate() args
    sig_action = inspect.signature(TAction.__init__)
    par_action = list(sig_action.parameters.values())

    par_activate = [
        inspect.Parameter("self", kind=inspect.Parameter.POSITIONAL_OR_KEYWORD)
    ] + [p for p in par_action if p.name in params]
    sig_activate = inspect.Signature(
        par_activate, return_annotation=Optional[List[Action]]
    )

    sig_init = "TODO"  # TODO: Actually create a proper init signature!

    # TODO: Set sig_init
    def __init__(self, game: Game, /, owner: Actor, name: str, desc: str = desc):
        super().__init__(game, owner, name, desc=desc)

    @with_signature(sig_activate)
    def activate(self, *args, **kwargs) -> Optional[List[Action]]:
        """Activate this ability with some arguments."""

        try:
            return [self.TAction(self.game, self, *args, **kwargs)]
        except Exception as e:
            logger.exception("Error executing action:")
            if False:  # Set for debugging errors :)
                raise
            return None

    GeneratedAbility = type(
        name, (cls,), {"__init__": __init__, "activate": activate, "__doc__": doc}
    )
    GeneratedAbility.TAction = TAction

    return GeneratedAbility

handle_activate(event: EActivate) -> Optional[List[Action]]

Handler to activate this ability.

Actor

Actor object.

add(self, obj: Union[Ability, Trigger, Faction])

Adds

Source code in core/state.py
def add(self, obj: Union[Ability, Trigger, Faction]):
    """Adds """
    if isinstance(obj, Ability):
        self.add_ability(obj)
    elif isinstance(obj, Trigger):
        self.add_trigger(obj)
    elif isinstance(obj, Faction):
        warnings.warn("Better to do `Faction.add_actor(me)` directly.")
        obj.add_actor(self)
    else:
        raise TypeError(f"Expected Ability or Trigger, got {obj!r}")

add_ability(self, ability: Ability)

Adds this ability to self, possibly removing the old owner.

Source code in core/state.py
def add_ability(self, ability: Ability):
    """Adds this ability to self, possibly removing the old owner."""
    if not isinstance(ability, Ability):
        raise TypeError(f"Expected Ability, got {ability!r}")
    if ability in self._abilities:
        return
    self._abilities.append(ability)
    if ability._owner is not self:
        ability._owner._abilities.remove(ability)
        ability._owner = self

add_trigger(self, trigger: Trigger)

Adds this trigger to self, possibly removing the old owner.

Source code in core/state.py
def add_trigger(self, trigger: Trigger):
    """Adds this trigger to self, possibly removing the old owner."""
    if not isinstance(trigger, Trigger):
        raise TypeError(f"Expected Trigger, got {trigger!r}")
    if trigger in self._triggers:
        return
    self._triggers.append(trigger)
    if trigger._owner is not self:
        trigger._owner._triggers.remove(trigger)
        trigger._owner = self

ATBase

Base object for abilities and triggers.

Attributes

game owner : Actor name : str The name of this object. desc : str Description. Default is "".

argument_names: List[str] property readonly

Names of arguments used when calling. Default is empty.

prefix_tags: List[str] property readonly

These are used for descriptions.

add_default_constraints(self)

Adds default constraints for this type. Override for your own types.

Source code in core/state.py
def add_default_constraints(self):
    super().add_default_constraints()
    # Default constraints for abilities and triggers:
    ConstraintOwnerAlive(self.game, self)
    ConstraintActorTargetsAlive(self.game, self)

full_description(self, full_constraints: bool = False, highlight_method: Callable = None) -> str

Gets the full description of the Action/Trigger.

If full_constraints, also adds the full descriptions of all constraints. By default, though, it skips these, adding only the prefix tags.

Source code in core/state.py
def full_description(
    self, full_constraints: bool = False, highlight_method: Callable = None
) -> str:
    """Gets the full description of the Action/Trigger.

    If `full_constraints`, also adds the full descriptions of all constraints.
    By default, though, it skips these, adding only the prefix tags.
    """
    prestr = ", ".join(self.prefix_tags)
    if prestr == "":
        prestr = "Any"
    arg_names = self.argument_names
    abil_targs = ", ".join(arg_names)
    if abil_targs != "":
        abil_targs = f" ({abil_targs})"

    if highlight_method is None:
        name = self.name
    else:
        name = highlight_method(self.name)

    res = f"[{prestr}] {name}{abil_targs}: {self.desc}"
    if full_constraints:
        fcd = []
        # TODO: Long descriptions for constraints
        fcd = [indent(x, ".   ") for x in fcd]
        res = "\n".join([res] + fcd)
    return res

ATConstraint

Constraint for Actions and Triggers. Will raise if used elsewhere.

owner: Actor property readonly

The owner of this constraint's parent.

ConstraintActorTargetsAlive

Any targets for the action, if they are Actors, must be alive.

check(self, action: Action) -> Optional[Constraint.Violation]

Checks whether the action is allowed.

If allowed, return None. If not allowed, return a Constraint.Violation

Source code in core/state.py
def check(self, action: Action) -> Optional[Constraint.Violation]:
    ai = ActionInspector(action)
    p2a: Dict[str, Actor] = ai.values_of_type(Actor)
    bads = []
    for p, a in p2a.items():
        if a.status["dead"]:  # default is None, which is falsy
            bads.append(f"{p!r} ({a.name!r})")
    if len(bads) > 0:
        return self.Violation("Targets are dead: " + ", ".join(bads))

ConstraintOwnerAlive

The ability's (trigger's) owner must be alive.

check(self, action: Action) -> Optional[Constraint.Violation]

Checks whether the action is allowed.

If allowed, return None. If not allowed, return a Constraint.Violation

Source code in core/state.py
def check(self, action: Action) -> Optional[Constraint.Violation]:
    if self.owner.status["dead"]:  # default is None, which is falsey
        return self.Violation(f"{self.owner.name!r} (owner) is dead.")

EActivate

Event of ability activation.

That is, this event is triggered by a player trying to activate their Ability.

EStatusChange

The Status has changed for some Actor.

Faction

Faction, a.k.a. Alignment.

OutcomeChecker

Checks for Faction Outcome. Base class.

Status

dict-like representation of an actor's status.

Access of empty attribs gives None. Changing an attribute emits an EStatusChange event.

Attributes

parent: Actor attribs : dict Raw keyword arguments for the status.

Trigger

Basic Trigger object.

Attributes

game : Game owner : Actor name : str The Trigger's name. desc : str Description. Default is "".

prefix_tags: List[str] property readonly

These are used for descriptions.

open_mafia_engine.core.phase_cycle

AbstractPhaseSystem

Interface for a phase system.

It's possible to see all phases by using game.phase_system.possible_phases

current_phase: Phase property writable

Returns the current phase.

possible_phases: Iterable[Phase] property readonly

Returns all possible phases (as a new iterable).

If it is infinite, override getitem as well!

bump_phase(self) -> Phase

Updates the phase to use the next one, then returns the current one.

Source code in core/phase_cycle.py
@abstractmethod
def bump_phase(self) -> Phase:
    """Updates the phase to use the next one, then returns the current one."""

gen(*args, **kwargs) -> Callable[[Game], AbstractPhaseSystem] classmethod

Create a callable that generates a phase cycle.

Source code in core/phase_cycle.py
@classmethod
def gen(cls, *args, **kwargs) -> Callable[[Game], AbstractPhaseSystem]:
    """Create a callable that generates a phase cycle."""

    def func_gen(game: Game) -> AbstractPhaseSystem:
        return cls(game, *args, **kwargs)

    return func_gen

system_phase_change(event: ETryPhaseChange) -> Optional[List[PhaseChangeAction]]

Some external system asked for a phase change.

EPostPhaseChange

Phase has changed.

EPrePhaseChange

Phase is about to change.

ETryPhaseChange

Try to change the phase.

Phase

Represents a monolithic "phase" of action.

Attributes

name : str The current phase name. action_resolution : ActionResolutionType One of {"instant", "end_of_phase"}

PhaseChangeAction

Action to change the phase.

Parameters

new_phase : None or Phase The resulting phase. By default, None uses the next phase. old_phase : Phase The phase that this action was created in.

Post

Phase has changed.

Pre

Phase is about to change.

doit(self)

Performs the action.

Source code in core/phase_cycle.py
def doit(self):
    if self.new_phase is None:
        self.game.phase_system.bump_phase()
    else:
        self.game.phase_system.current_phase = self.new_phase

SimplePhaseCycle

Simple phase cycle definition.

Parameters

game : Game cycle : None or List[Tuple[str, ActionResolutionType]] The cycle definition. Submit pairs of (name, resolution_type). By default, uses [("day", "instant"), ("night", "end_of_phase")]

current_phase: Phase property writable

Returns the current phase.

possible_phases: List[Phase] property readonly

Returns all possible phases (as a new iterable).

If it is infinite, override getitem as well!

bump_phase(self) -> Phase

Updates the phase to use the next one, then returns the current one.

Trying to bump on shutdown phase will be ignored.

Source code in core/phase_cycle.py
def bump_phase(self) -> Phase:
    """Updates the phase to use the next one, then returns the current one.

    Trying to bump on `shutdown` phase will be ignored.
    """
    if self._i == self._STARTUP:
        self._i = 0
        return self.current_phase
    elif self._i == self._SHUTDOWN:
        return self.current_phase
        # raise ValueError(f"Cannot bump shutdown phase: {self.shutdown}")
    self._i += 1
    return self.current_phase

gen(cycle: List[Tuple[str, ActionResolutionType]] = None, current_phase: Optional[str] = None) -> Callable[[Game], SimplePhaseCycle] classmethod

Generator for a simple phase cycle.

Source code in core/phase_cycle.py
@classmethod
def gen(
    cls,
    cycle: List[Tuple[str, ActionResolutionType]] = None,
    current_phase: Optional[str] = None,
) -> Callable[[Game], SimplePhaseCycle]:
    """Generator for a simple phase cycle."""
    return super().gen(cycle=cycle, current_phase=current_phase)

open_mafia_engine.core.auxiliary

AuxHelper

Auxiliary object helper.

add(self, obj: AuxObject)

Adds the aux object to self.

Source code in core/auxiliary.py
def add(self, obj: AuxObject):
    """Adds the aux object to self."""
    if not isinstance(obj, AuxObject):
        raise TypeError(f"Expected AuxObject, got {obj!r}")
    if self._key_map.get(obj.key) == obj:
        return
    elif obj.key in self._key_map.keys():
        # FIXME: different object with the same key!
        return
    elif obj in self._key_map.values():
        # FIXME: same object with different key? Key should be able to change.
        return
    # OK, let's add it
    if len(self) > self.max_objects:
        raise ValueError(f"Reached {self.max_objects} (max) aux objects!")
    self._key_map[obj.key] = obj

filter_by_type(self, T: Type[AuxObject]) -> List[AuxObject]

Returns all AuxObjects with the given type.

You can pass in Union types as well.

Source code in core/auxiliary.py
def filter_by_type(self, T: Type[AuxObject]) -> List[AuxObject]:
    """Returns all `AuxObject`s with the given type.

    You can pass in Union types as well.
    """

    if get_origin(T) == Union:
        T_raw = get_args(T)
        T = tuple(x for x in T_raw if x is not None)
        # T is a tuple of types

    def chk(x):
        try:
            return isinstance(x, T)
        except Exception:
            return False

    return [x for x in self._key_map.values() if chk(x)]

remove(self, obj: AuxObject)

Removes obj from self.

Source code in core/auxiliary.py
def remove(self, obj: AuxObject):
    """Removes `obj` from self."""
    if not isinstance(obj, AuxObject):
        raise TypeError(f"Expected AuxObject, got {obj!r}")
    found = self._key_map.get(obj.key)
    if found is None:
        return
    elif found == obj:
        del self._key_map[obj.key]
        obj._unsub()
    else:
        # FIXME: Object has different key, or another object has same key
        return

AuxObject

Base class for auxiliary objects.

Aux objects have a key they are retrievable by.

Attributes

game : Game key : str The key to retrieve the object by. If None (default), generates one.

generate_key() -> str classmethod

Generates a key for this class (used if None is passed in init).

Source code in core/auxiliary.py
@classmethod
def generate_key(cls) -> str:
    """Generates a key for this class (used if None is passed in __init__)."""
    return cls.__qualname__ + "_" + str(uuid4()).replace("-", "")

get_or_create(/, game: Game, key: str = None, *, use_default_constraints: bool = True, **kwargs) -> AuxObject classmethod

Finds the AuxObject by key or, if there is no object, creates it.

Source code in core/auxiliary.py
@classmethod
def get_or_create(
    cls,
    game: Game,
    /,
    key: str = None,
    *,
    use_default_constraints: bool = True,
    **kwargs,
) -> AuxObject:
    """Finds the AuxObject by key or, if there is no object, creates it."""
    if key is None:
        key = cls.generate_key()
    res = game.aux.get(key)
    if res is None:
        res = cls(
            game, key=key, use_default_constraints=use_default_constraints, **kwargs
        )
    if not isinstance(res, cls):
        raise TypeError(f"Wrong type for AuxObject: expected {cls!r}, got {res!r}")
    return res

RemoveAuxAction

Removes the aux object, at the end of the stack.

doit(self)

Performs the action.

Source code in core/auxiliary.py
def doit(self):
    self.game.aux.remove(self.target)

open_mafia_engine.core.outcome

EOutcomeAchieved

An outcome has been achieved.

OutcomeAction

A Faction achieves victory or defeat.

Post

An outcome has been achieved.

Pre

A Faction is about to achieve an Outcome.

doit(self)

Sets the "outcome" status for each Actor in Faction?

Source code in core/outcome.py
def doit(self):
    """Sets the "outcome" status for each Actor in Faction?"""
    for actor in self.faction.actors:
        actor.status["outcome"] = self.outcome

open_mafia_engine.core.ender

EGameEnded

The game has ended.

outcome_for(self, faction: Faction) -> Outcome

Returns the outcome for a given faction.

Source code in core/ender.py
@inject_converters
def outcome_for(self, faction: Faction) -> Outcome:
    """Returns the outcome for a given faction."""
    return self.outcomes[faction.name]

EndTheGame

Action that ends the game.

Maybe this should inherit from PhaseChangeAction? I'm skeptical. Ending the game is significantly different.

Post

The game has ended.

outcome_for(self, faction: Faction) -> Outcome

Returns the outcome for a given faction.

Source code in core/ender.py
@inject_converters
def outcome_for(self, faction: Faction) -> Outcome:
    """Returns the outcome for a given faction."""
    return self.outcomes[faction.name]

Pre

The game is about to end.

doit(self)

Ends the game, setting the phase to 'shutdown'.

Source code in core/ender.py
def doit(self):
    """Ends the game, setting the phase to 'shutdown'."""
    self.game.change_phase(self.game.phase_system.shutdown)

GameEnder

Ends the game when all factions get an Outcome.

handle_outcome(event: EOutcomeAchieved) -> Optional[List[EndTheGame]]

Checks off an outcome. If all factions have an outcome, ends the game.

open_mafia_engine.core.converters

get_ability_by_path(game: Game, obj: str) -> Ability

Gets the ability by 'path' made of names.

Assuming PATH_SEP is "/", this will parse as:

1
"{actor_name}/ability/{ability_name}"

This will do fuzzy matching on Actor and Ability separately. TODO: Maybe do fuzzy matching on total string?

Source code in core/converters.py
@converter.register
def get_ability_by_path(game: Game, obj: str) -> Ability:
    """Gets the ability by 'path' made of names.

    Assuming PATH_SEP is "/", this will parse as:

        "{actor_name}/ability/{ability_name}"

    This will do fuzzy matching on Actor and Ability separately.
    TODO: Maybe do fuzzy matching on total string?
    """
    try:
        owner_name, _abil, abil_name = get_parts(obj)

        assert _abil == ABILITY
    except Exception as e:
        raise ValueError(f"Bad/non-existing path for Ability: {obj}") from e

    owner: Actor = get_actor_by_name(game, owner_name)
    matcher = FuzzyMatcher({ab.name: ab for ab in owner.abilities}, score_cutoff=10)
    try:
        return matcher[abil_name]
    except Exception as e:
        raise ValueError(f"Could not find Ability by path: {obj!r}") from e

get_actor_by_name(game: Game, obj: str) -> Actor

Gets the Actor by exact or fuzzy name match.

Source code in core/converters.py
@converter.register
def get_actor_by_name(game: Game, obj: str) -> Actor:
    """Gets the Actor by exact or fuzzy name match."""
    matcher = FuzzyMatcher({a.name: a for a in game.actors}, score_cutoff=10)
    try:
        return matcher[obj]
    except Exception as e:
        raise ValueError(f"Could not find Actor by name: {obj!r}") from e

get_faction_by_name(game: Game, obj: str) -> Faction

Gets the Faction by exact or fuzzy name match.

Source code in core/converters.py
@converter.register
def get_faction_by_name(game: Game, obj: str) -> Faction:
    """Gets the Faction by exact or fuzzy name match."""

    matcher = FuzzyMatcher({f.name: f for f in game.factions}, score_cutoff=20)
    try:
        return matcher[obj]
    except Exception as e:
        raise ValueError(f"Could not find Faction by name: {obj!r}") from e

get_phase_by_name(game: Game, obj: str) -> Phase

Gets the phase by name from the cycle. Can raise KeyError.

Source code in core/converters.py
@converter.register
def get_phase_by_name(game: Game, obj: str) -> Phase:
    """Gets the phase by name from the cycle. Can raise KeyError."""
    try:
        return game.phase_system[obj]
    except KeyError:
        pass

    # Make sure we don't infinitely loop through phases
    N_MAX = 20
    options = {}
    for i, p in enumerate(game.phase_system.possible_phases):
        if i > N_MAX:
            warnings.warn(f"Found over {N_MAX} phases; maybe infinite?")
            break
        options[p.name] = p
    matcher = FuzzyMatcher(options, score_cutoff=50)
    return matcher[obj]

get_trigger_by_path(game: Game, obj: str) -> Trigger

Gets the trigger by 'path' made of names.

Assuming PATH_SEP is "/", this will parse as:

1
"{actor_name}/trigger/{trigger_name}"

This will do fuzzy matching on Actor and Trigger separately. TODO: Maybe do fuzzy matching on total string?

Source code in core/converters.py
@converter.register
def get_trigger_by_path(game: Game, obj: str) -> Trigger:
    """Gets the trigger by 'path' made of names.

    Assuming PATH_SEP is "/", this will parse as:

        "{actor_name}/trigger/{trigger_name}"

    This will do fuzzy matching on Actor and Trigger separately.
    TODO: Maybe do fuzzy matching on total string?
    """
    try:
        owner_name, _trig, trig_name = get_parts(obj)

        assert _trig == TRIGGER
    except Exception as e:
        raise ValueError(f"Bad/non-existing path for Trigger: {obj}") from e

    owner: Actor = get_actor_by_name(game, owner_name)
    matcher = FuzzyMatcher({tr.name: tr for tr in owner.triggers}, score_cutoff=10)
    try:
        return matcher[trig_name]
    except Exception as e:
        raise ValueError(f"Could not find Trigger by path: {obj!r}") from e

open_mafia_engine.core.enums

ActionResolutionType

How actions are resolved.

Outcome

Outcome (victory or defeat).

open_mafia_engine.core.naming