Pulling text from the input

Hello!

I’ve been working on a project which involves the player being able to build new structures (adding rooms to the map). This has been giving me difficulty, and I was hoping for some advice.

Right now, I cannot get the following action to work correctly. It continually asserts that “I can’t see any such thing”, even when I am entering the name from the table.
[rant=Code][code]A thing has a number called quantity. Quantity is usually 0. [Other actions can raise this; such actions work]
Dirt is a thing. Dirt is nowhere. The quantity of dirt is 0.

Understand “build [something]” as building.
Building is an action applying to one thing.
Check building:
if “[the noun]” is a name listed in Table 1:
let alice be the thing corresponding to a name of “[the noun]” in the Table of Buildable Rooms;
let bob be the amount corresponding to a name of “[the noun]” in the Table of Buildable Rooms;
if the quantity of alice is less than bob:
say “You do not have the requisite materials.” instead;

[Large portion of omitted code]

Table 1 - Buildable Rooms
Name Thing Amount Room
“Room” Dirt 5 Small Room[/code][/rant]
I believe the issue is that the definition “Building is an action applying to one thing.” insists that the player be able to view/touch the item in question. However, I do not want that functionality. I just need to use the text that the user entered.

Any help would be appreciated. Let me know if you would like more of the code (I tried to only put relevant parts). Thanks in advance.

If you want to make an action that applies to arbitrary text instead of to things that the player can see/touch, you can make it an action applying to a topic rather than a thing. Like this:

Understand "build [text]" as building. Building is an action applying to one topic.

And then instead of using “the noun” you can use “the topic understood.” And you can make the table have a Topic column rather than a Name column–that’s slightly more flexible (look up the section on Topic columns in the documentation). Also, once you run a “listed in” check on a table, Inform chooses the row of the table that you’ve found, so you can just cite “the thing entry” rather than going through the “thing corresponding to a name” formulation.

Putting it together:

[code]A thing has a number called quantity. Quantity is usually 0. [Other actions can raise this; such actions work]
Dirt is a thing. Dirt is nowhere. The quantity of dirt is 0.

Understand “build [text]” as building.
Building is an action applying to one topic.
Check building:
if the topic understood is a topic listed in Table 1:
let alice be the thing entry;
let bob be the amount entry;
if the quantity of alice is less than bob:
say “You do not have the requisite materials.” instead;
otherwise:
say “I don’t know how to build that.” instead.

Report building: say “Building report goes here.”

[Large portion of omitted code]

Table 1 - Buildable Rooms
Topic Thing Amount Room
“Room” Dirt 5 Small Room

Small Room is a room.[/code]

Now, if you really want to have the action just apply to rooms rather than to arbitrary texts, you can do it something like this: First, make your Understand line apply to “build [any room]” rather than “build [something]”. The “any” lets the action refer to any room, whether or not it’s in scope. Second, make the action apply to “one visible thing”; this (somewhat counterintuitively) means the action will work for anything you can refer to, whether or not you can touch it, while “one thing” means you have to be able to touch it. Combined, these two let the action work for any room whatsoever.

Then, if you’re doing this you should have the column list the rooms rather than names for them. (If you want a names column instead, just go with the approach I mentioned above and make you action apply to a topic.) So you don’t have quotes–what’s in the column is not the room’s name, but the room itself. So:

[code]A thing has a number called quantity. Quantity is usually 0. [Other actions can raise this; such actions work]
Dirt is a thing. Dirt is nowhere. The quantity of dirt is 10.

Understand “build [any room]” as building.
Building is an action applying to one visible thing.
Check building:
if the noun is a room listed in Table 1:
let alice be the thing entry;
let bob be the amount entry;
if the quantity of alice is less than bob:
say “You do not have the requisite materials.” instead;
otherwise:
blank out the whole row; [removes the row so we can’t build that twice]
otherwise:
say “That’s not a room you can build.” instead.

Carry out building:
change the north exit of the location to the noun.
Report building: say “[The noun] is now north of here.”

[Large portion of omitted code]

Table 1 - Buildable Rooms
Room Thing Amount
Small Room Dirt 5

Start Place is a room.

Small Room is a room.
[/code]

Hope this is helpful!

You can remove the touchability constraint by defining the action as applying to “one visible thing”, and you can place things that would normally be out of scope into scope for the action by using the grammar token [any thing] instead of [something].

However, if you just want to use arbitrary text that the player entered, then you want to define the action as applying to a topic and to use the [text] grammar token in your understand statement. Also, if a test for “is listed” succeeds, then the matching row is automatically chosen, so you can refer to the various columns in that row directly (e.g., as thing entry or amount entry).

