Thursday, October 27, 2022

Acuitas Diary #54 (October 2022)

I wish I had more new features to write about this month, but I had to slow down a little bit. There was too much going on, and my brain started feeling like it was trying to brown out and stop working. So instead I'm going to talk about overhauling the Narrative module. I had gotten it pretty far, and I decided it was time to go back and address some pain points I kept running into over and over as I was adding new functions. This work is not exactly "easy" but it's much easier than adding new functionality.

A black-and-white sketch of a fountain pen lying on a collection of small notes.

A big thing I wanted to address was ease of negating or rescinding conditions in a story. Each time a new fact is introduced by a story sentence, it produces a spray of implications which are all collected by the Narrative Engine and used for future reasoning about the story. For example, the story sentence "Jack is cold" will produce the implication "Jack is uncomfortable" and will register "Jack is cold" as a problem state.

Now let's say that, at some later point in the story, we find the sentence "Jack is not cold anymore." This will replace "Jack is cold" in the current worldstate that the Narrative Engine is tracking, and will officially solve the "Jack is cold" problem. But what about "Jack is uncomfortable"? "Jack is not cold" does NOT automatically generate the implication "Jack is not uncomfortable," since a human can be uncomfortable for any number of other reasons. So there is nothing to negate "Jack is uncomfortable" and get it out of the worldstate.

Or suppose that Jack is cold, and he solves the problem by getting a jacket. "Jack wears jacket" implies "Jack is warm" implies "Jack is not cold," and this cancels "Jack is cold" out of the worldstate. But then he loses the jacket. The Narrative module ought to know that he returns to his default state of being cold ... but it doesn't.

These are just two examples of how reversing a condition can leave orphaned implied results scattered around the Narrative Engine's data structures. So as part of the overhaul, I'm working on ways to include pointers between facts registered in the Narrative's worldstate and *anything else* that was created, activated, or deactivated as a result of their presence. So when a fact gets rescinded, the Narrative Engine can easily walk through all the effects of its existence and rescind them too.

A fact can inherit its presence from multiple other facts. Let's say that Jack is uncomfortable because he's cold and hungry. If either "Jack is cold" or "Jack is hungry" is negated, "Jack is uncomfortable" will continue to be true, since *one* of the conditions that causes it is still in effect. But if "Jack is cold" and "Jack is hungry" are both negated, "Jack is uncomfortable" will be deactivated as well, since it has lost all its "supports."

A fact can also be suppressed by another fact (as in the example of the jacket canceling out the default state of cold). The solution in this case is not to remove "Jack is cold" from the worldstate, but to deactivate it and specify that "Jack wears jacket" is the source of the deactivation. If "Jack wears jacket" is deactivated, "Jack is cold" automatically becomes active again.

I had never planned a way to maintain this complex web of connections when I first wrote the module - it's one of the things I had to learn was necessary by doing the work - hence the major overhaul. And that's only one thing I'm changing; I'm also working on efficiency improvements, clarity, and general cleanup. I'm hoping to have a solid foundation from which to pursue my Goal Story next year.

Until the next cycle,
Jenny

2 comments:

  1. A lot of the work I've had to do is with flags and iterating through a number of containers. While cancelling an effect if and only if another effect is in play is functional, have you considered iterating through each condition and using boolean ors? Like foreach condition, isCold = isCold || condition.makesCold

    Then you could iterate for ColdProof and do final logic after that. Obviously you're far more familiar with your work and I'm seeing through the lens of mine, but I figured I'd ask if the concept has already been considered and discarded.

    ReplyDelete
    Replies
    1. I don't think I *had* considered this, but it's a struggle for me to decide whether it's better than what I'm doing, or not.

      So currently, when a new condition C arrives, I look up C.makesX and C.makesNotX, then iterate over the X and turn isX on or off as appropriate. The problem has been that, if notC comes in later, the process is not strictly reversible. I think you're saying that I could, instead, simply store C and C.makes[Not]X, and then - instead of storing True/False values that could potentially go stale, for all the isX - evaluate them when they're needed, by checking whether any currently stored conditions have makesX or makesNotX as a property.

      I think the main reason it doesn't immediately grab me has to do with this being a Narrative tracker. When a new sentence comes in, the emphasis is on "what changed, and how does Acuitas react to that?" So it kind of makes sense to reason forward from the new condition to the subset of states it alters, rather than picking out states to query and then evaluating whether they're true or not.

      I could also be misunderstanding something, since I'm trying to do translation from your architecture to mine. I don't have any actual distinction between containers and flags - it's all just facts that imply other facts. And the set of facts that could become involved in a story is not predefined and finite.

      Delete