LiSE

engine

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.

class LiSE.Engine(prefix: ~os.PathLike | str = '.', *, string: ~LiSE.xcollections.StringStore | dict = None, trigger: ~LiSE.xcollections.FunctionStore | module = None, prereq: ~LiSE.xcollections.FunctionStore | module = None, action: ~LiSE.xcollections.FunctionStore | module = None, function: ~LiSE.xcollections.FunctionStore | module = None, method: ~LiSE.xcollections.MethodStore | module = None, main_branch: str = None, connect_string: str = None, connect_args: dict = None, schema_cls: ~typing.Type[~LiSE.engine.AbstractSchema] = <class 'LiSE.engine.NullSchema'>, flush_interval: int | None = 1, keyframe_interval: int | None = 10, commit_interval: int = None, random_seed: int = None, logfun: function = None, clear: bool = False, keep_rules_journal: bool = True, keyframe_on_close: bool = True, cache_arranger: bool = False, enforce_end_of_time: bool = True)[source]

LiSE, the Life Simulator Engine.

Parameters:
  • prefix – directory containing the simulation and its code; defaults to the working directory.

  • string – module storing strings to be used in the game; if absent, we’ll use a LiSE.xcollections.StringStore to keep them in a JSON file in the prefix.

  • function – module containing utility functions; if absent, we’ll use a LiSE.xcollections.FunctionStore to keep them in a .py file in the prefix

  • method – module containing functions taking this engine as first arg; if absent, we’ll use a LiSE.xcollections.FunctionStore to keep them in a .py file in the prefix.

  • trigger – module containing trigger functions, taking a LiSE entity and returning a boolean for whether to run a rule; if absent, we’ll use a LiSE.xcollections.FunctionStore to keep them in a .py file in the prefix.

  • prereq – module containing prereq functions, taking a LiSE entity and returning a boolean for whether to permit a rule to run; if absent, we’ll use a LiSE.xcollections.FunctionStore to keep them in a .py file in the prefix.

  • action – module containing action functions, taking a LiSE entity and mutating it (and possibly the rest of the world); if absent, we’ll use a LiSE.xcollections.FunctionStore to keep them in a .py file in the prefix.

  • main_branch – the string name of the branch to start from. Defaults to “trunk” if not set in some prior session. You should only change this if your game generates new initial conditions for each playthrough.

  • connect_string – a rfc1738 URI for a database to connect to. Leave None to use the SQLite database in the prefix.

  • connect_args – dictionary of keyword arguments for the database connection

  • schema – a Schema class that determines which changes to allow to the world; used when a player should not be able to change just anything. Defaults to NullSchema.

  • flush_interval – LiSE will put pending changes into the database transaction every flush_interval turns. If None, only flush on commit. Default 1.

  • keyframe_interval – How many turns to pass before automatically snapping a keyframe, default 10. If None, you’ll need to call snap_keyframe yourself.

  • commit_interval – LiSE will commit changes to disk every commit_interval turns. If None (the default), only commit on close or manual call to commit.

  • random_seed – a number to initialize the randomizer.

  • logfun – an optional function taking arguments level, message, which should log message somehow.

  • clear – whether to delete any and all existing data and code in prefix and the database. Use with caution!

  • keep_rules_journal – Boolean; if True (the default), keep information on the behavior of the rules engine in the database. Makes the database rather large, but useful for debugging.

  • keyframe_on_close – Whether to snap a keyframe when closing the engine, default True. This is usually what you want, as it will make future startups faster, but could cause database bloat if your game runs few turns per session.

  • cache_arranger – Whether to start a background thread that indexes the caches to make time travel faster when it’s to points we anticipate. If you use this, you can specify some other point in time to index by putting the (branch, turn, tick) in my cache_arrange_queue. Default False.

  • enforce_end_of_time – Whether to raise an exception when time travelling to a point after the time that’s been simulated. Default True. You normally want this, but it could cause problems if you’re not using the rules engine.

