Everything for Electronics

Microcontrollers, Software, and You — Part 4

Microcontrollers, Software, and You — Part 4

By Jack Purdum    View In Digital Edition  


Writing the In Between Program

We ended our Part 3 article by giving you an assignment that emulates the card game, often called In Between (but better known as Acey Deucey). Recall that the objective of the game was to have the “dealer” turn two cards face up and then have the player bet that the next card would fall “in between” the two face-up cards. If the user thinks the range is too small, they bet 0 dollars. The program would then repeat itself. The player is given $100 to start the game.

As with any task, the first thing you need to determine is the starting point of that task. You already know that the Five Program Steps is a logical place to begin writing the In Between program.

I usually do this by writing down the Five Program Steps on a piece of paper, leaving enough room between each step to write in the details that are needed. Task List 1 shows what my worksheet looked like when I got done.

Task List 1. The Five Program Steps for In Between.

Step 1. Initialization

  • Initialize the Serial object so we can display results on the PC monitor via the Serial monitor.
  • Seed the random number generator.
  • Give the player $100.
  • Display a sign-on message about the program (instructions on how to play?).

Step 2. Input

  • Get the High and Low cards.
  • Get the amount of the player’s bet.
  • Get the In Between card.

Step 3. Process

  • Check to see if the In Between card falls within the High and Low cards.

          ⇒ If Yes, increase the player’s cash by the bet amount.
          ⇒ If No, subtract the bet amount.
              → If the player lost, does he have a positive cash balance?

  • If Yes, play the next hand.
  • If No, *** Not sure right now how I want to process this event.

Step 4. Output

  • Tell the player if they won or lost.
  • Display cash balance.
  • Go back to Step 2.

Step 5. Termination

  • Not sure if I want to terminate game if cash balance is $0.

If you use the Five Program Steps as a starting point, the program almost writes itself. Now let’s look at the code that is used to flesh out the steps.

Initialization

You already know that the setup() function is where we usually put the initialization code. You also know that setup() is only executed once when power is first applied to the microcontroller. With that in mind, Listing 1 presents the code for one possible setup() function.

void setup() {
  Serial.begin(9600);            // Baud rate for communication with PC

  while (!Serial) {
    ;                                  // wait for serial port to connect.
  }
  // **************** Step 1, Initialization ***************
  randomSeed(analogRead(A0));           // Seed the random number generator
  playersCash = 100;                   // Player starts out with $100
  InBetweenInstructions();            // How to play
}

LISTING 1. The setup() function.


The program starts by instantiating the Serial object using the object’s begin(9600) method call. The while (!Serial) looks a little odd, but is essentially an infinite while loop. Verbalizing the code, it’s saying: “I realize that instantiating the Serial object can take some time because it needs to communicate with the PC via the USB cable. So, I’ll just spin around here until I get an internal message from the PC that the communication link is active.”

Sometimes establishing a COM link with a PC takes more time than the processing speed is plowing through instructions. The while loop with no statements in its loop body is just a simple way of waiting until things are set up properly. In this case, when the Serial object is instantiated, the while loop’s test expression (!Serial) changes from a logic false value to logic true (i.e., the expression !Serial is read “Not Serial” which is the same as saying it’s not yet instantiated). When the logic becomes true, the while loop ends, and program control moves to the next statement.

If you read the documentation for the random number generator that is available in the IDE (integrated development environment), the random() function produces a series of “pseudo-random numbers.”
What?

The algorithm used by the random number generator does produce a sequence of random numbers. For example, suppose you want to simulate tossing a coin 10,000 times where 0 is a tail and 1 is a head. That is, if you called the function using random(0, 2) 10,000 times, it would produce approximately 5,000 heads and 5,000 tails. I actually placed this code at the very bottom of the setup() function and ran it:

int h, t, v;
h = t = 0;
for (int i = 0; i < 10000; i++) {
  v = random(0, 2);
  if (v == 0)
    t++;
  else
    h++;
}
Serial.print(“t = “);
Serial.print(t);
Serial.print(“  h = “);
Serial.println(h);

It showed that there were 5,029 tails and 4,971 heads ... close enough to the expected 5,000 for each. When I ran it a second time, I got 4,951 and 5,049. Still pretty good.

