Learn Programming in Java


<<Previous | ToC | Next >>

ASCII Graphics & Subroutine(s)

After you have your Seaman game working with just keeping score, we can work on displaying the man on a boat or something like that. Traditionally, as a paper&pencil game, this is drawn with stick figures, but regular Java does not have stick figures without a lot of effort. Later on we can do this with GameEngine graphics (which is a little simpler than plain Java), but for now we'll use the traditional ASCII graphics, placing characters on the page arranged so the result resembles the desired picture.

You should understand, ASCII graphics, like teletypes and command lines, is the buggy-whip of computer technology, necessary when the car won't start, but largely replaced by more modern technology like electric cars, full-color graphics monitors and drag'n'drop direct manipulation. So if you want to skip this section, you won't miss much -- except we do some fancy things with arrays that you should know about.

Here is my simple image, when the word was not guessed and the guy is fully rendered. You are not stuck with this, you can do your own, but my explanation will go better if you just use mine for now:

        /|     _
       / |    ( )
      /  |    _|_
     /   |   / | \
    /    |  /  |  \
   /     |     |
  /      |    / \
 /       |   /   \
/________| _/     \_
______________________
\                    /


It is important for this to look good, that it be displayed in a monospace font like Courier, which most Java consoles already are. There are eleven lines of text in this graphic, and the simplest way to display it would be with eleven println commands, one for each line. We'll start with that.

Keep your existing game, we'll make this a separate subroutine, then call it from a single line in the main program. Each time we rewrite this subroutine, we can give it a new name, then change just that one line in the main program. When I did this, I also printed the whole seaman on his boat as a banner at the front, one more added line. I called mine "SeaAsky" and gave it a single integer parameter, the number of body parts to draw, which is how many wrong guesses so far. That would range from zero (just the boat) to fully seen = 2 legs + 2 arms + 1 spine + 1 head = 6. The subroutine returns no value -- that's called "void" in Java -- and is not Object-oriented (yet), which is spelled "static" in Java. Normally also you would declare it "private" because it's only being used here locally. But it doesn't affect anything at all. Place this subroutine inside your "class Hello", in the blank line just before your main() declaration, like this:

