Building a text string, passing results, and do callbacks exist in I7?

I’ve seen variations on this, like folks building a list of things with a list of indexed text, but not quite this…

In I7, we often do things like:

To say sharon_travel_things:
    say "Sharon catches up to you[if Room_Crossing encloses the player], carefully crossing the river on the floating log[else if Room_Swimming_Hole encloses the player] at the swimming hole[else if Room_Railroad_Tracks] as you reach the railroad crossing[else if Room_Grassy_Field encloses the player] in the big grassy field[else if player is in Region_Dirt_Road] as you walk along the dirt road[end if]."

However, as this gets more complicated, especially with nested if-thens which I7 doesn’t allow here, I want to simply build up a text string, like I have often done in Python or JS:

def sharon_travel_things():
	build_text = "Sharon catches up to you"
	if location == "Room_Crossing":
		build_text += ", carefully crossing the river on the floating log"
	elif location == "Room_Swimming_Hole":
		build_text += " at the swimming hole"
	elif location == "Room_Railroad_Tracks":
		build_text += " as you reach the railroad crossing"
	elif location == "Room_Grassy_Field":
		build_text += " in the big grassy field"
	elif region == "Region_Dirt_Road":
		build_text += " as you walk along the dirt road"
	build_text += "."
	print(build_text)

This code obviously has a lot more clarity and is much more maintainable. What is the I7 equivalent with the minimum fuss?

Just put multiple say statements in the phrase:

To say sharon_travel_things:
    say "Sharon catches up to you";
    if Room_Crossing encloses the player:
        say " carefully crossing the river on the floating log";
    otherwise if Room_Swimming_Hole encloses the player:
        say " at the swimming hole";
    otherwise if Room_Railroad_Tracks:
        say " as you reach the railroad crossing";
    otherwise if Room_Grassy_Field encloses the player:
        say " in the big grassy field";
    otherwise if player is in Region_Dirt_Road:
        say " as you walk along the dirt road";
    say ".";

This is practically identical to the in-text if-thens, but now allows for nested if-thens.

3 Likes

That’s definitely no fuss alright. That works okay as long as you don’t have anything that ends a sentence.

I’m pretty sure, however, that I have use cases where I’d like to build a string. Is that not a thing? In fact, I pass the entire text to another action that queues the text along with other things to say.

This is the
	journey_sharon_walk_catching_up rule:
	queue_report "Sharon catches up to you[if Room_Crossing encloses the player], carefully crossing the river on the floating log[else if Room_Swimming_Hole encloses the player] at the swimming hole[else if Room_Railroad_Tracks] as you reach the railroad crossing[else if Room_Grassy_Field encloses the player] in the big grassy field[else if player is in Region_Dirt_Road] as you walk along the dirt road[end if]." at priority 3;

The idiomatic Inform way is to use a phrase. You can set any text variable or object property by

now T is "[sharon_travel_things]";

(BTW, you don’t need underlines in Inform. Multi-word identifiers almost never cause issues.)

If for some reason you need to fix the result of a phrase to preserve its output at a point in time, then you do

now T is the substituted form of "[sharon_travel_things]";

See chapter 20.7 of the Inform manual.

You can write multiple sentences in a phrase! You could just put the period at the end of each of those branches instead, I was just copying what the original did.

1 Like

Aside: I use camelcase identifiers because I have had conflicts in a bigger IF. I can carefully control what the parser responds to with Understand "what/who/-- ever", "whatever" as what_ever.

Can I add to T? For instance, here’s what I want to do, but can’t:

To decide which text is transition_text:
    now T is "Sharon catches up to you";
    if Room_Crossing encloses the player:
        add " carefully crossing the river on the floating log" to T;
    otherwise if Room_Swimming_Hole encloses the player:
        add " at the swimming hole" to T;
    otherwise if Room_Railroad_Tracks:
        add " as you reach the railroad crossing" to T;
    otherwise if Room_Grassy_Field encloses the player:
        add " in the big grassy field" to T;
    otherwise if player is in Region_Dirt_Road:
        add " as you walk along the dirt road" to T;
    add "." to T;
	decide on T.

You don’t need to think of printing a string or building a string as an either-or. You can write the above with a series of say statements and then instead of having let t be transition_text somewhere you can have let t be "[transition_text]", or, if it’s important to lock in the specific corresponding text at the moment instead of using whatever the result is at time of evaluation, let t be the substituted form of "[transition_text]".