However, if I comment out the line:

//  randomSeed(analogRead(0));           // Seed the random number generator

and run the program, I get 4,920 tails and 5,080 heads. If I reset the program and run it again, I get 4,920 tails and 5,080 heads. Indeed, I can run the program over and over and I will get the exact same distribution numbers for heads and tails.

Something is not a random event if a million runs of 10,000 coin tosses produces the exact same results. This is why they call it a pseudo-random number generator.

The algorithm used for the pseudo-random number generator produces a repeatable sequence of random numbers. If it’s repeatable, the distributions are not random.

This property can be very useful when debugging code that uses random(). However, we want to generate a series of numbers that varies each time we restart the program. The algorithm provides for that by letting you provide a number that is used to seed the random number generator. The statement:

  randomSeed(analogRead(A0));           // Seed the random number generator

uses Nano analog pin number A0, reads its value, and then uses the value returned from the analogRead() function to seed the random number generator. The analogRead() function returns a value between 0 and 1023. The exact value depends on whatever numeric value equates to the noise that was on that pin when the program started. While it’s not a perfect random number seed, it’s good enough for most purposes.

The remainder of the setup() function is used to give the player $100 to play with and then displays some instructions to explain how the game is played. You can read that code in the complete program listing shown later.

Input

The next task for the program is to get some data for it to work on. Actually, we need five pieces of information to process for each hand in a game: 1) the low card value; 2) the high card value; 3) the in between value; 4) the amount of the bet; and 5) the player’s cash balance. We can get the first three values using these statements:

    card[LOCARD]  = random(1, 14);        // Get a random number from 1 - 13, inclusively
    card[HICARD]  = random(1, 14);
    card[BETCARD] = random(1, 14);        // May as well get the outcome card now...

I decided to use an array to store the relevant card information in the program. An array is nothing more than a group of identical data types placed back-to-back in memory.

After a lot of deep thinking, I decided to give the name of that card array the clever name card[]. In C, all arrays start with element 0 and increase after that. Therefore, I want card[0] to be the low card for the betting range, card[1] to be the high card, and card[2] to be the next card in the deck. It’s this last card that determines if the player wins or not.

Symbolic Constants

Okay, so what is this LOCARD, HICARD, BETCARD all about? These are called symbolic constants in a program. Symbolic constants are just textual names you assign to things in a program. At the top of the program, I have the constants defined as:

#define LOCARD    0
#define HICARD    1
#define BETCARD   2

Any place in the code where the compiler sees LOCARD, it replaces it with 0. Likewise, seeing either HICARD or BETCARD causes the compiler to substitute the test ‘1’ or ‘2,’ respectively, in the program. The purpose of symbolic constants is to make the code easier to read and understand. Which is easier for you to understand the variable’s purpose: card[0] or card[LOCARD]?

There is another advantage of using symbolic constants. For example, in 1972, the federal government changed the maximum speed limit on federal highways to 55 mph. Some states didn’t even have speed limits (e.g., Montana was “reasonable and proper” for road conditions) while most were between 60 and 80 mph.

Now, suppose you wrote some code that processed speeding tickets for your state. Assume your state highway speed limit was 70 mph. You could have dozens of places where you use that “70” number in your code. Now, with the stroke of a pen in Washington, that changes to “55.”

You need to change every instance of 70 mph in your program to 55 mph. The following statement might be where you calculate the amount of the fine. Note the 70 in the statement:

   fine = (speed - 70) * 10;    // speeding fine is $10 for every mph over 70

Simple, you say. Just do a global search-and-replace with your programming text editor and you’re done, right? Well, maybe. It would correctly change the line above. However, the problem is that global search-and-replace has the innate intelligence of a bag of hammers, so when it sees the line:

    countyYear = 1970;        // The year county passed the uniform arrest law

your global search-and-replace changes it to:

    countyYear = 1955;        // The year county passed the uniform arrest law

Chances are, this is not a change you meant to make. As a result, you really should look at each text that is “70” and see if it really should be changed. That can be very time-consuming and is still error-prone.

