Lab: Maze Editor (day 1)

For this lab, you will practice using ncurses and a game loop to build a maze editor. You’ll use this program to draw a maze in the terminal. You’ll revisit this program in the next lab, where you’ll add code to load and save a maze from a file.

To get started, you’ll need to fork and clone the repository for this lab.
You can find the starter code repository at https://git.cs.grinnell.edu/csc161/maze-editor. Refer back to earlier labs if you don’t remember all the steps to fork a repository, give your partner(s) access, and clone the repository. Make sure you clone your fork of the starter code repository. If you clone the original repository, you won’t be able to push your changes back to the git server.

A. Displaying a Maze

Before we add any interactive features, we’ll need our program to be able to display a maze. We’re going to store the maze in an array. Your task for this part of the lab is to write and test the following function:

void print_maze(int maze[MAZE_SIZE * MAZE_SIZE]);

The function will need to loop over the maze array and display it by printing either spaces or a special ncurses character ACS_CKBOARD. Note that the array is one dimensional, so you’ll need to convert row and column numbers to array indices. For any given row and column, we can always calculate index = row * MAZE_SIZE + col.

Printing One Cell

A good way to print special characters like ACS_CKBOARD is using the mvaddch function. If the value in the maze array is 0, the cell is open and you should print a space:

mvaddch(row, col, ' ');

However, if the value in the maze array is 1 then the cell is a wall and you should print a block:

mvaddch(row, col, ACS_CKBOARD);

You can read more about the special characters ncurses supports at https://tldp.org/HOWTO/NCURSES-Programming-HOWTO/misc.html if you are curious.

Characters in the terminal are not square, so the maze would look a little strange if we print it with a single character for each cell. Instead, each cell will be two characters wide but just one character tall. This introduces a new coordinate system we need to keep track of. While we may be printing the maze cell at row 3 and column 5, there will actually be two characters printed in the terminal coordinates (row 3, column 10 and row 3, column 11). One trick that may help you with this is to use mvaddch to print the first character, followed by addch to print the second character; this function prints a character at the current cursor position, which is one column to the right of the last character you printed.

Test Input and Output

You can test your implementation with the following maze:

int maze[MAZE_SIZE * MAZE_SIZE] = {
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1,
    1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1,
    1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1,
    1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
    1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1,
    1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1,
    1, 0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1,
    1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1,
    1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1,
    1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1,
    1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1,
    1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1,
    1, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1,
    1, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1,
    1, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1,
    1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1,
    1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 1,
    1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1,
    1, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 1,
    1, 0, 1, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, 1, 0, 1, 1, 1, 0, 1,
    1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1,
    1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1};

Add the maze variable to your main function and pass it to print_maze. Add an infinite loop after print_maze returns to stop the program so you have a chance to look at the output. You’ll need to type ctrl+c to exit the program for now. Your completed print_maze function should produce output that looks something like this:

▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒      ▒▒                  ▒▒          ▒▒  ▒▒
▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒  ▒▒  ▒▒
▒▒  ▒▒      ▒▒  ▒▒      ▒▒          ▒▒      ▒▒
▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒      ▒▒      ▒▒      ▒▒      ▒▒          ▒▒
▒▒▒▒▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒  ▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒
▒▒  ▒▒  ▒▒      ▒▒  ▒▒      ▒▒      ▒▒      ▒▒
▒▒  ▒▒  ▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒
▒▒  ▒▒      ▒▒      ▒▒              ▒▒  ▒▒  ▒▒
▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒
▒▒                  ▒▒  ▒▒          ▒▒      ▒▒
▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒
▒▒  ▒▒          ▒▒      ▒▒              ▒▒  ▒▒
▒▒  ▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒
▒▒  ▒▒      ▒▒          ▒▒              ▒▒  ▒▒
▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒
▒▒      ▒▒                  ▒▒          ▒▒  ▒▒
▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒
▒▒  ▒▒  ▒▒  ▒▒              ▒▒  ▒▒          ▒▒
▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒
▒▒      ▒▒      ▒▒                  ▒▒      ▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒

Commit your changes and push to the git server before moving on to the next part of the lab.

B. Handling Input

Now that we can display a maze using ncurses, we’re going to make this program interactive. We’ll do that using a game loop.

Working from the example in today’s reading, add a game loop to your main function. Write code to do the following things in your game loop:

  1. Check for input. If the user has typed q, leave the loop and exit the program.
  2. Call print_maze, and then call refresh() to update the terminal.
  3. Pause at the end of the loop to maintain a target frame rate set in the FRAME_RATE constant.

Normally we would clear the screen on each frame. However, we can skip clearing the screen in this program because we always display the entire maze to the screen. The new output will always cover up the old output, so clearing is unnecessary. The program would still work with a call to clear(), but it can cause flickering on some terminals, especially over a remote connection, so we’ll leave it out in this case.

Once you’ve finished this part of the lab your program should look the same, but pressing q should exit the program. Make sure you save, commit, and push your changes before moving on.

C. Moving a Cursor

Next, we’re going to display a cursor that the user can move around in the maze. You’ll need to add some state to keep track of the current cursor row and column. Inside the game loop, add code to respond to arrow keys to move the cursor up, down, left, and right.