But if you wanted to write it as a to decide what text, the straightforward way to concatenate strings in I7 is with adaptive text.

To decide which text is transition_text:
  let t be "Sharon catches up to you";
  if Room_Crossing encloses the player:
      now t is "[t] carefully crossing the river on the floating log";
  [...]
  decide on the substituted form of "[t].";

[Edited to note that this didn’t really say anything Dannii hadn’t already said; I’m just trying to emphasize how what he said really does constitute a satisfactory approach to the problem.]

2 Likes

I think what I’m struggling with is the portability of the results of a “to say” or “to decide” rule(? action? function?). I want to have some text constructed and then provide that text at the appropriate time in a more complicated rule.

At the risk of complicating my relatively simple question above, I guess I need to reveal what I’m trying to do. I have a set of actions/rules/functions/whatever that define a generalized NPC journey:

Part - NPC_Journeys

Chapter - The Properties

An npc_journey is a kind of thing.

An npc_journey can be in-progress.
	It is usually not in-progress.

An npc_journey can be run.
	It is usually not run.

An npc_journey has a person called the npc.

An npc_journey has a room called the origin.

An npc_journey has a room called the destination.

[How long has the NPC been in this location]
An npc_journey has a number called time_here.
	Time_here is usually zero.

[How long does the NPC want to wait at each location]
An npc_journey has a number called wait_time.
	Wait_time is usually 1.