What if, instead, you had done this:

  #define MAXHIGHWAYSPEED    70

  // a bunch of code that’s left out...

  fine = (speed - MAXHIGHWAYSPEED) * 10;  // speeding fine is $10 for every mph over 70

  // more left out code

  countyYear = 1970;               // The year county passed the uniform arrest law

Now, simply change the symbolic constant at the top of your program from “70” to “55” and recompile the program. All the places in the code where you used MAXHIGHWAYSPEED automatically gets changed and you don’t have to worry about it. Note how the countYear variable remains unchanged here, but is messed up with global search-and-replace. Moral of the story: Get rid of “magic numbers” in your code and use symbolic constants instead.

We still need to get the amount of the bet the user wants to make in this hand. We can use the GetNumber() function I gave you in the previous article:

     betAmount = GetNumber();

If the betAmount is not zero, the game’s afoot! If the bet amount is zero, we simply ignore the current numbers and go back and get a new set of input.

Process

Because the user placed a bet, we need to determine if they won. It they did, we need to add the amount of their bet to their betting cash balance. If they lost, we need to subtract the bet amount. The code to process the bet is:

        if (card[BETCARD] > card[LOCARD] && card[BETCARD] < card[HICARD]) {
          betFlag = 1;
        } else {
          betFlag = -1;
        }
        playersCash += betAmount * betFlag; // Same as adding or subtracting bet amount

The if statement is saying: If the bet card is greater than (‘>’) the low card AND (“&&”) the bet card is less than (‘<’) the high card, the player wins (betFlag = 1). Otherwise, the player loses (the house wins ties), the betFlag is -1, and we adjust the player’s cash accordingly.

The last statement needs some explanation. Suppose the amount the player wagered was $10. If they win, the expression betAmount * betFlag equals $10 (i.e., 10 * 1). If they lost the bet, betFlag is -1, so, betAmount * betFlag equals -10 (i.e., 10 * -1).

The “+=” operator means to add the results from the right side of the expression (betAmount * betFlag) to the left side of the expression (playerCash). You could write the same expression as:

       playersCash = playerCash + betAmount * betFlag;

but C programmers always prefer less writing to more. Because the multiply operator (‘*’) has higher precedence that the addition operator (‘+’), the multiplication takes place first.

The assignment operator (‘=’) takes the results of the expression(s) on the right side of the equal sign and assigns that value into the expression on the left side of the assignment operator (playersCash).

Output

A program isn’t worth much if it doesn’t do anything with its output. In our program, all we want to do is tell the player whether or not they won the bet and show the new balance. The following code fragment shows how we chose to do it:

      if (betFlag > 0) {
        Serial.print(“    Player wins!”);
      } else {
        Serial.print(“    Player loses to the house!”);
      }
      Serial.print(“            Player cash balance: “);
      Serial.println(playersCash);

The print() method of the Serial object displays the output on the Serial monitor on the player’s PC. The if statement simply determines which message to display.

Termination

We don’t have a Termination Step in our code, although you may want to add one yourself. We should also probably check the player’s cash balance before we let them play another game. If you want to end the game on a zero balance, you could put the game into an infinite loop:

    while (true)
    ;

Or, you could call the exit(0) function. The exit(0) function is a little more risky since it’s a C function that causes control to return to the operating system.

However, microcontrollers don’t have an op system per se, so the compiler is free to generate the code the compiler writers want. For the Arduino IDE, exit(0) clears the interrupts and enters an infinite while loop as illustrated above.

There are some other input checks we should do. For example, as the code stands now, the user could enter a minus number for the bet. Although we do check for a positive bet amount, we don’t tell the user anything is amiss if they bet a negative amount. We probably should. Listing 2 presents the complete program.

#define LOCARD    0
#define HICARD    1
#define BETCARD   2

int playersCash;

/*****
  Purpose: To accept a number from the keyboard via the Serial object

  Parameter list:
    char msg[]      an input prompt

  Return value
    int             the number that was entered

  CAUTION: Assumes the Serial object is instantiated
*****/
void InBetweenInstructions()
{
  Serial.println(“                           In Between\n\nRules:\n”);
  Serial.println(“You are shown two playing cards, face up. You must decide if you would                 like to”);
  Serial.println(“bet that the next card turned up falls between the range of the two face-                up”);
  Serial.println(“cards. If you want to make a bet, enter the number of dollars you want to             bet.”);
  Serial.println(“A bet of $0 means you do not want to place a bet and new cards are then                 dealt.”);
  Serial.print(“\nYour initial stake is $”);
  Serial.println(playersCash);
}

