Programming in C

Chapter 5, 7/13/93

Completing the Second Program: Blackjack, Structures and Strings

In the previous chapter we developed the functions needed to assemble a blackjack- playing program. Now we will complete the set and put together a complete game. Along the way two new important C features will be introduced.
The code for the complete second program follows. Many functions are the same; several new functions have been added. And of course the main() function is completely new.
---
#include < stdio.h >
#include < stdlib.h >
#include < time.h >

/* A new include file: fetches handy text string functions. */
#include  < string.h >

/* The maximum number of players (including the computerized dealer). */

#define PLAYERS_MAX 10

/* The maximum number of characters in a player's name. */

#define PLAYER_NAME_MAX_LEN 31


/* The maximum number of cards in a player's hand. */

#define HAND_CARDS_MAX 10

/* A player_struct structure holds all the information associated with
  a particular player. */

struct player_struct {
  char name[PLAYER_NAME_MAX_LEN+1];
  int cards_total;
  int hand[HAND_CARDS_MAX];
  int games_won;
  int busted;
};

/* Now we declare it to be a type, so we can conveniently declare
  variables containing player information. */

typedef struct player_struct player_t;
  
/* The function card_print takes a card as represented by an integer
  between 1 and 52 and displays the name of the card. The actual function
  comes later; here at the beginning we give a "prototype" to
  tell the compiler what to expect. */ 
  
void card_print(int card);

/* The function deck_shuffle accepts deck of cards, as represented
  by an array of integers, and shuffles them into a random order. */

void deck_shuffle(int deck[]);

/* The function deck_draw draws a card from a deck. It returns 
  an integer between 0 and 51 representing the card. deck_draw()
  reshuffles the deck if necessary. The function accepts an
  array of cards and a pointer to the number of the top card
  in the array, which it will update after drawing a card. */

int deck_draw(int deck[], int *card_top);

/* deck_init accepts a pointer to a deck of cards and fills it
  with an initial set of unshuffled cards. */

void deck_init(int deck[]);

/* deck_print prints out all the cards in the deck. (Naturally this
  isn't used in an actual game, but the function will help us
  verify that the program is correct.) */

void deck_print(int deck[]);

/* Return 1 if player has hit blackjack (21), false (0) if
  not. Pass address of player structure. */

int player_hand_blackjack(player_t *player);

/* Return 1 if player has busted, 0 if not. */

int player_hand_busted(player_t *player);

/* Return the minimum value of the player's hand (aces counted
  as 1). */

int player_hand_min_value(player_t *player);

/* Return the maximum value of the player's hand (aces counted
  as 11). */

int player_hand_max_value(player_t *player);

/* Read a line of input from standard input (stdin) and store
  it into the buffer s. Discard any characters beyond
  max_len-1 (leaving the last space for a null terminating character). */

void line_read(char *s, int max_len);

/* Fetch an integer (followed by a carriage return) from standard
  input. Takes advantage of line_read(). Takes no arguments. 
  We replace scanf() with line_read() and int_read() because
  scanf() does not respect carriage returns well, which can lead
  to serious confusion for the user of the program. */

int int_read();

/* Add a card to the player's hand. */

void player_hand_add(player_t *player, int c);

/* Print out a player's status. */

void player_print(player_t *player);

