Building a Roguelike in iOS Shortcuts

Diverging off of my last post about using iOS Shortcuts, an app built with the intention of productivity, for purely useless reasons, I’m going to go into detail on another one of my ill-conceived Shortcuts-in-progress: Roguemoji.

Link: https://www.icloud.com/shortcuts/a8b49fe817ed43f4b20b78322b2d8d07

With a bit of elbow grease and repetition, Shortcuts actually allows us to build a fairly plausible roguelike game, complete with random maps and items that we can pick up. No monsters yet, that’s on the way.

How it works

At its core, the roguelike “loop” reads very similarly to the text adventure outlined in the previous post: Find the player’s current position, draw the surroundings accordingly, allow the user to change position, repeat. What it does differently is incorporate graphics into the equation, allowing us to visualize a maze (dungeon,. labyrinth, what-have-you) around the player character, rather than relying on just text. Even though it may be more accurate to tradition to use ASCII characters, the iPhone gives us plenty of emoji to play with, so we’ll use them.

As with the text adventure, we start by defining all of our required variables outside of the main “loop”. Most importantly, we’ll need to know what our Map looks like, what our player’s Position is, and since we’re bringing graphics into this, we’ll define a Camera element, as well. The idea here being that we don’t want to show the entire map at once, and show instead a small part of it, centered around the player.

To define the Map, I recommend using the Text function, and drawing in a map of your liking using the Black Square emoji for walls, and the White Square emoji for walk-able floor tiles. The map should be a rectangle of any width/height you prefer, though I recommend keeping the width to something like 12 or 13 characters, as I found that gave me the best legibility while working in the app. Then, use the Set Variable function to save the Text as a variable that we can call up later.

Note: You’ll notice that in the example provided, I used a variety of different Text functions, and assigned them to a list. This allowed me to pseudo-randomly generate a map, which works well enough most of the time. Procedural map-building is a hallmark of the roguelike genre, but not yet a strength of mine, so this will be something we revisit later on. For the time being, feel free to use a single, static Text command to make your Map.

For example purposes, let’s say that we have a map that is 12 emoji wide, with a good spot for the player to start off in at 3 spaces in, and three lines from the top. Our first bit of the Shortcut should then look something like this:

Text: [Map of emoji blocks here]

Set Variable: Map

Number: 12

Set Variable: Map Width

Number: 3

Set Variable: Player X

Set Variable: Player Y

Notice that we’re using two separate variables for X and Y. Since our emoji map exists in a Text function, we can manipulate it into letting us use the two-dimensional arrays that our text adventure lacked. We’ll come back to that, though. Next, let’s look at the camera.

Because there’s only so much space on the phone screen, we don’t want to show the whole Map all at once. Instead, we want to show the player just a segment of it, centered on the player character. This should have enough information of the player character’s surroundings to be useful, but not so much that it becomes cumbersome or difficult for the program to load. After some experimenting, I settled for showing the player a 5-by-5 square, with the character positioned in the camera’s center as often as possible. To achieve this, we first set the Camera’s beginning X and Y positions by subtracting from the player position:

Number: 3

Set Variable: Player X

Set Variable: Player Y

Calculate: Subtract 3

Set Variable: Camera X

Set Variable: Camera Y

Number: 5

Set Variable: Camera Width

Set Variable: Camera Height

Using this, when the Player is at X,Y {3,3}, for instance, the Camera will be at {0,0}, showing us the first 5 characters of the first 5 rows of the Map. If this doesn’t make sense just yet, keep going, and play around with it later. You could also replace the “3” in the Calculate command with a variable that you can easily change later on, so that you can find a balance that you prefer.

Disclaimer: I’m sure you’ve already noticed something about Shortcuts: it takes a while to do literally anything. It certainly isn’t as efficient as programming this sort of thing in C++, Java, Bash, Python, or whatever. Hell, you could probably do this faster in BASIC if you wanted. Shortcuts does not equal programming. This is just how my brain works: taking something that is clearly intended for being useful, and making dumb games with it. Savvy? Okay, moving on.

The Camera

Now that we’ve done all of our setup for the Player and Camera positions, we have to turn that into something that actually does something, right? We’ll do this by returning to the “Nigh-Infinite Repeat” trick that we used for the text adventure in my previous post, which starts like this:

