Category Archives: theorycrafting

Some Notes on Player Complicity

A lot of the earlier articles on Bumbershoot Software were written “from my old notebooks”—old notes or forum posts or conversations that I’d kept around and then worked into full articles. That’s fine for retrocoding stuff, where the state of the art moves fairly slowly, but it’s not useful for fields that are still rapidly innovating. My old notebooks still have a lot of material from my 2004-2014 span being involved with the interactive fiction community, but a lot of the figures there are now reasonably well known names in academia and industry, so much of my notes on what we’d now put under the umbrella of “interactive narrative” are extremely outdated. I do sporadically check in on Nick Montfort’s and Emily Short’s blogs to sample the academic and avant-garde commercial viewpoints in the space, but even there I’ve drifted far enough away from both the state of the art and the goals of the craft that there isn’t much I can talk about or grapple with.

However, Emily Short has a new post up on “Choice Poetics” that struck a chord. Back when I’d write nine or ten thousand words each year about the annual Interactive Fiction Competition, I ended up refining some principles and approaches to the entries in the competition that ended up serving me pretty well in subsequent years when approaching computer games more generally. The papers linked in that article—particularly the one formally analyzing Papers, Please—have forced me to re-evaluate some of those old conclusions.

So, let’s talk about complicity in video games. I’m using this term in both a narrow and a broader sense. In the narrow sense, it’s talking about things a game can do to make a player feel bad about atrocities they commit or have their character commit within a game. More broadly, I want to move this beyond the crimes that the word “complicity” suggests and consider situations where the player feels responsible for their actions within or to a game world.

I first attempted to formalize this in 2010 when reviewing Jason McIntosh’s The Warbler’s Nest, because it was brief and extremely pointed and thus crystallized for me what made me feel like it was laying the responsibility for the events of the game directly at my feet. To wit, if a player is to feel responsible for an atrocity:

  1. The atrocity must be optional. Otherwise the player is no more responsible for these events than a reader is made responsible for the events in a story by the act of turning a page.
  2. The atrocity must be the easy path. Otherwise it’s just Bonus Cruelty you have to work harder for.
  3. The player must know the stakes. Otherwise their actions do not have the necessary import. Lock a player in a room with two unlabeled buttons, and make it clear that the game will do nothing else until one of the two buttons is pressed, and the player will feel no responsibility for the result. Indeed, this is only fair; were such a situation to obtain in real life, the responsibility for the consequences of this action would be firmly on the head of whoever rigged up the buttons, or locked the button-pusher in the room.

That was almost a decade ago. By now, I don’t think any of these hold up as well as they did when I originally devised them. Here are the main problems or complications that, I think, have emerged with it:

  • The player’s interaction mode may be too far removed for the analysis to apply. If game offers a predefined good path and evil path, the default assumption for many players is that they are expected to be completionist. The atrocities on the “evil path” may be optional, but if the player seeks to see the whole of the game’s content—particularly if certain maps, challenges, characters, or other such content is gated behind progress within it—they cease being optional when in pursuit of that goal. More simply, a player who has decided to rain meteors and dinosaur attacks upon their SimCity metropolis will feel no more remorse about this than they would for knocking over a tower of Jenga sticks. A player interacting with a game as an artifact to experiment with or as a library to exhaust has already made a decision that removes them from the analysis.
  • People feel responsible for their actions in real life even if they didn’t know the stakes. Blind or actively deceptive choices still fail, but there’s a lot more leeway here than I had originally formulated. Allowing the player character to feeding a starving child a peanut butter sandwich and then rewarding them by having child die in front of them from anaphylactic shock would be a rather mean thing to do, but a player might feel some responsibility for the action, since they acted without some obvious information (the child’s peanut allergy) that they in principle “should” have known. On the other hand, one could imagine a hospital/surgery simulation where the epilogue reveals that the player character was not a surgeon at all, but a deluded lunatic who was just cutting people up in a barn. In such a game the player would be unlikely to feel even fleeting remorse for the actions presented at the time as saving lives.
  • If an atrocity is difficult enough to perform, the analysis breaks down completely. This is hard to explain without actually working through examples, so I’ll defer that to my worked examples before.