property branch: str

The fork of the timestream that we’re on.

is_ancestor_of(parent: str, child: str) bool

Return whether child is a branch descended from parent at any remove.

property turn: int

Units of time that have passed since the sim started.

property time

Acts like a tuple of (branch, turn) for the most part.

This wraps a blinker.Signal. To set a function to be called whenever the branch or turn changes, pass it to the Engine.time.connect method.

property rule

A mapping of all rules that have been made.

property 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.

property eternal

A mapping of arbitrary data, not sensitive to sim-time.

It’s stored in the database. A good place to keep your game’s settings.

property universal

A mapping of arbitrary data that changes over sim-time.

The state of the randomizer is saved here under the key 'rando_state'.

property trigger

A mapping of, and decorator for, functions that might trigger a rule.

Decorated functions get stored in the mapping as well as a file, so they can be loaded back in when the game is resumed.

property prereq

A mapping of, and decorator for, functions a rule might require to return True for it to run.

property action

A mapping of, and decorator for, functions that might manipulate the world state as a result of a rule running.

property method

A mapping of, and decorator for, extension methods to be added to the engine object.

property function

A mapping of, and decorator for, generic functions.

property rule

A mapping of LiSE.rule.Rule objects, whether applied to an entity or not.

Can also be used as a decorator on functions to make them into new rules, with the decorated function as their initial action.

next_turn()

Make time move forward in the simulation.

Stops when the turn has ended, or a rule returns something non-None.

This is also a blinker.Signal, so you can register functions to be called when the simulation runs. Pass them to Engine.next_turn.connect(..).

Returns:

a pair, of which item 0 is the returned value from a rule if applicable (default: []), and item 1 is a delta describing changes to the simulation resulting from this call. See the following method, get_delta(), for a description of the delta format.

get_delta(branch: str, turn_from: int, tick_from: int, turn_to: int, tick_to: int) DeltaDict[source]

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'

  • 'unit_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'

advancing()

A context manager for when time is moving forward one turn at a time.

When used in LiSE, this means that the game is being simulated. It changes how the caching works, making it more efficient.

batch()

A context manager for when you’re creating lots of state.

Reads will be much slower in a batch, but writes will be faster.

You can combine this with advancing but it isn’t any faster.

plan(reset=True) PlanningContext

A context manager for ‘hypothetical’ edits.

Start a block of code like:

with orm.plan():
        ...

and any changes you make to the world state within that block will be ‘plans,’ meaning that they are used as defaults. The world will obey your plan unless you make changes to the same entities outside of the plan, in which case the world will obey those, and cancel any future plan.

New branches cannot be started within plans. The with orm.forward(): optimization is disabled within a with orm.plan(): block, so consider another approach instead of making a very large plan.

With reset=True (the default), when the plan block closes, the time will reset to when it began.

snap_keyframe() None

Make a copy of the complete state of the world.

You need to do this occasionally in order to keep time travel performant.

The keyframe will be saved to the database at the next call to flush.

delete_plan(plan: int) None

Delete the portion of a plan that has yet to occur.

Parameters:

plan – integer ID of a plan, as given by with self.plan() as plan:

new_character(name: Key, data: Graph = None, layout: bool = True, **kwargs) Character[source]

Create and return a new Character.

add_character(name: Key, data: Graph | DiGraph | dict = None, layout: bool = True, **kwargs) None[source]

Create a new character.

You’ll be able to access it as a Character object by looking up name in my character property.

data, if provided, should be a networkx.Graph or networkx.DiGraph object. The character will be a copy of it. You can use a dictionary instead, and it will be converted to a graph.

With layout=True (the default), compute a layout to make the graph show up nicely in ELiDE.

Any keyword arguments will be set as stats of the new character.

del_character(name: Key) None[source]

Remove the Character from the database entirely.

This also deletes all its history. You’d better be sure.

turns_when(qry: Query, mid_turn=False) QueryResult | set[source]

Return the turns when the query held true