int main() {
  /* The deck consists of an array of 52 integers. */
  int deck[52]; 
  
  /* The position in the deck of the topmost card. In a freshly
    shuffled deck, this position will be 0; when only one card
    is left, it will be 51. So deck[card_top] is the topmost
    card at any given time. */
  int card_top = 0;

  /* Counter variable. */
  int i;

  /* Card variable. */
  int c;

  /* Flag: have we completed the loop yet? */
  int done;

  /* Flag: do the players want a new game? */
  int new_game;

  /* The number of players. */
  int players_total;

  /* The array of players. */

  player_t players[PLAYERS_MAX];

  /* Convenience pointer to the dealer. */
 
  player_t *dealer = &players[0];

  /* Initialize random numbers. Take advantage of the clock to
    choose a different seed on each run. If your compiler encounters
    difficulty compiling this line, you may wish to remove it, but if 
    you do so the same sequence of cards will appear on each run. 

    If you are using a non- ANSI compiler you may need to declare
    a variable of type time_t ("time_t t") and replace the line
    below with these two lines: "time(&t);" and "srand(t);".
  */

  srand(time(0));

  /* Find out how many players we will have. */
  printf("The computer will be the dealer.\n");
  done = 0;
  /* Loop until we have an acceptable number */
  while (!done) {
    printf("How many human players (1-%d)? ", PLAYERS_MAX-1);
    players_total = int_read();

    /* Make sure there is at least one human and at least one free
      slot for the dealer */
    if ((players_total >= 1) && 
        (players_total < = (PLAYERS_MAX-1))) {
      /* An acceptable number */
      done = 1;
    } else {
      printf("That is not an acceptable number of human players.\n");
    }
  } 

  /* Add one player to account for computerized dealer */
  players_total++;

  /* Set up dealer (player 0) */
  strcpy(dealer->name, "HAL (Dealer)");
  dealer->games_won = 0;

  /* Now loop through players, initializing information and
    prompting for names. Human players start at index 1. */

  for (i=1; i < players_total; i++) {
    printf("Player %d, what is your name? ", i);
    line_read(players[i].name, PLAYER_NAME_MAX_LEN);
    players[i].games_won = 0;
  }

  /* Initialize the deck. */
  deck_init(deck);

  /* Now shuffle the deck. It need not be reshuffled before each hand
    (game) of blackjack; the deck_draw routine automatically shuffles
    it when it is empty. Enjoy yourselves, card- counters. */

  deck_shuffle(deck);

  /* Now let the players take turns, the dealer always going last. */
  
  /* Outer loop: games. */
  do {
    /* Flag */
    int blackjack = 0;

    /* Count of busted players */
    int busted_total = 0;

    /* # of player who hit blackjack */
    int blackjack_player;

    /* For each game we must clear the busted and cards_total fields. */
    for (i=0; i < players_total; i++) {
      players[i].cards_total = 0;
      players[i].busted = 0;
    }

    /* Loop through turns. */
    /* A game is over when either:
      all but one player has busted, or
      any player has hit blackjack. */
    while ((busted_total < (players_total-1)) && 
           (!blackjack)) {
      /* Human players who stood this turn */
      /* Loop through players and take turns. Save dealer for last
        (handle human players for now). */
      for (i=1; i < players_total; i++) {
        int choice;
        /* Convenience pointer to current player */
        player_t *p;
        p = &players[i];
        if (p->busted) {
          /* Go on to next player if player is busted. */
          continue;
        }

        player_print(p);
        printf("1. Hit\n");
        printf("2. Stand\n");
        printf("Your choice? ");
        choice = int_read();
        if (choice == 1) {
          int c;
          c = deck_draw(deck, &card_top);
          printf("You drew: ");
          card_print(c);
          printf("\n");
          player_hand_add(p, c); 
          if (player_hand_blackjack(p)) {
            blackjack = 1;
            blackjack_player = i;
            printf("BLACKJACK!\n");
            /* Break out of loop of players */
            break; 
          }
          if (player_hand_busted(p)) {
            p->busted = 1;
            busted_total++;
            printf("You BUSTED!\n");
          }
        } else { /* Stand */
          printf("You stand this turn.\n");
        } 
      }
      /* Now dealer's turn, but only if not busted and
        only if blackjack hasn't been hit */
      if ((!dealer->busted) && (!blackjack)) {
        player_print(dealer);
        /* STRATEGY: If the maximum value of the dealer's hand 
          is less than 18, OR the minimum value (with aces 
          treated as 1) is less than 12, take a card. 
          If all other players have busted, do nothing (and win). */
        if ((busted_total < (players_total - 1)) &&
             ((player_hand_max_value(dealer) < 18) ||
             (player_hand_min_value(dealer) < 12))) {
          int c;
          c = deck_draw(deck, &card_top);
          printf("Dealer drew: ");
          card_print(c);
          printf("\n");
          player_hand_add(dealer, c); 
          if (player_hand_busted(dealer)) {
            dealer->busted = 1;
            busted_total++;
            printf("Dealer BUSTED!\n");
          }
          if (player_hand_blackjack(dealer)) {
            blackjack = 1;
            blackjack_player = 0;
            printf("Dealer hits BLACKJACK!\n");
          }
        } else {
          printf("Dealer stands.\n");
        }
      }   
    }
    printf("The game is over.\n");

    /* Who won? */
    if (blackjack) {
      printf("%s won by hitting blackjack.\n", 
        players[blackjack_player].name);
      players[blackjack_player].games_won++;
    } else {
      for (i=0; i < players_total; i++) {
        /* Whoever did not bust was the winner. */
        if (!players[i].busted) {
          printf("%s was the winner.\n",
            players[i].name);
          players[i].games_won++;
          break; /* Break out of the for loop */
        } 
      }
    }
    printf("Standings:\n");
    for (i=0; (i < players_total); i++) {
      printf("%s has won %d game(s).\n",
        players[i].name, players[i].games_won);
    }
    printf("Enter 0 to quit or 1 to play another game.\n");
    new_game = int_read();
  } while (new_game);

  /* All went well. */
  return 0;
}
 
/* The actual code for card_print(). The "prototype" given earlier defined
  the arguments and return type of the function; now we provide the
  actual implementation. */
   
