Blog

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 👋💎

A note about AI usage:
This article was hand-written by real individuals working on Allegory.js. While we use LLMs to great effect to help ideate, review code, and work through architectural decisions, all blog content is created by humans, and reflects the unique experiences and viewpoints of the authors.

Beyond Branching and Scripting: The Philosophy of Allegory.js

The State of Affairs

If you want to make an Interactive Fiction (IF) game, you have your pick of some really powerful and well-established tools, as well as some very cool newer choices. The main tools in this space these days are Twine, Inform 7, and Ink ("the big three"). Some other options are Ren'Py, ChoiceScript, Adventuron, and Narrat. These tools are all amazing. Go check them out, seriously. The big three have huge communities, lots of resources, and they're all free to use.

Twine is unmatched for Choose Your Own Adventure (CYOA) games, which typically lean heavily into the narrative side of things; you end up with branching paths of hyperlinked webpages constituting your game. These are perfectly suited to the style of adventure where the player/reader is presented with some choices (do you want to take the left or right door?) and each one takes you down a set path. Think, the old Goosebumps books, where you skip to page 46 if you choose left, and 73 if you choose to go right, but way more powerful and well-suited for nonlinear narratives. However, for games where you need to track state and have reactive systems (e.g. you can pick up a key in the beginning of the game, and that stays in your inventory until the end of the game, where if you have the key, you can unlock a secret door; or you pick up an apple, and over time, it rots if you don't eat it), you run into some of the limitations of the engine. You have to rely on macros which aren't the prettiest or most intuitive to use, and you can end up with a lot of if-else checks for logic all over the place.

Inform 7 is a masterclass in developer experience and game world simulation. The plain English-like programming language you use to create games in Inform is frankly astonishing. You write code like "A car is a type of thing. A car has four doors. A door can be open or closed," and so on, to build the rules of your game universe. The compiler takes care of the data and simulation based on this expressive code, and as the player you can walk around, pick things up, open and close doors and containers--all sorts of things that make the game feel richly detailed. However, it has a few usability issues. The game developer needs to think of all synonyms and ways of expressing ideas, and if they miss something, you can end up in a situation where the engine doesn't understand it if you type "attack the goblin with the sword," and the player needs to figure out the special keywords, which might be something like "use sword on goblin". This can break immersion and frustrate the player, and create a bit of a learning curve for players. Also, the engine generates images that run in a VM; there are implementations of the VM for the web, but in terms of accessibility and styling, you have very few options. You are locked in to a Zork-style text adventure that some people (such as vision-impaired individuals) don't get to enjoy.

Ink is a really cool tool that excels at narrative branching and dialogue scripting. It is made as a sort of middleware that allows game devs to write their games' flows in a way that reads like a screenplay. It plugs into game engines like Unity to enable all sorts of audio-visual accompaniments and great console exportability. However, Ink itself does not provide a way to author game logic. If you want an inventory system, you have to write it in the "host" game engine itself. It's perfect for defining text flows, but isn't "batteries included" when it comes to other types of functionality.

There are lots of other awesome tools and engines in the IF space, and they each bring unique strengths to the table. They are all absolutely worth looking into, and one of them may be the perfect tool for making your game. So where does Allegory.js fit in?

An Engine for the Modern Web

Allegory.js is a web-native IF game engine with NLP capabilities and an ergonomic API


Allegory.js humbly builds on ideas of the titans which came before it, and it also has a few new tricks up its sleeve. Allegory.js aims to:

  • enable players to declare what they want to do (or even ask if they can do it), without memorizing special keywords and syntax, e.g.

"can I break that window with the rock?"


"slash at that goblin"


"talk to the guy with the hat"


  • provide a fluent API that gets out of the game developer's way
    ThereIsAContainer('old_chest')
        .containing('rusty_key')
        .onIntent('OPEN', () => Success("It creaks open."));
    
  • enable a quick development cycle out of the box for most people, while also giving the relentlessly "think-outside-the-box" 1% of game devs the tools to do whatever they can dream up
  • empower game devs to create a wide variety of games, from systems-light CYOA to hardcore simulation
  • put the full breadth of the JS/TS ecosystem at IF game devs' disposal
  • give all types of players access to games by baking in support for web accessibility tools and internationalization
  • make use of emerging machine learning techniques to make development faster while leaving the creative vision solely in the hands of the developer
  • ship in a tiny, efficient package that can run just as well on low-end phones as powerful desktops
  • be able to do everything locally in the player's browser without necessarily needing to pay/wait for API calls

The vision is a game engine that devs of all skill levels and creative inclinations will love to use.

What's Next?

This is the first installment of the Allegory.js blog. In future posts, we'll mostly be discussing architectural decisions and topics we've explored in the process of making this engine. Always feel free to leave thoughts in the GitHub discussions. Allegory.js is still in its infancy, and you are invited to follow along as it grows.

Until next time, thanks for stopping by, and take care!

A note about AI usage:
This article was hand-written by real individuals working on Allegory.js. While we use LLMs to great effect to help ideate, review code, and work through architectural decisions, all blog content is created by humans, and reflects the unique experiences and viewpoints of the authors.