Only the state of the world at the end of the turn is considered. To include turns where the query held true at some tick, but became false, set mid_turn=True

Parameters:

qry – a Query, likely constructed by comparing the result of a call to an entity’s historical method with the output of self.alias(..) or another historical(..)

apply_choices(choices: List[dict], dry_run=False, perfectionist=False) Tuple[List[Tuple[Any, Any]], List[Tuple[Any, Any]]][source]

Validate changes a player wants to make, and apply if acceptable.

Argument choices is a list of dictionaries, of which each must have values for "entity" (a LiSE entity) and "changes" – the later being a list of lists of pairs. Each change list is applied on a successive turn, and each pair (key, value) sets a key on the entity to a value on that turn.

Returns a pair of lists containing acceptance and rejection messages, which the UI may present as it sees fit. They are always in a pair with the change request as the zeroth item. The message may be None or a string.

Validator functions may return only a boolean indicating acceptance. If they instead return a pair, the initial boolean indicates acceptance and the following item is the message.

This function will not actually result in any simulation happening. It creates a plan. See my plan context manager for the precise meaning of this.

With dry_run=True just return the acceptances and rejections without really planning anything. With perfectionist=True apply changes if and only if all of them are accepted.

flush()[source]

Write pending changes to disk.

You can set a flush_interval when you instantiate Engine to call this every so many turns. However, this may cause your game to hitch up sometimes, so it’s better to call flush when you know the player won’t be running the simulation for a while.

commit() None

Write the state of all graphs and commit the transaction.

Also saves the current branch, turn, and tick.

close() None[source]

Commit changes and close the database.

unload() None

Remove everything from memory that can be removed.

character

The top level of the LiSE world model, the Character.

Based on NetworkX DiGraph objects with various additions and conveniences.

A Character is a graph that follows rules. Its rules may be assigned to run on only some portion of it. Each Character has a stat property that acts very much like a dictionary, in which you can store game-time-sensitive data for the rules to use.

You can designate some nodes in one Character as units of another, and then assign a rule to run on all of a Character’s units. This is useful for the common case where someone in your game has a location in the physical world (here, a Character, called ‘physical’) but also has a behavior flowchart, or a skill tree, that isn’t part of the physical world. In that case, the flowchart is the person’s Character, and their node in the physical world is a unit of it.

class LiSE.character.Character(db, name, data=None, **attr)[source]

A digraph that follows game rules and has a containment hierarchy

Nodes in a Character are subcategorized into Things and Places. Things have locations, and those locations may be Places or other Things. To get at those, use the thing and place mappings – but in situations where the distinction does not matter, you may simply address the Character as a mapping, as in NetworkX.

Characters may have units in other Characters. These are just nodes. You can apply rules to a Character’s units, and thus to any collection of nodes you want, perhaps in many different Characters. The unit attribute handles this. It is a mapping, keyed by the other Character’s name, then by the name of the node that is this Character’s unit. In the common case where a Character has exactly one unit, it may be retrieved as unit.only. When it has more than one unit, but only has any units in a single other Character, you can get the mapping of units in that Character as unit.node. Add units with the add_unit method and remove them with del_unit.

You can assign rules to Characters with their rule attribute, typically using it as a decorator (see LiSE.rule). You can do the same to some of Character’s attributes:

  • thing.rule to make a rule run on all Things in this Character every turn

  • place.rule to make a rule run on all Places in this Character every turn

  • node.rule to make a rule run on all Things and Places in this Character every turn

  • unit.rule to make a rule run on all the units this Character has every turn, regardless of what Character the unit is in

  • adj.rule to make a rule run on all the edges this Character has every turn

property stat

A mapping of game-time-sensitive data.

property place

A mapping of LiSE.node.Place objects in this Character.

Has a rule method for applying new rules to every Place here, and a rulebook property for assigning premade rulebooks.

property thing

A mapping of LiSE.node.Thing objects in this Character.