Below the fold, I’ll work through a number of games and show how the original and the modified analyses interact with it. That also means I will, by necessity, be spoiling some of the narrative mechanics in them, so I will list the games here as the spoiler warning. Proceed at your own risk if you have not played Utlima IV, Bioshock, The Warbler’s Nest, Spec Ops: The Line, Fallen London, Papers Please, and Undertale. I’ll try to be vague about exact details that are not commonly discussed, but it’s still going to require going further than I’d like without spoiler warnings.

Continue reading

Advertisements

Mandala: Premature Optimization

Before moving on to tackle the implementation of the next part of Mandala Checkers, I did a quick review of my initialization and display code to comment it more thoroughly and to see where I could optimize or generalize it. I found a couple of places where I could make my support routines more powerful and this ended up shrinking my program-so-far from 492 bytes to 465—over 5%.

The optimizations I found are sound—that is, they don’t actually break anything—but they also look incredibly sleazy. With this post I’ll look a little bit at those optimizations and show how they work. It turns out they’re valuable in general and not just as one-off sneaky tricks.

Trick 1: Beyond Call Elimination

Some optimizations don’t depend on any of the context or instructions around them. These are called peephole optimizations—they are essentially a search-and-replace transform for sequences of logical but inefficient instructions that can be replaced with smaller, more efficient, and possibly more obscure instructions.

There’s not always more obscure, though. Here are two rules involving jumps and procedure calls that are extremely logical:

  • A jump instruction to the very next instruction may simply be deleted.
  • A procedure call instruction followed immediately by a procedure return instruction may be replaced with a simple jump instruction.

When we put these rules together, we get a less obviously logical rule:

  • When two procedures are defined back-to-back, and the last thing the first procedure does is call the second, both the call to the second routine and the first routine’s return instruction may be deleted.

This third rule follows immediately from applying the second rule and then the first rule, and it suggests that if we organize our program’s functions appropriately, we can save some space by allowing some function calls to “fall through” into other functions. It turns out that my initialize-board code can reasonably fall through into the draw-board-on-screen code. That looks fine now, but it will start looking weird later when I want to redraw the board after a player makes a move—it will sort of look like I’m calling into the middle of the initialize-and-draw-board subroutine.

It gets weirder when the first procedure calls the second procedure multiple times. I don’t have an example of this yet in Mandala, but a whole lot of my test code needs to print out 16-bit hex values. I do this with three routines; hexout_16 calls hexout_8 twice, and hexout_8 calls hexout_4 twice. In each case, the final call can be eliminated, and the resulting code is one where hexout_16 is doing procedure calls into the middle of itself:

hexout_16:
        push    hl
        ld      a, h
        call    hexout_8
        pop     hl
        ld      a, l
hexout_8:
        push    af
        rra
        rra
        rra
        rra
        call    hexout_4
        pop     af
hexout_4:
        and     15
        add     28
        rst     $10
        ret

If you actually step through it, this code is doing some extremely strange things with its stack, especially if it’s in a context where hexout_16 is the only function that’s ever actually externally called. But if you break it up mentally into three functions that fall into each other, the design clears up right away.

Trick 2: Doing Trick 1 Backwards

The main thing I realized from my review of Mandala so far was that I was doing a lot of duplicative or unnecessary work when printing out status or prompt strings. My only routine for printing anything out was a simple routine named print_at, which took a pointer to a two-byte screen location code followed by a string of characters. It would move the cursor to that location code, then print out characters until it reached a value of 255. (Normally one stops at a character code of 0, but the ZX81’s wacky non-standard character set uses character zero for space.) Making this call cost six bytes, and so I had several places where I would print out a bunch of unnecessary newlines or spaces to spare myself the call, and there were other places where I would call it multiple times in a row to write at various parts of the screen.

