Blog

How to Build a Cyberpunk Saloon Without Losing Your Mind

There's one thing most game devs can agree on: making a game is really damn hard. It's difficult enough to come up with a cohesive idea for a game; having to translate your vision into engine API calls is an ordeal that causes many people to bounce off of game dev entirely. Allegory.js aims to eliminate this headache by delivering APIs crafted specifically for the type of game you want to make. Allegory.js is not the first project to try to do this. In particular, Inform 7 deeply inspired the direction that Allegory.js is heading in. In Inform 7, you can write something like The kitchen is a room. The wooden table is in the kitchen. On the table is a steaming mug. and it actually does something. The revelation that code could be so expressive blew my mind to bits. However, Inform 7 has some limitations that hold it back from being the final word in interactive fiction game creation.

The Cost of Specialization

Software architecture could be said to mostly be about managing tradeoffs. Inform 7 is a domain-specific language (DSL), going all-in on specialization for creating systems-driven interactive fiction games, and it does that really, really well; it is deeply expressive, intuitive, and focused. It is truly one of the coolest projects to ever grace the world of game creation. However, because of the approach taken in its creation, it makes some sacrifices: you can't integrate with external APIs (e.g. network calls for dynamic content delivery, analytics, cloud saves, or multiplayer, or adding visualization / audio cues), most implementations of the interpreter are not compatible with modern accessibility devices, it is virtually impossible to localize games for non-English-speaking players, the underlying legacy VMs struggle to scale with modern web integrations, the tooling is stuck in 2006, and its documentation is around 16k pages long. These limitations on their own aren't the end of the world, but together, they amount to a tool that is hard to recommend for a lot of projects.

Allegory.js aims to shift the slider a bit away from the DSL approach, nudging it in the direction of a generalist approach. Allegory.js is created with TypeScript. While TS lacks the poetic English-like syntax of Inform 7, it unlocks the entire modern web ecosystem. It allows devs to pull in a bewildering number of extant packages and resources, and it means games can run in the browser, with all of the flexibility and a11y / i18n goodness that comes with that. The goal for Allegory.js is still to have an extremely intuitive developer experience (DX). To achieve this, a different set of tradeoffs is made, as compared to Inform 7. Enter World Kits.

DSLs for the Rest of Us

A World Kit is a genre-specific facade that sits on top of the core Allegory.js engine. This facade turns the powerful, relatively technical API of the engine into a pseudo-domain-specific-language. Here's an example how it could look for setting up a scene in a cyberpunk saloon.

Note: the following examples are illustrative, and do not necessarily represent the final APIs of the engine or World Kits. The Allegory.js engine is still a work in progress!

Raw Allegory.js APIs:


const ecs = new ECS<MyRawSchema>();
const pipeline = new IntentPipeline(ecs /* ... */);

// 1. Manually Create Entities
const playerId = ecs.createEntity('player_1');
const saloonId = ecs.createEntity('saloon');
const cyberDeckId = ecs.createEntity('deck_01');

// 2. Build the Saloon (with manual lighting logic)
ecs.addComponent(saloonId, 'LocationData', { 
    name: "Neon Saloon", 
    desc: "Smells like ozone and cheap synthol." 
});
ecs.addComponent(saloonId, 'Illumination', { 
    isLit: true, 
    lightLevel: 55 
});

// 3. Build the Cyberdeck (with manual hierarchy and stats)
ecs.addComponent(cyberDeckId, 'Position', { parentId: saloonId });
ecs.addComponent(cyberDeckId, 'Interactable', { 
    name: "Militech Deck", 
    description: "A heavy, chrome cyberdeck." 
});
ecs.addComponent(cyberDeckId, 'Cyberware', { 
    ram: 10, 
    isHackable: true,
    iceLevel: 1 
});

// 4. Manually write the Standard "Take" Law
const RawTakeLaw = DefineLaw({
    name: 'RawTake',
    layer: LawLayers.kit, // StdLib Layer
    intents: ['TAKE'],
    matchers: [{ target: { components: ['Interactable', 'Position'] } }],
    apply: async (ctx) => {
        const targetPos = ctx.ecsUtils.getEntityComponentData(ctx.target.id, 'Position');
        const playerPos = ctx.ecsUtils.getEntityComponentData(ctx.actor.id, 'Position');
        
        if (targetPos.parentId !== playerPos.parentId) {
            return { status: 'REJECTED', narrations: ["It's not here."] };
        }

        return {
            status: 'COMPLETED',
            narrations: ["Taken."],
            mutations: [
                { op: 'UPDATE', entity: ctx.target.id, component: 'Position', value: { parentId: ctx.actor.id } }
            ]
        };
    }
});

