Just expanding on the Blueprint Manifesto with a few additional thoughts on implementation.
Blueprints are loaded from a file into a runtime environment. This environment, at its simplest, is just a list of all the blueprints by name. Blueprints can refer to each other. When an unknown identifier is found in a property expression, it will be checked against all the loaded blueprints.
In addition to the blueprints themselves, the runtime environment keeps track of a few other things, like global variables, the blueprint domain sets, and the function table (both built-in and user defined).
The format of a blueprint file is extremely simple. It is just a series of commands:
@blueprint MagicVase
@domain type = (item questItem)
@domain keyword = (ancient magical pottery expensive)
@property icon = "magicvase.png"
@property value = (rand 1000 1300)
@property description = "Created in the Ming Dynasty. Looks expensive."
@end
As you can see, there are three things at play. The first line defines the blueprint using a string identifier. Other blueprints can refer to this one by that name. Next up is the domain lists, which I'll talk about in the next section. Finally, the properties are just BlueScript expressions, be it a number, string, list, blueprint reference, or function call (or any combination thereof).
As I discussed in the Blind Mapmaker article on sets, the ability to group blueprints together by explicit criteria is important, especially if the design is going to include expansions of unknown future blueprints.
A blueprint defines the domain of a generator, but they, themselves, can belong to domains as well. Each domain is given a name (like type or keyword) and a series of items in that domain that it belongs to. The magic vase above belongs to the type domain (the set of all blueprints which have one or more keywords in that domain) and the item and questItem subdomains. Similar for keyword and ancient, magical, pottery, yada yada yada.
Rather than referring to a blueprint explicitly by name, you can access these domains directly. Here is a blueprint for a quest that requires something like a magic vase and will return a key of some sort in return:
@blueprint questAntiqueDealer
@domain type = (quest)
@domain keyword = (genericQuest noCombat)
@property needItems = (& [type:questItem] [keyword:ancient])
@property giveItems = (& [type:questItem] [keyword:key])
@property rooms = (Library, Study)
@end
You can refer to the domain sets simply by using the notation [:
There are two special notations. The first is the ALL set, which is simply the list of all the objects in a domain (ie [type:ALL]). The other is the not set, such as [type:!questItem]. This will return a set of items which are in the domain type, but do not have the keyword questItem. It is defined by removing the set [type:questItem] from the set [type:ALL].
The great thing about using the domains rather than maintaining the lists yourself, other than brevity, is the allowance for additional blueprints to be added later. If you add more quest items with the keyword ancient, they will automatically work with the antique dealer quest.
The domains are updated each time a blueprint is added to the runtime, such as the @end directive when loading from a file or when adding a blueprint manually through code. When the blueprint is added, it is added to the table of named blueprints, the domain:ALL sets for each domain it has, and each of the domain:keyword sets that it has. Since expressions in BlueScript are evaluated as needed, you can add or remove blueprints at any time and the domain sets will be up to date and work properly.
I think it is probably pretty important for properties to be able to access each other on the same blueprint. For instance, you can create a property which is a list of some sort, like a list of treasures, that is shared between multiple other properties, like list of objects that get dropped when an enemy is killed and the list of objects you can pick pocket off the enemy. In a way, properties can be a sort of class variable.
Accessing the properties on other blueprints, however, seems excessive for the limited scope of what a blueprint is. All the complicated logic needs to be done in a generator. I want to avoid putting complex code in the blueprints directly. Also, it'll simplify implementation exponentially. If I think of a really good reason to allow external property access, maybe I'll change my mind.
Another issue is whether a blueprint can set/change properties. I'm against it because, like I said, I want to keep the coding as simple and to the point as possible. Also, unlike Lisp, all expressions are evaluated. There is no way to return an unevaluated expression from a function, thus no way to use a function to set a property to an unevaluated expression. At most, you'd be able to set it to a string or number only, which might not be worth the effort.
Since there's no contextual way to tell the difference between a blueprint name (like MagicVase) or a property (like rooms), properties should be written with an ampersand in front of it, like &rooms.
Similar to how properties can store lists and values to be shared within a single blueprint, there will likely be times when you'll want to share data among many blueprints (like a list of treasure). Global properties can be defined easily enough and stored in the runtime. While global variables are looked down at in iterative programming, they are a simple solution to a complex problem.
Like properties, there's no easy way to tell the difference between a blueprint identifier and a global (or local) variable name. Variables will have a dollar sign before them, like $globalVar.
|
Local Variables and User Functions |
I'm on the fence on blueprint variables. I can see how they would be useful for things like loops, but I sort of think that the purpose of a blueprint property should be purely functional. It is mainly intended for list manipulation and creating random numbers. I'm not sure that variables are strictly needed beyond using properties to share data.
However, I can see where user-defined functions could be useful, and for that to happen, local variables are needed to describe the parameters within the code. Of course, I could limit the local variables to only function parameters.
Also, user functions should be global. The ability to make them part of blueprints is appeals at an object oriented level, but I don't think it needs to go that far. Again, trying to keep the purpose of blueprints simple - a simple solution to a complex problem - and not bloat them into their own programming paradigm.
It might be useful to be able to subclass blueprints. A subclassed blueprint would belong to any domains its parent down (and any it adds itself) and would have the same properties (which can be overriden or added to).
It would be a really easy thing to add. Blueprints would just have a link to their parent blueprint. When a master is created, a list of all the properties on both parent and child would be evaluated. In cases where both have the same property name, prefer the child. The domains would make telling them apart easy, as each subclass would add a new keyword to the type domain, like (item equipment weapon sword shortSword).
Just because something would be easy to add does not mean that it should be added. I can imagine situations where it would make things a little easier and potentially more secure by making sure that generators get blueprints with all the require properties. At this point, I'm of the opinion that subclassing is a worthwhile feature, if not a required one. The fact that there are no variables, functions, or writeable properties on a blueprint makes it easy to add this feature later.
|