Impossible Stairs postmortem (and, using Dialog)

I wanted to write about Impossible Stairs and using Dialog in general!

Origin of game

For several years now, I’ve offered a prize for IFComp where I make a small game in the world of the winner. I got the idea from Emily Short, who offered that prize in 2015, creating game The Mary Jane of Tomorrow in the world of Brain Guzzlers from Beyond. I don’t offer it every year because I leave gaps for my own projects.

The winner two years ago was Linus Åkesson, author of The Impossible Bottle. I was intrigued not only by his game, but by his language: Dialog.

I was working on my own game at the time (Grooverland), so I told him I’d get started in 2021. I said I’d like to make the game in Dialog, which he was happy for but said he didn’t require.

Early ideas

My first email to Linus had this basic idea:

Your game focused on space. What if the next game involved time?

A character from the main game (probably the same MC but maybe a parent or sibling) has a calendar or clock, and setting the clock or turning the pages changes what time they’re in.

I sketched out a small house, and tried to figure out what kind of mechanic to use.

I had to discard the clock and calendar, because it’s so fussy. Would people type “flip page” or “turn page” or “turn calendar” or “turn page to Wednesday” or “go Wed”? The Impossible Bottle was great because it did clever mechanics with only the basic verbs of the game (take, put, drop).

So I got the idea of stairs; going up and down the stairs would change the decade.

Using Dialog

Using Dialog was interesting. It’s the best language I’ve tried outside of Inform for the type of games I write (I’ve also tried Quest, Twine, Adventuron, and Adrift), and I think it’s equal to Inform.

It’s much more programmer-y. The main concept I had to wrap my mind around was a ‘predicate’, which is a bunch of words in parentheses. I wrote this guide to predicates in Dialog for a meeting of the Seattle IF Meet-Up:

Summary

Dialog uses statements in parentheses (),called ‘predicates’, as:

-function definitions and function calls