Number: 99999999999999999999999999999

Repeat: [Number] times

We’ll place that at the bottom of the program, under our setup functions, and put our gameplay “loop” inside. For starters, let’s go ahead and figure out what the Camera sees of the Map. I’ll write out what the functions look like first, then explain it line-for-line after. Feel free to also consult the example I provided for a less-than-perfect reference as you go.

Get Variable: Camera Height

Get Variable: Camera Width

Repeat: [Camera Height] times

Get Variable: Camera Y

Calculate: Add [Repeat Index 2]

Set Variable: Row To Get

Get Variable: Map

Split Text: New Lines

Get Item From List: Item at Index [Row To Get]

Set Variable: Current Row

Repeat: [Camera Width] times

Get Variable: Camera X

Calculate: Add [Repeat Index 3]

Set Variable: Character To Get

Get Variable: Current Row

Split Text: Every Character

Get Item From List: Item at Index [Character To Get]

Set Variable: Current Item

Get Variable: Character To Get

If: Equals [Player X]

Get Variable: Row To Get

If: Equals [Player Y]

Text: [Whatever emoji you want to represent your character]

Set Variable: Current Item

End If

End If

Get Variable: Current Item

Add To Variable: Row To Draw

End Repeat

Get Variable: Row To Draw

Split Text: New Lines

Combine Text: Custom [Leave blank]

Add To Variable: What The Camera Sees

Nothing

Set Variable: Row To Draw

End Repeat

Text: [What The Camera Sees]

Alright, wow! That was a lot. It looks like a lot. It is a lot.

But!

That is the core of what we’re doing here today. Everything else is secondary to this, and it’s not that hard once we break it down. Let’s do that now:

First, we’re going to put two more Repeat functions into our big Repeat function (yo dawg, etc., etc.). This is how we create the “square” of what our camera sees: the first Repeat looks at five rows, starting at Camera Y then adding the Repeat Index 2 each time to move to the next row down. Notice that it’s Repeat Index 2, because Repeat Index would correspond to our “master” repeat, and be no use at all. The second Repeat then takes whatever row we’re looking at, and breaks it up into individual characters. From there, we grab the Camera X value, and add Repeat Index 3 to find the individual character that we’re looking to draw.

If the result of Camera X plus Repeat Index 3 matches Player X, and If Camera Y plus Repeat Index 2 matches Player Y, that’s where the character is! Let’s draw that emoji instead of whatever’s on the map.

Whether we’re drawing the Player character or whatever’s at that place on the map, we’re going to Add To Variable to add it to our “Row To Draw”. If you’re playing around with the Add To Variable function, you’ll see pretty quickly that it adds whatever’s passed to it into a new line in the variable. While this would be fine if our map was one character wide and an infinite number of characters long, it doesn’t suit our two-dimensional look. That’s why, after we’ve gone through all of the steps in the second Repeat function (getting the characters out of the row), we Split Text on the “Row To Draw” variable, then immediately Combine Text. Combine Text allows us to combine a List (which is what Split Text gives us) using a Custom value, which we’re just going to leave blank. That takes our vertical column, and returns it as a horizontal row of the proper characters. We then Add To Variable again, adding this nice horizontal row to “What The Camera Sees”.

Before looping back around and going to the next row, let’s use the Nothing function to set the “Row to Draw” variable back to nil. Otherwise, we’ll just keep adding to that variable, and things will get real weird.

Once we have all of the rows that the Camera sees, we’ll use Text to neatly wrap them all up, and if you’re feeling intrepid, you can use Quick Look to check your work.

A Few Notes Before Braving Forth

So far, we’ve done a handful of things:

  1. Trick Shortcuts into letting us use Text as two-dimensional arrays

  2. Used those arrays to display only a small part of the overall map

  3. Separated our camera from our player

The first two things are essential parts of bringing the roguelike genre to this format; if we showed the player the whole map at once, that would not only ruin some of the surprise element, but also not fit on the most mobile device screens (although the iPhone XS is massive so, like, who knows).

The third thing is a simple one, probably, but one that I’m really proud of. That’s because it’s a fundamental element of making games that are set in the third-person (where you see your player character on the screen): the Camera object and the Player object are two separate entities, and can be moved independently. This means that as the Player character approaches the edges of the map, we can have them move all the way up to that edge, without the Camera trying to show the “great beyond” that doesn’t exist.