A thing has a number called quantity. Quantity is usually 0. [Other actions can raise this; such actions work]
Dirt is a thing. Dirt is nowhere. The quantity of dirt is 0.

Instead of jumping:
	now the quantity of dirt is the quantity of dirt + 5;
	say "You leap into the air and dislodge some dirt."

Understand "build [text]" as building.
Building is an action applying to one topic.

Check building:
	if the topic understood is a name listed in Table 1:		
		if the built entry is true:
			instead say "You've already built a [the topic understood].";
		if the quantity of thing entry < amount entry:
			instead say "You do not have the requisite materials.";
	otherwise:
		instead say "You don't know how to build a [the topic understood]."
		
Carry out building a name listed in Table 1:
	now the built entry is true;
	now the quantity of the thing entry is the quantity of the thing entry - the amount entry;
	say "[bracket][thing entry] quantity is now [quantity of thing entry].[close bracket]";
	change the way entry exit of from entry to room entry;
	change the opposite of the way entry exit of room entry to from entry.

Report building:
	say "You construct a [the topic understood]."

Table 1 - Buildable Rooms
Name (Topic)	Thing	Amount	Room	From (Room)	Way (Direction)	Built (truth state)
"Room"	Dirt	5	Small Room	Start Room	south	false	

Start Room is a room.

Small Room is a room.

Test me with "build duck / s / build room / jump / build room / build room / s / n".

Edits:

  • Realized that it’s the Table of Buildable Rooms, not the Table of Various Kinds of Buildable Objects Including Rooms.
  • Added “Built” column to handle the already built case (thanks matt w). I did this rather than blank out the row, so that we can distinguish between things that we’ve already built and things that we’ve never known how to build.
  • Combined the check rules into one to avoid walking through the table’s rows multiple times during checking. If it mattered, we could also carry the matching row forward as an action variable into the carry out stage. I’m not aware of any predefined “get the current row number” phrase, but we could abandon the “listed in” test and manually iterate through the table’s rows looking for a match while taking note of the row index.

By coincidence, I wrote an extension along that line for myself around a week ago. The extension was intended as a simplified code generation tool for Inform 7 (allowing you to create and destroy new rooms, add kinds, remove things etc., alter descriptions etc…). It used Object Kinds by Brady Garvin to allow you to create instances of the correct kind seamlessly. Then, you’d write “view source”, get some source code representing the current game state, and iterate from there.

Unfortunately, since I’ve found no aesthetically pleasing way to represent the real name of the objects in the generated source code, the project currently sits semi-abandoned. I’d be happy to share it with the community if there’s any interest to that effect.

Thanks all! It is now working. Huzzah!

I lied. It is not entirely working, but the new issue is unrelated to the previous one.

Does Inform allow you to change how many rooms exist while the game is running? I ask because when you try to build a room in two different locations, things get messed up (you can enter the building from either location, but can only exit into the place you built in most recently). I’ve been poking around in the documentation some, and it doesn’t seem to have anything about creating new rooms during runtime.

If this is impossible, I can do some coding shenanigans with variables to get a single room to seem like it’s in multiple places at once, but that would be a pain. So I figured I would check to see if there is more wisdom I can tap into.

Not really. The best way (as used in Dynamic Rooms) involves making a bunch of room objects and swapping them in and out at runtime.

The Glulx VM supports real dynamic allocation of objects, but Inform doesn’t take advantage of this by default.

The technique that’s most commonly used is to create a number of duplicate objects offstage and to bring them into play as they are created in the game world. See §4.14 of Writing with Inform and §3.11 of the DM4 for more on this technique.

Here’s an extension of the earlier program that creates a pool of 100 “blank” rooms and allocates and places them as needed:

A thing has a number called quantity. Quantity is usually 0. [Other actions can raise this; such actions work]
Dirt is a thing. Dirt is nowhere. The quantity of dirt is 0.

Instead of jumping:
	now the quantity of dirt is the quantity of dirt + 5;
	say "You leap into the air and dislodge some dirt."

Understand "build [text] to [direction]" as building.
Building is an action applying to one topic and one visible thing.

Check building when room-or-door second noun from location is not nothing:
	instead say "There's already something to the [second noun]."

Check building:
	if the topic understood is a name listed in Table 1:		
		if the built entry >= max entry:
			instead say "You can't build another [the topic understood].";
		if the quantity of thing entry < amount entry:
			instead say "You do not have the requisite materials.";
		if the number of free dynamic rooms is 0:
			instead say "All dynamic rooms are in use. Try again later.";
	otherwise:
		[This is not ideal. TODO: figure out how to programatically apply correct indefinite article to a topic/text.]
		instead say "You don't know how to build a/an [the topic understood]."
		
