Source code for LiSE.engine

# This file is part of LiSE, a framework for life simulation games.
# Copyright (c) Zachary Spector,  public@zacharyspector.com
"""The "engine" of LiSE is an object relational mapper with special
stores for game data and entities, as well as properties for manipulating the
flow of time.

"""
from functools import partial
from collections import defaultdict
from operator import attrgetter
from types import FunctionType, MethodType

import umsgpack
from blinker import Signal
from allegedb import ORM as gORM
from .util import reify, sort_set

from . import exc


class NoPlanningAttrGetter:
    __slots__ = ('_real',)

    def __init__(self, attr, *attrs):
        self._real = attrgetter(attr, *attrs)

    def __call__(self, obj):
        if obj._planning:
            raise exc.PlanError("Don't use randomization in a plan")
        return self._real(obj)


def getnoplan(attribute_name):
    return property(NoPlanningAttrGetter(attribute_name))


[docs]class InnerStopIteration(StopIteration): pass
[docs]class NextTurn(Signal): """Make time move forward in the simulation. Calls ``advance`` repeatedly, returning a list of the rules' return values. I am also a ``Signal``, so you can register functions to be called when the simulation runs. Pass them to my ``connect`` method. """ def __init__(self, engine): super().__init__() self.engine = engine def __call__(self): engine = self.engine start_branch, start_turn, start_tick = engine.btt() latest_turn = engine._turns_completed[start_branch] if start_turn < latest_turn: engine.turn += 1 self.send( engine, branch=engine.branch, turn=engine.turn, tick=engine.tick ) return [], engine.get_delta( branch=start_branch, turn_from=start_turn, turn_to=engine.turn, tick_from=start_tick, tick_to=engine.tick ) elif start_turn > latest_turn + 1: raise exc.RulesEngineError("Can't run the rules engine on any turn but the latest") if start_turn == latest_turn: # As a side effect, the following assignment sets the tick to # the latest in the new turn, which will be 0 if that turn has not # yet been simulated. engine.turn += 1 if engine.tick == 0: engine.universal['rando_state'] = engine._rando.getstate() else: engine._rando.setstate(engine.universal['rando_state']) with engine.advancing(): for res in iter(engine.advance, final_rule): if res: engine.universal['last_result'] = res engine.universal['last_result_idx'] = 0 engine.universal['rando_state'] = engine._rando.getstate() branch, turn, tick = engine.btt() self.send( engine, branch=branch, turn=turn, tick=tick ) return res, engine.get_delta( branch=start_branch, turn_from=start_turn, turn_to=turn, tick_from=start_tick, tick_to=tick ) engine._turns_completed[start_branch] = engine.turn engine.query.complete_turn(start_branch, engine.turn) self.send( self.engine, branch=engine.branch, turn=engine.turn, tick=engine.tick ) return [], engine.get_delta( branch=engine.branch, turn_from=start_turn, turn_to=engine.turn, tick_from=start_tick, tick_to=engine.tick )
[docs]class DummyEntity(dict): """Something to use in place of a node or edge""" __slots__ = ['engine'] def __init__(self, engine): self.engine = engine
[docs]class FinalRule: """A singleton sentinel for the rule iterator""" __slots__ = [] def __hash__(self): # completely random integer return 6448962173793096248
final_rule = FinalRule() MSGPACK_TUPLE = 0x00 MSGPACK_FROZENSET = 0x01 MSGPACK_SET = 0x02 MSGPACK_EXCEPTION = 0x03 MSGPACK_CHARACTER = 0x7f MSGPACK_PLACE = 0x7e MSGPACK_THING = 0x7d MSGPACK_PORTAL = 0x7c MSGPACK_FINAL_RULE = 0x7b MSGPACK_FUNCTION = 0x7a MSGPACK_METHOD = 0x79 MSGPACK_TRIGGER = 0x78 MSGPACK_PREREQ = 0x77 MSGPACK_ACTION = 0x76
[docs]class AbstractEngine(object): """Parent class to the real Engine as well as EngineProxy. Implements serialization methods and the __getattr__ for stored methods. By default, the deserializers will refuse to create LiSE entities. If you want them to, use my ``loading`` property to open a ``with`` block, in which deserialized entities will be created as needed. """ from contextlib import contextmanager
[docs] @contextmanager def loading(self): """Context manager for when you need to instantiate entities upon unpacking""" if getattr(self, '_initialized', False): raise ValueError("Already loading") self._initialized = False yield self._initialized = True
def __getattr__(self, item): meth = super().__getattribute__('method').__getattr__(item) return MethodType(meth, self) def _pack_character(self, char): return umsgpack.Ext(MSGPACK_CHARACTER, umsgpack.packb(char.name, ext_handlers=self._pack_handlers)) def _pack_place(self, place): return umsgpack.Ext(MSGPACK_PLACE, umsgpack.packb( (place.character.name, place.name), ext_handlers=self._pack_handlers )) def _pack_thing(self, thing): return umsgpack.Ext(MSGPACK_THING, umsgpack.packb( (thing.character.name, thing.name), ext_handlers=self._pack_handlers )) def _pack_portal(self, port): return umsgpack.Ext(MSGPACK_PORTAL, umsgpack.packb( (port.character.name, port.orig, port.dest), ext_handlers=self._pack_handlers )) def _pack_tuple(self, tup): return umsgpack.Ext(MSGPACK_TUPLE, umsgpack.packb(list(tup), ext_handlers=self._pack_handlers)) def _pack_frozenset(self, frozs): return umsgpack.Ext(MSGPACK_FROZENSET, umsgpack.packb(list(frozs), ext_handlers=self._pack_handlers)) def _pack_set(self, s): return umsgpack.Ext(MSGPACK_SET, umsgpack.packb(list(s), ext_handlers = self._pack_handlers)) def _pack_exception(self, exc): return umsgpack.Ext(MSGPACK_EXCEPTION, umsgpack.packb( [exc.__class__.__name__] + list(exc.args), ext_handlers=self._pack_handlers )) def _pack_func(self, func): return umsgpack.Ext({ 'method': MSGPACK_METHOD, 'function': MSGPACK_FUNCTION, 'trigger': MSGPACK_TRIGGER, 'prereq': MSGPACK_PREREQ, 'action': MSGPACK_ACTION }[func.__module__], umsgpack.packb(func.__name__)) def _pack_meth(self, func): return umsgpack.Ext(MSGPACK_METHOD, umsgpack.packb(func.__name__)) def _unpack_char(self, ext): charn = umsgpack.unpackb(ext.data, ext_handlers=self._unpack_handlers) try: return self.character[charn] except KeyError: if getattr(self, '_initialized', True): raise return self.char_cls(self, charn) def _unpack_place(self, ext): charn, placen = umsgpack.unpackb(ext.data, ext_handlers=self._unpack_handlers) try: char = self.character[charn] except KeyError: if getattr(self, '_initialized', True): raise return self.place_cls(self.char_cls(self, charn), placen) try: return char.place[placen] except KeyError: if getattr(self, '_initialized', True): raise return self.place_cls(char, placen) def _unpack_thing(self, ext): charn, thingn = umsgpack.unpackb(ext.data, ext_handlers=self._unpack_handlers) try: char = self.character[charn] except KeyError: if getattr(self, '_initialized', True): raise return self.thing_cls(self.char_cls(self, charn), thingn) try: return char.thing[thingn] except KeyError: if getattr(self, '_initialized', True): raise return self.thing_cls(char, thingn) def _unpack_portal(self, ext): charn, orign, destn = umsgpack.unpackb(ext.data, ext_handlers=self._unpack_handlers) try: char = self.character[charn] except KeyError: if getattr(self, '_initialized', True): raise char = self.char_cls(self, charn) try: return char.portal[orign][destn] except KeyError: if getattr(self, '_initialized', True): raise return self.portal_cls(char, orign, destn) def _unpack_trigger(self, ext): return getattr(self.trigger, umsgpack.unpackb(ext.data)) def _unpack_prereq(self, ext): return getattr(self.prereq, umsgpack.unpackb(ext.data)) def _unpack_action(self, ext): return getattr(self.action, umsgpack.unpackb(ext.data)) def _unpack_function(self, ext): return getattr(self.function, umsgpack.unpackb(ext.data)) def _unpack_method(self, ext): return getattr(self.method, umsgpack.unpackb(ext.data)) def _unpack_tuple(self, ext): return tuple(umsgpack.unpackb(ext.data, ext_handlers=self._unpack_handlers)) def _unpack_frozenset(self, ext): return frozenset(umsgpack.unpackb(ext.data, ext_handlers=self._unpack_handlers)) def _unpack_set(self, ext): return set(umsgpack.unpackb(ext.data, ext_handlers=self._unpack_handlers)) def _unpack_exception(self, ext): excs = { # builtin exceptions 'AssertionError': AssertionError, 'AttributeError': AttributeError, 'EOFError': EOFError, 'FloatingPointError': FloatingPointError, 'GeneratorExit': GeneratorExit, 'ImportError': ImportError, 'IndexError': IndexError, 'KeyError': KeyError, 'KeyboardInterrupt': KeyboardInterrupt, 'MemoryError': MemoryError, 'NameError': NameError, 'NotImplementedError': NotImplementedError, 'OSError': OSError, 'OverflowError': OverflowError, 'RecursionError': RecursionError, 'ReferenceError': ReferenceError, 'RuntimeError': RuntimeError, 'StopIteration': StopIteration, 'IndentationError': IndentationError, 'TabError': TabError, 'SystemError': SystemError, 'SystemExit': SystemExit, 'TypeError': TypeError, 'UnboundLocalError': UnboundLocalError, 'UnicodeError': UnicodeError, 'UnicodeEncodeError': UnicodeEncodeError, 'UnicodeDecodeError': UnicodeDecodeError, 'UnicodeTranslateError': UnicodeTranslateError, 'ValueError': ValueError, 'ZeroDivisionError': ZeroDivisionError, # LiSE exceptions 'NonUniqueError': exc.NonUniqueError, 'AmbiguousAvatarError': exc.AmbiguousAvatarError, 'AmbiguousUserError': exc.AmbiguousUserError, 'RulesEngineError': exc.RulesEngineError, 'RuleError': exc.RuleError, 'RedundantRuleError': exc.RedundantRuleError, 'UserFunctionError': exc.UserFunctionError, 'WorldIntegrityError': exc.WorldIntegrityError, 'CacheError': exc.CacheError, 'TravelException': exc.TravelException } data = umsgpack.unpackb(ext.data, ext_handlers=self._unpack_handlers) if data[0] not in excs: return Exception(*data) return excs[data[0]](*data[1:]) @reify def _unpack_handlers(self): return { MSGPACK_CHARACTER: self._unpack_char, MSGPACK_PLACE: self._unpack_place, MSGPACK_THING: self._unpack_thing, MSGPACK_PORTAL: self._unpack_portal, MSGPACK_FINAL_RULE: lambda obj: final_rule, MSGPACK_TUPLE: self._unpack_tuple, MSGPACK_FROZENSET: self._unpack_frozenset, MSGPACK_SET: self._unpack_set, MSGPACK_TRIGGER: self._unpack_trigger, MSGPACK_PREREQ: self._unpack_prereq, MSGPACK_ACTION: self._unpack_action, MSGPACK_FUNCTION: self._unpack_function, MSGPACK_METHOD: self._unpack_method, MSGPACK_EXCEPTION: self._unpack_exception } @reify def _pack_handlers(self): return { self.char_cls: self._pack_character, self.place_cls: self._pack_place, self.thing_cls: self._pack_thing, self.portal_cls: self._pack_portal, tuple: self._pack_tuple, frozenset: self._pack_frozenset, set: self._pack_set, FinalRule: lambda obj: umsgpack.Ext(MSGPACK_FINAL_RULE, b""), FunctionType: self._pack_func, MethodType: self._pack_meth, Exception: self._pack_exception } def pack(self, obj): return umsgpack.packb(obj, ext_handlers=self._pack_handlers) def unpack(self, bs): return umsgpack.unpackb(bs, ext_handlers=self._unpack_handlers)
[docs] def coinflip(self): """Return True or False with equal probability.""" return self.choice((True, False))
[docs] def roll_die(self, d): """Roll a die with ``d`` faces. Return the result.""" return self.randint(1, d)
[docs] def dice(self, n, d): """Roll ``n`` dice with ``d`` faces, and yield the results. This is an iterator. You'll get the result of each die in successon. """ for i in range(0, n): yield self.roll_die(d)
[docs] def dice_check(self, n, d, target, comparator='<='): """Roll ``n`` dice with ``d`` sides, sum them, and return whether they are <= ``target``. If ``comparator`` is provided, use it instead of <=. You may use a string like '<' or '>='. """ from operator import gt, lt, ge, le, eq, ne comps = { '>': gt, '<': lt, '>=': ge, '<=': le, '=': eq, '==': eq, '!=': ne } try: comparator = comps.get(comparator, comparator) except TypeError: pass return comparator(sum(self.dice(n, d)), target)
[docs] def percent_chance(self, pct): """Given a ``pct``% chance of something happening right now, decide at random whether it actually happens, and return ``True`` or ``False`` as appropriate. Values not between 0 and 100 are treated as though they were 0 or 100, whichever is nearer. """ if pct <= 0: return False if pct >= 100: return True return pct / 100 < self.random()
betavariate = getnoplan('_rando.betavariate') choice = getnoplan('_rando.choice') expovariate = getnoplan('_rando.expovariate') gammavariate = getnoplan('_rando.gammavariate') gauss = getnoplan('_rando.gauss') getrandbits = getnoplan('_rando.getrandbits') lognormvariate = getnoplan('_rando.lognormvariate') normalvariate = getnoplan('_rando.normalvariate') paretovariate = getnoplan('_rando.paretovariate') randint = getnoplan('_rando.randint') random = getnoplan('_rando.random') randrange = getnoplan('_rando.randrange') sample = getnoplan('_rando.sample') shuffle = getnoplan('_rando.shuffle') triangular = getnoplan('_rando.triangular') uniform = getnoplan('_rando.uniform') vonmisesvariate = getnoplan('_rando.vonmisesvariate') weibullvariate = getnoplan('_rando.weibullvariate')
[docs]class Engine(AbstractEngine, gORM): """LiSE, the Life Simulator Engine. Each instance of LiSE maintains a connection to a database representing the state of a simulated world. Simulation rules within this world are described by lists of Python functions, some of which make changes to the world. The top-level data structure within LiSE is the character. Most data within the world model is kept in some character or other; these will quite frequently represent people, but can be readily adapted to represent any kind of data that can be comfortably described as a graph or a JSON object. Every change to a character will be written to the database. LiSE tracks history as a series of turns. In each turn, each simulation rule is evaluated once for each of the simulated entities it's been applied to. World changes in a given turn are remembered together, such that the whole world state can be rewound: simply set the properties ``branch`` and ``turn`` back to what they were just before the change you want to undo. Properties: - ``branch``: The fork of the timestream that we're on. - ``turn``: Units of time that have passed since the sim started. - ``time``: ``(branch, turn)`` - ``tick``: A counter of how many changes have occurred this turn - ``character``: A mapping of :class:`Character` objects by name. - ``rule``: A mapping of all rules that have been made. - ``rulebook``: A mapping of lists of rules. They are followed in their order. A whole rulebook full of rules may be assigned to an entity at once. - ``trigger``: A mapping of functions that might trigger a rule. - ``prereq``: A mapping of functions a rule might require to return ``True`` for it to run. - ``action``: A mapping of functions that might manipulate the world state as a result of a rule running. - ``function``: A mapping of generic functions. - ``string``: A mapping of strings, probably shown to the player at some point. - ``eternal``: Mapping of arbitrary serializable objects. It isn't sensitive to sim-time. A good place to keep game settings. - ``universal``: Another mapping of arbitrary serializable objects, but this one *is* sensitive to sim-time. Each turn, the state of the randomizer is saved here under the key ``'rando_state'``. """ from .character import Character from .thing import Thing from .place import Place from .portal import Portal from .query import QueryEngine char_cls = Character thing_cls = Thing place_cls = node_cls = Place portal_cls = edge_cls = Portal query_engine_cls = QueryEngine illegal_graph_names = ['global', 'eternal', 'universal', 'rulebooks', 'rules'] illegal_node_names = ['nodes', 'node_val', 'edges', 'edge_val', 'things'] def _make_node(self, graph, node): if self._is_thing(graph.name, node): return self.thing_cls(graph, node) else: return self.place_cls(graph, node) def _make_edge(self, graph, orig, dest, idx=0): return self.portal_cls(graph, orig, dest)
[docs] def get_delta(self, branch, turn_from, tick_from, turn_to, tick_to): """Get a dictionary describing changes to the world. Most keys will be character names, and their values will be dictionaries of the character's stats' new values, with ``None`` for deleted keys. Characters' dictionaries have special keys 'nodes' and 'edges' which contain booleans indicating whether the node or edge exists at the moment, and 'node_val' and 'edge_val' for the stats of those entities. For edges (also called portals) these dictionaries are two layers deep, keyed first by the origin, then by the destination. Characters also have special keys for the various rulebooks they have: * 'character_rulebook' * 'avatar_rulebook' * 'character_thing_rulebook' * 'character_place_rulebook' * 'character_portal_rulebook' And each node and edge may have a 'rulebook' stat of its own. If a node is a thing, it gets a 'location'; when the 'location' is deleted, that means it's back to being a place. Keys at the top level that are not character names: * 'rulebooks', a dictionary keyed by the name of each changed rulebook, the value being a list of rule names * 'rules', a dictionary keyed by the name of each changed rule, containing any of the lists 'triggers', 'prereqs', and 'actions' """ from allegedb.window import update_window, update_backward_window if turn_from == turn_to: return self.get_turn_delta(branch, turn_to, tick_to, start_tick=tick_from) delta = super().get_delta(branch, turn_from, tick_from, turn_to, tick_to) if turn_from < turn_to: updater = partial(update_window, turn_from, tick_from, turn_to, tick_to) univbranches = self._universal_cache.settings avbranches = self._avatarness_cache.settings thbranches = self._things_cache.settings rbbranches = self._rulebooks_cache.settings trigbranches = self._triggers_cache.settings preqbranches = self._prereqs_cache.settings actbranches = self._actions_cache.settings charrbbranches = self._characters_rulebooks_cache.settings avrbbranches = self._avatars_rulebooks_cache.settings charthrbbranches = self._characters_things_rulebooks_cache.settings charplrbbranches = self._characters_places_rulebooks_cache.settings charporbbranches = self._characters_portals_rulebooks_cache.settings noderbbranches = self._nodes_rulebooks_cache.settings edgerbbranches = self._portals_rulebooks_cache.settings else: updater = partial(update_backward_window, turn_from, tick_from, turn_to, tick_to) univbranches = self._universal_cache.presettings avbranches = self._avatarness_cache.presettings thbranches = self._things_cache.presettings rbbranches = self._rulebooks_cache.presettings trigbranches = self._triggers_cache.presettings preqbranches = self._prereqs_cache.presettings actbranches = self._actions_cache.presettings charrbbranches = self._characters_rulebooks_cache.presettings avrbbranches = self._avatars_rulebooks_cache.presettings charthrbbranches = self._characters_things_rulebooks_cache.presettings charplrbbranches = self._characters_places_rulebooks_cache.presettings charporbbranches = self._characters_portals_rulebooks_cache.presettings noderbbranches = self._nodes_rulebooks_cache.presettings edgerbbranches = self._portals_rulebooks_cache.presettings def upduniv(_, key, val): delta.setdefault('universal', {})[key] = val if branch in univbranches: updater(upduniv, univbranches[branch]) def updav(char, graph, node, av): delta.setdefault(char, {}).setdefault('avatars', {}).setdefault(graph, {})[node] = bool(av) if branch in avbranches: updater(updav, avbranches[branch]) def updthing(char, thing, loc): if ( char in delta and 'nodes' in delta[char] and thing in delta[char]['nodes'] and not delta[char]['nodes'][thing] ): return thingd = delta.setdefault(char, {}).setdefault('node_val', {}).setdefault(thing, {}) thingd['location'] = loc if branch in thbranches: updater(updthing, thbranches[branch]) # TODO handle arrival_time and next_arrival_time stats of things def updrb(whatev, rulebook, rules): delta.setdefault('rulebooks', {})[rulebook] = rules if branch in rbbranches: updater(updrb, rbbranches[branch]) def updru(key, _, rule, funs): delta.setdefault('rules', {}).setdefault(rule, {})[key] = funs if branch in trigbranches: updater(partial(updru, 'triggers'), trigbranches[branch]) if branch in preqbranches: updater(partial(updru, 'prereqs'), preqbranches[branch]) if branch in actbranches: updater(partial(updru, 'actions'), actbranches[branch]) def updcrb(key, _, character, rulebook): delta.setdefault(character, {})[key] = rulebook if branch in charrbbranches: updater(partial(updcrb, 'character_rulebook'), charrbbranches[branch]) if branch in avrbbranches: updater(partial(updcrb, 'avatar_rulebook'), avrbbranches[branch]) if branch in charthrbbranches: updater(partial(updcrb, 'character_thing_rulebook'), charthrbbranches[branch]) if branch in charplrbbranches: updater(partial(updcrb, 'character_place_rulebook'), charplrbbranches[branch]) if branch in charporbbranches: updater(partial(updcrb, 'character_portal_rulebook'), charporbbranches[branch]) def updnoderb(character, node, rulebook): if ( character in delta and 'nodes' in delta[character] and node in delta[character]['nodes'] and not delta[character]['nodes'][node] ): return delta.setdefault(character, {}).setdefault('node_val', {}).setdefault(node, {})['rulebook'] = rulebook if branch in noderbbranches: updater(updnoderb, noderbbranches[branch]) def updedgerb(character, orig, dest, rulebook): if ( character in delta and 'edges' in delta[character] and orig in delta[character]['edges'] and dest in delta[character]['edges'][orig] and not delta[character]['edges'][orig][dest] ): return delta.setdefault(character, {}).setdefault('edge_val', {}).setdefault( orig, {}).setdefault(dest, {})['rulebook'] = rulebook if branch in edgerbbranches: updater(updedgerb, edgerbbranches[branch]) return delta
[docs] def get_turn_delta(self, branch=None, turn=None, tick=None, start_tick=0): """Get a dictionary describing changes to the world within a given turn Defaults to the present turn, and stops at the present tick unless specified. See the documentation for ``get_delta`` for a detailed description of the delta format. """ branch = branch or self.branch turn = turn or self.turn tick = tick or self.tick delta = super().get_turn_delta(branch, turn, start_tick, tick) if branch in self._avatarness_cache.settings and turn in self._avatarness_cache.settings[branch]: for chara, graph, node, is_av in self._avatarness_cache.settings[branch][turn][start_tick:tick]: delta.setdefault(chara, {}).setdefault('avatars', {}).setdefault(graph, {})[node] = is_av if branch in self._things_cache.settings and turn in self._things_cache.settings[branch]: for chara, thing, location in self._things_cache.settings[branch][turn][start_tick:tick]: thingd = delta.setdefault(chara, {}).setdefault('node_val', {}).setdefault(thing, {}) thingd['location'] = location delta['rulebooks'] = rbdif = {} if branch in self._rulebooks_cache.settings and turn in self._rulebooks_cache.settings[branch]: for _, rulebook, rules in self._rulebooks_cache.settings[branch][turn][start_tick:tick]: rbdif[rulebook] = rules delta['rules'] = rdif = {} if branch in self._triggers_cache.settings and turn in self._triggers_cache.settings[branch]: for _, rule, funs in self._triggers_cache.settings[branch][turn][start_tick:tick]: rdif.setdefault(rule, {})['triggers'] = funs if branch in self._prereqs_cache.settings and turn in self._prereqs_cache.settings[branch]: for _, rule, funs in self._prereqs_cache.settings[branch][turn][start_tick:tick]: rdif.setdefault(rule, {})['prereqs'] = funs if branch in self._actions_cache.settings and turn in self._triggers_cache.settings[branch]: for _, rule, funs in self._triggers_cache.settings[branch][turn][start_tick:tick]: rdif.setdefault(rule, {})['actions'] = funs if branch in self._characters_rulebooks_cache.settings and turn in self._characters_rulebooks_cache.settings[branch]: for _, character, rulebook in self._characters_rulebooks_cache.settings[branch][turn][start_tick:tick]: delta.setdefault(character, {})['character_rulebook'] = rulebook if branch in self._avatars_rulebooks_cache.settings and turn in self._avatars_rulebooks_cache.settings[branch]: for _, character, rulebook in self._avatars_rulebooks_cache.settings[branch][turn][start_tick:tick]: delta.setdefault(character, {})['avatar_rulebook'] = rulebook if branch in self._characters_things_rulebooks_cache.settings and turn in self._characters_things_rulebooks_cache.settings[branch]: for _, character, rulebook in self._characters_things_rulebooks_cache.settings[branch][turn][start_tick:tick]: delta.setdefault(character, {})['character_thing_rulebook'] = rulebook if branch in self._characters_places_rulebooks_cache.settings and turn in self._characters_places_rulebooks_cache.settings[branch]: for _, character, rulebook in self._characters_places_rulebooks_cache.settings[branch][turn][start_tick:tick]: delta.setdefault(character, {})['character_place_rulebook'] = rulebook if branch in self._characters_portals_rulebooks_cache.settings and turn in self._characters_portals_rulebooks_cache.settings[branch]: for _, character, rulebook in self._characters_portals_rulebooks_cache.settings[branch][turn][start_tick:tick]: delta.setdefault(character, {})['character_portal_rulebook'] = rulebook if branch in self._nodes_rulebooks_cache.settings and turn in self._nodes_rulebooks_cache.settings[branch]: for character, node, rulebook in self._nodes_rulebooks_cache.settings[branch][turn][start_tick:tick]: delta.setdefault(character, {}).setdefault('node_val', {}).setdefault(node, {})['rulebook'] = rulebook if branch in self._portals_rulebooks_cache.settings and turn in self._portals_rulebooks_cache.settings[branch]: for character, orig, dest, rulebook in self._portals_rulebooks_cache.settings[branch][turn][start_tick:tick]: delta.setdefault(character, {}).setdefault('edge_val', {})\ .setdefault(orig, {}).setdefault(dest, {})['rulebook'] = rulebook return delta
def _del_rulebook(self, rulebook): for (character, character_rulebooks) in \ self._characters_rulebooks_cache.items(): if rulebook not in character_rulebooks.values(): continue for (which, rb) in character_rulebooks.items(): if rb == rulebook: raise ValueError( "Rulebook still in use by {} as {}".format( character, which )) for (character, nodes) in self._nodes_rulebooks_cache.items(): if rulebook not in nodes.values(): continue for (node, rb) in nodes.items(): if rb == rulebook: raise ValueError( "Rulebook still in use by node " "{} in character {}".format( node, character )) for (character, origins) in self._portals_rulebooks_cache.items(): for (origin, destinations) in origins.items(): if rulebook not in destinations.values(): continue for (destination, rb) in destinations: if rb == rulebook: raise ValueError( "Rulebook still in use by portal " "{}->{} in character {}".format( origin, destination, character )) self.rule.query.rulebook_del_all(rulebook) del self._rulebooks_cache._data[rulebook] def _remember_avatarness( self, character, graph, node, is_avatar=True, branch=None, turn=None, tick=None ): """Use this to record a change in avatarness. Should be called whenever a node that wasn't an avatar of a character now is, and whenever a node that was an avatar of a character now isn't. ``character`` is the one using the node as an avatar, ``graph`` is the character the node is in. """ branch = branch or self.branch turn = turn or self.turn tick = tick or self.tick self._avatarness_cache.store( character, graph, node, branch, turn, tick, is_avatar ) self.query.avatar_set( character, graph, node, branch, turn, tick, is_avatar ) def _init_caches(self): from .xcollections import ( StringStore, FunctionStore, CharacterMapping, UniversalMapping ) from .cache import ( Cache, InitializedCache, EntitylessCache, InitializedEntitylessCache, AvatarnessCache, AvatarRulesHandledCache, CharacterThingRulesHandledCache, CharacterPlaceRulesHandledCache, CharacterPortalRulesHandledCache, NodeRulesHandledCache, PortalRulesHandledCache, CharacterRulesHandledCache, ThingsCache ) from .rule import AllRuleBooks, AllRules super()._init_caches() self._things_cache = ThingsCache(self) self._node_contents_cache = Cache(self) self.character = self.graph = CharacterMapping(self) self._universal_cache = EntitylessCache(self) self._rulebooks_cache = InitializedEntitylessCache(self) self._characters_rulebooks_cache = InitializedEntitylessCache(self) self._avatars_rulebooks_cache = InitializedEntitylessCache(self) self._characters_things_rulebooks_cache = InitializedEntitylessCache(self) self._characters_places_rulebooks_cache = InitializedEntitylessCache(self) self._characters_portals_rulebooks_cache = InitializedEntitylessCache(self) self._nodes_rulebooks_cache = InitializedCache(self) self._portals_rulebooks_cache = InitializedCache(self) self._triggers_cache = InitializedEntitylessCache(self) self._prereqs_cache = InitializedEntitylessCache(self) self._actions_cache = InitializedEntitylessCache(self) self._node_rules_handled_cache = NodeRulesHandledCache(self) self._portal_rules_handled_cache = PortalRulesHandledCache(self) self._character_rules_handled_cache = CharacterRulesHandledCache(self) self._avatar_rules_handled_cache = AvatarRulesHandledCache(self) self._character_thing_rules_handled_cache \ = CharacterThingRulesHandledCache(self) self._character_place_rules_handled_cache \ = CharacterPlaceRulesHandledCache(self) self._character_portal_rules_handled_cache \ = CharacterPortalRulesHandledCache(self) self._avatarness_cache = AvatarnessCache(self) self._turns_completed = defaultdict(lambda: max((0, self.turn - 1))) """The last turn when the rules engine ran in each branch""" self.eternal = self.query.globl self.universal = UniversalMapping(self) if hasattr(self, '_action_file'): self.action = FunctionStore(self._action_file) if hasattr(self, '_prereq_file'): self.prereq = FunctionStore(self._prereq_file) if hasattr(self, '_trigger_file'): self.trigger = FunctionStore(self._trigger_file) if hasattr(self, '_function_file'): self.function = FunctionStore(self._function_file) if hasattr(self, '_method_file'): self.method = FunctionStore(self._method_file) self.rule = AllRules(self) self.rulebook = AllRuleBooks(self) if hasattr(self, '_string_file'): self.string = StringStore( self.query, self._string_file, self.eternal.setdefault('language', 'eng') ) def _load_graphs(self): for charn in self.query.characters(): self._graph_objs[charn] = self.char_cls(self, charn, init_rulebooks=False) def __init__( self, worlddb, *, string='strings.json', function='function.py', method='method.py', trigger='trigger.py', prereq='prereq.py', action='action.py', connect_args={}, alchemy=False, commit_modulus=None, random_seed=None, logfun=None, validate=False, clear_code=False, clear_world=False ): """Store the connections for the world database and the code database; set up listeners; and start a transaction """ import os worlddbpath = worlddb.replace('sqlite:///', '') if clear_world and os.path.exists(worlddbpath): os.remove(worlddbpath) if isinstance(string, str): self._string_file = string if clear_code and os.path.exists(string): os.remove(string) else: self.string = string if isinstance(function, str): self._function_file = function if clear_code and os.path.exists(function): os.remove(function) else: self.function = function if isinstance(method, str): self._method_file = method if clear_code and os.path.exists(method): os.remove(method) else: self.method = method if isinstance(trigger, str): self._trigger_file = trigger if clear_code and os.path.exists(trigger): os.remove(trigger) else: self.trigger = trigger if isinstance(prereq, str): self._prereq_file = prereq if clear_code and os.path.exists(prereq): os.remove(prereq) else: self.prereq = prereq if isinstance(action, str): self._action_file = action if clear_code and os.path.exists(action): os.remove(action) else: self.action = action super().__init__( worlddb, connect_args=connect_args, alchemy=alchemy, validate=validate ) self.next_turn = NextTurn(self) if logfun is None: from logging import getLogger logger = getLogger(__name__) def logfun(level, msg): getattr(logger, level)(msg) self.log = logfun self.commit_modulus = commit_modulus self.random_seed = random_seed self._rules_iter = self._follow_rules() # set up the randomizer from random import Random self._rando = Random() if 'rando_state' in self.universal: self._rando.setstate(self.universal['rando_state']) else: self._rando.seed(self.random_seed) self.universal['rando_state'] = self._rando.getstate() if hasattr(self.method, 'init'): self.method.init(self) def _init_load(self, validate=False): from .rule import Rule q = self.query self._things_cache.load(q.things_dump(), validate) super()._init_load(validate=validate) self._avatarness_cache.load(q.avatars_dump(), validate) self._universal_cache.load(q.universals_dump(), validate) self._rulebooks_cache.load(q.rulebooks_dump(), validate) self._characters_rulebooks_cache.load(q.character_rulebook_dump(), validate) self._avatars_rulebooks_cache.load(q.avatar_rulebook_dump(), validate) self._characters_things_rulebooks_cache.load(q.character_thing_rulebook_dump(), validate) self._characters_places_rulebooks_cache.load(q.character_place_rulebook_dump(), validate) self._characters_portals_rulebooks_cache.load(q.character_portal_rulebook_dump(), validate) self._nodes_rulebooks_cache.load(q.node_rulebook_dump(), validate) self._portals_rulebooks_cache.load(q.portal_rulebook_dump(), validate) self._triggers_cache.load(q.rule_triggers_dump(), validate) self._prereqs_cache.load(q.rule_prereqs_dump(), validate) self._actions_cache.load(q.rule_actions_dump(), validate) for row in q.character_rules_handled_dump(): self._character_rules_handled_cache.store(*row, loading=True) for row in q.avatar_rules_handled_dump(): self._avatar_rules_handled_cache.store(*row, loading=True) for row in q.character_thing_rules_handled_dump(): self._character_thing_rules_handled_cache.store(*row, loading=True) for row in q.character_place_rules_handled_dump(): self._character_place_rules_handled_cache.store(*row, loading=True) for row in q.character_portal_rules_handled_dump(): self._character_portal_rules_handled_cache.store(*row, loading=True) for row in q.node_rules_handled_dump(): self._node_rules_handled_cache.store(*row, loading=True) for row in q.portal_rules_handled_dump(): self._portal_rules_handled_cache.store(*row, loading=True) self._turns_completed.update(q.turns_completed_dump()) self._rules_cache = {name: Rule(self, name, create=False) for name in q.rules_dump()} @property def stores(self): return ( self.action, self.prereq, self.trigger, self.function, self.method, self.string )
[docs] def debug(self, msg): """Log a message at level 'debug'""" self.log('debug', msg)
[docs] def info(self, msg): """Log a message at level 'info'""" self.log('info', msg)
[docs] def warning(self, msg): """Log a message at level 'warning'""" self.log('warning', msg)
[docs] def error(self, msg): """Log a message at level 'error'""" self.log('error', msg)
[docs] def critical(self, msg): """Log a message at level 'critical'""" self.log('critical', msg)
[docs] def close(self): """Commit changes and close the database.""" for store in self.stores: if hasattr(store, 'save'): store.save() super().close()
def __enter__(self): """Return myself. For compatibility with ``with`` semantics.""" return self def __exit__(self, *args): """Close on exit.""" self.close() def _set_branch(self, v): super()._set_branch(v) self.time.send(self.time, branch=self._obranch, turn=self._oturn) def _set_turn(self, v): super()._set_turn(v) self.time.send(self.time, branch=self._obranch, turn=self._oturn) def _handled_char(self, charn, rulebook, rulen, branch, turn, tick): try: self._character_rules_handled_cache.store( charn, rulebook, rulen, branch, turn, tick ) except ValueError: assert rulen in self._character_rules_handled_cache.handled[ charn, rulebook, branch, turn ] return self.query.handled_character_rule( charn, rulebook, rulen, branch, turn, tick ) def _handled_av(self, character, graph, avatar, rulebook, rule, branch, turn, tick): try: self._avatar_rules_handled_cache.store( character, graph, avatar, rulebook, rule, branch, turn, tick ) except ValueError: assert rule in self._avatar_rules_handled_cache.handled[ character, graph, avatar, rulebook, branch, turn ] return self.query.handled_avatar_rule( character, rulebook, rule, graph, avatar, branch, turn, tick ) def _handled_char_thing(self, character, thing, rulebook, rule, branch, turn, tick): try: self._character_thing_rules_handled_cache.store( character, thing, rulebook, rule, branch, turn, tick ) except ValueError: assert rule in self._character_thing_rules_handled_cache.handled[ character, thing, rulebook, branch, turn ] return self.query.handled_character_thing_rule( character, rulebook, rule, thing, branch, turn, tick ) def _handled_char_place(self, character, place, rulebook, rule, branch, turn, tick): try: self._character_place_rules_handled_cache.store( character, place, rulebook, rule, branch, turn, tick ) except ValueError: assert rule in self._character_place_rules_handled_cache.handled[ character, place, rulebook, branch, turn ] return self.query.handled_character_place_rule( character, rulebook, rule, place, branch, turn, tick ) def _handled_char_port(self, character, orig, dest, rulebook, rule, branch, turn, tick): try: self._character_portal_rules_handled_cache.store( character, orig, dest, rulebook, rule, branch, turn, tick ) except ValueError: assert rule in self._character_portal_rules_handled_cache.handled[ character, orig, dest, rulebook, branch, turn ] return self.query.handled_character_portal_rule( character, orig, dest, rulebook, rule, branch, turn, tick ) def _handled_node(self, character, node, rulebook, rule, branch, turn, tick): try: self._node_rules_handled_cache.store( character, node, rulebook, rule, branch, turn, tick ) except ValueError: assert rule in self._node_rules_handled_cache.handled[ character, node, rulebook, branch, turn ] return self.query.handled_node_rule( character, node, rulebook, rule, branch, turn, tick ) def _handled_portal(self, character, orig, dest, rulebook, rule, branch, turn, tick): try: self._portal_rules_handled_cache.store( character, orig, dest, rulebook, rule, branch, turn, tick ) except ValueError: assert rule in self._portal_rules_handled_cache.handled[ character, orig, dest, rulebook, branch, turn ] return self.query.handled_portal_rule( character, orig, dest, rulebook, rule, branch, turn, tick ) def _follow_rule(self, rule, handled_fun, branch, turn, *args): self.debug("following rule: " + repr(rule)) satisfied = True for prereq in rule.prereqs: res = prereq(*args) if not res: satisfied = False break if not satisfied: return handled_fun() for trigger in rule.triggers: res = trigger(*args) if res: break else: return handled_fun() actres = [] for action in rule.actions: res = action(*args) if res: actres.append(res) handled_fun() return actres def _follow_rules(self): # TODO: roll back changes done by rules that raise an exception # TODO: if there's a paradox while following some rule, start a new branch, copying handled rules from collections import defaultdict branch, turn, tick = self.btt() charmap = self.character rulemap = self.rule todo = defaultdict(list) def do_rule(tup): # Returns None if the entity following the rule no longer exists. # Better way to handle this? return { 'character': lambda charactername, rulebook, rulename: self._follow_rule( rulemap[rulename], partial(self._handled_char, charactername, rulebook, rulename, branch, turn, tick), branch, turn, charmap[charactername] ) if charactername in charmap else None, 'avatar': lambda charn, rulebook, graphn, avn, rulen: self._follow_rule( rulemap[rulen], partial(self._handled_av, charn, graphn, avn, rulebook, rulen, branch, turn, tick), branch, turn, charmap[graphn].node[avn] ) if self._node_exists(graphn, avn) else None, 'character_thing': lambda charn, rulebook, rulen, thingn: self._follow_rule( rulemap[rulen], partial(self._handled_char_thing, charn, thingn, rulebook, rulen, branch, turn, tick), branch, turn, charmap[charn].thing[thingn] ) if charn in charmap and thingn in charmap[charn].thing else None, 'character_place': lambda charn, rulebook, rulen, placen: self._follow_rule( rulemap[rulen], partial(self._handled_char_place, charn, placen, rulebook, rulen, branch, turn, tick), branch, turn, charmap[charn].place[placen] ) if charn in charmap and placen in charmap[charn].place else None, 'character_portal': lambda charn, rulebook, rulen, orign, destn: self._follow_rule( rulemap[rulen], partial(self._handled_char_port, charn, orign, destn, rulebook, rulen, branch, turn, tick), branch, turn, charmap[charn].portal[orign][destn] ) if self._edge_exists(charn, orign, destn) else None, 'node': lambda charn, noden, rulebook, rulen: self._follow_rule( rulemap[rulen], partial(self._handled_node, charn, noden, rulebook, rulen, branch, turn, tick), branch, turn, charmap[charn].node[noden] ) if self._node_exists(charn, noden) else None, 'portal': lambda charn, orign, destn, rulebook, rulen: self._follow_rule( rulemap[rulen], partial(self._handled_portal, charn, orign, destn, rulebook, rulen, branch, turn, tick), branch, turn, charmap[charn].portal[orign][destn] ) if self._edge_exists(charn, orign, destn) else None }[tup[0]](*tup[1:]) for ( charactername, rulebook, rulename ) in self._character_rules_handled_cache.iter_unhandled_rules( branch, turn, tick ): if charactername not in charmap: continue todo[rulebook].append(('character', charactername, rulebook, rulename)) for ( charn, graphn, avn, rulebook, rulen ) in self._avatar_rules_handled_cache.iter_unhandled_rules( branch, turn, tick ): if charn not in charmap: continue char = charmap[charn] if graphn not in char.avatar or avn not in char.avatar[graphn]: continue todo[rulebook].append(('avatar', charn, rulebook, graphn, avn, rulen)) for ( charn, thingn, rulebook, rulen ) in self._character_thing_rules_handled_cache.iter_unhandled_rules(branch, turn, tick): if charn not in charmap or thingn not in charmap[charn].thing: continue todo[rulebook].append(('character_thing', charn, rulebook, rulen, thingn)) for ( charn, placen, rulebook, rulen ) in self._character_place_rules_handled_cache.iter_unhandled_rules( branch, turn, tick ): if charn not in charmap or placen not in charmap[charn].place: continue todo[rulebook].append(('character_place', charn, rulebook, rulen, placen)) for ( charn, orign, destn, rulebook, rulen ) in self._character_portal_rules_handled_cache.iter_unhandled_rules( branch, turn, tick ): if charn not in charmap: continue char = charmap[charn] if orign not in char.portal or destn not in char.portal[orign]: continue todo[rulebook].append(('character_portal', charn, rulebook, rulen, orign, destn)) for ( charn, noden, rulebook, rulen ) in self._node_rules_handled_cache.iter_unhandled_rules( branch, turn, tick ): if charn not in charmap or noden not in charmap[charn]: continue todo[rulebook].append(('node', charn, noden, rulebook, rulen)) for ( charn, orign, destn, rulebook, rulen ) in self._portal_rules_handled_cache.iter_unhandled_rules( branch, turn, tick ): if charn not in charmap: continue char = charmap[charn] if orign not in char.portal or destn not in char.portal[orign]: continue todo[rulebook].append(('portal', charn, orign, destn, rulebook, rulen)) # TODO: rulebook priorities (not individual rule priorities, just follow the order of the rulebook) for rulebook in sort_set(todo.keys()): for rule in todo[rulebook]: try: yield do_rule(rule) except StopIteration: raise InnerStopIteration
[docs] def advance(self): """Follow the next rule if available. If we've run out of rules, reset the rules iterator. """ try: return next(self._rules_iter) except InnerStopIteration: self._rules_iter = self._follow_rules() return StopIteration() except StopIteration: self._rules_iter = self._follow_rules() return final_rule
# except Exception as ex: # self._rules_iter = self._follow_rules() # return ex
[docs] def new_character(self, name, data=None, **kwargs): """Create and return a new :class:`Character`.""" self.add_character(name, data, **kwargs) return self.character[name]
[docs] def add_character(self, name, data=None, **kwargs): """Create a new character. You'll be able to access it as a :class:`Character` object by looking up ``name`` in my ``character`` property. ``data``, if provided, should be a networkx-compatible graph object. Your new character will be a copy of it. Any keyword arguments will be set as stats of the new character. """ self._init_graph(name, 'DiGraph') self._graph_objs[name] = self.char_cls(self, name, data, **kwargs)
[docs] def del_character(self, name): """Remove the Character from the database entirely. This also deletes all its history. You'd better be sure. """ self.query.del_character(name) self.del_graph(name) del self.character[name]
def _is_thing(self, character, node): return self._things_cache.contains_entity(character, node, *self.btt()) def _set_thing_loc( self, character, node, loc ): branch, turn, tick = self.nbtt() self._things_cache.store(character, node, branch, turn, tick, loc) self.query.set_thing_loc( character, node, branch, turn, tick, loc ) def alias(self, v, stat='dummy'): from .util import EntityStatAccessor r = DummyEntity(self) r[stat] = v return EntityStatAccessor(r, stat, engine=self) def entityfy(self, v, stat='dummy'): from .query import Query from .util import EntityStatAccessor if ( isinstance(v, self.thing_cls) or isinstance(v, self.place_cls) or isinstance(v, self.portal_cls) or isinstance(v, Query) or isinstance(v, EntityStatAccessor) ): return v return self.alias(v, stat) def turns_when(self, qry): for branch, turn in qry.iter_turns(): yield turn