This is inconvenient. I’d be far better off generalizing the function so that if it saw, say, a 254, that would make it treat the next two bytes as a new location code. The routine started out looking something like this:

  1. Read two bytes from the string and move the cursor accordingly.
  2. Advance the string pointer two bytes to skip over the location code.
  3. Read a byte from the string.
  4. If the byte is 255, return from the function.
  5. If the byte is not 254, go to step 8.
  6. Advance the string pointer one byte to skip past the 254.
  7. Go to step 1.
  8. Print out the character read.
  9. Advance the string pointer one byte.
  10. Go to step 3.

There are a number of things we can do with this. The most obvious is to notice that if I ever need to print something out without relocating the cursor first, I can set a label print in the middle of the function, letting me effectively say “start at step 3.” This produces a result identical to what would happen if print_at moved the cursor and then called print… as long as we don’t look too closely at the loopback in step 7.

The second thing we can do is notice that there’s no reason we ever have to start at the first step stated. Instead of jumping around with 254 case, we can change step 5 to “If the byte is 254, go to step 0“, remove step 6 entirely, and move step 5 to before step 1.

That’s cute, and sound, and reliable, and it preserves the exact semantics of the previous function, but it’s also ugly. Is there a better way?

There is. The whole reason we need to have these extra jumps is because we need to increment the string pointer and then jump somewhere, and our tests only let us jump or return as the result of a test. But the real problem here is that we’re advancing the string pointer too late, all the way down in step 9! If we move that string pointer advance to just after step 3, we can make the test simply be “If the byte is 254, go to step 1” and our work is entirely done. It’s both cleaner and smaller.

Adding functionality does still cost a bit of space, though: interpreting 254 specially added four bytes to my print_at routine. However, setting up the pointer and making the call costs six bytes, so being able to wipe out even one call as a result results in a space savings. I was able to wipe out much more than one call.

Harm Reduction

But wait, modern programmers might observe. This involves some pretty aggressive use of crisscrossing GOTO statements. GOTO statements are considered harmful, they might say. Aren’t I corrupting the youth of today, they might ask.

To this I answer, no, no I am not, and I am not for a variety of reasons. The first is that all of these jumps are within a single screenful of code, and this was considered acceptable by the more practical developers of that era. The second is that the structure programming wars of the “GOTO considered harmful” era also eschewed things we take for granted as part of well-structured code today, such as mid-function return statements, or break and continue statements within a loop.

And finally, I’m in the clear here because the actual control flow here doesn’t even require break or continue:

  • Repeat:
    • Consume two characters from the string
    • Move the cursor as specified by those two characters
    • Repeat:
      • Consume one character from the string
      • If it is not 254 or 255, print it

      While character consumed was not 254 or 255

    While character consumed was not 255

That’s rigid enough to satisfy even the most rigorously excessive of structural programming requirements. It’s not quite what I actually do, directly—the actual machine code uses the equivalents of break and mid-function return, but the Z80 instruction actually has “return immediately if condition is true” as a specialized one-byte instruction. There’s really no reason to not take advantage of it if it’s there.

Trick 3: Dual-purpose Data

With the new printing procedure in hand, I went through the code and started coalescing large collections of print statements into single calls. However, there was still one nasty knot of calls left. These calls revolved around printing out the current score. It needed to print out the labels for each score, and then it needed to compute the character codes based on the current score and print those out individually.

The trick here is that this is not remotely necessary. Since the scores in this game are only ever one digit long, we can just embed the scores directly in the score string, and look at the character we’d print to determine what the score was. That moves the interpret-score-as-character code out of the board display and into the has-a-player-won logic, but it lets us get rid of a bunch of unnecessary math and lets us coalesce several more print statements.

This is also an instance of a somewhat more general technique where you use your display data directly as model data. This is a bad idea in modern GUI applications, where the view can change unpredictably and you want a stable model to inform any redraws, but for fixed character-cell displays like these systems, there’s no reason to not use the video matrix as a repository in memory of what is currently on the screen.

