Introduction ============ Life sims all seem to have two problems in common: Too much world state -------------------- The number of variables the game is tracking -- just for game logic, not graphics or physics or anything -- is very large. Like how The Sims tracks sims' opinions of one another, their likes and dislikes and so forth, even for the ones you never talk to and have shown no interest in. If you streamline a life sim to where it doesn't have extraneous detail, you lose a huge part of what makes it lifelike. This causes trouble for developers when even *they* don't understand why sims hate each other To address all those problems, LiSE provides a state container. Everything that ever happens in a game gets recorded, so that you can pick through the whole history and find out exactly when the butterfly flapped its wings to cause the cyclone. All of that history gets saved in a database, which is used in place of traditional save files. This means that if your testers discover something strange and want you to know about it, they can send you their database, and you'll know everything they did and everything that happened in their game. Too many rules -------------- Fans of life sims tend to appreciate complexity. Developers are best served by reducing complexity as much as possible. So LiSE makes it easy to compartmentalize complexity and choose what of it you want to deal with and when. It is a rules engine, an old concept from business software that lets you determine what conditions cause what effects. Here, conditions are Triggers and effects are Actions, and they're both lists of Python functions. Actions make some change to the state of the world, while Triggers look at the world once-per-turn and return a Boolean to show whether their Actions should happen. Architecture ------------ LiSE is a tool for constructing turn-based simulations following rules in a directed graph-based world model. It has special affordances for the kinds of things you might need to simulate in the life simulation genre. Rules are things the game should do in certain conditions. In LiSE, the "things to do" are called Actions, and are functions that can run arbitrary Python code. The conditions are divided into Triggers and Prereqs, of which only Triggers are truly necessary: they are also functions, but one of a rule's Triggers must return True for the Action to proceed. A directed graph is made of nodes and edges. The nodes are points without fixed locations--when drawing a graph, you may arrange the nodes however you like, as long as the edges connect them the same way. Edges in a directed graph connect one node to another node, but not vice-versa, so you can have nodes A and B where A is connected to B, but B is not connected to A. But you can have edges going in both directions between A and B. They're usually drawn as arrows. In LiSE, edges are called Portals, and nodes may be Places or Things. You can use these to represent whatever you want, but they have special properties to make it easier to model physical space: in particular, each Thing is located in exactly one node at a time (usually a Place), and may be travelling through one of the Portals leading out from there. Regardless, you can keep any data you like in a Thing, Place, or Portal by treating it like a dictionary. LiSE's directed graphs are called Characters. Every time something about a Character changes, LiSE remembers when it happened -- that is, which turn of the simulation. This allows the developer to look up the state of the world at some point in the past. This time travel is nearly real-time in most cases, to make it convenient to flip back and forth between a correct world state and an incorrect one and use your intuition to spot exactly what went wrong. Usage ----- The only LiSE class that you should ever instantiate yourself is ``Engine``. All simulation objects should be created and accessed through it. By default, it keeps the simulation code and world state in the working directory, but you can pass in another directory if you prefer. Either use it with a context manager (``with Engine() as eng:``) or call its ``.close()`` method when you're done changing things. World Modelling +++++++++++++++ Start by calling the engine's ``new_character`` method with a string ``name`` to get a character object. Draw a graph by calling the method ``new_place`` with many different ``name`` s, then linking them together with the method ``new_portal(origin, destination)``. To store data pertaining to some specific place, retrieve the place from the ``place`` mapping of the character: if the character is ``world`` and the place name is ``'home'``, you might do it like ``home = world.place['home']``. Portals are retrieved from the ``portal`` mapping, where you'll need the origin and the destination: if there's a portal from ``'home'`` to ``'narnia'``, you can get it like ``wardrobe = world.portal['home']['narnia']``, but if you haven't also made another portal going the other way, ``world.portal['narnia']['home']`` will raise ``KeyError``. Things, usually being located in places (but possibly in other things), are most conveniently created by the ``new_thing`` method of Place objects: ``alice = home.new_thing('alice')`` gets you a new Thing object located in ``home``. Things can be retrieved like ``alice = world.thing['alice']``. Ultimately, things and places are both just nodes, and both can be retrieved in a character's ``node`` mapping, but only things have methods like ``travel_to``, which finds a path to a destination and schedules movement along it. You can store data in things, places, and portals by treating them like dictionaries. If you want to store data in a character, use its ``stat`` property as a dictionary instead. Data stored in these objects, and in the ``universal`` property of the engine, can vary over time, and be rewound by setting ``turn`` to some time before. The engine's ``eternal`` property is not time-sensitive, and is mainly for storing settings, not simulation data. Rule Creation +++++++++++++ To create a rule, first decide what objects the rule should apply to. You can put a rule on a character, thing, place, or portal; and you can put a rule on a character's ``thing``, ``place``, and ``portal`` mappings, meaning the rule will be applied to *every* such entity within the character, even if it didn't exist when the rule was declared. All these items have a property ``rule`` that can be used as a decorator. Use this to decorate a function that performs the rule's action by making some change to the world state. The function should take only one argument, the item itself. At first, the rule object will not have any triggers, meaning the action will never happen. If you want it to run on *every* tick, pass the decorator ``always=True`` and think no more of it. But if you want to be more selective, use the rule's ``trigger`` decorator on another function with the same signature, and have it return ``True`` if the world is in such a state that the rule ought to run. Triggers must never mutate the world or use any randomness. If you like, you can also add prerequisites. These are like triggers, but use the ``prereq`` decorator, and should return ``True`` *unless* the action should *not* happen; if a single prerequisite returns ``False``, the action is cancelled. Prereqs may involve random elements. Use the ``engine`` property of any LiSE entity to get the engine, then use methods such as ``percent_chance`` and ``dice_check``. Time Control ++++++++++++ The current time is always accessible from the engine's ``branch`` and ``turn`` properties. In the common case where time is advancing forward one tick at a time, it should be done with the engine's ``next_turn`` method, which polls all the game rules before going to the next turn; but you can also change the time whenever you want, as long as ``branch`` is a string and ``turn`` is an integer. The rules will never be followed in response to your changing the time "by hand". It is possible to change the time as part of the action of a rule. This is how you would make something happen after a delay. Say you want a rule that puts the character ``alice`` to sleep, then wakes her up after eight turns (presumably hour-long).:: alice = engine.character['alice'] @alice.rule def sleep(character): character.stat['awake'] = False start_turn = character.engine.turn with character.engine.plan() as plan_num: character.engine.turn += 8 character.stat['awake'] = True character.stat['wake_plan'] = plan_num At the end of a ``plan():`` block, the game-time will be reset to its position at the start of that block. You can use the plan's ID number, ``plan_num`` in the above, to cancel it yourself -- some other rule could call ``engine.delete_plan(engine.character['alice'].stat['wake_plan'])``. Input Prompts +++++++++++++ LiSE itself doesn't know what a player is or how to accept input from them, but does use some conventions for communicating with a user interface such as ELiDE. To ask the player to make a decision, first define a method for them to call, then return a menu description like this one.:: @engine.method def wake_alice(self): self.character['alice'].stat['awake'] = True alice = engine.character['alice'] @alice.rule def wakeup(character): return "Wake up?", [("Yes", character.engine.wake_alice), ("No", None)] Only methods defined with the ``@engine.method`` function store may be used in a menu.