Once you have the cursor state and input code, it’s time to display it. We have to think carefully about how we can show where the cursor is while still making it clear whether the selected cell is a wall or an empty space. We could use color to indicate this (ncurses has good support for terminal colors) but to keep this lab simple we’ll use a simpler approach.

We’ll display the cursor with one of two possible strings: "__" if the cursor is over an empty cell, and "##" if the cursor is over a cell that contains a wall. The output should look something like this when the cursor is over a wall:

▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒      ▒▒                  ▒▒          ▒▒  ▒▒
▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒  ▒▒  ▒▒
▒▒  ▒▒      ▒▒  ▒▒      ▒▒          ▒▒      ▒▒
▒▒  ▒▒▒▒▒▒▒▒##  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒      ▒▒      ▒▒      ▒▒      ▒▒          ▒▒
▒▒▒▒▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒  ▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒
▒▒  ▒▒  ▒▒      ▒▒  ▒▒      ▒▒      ▒▒      ▒▒
▒▒  ▒▒  ▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒
▒▒  ▒▒      ▒▒      ▒▒              ▒▒  ▒▒  ▒▒
▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒
▒▒                  ▒▒  ▒▒          ▒▒      ▒▒
▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒
▒▒  ▒▒          ▒▒      ▒▒              ▒▒  ▒▒
▒▒  ▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒
▒▒  ▒▒      ▒▒          ▒▒              ▒▒  ▒▒
▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒
▒▒      ▒▒                  ▒▒          ▒▒  ▒▒
▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒
▒▒  ▒▒  ▒▒  ▒▒              ▒▒  ▒▒          ▒▒
▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒
▒▒      ▒▒      ▒▒                  ▒▒      ▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒

Or, if the cursor is over an empty cell:

▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒      ▒▒                  ▒▒          ▒▒  ▒▒
▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒  ▒▒  ▒▒
▒▒  ▒▒      ▒▒  ▒▒      ▒▒          ▒▒      ▒▒
▒▒  ▒▒▒▒▒▒▒▒▒▒__▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒
▒▒      ▒▒      ▒▒      ▒▒      ▒▒          ▒▒
▒▒▒▒▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒  ▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒
▒▒  ▒▒  ▒▒      ▒▒  ▒▒      ▒▒      ▒▒      ▒▒
▒▒  ▒▒  ▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒
▒▒  ▒▒      ▒▒      ▒▒              ▒▒  ▒▒  ▒▒
▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒
▒▒                  ▒▒  ▒▒          ▒▒      ▒▒
▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒
▒▒  ▒▒          ▒▒      ▒▒              ▒▒  ▒▒
▒▒  ▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒
▒▒  ▒▒      ▒▒          ▒▒              ▒▒  ▒▒
▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒
▒▒      ▒▒                  ▒▒          ▒▒  ▒▒
▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒
▒▒  ▒▒  ▒▒  ▒▒              ▒▒  ▒▒          ▒▒
▒▒  ▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒▒▒▒▒▒▒▒▒  ▒▒  ▒▒▒▒▒▒  ▒▒
▒▒      ▒▒      ▒▒                  ▒▒      ▒▒
▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒

This isn’t the most elegant way to show the current selection but it’s fairly easy to do. After you’ve called print_maze in your game loop, use mvprintw to print one of the two cursor strings at the cursor’s position. You’ll need to look inside the maze array to decide which cursor string to display. Don’t forget that each cell in the maze is actually two characters in the terminal, so you’ll have to do some calculating to figure out where to print the cursor string.

Commit and push your changes once you have the cursor working. We’ll focus on keeping the cursor in bounds and updating the actual maze in the next part.

D. Editing the Maze

Now that you can display a maze and the user can move a cursor around the grid, we’re going to finish the editor by allowing the user to type space to toggle the selected cell between an open space and a wall.

The example maze from the first part of this lab included a solid border around the edge of the maze. We’d like all of our mazes to have this border, so we’ll constrain the cursor’s movement to prevent the user from editing the border. That means the user should not be able to move the cursor onto the border at all. Add checks to your input handling code to make sure the cursor does not move onto any of the border cells. Do your best to write the conditions to constrain cursor movement, but there’s no harm in making an educated guess and then testing to see if your code works; you’ll often find that this is a good way to deal with off-by-one errors. Just make sure you understand why the condition you end up with is correct before you move on.

Finally, pressing space should toggle the selected cell between open space and wall. The getch function returns special values like KEY_UP for some keys, but when the user types regular letters or characters the result is just a char like ' ' in the case of the space bar. Other than that small detail, you should have all the pieces you need to complete the maze editor.

Commit and push your changes once you have everything working. We’ll learn how to add file saving and loading to this program in our next lab, but if you have time remaining there is an optional additional exercise below.

E. Optional Challenge: Running the Maze

If you have extra time at the end of this lab you should work on making your maze playable. When the user presses the r key, switch to a mode where the user controls an icon in the maze instead of moving a cursor to edit it. For simplicity, you can assume the user always starts in the upper left corner of the maze (coordinate 1, 1) and the goal is always the lower right (coordinate 21, 21). The user should be able to move through the maze using arrow keys, but should not be able to move into cells that contain walls.

You’ll need to keep track of which mode your program is in: editing versus running. The maze running mode will need a small amount of additional state to keep track of where the player is located, and you’ll need to handle arrow keys differently depending on which mode you are in.