First-class Functions at the Bare Metal

I’ve written in the past about how to use modern design methodologies on much older systems and languages, and I’ve also written a bit in passing on pulling design techniques from the past into the modern era. In doing that, I’ve missed a major part of language design and implementation, one that’s difficult to call “retro” or “modern” because it’s been a continuous background fixture through basically the entire history of programming languages of any kind.

This article will talk about the implementation of functions as values, which goes all the way back to LISP in the 1960s, and if you allow the lambda calculus, it predates electronic computers entirely.

For the purposes of this article, I’m going to assume at least a basic knowledge of some of the surrounding concepts. Many of these I’ve already covered in the past, so here’s some background reading if you’re just joining us:

  • Object-Oriented Programming at the Bare Metal: My first article here on topics like this. This covers the implementation of structures and records at the machine level and a few other concepts that I’ll echo in this article.
  • Call Stacks and their Implementations: This discusses the notions of stack frames and frame pointers as part of the implementation of local variables in ordinary functions. Since this article is about fancier uses of functions, this article describes the baseline from which it grows.
  • Variations on the 68000 ABI: This is less important than the other two; it’s a worked example from the previous article, implementing the basics of a stack frame on that chip. It also gives some examples of places where that discipline would fall short. This article will close that gap.

In addition, some stuff that it’s helpful to know but that I will try to explain as I go along:

  • Heaps. If you’re unfamiliar with a “heap”, just think of it as a magical void that you can summon chunks of memory out of and have to banish chunks back to once you’re done with them. In a garbage-collected heap chunks of memory magically return from whence they came once they are no longer relevant to anything.
  • The basics of higher-order functions and why they matter. This is an easier lift these days, as many of the advances of the functional programming tradition have become mainstream in the past five years in ways they really hadn’t in the previous fifteen.
  • Pascal, JavaScript, and the various 21st century iterations of C++. All of these languages, in their own way, have tried to present versions of these capabilities to the end user, and we’ll be exploring how close they got both the metal and to the mathematical ideals.
  • The C language and IA32 assembly language. I’ll be using these to express the simpler, machine-level operations, much like I did in the OO-at-the-metal article. Anything really crucial that shows up here will also be explained in words.

That’s a lot of stuff, but there’s a lot going on here. We’re in theoretical waters that for many, many years were treated as both the ivoriest of ivory tower inapplicable silliness and the deepest of deep magic. Below the fold, I’ll be showing you the wires, the smoke, and the mirrors that make the magic work.

Continue reading

Call Stacks and their Implementations

Here in 2018, any programming language you are likely to learn will break its units of computation into functions and allow declaration of local variables unique to every execution of that function. Every time a function is called, a new version of its variables springs into existence, separate from all others.

It is worth pausing for a moment to consider how unusual this arrangement really is. For the vast bulk of the programs I have written or examined on this blog over the years, nothing like this has been provided or exploited. Instead, we have relied on two simpler strategies:

  • Just use global variables everywhere. For the programs written in old dialects of BASIC, this was the only option. Various techniques were developed to manage the complexity that this produces, and for small assembly language programs, those techniques work just as well.
  • Give each function its own workspace. This is what I’ve been using for my larger assembly language projects. From the point of view of the final binary, this looks identical to the global-variables-everywhere case, but in this technique, one arranges the source code so that the assembler itself enforces that functions can’t accidentally use each other’s storage. The data is still carved out of the data segment as usual, but private names are given to those chunks so that they can’t be referenced by other code. This means that when you are writing a new function, you can not accidentally trash data by accidentally reusing a variable name.

