Overview

This lab is about getting some practice writing programs that use structs. We'll try to have some fun in the process, though. Folks always ask to do something game-like, so that's what we'll do. We're going to keep everything in the terminal, since dealing with graphics libraries would take us a bit too far off-topic for one lab period. However, we will use a libarary called "curses" (actually "ncurses") to allow us to write to arbitrary positions in the terminal window and to read keystrokes in a "nonblocking" manner.

Part 1: ncurses basics (name p1.cpp)

The ncurses library views your terminal window as a big 2D table of single-character positions. So think of coordinates as (row,col) rather than (x,y). Thinking of it this way, (0,0) is the upper left corner of the terminal window. Instead of using ncurses directly, We are providing you with with a "wrapper", which means simpler functions you call in order to make the more complex function calls to ncurses itself. The wrapper functions are defined in two files: easycurses.h and easycurses.cpp. Download these and save them into your lab directory. Note: Do not modify easycurses.h and easycurses.cpp!. To use easycurses/ncurses, you need to do a #include "easycurses.h" in your .cpp files (note that's quotes not angle brackets!), and you need to compile with the -l ncurses compiler flag and include easycurses.cpp on the compile line, like:
clang++ p1.cpp -o p1 easycurses.cpp -l ncurses

Create and compile p1.cpp under your account. This source code demonstrates some simple aspects of ncurses. Here's a few comments:

#include <iostream>
#include <unistd.h>
#include <cstdlib>
#include "easycurses.h"
using namespace std;

int main() {
  // Initialize ncurses
  startCurses();

  // Draw 4 digit characters to the terminal screen.
  drawChar('A', 0, 0);
  refreshWindow();
  usleep(800000);
  
  drawChar('B', 20, 30);
  refreshWindow();
  usleep(800000);

  drawChar('C', 20, 31);
  refreshWindow();
  usleep(800000);

  drawChar('D', 15, 50);
  refreshWindow();
  
  // Loop forever until user enters 'q'
  char c;
  do { 
    c = inputChar();
  } while ( c!= 'q' );

  // Close ncurses
  endCurses();
  
  return 0;
}
Initializing Ncurses
startCurses();
This line of code should appear at the point in your main() when you're ready to go into "ncurses mode". When this is executed, the screen goes blank and you are ready to write to arbitrary locations in the terminal window.

Draw a character and refresh screen
This code draws the char 'B' at position (20,30) in the terminal.

drawChar('B', 20, 30);
refreshWindow();
Important: The way drawing works in ncurses is this. Think of ncurses as working on its "painting" in private. For each call to drawChar, ncurses does indeed draw the char, but you don't get to see it, not until you call refreshWindow(), at which point ncurses shows you its masterpiece all at once! When you have many things to draw before the picture is complete, make all the many drawChar() calls involved, and then only call refreshWindow once when you are done! How about a pause after you draw? The following sleeps your program for 800000 microseconds (i.e., 0.8 second):
usleep(800000);     // need: #include <unistd.h>