As an example of this in our roguelike game, imagine that the player wants to move to a space at Row 1, Column 1 (Shortcuts only uses 1-indexed arrays, so there’s no real 0,0). Using what we’ve set up so far, once the player moves to that space, placing them in the middle of what the Camera is showing would first require pulling two empty rows, then two empty characters, before showing the player. You would be using only about a quarter of the space we set up for our Camera to display on, and risk errors or funky display issues.

Using what we’ve set up now, we can let the Player move all the way to that corner, and simultaneously make sure that the Camera doesn’t go past that point. The player would temporarily move out of the Camera’s center while they explore those edges, but come safely back to center once they turn back and explore the rest of the map. It’s a small thing when you put it into words, but speaks volumes for the quality of the final product.

Okay, coming down off my soap box. Now that we’ve set up the base variables, and our loop for using the camera to the player and the map, let’s set up the interface for actually playing the game.

Actually playing the game

If you’ll remember from our previous Text Adventure example, the main cruft of the game revolved around using the Choose From Menu function in a loop, wherein the choices adjusted the Player’s X and Y positions. Here, we’re going to do roughly the same thing, with some extra If functions to keep our Player and Camera in line, and from going off the map. Let’s start with dropping in the Choose From Menu, which we’ll place after the Text function we used at the end of the last section, and before the End Repeat at the end of the function (we want this to take place within our main loop).

Text: [What The Camera Sees]

Choose From Menu: [What The Camera Sees]

Up

Down

Left

Right

End Menu

Nothing

Set Variable: What The Camera Sees

End Repeat

Pretty basic so far. We display “What The Camera Sees”, then ask the player to choose from the four cardinal directions. If you wanted to get more fancy, you could add more things to the Text function (health, inventory, etc.) and use that in the menu instead, or add diagonal directions for a fun isometric feel. For now, we’ll keep things as simple as we can, probably.

We also pass Nothing back to the “What The Camera Sees” variable, so that we don’t just keep adding to it with each repeat.

Within each of the four options (which you’re welcome to rearrange or rename as you like, I ended up using emoji), we’re going to add If statements to make sure that there’s space that direction, then update the Player and Camera positions as needed. Because these get pretty big, pretty fast, I’ll take a look at each direction separately, then we can look at the Menu as a whole. Let’s start with the easiest one:

Up

Get Variable: Player Y

If: Is Greater Than 1

Calculate: Subtract 1

Set Variable: Player Y

Get Variable: Camera Y

If: Is Greater Than 1

Calculate: Subtract 1

Set Variable: Camera Y

End If

End If

We’ll start with the obvious: If the Player’s Y value is greater than 1 (I keep having to remind myself that Shortcuts uses arrays that start at 1), then we subtract 1 from that value, and assign the result back to Player Y.

If the Player changed positions, we’ll also check to see If the Camera has space to move, as well. We do this in the exact same way: If Camera Y is greater than 1, we subtract one from that value, and return it to Camera Y.

Challenge: We can apply that same basic function to the Left option in our Menu, but trading in Player X and Camera X for Player Y and Camera Y. Give it a shot!

We’ll now tackle the more complex movements:

Down

Get Variable: Map

Count: New Lines

Set Variable: Map Height

Get Variable: Player Y

If: Is Less Than [Map Height]

Calculate: Add 1

Set Variable: Player Y

Get Variable: Camera Y

Calculate: Add [Camera Height]

If: Is Less Than [Map Height]

Get Variable: Camera Y

Calculate: Add 1

Set Variable: Camera Y

End If

End If

At lot of the same notes as the other direction, but with some important additions. First, unlike the “Map Width” variable, we didn’t set a variable for getting the Map’s height, so we use Count to get the number of lines in “Map”, representing the number of rows we have to work with. You could do this right up top with Map Width, I supposed, but calling the action here allows us a degree of dynamism (Is that the right word? Probably not.) and gives us the opportunity to change the height of the Map between actions (secret rooms!).