This function-workspace technique is actually quite powerful. Very early high-level langauges, including the first few versions of FORTRAN, had this as their only implementation of variables, and even today languages like C support it with the static keyword. (Even less C-like languages just as Java still use static to mean something similar—they declare global storage within a controlled naming convention even there.) Writing assembly language code with nothing but static variables is quite easy, and it produces very fast code on most chips as well. It does, however, still have a number of serious flaws:

  • Recursion doesn’t work. The big advantage of local variables is that when you call yourself, things keep working, and you can use the chain of variables within a set of procedure calls to represent a history of values. With a single workspace, this capability is lost unless you manage a stack or worklist on your own. (We actually did do that for our floodfill routines.)
  • It uses more RAM than it needs to. If two functions never call each other, even indirectly, there’s no reason they can’t use the same RAM as scratch space. Our per-function allocation does make the resulting code a bit easier to debug (since we can study a single memory location and as a result restrict our focus to a single function), but for many of the 8-bit systems we’re considering RAM is desperately scarce.

One way around the recursion problem is to maintain a stack of values that represent the computation as it evolves. The traditional approaches to the problem of local variables exploit the support that microprocessors already have for hardware stacks to automate and simplify this process. Let’s look at those next.

The Hardware Stack

Most chips have some notion of a function call built directly into their instruction sets. Since return addresses need to be stored somewhere, these chips designate part of their address space as a call stack. Call instructions save the return address to the stack, and return instructions pop those values off the stack and jump to them.

Furthermore, every chip we’ve looked at has some notion of an interrupt mechanism. Since interrupts can happen at basically any time, interrupt handlers need to be able leave the CPU’s computation state exactly as they found it, once they return. Interrupt handlers furthermore can at least in principle nest just like procedure calls do, so they will need some kind of stack to store not just return addresses but the state of any registers or flags that they alter during processing.

Once you allow that, you also might as well allow arbitrary data to go on and off the stack. The simplest notion here is to “spill” data we don’t need onto the stack so that we can use the registers temporarily for something else, but since the stack is stored in the same memory as everything else, we can generally index directly into the stack, treating it like a big array or a strange structure of some kind.

Activation Records and Stack Frames

The key insight that lets local variables work is that this “strange structure” can be a notional structure that includes every local variable the function uses. We take all the information we need for some particular instance of a function call and put it on the stack. We then reference all our local variables by treating the stack pointer as a sort of pointer to this structure. (Such structures, corresponding as they do to a single specific call (or “activation”) of a specific function, are traditionally called activation records.)

If we need to use the stack inside the function, though, things get a little more complicated. The offset from the stack pointer to our local variables might change mid-function. That’s not fatal, but it is inconvenient, becuase code that refers to variables will name them inconsistently depending on what other instructions around them did. We can make the code that accesses our variables more consistent by caching the value of the stack pointer right after we allocate space for the local variables. This cached value (variously known as a base pointer or frame pointer) gives us the consistency we’d like.

The collection of all the data a function manages on the stack as part of its execution—its return address, its local variables, any temporary data it had to spill from registers along the way, and possibly the arguments it was called with—are collectively called its stack frame. A language or program for which stack frames are sufficient for all the calls it can make is said to adhere to stack discipline.

A Caveat

The key requirement of stack discipline is that local variables will never be used after the function invocation they are part of returns. Every C programmer has at some point caused vast and mysterious data corruption by absent-mindedly having a function return the address of one of its own local variables. C insists on stack discipline for all of its local variables, and C program break horribly if this is violated. Rust also insists on this in many places, but Rust’s “borrow checker” generalizes it a bit and will enforce the safety of it at compile time. (Both also have a non-stack region called the heap for managing data that intrinsically cannot fit into stack discipline—that is a story for another time. For now, at least, recognize that heap values in these languages are only ever accessed through pointer or reference variables that themselves subject to the stack discipline.) However, languages like JavaScript, Lisp, and Haskell allow a function to return a function that uses values from its own activation record. In these languages, the activation record (or parts of it, at least) must persist past a function call’s return to serve as storage for the function that was returned. The approaches to dealing with this generalization are a bit beyond the scope of this post, but just be aware that stack frames are not everything.

Implementations of Stack Frames

