/* ---------------------------------------------------------------------- */ /* * TADS 3 Hypothetical Action Tester * Version 4 * by Steve Breslin (steve.breslin@gmail.com) * Krister Fundin (fundin@yahoo.com) */ /* ---------------------------------------------------------------------- */ /* * Introduction. */ As the name implies, this extension is about examining the results of a hypothetical action -- one that we might be interested in executing, but only under certain circumstances. Without making any commitments, we can determine whether or not it would pass verify() and/or check(), and, if we are careful, we can even take a peek at the future game state after the action has taken place. This sort of testing might be useful for suggesting commands to the player (sorting out those that don't make any sense), or for making NPCs look smarter by not trying things that would fail for various reasons. To use the extension, we start by adding the library file "hypoTester.tl" to our project. We may also want to include "hypoTester.h" from any source files involved with hypothetical action testing. /* ---------------------------------------------------------------------- */ /* * Two types of tests. */ The hypoTester extension consists of two similar but actually quite different parts. The first part provides these functions: hypoVerify() hypoCheck() hypoVerifyCheck() isHypoActionAchieved() getHypoVerifyResults() and the second part provides these: hypoTest() hypoWhatIf() All of these functions have a similar purpose -- to determine what would happen if a certain action was executed, and, most importantly, whether it would succeed or not -- but they operate differently. The functions from the first category all create an action instance and silently carries out the verify() and/or check() stages through it. This does not result in any side effects, since one of the rules of these two stages is that game state is not allowed to change during them. As for the question of success or failure, there are many other reasons why an action could fail: implicit actions, beforeAction() methods, etc. These cannot be caught by the functions in the first category. Indeed, we might not want them to in some cases. An NPC, for instance, should act according to its knowledge of the world, and the verify() stage is precisely the right place for dealing with knowledge-based issues. The two functions in the second category are different, since they determine the consequences of an action by actually executing it, after which the game state can be analyzed freely. When the test is completed, the VM's built-in undo facility is used in order to revert any changes caused by the action. This kind of test will tell us without any doubt whether an action succeeded or not, but it won't necessarily tell us _why_ an action failed, and thus it is hard to know whether the failure would have been obvious to the actor. We should also beware that these undo tests have their dangers. Not all changes can be undone, so we have to make sure that the action we evaluate doesn't E.G. end the game, ask for input, update banners, write to external files, etc. There is also the potential problem of running out of undo records. The more changes we make, the more records we use up, and there is only a finite supply of these. (The exact number can vary from one interpreter to another.) If we run out, then we won't be able to get back to the original savepoint, and that means serious trouble. Even if we don't run out of records, we may limit the number of consecutive turns that the player can take back with the UNDO command. In summary, the functions that use verify() and check() should be safe to call, but they might not give us all the information we need. Undo tests can give us more information, but this information is necessarily of a slightly different type, and we must also use these tests with greater caution. /* ---------------------------------------------------------------------- */ /* * Hypothetical verify() and check(). */ To see whether an action would pass verify() or not, we can use the hypoVerify() function. The arguments are the actor, the action class and the involved objects, if any. To test whether it would be logical for the actor bob to open the front door, we could make the following call: hypoVerify(bob, OpenAction, frontDoor) Note that we have to write the name of the action in full, including the "-Action" suffix. This function returns true if the action was logical and would have been allowed to proceed if executed normally. Verify results such as nonObvious and dangerous are not disallowing in this sense. We can, however, disallow an action in a hypoVerify() test while still allowing it during normal object resolution, by using the special hypoIllogical verify result. If we need more detailed information about the verify results, we can also use the getHypoVerifyResults() function. The arguments are the same, but this function returns a VerifyResultList object. For more information on these, consult the adv3 sources (the verify.t file in this case). Another variation of hypothetical verification is the isHypoActionAchieved() function, which returns true if the action would be illogical because it has already been achieved. This is done by checking for an illogicalAlready verify result. Hypothetical check() is similar. We can use the hypoCheck() function to test if an action would pass the check() stage. Also, the hypoVerifyCheck() is a short-cut for testing both verify() and check() at the same time. Again, the arguments are the same: hypoVerifyCheck(bob, OpenAction, frontDoor) /* ---------------------------------------------------------------------- */ /* * Hypothetical action execution. */ Two similar functions are provided for undo testing. They are called hypoTest() and hypoWhatIf(). The first one, hypoTest(), is relatively straight-forward. It simply executes a given action and determines whether it succeeded or not. In this case, success is defined as the absence of a failure report in the transcript. The hypoWhatIf() function is a bit more powerful. It takes a callback function as the first argument. After executing the action, it evaluates the callback and returns the result. This way, we can make arbitrary queries about a future game state. Now we can go one step further with our front door example, and actually see what would happen when trying to open it. We could use either of the two functions we have learned: hypoTest(bob, OpenAction, frontDoor) hypoWhatIf({: frontDoor.isOpen() }, bob, OpenAction, frontDoor) In the first case, we just look for a failure report, and in the second case, we explicitly check if the door has been opened. Which one we want to use could depend on the circumstances, but here, hypoTest() would probably be the more natural choice. Another special feature of these two functions is that they can execute more than one action at the same time. This can be achieved by wrapping each action specification in a list, like so: hypoTest([bob, UnlockWithAction, frontDoor, houseKey], [bob, OpenAction, frontDoor]) Note that the hypoTest() function will return nil as soon as the first action has failed. There is no need to test the remaining actions, since the overall result would still be failure. The hypoWhatIf() function, however, always executes all the actions. Finally, having already warned against doing un-undoable things during an undo test, we could have use for the gUndoTesting variable. This is set to true only during a call to one of these two functions, so that if we have to test an action that could potentially end the game, for instance, then we could check gUndoTesting in our action code and simply skip that part during an undo test. /* ---------------------------------------------------------------------- */ /* * Bugs and limitations. */ When using hypothetical verify() and check(), remapping may not produce the exact same results as when executing an action, if the action has two or more object slots. Put simply, if the direct object remaps action A to action B, then the verify/check handlers for action B will be called on the direct object, as expected, but on the indirect object, the handlers for the unremapped action A are called instead.