Part 9. Putting it together

We've learned quite a lot about C programming! This series is inteded as a "C programming 101" topic, so we've only covered the basics of C programming—but with what we've learned, we can now write some serious applications. What you create is limited only by your imagination.

Let's get started by writing a turn-based game. To help us focus on the core parts of the application, I'll first focus on the "outline" of a two-player game. We can add to that later, to bring the game to life.

Senet game

For my demonstration, I decided to write an implementation of the Senet board game. Senet is a very old game, dating back to ancient Egypt. Because it is so old, and no one in ancient Egypt preserved the rules for later generations, the exact rules are somewhat of a mystery. Historians have tried to figure out the rules of Senet, but they disagree in the details. I prefer a variation of a common Senet rule set:

Board layout
The Senet game board is 3 rows of 10 columns each. Game play moves in a "backwards S" configuration. Some square on the board are significant, which I'll explain later.
Game play
Each player has 5 pieces to move off the board. (Each player is a different color, let's say and .) Start the game by placing the pieces on the first row, alternating the player 1&2 pieces. Players take turns to move a single piece. To move: "throw" a set of four flat throwing sticks (one side is black, the other is white) and move the same number of square as you have "white" sides showing (1 white face up, move 1 square…). If you cannot make a valid move, you lose your turn.
Attacks
Each square on the board can only contain one piece. If you land on another player's square, you swap places.
Special squares
Several squares hold special significance. On the last 3 squares, players must "throw" exactly 1, 2, or 3 to move off the board. The fourth square from the end is a "trap." Any player who lands on this square automatically jumps back to square 15. If there is already a player on square 15, you cannot make this move. (Historians disagree on this rule, but that's the rule I prefer.) The fifth square from the end holds special significance: you must reach this square by an exact throw before you can move on.
3 2 1

Programming the board

Senet is a great example program, because it is a simple game that requires some math to set up the board correctly. The 30 squares run in a "backwards S" configuration that makes "up," "down," "left," and "right" tricky to navigate directly.

1 2 3 4 5 6 7 8 9 10
20 19 18 17 16 15 14 13 12 11
21 22 23 24 25 26 27 28 29 30

I counted the squares from 1 in the above diagram. But remember that C counts array elements from 0.

Before I start programming, let me consider a few functions I'll need to to write the Senet program. I know that I'll want to "draw" in each square, to update each player's piece, or to put a special indicator in the square, or to highlight the square during selection. This calls for a general function to define the "window" for each square. I'll write this function by passing the number of the square, and I'll let the function figure out the row and column location.

This is a good time to decide how big each square should be. I would like the squares to be visually distinct from each other, so I should have one column and one row between each. With an 80 column screen, that is easy math: each square should be 7 columns wide, plus an extra blank column after it. That lets me fit 10 squares in an 80 column display.

Each character on the 80×25 display isn't a perfect square. The pixels that make up each character are about 8×16. So I can make a rough square by making each square 7 columns wide and 4 rows high. I'll put an extra row between each, to visually separate the squares.

This set_square function performs the calculation to locate the square's row and column, and defines the text window with the appropriate dimensions:

void
set_square(int sq)
{
   int row, col;

   /* 0  1  2  3  4  5  6  7  8  9
      19 18 17 16 15 14 13 12 11 10
      20 21 22 23 24 25 26 27 28 29 */

   /* squares are 7 columns wide, at every 8: 1,9,17,... */

   if ((sq >= 10) && (sq <= 19)) {
      /* middle row counts backwards */
      col = (19 - sq) * 8 + 1;
   }
   else {
      /* top and bottom rows count forwards */
      col = (sq > 19 ? sq - 20 : sq) * 8 + 1;
   }

   /* squares are 4 rows high, at every 5 rows */
   row = sq / 10 * 5 + 1;

   /* squares are 4 rows tall .. so 1+3=4 */
   /* squares are 7 cols wide .. so 1+6=7 */
   _settextwindow(row, col, row + 3, col + 6);
}

As part of the column calculation, I've used the if-else shorthand to test if the square sq is in the first or last row. Multiplication by 8 will set the squares apart by 8 screen columns, and adding 1 will ensure the squares start at screen column 1.

For the row calculation, I rely on integer math. Dividing the square number (0–29) by 10 should give a number (0–2) to indicate the row. Multiplying this by 5 will set the squares apart by 5 (0, 5, or 10). Adding 1 at the end will ensure squares start at screen rows 1, 6, or 11.

Once I have the text window defined, I can draw in it, including clearing it with a background color. This draw_square function calls set_square to define the square's text window, then clears it with a color before drawing any special characters.

void
draw_square(int sq, int active)
{
   set_square(sq);

   /* set the correct color, depending if this is an "active" (highlighted) square */

   ⋮

   _clearscreen(_GWINDOW);

   /* add other markers to the square for the special squares on the board */

   ⋮
}

Later in the program, I will want to provide a mechanism to select a square (such as for a player to move a piece). So I've included a parameter to indicate if this square is "active" or "highlighted," and to set the color appropriately:

void
draw_square(int sq, int active)
{
   set_square(sq);

   if (active) {
      _setbkcolor(3);                  /* cyan */
      _settextcolor(11);               /* bright cyan */
   }
   else {
      _setbkcolor(1);                  /* blue */
      _settextcolor(9);                /* bright blue */
   }

   _clearscreen(_GWINDOW);

   /* special squares */

   switch (sq) {
   case 29:
      _settextposition(1, 4);
      _outtext("\340");                /* alpha */
      break;
   case 28:
      _settextposition(1, 3);
      _outtext("\341 \341");           /* beta */
      break;
   case 27:
      _settextposition(1, 2);
      _outtext("\342 \342 \342");      /* gamma */
      break;
   case 26:
      _settextposition(1, 1);
      _outtext("\030\030\030\030\030\030\030"); /* up arrow */
      break;
   case 25:
      _settextposition(1, 1);
      _outtext("\260\260\260\260\260\260\260"); /* dotted */
      break;
   case 14:
      _settextposition(1, 1);
      _outtext("\031\031\031\031\031\031\031"); /* down arrow */
   }

   /* corner squares */

   switch (sq) {
   case 0:
      _settextposition(4, 1);
      putch('\032');                   /* right arrow */
      break;
   case 9:
      _settextposition(4, 7);
      putch('\277');                   /* corner */
      break;
   case 10:
      _settextposition(1, 7);
      putch('\331');                   /* corner */
      break;
   case 19:
      _settextposition(4, 1);
      putch('\332');                   /* corner */
      break;
   case 20:
      _settextposition(1, 1);
      putch('\300');                   /* corner */
      break;
   case 29:
      _settextposition(4, 7);
      putch('\032');                   /* right arrow */
   }
}

In this function, I've included some references to the DOS extended character set, including the line-drawing characters. The comments indicate what each special character code represents, including a dotted patter, arrows, and corner connectors.

I should also write a function to draw a player's piece on the square. This seems pretty important for a board game that moves pieces around:

void
draw_player(int sq, int p)
{
   set_square(sq);

   _settextposition(3, 4);
   _settextcolor(15);                  /* bright white */

   switch (p) {
   case 1:
      _outtext("\001");
      break;
   case 2:
      _outtext("\002");
      break;
   default:
      _outtext(" ");
   }
}

My draw_player function uses the DOS extended character set again, this time to draw a filled or empty "happy face" character. To me, that seems like it's representative of a player.

The function uses set_square at the beginning to ensure the text window is set appropriately. If I am careful to only call draw_player immediately after draw_square, then I can remove the call to set_square. That assumes the text window hasn't been modified since we drew the square. I'll leave it in for now, but you can remove set_square in your own program if you wish.

Selecting your move

Now that I have a function to draw each square, and to draw a player's piece in the square, I can write a function that lets the user select a square. The general outline of this function will be:

int
select_square(int *board, int player)
{
   /* print a prompt */

   ⋮

   /* highlight a square */

   ⋮

   do {
      /* loop until the user selects a highlighted square by tapping the Space key */

      ⋮

      key = getch();

      switch (key) {
        /* change which square is highlighted, depending on up/down/left/right */

        ⋮
      }
   } while ((key != 'Q') && (key != ' '));

   /* return the selected square (0-29) or -1 to quit */

   ⋮
}

I'll write the function following the above model. I'll print a prompt that the user should select a square with a piece to move, then enter a loop where the user can change the highlighted square using the up, down, left, and right arrow keys. After the loop, I'll need to return the index of the board array, or some other code if the user quit the game instead.

int
select_square(int *board, int player)
{
   int i = 0;
   int key;

   print_message("select a piece to move .. Q to quit");

   draw_square(i, 1);                  /* 1 = active */
   draw_player(i, board[i]);

   do {
      key = getch();

      switch (key) {
      case ' ':
         if (board[i] == 0) {
            print_error("cannot select an empty square");
            key = 'x';                 /* reset key to not jump out of the loop */
         }
         else if (board[i] != player) {
            print_error("that is not your piece");
            key = 'x';
         }
         break;

      case 0:
         /* extended key .. call getch() again for ext key */
         key = getch();

         switch (key) {
         case 'K':                    /* left */
            if ((i >= 10) && (i <= 19)) {
               /* middle row */
               draw_square(i, 0);      /* not active */
               draw_player(i, board[i]);

               i++;

               if (i == 20) {
                  i = 19;
               }

               draw_square(i, 1);      /* active */
               draw_player(i, board[i]);
            }
            else {
               /* top or bottom row */
               draw_square(i, 0);      /* not active */
               draw_player(i, board[i]);

               i--;

               if (i == -1) {
                  i = 0;
               }
               else if (i == 19) {
                  i = 20;
               }

               draw_square(i, 1);      /* active */
               draw_player(i, board[i]);
            }

            break;

         case 'M':                    /* right */
            if ((i >= 10) && (i <= 19)) {
               /* middle row */
               draw_square(i, 0);
               draw_player(i, board[i]);

               i--;

               if (i == 9) {
                  i = 10;
               }

               draw_square(i, 1);
               draw_player(i, board[i]);
            }
            else {
               /* top or bottom row */
               draw_square(i, 0);
               draw_player(i, board[i]);

               i++;

               if (i == 30) {
                  i = 29;
               }
               else if (i == 10) {
                  i = 9;
               }

               draw_square(i, 1);
               draw_player(i, board[i]);
            }

            break;

            /* 0  1  2  3  4  5  6  7  8  9
               19 18 17 16 15 14 13 12 11 10
               20 21 22 23 24 25 26 27 28 29 */

         case 'P':                    /* down .. jump to next row */
            if (i <= 9) {
               /* first row */
               draw_square(i, 0);
               draw_player(i, board[i]);

               i = 19 - i;

               draw_square(i, 1);
               draw_player(i, board[i]);
            }
            else if ((i >= 10) && (i <= 19)) {
               /* middle row */
               draw_square(i, 0);
               draw_player(i, board[i]);

               i = 39 - i;

               draw_square(i, 1);
               draw_player(i, board[i]);
            }
            /* else .. don't move if bottom row */

            break;

         case 'H':                    /* up .. jump to previous row */
            if ((i >= 10) && (i <= 19)) {
               /* second row */
               draw_square(i, 0);
               draw_player(i, board[i]);

               i = 19 - i;

               draw_square(i, 1);
               draw_player(i, board[i]);
            }
            else if (i >= 20) {
               /* third row */
               draw_square(i, 0);
               draw_player(i, board[i]);

               /* i = 29 - i + 10; */
               i = 39 - i;

               draw_square(i, 1);
               draw_player(i, board[i]);
            }
            /* else .. don't move if first row */
         }
      }
   } while ((key != 'Q') && (key != ' '));

   if (key == 'Q') {
      return -1;
   }
   else {
      return i;
   }
}

Printing messages

The select_square function started by displaying a message to the user, so I'll need a print_message function that will print a message to the screen. I'll put this at the bottom of the screen, so it doesn't get in the way of my game board. Just like our programming examples from part 8 of the series, we can easily display a one-line message by defining a text window from row 25 × column 1 to row 25 × column 80. I'll write a pair of functions to do the work for me: one to clear the message line, and another to print in it.

void
clear_message(void)
{
   _settextwindow(25, 1, 25, 80);
   _setbkcolor(7);                     /* white */
   _clearscreen(_GWINDOW);
}

void
print_message(char *msg)
{
   clear_message();
   _settextposition(1, 1);
   _settextcolor(0);                   /* black */
   _outtext(msg);
}

But what if I need to print an error message? I could use the same print_message function, but that would display the error in the same color as any normal message. To get the user's attention, let's write a print_error function that displays error messages in a red message bar. To get the user's attention, we'll use the sound function from i86.h to turn on the PC speaker. This sets a constant noise, so we'll wait for a bit, then turn it off; the effect will be a "beep" alert at whatever pitch we selected in the sound function.

void
clear_error(void)
{
   _settextwindow(25, 1, 25, 80);
   _setbkcolor(4);                     /* red */
   _clearscreen(_GWINDOW);
}

void
print_error(char *msg)
{
   clear_error();
   _settextposition(1, 1);
   _settextcolor(14);                  /* bright yellow */
   _outtext(msg);

   sound(900);
   delay(50);                          /* milliseconds */
   nosound();

   delay(500);
   clear_message();
}

Putting it together

And with those functions, we have the outline of a Senet game! This will draw the game board, and allow the user to select a square that contains a piece to move:

#include <conio.h>
#include <graph.h>
#include <i86.h>                       /* sound, nosound, delay */

void
clear_message(void)
{
   ⋮
}

void
print_message(char *msg)
{
   ⋮
}

void
clear_error(void)
{
   ⋮
}

void
print_error(char *msg)
{
   ⋮
}

void
set_square(int sq)
{
   ⋮
}

void
draw_square(int sq, int active)
{
   ⋮
}

void
draw_player(int sq, int p)
{
   ⋮
}

int
select_square(int *board, int player)
{
   ⋮
}

int
main()
{
   int i;
   int player = 1;
   int board[30];

   /* clear screen, draw board */

   _setvideomode(_TEXTC80);
   _displaycursor(_GCURSOROFF);

   _setbkcolor(2);                     /* green */
   _clearscreen(_GCLEARSCREEN);

   for (i = 0; i < 30; i++) {
      draw_square(i, 0);               /* 0 = not active */
      board[i] = 0;
   }

   /* draw players */

   for (i = 0; i < 10; i += 2) {
      draw_player(i, 1);
      board[i] = 1;

      draw_player(i + 1, 2);
      board[i + 1] = 2;
   }

   /* play game */

   select_square(board, player);

   /* quit */

   print_message("THANKS FOR PLAYING .. press any key to quit");
   getch();

   _displaycursor(_GCURSORON);
   _setvideomode(_DEFAULTMODE);
   return 0;
}

With this starting point, we would need to add a few functions to finish the game: "Throw" the set of throwing sticks. Determine if the player made a valid move. Swap pieces during an attack. Move pieces from the "trap" on square 27 to the "resurrection" on square 15. Move pieces off the board from squares 28, 29, and 30. The first player to move their 5 pieces off the board wins.

Senet game

We will continue working on this program next week.

PRACTICE

Let's get ready for next week by writing these functions:

Practice function 1.

Write a throw_sticks function that simulates "throwing" the four throwing sticks. Each stick shows either a "white" or a "black" side, so it's the same as flipping a coin. You can implement this in several ways. For example, you could write a function to return a struct of four "heads" or "tails" values—that's the same as generating four random numbers and returning four "even" or "odd" values. Or, you could write a function that generates a single random number, then return just the last four 0/1 bits using a binary AND.

Practice function 2.

Write a show_sticks function that takes the output from throw_sticks and displays the sticks visually on the screen.

Practice function 3.

Write a my_move function that takes the output from throw_sticks and determines the number of spaces the player can move their piece. If the "throw" is 4, then the player can move 4 spaces. If the "throw" is 0, then the player cannot move and loses their turn.

Practice function 4.

Write a function int is_valid_move(int *board, int player, int move, int sq), where move is the number of spaces to move (from practice function 3), to determine if the player's selected move from square sq is valid. For example, if the user selects to move 1 space from square 26 onto the "trap" on square 27, but there is already a piece on the "resurrection" square 15, the move is not allowed. Return a True (non-zero) value if it is a valid move, or a False (zero) value if it is not.

Practice function 5.

Write a function int any_valid_move(int *board, int player, int move) to evaluate the board and determine if the player can make any valid moves. In one "end game" example, the player might have already moved four pieces off the board, leaving one piece on square 29 (requires a "throw" of 2 to move off). If the "throw" is anything other than 2, the player cannot make a valid move and loses their turn.

Need help? Check out the sample solutions.