void card_print(int card) {
  /* Suit of the card (0-3). */
  int suit;
  
  /* Value of the card (2-14). */
  int value;

  /* Calculate the value of the card. The % operator divides the
    number of the card by 13 and yields the *remainder* of that
    division, yielding a number between 0 and 12. */

  value = (card % 13) + 2;

  /* Calculate the suit of the card. In C, division operations
    round down, so any card number between 0 and 12, when divided
    by 13, will yield 0; any card between 13 and 25 will yield
    1; and so on. */

  suit = card / 13;

  /* Now print out the name of the card. */

  printf("the ");

  /* For the number cards (value between 2 and 10), just print 
    the number */

  if (value < 11) {
    printf("%d ", value);
  } else {
    /* Otherwise it's a "face" card; use a switch to print the names */
    switch (value) {
      case 11: 
        printf("jack ");
        break;
      case 12:
        printf("queen ");
        break;
      case 13:
        printf("king ");
        break;
      case 14:
        printf("ace ");
        break;
      default:
        /* This shouldn't happen, so say so */
        printf("Unknown card! Value: %d\n", value);
        break;
    } 
  }
  printf("of ");
  switch (suit) {
    case 0:
      printf("hearts");
      break;
    case 1:
      printf("diamonds");
      break;
    case 2:
      printf("spades");
      break;
    case 3:
      printf("clubs");
      break;
    default:
      printf("Unknown suit! suit: %d\n", suit);
      break;
  }
}

/* Implementation of deck_shuffle. */

void deck_shuffle(int deck[]) {
  /* Counter variable */
  int i;

  /* The card being swapped */
  int c;
 
  /* Position of card being swapped with */ 
  int cpos;

  /* Our shuffling algorithm will be a simple one. It doesn't produce
    a perfect distribution of cards, but neither does a real shuffling! 
    We will exchange each card with another card, once. */
  printf("Shuffling\n");
  for (i=0; (i < 52); i++) {
    /* Swap with a random position between 0 and 51.
      NOTE TO EXPERTS: I know rand() is not one of the better
      random-number generators in the world, but at least it's
      available in all standard C systems! */ 

    cpos = rand() % 52;
    /* Get the card */ 
    c = deck[i];
    /* Now swap the two cards */ 
    deck[i] = deck[cpos];
    deck[cpos] = c;
  }
}

/* Implementation of deck_draw(). The second argument is a "pointer"
  to a variable containing the number of the current top card.
  This approach allows deck_draw() to modify the value. */
  
int deck_draw(int deck[], int *card_top) {
  /* Position of top card */
  int pos;
  
  /* Card to be returned */
  int c;

  /* Use the * operator, which in this context is the opposite of
    the & operator, to fetch the *value* stored at the *location*
    referred to by the pointer card_top */
  pos = *card_top;

  /* Fetch the card from the deck */
  c = deck[pos];
 
  /* Advance the card pointer */
  pos++;

  /* If we have just drawn the last card, reshuffle the deck */
  if (pos == 52) {
    deck_shuffle(deck);
    /* Which brings us back to the first card, at position 0 */
    pos = 0;
  }

  /* Now store this pos back into the location pointed to by card_top */ 

  *card_top = pos;

  /* Finally we return the card drawn */
  return c;
}

/* Implementation of deck_init() */

void deck_init(int deck[]) {
  /* Counter variable */
  int i;

  /* Fill the deck with the 52 cards, in order */
  for (i=0; (i < 52); i++) {
    deck[i] = i;
  }
}

/* Implementation of deck_print() */

void deck_print(int deck[]) {
  /* Counter variable */
  int i;
  /* Current card */
  int c;

  /* Print out the 52 cards */
  for (i=0; (i < 52); i++) {
    c = deck[i];
    card_print(c);
    /* Add a carriage return to separate cards */
    printf("\n");
  } 
}

void line_read(char *s, int max_len) {
  int len;
  /* Now use fgets to read a line of input from the user. scanf("%s") would
     be easier, but would not allow spaces. */
  fgets(s, max_len, stdin);
  len = strlen(s);
  if (s[len-1] == '\n') {
    s[len-1] = '\0';
  }    
}   

int int_read() {
  char s[10];
  int i;
  line_read(s, 10);
  /* atoi(): a standard function which accepts a string such as "123" and 
    returns an integer such as 123. */
  i = atoi(s);
  return i;
}

int player_hand_blackjack(player_t *p) {
  int min_value;
  int aces;
  min_value = player_hand_min_value(p);
  aces = player_hand_count_aces(p);
  if (min_value == 21) {
    /* Regular cards total 21- all set */
    return 1;
  }
  if (!aces) {
    /* Cards don't total 21 and no aces- can't be blackjack */
    return 0;
  } else {
    /* Try counting one ace as 11 instead of 1 by adding 10. No more
      than one ace can be usefully counted as 11, since 2 would
      be 22, so even if we hold several aces we only need to check
      the result of holding one. */
    if ((min_value + 10) == 21) {
      /* Blackjack with one ace counted as 11 */
      return 1;
    }
  }
  /* No blackjack */
  return 0;
}    

int player_hand_min_value(player_t *p) { 
  int i;
  int value;
  int c;
  int score = 0;
  for (i=0; i < (p->cards_total); i++) {
    c = p->hand[i];
    value = (c % 13) + 2;
    /* Take care of aces and face cards */
    if (value == 14) {
      score += 1;
    } else if (value > 10) {
      score += 10;
    } else {
      score += value;
    }
  }
  return score;
}

int player_hand_count_aces(player_t *p) {
  int i;
  int c;
  int aces = 0;
  for (i=0; i < (p->cards_total); i++) {
    int value;
    c = p->hand[i];
    value = (c % 13) + 2;
    if (value == 14) {
      /* Increment count of aces */
      aces++;
    }
  }
  return aces;
}

