On Laws & Specificity
The Problem
In Allegory.js, the goal is to create an engine that allows for systems to be added or removed without breaking anything. The engine itself is designed to be completely unopinionated about what kind of game is being made—whether it's a deep simulation game where every one of the player's commands ticks a clock forward and the whole game world progresses (Interactive Fiction Pathologic, anyone?), or a linear choose your own adventure game with a deep dialogue system. So how can an engine be deeply unopinionated and useful at the same time? That's the question I grappled with for quite a while. If the engine really didn't care what you did as a developer, then I'd really just be shipping an ECS. And honestly, there are already great ones out there. ECS is extremely powerful, but it's not really suited to IF games—on the surface. It would take a lot of work and thought to build a game from scratch only using a smart parser and an ECS module as your baseline. The vision for Allegory.js is to make it stupidly easy to build a game, while still allowing the tinkerers out there to do anything they can imagine. That's where Laws come in.
Laying Down the Law
Once the engine has extracted the player's intent(s) (more on that in a future post), the engine needed to be able to slot in logic to handle the intent. Sure, we have extracted a magic word (ATTACK) and a target (goblin_03) and an implement (rusty_sword) from the player's command. Now what? If the game engine is unopinionated, which it needs to be, the engine can't have game logic hardcoded in it which handles attacking a goblin with a sword. A game about taming wild horses in pre-colonization North America doesn't have goblins, or swords, or attacks. So we (my AI pair-programmer and myself) came up with a system of Laws. A Law is basically a container for an Intent handler with some important metadata; it reacts to the player's commands by suggesting mutations to the game state, events to emit, and narrations to display to the player. A game may have any number of Laws, handling things from dialogue to combat to companion pet behavior. Laws are what make a game react to the player directly.
...but what if we have 75 Laws (god forbid)? How do we know which Law should handle the Intent? Well, if there is only one Law that cares about ATTACK, it's easy. What if there are two Laws that say they can handle attacks though? Perhaps it's a horror-adventure game, and one Law, SanityLaw, checks if a player has a certain level of sanity before they are allowed to do any action. Perhaps another more basic Law, CombatLaw, just looks at the attacker's stats and weapon and target, and calculates whether the attacks lands and how much damage it does. We now have two Laws saying they can handle the Intent of the player. Which one runs first? How do we determine whether the Intent is swallowed by the first Law or if it needs to be handled by the second Law too? We don't want to duplicate all of the combat logic from CombatLaw inside of SanityLaw, and we want the latter to run first, so it can decide if the player is allowed to attack at all, or if they drop their sword in fear. We need to get specific about Law sequencing.
Specificity Wars 2.0
In CSS, there is a concept of specificity; if a selector like .my-class says some text should be green, and .my-class:not(.valid) says it should be red, then the text will be red when the element does not have the valid class. The selector with the most specificity wins. Allegory.js takes a cue from CSS here. If a Law is able to just say what its priority is, so that SanityLaw definitely always runs before CombatLaw, there is no idiomatic way to let the creator of each Law (which may come from plugins, the engine's standard library, or the game dev) declare when in the execution order their Law should come. In CSS, you end up with z-index: 999999 !important all over the place because everyone's need is urgent to them. And when everything is important, nothing is important. The specificity system in CSS is actually very elegant when not abused, so for Allegory.js, what if we could use specificity to decide Law execution order, but not allow arbitrary "my Law is the most important" assertions? This would naturally lead to a sane and orderly execution of Laws, because each Law would run its handler in the proper pecking order. So, a Law not only has a handler for Intents, but also has a property called matchers. A Law can declare any number of matchers, which are essentially classifications of Intent that the Law cares about. A matcher is, without getting too into the weeds, just a list of actor, target, and implement specifiers; for each actor/target/implement entity, the law can define different criteria that it cares about, e.g. "I care about all Intents where the actor has PlayerComponent, and has SanityComponent.sanity_level < 0.3."
When an Intent goes to auction, Laws "bid" to be able to handle it. Each bid is just the specificity score of the matcher which most aligns with the Intent. Maybe matching a component on the actor is worth 20 points, and matching a tag is worth 5 points, and matching an ID is worth 100 points. It's all about specificity. Whatever Law has the most specificity wins the bid and gets to handle the Intent. The Law can then:
- "pass", which allows the next highest bidder to handle the event (e.g. for cases where a Law just adds flavor text, or makes a small adjustment to a stat)
- "complete" (prevent any other Laws from running)
- "reject" (determining that an Intent is actually invalid, and roll back all changes)
Each Law that handles an Intent piles on its suggested changes and narrations and events, and if no Laws reject the intent, those state changes are committed and the narrations show on screen and the events emit (more on events in a future post).
There's Levels to It
Specificity as described is effective, but lacks a mechanism for classifying Laws and ensuring that the vanilla, engine-level Laws don't happen to override a more specific category of Law, like one implemented by a game dev. Specificity, then, was decided to be a tuple, [level, score]. There are four levels:
/*********************
*** Engine Layers ***
*********************/
// built-in laws, which typically handle basic functions (like saving) or
// indicating that the engine does not understand a command,
// and which are easily overwritten. Typically returns a COMPLETED in the contribution
Core = 0,
// laws packaged with the engine that the dev may choose to use;
// readily relinquish control of Intents. Often returns a COMPLETED in the contribution
// e.g., "Take command moves item to inventory"
StdLib = 1,
/***********************
*** Userland Layers ***
***********************/
// laws related to the specific game being built
// (and/or laws coming from a genre template)
// e.g., "Taking a Cursed Item deals damage"
Domain = 2,
// laws related to a specific entity, attached via Script
// e.g. e.g., "Taking the Idol triggers the boulder trap"
Instance = 3
Putting it all Together
Once we have the score tuple for every Law which says "yes, I know how to handle that type of intent", they are ordered first by level, second by score, and... That's it! We have a system which predictably and reproducibly executes code based on the player's command. The standard library of the engine will come with a bunch of useful Laws, and devs needing more flexibility can define their own. All you need to do is describe, in a structured way, when your Law cares about an Intent, and the engine does the rest.
~*~*~*~*~*~*~*~*~*~*~
That's all for today! Thanks for stopping by 👋💎