Has a rule method for applying new rules to every Thing here, and a rulebook property for assigning premade rulebooks.

property node

A mapping of LiSE.node.Thing and LiSE.node.Place objects in this Character.

Has a rule method for applying new rules to every Node here, and a rulebook property for assigning premade rulebooks.

property portal

A two-layer mapping of LiSE.portal.Portal objects in this Character, by origin and destination

Has a rule method for applying new rules to every Portal here, and a rulebook property for assigning premade rulebooks.

Aliases: adj, edge, succ

property preportal

A two-layer mapping of LiSE.portal.Portal objects in this Character, by destination and origin

Has a rule method for applying new rules to every Portal here, and a rulebook property for assigning premade rulebooks.

Alias: pred

property unit

A mapping of this character’s units in other characters.

Units are nodes in other characters that are in some sense part of this one. A common example in strategy games is when a general leads an army: the general is one Character, with a graph representing the state of their AI; the battle map is another Character; and the general’s units, though not in the general’s Character, are still under their command, and therefore follow rules defined on the general’s unit.rule subproperty.

add_portal(origin, destination, **kwargs)[source]

Connect the origin to the destination with a Portal.

Keyword arguments are attributes of the Portal.

new_portal(origin, destination, **kwargs)[source]

Create a portal and return it

add_portals_from(seq, **kwargs)[source]

Make portals for a sequence of (origin, destination) pairs

Actually, triples are acceptable too, in which case the third item is a dictionary of stats for the new Portal.

add_thing(name, location, **kwargs)[source]

Make a new Thing and set its location

new_thing(name, location, **kwargs)

Add a Thing and return it.

If there’s already a Thing by that name, put a number on the end.

add_things_from(seq, **attrs)[source]

Make many new Things

add_place(node_for_adding, **attr)[source]

Add a new Place

add_places_from(seq, **attrs)[source]

Take a series of place names and add the lot.

new_place(name, **kwargs)

Add a Place and return it.

If there’s already a Place by that name, put a number on the end.

historical(stat)[source]

Get a historical view on the given stat

This functions like the value of the stat, but changes when you time travel. Comparisons performed on the historical view can be passed to engine.turns_when to find out when the comparison held true.

place2thing(name, location)[source]

Turn a Place into a Thing with the given location.

It will keep all its attached Portals.

portals()[source]

Iterate over all portals.

remove_portal(origin, destination)[source]
remove_unit(a, b=None)[source]

This is no longer my unit, though it still exists

thing2place(name)[source]

Unset a Thing’s location, and thus turn it into a Place.

units()[source]

Iterate over all my units

Regardless of what character they are in.

facade()[source]

Return a temporary copy of this Character

A Facade looks like its Character, but doesn’t do any of the stuff Characters do to save changes to the database, nor enable time travel. This makes it much speedier to work with.

node

The nodes of LiSE’s character graphs.

Every node that actually exists is either a Place or a Thing, but they have a lot in common.

class LiSE.node.Node(character, name)[source]

The fundamental graph component, which portals go between.

Every LiSE node is either a thing or a place. They share in common the abilities to follow rules; to be connected by portals; and to contain things.

This is truthy if it exists, falsy if it’s been deleted.

property user: UserMapping
property portal: Dests

A mapping of portals leading out from this node.

Aliases portal, adj, edge, successor, and succ are available.

property preportal: Origs

A mapping of portals leading to this node.

Aliases preportal, predecessor and pred are available.

property content: NodeContent

A mapping of Thing objects that are here

contents() NodeContentValues[source]

A set-like object containing Thing objects that are here

successors() Iterator[Place][source]

Iterate over nodes with edges leading from here to there.

predecessors() Iterator[Place][source]

Iterate over nodes with edges leading here from there.

shortest_path(dest: Key | Node, weight: Key = None) List[Key][source]

Return a list of node names leading from me to dest.

Raise ValueError if dest is not a node in my character or the name of one.

shortest_path_length(dest: Key | Node, weight: Key = None) int[source]

