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