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.

Since each "throw" of the sticks is a series of "heads" or "tails" ("white" sites and "black" sides) I decided the most memory efficient way to store the results was by generating a single random integer, then using a binary AND to only use the last four bits. This is a good assumption if you know that your random number function also generates random bits in those positions. For example, this method is not a great idea if the random number function tends to generate more odd numbers than even numbers. (When in doubt, write a short test program to try it out.)

int
throw_sticks(void)
{
   /* returns four "throwing sticks" as a single bit pattern */
   /* assumes you've already called srandom(seed) */

   int rnd;

   rnd = (int) random();

   /* binary goes like 8421 */
   /* 8+4+2+1=15 is binary 1111 */

   return (rnd & 15);
}

I could have written this as a one-line function, at the cost of readability. Breaking this function into two statements with a temporary rnd value makes more sense.

Practice function 2.

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

I'll write this function with the assumption that the "sticks" throw is passed as a single integer value, where only the last four bits hold any meaning. While this sample function doesn't use conio to display the "sticks" results in a window, you can easily modify the function to do so. Once I've created the display string, you just need to print the result to the screen. I'm using puts in this example, but you could also use _outtext if you used conio.

void
show_sticks(int sticks)
{
   /* remember the bit pattern for the sticks is binary 1111 */
   /* and binary goes like 8421 */

   /* so I can evaluate each "stick" as a separate binary AND value */

   char display[] = "x x x x";
   int row;

   /* fill in the display for each bit */
   /* 333 is a filled box, 263 is a thin vert line */

   display[0] = (sticks & 8 ? '\333' : '\263');
   display[2] = (sticks & 4 ? '\333' : '\263');
   display[4] = (sticks & 2 ? '\333' : '\263');
   display[6] = (sticks & 1 ? '\333' : '\263');

   /* display the results */

   for (row = 0; row < 4; row++) {
      puts(display);
   }
}

I used the DOS extended character set in this example. When I repeat the single line of text several times, the "white" throwing sticks will appear as thick lines, and the "black" throwing sticks will appear as narrow lines.

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.

Converting the "sticks" throw into a number is basically counting the number of "1" bits. You can do this in several ways; I chose to iterate through four bits (0–3) and shifted the "1" bit left the appropriate number of places. This allowed me to selectively "mask" the value I wanted to pick out using the binary AND.

int
my_move(int sticks)
{
   /* remember the bit pattern for the sticks is binary 1111 */
   /* and binary goes like 8421 */
   /* so I can evaluate each "stick" as a separate binary AND value */

   int nmoves = 0;
   int shift;

   for (shift = 0; shift <= 3; shift++) {
      if (sticks & (1 << shift)) {
         nmoves++;
      }
   }

   return nmoves;
}

You could also write this function using four separate statements, where each tests a different bit (8, 4, 2, and 1) using the binary AND. For example:

   if (sticks & 8) {
      nmoves++;
   }
   ⋮

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.

This function essentially contains the core game logic. To determine if the requested move is valid, you need to run through each of the rules. If you're on the last three squares, you need to "throw" exactly that number to move off the board. You must stop exactly on the fifth square from the end. And so on. this function also must determine if the requested move would result in the capture of an opponent's piece.

I wrote my function to return a True value (non-zero value) if the move is valid, and a False value (zero) if it is not. Specifically, the function returns 1 if the move would advance a piece, -1 if the move would capture an opponent's piece, and 0 if the move is not allowed.

int
is_valid_move(int *board, int player, int move, int sq)
{
   /* return True or False if the player can make this move from sq */
   /* return: +1 if yes, -1 if yes&capture, 0 if no */

   int otherplayer;

   /* it's dumb to allow move=0 */

   if (move == 0) {
      return 0;
   }

   /* this function encapsulates most of the rules */

   switch (sq) {
   case 29:                           /* "1" square */
      /* may only move off if move=1 */
      if (move == 1) {
         return 1;
      }
      else {
         return 0;
      }
      break;
   case 28:                           /* "2" square */
      if (move == 2) {
         return 1;
      }
      else {
         return 0;
      }
      break;
   case 27:                           /* "3" square */
      if (move == 3) {
         return 1;
      }
      else {
         return 0;
      }
      break;
      /* there is no case for sq=26 because that's the trap square */
   case 25:                           /* the "go from here" square */
      /* cannot move if that would require resurrection when sq=14 is occupied */
      if (board[14] == 0) {
         return 1;
      }
      else {
         return 0;
      }
      break;
      /* squares before the "go from here" .. must stop on sq=25 exactly */
   case 24:
   case 23:
   case 22:
   case 21:
      if (move <= (25 - sq)) {
         return 1;
      }
      else {
         return 0;
      }
      break;
   default:
      /* I think that's everything .. check if this is a capture */
      otherplayer = (player == 1 ? 2 : 1);

      if (board[sq + move] == otherplayer) {
         return -1;                    /* capture */
      }
      else if (board[sq + move] == player) {
         return 0;                     /* can't capture your own piece */
      }
   }

   /* if not caught already, then the square you're trying to move to
      must be okay */

   return 1;
}

It is helpful to remember that the game board is represented as a 1-dimensional array (indices 0–29). Each entry in the board array is 1 to indicate a piece belonging to player 1, 2 for a piece belonging to player 2, and 0 if the square is empty.

With that assumption, you can check if a space holds an opponent's piece by setting the otherplayer variable. The if shorthand makes sense here, since we only need to set the variable to 1 or 2.

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.

Having written a function to test if a given move is valid, you can use the is_valid_move function to test other possible moves. Because the game board is only 30 squares, a quick way to determine if there are any valid moves is to iterate across all the squares.

int
any_valid_move(int *board, int player, int move)
{
   /* determine if there are ANY valid moves on the board for this player */

   int sq;

   /* just all the squares and test each one */

   for (sq = 0; sq < 30; sq++) {
      if (board[sq] == player) {
         if (is_valid_move(board, player, move, sq)) {
            return (sq + 1);           /* true: at least one valid move found */
         }
      }
   }                                   /* for */

   /* we reach this point only if no valid moves were found */

   return 0;
}

I like to return values that are meaningful, so I wrote this function to return the square (counting from 1) where I found the first valid move. This gives a non-zero value, which C treats as a True value. If the function found no valid moves, it returns zero, which is a False value.