[i7] Generating a random map at start of play

OK, here’s an attempt, with lots and lots of comments. As I say in the comments, I made some questionable design decisions, notably having the map placement done in a “to decide” phrase (so the phrase “if locus can be placed” actually puts locus on the map). But it does include a general way of putting rooms in and a method for making exceptions for special rooms as well.

"random generated dungeon" by Matt Weiner

A chamber is a kind of room. Some chambers are defined by the Table of Dungeons. [The chambers are the rooms that get placed randomly by the standard procedure. The Table just lets us define a bunch of them at once without having to say "Foo is a chamber." See 15.16.]

A room has a number called the maximum exits. The maximum exits of a room is usually 4. [Does what you think -- lets you make sure that certain rooms can have no more than a certain number of exits.]

Egress relates a room (called the place) to a direction (called the way) when the room-or-door the way from the place is not nothing. [Which will be true whenever you can go that way on the map. Putting doors in place randomly is pretty much impossible, but you could have a set pair of rooms with a door between them if you wrote appropriate routines for them.]
The verb to open to (it opens to, they open to, it opened to, it is opened to, it is opening to) implies the egress relation. ["Open to" and its conjugations will be getting all the work in the source code.]

After looking: say "You can go [list of directions opened to by the location]." [If we have procedurally generated exits, we'll need a procedural way of telling the player about them. If you had more detailed room descriptions you could fit something like this in the room description if you wanted, though it might be wise to include a default that did this if you hadn't written any other description of the exits.]

Table of Dungeons
chamber	maximum exits	description
Ordinary Room	3	"Nothing of interest here."
Bland Room	3	"Less of interest here."
Crossing Room	4	"This room appears to be central."
Cul-De-Sac	1	"Better go back the way you came."
Dead End	1	"Nothing happening here."
Interesting Room	4	"This room is interesting!"
Coffin Room	3	"Some coffins etc."
Dragon Room	2	"There is a fearsome dragon here guarding the treasure on the other side!"
Ho-Hum Room	3	"Yeah, it's a room."
Side To Side Room	2	"This room may or may not take you to another room."
Possible Branch Room	3	"Maybe this room will branch?"
Other Possible Branch Room	3	"Or this one might branch?"

