Variable-friendly equivalent to newActorAction() in adv3

The “standard” way to create an action as a new turn for an NPC is to use newActorAction(). Unfortunately, this is a macro, and it uses a somewhat idiosyncratic syntax. I.e., if bob is an Actor instance, something like:

     newActorAction(bob, North, nil);

…to have bob take a turn executing NorthAction. But this, as an example, won’t compile:

     actorAction() {
          _doAction(North, nil);
     _doAction(action, objs) {
          newActorAction(self, action, objs);

Because North isn’t defined, and the macro will try to construct an instance of actionAction, which also isn’t defined.

This means that any NPC AI that relies on newActorAction() ends up having to be a big messy switch() statement or an even uglier mass of if-then spaghetti. Which is obviously undesirable.

So…is there some other way (in adv3) to do this sort of thing? You could obviously do something like build a bunch methods that shadow all the Action types you want to be able to script (so things like _doNorth() { newActorAction(self, North, nil); } and so on, and then you can have a LookupTable that maps “north” to _doNorth().

Or am I fundamentally thinking about this the wrong way? The assumption in general seems to be…well, okay. The general assumption seems to be that NPCs are mostly static. But let’s ignore that. For NPCs with a bunch of scripting, the general assumption seems to be that you’ll just be miracle-ing your NPC into doing whatever they want to do. Like your scripting decides that the NPC wants to go to the stuff library, you just alice.moveInto(stuffyLibrary) or whatever. That is: basically ignore any game logic the player is bound by in order to just let the NPC do whatever it needs to do.

I really don’t want to take that approach, I want all the NPCs to be playing by the same rules the player has. I’m aware of a couple things that kinda do this (like, in the travel example, scriptedTravelTo() to move to a connected location), but as a general model it doesn’t appear to be well supported, and doesn’t seem to be an approach implemented in many games (or at least those adv3 games that have made their source available).

Am I just missing something obvious, or am I just asking for something silly here?

Can you not just use the underlying _newAction function instead of the newActorAction macro? You simply have to pass NorthAction in the proper place instead of North. doNorth() could be
You shouldn’t need to pass nil for the last argument if there are no grammatical objects.
Also, is actorAction specifically where you want NPC autonomy to happen? It seems like the kind of thing you’re wanting should go in the takeTurn() method or an AgendaItem object…

Unless you’ve got something very particular up your sleeve (which is great), scriptedTravelTo works very well for random NPC movement, you just police all the direction options in the Actor’s room (maybe also checking that this actor can pass), put the destinations in a list, and call scriptedTravelTo(rand(optionsList));
I have several Actors with a moveAgendaItem like this…

I’m leery of _newAction() because of that leading underscore. I guess I don’t know what TADS3/adv3’s official naming conventions are, but in general if I see that, I assume that means the function/method is for internal use only—and so it could change at any update. Of course the most recent TADS3 release is now almost a decade old, so it’s probably safe to consider it set in stone. But there’s that.

It also doesn’t quite do what I want. I think what I’m probably going to end up doing is something like:

                // Replacing this... 
                //newActorAction(actor, Take, pebble);

                // ...with this... 
                execCommand(actor, 'take pebble');

…where elsewhere we have something like…

        execCommand(actor, cmd) {
                executeCommand(actor, actor, cmdTokenizer.tokenize(cmd), true);

…only with more sanity checking. The idea being that it’s easier/less expensive/more readable to programatically build NPC commands as text instead of needing to convert/build a hash table/whatever all possible actions/objects/whatever in a way that it’s easy to refer to and perform logic on them.

No, I was just using actorAction() because it’s the easiest way to illustrate a “working” example. In the code I’m actually working on the logic in question is spread out across a decision engine that implements a FSM with probabilistic state transitions and a bunch of AgendaItem instances that encapsulate overall “macro” behaviors (fetch an object in the abstract, navigate to a known location, look for an unknown location, play a game of poker, whatever). But that’s thousand of lines of code and therefore not practical to cut and paste into a post.

1 Like

It sounds like you’ve got an interesting plan…
I’m curious as to how _newAction doesn’t quite do what you want, since newActorAction is just another name for it.
As far as executeCommand, that’s an interesting idea: I wonder, will you run into disambig issues that way though? Because with newAction you can name code objects and use valWhich… what happens if you program a character to ‘take card’, passing it to executeCommand, and the parser wants to know if you mean the queen or the ace? Or the apple on the counter as opposed to the apple on the plate? Will your logic always be able to know that the string isn’t ambiguous?

Well, newActorAction() doesn’t really do what I want either. It’s just the thing whose intended behavior seems to be closest to what I want (and is actually implemented by adv3) . What I actually want is something like:

     r = actor.exec(cmd);
     if(r.isFailure) {
          // handler
     } else {
          // handler

newActorAction() almost does this sort of thing, but it expects you to have an Action instance as well as whatever Thing instances you want to perform the Action on. And TADS3/adv3 unfortunately doesn’t really go out of its way to make it easy to switch between “parser space” identifiers and Action and Thing instances. It’s possible, and I’ve written code that does it, but it’s kudgy. Which is annoying, because figuring out stuff like what, if anything >TAKE THE RED BOOK would match is absolutely part of the basic business the parser takes care of in order to do its job.

Anyway, I think the “solution” I’ll end up going with, unless there’s something I’m missing and/or I think of something more clever, is to just wrap everything in a big try()/catch()/finally() block(s) and implement a simplified version of executeCommand(), with a fake transcript and savepoint() and undo().

But that seems like a super heavyweight approach for what feels like it’s a fairly simple task (in that it’s already very similar to stuff the parser is already doing itself).

Sounds ambitious. Good luck… I hope I can see the finished product.