(getgrossed)
	"Ew!"
	(now)(#player sick)

(instead of [eat paper])
	(getgrossed)

(instead of [eat mud])
	(getgrossed)

-Conditional statements that stop everything after them

(instead of [say merry christmas])
	(month < 12)
		"It's not even Christmas yet!" shouts Scrooge.

-Local variable definition and assignment

(instead of [ponder $ponderThing])
	(player is #in $ponderRoom)

-acting as global boolean variables

(after [eat #muffin])
	(now)(muffingone)
	
(on every tick)

They can be inserted literally anywhere.

For instance:

(dict #muffin) cupcake
(dirty #muffin)
dirty

This makes ‘dirty’ a synonym for a muffin only if the muffin is dirty.
(End of Intro to predicates)

The predicate structure is elegant, but takes a while to get used to. I plan on posting my impossible stairs code; I tried to do it before on IFarchive but it didn’t go through, so I resubmitted it and my compiled file today.

Challenges

Dialog was amazing at some things, like white space. I think 5-10% of most of my Inform projects end up with chasing down whitespace bugs, but I never had a single one with Dialog.

It also has an amazing online integration and an excellent built-in conversation system.

My biggest challenge, though, was with global variables. Most variables are set and checked using local variables in predicates, and the global ones just didn’t seem to work well. But I needed global variables to keep track of what decade I was in. I ended up passing the global variable into local variables, messing with it there, and passing it back up.

Game-design wise, I had another challenge in how to handle time travel. I could either make 5 copies of every room or keep track of time as a variable. I ended up doing the latter, which required a ton of extra code. For instance, here’s how I handled scenery objects and movable objects (which were a huge pain due to being able to be placed on supporters, etc.):

Summary
(on every tick)
	(TimeAge $CurrentTime)
	*($object sceneryObj)
		($object sitsin $destination)
			(if)($object age $CurrentTime)(then)
				(now)($object is #in $destination)
			(else)
				(now)($object is #in #Limbo)
			(endif)


(on every tick)
	(TimeAge $CurrentTime)
	*($object travels)
		(if) ($object is #on #pedestal)(then)
			(now)($object pedestaled)
			(#Pedestal is #in $currentRoom)
				(now)($object belongs $currentRoom)
				(now)($object age $CurrentTime)			
		(elseif)($object is #heldby #CJ)(then)
			(now)~($object pedestaled)
			*(stablesupporter $supporter)
				(now)~($object beihSupport supporter)
			(#CJ is #in $currentRoom)
				(now)($object belongs $currentRoom)
				(now)($object age $CurrentTime)
		(else) 
			(now)~($object pedestaled)
			($object belongs $destination)
				(if)($object age $CurrentTime)(then)
					($object is #on $supporter)
						(stablesupporter $supporter)
							(now)($object beihSupport $supporter)
					(or)
						(if) ($object beihSupport $supporter)(then)
							(now)($object is #on $supporter)
						(else)
							(now)($object is #in $destination)
						(endif)
				(else)
					(now)($object is #in #Limbo)
				(endif)
		(endif)

Here TimeAge is the globabl variable and everything with a $ is a local variable. ‘beih’ is one of two Cantonese words I use a lot in my code, indicating passive voice; the other is ‘meih’, meaning ‘not yet’.

Testing

My philosophy is to test early and test often. Dee Cooke mentioned in her excellent post-mortem that having a lot of testers kind of ended up making the game a bit too easy, and I can’t argue with that; I’ve always had lots of testers and my games have indeed ended up kind of easy.

But I think the benefits outweigh the drawbacks. I first sent the game to a few people on Twitter who volunteered when I posted looking for help; the game was still very rough and choppy at that time. I’ve attached that game file as ImpossibleStairsVer1, in case anyone wants to compare the first version to the final.
ImpossibleStairsVer1.z8 (132.8 KB)

The major feedback was that people wanted more in a lot of areas. Many people said the first version was short, so I added additional puzzles. Puzzles that weren’t in the original include:

  • The food puzzle (including the cinnamon puzzle) and the whole pantry
  • The crossword puzzle book and ordering form
  • The opening area and the paper airplane
  • The final dinner

Originally, I made all the NPCs bland and archetypal intentionally, to make them doll-like, but many people found this dissatisfying, especially compared to the very interactable characters from The Impossible Bottle. So I added a job for Uncle Joe, for instance, and more personality to the other characters.

I found the whole premise of seeing loved ones dying sad from the start, especially since two of my grandparents and a student I had taught for a couple of years had all died recently. So I decided to not reach for any emotion at all; by keeping the facts of death obvious but not commented on, I figured everyone could project their own feelings onto the game, making it (hopefully) more effective. I did put in the memory board to make it clear who had passed on.

People wanted more and more with NPCs, both present and dead. They wanted to know a lot about the mom, so I added more conversational topics about that.

I also wanted to emulate the status bar in Impossible Bottle, which turned out not to be completely built-in. Here was my code for it:

Summary
(style class @status)
	height: 1em;

(style class @timename)
	height: 1em;
	font-size: .9em;

(style class @instatus)
	height: 2em;
	font-size: .9em;
	margin-top: .9em;
	
(style class @help)
	height: 1em;
	width: 20ch;
	float: right;
	font-size: .9em;
	margin-top: .1em

(barTime)
	(TimeAge $CurrentTime)
		(if)($CurrentTime = 1)(then)
				Year: 1961
		(elseif)($CurrentTime = 2)(then)
				Year: 1981
		(elseif)($CurrentTime = 3)(then)
				Year: 2001
		(elseif)($CurrentTime = 4)(then)
				Year: 2021
		(else)
				Year: 2041
		(endif)

(redraw status bar)
	(status bar @status) {
			(if)(interpreter supports links)(then)
				(div @help) {
					(link){Help}
				}
			(endif)
			(div @timename){
				(space 1)(barTime) Place:(status headline)
			}
		}
	(if)(interpreter supports links)(then)
		(if)(turnonstatus)(then)
			(inline status bar @instatus){(statuscontents)}
		(endif)
	(endif)
	

(statusactivate node $obj)
	(now)~(turnonstatus)
	(activate node $obj)

(after disp (terminating $))
	(now)(turnonstatus)
	
(statuscontents)
	(par)
	\[(link){Look},(link){Inventory},
		(if)(#ToDoList is #heldby #CJ)(then)
			(link){Goals};
		(endif)
		(exitlinks);
		(contextlinks)
	\]	

(exitlinks)
	(current room $currentRoom)
		(collect $Obj)
			*(from $currentRoom go $Obj to $)
		(into $List)
		(A $List)
		
(pronoun $object)
	(if)($object = #CJ)(then)
		me
	(elseif)(female $object)(then)
		her
	(elseif)(male $object)(then)
		him
	(elseif)(plural $object)(then)
		them
	(else)
		it
	(endif)
		
(global variable (LastObject #paperairplane))

(after [examine $obj])
	~(not here $obj)
		(now)(LastObject $obj)

(after [take $obj])
	~(not here $obj)
		(now)(LastObject $obj)

(on every tick)
	(LastObject $obj)
		~($obj is in scope)
			(now)(LastObject #Limbo)

%%objcounter's only purpose is to make links not 'sticky'
%%(after [examine $obj])
%%	(now)(LastObject $obj)
%%	(now)~($obj oncenoticed)
		
%%(after (notice player's $obj))
%%	(now)(LastObject $obj)
%%	(now)~($obj oncenoticed)
		
(contextlinks)
	(malelinks)
	(femalelinks)
	(itemlinks)
	(itemslinks)

(malelinks)
	(LastObject $tempLast)
		(him refers to $tempLast)
			(object $tempLast)
				(name $tempLast):
				(link){Talk to (pronoun $tempLast)}
		(or)
(femalelinks)	
	(LastObject $tempLast)
		(her refers to $tempLast)
			(object $tempLast)
					(name $tempLast):
					(link){Talk to (pronoun $tempLast)}
		(or)

(itemlinks)
	(LastObject $tempLast)
		(player's it refers to $tempLast)
			(object $tempLast)
				(if)($tempLast is #heldby #CJ)(then)
					(name $tempLast):
					(link){Examine (pronoun $tempLast)},
					(link){Give (pronoun $tempLast)},
					(link){Drop (pronoun $tempLast)}
					(if)($tempLast = #sapling)(then)
						(link){Plant it},
					(endif)
					(if)($tempLast = #servingdish)(then)
						(if)(#dishtowel is #heldby #CJ)(then)
							(link){Wipe it},
						(endif)
					(endif)
				(else)
					(name $tempLast):
					(link){Examine (pronoun $tempLast)},
					(if)($tempLast is closed)(then)
						(link){Open it},
					(endif)
					(if)($tempLast = #oldtelevision)(then)
						(link){Change channel},
					(endif)
					(if)($tempLast = #newtelevision)(then)
						(link){Change channel},
					(endif)
					(if)($tempLast = #dial)(then)
						(link){Turn it},
					(endif)
					(link){Take (pronoun $tempLast)}
						(now)($tempLast oncenoticed)
				(endif)
		(or)
(itemslinks)
	(LastObject $tempLast)
		(them refers to $tempLastArray)
			($tempLast is one of $tempLastArray)
				(object $tempLast)
					(if)($tempLast is #heldby #CJ)(then)
						(name $tempLast):
						(link){Examine (pronoun $tempLast)},
						(link){Give (pronoun $tempLast)},
						(if)($tempLast = #sapling)(then)
							(link){Plant it},
						(endif)
						(if)($tempLast = #servingdish)(then)
							(if)(#dishtowel is #heldby #CJ)(then)
								(link){Wipe it},
							(endif)
						(endif)
						(link){Drop (pronoun $tempLast)}
						(now)($tempLast oncenoticed)
						(if)($tempLast = #plates)(then)
							(if)(current room #Kitchen)(then)
								(link){Put them on table},
							(endif)
						(endif)
					(else)
						(name $tempLast):
						(link){Examine (pronoun $tempLast)},
						(if)($tempLast is closed)(then)
							(link){Open it},
						(endif)
						(link){Take (pronoun $tempLast)}
						(now)($tempLast oncenoticed)
					(endif)
		(or)

Release

I entered the game in Parsercomp because I volunteer with IFComp now and can no longer enter there, and Parsercomp is a cool competition. I was a bit hesitant entering since I had won the year previously and I didn’t want to be seen as being pushy or domineering, but seeing how close the competition was this year I shouldn’t have worried in the slightest.

I paid a student in my school to make the art, which I asked them to make look like the Impossible Bottle’s art as much as possible.

I was pleased with the response people had, and I was glad that Linus liked it, as he was the number 1 target to please.

The biggest negative feedback I had was that some puzzles were really long/tedious. I’ve considered removing one step of the Ada puzzle for a post-comp release, but will probably leave it.

Future

I loved using Dialog, and would use it again for any internet-focused project or one with whitespace shenanigans.

My current project is in Inform, though, which is a big project that will hopefully last 10+ hours, made of 10 IFComp-size games cobbled together into an overall storyline.

Thanks to everyone who voted, and thanks to the innovators and authors currently entering these comps! I feel like I learn so much from everyone, both the successful games and the ones that don’t go the way authors wanted. It’s great to see how vibrant and pleasant the community is!

20 Likes

I was hoping to see this postmortem for multiple reasons! I like your comment about how Linus was the #1 target. The wait was worth it, and this is obviously a game you can’t do overnight. I liked the use of the memory board to note the passage of time, too. It never felt like IS was forcing me to feel anything.

I was also curious how well Dialog would work for an Inform writer. Even though there’s a new version of Inform out, it’s great to see it spawned something else that didn’t feel it had to be faithful to Infocom to start. And it’s great you’re planning to post the code-I’ve learned so much from I7 code and it’d be great to do the same for Dialog.

We missed you in the authors’ forum. It’s great to know this is the reason why. Your posting this gives me motivation to start a “threads from past years” threads in the IFComp 2020/2021 author playhouses. I’ve thrown the idea around a bit.

3 Likes

Thanks for this look behind the curtain, Brian!

I have to say, I’m impressed you modeled the house as one set of locations with a time variable changing their status, rather than just brute-forcing a bunch of copy-and-pasted separate rooms which seems much less elegant but much easier to implement!

3 Likes

Writing IF is such a fun problem. For me, it feels like it involves both art and engineering. Now, I’m a stereotypical programmer that wants to test early and test often. However, I’m also a stereotypical artist that wants to hide everything until it’s ready. Even though I’ll probably never convince myself that I shouldn’t do that, I’m so happy to see you (and many others!) tout the benefits of having beta testers! And after reading and reviewing ParserComp entries, I can see first-hand the positive influence they’ve had on my experience.

4 Likes

Posting the source code here (in the unprocessed section of the ifarchive). I’ll update the link when it’s confirmed (and thanks to the tireless ifarchive team!):

https://ifarchive.org/if-archive/unprocessed/ImpossibleStairs.dg

5 Likes