After learning Dialog, I've discovered representing game logic using the logic programming paradigm is a lot of fun; the composability and non-locality makes it feel like some kind of superior ECS. The problem is that game state becomes difficult to work with. Dialog has some facilities for handling state, but ultimately it is limited to mutable unary & binary facts and a compile-time fixed number of objects. To work around these issues introduces serious ugliness and brings into question the performance of larger game worlds.
Dialog isn't the only logic programming language, however, so I've looked into how text games can be made in Prolog and Logtalk and discovered the problem of managing state is even bigger there: one either passes around the world state as a parameter, or uses the dangerous assert/retract family to mutate the database. To my knowledge, neither option is appropriate for large, complex game worlds as an unskilled solo dev.
At this point I am stuck. Is there something I'm missing to get around these limitations in Dialog? Is there a better way to handle state in Prolog-family languages?
The reason why state is so important for me is that I'm interested in making a text-oriented game with procedural generation and RPG mechanics which requires the ability to dynamically generate objects, manage large quantities of their instances, and support AI pathing about a large world with a constantly changing connection graph. More concretely, I need to express:
- runtime instantiation of an unbounded number of stateful objects
- stateful objects (e.g. containers) that must present multiple instances simultaneously (e.g. a room of treasure chests)
- pathable portals with dynamic destinations that may be moved by actors and have multiple instances in the same room
These are in service to the following gameplay outcomes:
- Topological rooms. Instead of representing only the world as a graph of rooms with binary occupancy, I also intend to emulate old-school "zones" by using a system of waypoints and relations within a single room. By itself, this may not seem like much of an issue (you can represent this as a tree), but when one considers how zones typically contain multiple instances of the same object, we arrive back at our problem.
- Autonomous AI. This seems unrelated, but imposes a crucial constraint - if an actor is performing an action that involves both a local object instance and a distant one, we can no longer use "floating" singleton objects to fake instances. This can be as simple as an actor by a tree looking to wave at another that happens to also be by the same kind of tree.
- Procedural worlds. Moving on, I'd like for these zone-rooms to be procedurally populated. While you can get away with floating for stateless objects in a single-actor world, all the player needs to do is drop an object by a tree and wander over to another to discover the ruse.
- Movable doors/rooms. Actor driven vehicles, for example.
Explanation of the Dialog Workarounds
For the limited arity of facts, you have the macro facility which allows you to bundle facts like so:
@($Obj is $Rel $Parent) *($Obj has relation $Rel) *($Obj has parent $Parent)
You may notice that this macro only works if
$Obj has only one relation. This can be worked around by interpreting some datastructure composed of lists, but will miss out on optimization by the compiler.
%%Does not work because mutable facts are deterministic @($Obj is $Rel $Parent) *($Obj has relation [$Rel $Parent]) %%Instead you must use linear search @($Obj is $Rel $Parent) *($Obj has relations $Rels) *([$Rel $Parent] is one of $Rels)
Similarly, you could enumerate these composite facts with IDs, but I also anticipate performance issues there.
%%Doesn't work because numbers aren't supported as the first argument @($Obj is $Rel $Parent ! $ID) *($ID has object $Obj) *($ID has relation $Rel) *($ID has parent $Parent) %%This scans every object; inefficient @($Obj is $Rel $Parent ! $ID) *(object $Obj concerns $ID) *(relation $Rel concerns $ID) *(parent $Parent concerns $ID)
For dynamic object creation, you can apply a similar tactic of assigning unique IDs to objects and tracking them with lists, but this again frustrates the compiler's optimization which is built around the object datatype (IIUC).
(new $Class is $ID) (there are $N instances) (world objects $Objs) (now) (world objects [$Class | $Objs]) ($N + 1 is $ID) (now) (there are $ID instances)