int player_hand_max_value(player_t *p) {
  int score;
  /* Instead of repeating code, we'll take advantage of the
    functions already written to calculate the maximum value
    of the hand (counting aces as 11). */
  score = player_hand_min_value(p);

  /* Add 10 for each ace (we already added 1 for min_value). */
  score += player_hand_count_aces(p) * 10;
  return score;
}

int player_hand_busted(player_t *p) {
  /* Just return whether minimum value of hand is greater than
    21 or not. */
  return (player_hand_min_value(p) > 21);
}

void player_hand_add(player_t *p, int c) {
  int i;
  i = p->cards_total;
  if (p->cards_total == HAND_CARDS_MAX) {
    fprintf(stderr, "Too many cards in hand-- maximum is %d\n",
      HAND_CARDS_MAX);
    exit(1);
  }
  p->hand[i] = c;
  p->cards_total++;
}

void player_print(player_t *p) {
  int i;
  /* Flag indicating whether we are at the start of a line or not. 
    We use this to print two cards on each line consistently. */
  int nline = 1;
  printf("\nPlayer: %s Cards: %d Games won: %d\n", 
    p->name, p->cards_total, p->games_won);
  for (i=0; i < p->cards_total; i++) {
    card_print(p->hand[i]);
    if (!nline) {
      printf("\n");
    } else {
      printf("  ");
    }
    nline = !nline;
  }
  /* Final check, so we get a carriage return after an odd number
    of cards, and no extra one after an even number. */
  if (!nline) {
    printf("\n");
  }
}

---
Once again we'll proceed line by line in examining the new features of the program.
First, note at the beginning the use of a new #include file:
#include < string.h >
string.h contains routines for manipulating strings. A string, generally speaking, is a series of text characters. We have already used very simple strings in the form of quoted text, as in the first argument of the printf() function.
Strings, in C, differ greatly from their treatment in languages such as BASIC, where there is a distinct string type in the language. Their treatment in C is closer to standard dialects of PASCAL. (Some versions of PASCAL include BASIC- like strings as well.)
To understand C strings, first take note that there is a distinct variable type for characters in C. This type is known as char. A variable of type "char" is usually one byte in size (a value between 0 and 255). This varies in very rare cases (the Mix C/ Power C compiler for MSDOS uses two bytes to represent char), but the important thing is that char is big enough to store one character of text-- a space, letter, number, or other symbol.
char variables are actually numeric variables, just like int and float; you can store a number between -128 and 127 in a char variable (assuming you have byte- sized char, which, again, is almost universal). Each common text character has a standard value between 32 and 255, and certain special sequences (such as carriage return and line feed) lie between 0 and 31.
(Actually, this describes only one character set, the ASCII character set. ASCII is by far the most common, and is the set of character values found on all personal computers and practically all Unix systems. Another system, EBCDIC, appears on some mainframes.)
So you can store a single character in a char variable. Of course this is usually insufficient. What is needed is a way to store a series of characters, such as a line of text.
In C, this is done by creating what is known as a null- terminated string. A null- terminated string is a series of characters followed by a null, or nil, character, which is always equivalent to the value zero (0). From now on when I refer to a string, I am referring to a null- terminated string, unless I explicitly say otherwise.
Since a C string is a series of char values, it is generally referred to by a pointer to type char, or copied into an an array of type char.
Since strings themselves are not a type in C per se, a collection of functions to to conveniently manipulate them has been provided; these functions are brought in by string.h.
Now consider the following line:
#define PLAYERS_MAX 10
This is not a C statement! That may seem odd, since this is a C program, but your program is not read immediately by the C compiler. It is first read by a program (or a portion of your compiler program) referred to as the "C preprocessor."
What the preprocessor does is take care of special "directives" that begin with a "#" sign.
You have seen one already in the form of the "#include" directive, which causes the compiler to treat the included file as part of your program. (Do not infer from this that the code of your functions should be broken off into separate include files; include files are generally used for prototypes and other declarations only. Separating actual code into separate files is called multiple- module programming, and we will explore it soon.)
Another preprocessor directive is "#define". "#define" allows you to create a "macro"- a single word which represents a number or even a short block of code.
So "#define PLAYERS_MAX 10" makes PLAYERS_MAX a macro name that represents the number 10. When the preprocessor runs, it will replace PLAYERS_MAX with 10 wherever it encounters PLAYERS_MAX.
In this case, we define the macro in order to do two things:
-- Provide a meaningful name for the maximum number of players in the game, instead of an arbitrary number.
-- Make the number EASY TO CHANGE. Since we use the macro consistently throughout the code instead of many occurrences of 10, we can change the maximum number of players simply by changing the macro definition and recompiling. No muss, no fuss.
I highly recommend this use of macros for most "constants" and especially for those that are likely to change.
The macros:
#define PLAYER_NAME_MAX_LEN 3
and
#define HAND_CARDS_MAX 10
provide the same benefits for the length of player names and the number of cards that can be in a player's hand, respectively. If either turns out to be insufficient, it can be easily changed.
Now consider the following "structure definition":
struct player_struct {
  char name[PLAYER_NAME_MAX_LEN+1];
  int cards_total;
  int hand[HAND_CARDS_MAX];
  int games_won;
  int busted;
};
It would be possible to write the complete game using only simple variables and arrays. But consider how many different arguments would have to be passed to functions in order to do this! A function which prints out information about a player would need to be passed the player's name, the number of cards the player has, the array of cards in that player's hand, and so on.
Instead, we take advantage of C's "struct" keyword to declare a "record type" which can be passed and copied as a whole, and its individual "fields" accessed when necessary.
A structure declaration consists of the word "struct", an (optional) name for the structure, and a list of variable declarations inside braces ("{" and "}"). The declaration is followed by a final semicolon. (Omitting this is a common error among new and old programmers.)
Now take a look at the first field's declaration:
char name[PLAYER_NAME_MAX_LEN+1];
Note the use of the "char" type described above. Here we are declaring an array of char large enough to store a player's name.
IMPORTANT: We must add one extra char of space in order to leave room for the NULL (0) character that terminates the string!
Now the structure has been described; but there is one more step necessary in order to turn it into a full- fledged C type. For this we use the "typedef" keyword:
typedef struct player_struct player_t;
A "typedef" statement begins with the keyword "typedef" and is followed by type declaration (such as a struct definition or an existing type like int, char or float), followed by the name we wish the newly defined type to have.
If we did not do this, we could still use player_struct, but we would have to declare each variable of that type as follows:
struct player_struct player;
By defining player_struct to be a type in its own right called player_t, we become able to use a much simpler declaration:
player_t player;
and so our player structure becomes a type in the same sense that char, int and float are types. We can even declare a pointer to a player_t, and in fact we will often do so, since structures are large and it is often "cheaper" to pass them by reference (address) rather than by copying them.
Again, typedef can be used to create simpler types as well. Consider the following:
typedef int card;
Had we used this tactic in the blackjack program, we would be able to declare variables of type "card". They would behave exactly as integers behave, but have the benefit of a more specific, readable name.
Now, skip past the functions we declared in the last chapter and take note of the following new prototype:
int player_hand_blackjack(player_t *player);
The purpose of this function is to determine whether a player has hit blackjack. Note that we pass a pointer to a player_t structure. This is the sort of highly convenient coding that makes structures so useful.
Why do we pass a pointer to the structure (note the "*") and not just the player_t itself? Because the structure is fairly large, and so copying it (remember, when you pass a value to a function, the function receives a copy) may not be a good idea, especially on systems where the "stack size" is limited, such as MSDOS systems. In general sizable structures should be passed by address (by way of a pointer) rather than directly.
The function will return 1 for true, or 0 for false, making its return value useful in an if statement.
The next prototype:
int player_hand_busted(player_t *player);
serves a similar purpose, but determines whether a player has busted (gone over 21 points even with aces counted as 1) rather than whether a player has hit blackjack.
Now consider the following pair of function prototypes:
int player_hand_min_value(player_t *player);