[How long does the NPC wait if the player's not there]
An npc_journey has a number called max_wait.
	Max_wait is usually 3.

[Does the NPC wait for the player?]
An npc_journey has a truth state called waits_for_player.
	Waits_for_player is usually true.

[Did we arrive?]
An npc_journey has a truth state called arrived.
	Arrived is usually false.

An npc_journey has a rule called the interrupt_test.

An npc_journey has a rule called the action_at_start.

An npc_journey has a rule called the action_at_end.

An npc_journey has a rule called the say_before_moving.

An npc_journey has a rule called the say_after_waiting.

An npc_journey has a rule called the say_catching_up.

Chapter - The Mechanics

Every turn:
	repeat with this_journey running through every in-progress npc_journey:
		step_a_journey for this_journey;

To step_a_journey for (this_journey - an npc_journey):
	[ test for interrupt ]
	if npc_is_interrupted on this_journey:
		stop the action;
	[ set some local vars to make things easier ]
	let npc be the npc of this_journey;
	let origin be the origin of this_journey;
	let destination be the destination of this_journey;
	now this_journey is run;
	now npc of this_journey is described;
    [etcetera]

So the values for any particular NPC journey function almost like callbacks, in that the general mechanism calls the appropriate rule at the appropriate time, e.g., action_at_start or say_before_moving.

journey_lee_walk is an npc_journey.
	The npc is Lee.
	The origin is Room_Blackberry_Tangle.
	The destination is Room_Grassy_Field.
	The wait_time is 2.
	The max_wait is 10.
	Waits_for_player is true.
	The interrupt_test is the journey_lee_walk_interrupt_test rule.
	The action_at_start is the journey_lee_walk_start rule.
	The action_at_end is the journey_lee_walk_end rule.
	The say_before_moving is the journey_lee_walk_before_moving rule.
	The say_after_waiting is the journey_lee_walk_after_waiting rule.
	The say_catching_up is the journey_lee_walk_catching_up rule.

Which has worked very well. However, now I’m fine-tuning when some of these rules trigger. But in trying to turn some of the above say rules into “to say” actions, I’m struggling with passing values to the calling/containing function.

An npc_journey has an [what? action? rule? object?] called the say_before_moving.

And then how do I say it? I know that I7 is as strongly typed as any of the other dozens of programming languages I’ve used, but I never know what the types are or how to pass them as callbacks. My head hurts.

“To decide which text is …” phrases are kind of weird, I don’t think there’s any reason to use them over normal “to say” phrases.

To concatenate texts in Inform you use text substitutions:

now joined text is "[part a][part b]";

If you want to build onto the same string then you need to use “substituted form of” as the docs explain:

now the accumulated tally is the substituted form of "[the accumulated tally]X";

But in most situations there’s really no need to do that. Just use a “to say” phrase, it’s the simplest and most natural way to do it.

It’s hard to know what you’re trying to do with your npc journey idea, but I suspect that it would be more straightforward to use actions directly. In particular, you can have very specific and contextual report rules, so that if your concern is having different output depending on whether the NPC is departing or arriving at the player’s location, you can do that with different report rules. In fact you may not even need a new custom action, possibly adding various rules to the standard going action might be enough.

I think there are some existing extensions which help with giving NPCs sequences of actions to follow, but I’m not very familiar with them.

If you could provide a transcript showing what you would like then we could better advise you on how to implement it.

The attempt is to create a general NPC action defined by a few simple variables. What is the origin? What is the destination? Who is the NPC doing the action? Do they wait for the player? What do they say when they leave the player? Then these general actions can be triggered when scenes begin and end when the NPC arrives (or when the player joins the NPC at the destination).

The challenge I have with “to say” phrases is that I want more control over when it is said. So the general NPC journey rules can trigger these sayings at appropriate times.

The rules I was using work alright, since I can specify for each particular NPC journey a starting or ending or moving rule to the general NPC journey action. So maybe I will stick with that rather than turn those into “to say” functions(actions? whatever the general class of this thing is?).

EDIT: I clarified the title of the post since it drifted pretty far from the question I started with…

There are several ways to do callback-ish things in I7. Rulebooks, rules, activities, or to-phrases can all be passed as parameters or stored as properties of kinds. But values of kind text that include adaptive text can be meaningfully viewed as callbacks in and of themselves.

An npc_journey has a text called the say_before_moving.
The say_before_moving of journey_lee_walk is "[lee_before_moving]".

To say lee_before_moving: [...]

It sounds to me like you’re clear on how texts that include adaptive text are really functions waiting to be called, i.e., they’re Schrödinger’s Cats existing in a state of quantum wossname until some moment when the wave function collapses and you find out what they really are. I’m not sure, but I think you may be thinking that using to decide what text or not plays a role in whether you’re getting an actual fixed string of characters whose wave function has already collapsed. It doesn’t; that’s a red herring.

It’s normal to be confused by this: it’s confusing and the docs could do a better job of addressing it. It left me confused for longer than I wished, for sure. See my discussion of texts here along with @drpeterbatesuk 's follow-ups.

It’s helpful to know that “text” (text that includes adaptive text) and “indexed text” (ordinary strings) used to be different kinds and they still are, under the hood. You can test which is which with if t is substituted (what used to be called “indexed text”, a plain string) or if t is unsubstituted (what used to be “text” as opposed to “indexed text”, i.e., something much like a reference to a function).

So…

To say foo: say "foo".
To decide what text is bar: decide on "bar".

when play begins:
  let t be "plugh"; [ t is substituted ]
  now t is bar; [ substituted ]
  now t is "[foo]"; [ unsubstituted ]
  now t is "[bar]"; [ unsubstituted ]
  now t is the substituted form of t; [ substituted ]
  [ t would always be substituted after that, no matter what had been on the right-hand side of `substituted form of` ]

So it’s possible for a to decide what text phrase to return a substituted or unsubstituted text. But even if what it returns is a substituted text, as in to decide what text is bar above, one could use "[bar]" instead of bar and then one would have an unsubstituted text. You and I may know that the substituted form of "[bar]" will always be “bar”, but I7 doesn’t “know” that.

I hope that clarifies how you can get what you want when you want it.

(As you’ve described what you want out of npc_journey, it has sounded to me like it would have been a good candidate for a family of rulebooks, but it sounds like a whole different representation isn’t what you’re after.)

Argg. So simple. Filed for future use.

Very possibly, but even though I am doing relatively complex things with I7, I’m woefully uneducated on some of the basics. I spent 25% of my time with I7 wishing I had started with a more procedural language, like maybe I6.

It’s a small thing, but there’s one context where I have an active preference for to decide what text: a function with nested loops can end up easier to write given that decide on provides a return statement, thus giving a quick way out.

…and saying all that out loud makes me notice that I can reproduce that advantage in two lines thus:

To say finally (sv - a sayable value): say finally "[sv]".
To say finally (t - a text): (- return TEXT_TY_Say({t}); -)
2 Likes

oops, the sayable value case above doesn’t work for reasons that should have been obvious. This mostly works:

To say finally (V - value of kind K): (- return PrintKindValuePair({-strong-kind:K}, {V}); -).

but doesn’t exclude unsayable values. Not sure there’s a way around that.

1 Like

There’s also “stop”, the equivalent to return; (i.e. ending a function that doesn’t return a value).

2 Likes