[These rooms won't be placed by quite the same algorithm.]
The Treasure Room is a room. "Treasure, oh yeah!" The maximum exits of the Treasure Room is 1.
The Start Room is a room. "Here begins your adventure." The player is in the Start Room. The maximum exits of the Start Room is 4.
The Exit To The Next Level is a room. "You can get to the next level from here." 

A room has a number called x-coordinate. A room has a number called y-coordinate. [The x and y coordinates let us avoid having rooms clash or other impossible geometries.]
The x-coordinate of a room is usually -99. The y-coordinate of a room is usually -99. [Signifying, "off the map."]

The x-coordinate of the Start Room is 0. The y-coordinate of the Start Room is 0.

A room can be placed or unplaced. A room is usually unplaced. The Start Room is placed. [We need to keep track of which rooms we've actually put on the map.]
A room can be tried or untried. [And this will be used when we're putting a room on the map, to figure out which rooms we've placed in next to.]

[Everything else is a giant cascade of user-defined phrases...]
When play begins: construct the dungeon. [This is the big one.]

[Now the tricky and perhaps unwise thing is that I've defined a phrase that tests whether a room can be placed on the map -- and if it can, it places it. So when you hit a phrase that tests whether the room can be placed, at the end of the test if it can be placed it *is* placed, with its coordinates set and the map connections made and everything. Running the "if" clause actually makes changes.
Ordinarily it would make more sense to call a phrase to place the chamber instead. But the issue here is that we need to know whether we actually succeeded in placing the chamber. So you can think of this as really an instruction to place the chamber if you can, and report back whether you could or not.
The Kerekruip code takes care of the problem by defining a rulebook for placing rooms in the dungeon; when you define a rulebook, you can give it outcomes, so if you run the placing rules for a room (or whatever they're called) you can look at the outcome to see if the room successfully got put on a map. This would probably be a wiser approach for a more complex project.]

To construct the dungeon:
	while a chamber is unplaced: [This loop will repeat until every chamber is placed. In a lot of applications you would just do "repeat with locus running through chambers" instead, but that would always do everything in the same order; often the order they're defined in the source code. In this case we want the order to be truly random. Another way to do this would be to put the chambers in a list and sort them in random order.]
		let locus be a random unplaced chamber;
		say "Placing [locus].";
		if locus can be placed: [Here's the test that actually amounts to an instruction to place the locus if you can, and tell us whether you succeeded.]
			now locus is placed; [I think this winds up being redundant because the code for testing whether the locus can be placed actually sets the placed property. But better safe than sorry. If we never set the placed property it'd be bad, because we'd loop infinitely.]
		otherwise:
			say "resetting.";
			reset the dungeon; [if we hit a brick wall placing a room, trash the map layout and start from the beginning]
	if the exit to the next level can be placed: [and we place the exit last, so it'll on average wind up farther away]
		say "Succeeded in making the dungeon!";
	otherwise:
		say "Resetting.";
		reset the dungeon.
			
To reset the dungeon:
	repeat with locus running through rooms:
		if locus is not the Start Room, remove locus from the map; [removing it from the map is a custom phrase; see below]
	construct the dungeon.
	
To remove (locus - a room) from the map: [this is meant to make everything the way it was before. It's not really tested, since I've never been unable to place a room]
	now locus is unplaced;
	now the x-coordinate of locus is -99;
	now the y-coordinate of locus is -99;
	repeat with the way running through directions:
		if the room-or-door the way of locus is not nothing:
			change the opposite of the way exit of the room-or-door the way of the locus to nowhere; [this syntax is pretty complicated ; since "the opposite of the way" is a direction, it resolves to something like "change the north exit of the next room to nowhere," which is to say, break the map connection]
			change the way exit of the locus to nowhere.

[A "to decide whether" phrase runs until it hits a "yes" or a "no" and then returns that as the answer. (If it reaches the end without making a decision, the answer is "no.")]
To decide whether (locus - a room) can be placed:
	now every room is untried;
	while a placed room is untried: [This will go through all the placed rooms, trying them one by one, until we find one we can put the locus next to. Again, we don't want "repeat through placed rooms" because that'll always do the same order. We could put the placed rooms in a list and sort it in random order, then repeat through that.]
		let entryway be a random untried placed room; [this creates "entryway" as a temporary variable]
		now entryway is tried;
		say "trying [entryway].";
		if the number of directions opened to by entryway is at least the maximum exits of entryway: [entryway is already "filled up," so we don't try it]
			next; [This stops the iteration of the "while" loop, so we go on to pick another untried placed room -- and the one we just tried is now marked as tried]
		otherwise:
			if the locus can be placed next to the entryway: [this calls a phrase below, which not only checks to see if the locus can be placed to the entryway, it actually places the locus if it can. So if the below block of code is executing, we've already placed the locus]
				[now let's see if we can open a connection to an already placed room]
				repeat through the Table of Cardinal Directions: [This looks at one row of the Table at the time; when we say things like "direction entry" after this it means "the direction entry in this row." The Table should have been randomly sorted by some code we're running; we could sort it randomly again but I'm not sure it makes a difference]
					if the number of directions opened to by locus is at least the maximum exits of locus: [the locus is filled up, so we don't need to check anymore]
						break; [this ends the repeat loop completely; see 11.12]
					otherwise if the room-or-door the direction entry of the locus is not nothing: [we've already made a map connection in the way we're looking]
						next; [this ends this iteration of the repeat loop and goes on to the next; again, see 11.12]
					otherwise if a room is mapped at (the x-coordinate of the locus plus the x-increment entry) by (the y-coordinate of the locus plus the y-increment entry): [this checks the map grid to see if there's a room there that we could open a connection to. note that "entry" means "entry in the chosen row of the Table of Cardinal Directions," which will correspond to the direction we're checking]
						let the candidate be the room mapped at (the x-coordinate of the locus plus the x-increment entry) by (the y-coordinate of the locus plus the y-increment entry); [we just ran through all the rooms looking for this one again; if efficiency were an issue we'd need to not do that, but at least we've created "the candidate" as a local variable so we don't need to keep doing it]
						if a random chance of 1 in 2 succeeds and the number of directions opened to by the candidate is less than the maximum exits of the candidate: [if both rooms can admit another exit, flip a coin to see if you want to make the connection]
							say "Connecting [locus] to [candidate].";
							change the direction entry exit of the locus to the candidate;
							change the opposite of the direction entry exit of the candidate to the locus;
				yes; [it might be hard to follow the indentation, but this is after the "repeat" loop and inside the "if the locus can be placed next to the entryway" clause. So, no matter what happens when we try to make additional map connections, we want to say "Yes, the locus was successfully placed on the map." This ends processing of this phrase.]
	no.	[And this one is outside and after the "while a placed room is untried" loop. So if we get here we've tried to put the locus next to every room that's on the map already, and the verdict is that the locus can't be placed.]
	

[But we might want some rooms, or special kinds of room, to have special placement rules. Here I've written a rule for the Dragon Room, which has to have the Treasure Room placed next to it.
To have an exception to the general rules for "To decide whether foo can be placed," just write a phrase that has something more specific in the header. Here the variable we use is as specific as possible: It only applies to the Dragon Room! When asked to decide whether a room can be placed, Inform will run the most specific phrase it can run that matches that room.]
To decide whether (locus - the Dragon Room) can be placed:
	now every room is untried; 
	while a placed room is untried: 
		let entryway be a random untried placed room;
		now entryway is tried;
		say "trying [entryway].";
		if the number of directions opened to by entryway is at least the maximum exits of entryway:
			next;
		otherwise:
			if the locus can be placed next to the entryway: [so far all this is as before, and at this point the Dragon Room will be on the map. But we still have to see whether we can put the Treasure Room next to it]
				sort the Table of Cardinal Directions Copy Two in random order; [I think we might need another copy of the table here so we don't mess up the loop through the Table of Cardinal Directions that we're still repeating through when we try to place the Dragon Room]
				repeat through the Table of Cardinal Directions Copy Two:
					say "Trying the Treasure Room to the [direction entry].";
					unless a room is mapped at (the x-coordinate of the locus plus the x-increment entry) by (the y-coordinate of the locus plus the y-increment entry):
						place the Treasure Room to the direction entry of the locus;
						now the Treasure Room is placed;
						now the x-coordinate of the Treasure Room is the x-coordinate of the locus plus the x-increment entry;
						now the y-coordinate of the Treasure Room is the y-coordinate of the locus plus the y-increment entry;
						say "succeeded in placing Treasure Room.";
						yes;
				say "Failed to place the Treasure Room."; [at this point we have repeated through the whole Table of Cardinal Directions Copy Two without placing the Treasure Room next to the Dragon Room, which would hit a "yes" and so terminate this whole "to decide" phrase, so we have to remove the Dragon Room from the map and start over with a new location for it]
				remove the locus from the map; [I think this might actually miss a case -- say we provisionally place the Dragon Room east of the Bland Room but are then unable to place the Treasure Room. I think we now go to the next "while a placed room is untried" entry having marked Bland Room as tried, without seeing whether we might be able to place Dragon Room west of the Bland Room and then place the Treasure Room off of that. If that were to cause trouble, we'd need to figure out a way to change that.]
	no.	[Again, this is after the "while a placed room is untried" loop, so we only hit it once we've tried every placed room.]
	

[The next phrase is the "to decide" phrase that actually does stuff. It looks for available spots next to the old room, and if it finds one, it actually puts the room there.]
To decide whether (new room - a room) can be placed next to (old room - a room):
	sort the Table of Cardinal Directions in random order; [We need to do this so we aren't trying the directions in the same order every time.]
	repeat through the Table of Cardinal Directions:
		say "Trying [direction entry].";
		unless a room is mapped at (the x-coordinate of the old room plus the x-increment entry) by (the y-coordinate of the old room plus the y-increment entry): [checking the map grid]
			place the new room to the direction entry of the old room; [this is a custom phrase; see below]
			say "succeeded with [direction entry].";
			now the new room is placed;
			now the x-coordinate of the new room is the x-coordinate of the old room plus the x-increment entry;
			now the y-coordinate of the new room is the y-coordinate of the old room plus the y-increment entry; [we've marked the room as being on the map, given it its map connections, and recorded its grid location]
			yes; [again, this terminates the phrase, saying we succeeded]
	no. [This will happen if we've gone through all the directions without getting to "yes."]

To place (new room - a room) to the (way - a direction) from/of (old room - a room):
	change the way exit of the old room to the new room; [this is a rather opaque formulation -- if the way is north it amounts to "change the north exit of the old room to the new room," that is, once you go north from the old room you'll get to the new room. See section 8.5; this is a special formulation because "now the new room is mapped the way of the old room" doesn't work for apparently complicated reasons. (I forgot about this formulation and it nearly drove me to distraction.)]
	change the opposite of the way exit of the new room to the old room. [And this is similar, except now the direction is "the opposite of the way"; so if the way is north, this amounts to "change the south exit of the new room to the old room." When you're changing exits by hand like this, you have to change both of them to ensure that you don't get a one-way connection.]
			

To decide which object is the room mapped at (x - a number) by (y - a number): [we have to say "which object" rather than "which room" because the answer might be nothing]
	say "Testing [x], [y].";
	repeat with test room running through placed rooms:
		if the x-coordinate of test room is x and the y-coordinate of test room is y, decide on test room;
	decide on nothing.
	
To decide whether a room is mapped at (x - a number) by (y - a number):
	if the room mapped at x by y is nothing, no;
	yes.

Table of Cardinal Directions
direction	x-increment	y-increment
North	0	1
South	0	-1
East	1	0
West	-1	0

Table of Cardinal Directions Copy Two [we need another copy for when we need a loop within a loop]
direction	x-increment	y-increment
North	0	1
South	0	-1
East	1	0
West	-1	0