/*****
  Purpose: To accept a number from the keyboard via the Serial object

  Parameter list:
    char msg[]      an input prompt

  Return value
    int             the number that was entered

  CAUTION: Assumes the Serial object is instantiated
*****/
int GetNumber()
{
  char buffer[10];
  int count;

  buffer[0] = ‘\0’;
  count = 0;
  while (true) {
    while (Serial.available() > 0) {
      count = Serial.readBytesUntil(‘\n’, buffer, sizeof(buffer) - 1);
      buffer[count] = ‘\0’;
    }
    if (count > 0)      // Did they enter a bet?
      break;
  }

  return atoi(buffer);
}


/*****
  Purpose: To display the two card values, lowest first

  Parameter list:
    char msg[]      an input prompt

  Return value
    void

  CAUTION: Assumes the Serial object is instantiated
*****/
void ShowInputCards(long deck[])
{
  long temp;

  if (deck[LOCARD] > deck[HICARD]) {   // Place in order for esier viewing
    temp         = deck[HICARD];
    deck[HICARD] = deck[LOCARD];
    deck[LOCARD] = temp;
  }
  Serial.print(deck[LOCARD]);
  Serial.print(“ to “);
  Serial.println(deck[HICARD]);
}


void setup() {
  Serial.begin(9600);

  while (!Serial) {
    ;                                   // wait for serial port to connect.
  }
  // **************** Step 1, Initialization ***************

  randomSeed(analogRead(A0));            // Seed the random number generator
  playersCash = 100;                    // Player starts out with $100
  InBetweenInstructions();

  /*
    int h, t, v;
    h = t = 0;
    for (int i = 0; i < 10000; i++) {
    v = random(0, 2);
    if (v == 0)
      t++;
    else
      h++;
    }
    Serial.print(“t = “);
    Serial.print(t);
    Serial.print(“  h = “);
    Serial.println(h);
  */
}


void loop()
{
  //  int promptFlag;
  int betFlag;
  long card[3];        // These are the card of concern for each hand
  int betAmount;

  //  promptFlag = 1;
  betAmount;

  betAmount = -1;

  // ********************** Step 2, Input **********************

  card[LOCARD]  = random(1, 14);        // Get a random number from 1 - 13, inclusively
  card[HICARD]  = random(1, 14);
  card[BETCARD] = random(1, 14);        // May as well get the number now...
  if (card[LOCARD] != card[HICARD]) {   // if the cards are not the same...
    ShowInputCards(card);                 // Show the inbetween range...
    Serial.print(“Enter wager amount: “);
    betAmount = GetNumber();
    Serial.println(betAmount);
    if (betAmount > 0) {
      Serial.print(“\n   card dealth is: “);
      Serial.println(card[BETCARD]);

      // ********************** Step 3, Process ********************
      if (card[BETCARD] > card[LOCARD] && card[BETCARD] < card[HICARD]) {
        betFlag = 1;
      } else {
        betFlag = -1;
      }
      playersCash += betAmount * betFlag; // Same as adding or subtracting bet amount

      // ********************** Step 4, Output ********************
      if (betFlag > 0) {
        Serial.print(“    Player wins!”);
      } else {
        Serial.print(“    Player loses to the house!”);
      }
      Serial.print(“            Player cash balance: “);
      Serial.println(playersCash);
    }
  }
  // How do you want to handle a player who has no more cash? Give them more or quit?0
}

LISTING 2. The complete In Between program code.


This code is my solution to the In Between programming task. I would expect yours to be very different than mine, depending on your programming experience. You might compare mine to yours and ask yourself which is “best.”

Down the Road

In possible future articles, we could look at various ways that you might make your code more efficient or easier to read, or use less code space ... perhaps all three at once! Stay tuned!  NV


Link to Part 1
Link to Part 2
Link to Part 3




Comments