Chimerror Productions

Welcome to Chimerror Productions

Hello! This is Chimerror Productions, a website by Jaycie “chimerror” Mitchell to show off her various artistic works as well as blog posts about making those works, such as this most recent one:

Quoll Work Thread, 2025-10-26 #2 - 2025-10-26 18:53

Tags: blog gamedev inform7 interactive fiction javascript programming quoll testing text adventures

And three hours later my refactor is in! I had quite a few issues converting my old JavaScript code to match my Inform 7 code…

Let’s look at the old Inform 7 code, especially because that gives information on the old JavaScript code:

To say start-taffificated-text:
  let TL be the taffification level of the player;
  if TL is greater than 0:
    if Vorple is supported:
      say "[Unicode 7]";
    otherwise:
      if Current-taffification-depth is 0:
        start capturing text;
    increment Current-taffification-depth.

To say end-taffificated-text:
  let TL be the taffification level of the player;
  if TL is greater than 0:
    if Vorple is supported:
      say "[Unicode 7]";
    otherwise:
      decrement Current-taffification-depth;
      if Current-taffification-depth is 0:
        stop capturing text;
        let CT be "[captured text]";
        let WC be the number of words in CT;
        repeat with WI running from 1 to WC:
          if a random chance of TL in 100 succeeds:
            let W be word number WI in CT;
            let WL be the number of characters in W;
            let TWS be Taffificated-short-words;
            if WL is greater than 3:
              now TWS is Taffificated-long-words;
            let TI be a random number between 1 and the number of entries in TWS;
            let TW be entry TI of TWS;
            if W is in upper case and WL is greater than 1: [So 'I' does not get written in upper case.]
              now TW is "[TW in upper case]";
            otherwise unless W is in lower case:
              now TW is "[TW in title case]";
            replace word number WI in CT with TW;
        say CT.

So when Vorple is supported, we just write out the bell character (UTF-8 0x07) when hitting both phrases. But when it’s not supported, the Inform 7 code must handle it itself. Note that nested uses of the phrases does not turn off taffification, but increments Current-taffification-depth, and only outputs the captured text once that hits 0.

Taffification is done by going through the captured text word by word and picking from one of two lists of taff-speak words based on if the word is longer than 3 characters. And we decide to do that based on the taffification level stored in TL.

Now let’s look at the original JavaScript code. Try to think of how this implementation is different than the Inform 7 code:

let taffificationLevel = 0;

function taffSpeakFilter(output) {
  const taffificationRate = taffificationLevel / 100.0;
  const wordRegex = /[0-9A-Za-z]/; // Don't use /\w/ because we don't want underscore to count
  const upperCaseRegex = /[A-Z]/;
  const lowerCaseRegex = /[a-z]/;
  let result = "";
  let inTafficatedText = false;
  let replaceNextWord = false;
  let nextWordLength = 0;
  let upperCase = false;
  let lowerCase = false;

  for (let char of output) {
    if (char == "\x07" && !inTafficatedText) {
      inTafficatedText = true;
    } else if (inTafficatedText) {
      if (wordRegex.test(char)) {
        if (nextWordLength == 0) {
          nextWordLength = 1;
          if (Math.random() <= taffificationRate) {
            replaceNextWord = true;
            upperCase = false;
            lowerCase = false;
          }
        }
        else {
          nextWordLength++;
        }

        if (!replaceNextWord) {
          result += char;
        }
        else if (upperCaseRegex.test(char)) {
          upperCase = true;
        }
        else if (lowerCaseRegex.test(char)) {
          lowerCase = true;
        }
      } else {
        if (replaceNextWord) {
          result += getTaffSpeakWord(nextWordLength, upperCase, lowerCase);
          replaceNextWord = false;
        }
        nextWordLength = 0;

        if (char == "\x07") {
          inTafficatedText = false;
        } else {
          result += char;
        }
      }
    } else {
      result += char;
    }
  }

  return result;
}

I’ll be honest, this code is not the easiest to read, but here we go. Since we are only looking for bell characters (written as a magic string…) the first time one is encountered, inTaffificatedText gets set to true. While this is true, we try to keep track of the length of words we are seeing as long as we see word characters (determined by the wordRegex regular expression). Based on the taffificationRate, we randomly may set replaceNextWord to true if we intend to replace that word.

When we finally reach a non-word character, we replace the word if need be. I didn’t include getTaffSpeakWord, but it basically does the same thing as picking from the two lists based on the word length. The exact details are not important to fixing the issue.

The big difference between the two implementations is that since the JavaScript implementation only looks for bell characters, it will not know if the bell character was written from the starting or ending taffification phrase. Here’s my modified function:

let taffificationLevel = 0;
let inTafficatedText = false; // Defined out here because we want it to remain in between calls of taffSpeakFilter.

function taffSpeakFilter(output) {
  const taffificationRate = taffificationLevel / 100.0;
  const wordRegex = /[0-9A-Za-z]/; // Don't use /\w/ because we don't want underscore to count
  const upperCaseRegex = /[A-Z]/;
  const lowerCaseRegex = /[a-z]/;
  const startTaffificationCharacter = "\x0F";
  const endTaffificationCharacter = "\x0E";

  let result = "";
  let replaceNextWord = false;
  let nextWordLength = 0;
  let upperCase = false;
  let lowerCase = false;
  let taffificationDepth = 0;
  for (let char of output) {
    if (char == startTaffificationCharacter) {
      taffificationDepth++;
      inTafficatedText = true;
    } else if (char == endTaffificationCharacter) {
      taffificationDepth--;
      if (taffificationDepth == 0) {
        inTafficatedText = false;
      }
    } else if (inTafficatedText) {
      if (wordRegex.test(char)) {
        if (nextWordLength == 0) {
          nextWordLength = 1;
          if (Math.random() <= taffificationRate) {
            replaceNextWord = true;
            upperCase = false;
            lowerCase = false;
          }
        }
        else {
          nextWordLength++;
        }

        if (!replaceNextWord) {
          result += char;
        }
        else if (upperCaseRegex.test(char)) {
          upperCase = true;
        }
        else if (lowerCaseRegex.test(char)) {
          lowerCase = true;
        }
      } else {
        if (replaceNextWord) {
          result += getTaffSpeakWord(nextWordLength, upperCase, lowerCase);
          replaceNextWord = false;
        }
        nextWordLength = 0;
        result += char;
      }
    } else {
      result += char;
    }
  }

  return result;
}

This is still a bit of a mess to read, but is much more predictable. The code can now know between the startTaffificationCharacter (Shift In, 0x0F) and the endTaffificationCharacter (Shift Out, 0x0E), and the Inform 7 code has been changed to output those instead when Vorple is supported. The inTaffificatedText variable has been moved out of the scope of the taffSpeakFilter function, because it turns out that it may get called multiple times when parsing this text, and thus inTaffificatedText may be true when it exits, but would get set to false when it was called again.

But most importantly, we check for endTaffificationCharacter outside of the else if (inTaffificatedText) block, because that is no longer important to whether we should do anything or not. Adding the taffificationDepth variable to limit setting inTaffifcatedText to false when we’re at depth 0, and now the implementations are almost the same. I say “almost” because I just realized that taffificationDepth should probably be outside of taffSpeakFilter as well…

Oh well, from my testing this has been an improvement, so I can just make that fix and return to my original plan to add [block-taffificated-text] and [unblock-taffificated-text] text substitutions!

Tomorrow, probably…

Jaycie “chimerror” Mitchell