Skip to main content

Designing a Game Randomizer

🦸 I! Am! That! Hero!


Heads up, this post is about tabletop gaming and programming. It's gonna get hella nerdy up in here. You have been warned.

Sentinels of the Multiverse: Definitive Edition is my favorite tabletop game, full-stop, no qualifiers. I routinely played it two or three nights a week, sometimes two or three times in a single night. It's a game with high replayability because there's a lot of variability in set up. It involves selecting five-to-seven decks of cards and some associated character cards, and there is a large pool to choose from. At present (in the latest edition of the game and its expansions) there are twenty-four hero decks, with three-to-four character variants each; twenty-four villain decks, with two character variants each; and sixteen environment decks. One villain plus one environment plus three-to-five heroes makes for a lot of options that need to be worked through any time you sit down to play. You can easily spend more time making decisions about what to play than you spend actually setting up the game. What I'm getting at is that this is a game that benefits enormously from having a setup randomizer.

So I wrote one.

While I have not hesitated to share it out with others, I mostly wrote it for myself, and I really didn't have any indication that other people were using it. By happenstance, I was one of the earlier backers to receive my copy of the new expansion and I updated the randomizer and shared it on a discord server and... apparently the thing is kinda popular? Not least because it was updated so quickly. A number of people on that discord server were excited that it had been updated because they use it all the time. Word got back to me that someone I don't know had shared it on a Sentinels subreddit. Someone even emailed me with a feature request!

That last bit, more than anything else, got me ruminating on the design choices that went into it and what my overall philosophy of the application was and my approach towards development. I made a lot of deliberate decisions and have never had any reason to share them. So I'm going to do that now.

Vision

I went into this with an idea of what I wanted it to be, and that vision was as much a matter of practicality as anything else. I liked the functionality of the randomizer in the video game implementation of the original edition of Sentinels that was coded by Handelabra Games, so I used that as a starting point. I wanted something that would be easy to maintain and expand, that offered an experience that was lightweight and streamlined but also felt complete. And I wanted to be able to use it from my phone.

So what does that actually mean?

Maintenance and expansibility are easy enough to understand. I don't have a ton of time to devote to this, and while I have a software development background, I rarely do front-end work, so this will need to be very simple. Also, I know that there will be more content coming out, and I'd like to be able to incorporate that content quickly without having to do major rewrites. But I don't want to fall into the trap of over-engineering solutions to try to future-proof the app against cases that I don't even know for sure are going to be coming down the pike. These are very normal coding principles. All well and good.

Streamlined-but-complete is a bit trickier to wrap one's head around. I didn't want something that would require a lot of clicks to be useable. Ultimately, this means removing explicit user choices, because every choice is a button-push. Every logical gate is something that needs to be tested—and testing a randomizer is surprisingly tricky. Ergo, this app cannot and should not be comprehensive. I need to make certain decisions on behalf of the hypothetical user, and I should only include an option if I believe that the added overhead of another button-push/decision doesn't take away from the overall experience more than the benefit of this feature adds to the overall experience. To that end, I'm thinking a lot about what people are likely to want actually randomized. And since my primary audience was myself, I could make these decisions with a fair amount of confidence.

That said, while it can't be too feature-rich, any included feature needs to feel complete. I don't want to half-ass something just to make my life easier. If it's going to be there, it needs to work.

Functionality

This is what the app needs to do. We need to assemble a team of heroes to take on a dastardly villain in a dynamic environment. The environment is easy: since there is only one per game, you add up all of the available environments and pick one randomly. Selecting a random element from an array is a relatively straightforward operation in Javascript.

Selecting heroes adds some complexity. The game supports three-to-five heroes—one per player or two heroes apiece in a two-player game. However since it's fully cooperative, it's possible for one person to play it solitaire-style with any number of heroes, and the Handelabra implementation had the number of heroes as one of the randomized elements. I liked that, since I mostly play this solo. But again, any feature here needs to be complete, so the user should be able to select a number of heroes: Three, Four, Five, or Surprise Me.