Because stack-discipline languages have been so central to computing over the years, chips have evolved alongside them to help support their features cleanly and efficiently. Despite that, there is still a lot of variation in how they are built and used. Let’s have a look at the chips that we’ve worked with here on Bumbershoot.

Continue reading

Variations on the 68000 ABI

Moving from the 8-bit computers to a 16-bit console system is a bit of cold water in the face—with no on-board operating system or BIOS-like software, I end up needing to write a large amount of support code that I’ll generally want to reuse. This is a bit of a problem, because assembly language provides no real protection against having various bits of your code stomp on other bits of your code. You need to keep everyone’s hands out of everyone else’s pockets and still let them efficiently hand things back and forth as needed.

For a lot of these older systems, or smaller programs, I’ve simply relied on documenting a contract for each function that’s exported: where values come in, where they come out, and what registers or memory are trashed as part of the process. The documentation of the C64 system calls in the Programmer’s Reference Guide and of DOS and BIOS system calls in the Interrupt List were both organized in this manner.

This doesn’t really scale.

It doesn’t scale both for trivial reasons—if you’re using a compiled language, it can’t read the comments you made—and for non-trivial reasons—there’s a lot of finicky stuff to keep track of and the cognitive load of adding to a system will just get larger and larger over time.

The solution for this is to adhere to some consistent contract that holds for all functions, or holds closely enough that you can pretend it does. We encountered this before in DOS when we were speeding up an old BASIC program. That provides a consistent mechanism for passing arguments, receiving return values, and dictating which registers must be left unchanged after any given routine returns. (While you might not trash all the registers that aren’t guaranteed to be preserved, anyone who calls you needs to pretend you did, so that you’re free to trash them six months later as part of a bug fix.)

But that really only buys you coexistence. A protocol like this will carry along with it assumptions about how your programs should be structured, and suggest ways to organize the internals of your routines as well. The disciplines that compilers use to keep everything interoperable should let our assembly language code scale as well, or give us some additional tricks we normally would not permit ourselves.

In this post, I’ll outline the technique used by Motorola 68000 compilers, adapt it to the Genesis, discuss what that means for program organization, and compare this protocol to ones used by other chips to show some more advanced techniques we can steal.

Continue reading

8-Bit Interlude: Test Cases

I’ve decided on what I want to do for my Game Boy program—an implementation of Conway’s Game of Life—and I’ve been working on implementing the core routines and getting them working before I get into the display aspects of it.

This of course raises an interesting question: how does one go about testing any of this stuff these days? It’s not like there are unit-testing frameworks you can plug a Commodore 64 into. Here are the techniques I’ve been using:

  1. For anything complicated, prototype in an HLL. That could be BASIC on the target system, but in most cases it’s going to be C or Python on a modern system. Note that this means you can get the prototype correct in an environment where you can use any testing tools you might care about.
  2. Test correctness against the HLL prototype. This looks a lot like traditional unit-testing; work out a set of inputs and the correct answers, and then run both your 8-bit assembly language routine and the C or Python prototype and make sure the answers match up.
  3. Unless you have to, don’t restrict yourself to the target platform. Math is math, and instructions are instructions. Any routine that isn’t directly commanding specialized hardware can run on any system that uses a compatible chip. There’s no reason to struggle with making logging or test cases run on an Atari 2600 or a NES when you can load identical code into a Commodore 64 and have it log results to the screen. (Heck, given VICE’s support for virtual disk drives, you can have it log to a file that you can then process later with modern tools.) Precise memory locations might change when you do this, because of the different memory maps of different computers, but if you have a symbolic assembler (spoiler alert: you do) this is handled by your toolchain already.

This isn’t really a new practice for me; long-time readers saw it in action when I was implementing a xorshift PRNG for the Z80 and 6502 chips, and I needed to reprise that effort for the LR35902. The resulting code is very similar to the Z80 code, but the Z80 implementation relied on several instructions the LR35902 didn’t have and also relied on the program code lying in RAM. The port thus renders the code “ROMmable” and replaces the 16-bit memory loads and stores with series of high-memory 8-bit operations. As a result of that, the way registers were allocated and their values juggled needed to change slightly as well. The end result costs 82 bytes of ROM and 4 bytes of HRAM. (In comparison, the Z80 implementation it was based on cost 65 bytes of RAM.)