Second, we grab Camera Y and Calculate where the bottom of the frame is, by adding the “Camera Height” variable that we set a forever ago. This way, we are actually checking to see if the bottom of the camera’s viewport is going to go off the edge of the map, then adjust the Camera Y value if it is not.

Challenge: This same concept can be applied to the Right option in our Menu, using Map With, Player X, and Camera X. Can you figure it out before we put everything together?

Putting Everything Together

By now, we have something resembling the structure of a basic, top-down game, with the potential for roguelike elements. Moving into those specific elements will complicate what we’ve done today, at least in my own brain, so I’m going to compile an overview of what we’ve done here, then go into more depth in my next post. If you have questions or comments, let me know! I’m positive I fucked something up somewhere, so don’t be shy.

If you’ve been following along, here’s likely what you have so far in your Shortcut:

Text: [Map of emoji blocks here]

Set Variable: Map

Number: 12

Set Variable: Map Width

Number: 3

Set Variable: Player X

Set Variable: Player Y

Calculate: Subtract 3

Set Variable: Camera X

Set Variable: Camera Y

Number: 5

Set Variable: Camera Width

Set Variable: Camera Height

Number: 99999999999999999999999999999

Repeat: [Number] times

Get Variable: Camera Height

Get Variable: Camera Width

Repeat: [Camera Height] times

Get Variable: Camera Y

Calculate: Add [Repeat Index 2]

Set Variable: Row To Get

Get Variable: Map

Split Text: New Lines

Get Item From List: Item at Index [Row To Get]

Set Variable: Current Row

Repeat: [Camera Width] times

Get Variable: Camera X

Calculate: Add [Repeat Index 3]

Set Variable: Character To Get

Get Variable: Current Row

Split Text: Every Character

Get Item From List: Item at Index [Character To Get]

Set Variable: Current Item

Get Variable: Character To Get

If: Equals [Player X]

Get Variable: Row To Get

If: Equals [Player Y]

Text: [Whatever emoji you want to represent your character]

Set Variable: Current Item

End If

End If

Get Variable: Current Item

Add To Variable: Row To Draw

End Repeat

Get Variable: Row To Draw

Split Text: New Lines

Combine Text: Custom [Leave blank]

Add To Variable: What The Camera Sees

Nothing

Set Variable: Row To Draw

End Repeat

Text: [What The Camera Sees]

Choose From Menu: [What The Camera Sees]

Up

Get Variable: Player Y

If: Is Greater Than 1

Calculate: Subtract 1

Set Variable: Player Y

Get Variable: Camera Y

If: Is Greater Than 1

Calculate: Subtract 1

Set Variable: Camera Y

End If

End If

Down

Get Variable: Map

Count: New Lines

Set Variable: Map Height

Get Variable: Player Y

If: Is Less Than [Map Height]

Calculate: Add 1

Set Variable: Player Y

Get Variable: Camera Y

Calculate: Add [Camera Height]

If: Is Less Than [Map Height]

Get Variable: Camera Y

Calculate: Add 1

Set Variable: Camera Y

End If

End If

Left

Get Variable: Player X

If: Is Greater Than 1

Calculate: Subtract 1

Set Variable: Player X

Get Variable: Camera X

If: Is Greater Than 1

Calculate: Subtract 1

Set Variable: Camera X

End If

End If

Right

Get Variable: Map Width

Get Variable: Player X

If: Is Less Than [Map Width]

Calculate: Add 1

Set Variable: Player X

Get Variable: Camera X

Calculate: Add [Camera Width]

If: Is Less Than [Map Width]

Get Variable: Camera X

Calculate: Add 1

Set Variable: Camera X

End If

End If

End Menu

Nothing

Set Variable: What The Camera Sees

End Repeat

At the end of it all, we have a map built from emoji, a camera that displays only part of that map, a player character right in the middle, and a way to move them around. That leaves us with a certain set of new challenges!

  1. At the start, I mentioned that I used White Square and Black Square emoji for drawing our map. How do we keep the player character from passing through Black Squares? (I did this in the example, check it out.)

  2. How would we go about creating random maps?

  3. How do we handle item interactions?

  4. Can we add random enemies to the map, with their own discrete movement and actions?

I’ve tried my hand at the first three items, and will be tackling the last one soon. In the next post, we’ll go into detail about some potential ways to make this happen, and try it out ourselves.