I recently was thinking about a way to test taking inventory every turn, using scripts.
The brute force method which might clog up transcripts is as follows: (note – this is cut/pasted/simplified from actual code. It is more meant as, okay, this simple case works)
volume testing inventory - not for release
constant-inventory-testing is a truth state that varies.
every turn when constant-inventory-testing is true (this is the always rehash inventory for testing rule):
say "===start constant inventory testing===[line break]";
try taking inventory;
say "===end constant inventory testing===[line break]";
chapter citing [ideally cit on and cit off, but the every-turn syntax is what matters here]
citing is an action out of world.
understand the command "cit" as something new.
understand "cit" as citing.
carry out citing:
now constant-inventory-testing is whether or not constant-inventory-testing is false;
say "constant-inventory-testing now [constant-inventory-testing].";
the rule succeeds;
This will always track inventory, and even better, I could write a trivial Python script to zap identical inventories. But it could cause transcript bloat.
volume testing inventory - not for release
last-items-carried is a number that varies.
every turn (this is the sometimes rehash inventory for testing rule):
if number of things carried by player is last-items-carried, continue the action;
say "Number of items has changed.";
say "===start conditional inventory testing===[line break]";
try taking inventory;
say "===end conditional inventory testing===[line break]";
now last-items-carried is number of things carried by the player;
This simple code has an obvious flaw. If, one turn, I GIVE MONEY TO HOT DOG VENDOR and the hot dog vendor gives me back a hot dog, the test won’t show up.
Is there a good way to say “if inventory has not changed since last turn?” Or should I just use one of these methods and say, okay, close enough?
The previous inventory list is a list of things that varies.
Every turn:
let the new inventory list be the list of things carried by the player;
if the new inventory list is not the previous inventory list:
try taking inventory;
now the previous inventory list is the new inventory list.
The order has to be consistent, but fortunately, Inform’s world model ensures it is!
Internally, every object has a “parent” pointer, a “first child” pointer, and a “next sibling” pointer. When Inform produces “the list of things carried by the player”, it starts at the player’s “first child” pointer, then follows the “next sibling” pointers until one of them points to nothing.
These pointers are only updated if something is added to or removed from that part of the tree, which would also alter the inventory listing (since things are listed in this internal sibling order).
That’s cool to know! I knew the ordering was (roughly) based on when things appeared first in the code. I’d have been okay with just comparing two lists by length and then seeing if each item in list A was in list B.
Also, now that you showed the code above, I feel a bit silly not thinking of it. I’ve used lists of items variable before. I guess having all that potential custom text in inventory muddled things for me a bit.
Now all that’s left to do is to decide whether or not I should note specifically what items changed in inventory.
Yeah, the initial state of the object tree is based on the order things are defined in the source code. After that, whenever a new child is added to something, it goes at the start of the sibling list. (It’s much easier to code that way, and O(1) instead of O(n).) This means newly-acquired things tend to appear first in inventory listing.
The following will confound that strategy, though:
lab is room.
the ball is carried by the player. the box is carried by the player. the apple is carried by the player.
Instead of jumping:
try taking inventory;
try dropping the apple;
try taking the apple;
try taking inventory;
taking something is inventory-altering.
dropping something is inventory-altering.
inserting something into is inventory-altering.
putting something on is inventory-altering.
eating something is inventory-altering.
enclosures is a list of things that varies.
To decide what action name is the current action name: (- action -).
when play begins:
now enclosures is the list of things enclosed by the player.
to inv-check:
let post-enclosures be the list of things enclosed by the player;
if post-enclosures is not enclosures begin;
say "inv changed by [current action].";
now enclosures is post-enclosures;
try taking inventory;
end if;
after implicitly taking something: inv-check.
first after inventory-altering: inv-check; continue the action.
lab is room.
the ball is carried by the player. the box is carried by the player. the apple is carried by the player.
Instead of jumping:
try taking inventory;
try dropping the apple;
try taking the apple;
try taking inventory;
I think this works so long as no After rules alter the inventory.
I think of it as “Why build a new list and do the comparison when it’s gratuitous?”.
To which an answer might be “so you don’t have to think about all the things that might change the inventory”, but I already know the things in the standard rules that can change the inventory and if I’m adding inventory-altering situations elsewhere, well, I’d know about those, too…
Final detail: many of my inventory checks needed to occur after a parser error.
I didn’t realize I could use this syntax, though it was sitting pretty clearly in the “printing a parser error” documentation:
after printing a parser error (this is the parser error inventory check rule):
follow the always rehash inventory for testing rule;
This may come in handy for others in much different circumstances. I had a logic blind spot thinking “well, the parser errors are the absolute end of a turn, game time can’t pass, can’t have any rulebooks after them.”