Carry out building a name listed in Table 1:
	now the built entry is the built entry + 1;
	now the quantity of the thing entry is the quantity of the thing entry - the amount entry;
	allocate prototype entry mapped second noun from location.

Report building:
	say "You construct a/an [the topic understood]."

Table 1 - Buildable Rooms
Name (Topic)	Thing	Amount	Prototype (Prototype Room)	Built (number)	Max (number)
"Armory"	Dirt	5	Armory	0	2
"Barracks"	Dirt	3	Barracks	0	99

[Define this before other stuff so that the player doesn't start in a dynamic or prototype room.]
Start Room is north of Other Room.
The ball is a thing in start room.
The rock is a thing in start room.

[These are generic room objects that are repurposed to be any kind of room.]
A dynamic room is a kind of room.
A dynamic room can be allocated or free. A dynamic room is usually free.
There are 100 dynamic rooms.
[Used to test free routine.]
[There is 1 dynamic room.] 

[A prototype room represents a particular kind of room that we want to build.]
A prototype room is a kind of room.
Blank is a prototype room. [default room]
Armory is a prototype room. "This recently constructed armory is filled with weapons."
Barracks is a prototype room. "This recently constructed barracks is filled with guards."

[We create a region for each prototype room and create that room's scenery as backdrops in that region. Each new instance of this prototype will be in this region because its I6 map_region property will be copied during allocation.]
Armory Region is a region. Armory is regionally in Armory Region.
Barracks Region is a region. Barracks is regionally in Barracks Region.
Some weapons are a backdrop regionally in Armory Region. The description is "The weapons are neatly organized in racks."
[I7 doesn't expose multiclassing, and we want the guards to be both a backdrop and a person, so we include a snippet of I6 to make that happen.]
Some guards are a backdrop regionally in Barracks Region. Include (- class (+ person +), -) when defining the guards. The description is "The guards are sleeping."

Include (-
[CopyAttribute src_obj dst_obj attribute;
  if (src_obj has attribute) {
      give dst_obj attribute;
  } else {
      give dst_obj ~attribute;
  }
];
-)
  
[This copies relevant room properties from a prototype room to a "blank slate" dynamic room.]
[Note: fully copying the prototype room's properties would erroneously overwrite fields such as IK1_count and IK1_link that are part of the dynamic room's identity as a distinct room from the prototype room.]
To copy (P - prototype room) to (D - dynamic room):
(-
	{D}.description = {P}.description;
	{D}.map_region = {P}.map_region;
	{D}.list_together = {P}.list_together;
	{D}.short_name = {P}.short_name;
	{D}.plural = {P}.plural;
	{D}.article = {P}.article;
	CopyAttribute({P}, {D}, light);
	CopyAttribute({P}, {D}, privately_named);
	CopyAttribute({P}, {D}, visited);
-)

[This grabs a free dynamic room and turns it into a copy of a prototype room using the info in Table 1.]
To allocate (P - prototype room) mapped (way - direction) from (from - room):
	let new room be a random free dynamic room;
	if new room is nothing, stop; [shouldn't happen; we checked earlier.]
	now new room is allocated;
	copy P to new room;
	change way exit of from to new room;
	change opposite of way exit of new room to from.
	
[This reverses the process and frees a room. Unused in this example and only slightly tested.]
To free (D - dynamic room):
	repeat with way running through directions:
		let adj be room-or-door way from D;
		if adj is nothing, next;
		change way exit of D to nowhere;
		change opposite of way exit of adj to nowhere;
	copy Blank to D;
	now D is free.

[Commented-out code used to test free routine.]	
[When play begins:
	allocate Armory mapped east from Start Room;
	free the room east from Start Room;
	allocate Barracks mapped north from Start Room.]
	
Test me with "take all / build armory to s / e / build armory to e / jump / build armory to e / e / x weapons / x guards / drop ball / w / build armory to e / n / jump / build armory to n / n / x weapons / w / drop rock / l / s / e / w / w / build armory to w / jump / build barracks to w / w / x guards / x weapons / kiss guards".

Edit: The initial code that I posted copied all room properties from the prototype room to the dynamic room. This included copying internal properties that distinguish a room from other rooms such as its Room instance count (IK1_count) and its link to the next room in the list of rooms (IK1_link). This resulted in all of the rooms created from a particular prototype sharing map connections since a room’s instance count is used to index into the MapStorage data structure in the I6 template code.

I’ve since changed the copying code to only copy a subset of properties and attributes. I’m not yet sure that this is the full set of properties and attributes that would need to be copied over.

Also, if additional properties are added to a prototype room, or relations added that involve one, then the code will need to be extended to handle them also.

Another approach that I considered was to save the values of fields that should not be overwritten, copy the complete prototype room, and then to restore those fields to their original values that we saved.

It would most likely be possible to do the copying at the I7 level. The reason that I originally chose the I6 level was that I thought that I could use Room.copy() to do a convenient blanket copy without any ill-effects.

Picking this out because this is a minor obsession of mine, but this isn’t too tough to do if you’re applying it directly to a text variable. (If you’re applying it to a substitution like “[one of]red[or]orange[or]green[at random] ball” it gets much nastier.) You can just pass the topic to a phrase that tests whether the first letter is a vowel sound. Here’s an example:

[code]Lab is a room. Anna is a woman in the lab.

To decide what text is a-an (string - text):
if the string starts with a vowel sound:
decide on the substituted form of “an [string]”;
otherwise:
decide on the substituted form of “a [string]”.

Instead of answering Anna that something:
say “‘Oh, [a-an topic understood],’ says Anna.”

To decide whether (string - a text) starts with a vowel sound:
let the first word be word number 1 in string;
if the first word is a word listed in the Table of Words That Start With Vowel Sounds, yes;
if the first word is a word listed in the Table of Words That Don’t Start With Vowel Sounds, no;
if character number 1 in the first word is a vowel, yes;
no.

To decide whether (letter - a text) is a vowel:
if letter exactly matches the regular expression “a|e|i|o|u|A|E|I|O|U”, yes;
no.

Table of Words That Start With Vowel Sounds
word
“hour”
“hourglass”
“honest”
“yttrium”

Table of Words That Don’t Start With Vowel Sounds
word
“uniform”
“unicorn”
“united”
“United”
“one”[/code]

Now, if you’re dealing with absolutely free-form input you’re going to get some mistakes when the player types in an exception that isn’t on the list (like “hourglass” or “UPS truck”). But this works decently well, and I doubt there’s going to be any way of resolving that issue without typing in the dictionary no matter what approach we take.

What do you mean by this? Is it not possible to make it look like regular code? e.g.

[code]Lab is a room.

A box is a container. The box is in Lab.[/code]

Well, the big issue is that you may create code, like so:

…and that will give you the proper object. When you view the code, the generated code will have something along the lines of

The relique is a thing. The printed name of the relique is "Tuthmose's Scepter".

So far so good. But when you copy and paste that into your code (as I envisioned the ideal workflow loop to be), and compile, the engine will have no idea of the Relique moniker, so it will instead try to grab the most salient-seeming word from the printed name. Not ideal, but I don’t know how to improve it without adding the kind of boilerplate I wanted to avoid in the first place.

Draconis’s suggestion of multiple versions of the same room is probably the standard way to do this. He also helped me with an extension called “Easy Doors” which makes it so you can change where doors lead without messing around with map directions. (“Now the sandy passageway leads to Secret Pyramid Room.”) If this would help at all, it’s in the public library.

Alright. Slightly different solution than I saw above. Sorry for the wall of code below.

This is a complete program (if you copy it into Inform, it will run). There are a ton of comments so that things make sense. Basically, I have a couple of ‘template’ rooms. To store map connections, I have a table at the bottom (right now it only stores n/s/e/w). That can be expanded to also include descriptions for each room. Then I store a location ID to keep track of which copy of the template room you’re in. Finally, I override going so that I use my table instead of the normal rules governing movement.
If you play around with this, “build” is a command that will create a new Copy Room to the north of where you are (if there’s not a room there, and if there’s room in the table).
There’s a lot of expanding to be done, but this is the base framework for a relatively dynamic game. It’s not infinite, but it can seem that way, and it works with Z-Machine.
If you need it larger, just add more blank rows.

[code]“Room System Testing” by SanKeshun

Part 1 - Normalish Bits

Start Room is a room. “This is the room you start in.[connection description]”.
North Room is a room. “This is the room to the north.[connection description]”. North Room is north of Start Room.
Copy Room is a room. “You are in copy room [location ID].[connection description]”.

Part 2 - Stuff to Make Moving Work

Section 1 - Global Variables

Location ID is a number that varies. Location ID is initially 0.
Highest location ID is a number that varies. Highest location ID is initially 4. [This changes for a larger or smaller map. For auto-generated maps, start at 0 and increment it as you build]

Connection description is a text that varies. Connection description is initially “”.
To update the connection description:
now connection description is “[line break]”;
if Location ID is an ID listed in Table 1:
if there is a North entry:
now connection description is "[connection description][the printed name of North entry] is to the north. ";
if there is a East entry:
now connection description is "[connection description][the printed name of East entry] is to the east. ";
if there is a South entry:
now connection description is "[connection description][the printed name of South entry] is to the south. ";
if there is a West entry:
now connection description is "[connection description][the printed name of West entry] is to the west. ";
if connection description is “[line break]”:
now connection description is “”;
otherwise:
say “Something is seriously wrong; you’re not where you should be.”
When play begins: update the connection description.

Section 2 - Fixing Going

[First we allow ourselves to move even if Inform can’t see the connection]
The determine map connection rule is not listed in the check going rulebook.
The can’t go that way rule is not listed in the check going rulebook.

[Check out the map (below) if this bit doesn’t make sense]
Instead of going:
if Location ID is an ID listed in Table 1:
let D be the noun;
let saveNum be location ID;
[say “Beginning move in direction [D].”;]
if D is north:
if there is a North entry:
now location ID is the NID entry;
[say “Beginning qing…”;]
try qing;
otherwise:
say “You cannot go that way (Spot A).” instead;
now location ID is saveNum;
if D is east:
if there is a East entry:
now location ID is the EID entry;
[say “Beginning qing…”;]
try qing;
otherwise:
say “You cannot go that way (Spot B).” instead;
now location ID is saveNum;
if D is south:
if there is a South entry:
now location ID is the SID entry;
[say “Beginning qing…”;]
try qing;
otherwise:
say “You cannot go that way (Spot C).” instead;
now location ID is saveNum;
if D is west:
if there is a West entry:
now location ID is the WID entry;
[say “Beginning qing…”;]
try qing;
otherwise:
say “You cannot go that way. (Spot D)” instead;
now location ID is saveNum;
[say “New move is done.” instead;]
otherwise:
say “New move has issues! That room doesn’t exist!” instead.

Section 3 - Qing (or, the Extra-Fiddly Bits)

Understand “kikuvashtu” as qing. [these are random and made up; they mean nothing][it’s pronounced “queueing”]
Qing is an action applying to nothing.

Check qing: [Although, I think this is already safeguarded against. Whatever]
if there is no Base Room corresponding to an ID of location ID in Table 1, say “That room does not exist.” instead.

Carry out qing:
update the connection description;
move the player to the Base Room corresponding to an ID of location ID in Table 1; [locationID has been updated by this point]
[say “The player has now been moved to [the Base Room corresponding to an ID of location ID in Table 1].”;]
let theCurrentLocation be location; [Does this seem stupid? If you say yes, you’re wrong. It fixed some of my bugs. I don’t know how.]
choose row with ID of location ID in Table 1;
if there is a North entry:
now theCurrentLocation is mapped south of the North entry;
[say “Now [theCurrentLocation] is mapped south of [the North entry].”;]
if there is a East entry:
now theCurrentLocation is mapped west of the East entry;
[say “Now [theCurrentLocation] is mapped west of [the East entry].”;]
if there is a South entry:
now the location is mapped north of the South entry;
[say “Now [theCurrentLocation] is mapped north of [the South entry].”;]
if there is a West entry:
now the location is mapped east of the West entry. [if you uncomment the says, change that period to a semicolon]
[say “Now [theCurrentLocation] is mapped east of [the West entry].”;]
[say “Successfully carried out setting the room.”]

Section 4 - Building New Rooms

Understand “build” as building.
Building is an action applying to nothing.

Check building:
choose the row with id of location ID in Table 1;
if there is a North entry, say “There’s no room!” instead;
if the number of blank rows in Table 1 is 0, say “You have insufficient materials; you’ll need to destroy a building (which you can’t do).” instead.

Carry out building:
now highest location ID is highest location ID + 1;
choose a blank row in Table 1;
now Base Room entry is Copy Room;
now ID entry is highest location ID;
now South entry is the location;
now SID entry is location ID;
choose the row with id of location ID in Table 1;
now North entry is Copy Room;
now NID entry is highest location ID;
update the connection description.

Report building:
say “You build a new room to the north! Huzzah!”.

Section 5 - The Map

Table 1 - Map
Base Room ID North NID East EID South SID West WID
Start Room 0 North Room 1 Copy Room 2 Copy Room 3 Copy Room 4
North Room 1 – -- – -- Start Room 0 – --
Copy Room 2 – -- – -- – -- Start Room 0
Copy Room 3 Start Room 0 – -- – -- – --
Copy Room 4 – -- Start Room 0 – -- – --
with 10 blank rows[/code]