Non-blocking I/O with inputChar()
Ncurses (at least as we've initialized it) changes the model for how we read data. You fetch the next character with a call to inputChar(). The funny thing is that this is "non-blocking I/O", which means that you don't hang around and wait for input to come.

If you call inputChar() and nobody presses a key, inputChar simply returns immediately with an error code.
Contrast this with cin >> c or cin.get(). Thus, this loop just spins waiting for inputChar() to report that the user has pressed q.

Exiting Ncurses
The endCurses() function call is required in order for ncurses to clean up after itself and restore the terminal window.

Important Compile and run this program (pressing 'q' will quit). Remember to compile like this:
clang++ -Wall -o p1 p1.cpp easycurses.cpp -l ncurses
The -Wall shows compiler "warnings", which it's actually always a good idea to do.

Part 1b: debugging pro-tip

Suppose in the Part1 code we wanted to print out an debugging message "Printed a B!" just after writing the "B". We'd just add the line
cout << "Printed a 2!" << endl;
... immediately after the drawCharAndRefresh('B',20,30). Compile and run that! What you will find is that this message pops up in the middle of our ncurses screen and messes things up. The "C" is no longer printed immediately to the right of the "B". It's a real problem when your debugging messages cause new bugs! So how can we write out debugging messages in an ncurses program?
  1. Use the standard error output stream (cerr) instead of standard out (cout) for the debugging message. This doesn't solve the problem immediately, but it helps. To see how ...
  2. Change your error message(s) to use cerr rather than cout, e.g.
    cerr << "Printed a B!" << endl;
    ... and run your program like this:
    ./p1 2> err
    What this does is run your program as usual except that any output written to standard error (cerr) gets "redirected" to the file err, which is created or overwritten each time this is run. Let your program run to completion, then give the command cat err to see what is written.
  3. Now, what you'd really like is to see the debugging output as it happens. Here's how you do that: Open a separate terminal window, cd to the lab directory containing your program and the "err" file, and give the command
    tail -f err
    Go back to your original terminal and type something like this:
    echo "whoooooooo" > err
    You should see the whoooooo pop up in the other terminal. Spooky, eh?
  4. Now run p1 in the original terminal like this:
    ./p1 2> err
    ... and watch as your ncurses window is pure and unblighted by debugging messages, but the message does pop up at the appropriate time in the second window. What's going on here is this: the 2> err is telling the shell to redirect standard error (cerr in our program) output to the file err. Meanwhile, tail -f is constantly monitoring the file err, printing out any new lines that get written to the file. You can keep rerunning, compiling and debugging your program without ever having to rerun tail -f, which is nice.
Note: This debugging technique is really useful!

Part 1c: Making Your Life Easier [Dr. Brown and Dr. Taylor's students: I'm recommending not to use this.]

Download rundebug to the same directory as your other files. This automates Part 1b above. The script will open a second debugging terminal for you automatically and then run your p1 program. Just download it, and turn its executable bit on:
chmod 755 rundebug

Now instead of running ./p1 you just:

./rundebug p1
After running, close the debug window that pops up.

NOTE: There is nothing to submit for part1.

Part 2 [30pts]: coming and going (name p2.cpp)

Write a program that reads in input from the user like this
4
a (10,15)
x (14,29)
k (5,5)
x (18,37)
before going into ncurses mode and then, once in ncurses mode, draws the letters to the screen at the specified positions, one at a time, in order, with 0.8sec pauses in between, and then, after another 0.8sec pause, erases them from the screen all at once. Erasing means writing a ' ' in the positions you want to erase. Remember to call refreshWindow() only once, i.e. after all the ' ' characters are written. if the user presses 'q', quit the program (remember to call endCurses()!), otherwise loop back to drawing the charcters again.

Important! I fully expect that this will be done with a good design that makes use of structs --- probably more than one --- and functions, and that the solution would work for a wide variety of inputs, not only for the input shown here.

Submit as: ~/bin/submit -c=SI204 -p=lab11 p2.cpp

Part 3a: You got to move it, move it (name p3.cpp)

Now we will animate things, i.e. each character will move on the screen. To start things off, let's have a single character, drawn as an 'X', and let's have the little X start at row 15, column 30. The character will have a direction associated with it — north, south, east or west. Start your X moving to the right (i.e., east). Note: design this program keeping in mind that ultimately there will be many characters moving simulatneously.

Your program will consist of a loop:

do {
  // draw character


  refreshWindow() // These two lines correspond to flipping to 
  usleep(80000);  // the next frame in the animation.

  // use inputChar to see if the user has pressed 'q'

  // move character one step in its current direction

}while('q' has not been pressed);

endCurses();

Important! At some point your character moves of the screen, and ncurses goes crazy --- nothing it draws makes sense anymore. Just kill the program with ctrl-c, and don't worry about it, because ...

Part 3b [40pts]: Not all who wander are lost (add on to p3.cpp)

Finish off Part 3 by making it take longer before your character runs off the screen. To make the character wander around a bit, let's say that at each step, immediately before actually moving, the character has a 1-in-10 chance of turning (changing its current direction by a 90 degree turn) before taking a step. Having decided to turn, it'll be 50/50 whether the turn is right or left.

Note 1: The struct representing a character (you did use a struct, right?) Is going to have a field that allows you to remember its current direction. Think of compass directions: N, S, E, W. Remember, though, that since we are using row/column coordinates, moving N=north means decreasing the row, while moving E=east means increasing column.

Note 2: seed the random number generator with srand(time(0)); or else it will do the same thing every time — that would be boring!

Submit as: ~/bin/submit -c=SI204 -p=lab11 p2.cpp p3.cpp

Part 4 [20pts]: Momma should I build a wall? (name p4.cpp)

Though your single animated character makes random turns, eventually it's going to make its way off the screen and strange things will happen. It'd be much cooler if the screen acted like a walled-in space that the character cannot escape. So let's do that. The only real difficulty here is that you need to know the height and width of the terminal window (in characters). Easycurses has a function for that:
void getWindowDimensions(int& row, int& col); // declared in easycurses.h
So the call getWindowDimensions(height,width) sets height to the number of rows on the screen, and width to the number of columns. Note, then, that the valid positions you can drawChar(row,col) to are when 0 ≤ row < height and 0 ≤ col < width.

Now that you can get the height and width of the terminal window, when you come to a "move" step you must check to see whether the move in question would take you off the screen. If it does, then instead of moving just change direction and leave it at that. Make the character "bounce" by simply reversing direction (north goes to south, east to west, etc). If you do this, your character will be walled in and will never leave the terminal window.

Important: In other words, before you move your character, you must check whether the move would push you off the screen, and switch direction instead of making the move.

Submit as: ~/bin/submit -c=SI204 -p=lab11 p2.cpp p3.cpp p4.cpp

Part 5 [10pts]: Yes it's that easy (name p5.cpp)

Now, instead of one randomly moving character, make it 20 (or 40 or 1000 or whatever you want!). If you've done things right, you should be able to do this trivially. If you haven't made good use of structs and functions, it might be painful!

Important! Only do refreshWindow() once per loop iteration, do it after all characters have been redrawn, and do it immediately prior to the call to usleep This is the way animations work: draw a whole new scene, and only overlay it once when it is all complete.

Submit as: ~/bin/submit -c=SI204 -p=lab11 p2.cpp p3.cpp p4.cpp p5.cpp

Part 6 (going further): Imperio (name p6.cpp)

Now, let's make one character that's different. This will be a character that the user controls. To keep it simple, we'll use the a,s,d,w keys. Immediately after the usleep(), and prior to actually moving anyone, do a
char kb = inputChar();
... and if kb is an 'a', change the one character's direction to west, a 'd', change it to east, an 's' change it to south, and a 'w', change it to north. Note that this one character won't be subject to the random direction changes, and we won't worry about the walls for him ... it'll be up to the user to keep the player on the board.

(Typing a 'q' should still be used to quit the game.)

Submit as: ~/bin/submit -c=SI204 -p=lab11 p2.cpp p3.cpp p4.cpp p5.cpp p6.cpp

Part 7 (going further): And in the end ... (name p7.cpp)

One last thing, if the user-controlled character collides with one of the other characters, or runs off the board, he dies and the game is over. When that happens, you might want to pause for a second or two before exiting ncurses. Oh, it'd also be a nice touch to report the number of steps (or seconds) the user stayed alive. You could add other fun things ... the tempo could increase, or more guys could enter the game or ... whatever else you can think of.

Submit as: ~/bin/submit -c=SI204 -p=lab11 p2.cpp p3.cpp p4.cpp p5.cpp p6.cpp p7.cpp