That PRNG post ended with a ZX81 program that would initialize the seed to a specific value and then dump 64 consecutive results from that seed. To test my Game Boy port, I merged these routines into the Hello World program from the last post and then did the matching initialization and calls.

I suppose I could have then gone to the effort of actually printing out the values on the screen so I could compare them to the ZX81 results. But as I also mentioned in the last post, the BGB emulator has a very good debugger, so instead I simply dumped 128 bytes of results into the “Work RAM” section just before turning on the video display again, and once the display appeared, I turned on the debugger and got a table to compare against:

gb_debug

I could probably have made my life easier if I’d stored them big-endian, truth be told, but it was nevertheless two minutes’ work to confirm the results all matched exactly.

If I needed to do this check regularly, I would include the expected answer set inside the ROM and have it do the comparison for me, but one other nice thing about “mathematical” functions like these is that regressions are less of a concern.

Perfect Play Across Genres

(This is another “from the archives” post, republished from an earlier, smaller distribution. I’d built this out of a series of discussions I’d had on IFmud and similar places with people who mostly played in these genres.

I haven’t updated the text of it much, and of course the sample size is restricted to the folks I talked to. The authoritative tone of much of this article should perhaps be taken with a grain of salt. —McM)

In the excellent and insanely detailed article Shmups 101: A Beginner’s Guide to 2D Shooters, we find the following uncontroversial claim:

Theoretical Perfection: Perhaps the single most important quality for any respectable shmup to possess: it should be technically possible for a player to make a “perfect” run through the game, without getting hit even once. Put another way, there should never be spots where eating damage is 100 percent unavoidable—no matter the situation, your raw skills should always be sufficient to get you through if you’re good enough. Of course, only a select few gamers actually are that good, but this ideal MUST be legitimately attainable: failing to tie up this crucial loose end during development is guaranteed to hamstring any shooter, no matter its strengths in other areas.

It seems like this has a lot of resonance in other genres, too, and takes different forms:

Perfect Play Should Be Possible

  • Platformers: Take this condition unmodified. A sufficiently good player should theoretically be able to finish without taking damage. (Health bars are native enough to platformers that this may be better phrased as “without dying”—nonlethal avoidable damage would be more likely to be acceptable in a platformer if the concept exists.)
  • Fighting Games: Deterministic damage model. Randomized mechanics are anathema to serious fighting-game players, who hope to see their competitions are purely skill-based, not due in any way to die rolls. This is the predominant reason the Smash Brothers games weren’t taken seriously at fighting game tournments for many years.
  • Interactive Fiction/Adventure Games: Much like the Fighting Game version, no random elements capable of deciding the game. Random elements may be acceptable, but there must exist some strategy that, properly applied, will win the game 100% of the time. This requirement is automatically met by puzzlebox games, and in a very real sense, in any game that has nothing isomorphic to randomized combat. However, even with randomized combat, it’s still possible to meet this requirement.

I would now want to add some variants to this, because when these are violated some group of even hardcore players gets turned off.

A Priori Perfect Play: “Perfect Information”