The app then needs to go through the list of available heroes and select X at random, put them in a turn order, and select a character card for each one. It also needs to avoid duplication of decks. You can't have the same hero appear twice in the same game, and you also can't have different versions of the same hero in the same game. "Legacy" and "First Appearance Legacy" are two variants of a character that both use the same deck, so if one of them is selected, the other needs to not be available.

Villains are simpler in that you only need one per game, but here is where I needed to start making some choices to factor in the Event system. The villain represents the game scenario. It defines win and loss conditions and provides the targets that determine how and when the game ends. The Events are a campaign mode for the game. They have dates on them, which means you can play them in sequence. They also have rewards that carry forward. The Event itself is a rules change for a villain, making that villain play slightly differently. Additionally, there are Critical Events. These are also tied to a specific villain deck, but unlike Events, they are a completely new scenario with a new character card. These are, for all intents and purposes, villain variants. Events weren't a thing in the original edition of Sentinels, but villain variants were. They were randomized in the Handelabra implementation, and I liked that, so I wanted to include it. But how?

As mentioned above, I wanted this to be streamlined, and that means removing choices. It would not be difficult to add a checkbox for "include Events" and another for "include Critical Events," but now I'm adding button-clicks. I reasoned that most users probably don't want to do an Event outside of a player-driven campaign, but that most users would want the villain variant to be a randomized element. Now, I don't mind including extra information that the user can choose to ignore, as long as things don't get cluttered. One option would be to include all Events and Critical Events and if the user doesn't want to do the Event, they can just use the base version of the villain instead. But that creates a statistical anomaly. If I reasoned that most users probably were going to ignore Events, but they're equally likely to be drawn, then I'm essentially doubling the likelihood of a normal villain being drawn over a variant villain (i.e., a Critical Event).

I debated trying to add a statistical weighting mechanism, and then I came to my damned senses and decided to include the CE's, treating them as variants, and nothing else. Implementation was simple then, make list of all villains and their variants, and pick one at random. If the user has no intention of playing the variant version of the villain, they can ignore it and play the base version instead.

Finally, I wanted it to be easy to churn through setups until you find one you like. So there should be a button you click to "Get Setup," and once you have a setup, the text on that button changes to "Get a New Setup." No reloads, no changing your parameters, just click a button.

Implementation

If you're even remotely interested in seeing the code, it's on my personal github. It ain't fancy, but it gets the job done.

First and foremost, this needed to be simple. I knew I would go years without touching it. I decided to write the whole thing as a single static web page and embed it in my blog. That way I don't need to worry about hosting, styling, or mobile conversion—it would inherit all of that from the blog template. It would also make development simple, since I just have to worry about a single html page. I can pop that open in a preview window in my development environment or even just drop it into a browser tab. I opted to use Vue as a framework because I was already familiar with it and because it's set up to have presentation (html) and scripting (Javascript) in the same file. Since I'm using the file as a data source as well, it would be lightning-fast, but the file could get long. It's not ideal, but it felt like a worthwhile trade-off.

