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 theprefix
.function – module containing utility functions; if absent, we’ll use a
LiSE.xcollections.FunctionStore
to keep them in a .py file in theprefix
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 theprefix
.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 theprefix
.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 theprefix
.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 theprefix
.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 theprefix
.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. IfNone
, only flush on commit. Default1
.keyframe_interval – How many turns to pass before automatically snapping a keyframe, default
10
. IfNone
, you’ll need to callsnap_keyframe
yourself.commit_interval – LiSE will commit changes to disk every
commit_interval
turns. IfNone
(the default), only commit on close or manual call tocommit
.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 mycache_arrange_queue
. DefaultFalse
.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.
- is_ancestor_of(parent: str, child: str) bool ¶
Return whether
child
is a branch descended fromparent
at any remove.
- 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 theEngine.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 toEngine.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 awith 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 upname
in mycharacter
property.data
, if provided, should be anetworkx.Graph
ornetworkx.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 ofself.alias(..)
or anotherhistorical(..)
- 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. Withperfectionist=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 instantiateEngine
to call this every so many turns. However, this may cause your game to hitch up sometimes, so it’s better to callflush
when you know the player won’t be running the simulation for a while.
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 asunit.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 asunit.node
. Add units with theadd_unit
method and remove them withdel_unit
.You can assign rules to Characters with their
rule
attribute, typically using it as a decorator (seeLiSE.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 turnplace.rule
to make a rule run on all Places in this Character every turnnode.rule
to make a rule run on all Things and Places in this Character every turnunit.rule
to make a rule run on all the units this Character has every turn, regardless of what Character the unit is inadj.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 thisCharacter
.Has a
rule
method for applying new rules to everyPlace
here, and arulebook
property for assigning premade rulebooks.
- property thing¶
A mapping of
LiSE.node.Thing
objects in thisCharacter
.Has a
rule
method for applying new rules to everyThing
here, and arulebook
property for assigning premade rulebooks.
- property node¶
A mapping of
LiSE.node.Thing
andLiSE.node.Place
objects in thisCharacter
.Has a
rule
method for applying new rules to everyNode
here, and arulebook
property for assigning premade rulebooks.
- property portal¶
A two-layer mapping of
LiSE.portal.Portal
objects in thisCharacter
, by origin and destinationHas a
rule
method for applying new rules to everyPortal
here, and arulebook
property for assigning premade rulebooks.Aliases:
adj
,edge
,succ
- property preportal¶
A two-layer mapping of
LiSE.portal.Portal
objects in thisCharacter
, by destination and originHas a
rule
method for applying new rules to everyPortal
here, and arulebook
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 anotherCharacter
; and the general’s units, though not in the general’sCharacter
, are still under their command, and therefore follow rules defined on the general’sunit.rule
subproperty.
- add_portal(origin, destination, **kwargs)[source]¶
Connect the origin to the destination with a
Portal
.Keyword arguments are attributes of the
Portal
.
- 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
.
- 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.
- 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.
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
, andsucc
are available.
- property preportal: Origs¶
A mapping of portals leading to this node.
Aliases
preportal
,predecessor
andpred
are available.
- property content: NodeContent¶
A mapping of
Thing
objects that are here
- shortest_path(dest: Key | Node, weight: Key = None) List[Key] [source]¶
Return a list of node names leading from me to
dest
.Raise
ValueError
ifdest
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
ifdest
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
ifdest
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.
- 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.
- 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.
- 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 thePortal
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 givenPlace
, taking an amount of time indicated by theweight
stat on thePortal
, if given; else 1 turn.Return the number of turns the travel will take.
- 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 eachPortal
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 myCharacter
. In either case, however, I will attempt to actually follow the path using myCharacter
, which might not be possible if the suppliedgraph
and myCharacter
are too different. If it’s not possible, I’ll raise aTravelException
, whosesubpath
attribute holds the part of the path that I can follow. To make me follow it, pass it to myfollow_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 alongLiSE entities are truthy so long as they exist, falsy if they’ve been deleted.
- origin¶
The
LiSE.node.Place
orLiSE.node.Thing
that this leads out from
- destination¶
The
LiSE.node.Place
orLiSE.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.
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 toaction.py
, where LiSE knows to run it when a rule triggers itcreate 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 namecopy the
whenever
function totrigger.py
, where LiSE knows to call it when a rule has it as a triggerset the function
whenever
as the first entry in the triggers list of the rule'do_something'
append the function
forever
to the same triggers listcopy
do_something_else
toaction.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
, oraction
decorator on a new function to add it to both the module and the rule.
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.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 itsconnect
method, and it will be called when a function is added, changed, or deleted. The keyword arguments will beattr
, the name of the function, andval
, the function itself.