int player_hand_max_value(player_t *player);
Both of these functions return the value of a player's hand in points, but there is a subtle difference. Since, in Blackjack, numbered cards are worth face value, face cards (jack, queen, king) are worth 10 points, and aces are worth *either 1 or 11* points, the value of a hand differs depending on how you count the aces! So we provide two functions, one which returns the value of the hand with aces counted as 1, and another which returns the value of the hand with aces counted as 11. These functions together are useful in helping the computerized dealer arrive at a strategy.
The next two prototypes are of functions that perform input in a "safer", less sensitive fashion than the scanf() function. First we have:
void line_read(char *s, int max_len);
This function's purpose is to read a line of input (ending with the user striking carriage return), and copy the characters to the location pointed to by "s".
The argument, max_len, indicates the size of the buffer which s points to. line_read promises to copy up to max_len-1 characters into the location pointed to by s (even if the user entered more than that) and add a null character at the end (remember, C strings MUST end in a null character to behave properly!).
For convenience, we also provide:
int int_read();
This function reads a line of input also, converts the text the user entered to an integer, and returns the integer. It is similar to scanf("%d", &x), where x is the integer variable being read into, but not identical, because scanf() is sensitive to spaces; if the user enters "4 5" instead of "45", scanf() treats this as two separate numbers by scanf() and the *next* call to scanf() will get the 5, which is confusing to the user (particularly in a situation where we are prompting for one number and the user should not be entering two!). Even worse, scanf() does not get rid of the carriage return following the user's input, which confuses attempts to read a line of text elsewhere after scanf() has read in a value. For these reasons we have replaced scanf() for our purposes with this pair of functions. (We will return to scanf() and its relatives for other purposes, however.)
To complete our set of tools, consider the following two prototypes:
void player_hand_add(player_t *player, int c);