This is a known consideration, but does not hold nearly as strongly. The idea here is that the game provides perfect information to the player, so that perception, reflex, and execution are the only things tested. Violating this requirement makes the game—at least on a first playthrough—more of a mind game. These usually end up being specific subgenres.

  • Platformers: If the player is not in danger, they can see a way to safely proceed. No invisible deathtraps, no leaps of faith. Invisible hazards may be acceptable if it is possible to guarantee their nonlethality vais some strategy and there is some cue that the strategy should be applied. (Violating this is an entire subgenre: “masocore”, of which Kaizo Mario World and I Wanna Be The Guy are the most famous, and the Karoshi games were the best.)
  • Shmups: Memorization should be effectively unnecessary. This is arguably too strong—familiarity will help in any game with scripted components, which is all of the shmups of note. However, this is pretty close to the platformer requirement. If a path branches, both branches should be ultimately viable, or it should be possible to switch branches after it becomes clear that one is not viable after all. (Violating this is the “memorizer” subgenre, of which R-Type is the best known example.)
  • Fighting Games: This would require that every move have a unique prefix, or at least that all moves with a unique prefix animation should all have identical counters. There isn’t a name for fighters that do this—I’m going to call them “reflex fighters” because they rely entirely on the reflexes and perception of the players. Unlike the other examples here, violating this requirement makes you mainstream&mash;modern fighters deliberately have moves with different counters have the same prefix as a way to simulate feinting, or, more directly, to add a mechanism by which two competitors can psych one another out. While fighting game enthusiasts loudly proclaim their fealty to “pure skill tests”, reflex fighters are considered a bridge too far—they move parts of the game they like out of the design space.
  • Interactive Fiction/Adventure Games: Winning the game should not require exploiting knowledge from “past lives”. (Violating this is an entire subgenre: “Accretive PC” games, of which Varicella may be the most famous example.)

Basic Toolkit Perfect Play

Perfect play should be possible using only the most “basic” of the game’s mechanics. This is not always considered a feature, because it by definition is making some skill tests totally optional, and sometimes that means removing them entirely.

  • Fighting Games: No special move should be only counterable by another special move; no matter what strategy your opponent takes, it ought to be theoretically possible for a sufficiently skilled player to win using only the basic toolset of basic attacks, defenses, dodges, and throws. (This is more or less true for all fighting games, under equal circumstances. Special abilities may be better at it, and if a player has been trapped by his opponent specials may be his only way to break it, but if you’ve gotten into your opponent’s head, there’s no need for anything fancy to win.)
  • Platformers: This is generally automatic. Most platformers—and in particular most challenge platformers—only have basic mechanics, and they are always available. When this is not the case, powerup-free runs should always be possible.
  • Shmups: In addition to “dying should be optional”—the basic perfect play criterion—bombing and powerups should also be optional. Guaranteeing this is usually how you guarantee the perfect play condition.
  • RPGs: No system mastery traps; any “sensible” build should be capable of beating the game. This is the genre where it’s most likely to not be considered a feature, because by definition it means that optimization doesn’t pay off as much as it could. Even so, games that encourage specialization (like Alpha Protocol or Deus Ex) carry with them an implicit promise that any specialization will eventually see you through, ideally with that specialization. Breaking this will still be considered OK as long as the core Perfect Play rule still applies.
  • Interactive Fiction/Adventure Game: The game should be beatable using only standard verbs. More or less automatically achieved by mouse-driven graphical adventures unless they do something amazingly gimmicky—not usually as valued in IF, which prefers the weaker requirement “properly clue all required nonstandard verbs”. What qualifies as a “standard verb” is a community consensus, which doesn’t really help novices. Still, if a standard verb can do something, the standard verb should at least be a synonym.

Perfect Play From Any Point

Regardless of the game’s situation, a sufficiently good player can provide a “perfect tail” from any point.

  • Shmups: Intrinsically impossible, since being one frame away from death will usually be too late. Weaker versions of this may apply.
  • Platformers: Intrinsically impossible for the same reason as for shmups; restricting to “possible starting from any point where the PC is not in immediate danger” is usually automatic if perfect play is possible at all.
  • Interactive Fiction/Adventure Games: This is traditionally measured on the Zarfian Cruelty Scale; any game with a rating of “Polite” or lower meets this criterion if it also meets the perfect play criterion. Community consensus is that this is very important, but not strictly mandatory.
  • Fighting Games: This is almost automatic. The basic concept here is that comebacks should always be possible, and since damage is deterministic and there is always a psychological component of some kind, if a player is not in the middle of being defeated by a trap or combo, a comeback will be possible.