T3: Generating Actions

I’m attempting to implement an Inform-style ‘test me’ action, and I can’t seem to figure out how to use nestedAction or nestedActorAction without causing a run-time error. Here’s my code:

DefineIAction (X1) execAction() { trawlerItself.turnCount = 13; // this line works fine... nestedActorAction(me, TravelVia, trawlerDeck.west); // ...but this one causes an error nestedActorAction(me, Take, tiedRope); "Advancing.... "; } ; VerbRule(X1) 'x1' : X1Action verbPhrase = 'advance/advancing the game' ;
I get a nil object reference in the constructor for this class in parser.t, on the line shown:

class PreResolvedProd: BasicProd construct(obj) { /* if it's not a collection, we need to make it a list */ if (!obj.ofKind(Collection))
The same thing happens if I replace nestedActorAction with nestedAction (and get rid of "me, " in the parameters). I don’t think the syntax for my nestedActions can be wrong, because the same syntax works in another part of the game.

To make matters more interesting, I’m not sure how to use Workbench’s Watch Expressions window to troubleshoot this, since I would need to step backward through the code in order to find out what method is trying to construct a new PreResolvedProd (with a nil object). I don’t know how to do that.

There has to be a way to build a list of actions that can be generated automatically from an in-game debugging command. Maybe I’m doing it all wrong. Suggestions welcome…

On further reflection, I can see the problem. The problem is that the end-of-turn rules (I7 term, but you know what I mean) are not being run. The nestedActions are being attempted in a context where trawlerDeck.west is still nil.

What I need, then, is a programmatic way to run the end-of-turn rules before each of my nestedActions, so as to arrange the model world appropriately (and generate output) before the next nestedAction is generated. Is there a way to do that?

I would probably tackle this using input scripts instead.

It might be nice to have an extension that implemented a “test me” equivalent by letting you define an object X with a list of inputs, writing those inputs to a file on startup, then replaying that file when the “test X” command was issued.

Workbench may provide that sort of thing already but I don’t use Workbench, so the above is how I’d go about it. I’ll see what I can put together.

Replaying an event script works – but it requires that the tester (namely, me) find the script file in a dialog box. This is slow and annoying. Once I’ve defined a script, it shows up in the Workbench Scripts window – but right-clicking on a file in this window and choosing Replay causes the game to start afresh, which is NOT what’s wanted for this type of testing command.

I’m seeking a command structure (a) that won’t require me to navigate a file dialog – that I can type as a single command in the game window – and (b) that can be run at a later point in the game – perhaps after I’ve reached the wicked witch’s hut and want to try out the 13 steps required to mix a potion.

Doesn’t setScriptFile() pretty much offer exactly what you want?

It does indeed. The trick is figuring out where to put the command script file. I tried putting it in the Scripts folder – no go. I tried putting it in the folder with the interpreter app – no go.

Turns out, the place to put the script is in the same folder with your source files. That works.

This method is still a little more roundabout than the I7 equivalent, but it’s not too bad. You can create (and edit as needed) a number of testing scripts and invoke them from within the game. For each script, you need either to define a separate IAction or to create a new Topic object for a ‘test’ TAction to access. Or else keep editing the ‘test me’ code before compiling so it will load whatever script you need.

Probably there’s a way to use a LiteralTAction, grab the string (‘test elixir’ giving you the string ‘elixir’) and then tuck the string into the argument to setScriptFile(). I’ll have to try that.

This is probably cool enough to qualify as a tip:

DefineLiteralAction(Test) execAction() { local str = getLiteral() + '.cmd'; setScriptFile(str); } ; VerbRule(Test) 'test' singleLiteral : TestAction verbPhrase = 'test/testing (what)' ;

Just create a test .cmd file with whatever series of commands you like, of the form:

…then name it something like ‘potion.cmd’ and save it to the same directory as your source files. Now the command ‘test potion’ in the game will run that series of commands.

The advantage of this method over the Scripts window, as already noted, is that the command script doesn’t have to start at the beginning of the game. The advantage over the ‘replay’ command is that it doesn’t open a dialog box.

Grooviness!

Here’s what I came up with, based on your code and my earlier ruminations.

DefineLiteralAction(Test)
	execAction()
	{
		local target = getLiteral().toLower();
		local script = allTests.valWhich({x: x.testName.toLower == target});
		if (script)
			script.run();
		else
			"Test sequence not found. ";
	}
;

VerbRule(Test)
	'test' singleLiteral
	: TestAction
	verbPhrase = 'test/testing (what)'
;

class Test: object
	testName = 'nil'
	testList = [ 'z' ]
	testFile = 'TEST_' + testName + '.TCMD'

	run
	{
		"Testing sequence: \"<<testName>>\". ";
		local out = File.openTextFile(testFile, FileAccessWrite);
		testList.forEach({x: out.writeFile('>' + x + '\n')});
		out.closeFile();
		setScriptFile(testFile);
	}
;

allTests: object
	lst()
	{
		if (lst_ == nil)
			initLst();
		return lst_;
	}

	initLst()
	{
		lst_ = new Vector(50);
		local obj = firstObj();
		while (obj != nil)
		{
			if(obj.ofKind(Test))
				lst_.append(obj);
			obj = nextObj(obj);
		}
		lst_ = lst_.toList();
	}

	valWhich(cond)
	{
		if (lst_ == nil)
			initLst();
		return lst_.valWhich(cond);
	}

	lst_ = nil
;

Then you can define new tests like so:

foo: Test
    testName = 'foo'
    testList =
    [
        'x me',
        'i'
    ]
;

bar: Test
    testName = 'bar'
    testList =
    [
        'look',
        'listen'
    ]
;

all: Test
    testName = 'all'
    testList =
    [
        'test foo',
        'test bar'
    ]
;

This looks really useful, so I’ve packaged it up as an extension for my own use, with one addition:

DefineSystemAction(ListTests)
    execSystemAction
    {

        if(allTests.lst.length == 0)
        {
            reportFailure('There are no test scripts defined in this game. ');
            exit;
        }
        
        foreach(local testObj in allTests.lst)
        {
            "<<testObj.testName>>";
            if(gAction.fully)                
            {
                ": ";
                foreach(local txt in testObj.testList)
                    "<<txt>>/";
            }
            "\n";
        }
    }
    fully = nil
;

VerbRule(ListTests)
    ('list' | 'l') 'tests' (| 'fully' -> fully)
    : ListTestsAction
    verbPhrase = 'list/listing test scripts'
;

This might be useful if you’ve set up a number of test scripts and need reminding of what they’re called and what they do while testing. The command LIST TESTS simply lists the names of the test script while the command LIST TESTS FULLY additionally supplies the list of commands that each test script invokes.

– Eric

Brilliant! It’s still a bit more fussy for the author to set up than in I7 (yes, I was re-reading your comparison article on Brass Lantern yesterday, Eric), but it neatly eliminates the need to have separate files on disk for each test script.

I hope you’ll upload this extension to the Archive.

Yes, but you can abbreviate it by defining a template:

Test template 'testName' [testList];

Then you can define your individual test scripts simply as:

Test 'foo' ['x me', 'i', 'jump'];
Test 'bar' ['pray', 'jump'];

Which is pretty much as brief at I7.

– Eric

Good suggestion! The one limitation, based on my attempts to use the template, is that the template can’t be defined in the tests.t extension file itself. A template, it appears, has to be defined in an .h file that is #included – or else in the same file where the template structure is being used.

This is not a terribly difficult thing for the author to manage, it’s just something to point out in the comments in the extension, if you decide to make it publicly available.

The inability to provide new templates as part of an extension is one of the few gripes I have with T3. You can work around it if your new classes inherit from existing ones, but that only takes you so far.

I have a couple custom ShuffledList types that bring TADS up to par with Inform 7 - one that provides an equivalent to “as decreasingly likely outcomes” and one that lets you randomize bits of otherwise static ShuffledList items, in approximately the same way as bracketed text and a “To say” rule.

Nothing terribly impressive but perhaps useful to others who cross over from I7. Trouble is they are awkward to use relative to their stock adv3 counterparts without template support. (I don’t think I’ve ever defined a ShuffledList or ShuffledEventList directly, and can’t imagine why anyone would want to.)

I thought templates were a core language feature, not tied to any kind of class or object and that you can write new templates with no restrictions.

You’re right that templates can be freely defined. They just can’t be bundled up with an extension, since they need to be defined in the source file where they are ultimately used - either directly in that file or in a header that the file includes.

It’s not the end of the world, but it does add some busywork for authors.

If you’re going to use your new template in many source files, you could always hack adv3.h. That’s what I’d do.

#including everything you need in your source files is something most programmers find intuitive though. Given that T3 is modeled somewhat after C++, I think having a header file for the extension would seem natural. Only non-programmers would find it a strange requirement. After all, you are programming when using T3. Also, having a header file gives you the ability to provide macros to the extension user, which gives you the ability to provide short syntax for otherwise multi-line code. Headers are a nice feature, not something to get annoyed about :slight_smile:

Or we could file a new wish at the T3 tracker for templates to be exported.