Today has not been a good day; I’ve been debugging a script that has caused me endless problems. This is it:
The purpose of this script is to pick a stage for an actor to go visit, with the proviso that the actor should not visit anybody with whom he has recently had a conversation. In fact, this is a Desirable script, so it establishes how desirable a particular stage is to visit. If the EventHappened in the first argument, then the second argument is selected; if not, then the third argument, a random value, is selected. In other words, this says, “If you haven’t visited anybody at all, pick a random number for Desirability, but if you HAVE visited somebody, give them ‘negative priority’ based on how recently you greeted them.”
Looks simple enough, right? Well, the first problem was that, for some reason, the StageOwners of the various stages were initialized incorrectly: Fate owned all the stages. I don’t know how this happened, but I had to go into the XML file and fix the entries by hand. My guess is that it was screwed up in one of the bad saves I discussed previously.
With that problem corrected, I thought I was home free. No such luck: after much fiddling around, I realized an important point that I had forgotten: the PickUpperIf is NOT a sequential processing IF. It calculates both the second argument and the third argument no matter what. It merely selects which of the two will be passed on as the result. Which means that the second argument is always calculated. Which means that the LookUpEvent operator is executed. But in the case where the actor has NOT spoken to the actor in question, LookUpEvent will return a poison result, which will poison that selection.
This script is guaranteed to always return the second argument. If EventHappened is true, then it returns the second argument. If EventHappened is false, then it poisons the option. Which means that it’s that time again:
So now I have to figure out how to fix this problem. It is a fundamental flaw in the design of Sappho — but what can I do to fix it? If a search of the HistoryBook comes up empty-handed, it shouldn’t poison the calculation — UNLESS the failure to find the appropriate event is intrinsically destructive to the calculation. How can I re-express this concept? Put the problem in plain language:
A. “If you search the HistoryBook and find nothing, you should poison the calculation because your result is meaningless.”
B. “If you search the HistoryBook and find nothing, you should return a null value.”
The Next Day
I have gone through a number of possible solutions, rejecting each one:
1. Create two versions of each search operator: a poisoning version and a null version. The problem with this is that I cannot define what a non-poisoning version would return. Consider the script shown above. The LookUpEvent operator would fail to find an event and would usually poison the result. But a non-poisoning version of LookUpEvent would not poison the calculation; so what would it return? If it returns a value inside the correct range of HistoryBook values, then the PastTime operator will return a bad value. If it returns a value outside the correct range of HistoryBook values, then the PastTime operator will generate poison.
2. Create a scope-definining operator for poison. I would create a new version of PickUpperIf (say, “PoisonFreePickUpperIf”) that uses a new global flag (say, “IgnorePoison”). Operators still generate poison as usual, but the poison would be disabled by the PoisonFreePickUpperIf. Unfortunately, there’s a killer problem: the PoisonFreePickUpperIf statement would be executed AFTER all of its arguments have been executed. Poison terminates the entire calculation, so… I don’t know...
3. Write a short-circuiting PickUpperIf. This is feasible because the boolean switcher is evaluated first. However, the PickUpperIf is evaluated last, so I would have to devise a scheme in which I set a global flag that intercepts the boolean result before the other two arguments are evaluated. This is very tricky business because it’s inside a recursive routine. However, after much screwing around, I came up with this simple solution:
if (zLabel.equals("PickUpperIf") && (i==1) && (stack[stackTop-1]==0)) {
++i; // skip the upper clause
push(0.0f); // substitute a null value
}
It works! Problem solved.