void player_print(player_t *player);
The first accepts a pointer to a player_t structure and an integer representing a card, and adds that card to the player's hand. (Note that the array of cards in the player's hand and the total number of cards in the player's hand, which are needed to do this, are both within the player structure; so by passing one pointer we get access to everything we need to manipulate.)
The second simply prints out a player's status.
Now, turn your attention to the main() function, which has been completely replaced with a new version which actually plays Blackjack.
The function begins by declaring several simple variables. Move past these to examine the following declaration:
player_t players[PLAYERS_MAX];
Here we are declaring an array of player_t structures. We can declare arrays of types we create using typedef in the same manner that we declare arrays of built-in types like int, float and char. Note the use of the PLAYERS_MAX macro, discussed earlier; the preprocessor replaces this with 10 (or whatever we have changed the value to).
Now consider the following:
 
  player_t *dealer = &players[0];
Here we declare a *pointer* to a player_t structure- a variable that will contain the location of a player_t structure, not the structure itself.
We initialize "dealer" to point at the first player in the array. Recall that "[" and "]" are used to index into an array, so players[0] yields the first player structure in the array. The "&" operator then yields the *location* of players[0]. Thus we set dealer to point (contain the location of) the first player, which will be the dealer.
(Note that we could have said the following:
player_t *dealer = players;
due to the fact that the name of an array, *by itself,* can be used as a pointer to its first element (element 0). But for clarity we have explicitly referred to element 0 and taken its address.)
We do this for convenience, to avoid referring to "players[0]" whenever we wish to refer to the dealer. (We could have used a macro instead, but the macro would be in effect throughout the file, so dealer could not have a different meaning in another function if we so desired.)
Next, we encouter a while loop which prompts the player for a number of human players between 1 and the maximum number minus one (to leave room for the dealer). The loop exits only when a number in that range has been entered, setting "done" to escape the loop.
Consider the "if" statement used to test whether the number of players is acceptable:
if ((players_total >= 1) && 
  (players_total < = (PLAYERS_MAX-1))) {
  ... Actual code ...
}
"&&" is a new operator which means "and". As long as the left and right arguments to "&&" are true (not zero), "&&" yields 1; otherwise it yields 0.
Next, we increment the number of players:
players_total++;
to allow for the dealer.
Finally, we begin initializing the information in each player_t structure. We begin with the dealer:
strcpy(dealer->name, "HAL (Dealer)");
the function strcpy() accepts two pointers to type char, and copies the null- terminated string pointed to by the SECOND into the space pointed to by the FIRST. (The order of such things can be confusing; keep your reference books handy, and take advantage of your online help, whenever you are in doubt. On Unix systems, use the command "man strcpy" to get information about strcpy and related functions.)
But what's going on in the first argument, "dealer->name"?
Recall that dealer is a pointer to a player_t structure. Structures contain one or more fields.
"->" is a new operator which accesses a field through a structure pointer.
That is, if you have a pointer p to a structure which contains two fields, a and b, you can refer to field b with the following expression:
p->b
So in the case of the strcpy() call, "->" is used to access the name field, which is an array of char. strcpy() expects to be passed a pointer to char, but fortunately, as mentioned earlier, the name of an array can be used as a pointer to its first element.
So, as a result of the call, "HAL (Dealer)" is copied into dealer->name.
Now consider the next line:
dealer->games_won = 0;
Here we have a simpler case of the use of the "->" operator, again accessing a field through the dealer pointer.
There is a corresponding operator "." which does exactly the same thing for cases in which you have the structure itself, rather than a pointer to it. For instance, the expression:
players[3].games_won++;
Would increment the number of games won by player 3 if placed in our main() function. (Note that we have used "[3]" to access structure number 3. players itself is an array of player_t structures, so we need to specify which structure we wish to access.)
Now consider the following for loop:
  for (i=1; i < players_total; i++) {
    printf("Player %d, what is your name? ", i);
    line_read(players[i].name, PLAYER_NAME_MAX_LEN);
    players[i].games_won = 0;
  }
Here we have the same basic goal as above, but this time we are initializing the human players, so we need to interact with the user. Note the call to our line_read() function; we pass players[i].name, which is an array of characters and is passed as an array to the first character in the arary. We also pass the maximum length of a player's name, to make sure that the user doesn't "run over" the space we have alloted for the name.
(Note that the for loop begins at element 1. This is because we have reserved element 0 for the dealer.)
After initializing and shuffling the deck using functions developed in the last chapter, we begin the actual game, or rather series of games. We enclose the game in a new kind of loop, called a "do" loop:
  do {
    /* Flag */
    int blackjack = 0;

    ... additional code ...

  } while (new_game);