Return the length of the path from me to dest.

Raise ValueError if dest is not a node in my character or the name of one.

path_exists(dest: Key | Node, weight: Key = None) bool[source]

Return whether there is a path leading from me to dest.

With weight, only consider edges that have a stat by the given name.

Raise ValueError if dest is not a node in my character or the name of one.

new_portal(other: Key | 'Node', **stats) LiSE.portal.Portal[source]

Connect a portal from here to another node, and return it.

new_thing(name: Key, **stats) Thing[source]

Create a new thing, located here, and return it.

historical(stat: Key) StatusAlias[source]

Return a reference to the values that a stat has had in the past.

You can use the reference in comparisons to make a history query, and execute the query by calling it, or passing it to self.engine.ticks_when.

delete() None[source]

Get rid of this, starting now.

Apart from deleting the node, this also informs all its users that it doesn’t exist and therefore can’t be their unit anymore.

class LiSE.node.Place(character, name)[source]

The kind of node where a thing might ultimately be located.

LiSE entities are truthy so long as they exist, falsy if they’ve been deleted.

delete() None[source]

Remove myself from the world model immediately.

class LiSE.node.Thing(character, name)[source]

The sort of item that has a particular location at any given time.

Things are always in Places or other Things, and may additionally be travelling through a Portal.

LiSE entities are truthy so long as they exist, falsy if they’ve been deleted.

clear() None[source]

Unset everything.

delete() None[source]

Get rid of this, starting now.

Apart from deleting the node, this also informs all its users that it doesn’t exist and therefore can’t be their unit anymore.

follow_path(path: list, weight: Key = None) int[source]

Go to several nodes in succession, deciding how long to spend in each by consulting the weight stat of the Portal connecting the one node to the next, default 1 turn.

Return the total number of turns the travel will take. Raise TravelException if I can’t follow the whole path, either because some of its nodes don’t exist, or because I’m scheduled to be somewhere else.

go_to_place(place: Node | Key, weight: Key = None) int[source]

Assuming I’m in a node that has a Portal direct to the given node, schedule myself to travel to the given Place, taking an amount of time indicated by the weight stat on the Portal, if given; else 1 turn.

Return the number of turns the travel will take.

property location: Node

The Thing or Place I’m in.

travel_to(dest: Node | Key, weight: Key = None, graph: DiGraph = None) int[source]

Find the shortest path to the given node from where I am now, and follow it.

If supplied, the weight stat of each Portal along the path will be used in pathfinding, and for deciding how long to stay in each Place along the way.

The graph argument may be any NetworkX-style graph. It will be used for pathfinding if supplied, otherwise I’ll use my Character. In either case, however, I will attempt to actually follow the path using my Character, which might not be possible if the supplied graph and my Character are too different. If it’s not possible, I’ll raise a TravelException, whose subpath attribute holds the part of the path that I can follow. To make me follow it, pass it to my follow_path method.

Return value is the number of turns the travel will take.

portal

Directed edges, as used by LiSE.

class LiSE.portal.Portal(graph: AbstractCharacter, orig: Key, dest: Key)[source]

Connection between two nodes that LiSE.node.Thing travel along

LiSE entities are truthy so long as they exist, falsy if they’ve been deleted.

origin

The LiSE.node.Place or LiSE.node.Thing that this leads out from

destination

The LiSE.node.Place or LiSE.node.Thing that this leads into

property character

The LiSE.character.Character that this is in

property engine

The LiSE.engine.Engine that this is in

property reciprocal: Portal

If there’s another Portal connecting the same origin and destination that I do, but going the opposite way, return it. Else raise KeyError.

historical(stat: Key) StatusAlias[source]

Return a reference to the values that a stat has had in the past.

You can use the reference in comparisons to make a history query, and execute the query by calling it, or passing it to self.engine.ticks_when.

delete() None[source]

Remove myself from my Character.

For symmetry with Thing and Place.

rule

