Roguelike Intelligence - Displaced Actions
Jump to navigation
Jump to search
Roguelike Intelligence, part 2B: Displaced Actions. One technique that is fairly important, and requires support in the basic architecture of your system, is the use of Displaced Actions. To recap the terminology I'm using in these articles: *Behaviors* are simply actions that the monster can take. They have titles like "attack-player" or "move-toward-player" or whatever, and a given behavior can be implemented in different ways (by use of different *actions*) for different monsters. *Tests* are boolean tests for conditions like "player-is-too-powerful" and "can-move-away-from-player" and so on. Tests are much like behaviors in that they can be implemented in different ways for different monsters. For example, "can-move-toward-player" may mean something different for a monster smart enough to do pathfinding. Different specific code for tests and behaviors is a simple way to represent different monster capabilities or monster intelligence. *stateless AIs* are basically nested if statements composed of tests and behaviors, in varying degrees of complexity. A stateless AI always picks a behavior to execute, in any conditions. Stateless AIs implement a pattern of different behaviors for a given type of monster; A given stateless AI will call on several to several-dozen tests and behaviors, and may accept as arguments any version of those tests and behaviors; thus the same stateless AI can produce many hundreds or thousands of different kinds of behavior depending on the exact versions of the tests and behaviors it calls on. *state machine AIs* are transition tables a monster can follow, which switch it between states depending on inputs. Each state contains a stateless AI that selects actions while the monster is in that state, and a set of parameters for the "observe" routine indicating the monster's sensory abilities and general alertness. The inputs to the state machine AI's are gathered by a general routine called "observe", which checks some central communications channel to discover things observed by that monster. I've advocated storing sense impressions in the dungeon itself, or in some central message queue, and having only one "observe" routine shared for all purposes by all monsters. Okay. recap's over, now I'm going to explain what I mean by "Displaced actions." Consider a Behavior called "switch-places-with-other-monster." When a monster executes this behavior, it is supposed to mean s/he trades places with an adjacent monster; typically this action will appear qualified by Tests such as "more-wounded-than-me" and "same-species-as-me" and "blocks-route-to-player," enabling a teamwork situation where wounded monsters facing the player are replaced in tag-team style by fresher reinforcements from the rear. Now, the monster that executes this action has performed something relatively simple: they've moved one square (presumably one square closer to the player). But this action also involved the monster with which places were switched, and as far as that monster is concerned this is a "Displaced action:" That is, an action chosen for it and executed on it by some other monster. The effects of a Displaced action on a monster are basically that the monster executes a Behavior just as though that monster's AI had chosen that behavior itself; it uses up time, moves the monster, may generate a message, etc. Now this is a relatively simple idea, but it's powerful. It allows teamwork amongst your monsters in ways that are very difficult or impossible to achieve otherwise, it allows shoving and maneuvering as well as hitting for damage, and generally adds a lot to the game. But, carried on to a slightly different conclusion, it allows you to save yourself a lot of coding work by building special behaviors into special objects or monsters. For example, remember Adom's Altars? If the player catches a monster stepping onto an altar he can sacrifice it. Presumably a monster can do the same thing to the player, but who has time to build special behavior into a hundred different state machines and extend the "observe" routine to cover the special case? It's far far better, I think, for the altar itself to be a monster, with a special displaced action that it's ready to use on any co-aligned, adjacent monster if the player-character should be so foolish as to step onto the altar. Thus, the code for sacrificing the player-character can be built into the altar itself. Now the altar AI is very simple: ALTAR_AI IF player-standing-on-same-square AND adjacent-coaligned-monster-can-sacrifice-player adjacent-coaligned-monster-sacrifices-player ELSE stand-still From the player's point of view this looks like every monster in the dungeon suddenly got smart enough to know how to sacrifice him if he steps onto their altar. But it's absolutely unnecessary to give any mention of this behavior in the monster AI's themselves, except for the Altar AI. Many other bits of your dungeon can work the same way; special levers that operate gates, special gateways that conjure demons, doorways that *somebody* has to lock before the orc guards flee the room, thrones that want one goblin king to sit on them, and all kinds of other things can work with generic monsters, just by having those objects have AI's that get other monsters to operate themselves using Displaced Actions. In order for a monster to be eligible to commit a displaced action, it has to have enough time. This is trickier than it seems. In most rogulikes, the monsters choose actions and act, one at a time. This may mean that, by the time the Altar, or doorway, or gate or throne or whatever acts, all the monsters around it have already committed their time for the next round and it has no monster it can make into its operator. So things that use Displaced Actions, in a turn-based system, need to have a pretty high "initiative" score so that they pick their operator before the operator decides to do something else that round. In roguelikes that use time or pulses, then you're going to have the situation where it becomes the Altar's turn and there are no monsters around it whose turn it also is. This means it picks its operator, but the operator isn't technically able to do anything until its turn comes around. There are a couple of ways to handle this; my own favorite is that when the altar/door/gate/whatever chooses "stand-still" because it can't find an eligible operator, its "stand-still" behavior waits until the instant before its chosen operator's turn comes around. (alternatively, this may be a different behavior named wait-upto- monster or similar: wait-on-monster waits until just *after* the monster's turn). This requires a function that can query another monster to see when its next turn is, but that's fairly easy. Then, when it wakes up an instant before its chosen operator, it uses its Displaced Action, which also uses up the operator's next turn -- and whatever the "instant" is (one 'pulse' or 'phase' for you guys who break up the round into even numbers of 'pulses' and 'phases' or maybe a tenth of a second for continuous-time games where dungeon time and next-turn are float values), that can be added to the time needed to complete the Displaced Action so it all comes out even. Anyway, with Displaced actions, you can fill your dungeon with all kinds of tricky traps and mechanisms, and most of your monsters will "magically" get smart enough to use them. In order to support Displaced Actions, you will need a bunch of tests allowing action-displacers to check on the status and location of nearby monsters, and, of course, a bunch of displaced-behaviors that the action-displacer can project. You will probably need a state variable that specifies which monster of the nearby ones is under consideration. Once you have those, you can implement stateless AI's and state machines that use displaced tests and displaced actions. Uses range from the simple (monsters trading places) to ways to implement class abilities (priestly "turn-undead" effects) to funny (wand that forces any monster to spend its next few actions singing and dancing) to the arbitrarily complicated (lever that floods the next room, other lever that releases piranhas, other lever that flushes next room, fourth lever that uses the rushing water to power a "shredder" so as to properly compost the effluvient...). Creativity is encouraged, but try to avoid getting excessively capricous or vicious. Most of the "best" uses of this capability are in fact pretty simple. Ray Dillinger