A "do" loop is just like a while loop, except that the condition comes at the end. This is convenient when we know we always want the loop to execute at least once.
A "do" statement is constructed as follows: the "do" keyword, followed by a statement (usually a compound statement between "{" and "}"), followed by the "while" keyword, followed by a conditional expression between parentheses, and completed by a final semicolon.
The statement (or series of statements between "{" and "}") following the "do" keyword is executed first. Then the condition following "while" is tested. As long as that condition remains true, the loop repeats.
(In this case, new_game is set by asking the user whether or not he or she wants to continue.)
Now, moving inside the loop, consider the following declaration:
/* Flag */
int blackjack = 0;
Earlier I stated that variables should be declared at the beginning of a function. It is also acceptable to declare variables at the beginning of any compound statement. Variable declarations must follow the initial "{", and be completed before any other statements appear. It is often convenient to declare variables inside such blocks for clarity; since they are only used within the block, it is convenient to be able to see their declarations when editing the block.
Variables declared inside a block supersede those outside the block. In other words, if there is an "int i" declared at the start of main() and another "int i" declared inside a "for" loop, code within the "for" loop sees the latter variable's value.
Now consider the following loop:
for (i=0; i < players_total; i++) {
  players[i].cards_total = 0;
  players[i].busted = 0;
}
Here we initialize the cards_total and busted fields each player (including the dealer). These fields need to be reset each game (hand) of Blackjack. (Some other fields, such as name and games_won, are *not* reset for each game.)
Next, we encounter a "while" loop through the game:
while ((busted_total < (players_total-1)) && 
       (!blackjack)) {
... Additional code ...
};
The conditional expression here is a bit complex, but it states the conditions under which a game should continue for another turn. In English:
-- At least two players have not "busted" (gone over 21 points and lost the game), and
-- No player has hit Blackjack (21 points).
As long as these conditions are true (which they are at the beginning of a game), the game continues for another round.
Now, consider the following loop:
for (i=1; i < players_total; i++) {
  int choice;
  player_t *p;
  p = &players[i];
  if (p->busted) {
    continue;
  }
  ... Additional code ...
}
Here we loop through the human players (again, human players begin at index 1). Just as we did for the dealer, we declare a convenience pointer and assign it the location of the current player. This saves a great deal of typing later on.
Note the if statement, which checks to see if a player is busted and, if so, skips to the next player by way of the "continue" statement.
The "continue" statement, when encountered inside any loop, skips over the remainder of the code inside the innermost loop and goes on to the next pass through the loop (if any). If several "for", "while", or "do" loops have been "nested", the innermost loop executing is the one affected.
Now consider the following:
player_print(p);
printf("1. Hit\n");
printf("2. Stand\n");
printf("Your choice? ");
choice = int_read();
Here we invoke the player_print function, passing it the location of the current player.
Next, we prompt the user for a choice: whether to "hit" (take a new card) or "stand" (take no card this turn). We take advantage of the new int_read() function here.
After this, we use an if statement to see whether the user has elected to "hit." If so, we execute the following code:
int c;
c = deck_draw(deck, &card_top);
printf("You drew: ");
card_print(c);
printf("\n");
player_hand_add(p, c); 

