Making sandwiches - supporter objects in inventory

… or, in other words, how to combine objects in the inventory?

Let’s assume there are several objects in the player’s possession:

Object ham_slice;
Object bread_slice has supporter;

What I would like to do is make a ham sandwich, use two or more objects together to create a new one:

>get ham_slice and bread_slice
TAKEN
>put ham_slice on bread_slice
YOU LACK THE DEXTERITY.
>drop bread_slice. put ham_slice on bread_slice.
YOU PUT THE HAM_SLICE ON THE BREAD_SLICE.

I guess the library doesn’t allow portable supporter objects and I should take a different route to solve this problem?

The issue is part of the definition of the default action routine for ##PutOn, called PutOnSub:

! StdLib 6.12 but also affects 6/11
[ PutOnSub ancestor;
    receive_action = ##PutOn;
    if (second == d_obj || actor in second) <<Drop noun, actor>>;
    if (parent(noun) == second) return L__M(##Drop, 1, noun);
    if (noun notin actor && ImplicitTake(noun)) return L__M(##PutOn, 1, noun);

    ancestor = CommonAncestor(noun, second);
    if (ancestor == noun) return L__M(##PutOn, 2, noun);
    if (ObjectIsUntouchable(second)) return;

    if (second ~= ancestor) {
        action = ##Receive;
        if (RunRoutines(second, before)) { action = ##PutOn; return; }
        action = ##PutOn;
    }
    if (second hasnt supporter) return L__M(##PutOn, 3, second);
    if (ancestor == actor)      return L__M(##PutOn, 4, second);  ! <--- PROBLEM LINE
    if (noun has worn && ImplicitDisrobe(noun)) return;

    if (ObjectDoesNotFit(noun, second) ||
        LibraryExtensions.RunWhile(ext_objectdoesnotfit, false, noun, second)) return;
    if (AtFullCapacity(noun, second)) return L__M(##PutOn, 6, second);

    move noun to second;

    if (AfterRoutines()) return;

    if (second ~= ancestor) {
        action = ##Receive;
        if (RunRoutines(second, after)) { action = ##PutOn; return; }
        action = ##PutOn;
    }
    if (keep_silent) return;
    if (multiflag) return L__M(##PutOn, 7);
    L__M(##PutOn, 8, noun, second);
];

I’m not certain what the intent of the original logic is, but it forbids putting any item in inventory on top of another item in inventory, as you are seeing.

If you want to allow this behavior in all situations, you can just replace the routine definition using the Replace directive and remove that line (which may cause other problems), or you can modify it (as in the following example).

Constant Story "Ham Sandwich";
Constant Headline "^an intfiction.org help forum problem^";


Replace PutOnSub;

Include "Parser";
Include "VerbLib";

[ PutOnSub ancestor;
    receive_action = ##PutOn;
    if (second == d_obj || actor in second) <<Drop noun, actor>>;
    if (parent(noun) == second) return L__M(##Drop, 1, noun);
    if (noun notin actor && ImplicitTake(noun)) return L__M(##PutOn, 1, noun);

    ancestor = CommonAncestor(noun, second);
    if (ancestor == noun) return L__M(##PutOn, 2, noun);
    if (ObjectIsUntouchable(second)) return;

    if (second ~= ancestor) {
        action = ##Receive;
        if (RunRoutines(second, before)) { action = ##PutOn; return; }
        action = ##PutOn;
    }
    if (second hasnt supporter) return L__M(##PutOn, 3, second);
    if (noun == actor || second == actor)      return L__M(##PutOn, 4, second); ! MODIFIED
    if (noun has worn && ImplicitDisrobe(noun)) return;

    if (ObjectDoesNotFit(noun, second) ||
        LibraryExtensions.RunWhile(ext_objectdoesnotfit, false, noun, second)) return;
    if (AtFullCapacity(noun, second)) return L__M(##PutOn, 6, second);

    move noun to second;

    if (AfterRoutines()) return;

    if (second ~= ancestor) {
        action = ##Receive;
        if (RunRoutines(second, after)) { action = ##PutOn; return; }
        action = ##PutOn;
    }
    if (keep_silent) return;
    if (multiflag) return L__M(##PutOn, 7);
    L__M(##PutOn, 8, noun, second);
];

Include "Grammar";


Class Room
    has light;

Room Start "Starting Point"
    with    description
                "An uninteresting room.";

Object ham_slice "slice of ham" Start
    with    name 'slice' 'ham'
    has edible;

Object bread_slice "slice of bread" Start
    with    name 'slice' 'bread'
    has edible supporter;

[ Initialise ;

    location = Start;

];
2 Likes

You are missing a 3rd object: the ham sandwich.

Object ham_slice "slice of ham" Room
    with
      name 'slice' 'ham',
      before [;
         Insert, PutOn:
            if (second == bread_slice) {
               remove self;
               remove bread_slice;
               move ham_sandwich to player;
               "I have a tasty sandwich now.";
            }
      ],
    has edible;

Object bread_slice "slice of bread" Room
    with name 'slice' 'bread',
    has edible;

Object ham_sandwich "ham sandwich"
   with name 'ham' 'sandwich',
   has edible;

(The code can be enhanced.)
Updated.

bread_slice can capture the ##Receive action in its before-routine and make the action happen manually. It can either just put the ham on the bread, or rename itself to “ham sandwich” or remove itself and the ham and put a ham sandwich in the player’s inventory instead.

Indeed, there’s nothing resembling this logic in the I7 Standard Rules (except to say that supporters are not by default portable)

Furthermore, the purpose is perhaps even more opaque than you suggest, since if for example the bread_slice and the ham_slice are both on a tray which is carried by the actor, the common ancestor of the bread_slice and the ham_slice will be the tray, not the actor, and the action will be permitted… a bit like the later stages of Up Up Up With A Fish in The Cat In The Hat….

Good point. (And I think that pushes it out of “quirk” territory and into “bug”. Perhaps @DavidG will be interested.)

Actually, perhaps not, since the ham_slice will be first subject to an implicit take in

if (noun notin actor && ImplicitTake(noun)) return L__M(##PutOn, 1, noun);

after which the common ancestor will be the actor :grimacing:

I think I can make this work without fiddling with the library. Stand by for a test game.

(edit)

Ohhh… I see it now. It’s currently not possible to put something on a tray that you’re carrying. I’ll fix it when I’m fully awake.

2 Likes

This took a bit more time than I thought it would, and it’s a bit fragile. But I’m confident that it can be adapted to handle any number of different fillings and remove the fragility. The deal with the auto-closing breadbox is because I didn’t want to mess with scope to select the obvious choice of what slice of bread to put the ham on when typing PUT HAM ON BREAD and what slice of bread to take when going for the top slice.

To make a sandwich:

OPEN BREADBOX
TAKE BREAD
PUT BREAD ON PLATE
TAKE HAM
PUT HAM ON BREAD
TAKE CHEESE
PUT CHEESE ON HAM
OPEN BREADBOX
TAKE BREAD FROM BREADBOX
PUT BREAD ON CHEESE
EXAMINE SANDWICH
EAT SANDWICH

The sandwich is an openable container because of fiddling around and testing. Maybe a sandwich could be started and then an open sandwich is swapped into play. Put the ingredients into the sandwich and then close it. Food for though (heheh).

And now the game. Feel free to use it in your own work.

Click to expand!
!% $OMIT_UNUSED_ROUTINES=1
Constant DEBUG;
Constant Story "THE SANDWICH MAKER";
Constant Headline "^Adventures in Handheld Meal Creation.^";
Include "parser.h";
Include "verblib.h";

Class Bread
	with name 'bread' 'slice' 'slices//p',
	short_name "slice of bread",
	plural "slices of bread",
	capacity 1,
	before [;
	Receive:
		if (noun ofclass Bread)
			"You can't put a slice of bread on another slice of bread.  That's not a good sandwich.";
	],
	after [i j;
	PutOn:
		if (second ofclass Filling) {
			i = parent(second);
			j = parent(i);
			if (i ofclass Bread) {
				move Sandwich to parent(i);
				move second to Sandwich;
				remove i;
			} else if (j ofclass Bread) {
				move i to Sandwich;
				move second to Sandwich;
				move Sandwich to parent(j); 
				remove j;
			}
			"You have made a sandwich!";
		}
	],
	has supporter edible;

Object Sandwich "sandwich"
	with name 'sandwich',
	description [;
		print "It's a delicious sandwich made with ";
		if (child(self)) {
			WriteListFrom(child(self), ENGLISH_BIT);
			"";
		}
	],
	has edible container transparent openable;

Class Filling
	with name 'slice' 'slices//p',
	capacity 1,
	has supporter edible;

Class Ham
	class Filling
	with name 'meat' 'ham' 'slice' 'slices//p',
	short_name "slice of ham",
	plural "slices of ham",
	description "A delicious slice of ham.";

Class Cheese
	class Filling
	with name 'cheese' 'slice' 'slices//p',
	short_name "slice of cheese",
	plural "slices of cheese",
	description "A delicious slice of cheese.";

Object Shop "The Sandwich Shop"
	with description "You're in a brightly-lit sandwich shop.",
	has light;

Object Table "table" Shop
	with name 'table',
	description "A table with lots of sandwich ingredients.",
	has supporter;

Ham -> ->;
Cheese -> ->;

Object -> Breadbox "breadbox"
	with name 'breadbox' 'box',
	description "An auto-closing breadbox.",
	after [;
	LetGo:
		give self ~open;
		"After you take ", (the) noun, ", ", (the) self, " automatically closes.";
	],
	has openable container;

Bread -> ->;
Bread -> ->;
Bread -> ->;
Bread -> ->;
Bread -> ->;
Bread -> ->;

Object Plate "plate" Table
	with name 'plate',
	capacity 1,
	before [;
	Examine:
		<<Search self>>;
	],
	has supporter;

[ Initialise;
	location = Shop;
	"Another fine day in the sandwich shop.";
];
Include "grammar.h";
1 Like

Thank you all for your effort, especially @otistdog and @DavidG for taking the time to write this stuff down!

The question is, is there a logical reason why the player shouldn’t be able to put objects onto portable containers? At first glance, it seems the reverse should be the default - that one would need to specifically prevent the player from performing such action.
Especially on 8bit retro platforms, where every custom routine uses precious bytes.

EDIT: I did a quick test with PunyInform library and it does allow placing objects on portable containers.

1 Like

Ok, people, between this thread and that other one (“Rooms is actually a common last name”), are you trying to get me to rupture a lung?

Coding can be hilarious!

Personally, I was surprised to learn that this kind of action was forbidden, but it does seem to be intentional. Interestingly, the relevant check has changed a little over time:

StdLib 6/1 to 6/4

if (parent(second)==player) return L__M(##PutOn,4);

StdLib 6/5 to 6/11 (EDIT: 6.12 is different, see DavidG’s post below)

if (ancestor == player) return L__M(##PutOn,4);

The difference would seem to be that the newer version prevents the “items on a tray” type of scenario suggested by @drpeterbatesuk.

Note that the Replace directive prevents compilation of the original PutOnSub, so using the alternate should not result in a significantly different level of memory consumption.

I have filed an issue at https://gitlab.com/DavidGriffith/inform6lib/-/issues/119 to address this problem of putting an object on top of a carried object. Currently I’m working out problems with picking up and dropping things in https://gitlab.com/DavidGriffith/inform6lib/-/issues/88

(edit)

Issue 88 is now closed.

The George Rooms problem reminded me of Little Bobby Tables. Whether or not Rooms is a common surname, that affair did expose a legitimate problem that deserves better treatment than “don’t do that”. And of course, it is funny.

2 Likes

Right. And now I’m off spelunking the forum (or the web?) for “Little Bobby Tables”.

1 Like

Whaddaya know! It’s from xkcd.com. My favorite webcomic!

image

1 Like

Hey, looks like you’re one of today’s lucky 10000. :)

3 Likes

Looking at the original question: putting an object on top of a carried supporter… Looking at the Library code going back to 6/1, it seems that all the time the intent was to disallow this. I don’t know why it worked for 6/1 through 6/4 and there are compatibility problems with trying to use those old libraries now. Here is the offending code again:

if (ancestor == actor)      return L__M(##PutOn, 4, second);

Prior to 6/12, player was used instead of actor. This change allowed for more flexibility with NPCs. I can’t think of any other reason why this code would be there.

There is more. In DropSub() is this right at the start:

if (noun == actor)         return L__M(##PutOn, 4, noun);

As far as I can tell, there’s no way for noun to be equal to actor because all grammar that calls DropSub() requires the noun to be held. I suppose I could keep it around just in case. In the meantime I’ll just delete the offending line from PutOnSub().