In my previous post, I tested Adventure, a game that had the decency to come with its own specification: the original cartridge. This time I’m throwing down a gauntlet. Literally. And this time there’s no cartridge to hide behind.

Let’s move forward a few years from Adventure, to 1985. Arcades were still the place to be (or so I told myself at the time). It was in this hallowed year that Atari Games released Gauntlet, a top-down dungeon crawler where up to four players picked a Warrior, a Valkyrie, a Wizard or an Elf and fought their way through mazes. These mazes were packed with ghosts, grunts, demons. Even Death himself (herself? itself?) made an appearance. It was one of the first arcade games built around cooperation rather than competition.
The game also had an incredible magic ability: it was very good at turning your quarters into health! You see, your health drained constantly. Even when nothing was hurting you. The game did its level best to relieve this angst by always politely suggesting another coin to get that health back up. And then there was the narrator. If you were there, you still hear “Warrior needs food, badly” in your sleep. And you still haven’t forgiven whoever you were playing with that accidentally shot the food.
Because apparently I didn’t learn my lesson with the last one, I built a browser homage to this game called Gauntlet Reforged. It’s open sourced and it’s playable online. It’s written in plain JavaScript on an HTML5 canvas, and it’s a partial implementation: seven training levels that teach the mechanics, followed by ten dungeon levels.
This is an homage, not the original. It isn’t affiliated with Atari, Midway or Warner Bros., and none of their assets are in it. The README explains that in more legal detail than you probably want.
So, as with Adventure, let’s talk about testing it. But there’s a big difference this time, and it shapes almost everything that follows.
Purpose and Audience
With Adventure, the goal was fidelity: find every place where my version behaves differently from the 1979 original. This game, on the other hand, is an homage, not a port. There’s no original program to compare against line by line. By this I mean there’s no readily available, full assembly listing for the game that I could annotate.
Gauntlet ran on the Atari System 2 board hardware. Unlike earlier, simpler games (like Pac-Man or Asteroids, which used a single 8-bit CPU), Gauntlet relied on a T11 primary CPU. This CPU was running a variant of the PDP-11 instruction set, co-processing alongside a 6502 audio CPU, dedicated custom graphics hardware, and a complex bank-switching memory mapping system. Fully tracing and documenting that architecture by hand is a massive undertaking. And, yes, I started to go do that path. My sanity gave out before my patience did.
So, instead of relying directly on a programmatic disassembly, as I did for Adventure, the goal here is to find where the game fails its own promises: what the README (that I wrote) says it does, what the code (that I wrote) says it intends, and what a player reasonably expects. On that last point, it’s worth noting that the player does not have to be someone who has ever seen the original game.
As before, this strategy serves two kinds of tester.
- Testers who explore by playing. You’ve got the live game, the README’s controls, and the URL flags. You can use keyboards, gamepads, a second player (if you can convince someone), notes and screen recordings.
- Testers who explore the code. This is all about reading JavaScript; using the main game file, the engine scripts, the browser console, a little bit of code review, along with scripted play in a browser.
And as before, the two tracks feed each other. A tester who sees a monster freeze can consider their level, the character and the flags they were using and try to dig into the code. A tester who spots a suspicious line in the code now has a very precise thing to try in the game.
What Correct Means
This is where losing the cartridge hurts. With Adventure, the original game was the specification, and when the two disagreed, my version was wrong. Here, the oracle is a lot weaker. That being said, here are the sources a tester can lean on, in order of authority.
- The README. It makes specific, checkable claims: the controls for each player, what every URL flag does, how hard mode routes monsters, and what the Konami code switches off. Every claim in there is a test waiting to happen.
- Intent stated in the code. The main file is heavily commented, and the comments often state a rule outright, such as dead players counting as done when deciding whether a level is finished. A comment that disagrees with the code beside it is not a proof statement, but it’s certainly a bit of evidence along the path.
- Consistency. The same action should behave the same way everywhere it can happen. A player joining from the menu and a player joining mid-run should get the same setup. A keyboard player and a gamepad player should have the same abilities. This is the consistency heuristic that good testers have honed through a career wading through inconsistency.
- The 1985 arcade game. It’s a loose reference for feel and rules, not a specification. The README says this is a partial implementation, so a missing arcade feature isn’t a bug unless the README promises it.
When a behavior is surprising but no source settles it, a tester should, of course, record it as a question for the developer rather than a bug. Now, in this case, the author is me, so I got to argue with myself before putting this post out there. That’s actually more productive than it sounds.
This is a much more typical testing situation than Adventure was. Most software doesn’t come with a reference implementation. It comes with documentation, code comments and people’s expectations, all of which can disagree with each other.
On that point about “question not bug,” there is a caveat to that. Sometimes something surprising is considered a value threatener. If we go with the idea that a bug is anything that potentially threatens value, we can frame that around what a reasonable player would determine about the game. If a reasonable player might conclude something is a bug, we should probably treat it that way while we investigate.
Risk Areas
In terms of risk there is something that, if you’re a tester used to dealing with builds, you might glom onto looking at this repo. This facet is also how this project differs substantially from Adventure. The highest risk here isn’t in the game logic at all! Every push to the main branch goes straight to the live site, and nothing automated checks it first.
With that in mind, let’s consider some risks, with the one I just stated right at the top.
| Rank | Risk | Why it is risky here | Tracks |
| 1 | No safety net before deploy | The only workflow deploys on every push to main. There are no automated tests, so a broken change ships immediately | Technical |
| 2 | Stale generated bundles | Two of the three scripts the page loads are generated and committed. Editing an engine source without rebuilding ships old code, and the dev server hides it | Technical |
| 3 | Multiplayer and controllers | Up to two keyboard players plus gamepads, joining from the menu or mid-run, each claiming a character and a control set | Human |
| 4 | State machine and timers | Eight game states, plus a help popup that resumes on a wall-clock timer rather than game time | Both |
| 5 | Level data | Each level is a PNG whose pixel colors encode walls, doors, monsters and treasure. A wrong color is a silent map bug | Both |
| 6 | Randomness | Monster spawns, directions and tie-breaks are random, so bugs hide until the right roll | Both |
| 7 | Hard mode routing | Monsters follow a shared distance map that must treat closed doors as walls and update as players move | Both |
| 8 | Saved progress | The level checkpoint, high score and difficulty persist in browser storage between sessions | Human |
As a developer, let’s say you created this project. Those first two might sting a bit. Putting myself in that spot (since I was the developer who created this project), I could have been spending a lot of time worrying about the monsters and very little worrying about how the game gets to players. That’s a pattern worth noticing in your own projects: the risk you think about most is often not the risk that bites the most.
I can’t stress this part enough. Adventure and Gauntlet were designed to build and deploy in very different ways to showcase one of the most problematic types of internal qualities.
Test Layers and Testability
Unlike Adventure, there are no automated tests in play for this project at the time of writing. The good news is that the implementation already exposes most of what a test harness needs. The backbone is the game’s state machine. The game only simulates while it’s in one of three states; everything else is menus, loading screens and popups.

