NaNoGenMo / 2018

National Novel Generation Month, 2018 edition.
https://nanogenmo.github.io/
112 stars 6 forks source link

Plot calculus #16

Open hendrikboom3 opened 5 years ago

hendrikboom3 commented 5 years ago

I'd like to develop a formal system for generating plots. I'm inspired by previous generative attempts by others. In particular (wish I had links; may have to edit this later):

The drawback in the first one seems to be that there is no direction. There does not seem to be much in the way of individual motives, desires, which could lead to conflicts.

The drawback in the second was that the actions and situations were modeled by fixed strings. This made for a finite search space. I'm hoping to parameterize the actions and situations to make the system more flexible.

Thus I'm going to introduce unification for a logic in the style of the first inspiration, and a probabilistic choice of actions in the style of the second.

I'm also likely to do this while reading Antonio Damasio's book, the Strange Order of Things, which is about the evolution of mind. I'm expecting to learn something a bit formal about emotions there, but I'll see what happens.

The whole process of writing this is likely to be very heuristic and experimental, with only intuitive exploration of partial results. Advice and commentary will be very welcome.

hendrikboom3 commented 5 years ago

Another drawback is the question of too much detail. Readers get bored when everything is presented in too much detail. So a good system (not that I'm likely to com up with one this good) should also figure what the reader is likely to assume, or predict, and not spell it out in the final text.

enkiv2 commented 5 years ago

Sounds like the second one was my 2015 "scene/sequel" project?

On Fri, Oct 26, 2018, 3:45 PM hendrikboom3 notifications@github.com wrote:

Another drawback is the question of too much detail. Readers get bored when everything is presented in too much detail. So a good system (not that I'm likely to com up with one this good) should also figure what the reader is likely to assume, or predict, and not spell it out in the final text.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/NaNoGenMo/2018/issues/16#issuecomment-433522673, or mute the thread https://github.com/notifications/unsubscribe-auth/AAd6GQWd5QO4KzKzTLqHVA0LavjuZe1zks5uo2ZYgaJpZM4X8yAB .

hendrikboom3 commented 5 years ago

Sounds like the second one was my 2015 "scene/sequel" project?

Indeed, it was. I have the code sitting on my hard drive. But I'm having trouble finding the link now. Care to provide it? I should be able to cite sources.

-- hendrik

hendrikboom3 commented 5 years ago

I've cleaned up and shortened u unification algorithm, making the substitutions a separate part of the API. It looks more user-friendly now, and is shorter as well.

The discussion on https://github.com/NaNoGenMo/2018/issues/6 has convinced me i don't need full unification. But I have it now, so I may as well go ahead and use it.

Next step is to implement some kind of rule on top of unification, preferably isolating rule implementation from any kind of side-effects or control flow. Something that can be used in a functional style (no matter how it's implemented inside). Something like a function taking a form, unifying it with the antecedent of a rule, and returning the consequences and a list of potential actions. Those actions to be applied or not by some higher procedures. And I'll likely have to implement some mechanism for producing strings as output -- presumably as potential actions. And maybe code some John-wants-Mary, Mary loves-Bob kind of situations and see what can ensue. Hmmm. Going to have to have antecedents that aren't just single forms... Interesting.

hendrikboom3 commented 5 years ago

The left side of a Prolog rule is a conclusion, and Prolog tries to prove the conclusion by first proving the right side.

My rules as currently envisaged are opposite. A rule consists of antecedents on the left, which are a kind of precondition, testing whether the rule is applicable, and then consequents on the right.

The left side of one of the rules I'm thinking of is more like a premiss. The right side lists what can be concluded and done based on that. Usually this is new facts, some of which are established by performing actions. And, of course, some of these actions erase existing facts.

If I have any interaction with Prolog-style rules, I'll be matching the left sides of my rules with the left sides of Prolog-style rules. In fact, a Prolog-style rule is just like one of my rules, except left and right are interchanged, and Prolog restricts everything to just one consequent (on the left), which is not an action.

In fact, this suggests I can do some kind of planning with my rules. If an antecedent isn't already satisfied, I can look for another rule that establishes it (along with a bunch of side effects, which may or may not be what's wanted. This might be usable for author planning (which directs the arc of the novel) or character planning (which is a character trying to accomplish something). Same formalism, perhaps multiple uses.

cpressey commented 5 years ago

Putting it in terms of antecedents and consequents reminds me that sometimes Prolog's execution model is described as searching for a proof. It's a little weird to think of a story as a proof, maybe, but a proof is basically a set of steps that show you can get from some point A to some point B, and a story is basically a set of steps from some point A to some point B too, so maybe it's not that weird.

hendrikboom3 commented 5 years ago

Just managed to implement the jealousy plot trope:

It successfully predicted John kills Jim from John loves Amy and Amy loves Jim. Unfortunately, from John loves Amy and Amy loves John, the same pattern predicted

Requited love should not lead to a double suicide. Well, it happened in Romeo and Juliet, but there were a lot of complications.

All of this represented in a predicate calculus with function symbols and free variables but no quantifiers rather than English.

I'll have to think awhile about specifying that variables sometimes have to be instantiated with different individuals. And I'll have to figure out how to represent different individuals themselves.

pointyointment commented 5 years ago

How about if (A loves B && B loves C && A != C) then {A kills C}?

hendrikboom3 commented 5 years ago

The problem is how to implement !=. I could, or course, enumerate all pairs of things and provide a separate statement that they are not equal. But that is almost n-squared rules. Fewer if I apply a rule that a != b iff b != a. But commutative operations are a complication in a unification engine. I think implementing equality or inequality usably will require a rethinking of the logic engine. That isn't a dismissal; I'm entertaining ideas on how to do that already. But it will take a while.

enkiv2 commented 5 years ago

Can equality and inequality somehow be threaded into the mechanism that enumerates possibilities?

For instance, equality is equivalent to aliasing: all variables that are declared equal are actually alternate names for the same value (so variables declared equal don't add to the cost of the cartesian product).

If inequality can be hinted to the engine (maybe even in the form of the conjunction greater-than OR less-than, for the sake of processing each half separately or maybe even in parallel), it can even make processing faster. (Plus, the same mechanics could be used for pattern matching.)

On Wed, Nov 7, 2018 at 8:59 AM hendrikboom3 notifications@github.com wrote:

The problem is yow to implement !=. I could, or course, enumerate all pairs of things and provide a separate statement that they are not equal. But that is almost n-squared rules. Fewer if I apply a rule that a != b iff b != a. But commutative operations are a complication in a unification engine. I think implementing equality or inequality usably will require a rethinking of the logic engine. That isn't a dismissal; I'm entertaining ideas on how to do that already. But it will take a while.

— You are receiving this because you commented.

Reply to this email directly, view it on GitHub https://github.com/NaNoGenMo/2018/issues/16#issuecomment-436631564, or mute the thread https://github.com/notifications/unsubscribe-auth/AAd6GXvStZB0Yyn5XlZkkzCzZCC-eOBIks5usudcgaJpZM4X8yAB .

cpressey commented 5 years ago

The way I solved this in Samovar, fwiw, was to say that two different variables could never be bound to the same term. i.e. {A=alice,B=alice} is simply not a valid unifier. It rules out being able to express things like "Alice loves Alice" or "Alice kills Alice" but I decided that was a reasonable trade-off.

hendrikboom3 commented 5 years ago

It feels like a terrible hack. The semantics are no longer clear. Does this mean, for example, that we can have {A = f(U), U = g(V), B = f(g(V))}? If so, when do we discover that A and B end up mapping to the same term after all substitutions have been applied?

I've thought of adding a predicate "differs" so I can say differs[A, B] in an antecedent, interpreting it as a constraint that A and B cannot be unified; i.e., that there is no substitution that can make them the same. That test of course is only well-defined after all the unifications on antecedents have been done, but there are likely situations where it can be used to cut exponential search growth early.

It doesn't have any clear semantics; it's merely a syntactic hack. It's apparent advantage seems to be that I can't see its consequences and so I can't formulate meaningful counterexamples. Not an convincing advantage. But I might try it out experimentally.

hendrikboom3 commented 5 years ago

I'm starting to wonder whether my whole approach is wrong. Instead of a kind of cause-and-effect, precondition and action system, might I instead want a collection of plot typical threads that can be fused together in various ways. The valences that connect them would be actions or states that are shared between different plot threads? There would be no more mandatory Prolog-like splitting rules into premisses and conclusions.

The more tangled the resulting web, the more intricate the plot.

No, I'm not rushing to throw everything out and start anew. I want what I've got now to at least work before I start anew. What I've got now might very well turn out to be a part of the new plot order. Actions will still need motives and means.

hendrikboom3 commented 5 years ago

So with the rules

loves[b,c], loves[c,d], kills[b,d] : ;
    : loves[john[], mary[]] ;
: loves[mary[],jim[]];
findweapon[b, w] : kills[b, d];
: findweapon[john[], sword[]];
: findweapon[john[], gun[]].

give the plots:

loves[john[],mary[]]
loves[mary[],jim[]]
findweapon[john[],sword[]]
kills[john[],jim[]]

and

loves[john[],mary[]]
loves[mary[],jim[]]
findweapon[john[],gun[]]
kills[john[],jim[]]

What I don't like about the rules is that in order to fire rule

findweapon[b, w] : kills[b, d];

which says that findweapon is a precondition for killing, I have to write the eternal triangle rule as

    loves[b,c], loves[c,d], kills[b,d] : ;

instead of loves[b,c], loves[c,d] : kills[b,d] ; which expresses more intuitively that the two conflicting loves causes the murder.

This is because an antecedent always is matched up with a consequent in a deduction chain.

This is what made me wonder about plot threads -- we just have two plot threads:

loves[b,c], loves[c,d], kills[b,d] ;

and findweapon[b, w], kills[b, d]; which just expresses that there are two plot threads that come together in the action kills[b, d].

Now weaving plot threads together is one of the ways that complex narratives are composed of simple plot elements.

But this in no way expresses that both rules express preconditions for the murder -- motive and opportunity.

So ... Do I need to change the formalism, or just learn better how to use it?

-- hendrik

hendrikboom3 commented 5 years ago

I could have written

loves[b,c], loves[c,d]: motivetokill[b,d] : ;
    : loves[john[], mary[]] ;
: loves[mary[],jim[]];
motivetokill[b, d], findweapon[b, w] : kills[b, d];
: findweapon[john[], sword[]];
: findweapon[john[], gun[]].

And presumably there can be a lot more elabotation in this style. Perhaps this is a good way of writing it. I do notice that every one of these rules has only one consequent. This is just like in Prolog, although Prolog writes its consequents on the left.

I have implemented rules with multiple consequents in the search process, but haven't put them together in the resulting plot. I suppose I should get around to figuring this out.

There can be actions that happen before and after the linked consequent, and this leads to a directed graph of actions and conditions.

I'll have to figure it out if I try the plot-thread approach, too. So maybe the plot-thread mechanism is no mor difficult than to implement than the deductive one. The question is which better expresses plot ideas. There are still difficulties ensuring adequate causality with just plot threads. Maybe I'll have to have both.

Weaving new threads into a complex plot has to be constrained to avoid temporal cycles. (I'm ignoring time travel stories here.) Now doing frequent topological sorts is unacceptably inefficient when the plot gets large. I could simply assign numerical times to every event, but that will seriously impede weaving flexibility. Could there be some relevant graph-theoretical algorithms? Probably.