(Again, we declare a new variable, which is acceptable at the beginning of the block.)
Here we draw a card from the deck, as in the previous chapter, and display it to the user. We then invoke player_hand_add to add the card to the player's hand.
Now consider the following:
if (player_hand_blackjack(p)) {
  blackjack = 1;
  blackjack_player = i;
  printf("BLACKJACK!\n");
  break; 
}
Here we invoke the player_hand_blackjack() function to find out if the player has won the game by hitting 21 exactly; if so, we set the "blackjack" flag to 1 (true), and set the blackjack_player variable to indicate the index of the winning player. We then use the "break" statement to escape the for loop, since the game is over at this point.
The break statement is similar to the continue statement, except that it escapes the innermost loop entirely, instead of skipping to the next iteration (pass through the loop).
We then do nearly the same thing for the case in which the player has "busted" (gone over 21 points), except that here we do not break out of the loop, since the other players continue when one player goes bust.
Following this we encounter the "else" clause of the if statement which determined whether the player was taking a "hit" or a "stand". In the case of a stand we simply print a message to that effect.
Next we encounter the code for the computerized dealer's turn. This code is very similar to the code for the human players.
First, we encounter a test to see whether the computer should move at all:
if ((!dealer->busted) && (!blackjack)) {
  ... dealer code ...
}
If the dealer has *not* busted, and no one has hit blackjack, the dealer moves.
The interesting part of the dealer code lies in the following "if" statement, which encodes the dealer's strategy for deciding whether to "hit" or "stand":
if ((busted_total < (players_total - 1)) &&
    ((player_hand_max_value(dealer) < 18) ||
    (player_hand_min_value(dealer) < 12))) {
  ... Dealer takes a hit ...
}
We see another new operator introduced here, the "||" operator (two vertical lines), which means "or". If either of its left and right arguments is not zero, it yields 1; otherwise it yields 0 (false).
Here is an English translation:
Take a new card (hit) if:
-- At least one other player has not busted (we haven't already won!), and
EITHER
-- the maximum value of our hand (aces counted as 11) is less than 18, or
-- the minimum value of our hand (aces counted as 1) is less than 12.
Thus, assuming the dealer hasn't already won, the dealer will hit if he either has a poor hand (worth less than 18) or can comfortably risk a hit (worth less than 12, so even a face card won't bust).
(Not being a professional dealer, I can't say whether this is the best strategy in the world, but it beats the pants off me most of the time!)
The remainder of the code, which handles the dealer's drawing of a card (hit), is similar to that for human players.
After the outer while() loop controlling the game has exited, the next block of code determines who won the game. If a player hit blackjack, this is simple.
if (blackjack) {
  printf("%s won by hitting blackjack.\n", 
  players[blackjack_player].name);
  players[blackjack_player].games_won++;
}
Note the use of the "%s" sequence in our call to the printf() function. This sequence outputs a string argument, just as "%d" and "%f" output numeric arguments. Thus the name of the player is substituted into the sentence.
Now consider the else clause (in which no player hit blackjack). Here, we know that whoever did *not* bust (go over 21) must be the winner!
else {
  for (i=0; i < players_total; i++) {
    if (!players[i].busted) {
      ... The winner, so we announce their victory and break out of
          the loop. ...
    }
  }
}
After determining the victor, we print out the current standings in a simple for loop, and then ask the player(s) whether they wish to continue. If not, the program returns to the operating system.
Now we will consider the additional functions added to break up the code:
void line_read(char *s, int max_len) { ... }
This function reads a line of input from the user, ending in a carriage return, and copies up to max_len-1 characters into the space pointed to by s. It then adds a terminating null.
Consider the following line:
fgets(s, max_len, stdin);
Here we call the function fgets(), which is part of the standard I/O package brought in by stdio.h. fgets accepts three arguments: a pointer to char (s in this case), a maximum number of characters (of which it reserves the last for a terminating null), and a "stream."
Streams are variables of type "FILE *". They are used to access files on disk, and also to communicate with other devices. In this case, we are using the stream "stdin".
"stdin" is a global variable, accessible to any program which includes stdio.h. It refers to the stream of input from the user.
(There is a gets() function which always uses stdin without the need to state it explicitly, but unfortunately it is unsafe, because it does not allow you to specify a maximum number of characters! Thus the user can enter more characters than you provided space for and crash your program in unpredictable, usually awful ways.)
We will explore streams (and files) further in the next chapter.
Now consider the following:
len = strlen(s);
Here we invoke the function strlen(), provided by string.h, in order to fetch the length of the string pointed to by s. (The length of the string is the number of characters *before* the terminating null, and does not include the null itself.)
We have one last task to take care of: fgets() stores the carriage return itself in the string, so we need to remove it. Consider the following code:
if (s[len-1] == '\n') {
  s[len-1] = '\0';
}
Here we access the last element in the string (again, not including the null), test to see if it is a carriage return, and, if so, set it to a null, ending the string at that point to get rid of the carriage return.
"But s is a pointer, not an array!"
Yes, but we can use "[n]" after a pointer name to refer to the nth item following the location pointed to. Thus:
s[0]
is equivalent to
*s
in that it accesses the value of the first (0th) item at the location s points to.
"So pointers and arrays are the same?"
Not quite. There are significant differences, which will be discussed in later chapters. But whenever an array is passed to a function, it "decays" into a pointer to the same type. In fact, when we declare functions like this:
int total(int x[]);
x[] is only a synonym for this:
int total(int *x);
"OK, so what's this '\n' business?"
As previously mentioned, double- quotes ("") are used to code entire strings in C, and the compiler will null- terminate them.
Single- quotes, on the other hand, encode "character constants", or single characters. So the following:
char x = 'z';
sets the char variable x equal to the numeric representation of the character 'z'.
"Right. So why the \n?"
Recall that \n means "carriage return" in a printf statement's first argument (or in any other string for that matter). It can also be used as a single character. Here we need to test the *single character* at the end of the string, so we compare it to the character constant '\n' (carriage return):
s[len-1] == '\n'
tests whether the last character before the null is a carriage return, and
s[len-1] = '\0';
sets it to a null if so, shortening the string by one character as far as functions that examine strings (like printf()) are concerned.
After taking care of this nuance, we return to the calling function.
The function int_read() contains just one new item of note:
i = atoi(s);
The function atoi() accepts a pointer to char as an argument, reads the null- terminated string at that location, and returns an equivalent integer. So atoi("5") returns the integer 5.
The remaining functions break no new ground, and so I will not discuss them in detail here. The sole exception is tucked inside player_hand_add():
fprintf(stderr, "Too many cards in hand-- maximum is %d\n",
  HAND_CARDS_MAX);
exit(1);
This code is executed when too many cards have been added to the player's hand. In this case, an error message is printed, using the function fprintf().
fprintf is a close relative of printf; the only difference is that it takes a stream (like stdin, discussed earlier, but an output stream in this case) as its first argument. In this case we pass the stream "stderr", or standard error, which is used to report internal errors in the program. (The regular printf() function is equivalent to calling fprintf with a first argument of "stdout", the standard output stream.)
The exit() call that follows is interesting because it EXITS THE PROGRAM IMMEDIATELY, returning the result it is passed (1, not 0, since something has gone wrong).
This sort of error- detecting code is important because it draws the user's attention, or better yet your own attention, before another user sees the program. If the error were to be quietly ignored the problem might not be detected until it became serious. (Of course, this is only a blackjack game, but more serious programs need such precautions.)
PHEW!
Compile and run the game. Grab a few friends if you like, it supports plenty of players.
<< Previous