What this means is that I built this project with what I call the integration seams already in mind. For a tester, or even just another developer, these seams already exist and need no code changes to provide testability.
| Seam | What it gives a tester |
| The running game in the console | Level skipping, hard mode, grid and tileset debugging, and access to players, monsters and map state |
| URL flags | Jump to any level, pick a character, switch off damage, monsters, generators or treasure, and preload keys and potions |
| The Test Level | Level 0 is a built-in map reachable only through the level flag |
| The engine’s manual mode | The game loop can be stopped and stepped one fixed frame at a time |
| One random number helper | Every random draw goes through one function, so a seeded generator injected at page load makes runs replayable |
That’s a contrast with Adventure, where I had to suggest changes to the code to make deeper testing possible. Here, the seams are already sitting there because I designed them to be there. They just weren’t initally being used by anything except me while debugging.
Later in this post, I’ll add one seam that did take new code: a computer-controlled second player, for all the testing that needs two people when only one is available.
Built on those seams, there are some layers worth adding. You might want to think about what those are. As in the previous post, I’ll provide an answer key near the end of this post for my ideas.
Heuristics for Exploratory Play
The heuristics I’ll talk about next need no code. As with Adventure, my recommendation would be to pick a few per session rather than trying them all at once. The URL flags are your friend here: they let you skip straight to the interesting situation instead of playing through three levels to get there.
This, by the way, is crucial in game testing. Think about some of the complicated games you have played or seen where you wondered how testers kept testing some part of the game that only took place after hours of narrative and repetitive traversing. Well, this is how: you build orchestrators. This is really just a fancy way of configuring an application to be in a given state that you would do in any application.
As with the previous post, before reading on you might want to explore the game a bit and consider what you would do in terms of testing it. Then come back here and see if you missed anything. Or if I missed anything!
One thing worth noticing before you start: every heuristic in the next list assumes the players are sitting at the same keyboard. That’s how the arcade cabinet worked, and it’s what the README describes. But this is a web page with a link you can share, and anyone who sends that link to a friend in another city will reasonably expect to play together. They can’t. Each browser runs its own separate game.
Is that a bug? None of the sources from “What Correct Means” say so. The README never promises remote play, and the arcade game never had it. But it’s a strong user expectation, and this is exactly the kind of surprise that goes on the list as a question for the author rather than a bug. Here, again, that author is me.
Players and Controllers
- Join every way. Start with each number key, join a second player mid-run, and add a gamepad. Check that each player gets the character and control set that the README promises.
- Fight over a character. Have both players press the same number key. Have a gamepad claim a character a keyboard player already holds.
- Split the party. Walk the two players to opposite ends of a large level and watch the camera zoom. Let one player die while the other carries on.
- Leave at the worst time. Let one player reach the exit while the other is dead, dying or still fighting.
Testing alone? Add ?buddy=guardian to the URL and the game gives you a computer-controlled second player. I built in eight personas, each built to stress something different, and I’ll come back to them, and to what they found, in “A Second Player Who Isn’t There” later on.
Doors, Keys and Exits
- Count keys across levels. Collect keys, exit, and check how many you start the next level with. Keys and potions behave differently.
- Open a run of doors. Doors next to each other open together. Try long runs, corners and doors next to walls.
- Starve yourself of keys. Combine the no-treasure flag with fewer keys than a level needs, as the README warns.
Combat and Health
- Stand still and watch. Health drains by 1 every half second. Check how long each character lasts, and what food and potions add back.
- Fire while surrounded. A player can’t move while firing, so find out how quickly a crowd can pin someone down.
- Use a potion at the edge. Trigger a potion with monsters just inside and just outside its range, for each character.
States, Timers and the Browser
- Dismiss help, then trigger more help. The help popup resumes itself after 4 seconds of real time. Dismiss one early and reach the next level’s popup quickly.
- Leave and return. Switch to another tab for a while, then come back. The game catches up on missed time in one burst of up to a second.
- Type the Konami code mid-fight. Its arrow keys move player 1, and its A key moves player 2 left.
- Check saved progress. Quit, reload and play again. Check where the game resumes after a loss, after a win, and after the start-from-level-1 link.
Hard Mode
- Hide behind doors. Monsters in hard mode should treat closed doors as walls. Open a door and see whether they reroute.
- Compare the modes. Play the same level in both modes and confirm the ghost behaves the same, as the README says.
As you can no doubt see, the regression surface here is potentially large. Imagine having to play through and test all of that again (and again … and again) if developers aren’t sure what the risk surface is for each change they make.
Heuristics for Reading the Code
These next heuristics are for testers who read the JavaScript. And here’s the fun part: reading the code for this strategy already turned up real findings. Each one in the table below was found by the heuristic named beside it, which is about the best argument I can make that these heuristics are worth something.
| Finding | Where | Effect | Heuristic |
| The random integer helper rounds instead of flooring | randomInt in the engine’s math helpers | The lowest and highest values come up half as often as the others. For a random direction from 0 to 7, up and up-left are half as likely. The middle monster death sound plays half the time | Check the range math on every random call |
| The random boolean helper calls a function that does not exist | randomBool in the same file | It would throw the first time anything called it. Nothing calls it yet | Check that every helper is actually reachable and runs |
| A pain sound is only rolled once the previous one has finished playing | onPlayerHurt in the main file | Audio runs on real time, so the number of random draws, and every roll after them, depends on how fast the machine runs. Seeded replays drift without a workaround | Count every draw |
| The help popup’s resume timer is never cancelled | onenterhelp in the main file | If a popup is dismissed early and another appears within 4 seconds, the first popup’s timer closes the second one early | Look for timers that outlive the state that set them |
If that first finding looks familiar, it should. The Game 3 freeze in Adventure was also a rounding mistake in random number range math. Different game, same class of bug. That’s exactly why “check the range math” made both lists.
Randomness
- Check the range math on every random call. Confirm each call can reach every intended value, equally often unless unequal odds are the point.
- Count every draw. Sound effects also draw random numbers. Any change to when sounds play changes every later roll, which matters for seeded replays.
State and Timing
- Look for timers that outlive their state. Any timeout set on entering a state should be cancelled on leaving it, or should check that it still applies.
- Separate game time from real time. The health drain and animations count frames, but the help popup counts milliseconds. Mixing the two makes behavior depend on how fast the browser runs.
- Remember the camera rule. Monsters more than about a screen away from the camera don’t move at all. A camera or zoom change therefore changes the simulation, not just the picture.
- List what survives a level change. Keys reset on every level, potions and score don’t. Check each field on the player for whether it should carry over.
Reuse and Data
- Check pooled objects reset fully. Monsters, projectiles and effects are recycled. Every field must be set again on reuse, or old values leak into new objects.
- Validate level images. Level contents come from pixel colors. A tool that checks each level for one start, a reachable exit and enough keys would catch map mistakes before players do.
The Build Itself
- Rebuild and compare before every push. Regenerate the bundles and confirm nothing changed that shouldn’t have. The dev server serves the committed bundles, not the sources.
- Remember that main is live. With deploy on every push, add a build check to CI before trusting automated dependency updates.
Sample Charters
As with Adventure, each charter follows the form: explore a target, with some resources, to discover some information. Time boxes are suggestions.
| Charter | Track | Time box |
| Explore joining with every key and a gamepad, to discover any player left without a character or controls | Human | 45 min |
| Explore two-player exits where one player dies first, to discover a level that never ends | Human | 30 min |
| Explore keys and doors across several levels, to discover a level that cannot be finished | Human | 45 min |
| Explore help popups dismissed early, to discover popups closing on their own | Human | 20 min |
| Explore hard mode around doors, to discover monsters that walk through doors or stop chasing | Both | 45 min |
| Explore the game after leaving its tab in the background, to discover damage or deaths during catch-up | Both | 30 min |
| Explore every random draw in the code, to discover biased or unreachable outcomes | Technical | 45 min |
| Explore the level images with a script, to discover missing starts, exits or keys | Technical | 60 min |
At the end of a session, write down what was covered, what was found and what’s still unexplored. The unexplored list becomes the next charter. Do you see here why, as I said in the first post, I think games can be a very fun way to showcase the necessity of a whole lot of good test thinking?
Part of why I do these projects is because people often ask “What’s a good way to start getting junior testers to think like testers?” The key, as with everything, is practice. Build the mental muscles that hone the skills and form the craft. I think games are a particularly fun way to do that.
And, Yes, the AI Part
You knew this was coming. With Adventure, I built an agent that won Game 1 and recorded the run. I wanted to do the same here, and the obvious first target was Training One, whose own help text sets the goal: shoot ghosts and find the exit.
As before, the agent is a test instrument, not a player. If it clears a level, that proves the level can be finished, and its recorded run becomes a regression test. However, this game needed a different kind of harness than Adventure did, and that turned out to be the interesting part.
The Problem: This Game Isn’t Deterministic
Adventure‘s Games 1 and 2 have no randomness, so the same key presses always produce the same game. Gauntlet Reforged rolls dice constantly: where monsters spawn, which way they wander, how ties get broken. It also runs on the display’s clock, so how many frames happen per second depends on your screen. Record a run on one machine, play it back on another, and you get a different game.
The practical upshot is that a run is repeatable only if both the random numbers and the frame timing are fixed. The harness does that in three steps, and none of them change the game’s code:
- Seed the random numbers. A script injected before the page loads replaces the browser’s random source with a seeded generator. Every random draw in the game goes through it.
- Step frames by hand. Once the level is loaded, the harness stops the engine’s own loop and advances the game one fixed frame at a time, using the engine’s manual mode.
- Drive the player directly. The harness presses and releases the same move and fire controls the keyboard uses, frame by frame, and records them.
With those three in place, a recording is just a seed plus a list of inputs, one per frame. Replaying it should give the same game every time. Notice “should.” We’ll come back to that.
How the Agent Plays
The agent itself is simple, and deliberately so. It drives the real game in a headless browser. It reads the level’s map from the running game and plans a path to the exit, tile by tile, in eight directions, without cutting corners between two walls. Every frame, it checks whether a ghost is lined up with it in one of the eight firing directions with a clear line of fire. If one is, it stops and shoots. Otherwise, it keeps walking.
That’s it. No learning, no clever tactics. For a test instrument, simple is a feature. When a simple agent fails, you can tell why.
The Result
On 21 October 2026 it cleared Training One with the warrior:
| Measure | Result |
| Ghosts shot | All 82 |
| Exit reached | Frame 2,050, about 34 seconds |
| Health at the exit | 509 of 500, after the exit bonus |
| Replays of the recording | Same exit frame every time, stepped fast or played in real time |
Here’s the run, filmed from the real page.
Notice how the agent doesn’t have a great deal of self-preservation. It takes far more damage than it needs to. For this first level, that’s not as impactful. But as the levels go on that, strategy would rapidly become untenable.
Case Study: The Replay That Wouldn’t Repeat
Remember that “should”? The replays should repeat. Yeah, well, the first replays didn’t repeat. Seeding the random numbers and stepping frames by hand weren’t enough on their own. Two runs of the same recording, with the same seed, would play out identically for a while and then quietly drift apart. Sooner or later a monster would move differently in one run than the other, and from there the two games had nothing to do with each other.
My first thought was the logical one: obviously there was some flaw in the universe and randomness no longer worked the way it should. Upon further investigation, that turned out to be false. However, the way to find the cause was the same technique I’d suggested for comparing Adventure against the original: run twice, compare the game state frame by frame, and look at the first frame where they differ. That turned up two sources of drift.
The first was a frame the engine had already scheduled. When the harness stopped the engine’s loop, one animation frame was already queued and ran anyway, at a time the harness didn’t control. The fix was to let that frame run while the level’s help popup was still up, when it couldn’t change anything, and then reset the frame counter.
The second was more interesting, and it’s the third finding in the code table above. When a player gets hurt, the game plays a pain sound, picked at random. But it only does that if the previous pain sound has finished playing:
|
1 2 |
if (!player.hurtSound || player.hurtSound.ended) // only play 1 at a time player.hurtSound = this.play(this.sounds[player.type.sex + "pain" + Game.Math.randomInt(1,2)]); |
That looks harmless. It’s even sensible: you don’t want pain sounds stacking up. (Well, maybe you do. But then you’re weird.) The key here is that audio plays in real time, not game time. On a fast run, the sound hasn’t finished, so no random number is drawn. On a slower run, it has, so one is. One extra draw shifts every random number after it, and the whole game diverges. The harness works around this by reporting every sound as finished, so the roll happens every time and audio timing can’t touch the random sequence.
Did that bug ever affect a human player? Not really. I feel fairly safe in saying not too many people would notice that a ghost’s wander direction depends on whether the warrior’s last grunt of pain finished. But it’s precisely the kind of thing that makes a game impossible to test repeatably. And notice that it was the agent work that found it. I wasn’t looking for a bug; I was trying to build a test instrument, and the instrument refused to work until the game told the truth about where its randomness came from.
That’s what led to the “count every draw” heuristic. Anything that draws a random number based on something outside the game’s own clock, whether that’s audio, network, or the user’s machine, breaks determinism. And if you can’t repeat a run, you can’t (as easily) turn a bug report into a test.
Heuristics for Judging Agent Results
- A win proves reachability, not quality. It shows the level can be finished, not that it plays well.
- A failure needs a replay. Before blaming the game, replay the recording and watch what the agent did.
- Keep the seed with the recording. A recording without its seed replays a different game.
- Treat exploits as leads. If the agent finds a shortcut through a wall or past a door, check whether a player can do the same.
- When a replay drifts, find the first frame. Comparing two runs frame by frame until their state first differs is the fastest way to find a hidden source of nondeterminism.
A Second Player Who Isn’t There
Go back to the “Players and Controllers” heuristics for a moment. Split the party. Leave at the worst time. Let one player die while the other carries on. Every one of those needs two people, and earlier I pointed out that those two people have to be sitting at the same keyboard. Which raises an awkward question for a tester working alone: who is player two?
For most of this project, the honest answer was nobody. I tested by myself, which means I mostly tested a single-player game. So I added what is essentially a test cheat: a computer-controlled second player, which I call the buddy. Add ?buddy=guardian to the URL, or call game.addBuddy() from the console mid-run, and a second character joins and plays alongside you.
This is a different thing from the Training One agent. That agent is an instrument. It plays alone, and its job is to prove a level can be finished and leave a recording behind. The buddy is a collaborator. Its job is to make a human’s exploratory session possible.
Personas, Not Just a Partner
Here’s the thing about playing with other people: they don’t all play the same way. Some cover you. Some run off and leave you. Some eat all the food. (You know who you are.) Each of those habits puts a different kind of stress on the game, so the buddy comes in personas. Each persona is a set of traits, such as how far it lets you get before it comes back, what it will shoot, what it will pick up, and how quickly it reacts.
| Persona | Plays like | What it stresses |
| Guardian | Stays close, shoots anything that comes near you, and uses a potion when you’re swarmed | Whether a helpful second player works at all |
| Food Hog | Eats every bit of food it can reach, needed or not | Health balance when one player gets none of the food |
| Wanderer | Roams the parts of the map farthest from you | The shared camera at its widest zoom |
| Glory Hog | Goes hunting on its own, steals kills, and grabs the gold | Score attribution, and the best-score pick at the end of a game |
| Rusher | Heads for the exit straight away | The rule for when a level is finished |
| Freeloader | Never fights, and keeps you between itself and the monsters | How monsters pick targets when one player is passive |
| Panicker | Uses a potion the moment a monster comes near, and picks up every potion it can | Potion range and waste |
| Rookie | Slow to react, often aims a direction off, and walks straight over treasure, poison included | Damage, deaths, and one player carrying on alone |
A persona is really a test charter that walks around on screen. “Explore the shared camera at its widest zoom” is a charter. The Wanderer is that charter, playing itself while you watch what the camera does.
One design rule mattered more than any of the traits: the buddy can only be annoying through things a real player could do. It presses the same controls, moves at the same speed, suffers the same health drain, and gets nothing extra. Otherwise I’d be testing the buddy, not the game. It also never draws from the game’s random numbers. Even the Rookie’s fumbles come from its own fixed-seed dice. So a seeded run with a buddy still replays exactly, and everything from the previous section still holds.
Traits can also be pushed to extremes from the console, which is handy when a session calls for a truly terrible teammate:
|
1 2 3 |
game.addBuddy('guardian'); // add a buddy mid-run game.addBuddy('foodhog', 'elf', { leash: 3 }); // pick the character, and override any trait game.removeBuddy(); |
Here’s a video that shows one of the buddies in action. You might be forgiven for thinking that this buddy looks to be playing like an idiot. Well, yeah, this buddy was told to be a test helper and show specifically how game map resolution changes dynamically when two players get apart from each other. Even with that goal in mind, it still knew enough to not get killed by swarms of monsters.
The Bugs That Needed Two Players
Building the buddy turned up three bugs, and none of them were in the buddy.
- Every level after the first froze two players in place. At the start of a level, the game puts every player on the start tile. Players block each other, and two players standing in exactly the same spot block every possible move. Neither could ever move again. I spotted this while reading the code to wire the buddy in, and then confirmed it with two scripted players.
- A player who had exited could block the exit. A player stays solid after going through the exit, and on Training Two the only tile leading to the exit is the one they’re left standing on. The second player can never get out. This one the buddy found the honest way: it got stuck.
- One player exiting and the other dying meant the level never ended. The game finishes a level when everyone has either exited or died, but it only checks when someone finishes exiting. If the last player standing dies instead, nothing ever checks again, and the game just sits there.
Notice what those have in common. They have nothing to do with monsters, combat or level design. All three exist only when there are two players, and all three had been in the game since the two months I wrote the initial implementation. They stayed hidden for the simplest reason there is: the game was being tested by one person!
The third one is exactly the “Leave at the worst time” heuristic from earlier. I wrote that heuristic before I had any practical way to act on it alone. That’s the gap between having a good test idea and having the means to carry it out. Testability isn’t only about whether the code can be tested. It’s also about whether the tester can actually get the game into the situation the test needs.
Along the way, a few smaller things turned up that belong on the questions list rather than the bug list. Player shots can’t hurt food, so the arcade tradition of shooting the food isn’t possible here. Food does stop shots, though, and so does the other player, which means your teammate makes a fine shield and a terrible gunner. And reading the code, dead players stay solid too, so a fallen teammate can block a corridor.
Heuristics for Testing with Personas
- Build the tester you’re missing. If a test needs a second person, a slow reactor or someone who does annoying things, make one.
- Annoying, but legal. A persona should only ever use moves a real player has. A persona with special powers mostly tests itself.
- Name what the persona stresses. If you can’t say what a persona is for, it isn’t a test tool yet. It’s a toy.
- Keep the dice separate. Anything random in a persona should come from its own seeded source, so a run with a persona replays like any other.
- Expect the plumbing to break first. The first thing a new kind of player tests is everything that quietly assumed there would only ever be one.
What I really hope all this shows is that the appropriate use of automation, of which AI agents are certainly one representation, is not just a sufficient skill of testers. It’s a necessary skill. None of it replaces any of the exploratory elements, but it absolutely helps turn those exploratory elements into repeated test helpers.
Answer Key: Five Changes for Testability
These are the five changes I came up with for the exercise in the Testability section. If you found others, I would genuinely like to hear about them.
| Layer | What it checks | Track |
| Build check in CI | The bundles build, and the committed bundles match a fresh build | Technical |
| Unit tests for the engine helpers | Math, overlap and random helpers in the engine files | Technical |
| Scripted browser tests | A seeded game stepped frame by frame with scripted input, checking positions, health and state | Technical |
| Level data checks | Every level has one start, a reachable exit, and a key for every locked door | Technical |
| Exploratory sessions | Time-boxed play against a charter | Human |
I should note that the buddy from the previous section doesn’t replace that last layer, and it isn’t meant to. What it does is widen it. An exploratory session that used to need two people can now be run by one, with a persona playing the part of whichever kind of teammate the charter calls for.
Notice also that this kind of “buddy” works well in a cooperative game. It a competitive game, with Player-vs-Player aspects, it would be incredibly weak.
What next?
Okay, so far in this series, we’ve had an adventure. Now we ran a gauntlet. Next I’m going to ask you to prepare yourself for something a little more … improbable.