public Class Hello {

  private static void SeaAsky(int nerrs) {

    } // end of SeaAsky

  public static void main() {
...

Notice that BlueJ puts a second (solid) yellow box around your new subroutine.

Now we can start to fill in the code for this subroutine. If we do the whole seaman first, then it's easier to do the other variations by copy and paste. I did the first and last three lines, you can fill in the rest:

private static void SeaAsky(int nerrs) {
  if (nerrs >= 6) {
    System.out.println("        /|     _");
    System.out.println("       / |    ( )");
    System.out.println("      /  |    _|_");
    System.out.println("");
    System.out.println("");
    System.out.println("");
    System.out.println("");
    System.out.println("");
    System.out.println("/________| _/     \\_");
    System.out.println("______________________");
    System.out.println("\\                    /");
    return;}
  if (nerrs==5) { // insert a copy of the above,
                 // ..less the sailor's head
    return;}
  // etc
  } // end of SeaAsky
Notice that I doubled up the back slant. It's an escape character in Java, it means "take the next character literally" (or alternatively "interpret the following letter as the name of a control character"), so a pair of them together takes the second one literally, which is what we want. You need to do it for each of your back slants.

Let's try it out. In your existing main program, after the variable declarations, but before any other code, insert the single line

SeaAsky(6);


Compile it and run it. Did it work OK? If not, can you figure out why not?

We now have six more values of nerrs (zero through five) to fill out code for. Here are the first two plays, no errors, then one (the left leg alone), and so on:

        /|         (0)             /|         (1)
       / |                        / |
      /  |                       /  |
     /   |                      /   |
    /    |                     /    |
   /     |                    /     |
  /      |                   /      |    /
 /       |                  /       |   /
/________|                 /________| _/
______________________     ______________________
\                    /     \                    /


Zero is just the sail and the boat hull/deck, copy the same print lines you used for the full seaman, but erase the seaman. Can you do that? Try it. For each error count, you add more and more of the seaman's body. The easy way is to work backwards from six, so for five you start with the same eleven lines, but erase just the head. Then copy the most recent eleven lines (missing just the head), and erase also the right arm for number 4. Then copy #4 and erase also the left arm for #3. You can do that.

Now, to test the creation. Down in your main code, where you print out the number of errors, insert another call to SeaAsky, but giving the number of errors as a parameter instead of 7. Play your game (but lose), and see if the seaman is built correctly.

You could show this program to your family and friends, and they will all say nice things to you. Show it to a programmer, and he will yawn. That's because you have a hundred lines of code, when it could be much smaller and more elegant. Arrays are more elegant than straight code. Let's do that.
 

String Arrays

We could make an array of the eleven lines (quoted strings) to draw the seaman, or I think I would do a two-dimensioned array (array of String arrays), so that the first index is how many misses (added body parts) so far, and the second index is the eleven lines of text for that view of the seaman on his boat. Does that make sense? Here is the beginning (ther empty boat) and the last two lines at the end (with the man fully drawn and ready to set sail, and before that missing his head) of that declaration, copied and pasted from my images above:
final String[][] SeaStrs = {
    {"        /|", "       / |", "      /  |",
     "     /   |", "    /    |", "   /     |",
     "  /      |", " /       |", "/________|",
     "______________________",
     "\\                    /"},
    ...
    {"        /|",
     "       / |",
     "      /  |    _|_",
     "     /   |   / | \\",
     "    /    |  /  |  \\",
     "   /     |     |",
     "  /      |    / \\",
     " /       |   /   \\",
     "/________| _/     \\_",
     "______________________",
     "\\                    /"},
    {"        /|     _",
     "       / |    ( )",
     "      /  |    _|_",
     "     /   |   / | \\",
     "    /    |  /  |  \\",
     "   /     |     |",
     "  /      |    / \\",
     " /       |   /   \\",
     "/________| _/     \\_",
     "______________________",
     "\\                    /"}};


Notice that for each of seven plays (boat only = no guy, then left leg, right leg, backbone, left arm, right arm, and finally a head on the whole guy) you need eleven string lines, a total of 77 string constants, eleven in each of seven brace pairs, all within a containing brace pair (you get to fill in the missing eight in the middle, where the three dots are). Don't forget to double (escape) the back slants. Whew! Later, I'll show you how to do this in the Game Engine, which looks nicer and has other advantages.

I would make a new subroutine SeaAsk2d:

private static void SeaAsk2d(int nerrs) {
  final String[][] SeaStrs = {...

  } // end of SeaAsk2d


Now, instead of 77 print statements, you can do it with one, inside a for-loop:

for (int nx=0; nx<11; nx++) System.out.println(SeaStrs[nerrs][nx]);


But we still have that big ugly array of strings. We can do better.
 

Index Arrays

Notice that we have seven copies of the boat, all identical except some lines have some part of the seaman on the right. Suppose we split those lines into eleven boat lines, and then nine optional lines of body parts. Actually we have two versions of the arms and legs, one with the left leg or arm only, and then a second set with both, for 15 body part lines, and a total of only 26 lines for all possible boat views (instead of 77). I added one more element in this array, an empty string at the front (the 0th element) to serve where a body part is not yet drawn, in a single-dimensioned array like this:
final String[] uniques = { "",                // 0
    "        /|", "       / |", "      /  |",  // 1..3
    "     /   |", "    /    |", "   /     |",  // 4..6
    "  /      |", " /       |", "/________|",  // 7..9
    "______________________",                   // 10
    "\\                    /",                  // 11
    "     |", "     __", "    ( )", "    _|_",     // 12..15
    "   / |", "   / | \\", "  /  |", "  /  |  \\", // 16..19
    ... // 20..25
    };
You get to fill out the legs part of the array. Hmm, it looks like I made only a single line for both shoulders, to be used when the left arm is added and kept there when the right arm is added.

We'll call this version SeaAsk3i because we now have a 3-dimension array, but now it's an integer array, and the numbers are the index numbers of the string array, as annotated in the comments. The first dimension is still the error number, zero for no errors (just the empty boat), six for a fully drawn seaman on the boat as before. The third dimension has two elements for each line of each error number, the boat on the left, and then the sailor's body part on the right, or else zero (meaning no body part). The first line of every error number less than six draws just the tip of the sail (uniques[1]) and then nothing (uniques[1]), is a single line, {1,0}. The next line has the next segment of the sail ([2]) and again nothing for six error numbers, {2,0}, and then for error numbers six and seven, the sailor's head {2,14}. I set uniques[12] to be the spine, which we use on line 5 of every error number from three on, and for the rest of the spine up the sailor's head on error number three.

  final int[][][] iSea = {{{1,0},{2,0},...,{9,0},{10,0},{11,0}} // no errors
      {{1,0},{2,0},...,{9,24},{10,0},{11,0}} // one error
      ...
      {{1,0},{2,14},...,{9,24},{10,0},{11,0}} // six errors
      {{1,13},{2,14},...,{9,24},{10,0},{11,0}}}; // seven errors
I left some gaps for you to fill in. The only tricky part is the arms and legs, where you have separate line numbers for the left arm and leg and both arms and both legs. In my numbering above, string line 15 is both shoulders, 16 is the top of the left arm, and 17 is the tops of both arms; similarly 18 is the left hand and 19 is both hands. So for four errors, your index array would show '{3,15},{4,16},{5,18}' in those three positions, and for five or more errors you would put '{3,15},{4,17},{5,19}' there. Does that make sense?

You need to convince yourself why those are the correct numbers, or you will not be able finish out the table. Remember, you are defining the leg segments, you need to make sure your ASCII graphic leg segments correspond to the index table values you choose for them. Don't worry if you make a mistake, it will be obvious when you go to draw the sailor, and then you can count over to what it drew against what it should have drawn, and correct the miscount.

So your new subroutine starts out like this:

private static void SeaAsk3i(int nerrs) {
  final String[] uniques = { "", // 0
     ...};
  final int[][][] iSea = {{{1,0},{2,0},...,
      ...}};

  } // end of SeaAsk2i

You get to fill in the missing table data.

Now let's build the for-loop carefully. The loop part is the same, but the print statement has a boat part and a sailor part. What's different is how we got to the text being printed. Recall it's somewhere in the new uniques array, but where? The new array iSea is still an array of arrays, and the first index is still the number of errors so far -- are you still with me? -- and the second index is still the control variable of the for-loop, but each entry in the array, instead of being an actual string to print, it is a pair of numbers, the first is the index of that string in uniques which is the boat segment for that line, and the second number is either zero (no body part to draw) or else the index of that body part in the same array. We collect these two numbers into two integer variables bx and sx, which each then indexes the uniques array to fetch one of the two strings to be drawn on that line. So we could write it like this:

for (int nx=0; nx<11; nx++) {
  int bx = iSea[nerrs][nx][0];
  int sx = iSea[nerrs][nx][1];
  System.out.println(uniques[bx] + uniques[sx]);}
or alternatively:
String aLine = "";
for (int nx=0; nx<11; nx++) {
  for (int kx=0; kx<2; kx++)
    aLine = aLine + uniques[iSea[nerrs][nx][kx]];
  System.out.println(aLine);
  aLine = "";}


Do you see how that works? Wouldn't you agree, this is a lot more "elegant" than 77 print lines?

Important! Do not skip to the next page until you understand exactly how this works. If necessary, put the 4-line version of the for-loop in your program and step through it in the debugger, so you can see where the numbers come and go. If you need help, that's what the mentor is for. If you go into programming, then you need to be able to read (and understand) code like this even if you never write another complicated array indexing program in your life.

Next: Calculator

<<Previous | ToC | Next >>

Revised: 2021 August 28