I’ve had in mind from the beginning that Bedquilt’s world model was going to be represented by an in-memory graph-structured database. I was originally hoping to be able to grab one off the self, but after hunting around on crates.io I couldn’t find anything quite right, so I’m just going to write one that has all the features I want and none that I don’t. What follow are some preliminary design notes for it. Nothing here is implemented or rigorously specified yet.
Although Bedquilt targets Glulx, it isn’t going to rely at all on Glulx’s game-state instructions (save/restore/undo/etc). This keeps the door open for eventual support for native compilation or compilation for other VMs that don’t provide similar functionality. Part of the database’s job is to handle these things. A dump of the database will comprise Bedquilt’s save format, and the database will provide space efficient checkpoint-and-rollback functionality to support undo.
There are four sorts of things that can be contained in the database:
- A value is a simple datum such a number or a string.
- An entity corresponds to what other systems might call an object. On its own, it is represented just by an opaque ID that is distinct from the ID of each other entity.
- A property is a binary relation from an entity to a value.
- A relationship is a binary relation from an entity to another entity.
If your game includes a white house west of an open field, this might be represented in your database by two entities, two instances of a “name” property which relate one entity to the value “white house” and the other to the value “open field”, and one instance of a “west” relationship which relates one entity to the other.
The sources for your game will include one or more .bqs
files which define a database schema, and a .bqd
file containing data which conforms to that schema and defines your initial game state. .bqs
files should be largely reusable from one game to another and can have ecosystems built around them, while pretty much everything in the .bqd
file will be unique to your game. Bedquilt will include a code generator which translates .bqs
and .bqd
files into Rust code. The .bqs
file will be translated into types and traits which correspond to the schema with CRUD methods that enforce and guarantee data integrity, while the .bqd
file will be translated into a big long function that you can invoke to populate your database at the start of new game.
Within the schema language, you can define classes to which entities belong. These classes can be used as constraints on the domain and range of relationships, and the domain of properties. Classes can multiply inherit from each other, and every class descends from the built-in class any
. Properties and relationships can also have cardinality constraints. To define a class named Room
and a require that the west
property relate a room to at most one other room, you would write
class Room;
relationship west : Room -> one Room;
one
is the only cardinality keyword syntactically allowed here, and means “at most one” as opposed to “exactly one”. If the keyword were omitted, a room would be allowed to have any number (including zero) of rooms to its west. If you want a property or relationship to be required, you have to specify that as part of a class definition. For example,
property name : any -> one String;
class Room has name;
specifies that while any entity is allowed to have a name (but can have at most one of them), a Room is required to have one.
Properties and relationships are immutable by default. You can change this by adding the mut
keyword to their declaration, or make them mutable only when applied to specific classes by specifying that in the class definition. However, once you’ve made something mutable, its cardinality becomes frozen and subclasses can’t add any more cardinality constraints, since those could get violated if you mutated something through a superclass.
A .bqd
file matched to a schema like the above could look like this:
entity field : Room {
name = "Open Field",
west = house
}
entity house : Room {
name = "White House"
}
When the schema is compiled, it’ll produce a Rust module containing code such as this:
pub trait Name {
// The trailing underscore is a naming convention for optional properties.
fn name_(&self) -> Option<&str>;
}
pub trait HasName : Name {
fn name(&self) -> &str;
}
// `Entity` is a type built into the core database engine. It takes a
// lifetime parameter because it holds a borrow on the database itself.
impl Name for Entity<'_> { ... };
/// A newtype wrapper.
struct Room<'a>(Entity<'a>);
trait West {
fn west_(&self) -> Option<Room<'_>>;
}
impl <'a> From<Room<'a>> for Entity<'a> { ... }
impl Name for Room<'_> { ... }
impl HasName for Room<'_> { ... }
impl West for Room<'_> { ... }
This shows the basic idea of how you can traverse your world graph one node at a time. Of course there will also be operations for creating new entities, and for mutating any properties or relationships declared mut
. Where the database gets interestingly powerful is that you’ll also be able to operate on sets. Operators will be provided to:
- Get the set of all entities belonging to a particular class.
- Compute the union, intersection, or difference of sets.
- Invert a property or relationship (“find everything in the database whose
name
property is anything in this set”). - Filter a set according to a predicate (“retain elements of this set whose
lit
property istrue
”). - Map a property or relationship over a set (“give me the set of all rooms that are west of any room in this set.”)
- Find the transitive closure of such a map (“find all rooms that are reachable by going west repeatedly”).
Fans of Dialog should be pleased by the observation that, with the inclusion of the transitive closure operator, queries written in this algebra closely resemble logic programming — c.f. Datalog. In contrast to Dialog, however, you’ll have this query facility embedded within a multiparadigm language rather than it being your entire language. Consequently, you can use it when it’s a natural fit, and just write imperative code when it isn’t.