The fundamental unit of game logic, the Rule, and structures to store and organize them in.

A Rule is three lists of functions: triggers, prereqs, and actions. The actions do something, anything that you need your game to do, but probably making a specific change to the world model. The triggers and prereqs between them specify when the action should occur: any of its triggers can tell it to happen, but then any of its prereqs may stop it from happening.

Rules are assembled into RuleBooks, essentially just lists of Rules that can then be assigned to be followed by any game entity – but each game entity has its own RuleBook by default, and you never need to change that.

To add a new rule to a LiSE entity, the easiest thing is to use the decorator syntax:

@entity.rule
def do_something(entity):
        ...

@do_something.trigger
def whenever(entity):
        ...

@do_something.trigger
def forever(entity):
        ...

@do_something.action
def do_something_else(entity):
        ...

When run, this code will:

  • copy the do_something function to action.py, where LiSE knows to run it when a rule triggers it

  • create a new rule named 'do_something'

  • set the function do_something as the first (and, so far, only) entry in the actions list of the rule by that name

  • copy the whenever function to trigger.py, where LiSE knows to call it when a rule has it as a trigger

  • set the function whenever as the first entry in the triggers list of the rule 'do_something'

  • append the function forever to the same triggers list

  • copy do_something_else to action.py

  • append do_something_else to the actions list of the rule

The trigger, prereq, and action attributes of Rule objects may also be used like lists. You can put functions in them yourself, provided they are already present in the correct module. If it’s inconvenient to get the actual function object, use a string of the function’s name.

class LiSE.rule.Rule(engine, name, triggers=None, prereqs=None, actions=None, create=True)[source]

Stuff that might happen in the simulation under some conditions

Rules are comprised of three lists of functions:

  • actions, which mutate the world state

  • triggers, which make the actions happen

  • prereqs, which prevent the actions from happening when triggered

Each kind of function should be stored in the appropriate module supplied to the LiSE core at startup. This makes it possible to load the functions on later startups. You may instead use the string name of a function already stored in the module, or use the trigger, prereq, or action decorator on a new function to add it to both the module and the rule.

action(fun)[source]

Decorator to append the function to my actions list.

always()[source]

Arrange to be triggered every turn

duplicate(newname)[source]

Return a new rule that’s just like this one, but under a new name.

prereq(fun)[source]

Decorator to append the function to my prereqs list.

trigger(fun)[source]

Decorator to append the function to my triggers list.

query

Database access and query builder

Sometimes you want to know when some stat of a LiSE entity had a particular value. To find out, construct a historical query and pass it to Engine.turns_when, like this:

physical = engine.character['physical']
that = physical.thing['that']
hist_loc = that.historical('location')
print(list(engine.turns_when(hist_loc == 'there')))

You’ll get the turns when that was there.

Other comparison operators like > and < work as well.

class LiSE.query.QueryResult(windows_l, windows_r, oper, end_of_time)[source]

A slightly lazy tuple-like object holding a history query’s results

Testing for membership of a turn number in a QueryResult only evaluates the predicate for that turn number, and testing for membership of nearby turns is fast. Accessing the start or the end of the QueryResult only evaluates the initial or final item. Other forms of access cause the whole query to be evaluated in parallel.

xcollections

Common classes for collections in LiSE

Notably includes wrappers for mutable objects, allowing them to be stored in the database. These simply store the new value.

Most of these are subclasses of blinker.Signal, so you can listen for changes using the connect(..) method.

class LiSE.xcollections.StringStore(query, prefix, lang='eng')[source]
class LiSE.xcollections.FunctionStore(filename)[source]

A module-like object that lets you alter its code and save your changes.

Instantiate it with a path to a file that you want to keep the code in. Assign functions to its attributes, then call its save() method, and they’ll be unparsed and written to the file.

This is a Signal, so you can pass a function to its connect method, and it will be called when a function is added, changed, or deleted. The keyword arguments will be attr, the name of the function, and val, the function itself.