(I say the file could get long, but at present it's only 200 lines, which is still on the short side for a Vue file.)

Apart from Vue, I didn't bring in any other external libraries. That meant I wouldn't need to update them or worry about security vulnerabilities (which shouldn't be a thing in a static html page but, these days, I mean who even knows!). The fewer moving parts, the less likely it was to break while I wasn't watching. It meant sacrificing some quality-of-life tools that I'm used to, but the trade-off was worth it, considering how simple it was.

The Base Game

For the base game, I just needed a list of environments, a list of villains and their variants, and a list of heroes. For logical reasons, I would need a data object that mapped each hero to a list of its possible variants, but the base game only included two variants per hero and they were the same for each one: the base version and the "First Appearance" version. This meant that I could generate the entire variant-mapping object dynamically from the list of heroes. I could have also just picked a hero and added a coin-flip to concatenate "First Appearance " to front of the hero name, but knowing that there would be more variants coming, I decided to future-proof it a little.

The actual coding was very straightforward. I broke out a few functions to get different parts of the setup that could be called from an orchestrator function and wrote a helper function that would return a single random element from an array. Read as: the kind of stuff any junior coder would know how to do. I put those pieces in place, made some some stupid mistakes, did some manual testing to catch my stupid mistakes, hemmed and hawed about whether Javascript's Math.random() function was random enough, and then uploaded it to my blog and started using it.

The First Expansion

Rook City Renegades was the first of five planned expansions. It adds six new heroes, five new environments, and nine new villains. Each villain has an Event and a Critical Event. Each hero comes with four variants. And it also includes six variants for heroes from the base game. So I had some things to figure out. I would need a way to include different "sets" of content dynamically, which meant abstracting the data model a bit. I would also need to make some choices.

Abstracting the data model was easy enough. I created a list of "set" objects. Each set includes a name of the expansion, a flag indicating that it should be included, a list of included villains, a list of environments, a list of heroes, and a hero variant-mapping object. I added a "select sets" list of checkboxes that iterates over the sets dynamically and links their "included" flags to that checkbox. This would make expansibility pretty easy. Create a new set and it automatically gets added. No logic changes necessary.

The first question I had was what should be included by default. Because each expansion has enough decks to run a game, they could be considered standalones. They don't include enough tokens and spinners, but that is workaround-able. The intention, however, is that anyone playing would own the base game. But, it's possible—likely, even—that someone might want to only play content from one of the expansions, especially if it was new and they wanted to try things out. So I opted to set the "included" flag to "true" for the base game, and leave everything else as "false."

The mechanics of adding new variants to base game characters raised some interesting challenges. I was already creating the mapping object dynamically to include the base and "First Appearance" versions of each hero. That meant that each set would include a mapping object that only had two of the four variants for that character. And RCR would also have six more object keys that mapped to a list with a single object in them. (You can see them in lines 71-84 of the above-linked file, which I would link to those lines here but the link will be stale the next time I update the file.) It's a little clunky. I don't love it. But it works and I don't have a better solution in mind, so ¯\_(ツ)_/¯

The other question to ponder is whether or not the new variants for the base game should be considered as part of the expansion set when randomizing. "Stealth-Suit Bunker" is a new variant for Bunker included in RCR. But if you only selected Rook City Renegades and not the base game, but you drew that hero, would you be happy about that? I was torn, because I think it would be more accurate to say yes. Yes, if you only selected a single expansion and not the base game, base game hero variants included in that expansion should be fair game. But I opted against it because it violates the Principle of Least Surprise. I would expect that someone excluding the base game from their choices would be expecting to only get heroes from the expansions they selected. I briefly debated adding a checkbox for this. Briefly.

The New Expansion

Disparation is the second expansion and I received it last Wednesday. Because I had created the randomizer to be expansible, adding the new content was very straightforward. Except... there were a few wrinkles. (I say that like it's a bad thing... even with new logic and testing it still only took me an hour-and-a-half.)

The first issue was one that I knew about before the game shipped. One of the new heroes is "Darkstrife/Painstake" who are a pair of heroes that operate from a single deck. But they each have their own character card, and each character card has its own variants. And—importantly—the variants of those two heroes can be mixed and matched. So I would need to be able to account for this. I debated dynamically generating a mapping of the sixteen possible combinations, but I hated that solution. First, it felt like a hack. Second, it messed with the logic I already had in place to dynamically generate "First Appearance" variants. Also, I knew that there would be a future deck for The Southwest Sentinels, which is a hero team that has four character cards operating from a single deck. So if dynamically jumbling a list of sixteen felt like a hack, then dynamically jumbling a list of 256 felt even worse.

The solution I landed on turned out to be rather simple. I created variant mappings for "Darkstrife" and "Painstake" individually and put a logical gate that looks for a slash in the name of the hero. If so, split the name at the slashes and get the variants individually, then concatenate the results back into a single slash-delimited string. Easy peasy. This could be re-used for The Southwest Sentinels; I'd just need to render them as "Mainstay/The Idealist/Doctor Medico/Writhe" and I could run that through the same function. Plus, it let me do a little recursive logic, and if there's one thing that makes a Javascript developer feel fancy, it's writing recursive functions.

The wrinkle I hadn't anticipated involved one of the villain variants. This is a spoiler for Disparation content, so if that's something you care about, skip down a bit.

HERE THERE BE SPOILERS

The Critical Event for the villain "Miss Information" has her alter ego Amenia Twain being kidnapped by another villain. So in order for this feature to be complete, I would need to randomize not one but two villains in this specific instance. I decided to phrase it as "Amenia Twain Kidnapped By " plus the new villain. Unlike the hero logic, I don't have anything in the villain logic to remove decks that have already been selected. The quick-and-dirty solution is to make a recursive call and then evaluate the result. If it starts with "Amenia Twain Kidnapped by Amenia Twain Kidnapped by " or "Amenia Twain Kidnapped by Miss Information," get a new villain. As solutions go... I don't love it. I will probably go back an clean it up to prevent the error rather than catching it after the fact.

That said, the fact that I took this little extra step was commented on by someone in the aforementioned discord server. They appreciated that I'd made the extra effort, and let me tell you the warm-fuzzies I got... my heart grew three sized that day. That day being last Thursday. Anyhoo.

END SPOILERS

The final wrinkle with Disparation is the one that I got a feature request about. Disparation includes a "Principles" deck that has optional rules for heroes that you select during setup. You can deal them out to each hero or draw three and pick one... there are a number of ways to do it if you even choose to play them at all. I hemmed and hawed about how to include them. Do I have a dropdown to draw one, draw three, or not use them? Do I just add three to each hero and let the user either live with the first one or make a decision on the other three? That cuts down on the clicks but it adds a bunch of noise to the setup page.

Cooler heads eventually prevailed when I realized that I didn't need to account for the Principles deck at all because it already has a built-in randomizer. Because it's a deck of cards. You can just shuffle and deal them.

The Future?

As I mentioned above, there were five planned expansions and, of those five, two expansions have already been released. That leaves roughly half of the game content still out there. And then...

In April of 2025, Greater Than Games, a subsidiary of Flat River Group, laid off almost its entire work force. The company that makes and distributes Sentinels of the Multiverse had, for all intents and purposes, folded. This was the culmination of several factors: an unfortunate medical turn for one of the creators that set the project back by a year; some business decisions that, to an outside observer, could charitably be called "questionable"; and, of course, the Trump tariffs that decimated the gaming industry. (Yes, that's the least of his sins, but it still stings.) There was some question about whether or not the crowd-funded Disparation expansion, which had already gone to the printers, would even come out.

As of January 1st, 2026, the creators of Sentinels have separated from their parent company and expressed optimism about the future of the IP. So while the future is very much in question, there is a reasonable chance that the remaining three expansions might come out.

What does this mean?

For the randomizer? Complexity. Vengeance Returned, the next slated expansion, introduces a new game mode where your team of heroes goes up against a team of villains. The implementation of this in the original edition was... not very well loved... so it stands to reason that there will be a big overhaul. I won't be turning this one around in an hour and a half. But that's fine. The expansion after that, probably to be called Cosmic Tales, will be a normal expansion, and if they get far enough to release the sixth one, Oblivaeon, it may or may not introduce a new game mode in addition to more variants for all the preceding villains and heroes.

That'll be a lot to sort through. But the part of me that still likes the problem-solving aspect of programming is looking forward to the challenge. I mean... this game is a math problem baked into an engine-builder that thinks it's an RPG and is layered with Tolkein-esque levels of thematic world-building. Small wonder that I just eat this shit up. And smaller wonder that I would take the idea of writing a randomizer and turn it into a project that I'm so fond of that I've written [checks word count] a nearly four-thousand-word essay about the design principles that went into it.

Kind of shocking that I wasn't diagnosed as neurodivergent until about five years ago, right? RIGHT!?

Okay, but what does this mean?

To be honest, this was a blow. In a year that was already very unnerving for me due to changes in my work situation and [waves hands at world] circumstances, having the makers of my favorite game shutter their offices was a source of depression that I just didn't know how to deal with. So I avoided dealing with it. From April 2025 to January 2026, I only played Sentinels twice, and only at the behest of someone else. It wasn't until I got Disparation in my hands that I got excited about the game again and forced myself to process my grief over the possibility that it might just be over now.

But I'm choosing to be optimistic. I have every intention of figuring out how to program Team Villains into this thing and posting it on a discord server as soon as it's updated, because that will enrich the lives of a handful of people on the internet that I've never met.

I suppose there are worse outlooks you could have.

]{p

Comments