// 5. Manually write the Cyberdeck Instance Hook (Layer 3)
const DeckTakeOverrideLaw = DefineLaw({
    name: 'DeckTakeFlavor',
    layer: LawLayers.instance, // Instance Layer (Overrides standard physics)
    intents: ['TAKE'],
    matchers: [{ target: { ids: ['deck_01'] } }], // Hardcoded to this exact entity
    apply: async (ctx) => {
        return {
            status: 'PASSED', // Allow the standard RawTakeLaw to run next
            narrations: ["Sparks fly as you disconnect it from the bar."]
        };
    }
});

// 6. Register everything
pipeline.ratifyLaw(RawTakeLaw);
pipeline.ratifyLaw(DeckTakeOverrideLaw);

// Note: To make 'isHackable()' work, we would also need to write a 40-line 
// HackingLaw here, but for brevity, we assume it's already registered.

Using the Cyberpunk World Kit


// 1. Initialize the Game with the Kit (Registers standard Laws automatically)
const game = Game.create({
    kits: [CyberpunkKit] 
});

// 2. Define the world using the Kit's Fluent API
const Saloon = Location('Neon Saloon')
    .describedAs("Smells like ozone and cheap synthol.")
    .isIlluminated(); // Automatically adds LightSource components

// 3. Create genre-specific items
const Deck = CyberDeck('Militech Deck')
    .withRAM(10)      // Automatically adds HackingStats component
    .isHackable()     // Opts into the HackLaw provided by the kit
    .locatedIn(Saloon); // Handles the ECS Position component mapping behind the scenes

// 4. Add custom Instance-level logic (Layer 3) easily
Deck.onIntent('TAKE', (ctx) => {
    return {
        status: 'PASSED', // Let the Kit's standard "Take" physics happen...
        narrations: ["Sparks fly as you disconnect it from the bar."] // ...but add flavor.
    };
});

game.start();

Looking at the two examples above, the code using the cyberpunk World Kit is a lot more readable and intuitive than just using the base engine. There is a lot of boilerplate that the game developer doesn't have to type out in the World Kit example-- but more importantly, the code in the World Kit example is declarative, whereas the raw engine version is imperative. In the raw version, you need to understand how the Entity-Component-System (ECS) module works, and you also need to understand how to author Laws. In the World Kit version, the author can just focus on painting a scene, and let the facade handle the abstractions.

Part of the appeal of World Kits is that they are plug-and-play. There will be several available following the 1.0 launch of the engine, and the community will be empowered to create all sorts of World Kits that I could never dream up. Some World Kits may even include things like UI elements or sound packs or accessibility features. This approach pulls in some of the syntactic magic of Inform 7, while still allowing devs to make use of modern tooling. The main tradeoff is that World Kits require nontrivial upfront work to develop. This is a tradeoff that feels appropriate to me though-- one or more people can spend time cooking up a cool Kit, and after that, any number of people can create entire games in that genre without needing deep technical chops.

World Kits will also be composable, so you could pull in the Western Adventure Kit and the Cyberpunk Kit, and you automatically have a mashup of genres without doing any manual labor at all. There will be some well-documented nuances regarding World Kit compatibility, e.g. a Create Your Own Adventure Kit with no simulation elements won't necessarily dovetail perfectly with a Zork-like Adventure Game Kit.

It's really important for the feasibility of Allegory.js that World Kits exist alongside custom-made code. For a good number of games, the World Kit will provide everything needed to create the story that the author wants to tell. For others, the game logic may need to have, for example, bespoke Laws which add in entirely novel mechanics.

Wrapping Up

World Kits help bridge the gap between the deeply specialized approach of Inform 7 with the generalized power of a more traditional game engine. The result is a game engine flexible enough to be useful for both modestly technically-inclined people looking to express a story in their head, as well as developers looking to craft games with deeply custom systems and mechanics.

As always, thanks for tuning in and following along as Allegory.js is built! Enjoy the rest of your day :)

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.

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.
    Core = 0,

    // Laws coming from world kits, providing the generic functionality which is
    // likely to be used by games in a given genre
    // e.g. for a game using the adventure game world kit, this would include
    //  things like movement, perception, combat, and inventory
    Kit = 1,

    /***********************
     *** Userland Layers ***
     ***********************/
    // Laws related to the specific game being built. The default layer for new Laws created by game devs.
    // e.g. "Taking a Cursed Item deals damage"
    Game = 2,

    // Laws related to a specific entity, attached via Script
    // 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.