Chapter 2. The Interaction Hierarchy

In This Chapter

  • Uh, when does this get fun?

  • Playing (interacting)

  • Playing along (engaging)

  • Player authoring (immersing)

  • How interactivity, engagement, and immersions are related

Uh, When Does This Get Fun?

In Chapter 1, “Introduction to Video Game Artificial Intelligence (VGAI),” you developed a game-playing AI algorithm for Tic-Tac-Toe and implemented it in XNA. You correctly identified that it might be an awesome game-playing AI implementation, but as VGAI, it ain’t much to sneeze at. Particularly, there were two major flaws:

  • Flaw #1: C’mon! This game is unbeatable! Where’s the challenge in that?

  • Flaw #2: Um, yeah. This does nothing to increase believability, immersion, or engagement. In fact, it is so dad-gum hard, it disengages me rather quickly. That’s a rather nasty flaw in a game.

At any rate, there’s good news. Because you now know where your previous effort missed the mark, you can set about fixing it. Low-hanging fruit is the easiest to pick, so let’s start with the “this game is insanely hard” bit. Obviously, now that you went to all the trouble of building this insanely good Tic-Tac-Toe AI, you need to dumb it down so that players stand any kind of chance.

You have to be careful, however, that you don’t make the AI appear to be “dumb” to the player. In other words, you need to consider the idea of “appropriate levels” of challenge. Let’s take a look at a simple, low-budget, yet extremely successful way of accomplishing exactly that: the “difficulty settings” approach.

Make a clone of the TicTacToeGM project called TicTacToe_v0.1. (Start a new project and choose Add Existing Item for all the items in the TicTacToeGM content and code directories.) You will need to add a couple of UI components to allow the player to select the difficulty level of the game. I like radio buttons, and in my desire to keep things simple, I created a piece of art called radioButtonOff.png and radioButtonOn.png. You can use my stuff or create your own, at your pleasure. I will, of course, continue on my merry way as if you believe me to be the best dang artist evah.

You’re going to add four layers of difficulty—in plain English: easy, medium, hard, and GM level. You need to add textures for four radio buttons, so in our members list section for the mouse and UI, let’s create Texture2D objects named xxxxBtn, xxxxBtnOn and xxxxBtnOff, where xxxx is replaced by easy, med, hard, or gm. Of course, any game that gives you an easy mode is inherently boring, so let’s call these modes Wimp, Vanilla, Interesting, and Insanomatic, and create four string objects as text descriptors, named xxxxText. Let’s initialize them with the declaration and make them static. Finally, you need four Vector2 objects, named xxxxPosition. You will also load a single spriteFont object for printing UI items.

The mouse and UI member list should now look like this:

//mouse and UI textures
Texture2D mPointer;
Texture2D textWindow;
Texture2D playBtn;
Texture2D playBtnUp;
Texture2D playBtnDown;
Texture2D resetBtn;
Texture2D resetBtnUp;
Texture2D resetBtnDown;
Texture2D easyBtn;
Texture2D medBtn;
Texture2D hardBtn;
Texture2D gmBtn;
Texture2D easyBtnOff;
Texture2D easyBtnOn;
Texture2D medBtnOff;
Texture2D medBtnOn;
Texture2D hardBtnOff;
Texture2D hardBtnOn;
Texture2D gmBtnOff;
Texture2D gmBtnOn;
SpriteFont UIFont;
static String easyText = "     Wimp";
static String medText = "     Vanilla";
static String hardText = "     Interesting";
static String gmText = "     Insanomatic";
Vector2 easyPosition;
Vector2 medPosition;
Vector2 hardPosition;
Vector2 gmPosition;
Vector2 textWindowLoc;
Vector2 playButtonLoc;
Vector2 resetButtonLoc;
static ScreenText screenText = new ScreenText();

We’ll return to the members list in a minute when we add state variables to the game to handle these settings. For now, let’s move on to make the UI functional with these new bits. You need to add content and to initialize the positions.

In the Initialize() method, you need to give these positions reasonable values. You already know that a value of 196 for x is good (from our play and reset buttons), but to make it pretty, let’s give it an additional 14 pixels of offset. Our y values should be 125, 145, 165, and 185 for each subsequent position. Add this code:

easyPosition = new Vector2(210, 125);
medPosition = new Vector2(210, 145);
hardPosition = new Vector2(210, 165);
gmPosition = new Vector2(210, 185);

You might be muttering under your breath about the position of the text descriptors. We’re going to cheat (which is why we included the white space at the beginning of the string) and put them in the same spot as the texture.

Inside LoadContent, you need to read in the images for each button type and initialize the default button positions. You do this in a straightforward way that results in this code:

//load mouse pointer and UI textures
mPointer = this.Content.Load<Texture2D>("playerXPointer");
textWindow = this.Content.Load<Texture2D>("TextWindow");
playBtnDown = this.Content.Load<Texture2D>("playDown");
playBtnUp = this.Content.Load<Texture2D>("playUp");
resetBtnDown = this.Content.Load<Texture2D>("resetDown");
resetBtnUp = this.Content.Load<Texture2D>("resetUp");
easyBtnOff = this.Content.Load<Texture2D>("radioButtonOff");
medBtnOff = this.Content.Load<Texture2D>("radioButtonOff");
hardBtnOff = this.Content.Load<Texture2D>("radioButtonOff");
gmBtnOff = this.Content.Load<Texture2D>("radioButtonOff");
easyBtnOn = this.Content.Load<Texture2D>("radioButtonOn");
medBtnOn = this.Content.Load<Texture2D>("radioButtonOn");
hardBtnOn = this.Content.Load<Texture2D>("radioButtonOn");
gmBtnOn = this.Content.Load<Texture2D>("radioButtonOn");
UIFont = this.Content.Load<SpriteFont>("15");

//init buttons
playBtn = playBtnUp;
resetBtn = resetBtnUp;
easyBtn = easyBtnOff;
medBtn = medBtnOff;
hardBtn = hardBtnOff;
gmBtn = gmBtnOn;

Finally, you just need to draw this stuff. As you can guess, you’re going to put code into the Draw() method, thusly (mwaahaahaahaahaa!):

//draw UI items
spriteBatch.Draw(playBtn, playButtonLoc, Color.White);
spriteBatch.Draw(resetBtn, resetButtonLoc, Color.White);
spriteBatch.Draw(textWindow, textWindowLoc, Color.White);
spriteBatch.Draw(easyBtn, easyPosition, Color.White);
spriteBatch.Draw(medBtn, medPosition, Color.White);
spriteBatch.Draw(hardBtn, hardPosition, Color.White);
spriteBatch.Draw(gmBtn, gmPosition, Color.White);
spriteBatch.DrawString(UIFont, easyText, easyPosition, Color.Black);
spriteBatch.DrawString(UIFont, medText, medPosition, Color.Black);
spriteBatch.DrawString(UIFont, hardText, hardPosition, Color.Black);
spriteBatch.DrawString(UIFont, gmText, gmPosition, Color.Black);

Save this code and compile it, and if the bits are all aligned, you should see something like what’s shown in Figure 2.1.

The new UI with “difficulty” options.

Figure 2.1. The new UI with “difficulty” options.

Now, Valued Reader, let’s make all that prettiness mean something. Our tasks are pretty simple: Create some state variables to control the UI and the AI, connect the UI to the Input class so you can test for mouse clicks, and add some AI flags based on this state to govern how well the AI plays. Easy, right?

Again with the low-hanging fruit: Let’s create our state variables. All you really need is a flag that tells you which difficulty mode you are in. You could create four Booleans, but that would mean you need lots of conditional checks to determine which state you are in. A better approach is to create an integer flag, restrict its values to 0, 1, 2, or 3, and let that flag govern our state in AI and the UI. In that case, you can use switch statements instead of a bunch of if-else conditions, which will look more pretty in the code.

In the member list, add a single integer named diffState to the AI and game state variables section:

//AI and game state variables
const int MAX_TURNS = 9;
int turn;
int moveAI;
int drawTurn;
bool AIWin;
bool playerWin;
int diffState;

Easy enough. Now you make it match the default UI settings by setting its value to 3 in the initialize method:

//AI and game state variables intialization
turn = 0;
moveAI = -1;
AIWin = false;
playerWin = false;
winMsgSent = false;
inPlay = false;
drawTurn = MAX_TURNS;
diffState = 3;

Let’s hook up the input controller next so the Update() method code will make sense. If you recall, you have a class that handles input hardware polling called Input.cs. You need to determine whether the player has clicked on one of the difficulty UI buttons. Fortunately, you have a good template for this method in the PlayBtnClicked() method, shown here:

        //this method determines if play button has been clicked
public bool PlayBtnClicked()
{
    //if the mouse is over the play button
    if (Mouse.X >= 196 && Mouse.X < 397 &&
        Mouse.Y >= 20 && Mouse.Y < 69)
    {
        //check to see if the mouse left button was down, but now is up
        return oldMState.LeftButton == ButtonState.Pressed &&
               mouseState.LeftButton == ButtonState.Released;
    }
    else return false;
}

You can modify that code to test the UI space for each difficulty setting, like this:

        //this method determines if easy radio button has been clicked
public bool EasyBtnClicked()
{
    //if the mouse is over the easy radio button
    if (Mouse.X >= 210 && Mouse.X < 397 &&
        Mouse.Y >= 125 && Mouse.Y < 140)
    {
        //check to see if the mouse left button was down, but now is up
        return oldMState.LeftButton == ButtonState.Pressed &&
               mouseState.LeftButton == ButtonState.Released;
    }
    else return false;
}
//this method determines if the easy radio button has been clicked
public bool MedBtnClicked()
{
    //if the mouse is over the med radio button
    if (Mouse.X >= 210 && Mouse.X < 397 &&
        Mouse.Y >= 145 && Mouse.Y < 160)
    {
        //check to see if the mouse left button was down, but now is up
        return oldMState.LeftButton == ButtonState.Pressed &&
               mouseState.LeftButton == ButtonState.Released;
    }
    else return false;
}

//this method determines if the hard radio button has been clicked
public bool HardBtnClicked()
{
    //if the mouse is over the hard radio button
    if (Mouse.X >= 210 && Mouse.X < 397 &&
        Mouse.Y >= 165 && Mouse.Y < 180)
    {
        //check to see if the mouse left button was down, but now is up
        return oldMState.LeftButton == ButtonState.Pressed &&
               mouseState.LeftButton == ButtonState.Released;
    }
    else return false;
}

//this method determines if the gm radio button has been clicked
public bool GMBtnClicked()
{
    //if the mouse is over the gm radio button
    if (Mouse.X >= 210 && Mouse.X < 397 &&
        Mouse.Y >= 185 && Mouse.Y < 200)
    {
        //check to see if the mouse left button was down, but now is up
        return oldMState.LeftButton == ButtonState.Pressed &&
               mouseState.LeftButton == ButtonState.Released;
    }
    else return false;
}

Just add the preceding methods to the end of the class and save it.

Back in the Update() method for Game1, find the line of code that calls the input class’s Update() method. It looks like this:

//update the input handler
input.Update(gameTime);

Immediately after that line, you’ll check the difficulty buttons’ states. It will be a series of calls to the methods you just wrote, in the form of conditional statements. Take a look at this:

//update the difficulty game state variable based on player input
if (input.EasyBtnClicked())
{
    diffState = 0;
    screenText.Print("Ah. The EASY way out?");
}
else if (input.MedBtnClicked())
{
    diffState = 1;
    screenText.Print("Sigh. Boring!");
}
else if (input.HardBtnClicked())
{
    diffState = 2;
    screenText.Print("You might be a worthy adversary…");
}
else if (input.GMBtnClicked())
{
    diffState = 3;
    screenText.Print("Prepare to lose.");
    screenText.Print("My name is TicTacToeGM…");
}

Now that the state update is handled, you need to update the UI based on the state. For now, you’re going to ignore the AI. All you need to do is check the diffState value and, based on that value, change the texture of the appropriate radio button. You can also add a chat item for a bit of panache. Immediately after the preceding conditionals, let’s add this:

//update the UI based on the diffState update above
switch (diffState)
{
  case 0:
      easyBtn = easyBtnOn;
      medBtn = medBtnOff;
      hardBtn = hardBtnOff;
      gmBtn = gmBtnOff;
      break;
  case 1:
      easyBtn = easyBtnOff;
      medBtn = medBtnOn;
      hardBtn = hardBtnOff;
      gmBtn = gmBtnOff;
      break;
  case 2:
      easyBtn = easyBtnOff;
      medBtn = medBtnOff;
      hardBtn = hardBtnOn;
      gmBtn = gmBtnOff;
      break;
  case 3:
      easyBtn = easyBtnOff;
      medBtn = medBtnOff;
      hardBtn = hardBtnOff;
      gmBtn = gmBtnOn;
      break;
  default:
      easyBtn = easyBtnOff;
      medBtn = medBtnOff;
      hardBtn = hardBtnOff;
      gmBtn = gmBtnOn;
      diffState = 3;
      break;
}

This is pretty straightforward stuff. Save and run it. Figure 2.2 shows the game at startup; Figure 2.3 shows one of the panache messages being displayed. You get the idea.

The game at startup.

Figure 2.2. The game at startup.

A panache message.

Figure 2.3. A panache message.

Note

I do get asked quite a bit about my almost religious fervor for including a default case in switch statements. Let’s just say that if all goes according to plan, the code will never get called, so you’ve wasted nothing but a few seconds typing. But since few things ever go according to plan all the time, this bit of typing will save you some headaches.

All that’s left is to mutate the AI in response to the difficulty state. This is the trickiest part of the whole schmeer (yes, Valued Reader, “schmeer” is, in fact, a technical AI term—like “stuff”). You could do something like make the easy state pick random numbers in the doAI() method to determine where to put the AI tokens. It would be simple. In the member list, add a Random instance:

//AI and game state variables
const int MAX_TURNS = 9;
int turn;
int moveAI;
int drawTurn;
bool AIWin;
bool playerWin;
int diffState;
Random r = new Random();

Then, in the doAI() method, add this at the top of the method:

//a really bad easy mode
if (diffState == 0)
{
    moveAI = r.Next(7);

    while (board[moveAI].Value != 2)
    {
       moveAI = r.Next(7);
    }

    return;
}

Pretend you didn’t read the comment at the top of the code block, and save it, run it, pick easy mode, and play. Figure 2.4 shows why this is such a bad easy mode…I picked, in order, cell 2, cell 5, and cell 8. By random chance, the AI picked cell 3 and then cell 1, completely ignoring my march down the right column.

My mad march to victory!

Figure 2.4. My mad march to victory!

Why is this bad? It’s the complete opposite of our mega-unbeatable AI model. It’s so easy you can beat it in your sleep. It will be fun for exactly zero seconds. You’ll also be in that same leaking boat as my senior dev friend—the AI will be considered “dumb” by players.

Okay, so let’s cut that nonsense out of there (but leave in the Random object) and make a few fun modes based on variable difficulty. What makes our GM mode so hard to beat? For one thing, it handles all the special cases perfectly. That being the case, you could simply turn off some of the special case handling to make hard mode differentiate from GM mode.

In my opinion, the Split Attack Gambit and the Misdirection Gambit are pretty advanced Tic-Tac-Toe strategies. Let’s reserve those only for GM mode. You can do this by guarding the code to handle those methods with a check of diffState. Like this:

//else check for the Split Attack Gambit
//only allow this gambit to be defended in GM difficulty
else if ((diffState == 3) && (board[7].Value == 3))
{
    if (board[0].Value == 3)
    {
        moveAI = 6;
        return;
    }
    else if (board[2].Value == 3)
    {
        moveAI = 8;
        return;
    }
}

As you can see, one simple relation check makes this mode a GM-only defense. You add it first for efficiency’s sake (if diffState != 3, then it should ignore the rest of the equation). You’ll do the same thing for the Misdirection Gambit:

else if ((diffState == 3) && (turn == 5))
{
//check to see if we are in danger from the Misdirection Gambit

//check to see if the AI owns the center square
if (board[4].Value == 5)
{
    //if so, check to see if the player as two adjacent non-corner cells
    if (board[1].Value == 3 && board[3].Value == 3 && board[0].Value == 2)
    {
         //the player owns the top and left non-corner cells, pick cell 0 to block
         moveAI = 0;
         return;
    }
    else if (board[1].Value == 3 && board[5].Value == 3 && board[2].Value == 2)
    {
         //the player owns the top and right non-corner cells, pick cell 2 to block
         moveAI = 2;
         return;
    }
    else if (board[7].Value == 3 && board[5].Value == 3 && board[8].Value == 2)
    {
         //player owns the bottom and right non-corner cells, pick cell 8 to block
         moveAI = 8;
         return;
    }
    else if (board[3].Value == 3 && board[7].Value == 3 && board[6].Value == 2)
    {
         //player owns the bottom and left non-corner cells, pick cell 6 to block
         moveAI = 6;
         return;
    }
}
//else proceed with regular algorithm
}

Cool. Run it as is and check the difference in a hard game versus a GM game. Figure 2.5 shows me winning with the Split Attack Gambit. Yeah! TAKE THAT MR. SNOOTY-GM-AI!! Er…ahem. Pardon me.

Lawlz! Mr. GM-AI gets swatted.

Figure 2.5. Lawlz! Mr. GM-AI gets swatted.

The question becomes, is hard still too hard? If you don’t know these two gambits, is the game sufficiently hard? It depends on what our target is for the hard mode. I coded the example thinking hard should make you work for a win—hard enough that you really have to be a good Tic-Tac-Toer (is that even a word?) to win. Did I accomplish that task? I think so—it’s still hard to beat, and it still defends itself well. Even if you disagree, you get the picture, I think, and can modify the hard mode to your heart’s content.

Let’s move on to medium mode. What should be our metric here? I’d think the medium setting should allow mediocre Tic-Tac-Toe players to win. With that in mind, let’s look for a few points in the game where you can make that distinction.

First, you can probably agree that a mediocre player may miss some potential three-in-a-rows (i.e., not identify that the opponent is about to score), but that the probability should be relatively low. That said, let’s use the instance of Random you left in the code from the bad easy mode to attach a probability on the AI “knowing” the player can win in the next round. All you need to add is a check on the diffState value in conjunction with a check that a random number from the Random instance is higher than some probability (like 75 percent). If you pass that check, let the AI return with the blocking move. Otherwise you go on with the move-finding algorithm (i.e., ignore the blocking move). You can accomplish this easily by changing a tiny bit of code:

//check to see if the player can win the next move, and if so, block it
IdentifyWin(1);
if (moveAI != -1)
{
    //in medium mode pick the blocking move 75 percent of the time
    int p = r.Next(100);
    if ((diffState < 2) && (p < 75))
    {
        return;
    }
    else if ( diffState > 1 )
    {
        return;
    }
}

The big question to you, Valued Reader: Is that enough? I’m glad you agree that it isn’t. You should also dumb down Mr. Snooty AI a bit more by making the Corner Gambit hard and GM mode only, and by randomizing how well it can solve the remaining gambits. To ignore the Corner Gambit in medium mode, you will change the existing code to test for hard and GM modes:

//check to see if the the 1,4,7 column and 3,4,5 row both contain a single X, a
//single O, and a space, which is the test for the Corner Gambit
//only defend this gambit in Hard and GM modes
if ((diffState >= 2 ) && (board[1].Value * board[4].Value * board[7].Value == 30) &&
    (board[3].Value * board[4].Value * board[5].Value == 30))
{
    //defend against the gambit by picking a corner square adjacent to a player
    //token
    if (board[1].Value == 3)
    {
        //given the board layout, if we are here, we know position 0 is open
        moveAI = 0;
        return;
    }
    else if (board[7].Value == 3)
    {
        //given the board layout, if we are here, we know position 8 is open
        moveAI = 8;
        return;
    }
}

To randomize the Sandbagging Gambit for medium mode, do this:

//check for and defend against the Sandbagging Gambit
if (board[0].Value * board[4].Value * board[8].Value == 45)
{
    //in medium mode, defend properly 20 percent of the time
    if (diffState < 2)
    {
        int p = r.Next(100);
        if (p < 20)
        {
            //to defend, choose the open upper corner
            moveAI = 2;
            return;
        }
    }
    else
    {
        //to defend, choose the open upper corner
        moveAI = 2;
        return;
    }
}
else if (board[2].Value * board[4].Value * board[6].Value == 45)
{
    //in medium mode, defend properly 20 percent of the time
    if (diffState < 2)
    {
        int p = r.Next(100);
        if (p < 20)
        {
            //to defend, choose the open upper corner
            moveAI = 0;
            return;
        }
    }
    else
    {
         //to defend, choose the open upper corner
         moveAI = 0;
         return;
    }
}

I’m going to say that the Center Square Gambit is basic enough that you should leave it alone for medium mode. Save what you’ve got and play it. Is medium mode significantly different from hard and insane? Is it still challenging (or could it be to mediocre players)? I think so, and what I say goes!

Last, and certainly least, is easy mode. This is probably going to be the hardest mode to code. How can you make the game easy to beat but not trivially easy? Should the AI be able to defend the same gambits that the medium mode AI can, but just assign different (lower) probabilities? Or should you cut all the gambits altogether?

Let’s try limiting the AI to no gambit defense. This is a simple solution:

//only handle gambit cases if the AI is NOT in easy mode
if (diffState > 0)
{
    //there were no winning moves, so begin special case checking
    if (turn == 1)
    {
        //if we are in this method and turn == 1, the player had the first move and
        //we need to defend against the Center Square Gambit
        if (board[4].Value == 3)
        {
            //the player took the center square so block the Center Square Gambit
            //by choosing a corner square instead of a non-corner square. Since
            //this is the second move of the game, all corners are open, so choose
            //the upper-left corner (position 0)
            moveAI = 0;
            return;
        }
        //if the player did not choose the center square, continue with the
        //normal algorithm
    }
    else if (turn == 3)
    {
        //if we are in this method and turn == 3, the player has taken two moves,
        //and the AI has taken 1. There are two gambits that may need to be
        //defended against

        //check to see if the AI owns the center square
        if (board[4].Value == 5)
        {
            //check to see if the the 1,4,7 column and 3,4,5 row both contain a
            //single X, a single O, and a space, which is the test for the Corner
            //Gambit
            //only defend this gambit in Hard and GM modes
            if ((diffState >= 2) && (board[1].Value * board[4].Value * board[7].
                 Value == 30) &&
                  (board[3].Value * board[4].Value * board[5].Value == 30))
            {
                  //defend against the gambit by picking a corner square adjacent
                  //to a player token
                  if (board[1].Value == 3)
                  {
                      //given the board layout, if we are here, we know position
                      //0 is open
                      moveAI = 0;
                      return;
                  }
                  else if (board[7].Value == 3)
                  {
                       //given the board layout, if we are here, we know position
                       //8 is open
                       moveAI = 8;
                       return;
                  }
             }
             //else check for the Split Attack Gambit
             //only allow this gambit to be defended in GM difficulty
             else if ((diffState == 3) && (board[7].Value == 3))
             {
                  if (board[0].Value == 3)
                  {
                       moveAI = 6;
                       return;
                  }
                  else if (board[2].Value == 3)
                  {
                       moveAI = 8;
                       return;
                  }
              }
              //else continue normal algorithm
         }
         //next check to see if the player owns the center
         else if (board[4].Value == 3)
         {
              //check for and defend against the Sandbagging Gambit
              if (board[0].Value * board[4].Value * board[8].Value == 45)
              {
                  //in medium mode, defend properly 20 percent of the time
                  if (diffState < 2)
                  {
                      int p = r.Next(100);
                      if (p < 20)
                      {
                          //to defend, choose the open upper corner
                          moveAI = 2;
                          return;
                      }
                  }
                  else
                  {
                      //to defend, choose the open upper corner
                      moveAI = 2;
                      return;
              }
          }
          else if (board[2].Value * board[4].Value * board[6].Value == 45)
          {
               //in medium mode, defend properly 20 percent of the time
               if (diffState < 2)
               {
                   int p = r.Next(100);
                   if (p < 20)
                   {
                       //to defend, choose the open upper corner
                       moveAI = 0;
                       return;
                   }
              }
              else
              {
                   //to defend, choose the open upper corner
                   moveAI = 0;
                   return;
              }
         }
         //else continue the normal algorithm
    }
}
else if ((diffState == 3) && (turn == 5))
{
     //check to see if we are in danger from the Misdirection Gambit

     //check to see if the AI owns the center square
     if (board[4].Value == 5)
     {
          //if so, check to see if the player as two adjacent non-corner cells
          if (board[1].Value == 3 && board[3].Value == 3 && board[0].Value == 2)
          {
               //the player owns the top and left non-corner cells,
               //pick cell 0 to block
               moveAI = 0;
               return;
          }
          else if (board[1].Value == 3 && board[5].Value == 3 &&
              board[2].Value == 2)
          {
              //the player owns the top and right non-corner cells,
              //pick cell 2 to block
              moveAI = 2;
              return;
          }
          else if (board[7].Value == 3 && board[5].Value == 3 &&
               board[8].Value == 2)
          {
               //player owns the bottom and right non-corner cells,
               //pick cell 8 to block
               moveAI = 8;
               return;
          }
          else if (board[3].Value == 3 && board[7].Value == 3 &&
               board[6].Value == 2)
          {
               //player owns the bottom and left non-corner cells,
               //pick cell 6 to block
               moveAI = 6;
               return;
          }
      }
      //else proceed with regular algorithm
   }
}

Long story short, easy mode applies only our original Tic-Tac-Toe algorithm, and blocks only potential player three-in-a-rows 75 percent of the time. It doesn’t handle special cases (except by luck of the draw), and it is fairly easy to beat. It meets a target of having an AI that a novice can beat some of the time, and that an expert can absolutely slay all of the time. (Woot! PVP-Tic-Tac-Toe!)

Ready for the brutally hard question? Is what you just did enough to make this game a fun game? My brutally hard answer: only for a short time. Let’s face it: This game doesn’t have enough to hook us for long. As soon as you figure out how to win, it’s over. At any rate, all you handled here was the “impossible to beat” flaw. Handling that did partially address the second flaw (believability), but it leaves a lot of room for improvement.

Never fear, Valued Reader, as promised, you can fix the remaining problems (or you can at least act pretty confident that you can). As strange as it may sound on the face of it, the remaining flaws are problems with the game’s interaction hierarchy. Further, believability itself is fully defined by the interaction hierarchy.

Yeah. So. What’s this mythical interaction hierarchy thing? There are three very important concepts in video games:

  • Interactivity

  • Engagement

  • Immersion

Further, these three concepts can be used as a quality metric for any game. After we’ve discussed these three concepts in detail, we will return to the concept of an over-arching hierarchy defined by them.

In this chapter, you will examine the concepts and their relationship to one another, and you will hack away at the Tic-Tac-Toe game in an effort to make it better. The chapter will end with a discussion of how the aforementioned three concepts are linked hierarchically and what the implications for character AI are.

Wanna Play?

What is play, exactly? Oh boy. This could rapidly turn into another one of those long chapters where I say things like, “Unfortunately there is no clear and widely accepted definition….” Yep. That’s a prevalent theme in this work. Fortunately, however, Katie Salen and Eric Zimmerman do an incredible job of addressing this issue in their book Rules of Play, so I see no reason to try to re-create their collective genius here. Suffice it to say that there are tons of definitions, arguments, pleas, prayers, and declarations of clarity out there.

At its base, however, play in the sense you care about here (i.e., playing a game) is defined by interaction. Specifically, play is the interaction of a player (or players) and a game system and the experience that results. Well, that was certainly easy!

What is interaction then? You guessed it—yet another hairy term with definitions flying around like gnats on a summer night. By some definitions, anything that reacts to some input is interactive. By other definitions, this book itself is interactive (assuming, of course, that you are still conscious and reading), as it acts as a medium across which I deliver my message to you, or because you are touching or otherwise changing the book (at least idly turning the pages or closing the cover in a rage). However, neither of these definitions is very productive in this problem space. If everything is interactive, then the term becomes meaningless as both a differentiation tool and a design metric.

So what definition should you use? Let’s start by looking at what kinds of things can be interactive:

  • Human conversation is certainly interactive.

  • Human gesture and expression is certainly interactive.

  • Games (both digital and analog) can be interactive.

  • Live music can be interactive.

  • Creating or participating in fiction can be interactive.

  • The creative artistic process can be interactive (e.g., when the medium changes what the creator intended to make).

  • The creative programming process can be interactive (e.g., when what you set out to write is changed by the process of coding it).

  • Thinking can be interactive (e.g., when you create that reflective self and have an internal discussion).

Okay, so what kinds of things aren’t interactive in the sense that’s meaningful for our discussion of video games?

  • Pressing a button (digital or analog)

  • Listening to music

  • Using a doorknob

  • Passive activities (viewing art, consuming fiction, etc.)

  • Web pages

What are the differences between these two sets of activities? The first, and most glaring, difference is that the first set of activities consists of n-ary functions. Plainly speaking, these things all require at least two entities. Whoa, whoa, whoa. Hold on a minute here, Mr. Authorman. You said these things require at least two individuals. What about the thinking activity? Yes, Valued Reader, even the thinking activity requires two individuals. Who are they? You and your reflective self.

In the case of the creative artistic or programming activity where the “muse” becomes interactive, the second entity is also your reflective self. (Although you call it the medium, muse, or whatever, it’s actually your brain making the muse work.)

Also, the interactive activities are specific to a certain context that establishes both the meaning and the ground rules for the interaction. For example, in a human conversation, the context is defined by the topic being discussed and whatever cultural norms apply. Also, the ground rules are established by the syntax and semantics of the language being used; cultural rules and norms have their impact here as well.

Finally, these activities are also repeatable in a meaningful way. By way of explanation on this point, let’s consider what has become a fairly ubiquitous and meaningless interaction commonly found between humans. Many times, when interacting with people you don’t know well, a common starter to conversations is as follows:

Person 1: “Hello, how are you?”

Person 2: “Fine. How are you?”

Person 1: “Fine. The reason I’m speaking to you is….”

This is generally meaningless, as the two parties frequently have little or no concern about the answers to these questions. They’ve just been taught that they should ask and answer them to be polite. When someone responds differently than “Fine” to the question or does not ask “How are you?” in turn, you are frequently jolted over a mental speed bump, and the conversation gets a little derailed. To further illustrate how this interaction has become meaningless, let’s examine what happens when one person is slightly distracted (see Figure 2.6 for comic relief):

Person 1: “Hello, how are you?”

Person 2: “Fine. How are you?”

Person 1: “Fine. How are you?”

Meaningless interaction.

Figure 2.6. Meaningless interaction.

What, then, makes an interaction meaningful? Both parties should be in a position to change the scope of the interaction. In addition, the inputs to the interaction should have importance to the relationship that exists between the parties of the interaction. Note also that this context does not imply any limits on the kind of coupling that the interaction requires. Interaction can certainly be decoupled over time and may occur between two parties that don’t know who the other is. In fact, one of my colleagues at R.I.T. had a very interesting interaction with a group of anonymous students. Overnight, a huge collage of photos culled from a recent magazine article about the professor mysteriously became taped to the wall, floor to ceiling. The professor was amused and left a public note for the pranksters. Based on that note, other students began to draw on the pictures, making each one a new description of the professor. This went on for several iterations over the span of a couple of weeks. Certainly, this instance describes interaction.

Interaction, then, is defined by the following:

  • A semantic context (or environment) that defines the interaction

  • Repeatability across the session of interaction

  • A meaningful exchange

  • A reciprocal relationship of two or more

Having said that, let’s take a look at the TicTacToe_v0.1 game in light of the definition. If, Valued Reader, you are willing to go as far as saying that the AI represented in TicTacToe_v0.1 is an entity that can participate in a relationship and that that relationship can be reciprocal, then let’s examine the other three points (and yes, you will return to this assumption, of course).

Does our implementation provide a context or environment that can provide meaning to the interaction? Yes. It has an interface to the game that provides context, and there is the formal space for the gameplay. There are rules you enforce—the round-robin turn-based approach, accepting tokens only in empty spaces, etc.

Is the environment very deep? Not really. It’s just an interface for the game of Tic-Tac-Toe. Perhaps, then, you can work toward making the environment deeper.

Does the game offer repeatability? Certainly. Every time the player changes the difficulty setting, you repeat the same line of text. Also, in the GM difficulty setting, you offer the repeated opportunity to lose to the AI. Perfect. This begs the question, however, if being repeatable is enough. I’m not convinced that in a game, an annoying yet repeatable interaction does anyone any good. Yes, there are numerous counterexamples of annoying, nerve-grinding gameplay out there, but you really want to be better than that, right? Let’s leave this point alone for the time being, and simply say that interaction requires repeatability, but good interaction should be enjoyable, fun, and should motivate a desire in the player to repeat the interaction.

Yes, these examples are kind of trite and partially tongue in cheek, but the point made by the discussion is absolutely valid. You should also consider whether repeating the same line of text every time affects believability; we’ll return to that discussion later in this chapter. Beyond these points, does the game offer repeatable interaction? Yes, by the nature of varying difficulty levels, you offer repeatable interaction, and this repeatability is iterative—that is, you can replay the game on a higher difficulty level and have a new experience with the interaction. In the context of the TicTacToeGM you created in Chapter 1, the answer would be a definite maybe—you would have repeatability, but the experience would be fundamentally the same, and you’re back to the question of whether the repeatability leads to a good interaction.

Does the game provide a method of meaningful exchange? In my opinion, the answer is no. Why isn’t the exchange meaningful? Simply put, all you can do here is try to beat or at least tie the AI at various levels of difficulty. What makes playing Tic-Tac-Toe with a human meaningful is not limited to the actual game itself—it’s the interaction between the players that makes it have meaning. Does this AI, then, provide a player-to-player level of activity? Heck no.

This brings us full circle back to our first assumption: Is the AI you wrote for this version of the game sufficient to carry on a reciprocal relationship? What does that phrase even mean in this context? It’s likely you both have some colloquial definition for this phrase in mind, but as you’ve already seen, that isn’t good enough for the crazy man writing this book!

For games, let’s define reciprocal as something shared, felt or shown by both sides. The best game-centric definition for relationship is the connection between participants. So a reciprocal relationship for our purposes is the connection between participants that is shared, felt, or shown by all sides.

Does our TicTacToe_v0.1 share any connection with the player or at least show some connection? Again, in my opinion, it does not. Even if you bribed me to be extremely generous in my analysis of the game (which would, of course, cost two Twinkies), the best you could say of our AI is that it completely ignores the player or, at best, reacts only to certain mouse clicks. (Of course, you know that it reacts to everything you tell it to react to, but what a player sees is that when the player moves, the game moves, and that when the player changes the difficulty setting, the game makes a repetitive comment.) That’s probably not very good news in terms of interaction. In addition, I certainly don’t feel any connection to the AI—it’s just something that puts Os on the screen. (It is certainly better in v0.1 than in the TicTacToeGM version; in GM mode, the AI puts Os on the board and frustrates me with its über strategy-crushing play.)

The good news is that you made an iterative step with TicTacToe_v0.1, and it should be clear that in the sense of game interaction discussed in this section, that version is miles better than the TicTacToeGM version you created in Chapter 1—even though, or perhaps because, the GM version uses an unbeatable AI all the time. If this were going to be a commercial game, you’d have to tune the GM mode in v0.1 to allow a player to win—just very rarely. You would have to do this so that even advanced Tic-Tac-Toe players could play against an appropriate opponent but allow them to succeed (which is a crucial point in game design).

To summarize, by the aforementioned definition of interaction, our TicTacToeGM game is, at best, only minimally interactive. While it does provide an adequate environment and context for interaction, the repeatability of the interaction will likely annoy players (probably a bad thing), the exchange is not likely meaningful (definitely a bad thing), and the AI is inadequate to create a reciprocal relationship. Wow, that’s pretty brutal. So be it. You need more of this kind of brutal self-analysis in game design.

The TicTacToe_v0.1 game is more interactive. It provides the same environment and context as the GM game, and the repeatability is more appropriate and meaningful. While there still might be annoying bits (like the same phrase repeated over and over), the gameplay should not be annoying (or at least you can turn off the annoying mode of gameplay). Unfortunately, the AI is still insufficient to create a meaningful reciprocal relationship, in my opinion. Don’t get too upset—there are very few games, even hugely successful commercial titles, that can pass this metric as we’ve defined it—partially because of the qualitative nature of the definition and partially because this issue overlaps with more complex issues like believability, engagement, and immersion.

This last bit brings us back to Turing, and provides a nifty little segue into talking about what I like to call balanced versus unbalanced interaction. A balanced interaction is one in which both parties are equally important to the interaction’s success. For instance, in human-to-human interactions such as team building, each party’s involvement in the interaction is crucial. On the other hand, an unbalanced interaction is one in which one party is more important to the success of the interaction than the other. A game is a perfect example of unbalanced interaction. At the end of the day, no one really cares if the game feels a connection with the human player—you only care that the player feels the connection. At least until later chapters….

In Chapter 1, we discussed Turing’s imitation game and proposed that the goal of video-game AI should be to express actions in a manner that appears to be human. It should be clear that any entity that meets that goal requires only an unbalanced interaction (although a balanced interaction may be crucial to long-term interactions). Okay, fine, but how do you go about making the player feel some connection to the AI? You need the player’s help to do that—and to get the player’s help, you need to engage and then immerse him or her in the game. We’ll discuss that in the next two sections of this chapter.

C’mon! Play Along!

Again with the massively over-used terms with 98,000 different definitions: Let’s talk about engagement in video games. First, though, let’s take a second to talk about what you need to happen for a game to be successful. This discussion will not include a graphics versus gameplay soapbox, but rather a description of how studios make money. As a gross simplification, studios earn their money by selling boxes to players.

Repeat business requires good games. (Or really great marketing! Or both!) Unfortunately, what is meant by “good games” is not clearly defined for all cases, but you do know that games that hook a player and keep that player hooked long term get more positive player evaluations than those that don’t. How do games set the hook and keep the hook set? In the world of pat answers, I’ll just say “By being really good games,” but that just brings us full circle. And although I like circles as much as any other shape, and despite my penchant for crop circles, I don’t like circular definitions (I’m sure you are shocked, Valued Reader, by this revelation).

Games get us hooked by engaging us—either in some fictive environment or in some specific game mechanic. Games keep us hooked by keeping us engaged. Engagement in games, however, is a very specific concept. In my experience, the definitions that may fit the term engagement in other disciplines do not work as well in games.

Brenda Laurel’s definition of engagement from her book Computers as Theatre (and ultimately based on Samuel Taylor Coleridge’s work) is particularly awesome for use in this context. According to her, engagement is a player’s willing participation in the fiction presented by the game. The payoff for the player is that by going along with the fiction, he or she can experience emotional responses to the fiction while remaining safe from any actual harmful effects.

This topic gets even more complicated since there are four distinct types of engagement:

  • Emotional engagement. This comes down to trying to get the player to become emotionally invested in the gameplay.

  • Psychological engagement. This requires involving the player’s personality in gameplay.

  • Intellectual engagement. This involves challenging the player on a problem-solving or logical level.

  • Physical engagement. Here, there is the creation of a connection with the player on a physical level (think Wii!).

Let’s take a more in-depth look at these, but in reverse order (you know I have to throw some kind of curve here…). Creating a physical connection with a player is going to require novel control sets or interesting gidgets like rumble packs; nifty little guitar-shaped, skateboard-shaped, etc. controllers; or accelerometer-based controllers. This physical connection adds to the complete picture of the game and can deepen play experience. However, it’s not directly germane to character AI, so we won’t spend much more time on this one.

Players want to be challenged, and they want to solve problems in games. Generating challenge in a video game is pretty easy in most games (by cheating—the game is harder because the AI is faster, omniscient, able to walk through walls, etc.), but generating meaningful challenge requires an understanding (or intuition) of how people think and using that understanding to create problems that increase the fun. With a good design, character AI can augment your efforts to create intellectual engagement, but can’t, on its own, replace the challenging elements.

Creating psychological engagement really means using aspects of human psychology to create a desire to play and to continue playing the game. Of course, there are as many facets to the generation of psychological engagement as there are to any other psychology-dependent task. Luckily, we already have natural and ubiquitous methods for engaging people at this level—storytelling, interaction, humor, sarcasm, wit, cinematography, etc.—and virtually all of them exist in the realm of character AI. What kinds of things can you do to increase psychological engagement specifically? You can provide virtual teammates—both flawed and heroic. You can provide virtual enemies or virtual friends to compete against. You can provide a context for the engagement—a fictional world chock-a-block with interesting NPCs to talk to, fight, laugh at, etc. Are any of these simple tasks? Maybe some appear simple if you don’t go deeper than the surface, but to provide meaningful and lasting interaction, you’ve got to do a lot of work.

Eliciting someone’s emotional investment in a piece of fiction can seem like a magical process, but there are numerous tools and methods for creating emotive content, although not many of them are directly applicable to games. For instance, in fiction writing, it is helpful to know the emotional targets for the reader—both the starting and ending emotions and any steps in between. You can also make this process easier in fiction by writing to all five of the human senses. You can describe the scene in terms that are ubiquitous with respect to the emotions and feelings generated (e.g., the antiseptic and medicinal smells of an emergency room, the sound of a siren, the sound of doctors and nurses scrambling to save a patient, cold air wafting across your skin, etc.). In cinema, you can add visual elements to the fray; in addition, there are numerous techniques in the film industry for achieving engagement based on visual and aural language. In games, you get to cherry pick those techniques that will work best and adapt them to your peculiar context.

None of the things mentioned in the last paragraph are directly achievable with character AI, however, and that is, after all, the topic at hand. Still, the area of emotional engagement is the one where character AI can have the largest impact. But…but…wait, Mr. Authorman…You just said…I know. What you can do with character AI is create NPCs that the players can love or hate or be annoyed or amused by. To achieve a high level of engagement, however, you have to provide them with something that players can sink their teeth into. You have to endow the character with at least the appearance of a mental state—that is, the appearance that he or she has thoughts, goals, emotions, personality, etc. We will speak more about why this is (as well as exactly what this all means) in Chapters 5, “Gidgets, Gadgets, and Gizbots,” and 6, “Pretending to Be an Individual: Synthetic Personality”; for now, you need to know that this is our ultimate goal and that if you achieve this, you are almost guaranteed to generate emotional engagement.

Build Your Own (Virtual) Reality!

Okay. So we need to develop a really complex narrative for Tic-Tac-Toe? Er…no. The game itself is a fictional environment; you just need to identify which parts of the game’s structure create and support the fiction. There’s good news here. The same things that define traditional fiction environments—things like setting, characters, dialog, narration, etc.—also exist in games. In fact, you can make a pretty strong argument that fiction is fiction, despite the medium in which it is expressed. That allows you to borrow techniques from other disciplines when creating games.

Nice! So you can use semiotics, metaphors, meronomy, synchedoche, and a lot of other things described by really long words! Awesome! My peculiar brand of silliness aside, it is pretty awesome that you can use these techniques to help make games better. For instance, in the TicTacToeGM game, you can increase the interactivity of the game by creating a character for the player to interact with (or play against, as the case may be). To accomplish this, you need to allow the AI to interact with the player in some way other than just creating moves on the Tic-Tac-Toe board (which is an interaction after all). How, you ask? You may have noticed this really powerful gizbot we added to put text on the screen for the player…maybe you can use it to your advantage!

Let’s define the character by establishing a set of statements the character might make to the player and identify when the character should make those statements. To do so and support any kind of believability for your AI character, you need to define what kind of person your AI character is. Yes, this sounds an awful lot like a character sketch—and in a way, it is. You need this level of design, however, so that your character acts in a consistent manner. In other words, you have to do your part in maintaining the suspension of disbelief that Brenda Laurel talks about.

Let’s give our AI an edge. I don’t mean we’ll let him cheat (well, no more than we already do); instead, I mean that we should give him attitude. Let’s give him a nice set of sarcastic taunts and witticisms and let him throw them at the player based on the states you’ve already identified in the AI. You’ve already started down this road by naming the gameplay modes as you did, so this will work out beautiferously.

Create a TicTacToeGame_v0.2 solution that includes everything from the 0.1 version. Because this game class is starting to get really big, and because I have the feeling that you, Valued Reader, will likely force me to do more work with this whole taunting thing, let’s create a class that generates the taunts named Dialog.cs. What do we need this class to do exactly? For now, this class can get away with being a simplified kind of manager (or factory) class. Essentially, you will create a set of strings that will contain these taunts and access them from the game class.

What kinds of dialog do you need? You can figure this out by looking again at the DoAI() method in Game.cs and doing a bit of creative design to support the goal of this character having an edge. For instance, the first thing you check in that method is whether the AI can win on the next move. That’s a great place to start. What would a sarcastic, taunting player say to his opponent right before he wins? How about something like: “Ha! Pwnt you noob!”

Inside Dialog.cs, add the following line at the top of the file (i.e., as a member):

private const string AIWin = "Ha! Pwnt you noob!";

Why private? I could go on and on here about good software engineering practices, encapsulation, and average rainfall in non-tropical sub-arctic regions, but at the most basic level, you make this thing private because later I’m going to change a bunch of stuff and I don’t want to have to rewrite everything. Why const? Simply put, you’re not going to be changing this string ever (at least not during run time). That being the case, you should let the compiler know so it can do optimization-fu on the memory you’re reserving. Why use the string type instead of making a String object? Personal preference, really, as there is no real difference in C#.

Next, you need a way to access this private variable. Since you’re using C#, you can easily use a property for this data. To do so, you need to add this code to the properties section of the class:

public string AIWinTaunt
{
    get { return AIWin; }
}

Obviously, you need to use this fancy new dialog system in the game. To enable that, you need to add a Dialog object to the game class. At line 63, I added the following:

Dialog dialog;

I then initialized the dialog class in the Initialize() method like so:

//dialog system init
dialog = new Dialog();

Finally, at line 267 in the Update() method, I added to the AIWin case code like this:

//check to see if the AI already won
if ( (!winMsgSent) && CheckWin(0))
{
    AIWin = true;
    winMsgSent = true;
    screenText.Print(dialog.AIWinTaunt, 3500);
    screenText.Print("YOU LOST!",
        15000,
        BoldBannerPos,
        Color.Red,
        ScreenText.FontName.ExclaimBold);
}

If you play the game now and allow it to win, you will be rewarded by a brutal taunting! Fun stuff.

Okay, so you have one taunt that the AI can smash in our faces. What else can you add? The next logical step would be to add a player wins statement. You know, something like “Hmph. Cheat much?” You’ll follow the same steps as before to add this entry to the Dialog class (private static member variable, public property to access it). Then, in the Game.cs class at line 255, you will change the player win state like this:

//check to see if the player already won
if ( (! winMsgSent) && CheckWin(1))
{
    playerWin = true;
    winMsgSent = true;
    screenText.Print(dialog.PlayerWinTaunt, 3500);
    screenText.Print("YOU WON!",
        15000,
        BoldBannerPos,
        Color.Red,
        ScreenText.FontName.ExclaimBold);
}

Since you’ve gone to these great lengths to create a Dialog class, you should replace the static instances of dialog you already had in the game. This will allow you to be really flexible (like ballet dancers!) and easily change your lines of dialog. Doing this is really simple and really mechanical…you just stroll through the Game1.cs file and change each instance where you used a static member variable or a constant string (moving the string to a variable declared in the dialog class and then accessing the string appropriately). This is pretty straightforward, so I’ll just do it all and update the source.

For the difficulty dialog, add the following to Dialog.cs:

#region difficultyDialog
private const string Easy = "Ah. The EASY way out?";
private const string Medium = "Sigh. Boring!";
private const string Hard = "You might be a worthy adversary…";
private const string Insane = "I am TicTacToeGM. Prepare to lose!";
#endregion

Add the following properties to Dialog.cs:

#region difficultyDialog
public string EasyMode
{
    get { return Easy; }
}
public string MediumMode
{
    get { return Medium; }
}
public string HardMode
{
    get { return Hard; }
}
public string InsaneMode
{
    get { return Insane; }
}
#endregion

In Game1.cs, replace the existing dialog lines (starting at line 294) like this:

//update the difficulty game state variable based on player input
if (input.EasyBtnClicked())
{
    diffState = 0;
    screenText.Print(dialog.EasyMode);
}
else if (input.MedBtnClicked())
{
    diffState = 1;
    screenText.Print(dialog.MediumMode);
}
else if (input.HardBtnClicked())
{
    diffState = 2;
    screenText.Print(dialog.HardMode);
}
else if (input.GMBtnClicked())
{
    diffState = 3;
    screenText.Print(dialog.InsaneMode);
}

For all those nice constant strings you put in to update the player on the game state, you just replace them by adding the following to Dialog.cs:

#region gameplayUpdates
private const string Win = "YOU WON!";
private const string Lose = "YOU LOST!";
private const string Draw = "DRAW!";
private const string NoMore = "No more moves…";
private const string Taken = "That space is taken!";
private const string Three = "Three in a row! ";
#endregion

Again, augment Dialog.cs with the following properties:

#region gameplayUpdates
public string Winner
{
    get { return Win; }
}
public string Loser
{
    get { return Lose; }
}
public string Drawer //:)
{
    get { return Draw; }
}
public string NoMoreMoves
{
    get { return NoMore; }
}
public string SpaceTaken
{
    get { return Taken; }
}
public string ThreeInARow
{
    get { return Three; }
}

In Game1.cs, replace the termination status dialog starting at line 250 (I have bold-faced the lines that changed:

//check to see if the player already won
if ( (! winMsgSent) && CheckWin(1))
{
    playerWin = true;
    winMsgSent = true;
    screenText.Print(dialog.PlayerWinTaunt, 3500);
    screenText.Print(dialog.Winner,
        15000,
        BoldBannerPos,
        Color.Red,
        ScreenText.FontName.ExclaimBold);
}

//check to see if the AI already won
if ( (!winMsgSent) && CheckWin(0))
{
    AIWin = true;
    winMsgSent = true;
    screenText.Print(dialog.AIWinTaunt, 3500);
    screenText.Print(dialog.Loser,
        15000,
        BoldBannerPos,
        Color.Red,
        ScreenText.FontName.ExclaimBold);
}

//check for a draw
if ((!winMsgSent) && turn == drawTurn)
{
    screenText.Print(dialog.Drawer,
        15000,
        BoldBannerPos,
        Color.Red,
        ScreenText.FontName.ExclaimBold);
    //turn off the game
    AIWin = playerWin = true;
    winMsgSent = true;
    screenText.Print(dialog.NoMoreMoves);
}

Those pesky “This space taken” lines get changed as follows on lines 499, 514, 529, 549, 564, 579, 599, 614, and 629:

screenText.Print(dialog.SpaceTaken, 3500);

The not-quite-as-pesky “Three in a row” lines get similarly mutated starting at line 1174:

//check rows
for (int i = 0; i < 9; i += 3)
{
    if (board[i].Value * board[i + 1].Value * board[i + 2].Value == testval)
    {
        screenText.Print(dialog.ThreeInARow + i + "," + (i + 1) + "," + (i + 2));
        winMsgSent = true;
        return true;
    }
}

//check columns
for (int i = 0; i < 3; i++)
{
    if (board[i].Value * board[i + 3].Value * board[i + 6].Value == testval)
    {
        screenText.Print(dialog.ThreeInARow + i + "," + (i + 3) + "," + (i + 6));
        winMsgSent = true;
        return true;
    }
}

//check the 0,4,8 diagonal
if (board[0].Value * board[4].Value * board[8].Value == testval)
{
        screenText.Print(dialog.ThreeInARow + "0,4,8");
        winMsgSent = true;
        return true;
}

//check the 2,4,6 diagonal
if (board[2].Value * board[4].Value * board[6].Value == testval)
{
        screenText.Print(dialog.ThreeInARow +"2,4,6");
        winMsgSent = true;
        return true;
}

Great! You are well on your way to creating a nice character to play Tic-Tac-Toe against. The rest of the tasks are fairly mechanical, doing the same steps as before to add new text taunts and play them at the appropriate time in the game. I suggest you add these taunts to each state change of the AI:

  • When the AI detects the player may win in the next round and tries to block: “No way! I’m on to you!”

  • When the AI detects and blocks the Center Square Gambit: “I’m too good for you.”

  • When the AI detects and blocks the Corner Gambit: “The corner thing? Really??”

  • When the AI detects and blocks the Split Attack Gambit: “Ur not serious, r u?”

  • When the AI detects and blocks the Sandbagging Gambit: “Oh gee, I’m completely confused…NOT!”

  • When the AI detects and blocks the Misdirection Gambit: “Wow. Just wow. Pwnt.”

Since we have this decided, we just need to update our Dialog class like this:

public class Dialog
    {
        #region members
        #region difficultyDialog
        private const string Easy = "Ah. The EASY way out?";
        private const string Medium = "Sigh. Boring!";
        private const string Hard = "You might be a worthy adversary…";
        private const string Insane = "My name is TicTacToeGM… Prepare to lose!";
        #endregion
        #region gameplayUpdates
        private const string Win = "YOU WON!";
        private const string Lose = "YOU LOST!";
        private const string Draw = "DRAW!";
        private const string NoMore = "No more moves…";
        private const string Taken = "That space is taken!";
        private const string Three = "Three in a row! ";
        #endregion
        #region AITaunts
        private const string AIWin = "Ha! Pwnt you noob!";
        private const string PlayerWin = "Hmph. Cheat much?";
        private const string AIBlock = "No way! I';m on to you!";
        private const string CenterSquare = "I'm too good for you.";
        private const string Corner = "The corner thing? Really??";
        private const string Split = "Ur not serious r u?";
        private const string SandBag = "Oh gee, I'm completely confused…NOT!";
        private const string Misdirect = "Wow. Just wow. Pwnt.";
        #endregion
        #endregion

        #region properties
        #region difficultyDialog
        public string EasyMode
        {
            get { return Easy; }
        }
        public string MediumMode
        {
            get { return Medium; }
        }
        public string HardMode
        {
            get { return Hard; }
        }
        public string InsaneMode
        {
            get { return Insane; }
        }
        #endregion
        #region gameplayUpdates
        public string Winner
        {
            get { return Win; }
        }
        public string Loser
        {
            get { return Lose; }
        }
        public string Drawer   //:)
        {
            get { return Draw; }
        }
        public string NoMoreMoves
        {
            get { return NoMore; }
        }
        public string SpaceTaken
        {
            get { return Taken; }
        }
        public string ThreeInARow
        {
            get { return Three; }
        }
        #endregion
        #region AITaunts
        public string AIWinTaunt
        {
            get { return AIWin; }
        }
        public string PlayerWinTaunt
        {
            get { return PlayerWin; }
        }
        public string AIBlockTaunt
        {
            get { return AIBlock; }
        }
        public string CenterSquareTaunt
        {
            get { return CenterSquare; }
        }
        public string CornerTaunt
        {
            get { return Corner; }
        }
        public string SplitTaunt
        {
            get { return Split; }
        }
        public string SandBagTaunt
        {
            get { return SandBag; }
        }
        public string MisdirectTaunt
        {
            get { return Misdirect; }
        }
        #endregion
        #endregion
}

Now that you have this daunting taunt structure, all that’s left is to hook it into the AI in all the right places. This is the hardest part, but it’s still not very hard…all you need to do is to put the dialog calls into the right places. Here’s how to alter the doAI() method (I’ve bold-faced the new lines to make it easier to pick them out):

//only handle gambit cases if the AI is NOT in easy mode
if (diffState > 0)
{
    //there were no winning moves, so begin special case checking
    if (turn == 1)
{
    //if we are in this method and turn == 1, the player had the first move
    //and we need to defend against the Center Square Gambit
    if (board[4].Value == 3)
    {
         //the player took the center square so block the Center Square Gambit
         //by choosing a corner square instead of a non-corner square.
         //since this is the second move of the game, all corners are open,
         //so choose the upper-left corner (position 0)
         screenText.Print(dialog.CenterSquareTaunt, 3500);
         moveAI = 0;
         return;
    }
    //if the player did not choose the center square, continue with the
    //normal algorithm
}
else if (turn == 3)
{
    //if we are in this method and turn == 3, the player has taken two
    //moves, and the AI has taken 1. There are two gambits that may need to
    //be defended against

    //check to see if the AI owns the center square
    if (board[4].Value == 5)
    {
         //check to see if the the 1,4,7 column and 3,4,5 row both contain a
         //single X, a single O, and a space, which is the test for the
         //Corner Gambit
         //only defend this gambit in Hard and GM modes
         if ((diffState >= 2) && (board[1].Value * board[4].Value *
              board[7].Value == 30) &&
             (board[3].Value * board[4].Value * board[5].Value == 30))
         {
             //defend against the gambit by picking a corner square adjacent
             //to a player token
             if (board[1].Value == 3)
             {
                 //given the board layout, if we are here, we know position
                 //0 is open
                 screenText.Print(dialog.CornerTaunt, 3500);
                 moveAI = 0;
                 return;
             }
            else if (board[7].Value == 3)
            {
                 //given the board layout, if we are here, we know position
                 //8 is open
                 screenText.Print(dialog.CornerTaunt, 3500);
                 moveAI = 8;
                 return;
            }
         }
         //else check for the Split Attack Gambit
         //only allow this gambit to be defended in GM difficulty
         else if ((diffState == 3) && (board[7].Value == 3))
         {
            if (board[0].Value == 3)
            {
                screenText.Print(dialog.SplitTaunt, 3500);
                moveAI = 6;
                return;
            }
            else if (board[2].Value == 3)
            {
                screenText.Print(dialog.SplitTaunt, 3500);
                moveAI = 8;
                return;
            }
         }
         //else continue normal algorithm
     }
     //next check to see if the player owns the center
     else if (board[4].Value == 3)
     {
         //check for and defend against the Sandbagging Gambit
         if (board[0].Value * board[4].Value * board[8].Value == 45)
         {
              //in medium mode, defend properly 20 percent of the time
              if (diffState < 2)
              {
                  int p = r.Next(100);
                  if (p < 20)
                  {
                      //to defend, choose the open upper corner
                      screenText.Print(dialog.SandBagTaunt, 3500);
                      moveAI = 2;
                      return;
                  }
              }
              else
              {
                  //to defend, choose the open upper corner
                  screenText.Print(dialog.SandBagTaunt, 3500);
                  moveAI = 2;
                  return;
              }

         }
         else if (board[2].Value * board[4].Value * board[6].Value == 45)
         {
              //in medium mode, defend properly 20 percent of the time
              if (diffState < 2)
              {
                  int p = r.Next(100);
                  if (p < 20)
                  {
                       //to defend, choose the open upper corner
                       screenText.Print(dialog.SandBagTaunt, 3500);
                       moveAI = 0;
                       return;
                  }
              }
              else
              {
                  //to defend, choose the open upper corner
                  screenText.Print(dialog.SandBagTaunt, 3500);
                  moveAI = 0;
                  return;
              }
         }
         //else continue the normal algorithm
     }
}
else if ((diffState == 3) && (turn == 5))
{
    //check to see if we are in danger from the Misdirection Gambit

    //check to see if the AI owns the center square
    if (board[4].Value == 5)
    {
        //if so, check to see if the player as two adjacent non-corner cells
        if (board[1].Value == 3 && board[3].Value == 3 && board[0].Value == 2)
        {
             //the player owns the top and left non-corner cells,
             //pick cell 0 to block
             screenText.Print(dialog.MisdirectTaunt, 3500);
             moveAI = 0;
             return;
        }
        else if (board[1].Value == 3 && board[5].Value == 3 &&
             board[2].Value == 2)
        {
             //the player owns the top and right non-corner cells,
             //pick cell 2 to block
             screenText.Print(dialog.MisdirectTaunt, 3500);
             moveAI = 2;
             return;
        }
        else if (board[7].Value == 3 && board[5].Value == 3 &&
            board[8].Value == 2)
        {
            //player owns the bottom and right non-corner cells,
            //pick cell 8 to block
            screenText.Print(dialog.MisdirectTaunt, 3500);
            moveAI = 8;
            return;
        }
        else if (board[3].Value == 3 && board[7].Value == 3 &&
            board[6].Value == 2)
        {
            //player owns the bottom and left non-corner cells,
            //pick cell 6 to block
            screenText.Print(dialog.MisdirectTaunt, 3500);
            moveAI = 6;
            return;
        }
    }
    //else proceed with regular algorithm
  }
}

Once you add this code, our super AI is now a character on the scale of Darth Vader, right? Well, no, but our game is showing an inkling of characterness. Go ahead and play with it a bit and form your own opinions…I’ll wait.

I know what you are thinking, Valued Reader. It is true that I did say this is what you needed to do, and here I am now complaining that it doesn’t provide Darth Vader–level character. Why in the heck not? What’s the matter with me (beyond the obvious…)?

If you played more than a few rounds of Tic-Tac-Toe with the taunts enabled, you probably realized very quickly that these static taunts are going to get pretty repetitive and predictable. Neither of those traits is very attractive if you are evaluating the level of engagement generated. And besides, this repetitiveness was one of the things you were trying to fix.

So why in the heck did we go through all this? I admit it was a sneaky ploy to start with the simple changes we made. Hopefully, you won’t hold this against me. We did lay the groundwork for a system that won’t be quite so repetitive. More than that, however, I wanted to show this static method—it’s fairly common in games, despite the relatively low payoff.

You can expand what you’ve got by making a few simple changes. Basically, you’re going to convert these static single statements into equivalence classes of similar feeling statements and then select from them randomly. You can also take the time at this point to refine your statements and make sure they are creating a feeling of character for the game.

Because of the property architecture we used in the dialog class, we’ll just change our simple, single string data with arrays of strings and then alter the property methods. Let’s start with the difficulty dialog (all changes will be in Dialog.cs):

#region difficultyDialog
private string[] Easy = { "Ah. The EASY way out?", "Wimp.", "I know I'm too good for
    you.", "Do you live in a bubble?" };
private string[] Medium = { "Sigh. Boring!", "Do you also like mayo and white
    bread?", "Where's your pocket protector?", "My dog plays this level." };
private string[] Hard = { "You might be a worthy adversary…", "Up for a challenge,
    eh?", "You will still lose.", "Kasparov lost on hard." };
private string[] Insane = { "I am TicTacToeGM. Prepare to lose!", "Switching on
    Skynet…", "Are you wearing your helmet?", "Destruction in t-minus 15…" };
#endregion

Note

You might notice that I’ve gotten rid of the const keyword in all these declarations. Some of you already know why: Compilers are contrary, picky beasts whose sole purpose in life is to enforce willy-nilly rules in a completely arbitrary fashion. Okay, actually, an array can be initialized as a const only if its value is null, which kind of defeats the purpose of the inline instantiation I want to use here. So fine! No const for you! One year!

To enable the next bit, you also need to add an instance of the Random class to the members list:

private Random rand = new Random();

Then you need to decide on some weights (i.e., the selection probability you want to attach to each of the preceding lines) for each dialog type. In this case, I’m going to go with an equal distribution, so no line should appear more than the next. You will do all the work inside the property with a few simple conditional statements:

#region difficultyDialog
public string EasyMode
{
    get {
        int p = rand.Next() % 100; //get a random value between 1 and 100 to use
             in probability testing
        if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
        {
            return Easy[0];
        } else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
        {
            return Easy[1];
        }
        else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
        {
            return Easy[2];
        }
        else //probability = 25 percent -- range is 75 <= x <= 99
        {
            return Easy[3];
        }
    }
}
public string MediumMode
{
    get
    {
        int p = rand.Next() % 100; //get a random value between 1 and 100 to use
            in probability testing
        if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
        {
            return Medium[0];
        }
        else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
        {
            return Medium[1];
        }
        else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
        {
            return Medium[2];
        }
        else //probability = 25 percent -- range is 75 <= x <= 99
        {
            return Medium[3];
        }
    }
}
public string HardMode
{
    get
    {
        int p = rand.Next() % 100; //get a random value between 1 and 100 to use
            in probability testing
        if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
        {
            return Hard[0];
        }
        else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
        {
            return Hard[1];
        }
        else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
        {
            return Hard[2];
        }
        else  //probability = 25 percent -- range is 75 <= x <= 99
        {
            return Hard[3];
        }
    }
}
public string InsaneMode
{
    get
    {
        int p = rand.Next() % 100; //get a random value between 1 and 100 to use
            in probability testing
        if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
        {
            return Insane[0];
        }
        else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
        {
            return Insane[1];
        }
        else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
        {
            return Insane[2];
        }
        else  //probability = 25 percent -- range is 75 <= x <= 99
        {
            return Insane[3];
        }
    }
}
#endregion

I know what you are thinking: “Damn it, Erik! I’m a doctor, not a statistician!” Right? Right? For my next trick, I will tell you what color socks you are wearing: greenbluegrayredblackpinkbrownwhite. Am I right?

In VGAI, we often try to combat the specter of predictability by doing exactly what I did above: We add randomness to the equation in the hopes that a probabilistic approach can be tuned to provide unpredictable behaviors (or at least add a dynamic feel to the way the AI acts) while still using a static system of behavior. Whoa, whoa, whoa…. Hang on a minute…. Static system of behavior? Dynamic-acting AI? Switching on Skynet?

Even the most predictable human on the planet has the capacity to surprise us with an unexpected behavior. In fact, I’m willing to go on record and say that the unpredictable nature of human opponents is the very reason you prefer to play against humans over AI, but even ignoring that gem of wisdom, if you want people to be engaged, you have to keep them actively thinking and imagining. Simple repetitive behavior is one of the fastest ways to achieve exactly the opposite.

Yes, but Mr. Authorman, you only added three extra behaviors here. Just how dynamic is dynamic enough? In all likelihood, it will take more than four lines of dialog when the response happens often. Okay, that was a small lie. It absolutely will take more than four. How many is enough? The pat answer is, THE number it takes to keep players from getting bored. Unfortunately, although that answer is pat, it’s also the only answer available. Having said that, for the simple purpose of exploring and illustrating this example, four is enough. I know that because I’m the boss, and what I say goes.

Ahem. Moving right along…. You can do the same type of trick in the gameplay update dialog section to add some flavor:

#region gameplayUpdates
private string[] Win = { "YOU WON!", "I LOST??", "CHEATER!!", "You…win??" };
    // must be shorter than 10 characters due to placement
private string[] Lose = { "YOU LOST!", "HA! I WON!", "NEXT!", "OUCH!" };
    // must be shorter than 10 characters due to placement
private string[] Draw = { "DRAW!", "TIE??", "TRY much?!", "TIE=I WIN!" };
    // must be shorter than 10 characters due to placement
private string[] NoMore = { "No more moves…", "It's over, man.",
    "So now we play on the lines?", "Party over. Out of moves."};
private string[] Taken = {"That space is taken!","Stop trying to cheat.",
    "Um…there's a token there.", "Yeah…Do you need glasses?"};
private string[] Three = { "Three in a row! ", "Tada! ", "Read it and weep ",
    "Pretend I'm excited " };
#endregion

And change the properties around a bit like so:

#region gameplayUpdates
        public string Winner
        {
            get
            {
               int p = rand.Next() % 100; //get a random value between 1 and 100
                   to use in probability testing
               if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
               {
                   return Win[0];
               }
               else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
               {
                   return Win[1];
               }
               else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
               {
                   return Win[2];
               }
               else  //probability = 25 percent -- range is 75 <= x <= 99
               {
                   return Win[3];
               }
        }
   }
   public string Loser
   {
       get
       {
          int p = rand.Next() % 100; //get a random value between 1 and 100
              to use in probability testing
          if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
          {
              return Lose[0];
          }
          else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
          {
              return Lose[1];
          }
          else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
          {
              return Lose[2];
          }
          else  //probability = 25 percent -- range is 75 <= x <= 99
          {
              return Lose[3];
          }
      }
   }
   public string Drawer //:)
   {
       get
       {
           int p = rand.Next() % 100; //get a random value between 1 and 100
               to use in probability testing
           if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
           {
               return Draw[0];
           }
           else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
           {
               return Draw[1];
           }
           else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
           {
               return Draw[2];
           }
           else //probability = 25 percent -- range is 75 <= x <= 99
           {
               return Draw[3];
           }
       }
   }
   public string NoMoreMoves
   {
       get
       {
           int p = rand.Next() % 100; //get a random value between 1 and 100
               to use in probability testing
           if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
           {
               return NoMore[0];
           }
           else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
           {
               return NoMore[1];
           }
           else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
           {
               return NoMore[2];
           }
           else  //probability = 25 percent -- range is 75 <= x <= 99
           {
               return NoMore[3];
           }
       }
   }
   public string SpaceTaken
   {
       get
       {
           int p = rand.Next() % 100; //get a random value between 1 and 100
               to use in probability testing
           if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
           {
               return Taken[0];
           }
           else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
           {
               return Taken[1];
           }
           else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
           {
               return Taken[2];
           }
           else  //probability = 25 percent -- range is 75 <= x <= 99
           {
               return Taken[3];
           }
       }
   }
   public string ThreeInARow
   {
       get
       {
           int p = rand.Next() % 100; //get a random value between 1 and 100
               to use in probability testing
           if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
           {
               return Three[0];
           }
           else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
           {
               return Three[1];
           }
           else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
           {
               return Three[2];
           }
           else //probability = 25 percent -- range is 75 <= x <= 99
           {
               return Three[3];
           }
       }
   }
   #endregion

Of course, all that’s left in this fine little plan to take over the world is to perform the same kind of actions with the AITaunts section of the Dialog class. You’ve seen and done this before, so I will leave the final changes to the dialog in your hands (or you may look at mine in the final code review below).

It is true that I’m still not doing anything interesting with the probabilities here. (It’s also true that you don’t really need conditionals in this simple of a system; you can just get a random integer between 0 and 3 and access the array with that integer, but this way is more fun and allows you the opportunity to pretend you might later tweak the probability weights.) That’s fine; you can tune them later if you want to tweak the responses in one direction or another. What you have at this point in the code is a game with a marginally interesting dialog system. Take a little break and play with it a bit with the various settings and get a feel for the gameplay and dialog system. I’m getting a cup of coffee….

In my opinion, we’ve built a game that is more engaging than its predecessor. I agree that this game will not win any awards for gameplay or engagement, and that what we’ve done with the dialog class is merely brush the surface to illustrate a couple of common methods. As you extend this work later to introduce other concepts, you will make this dialog system more and more complex.

How can you evaluate success? Ask yourself these questions: Does the dialog system encourage any emotional, psychological, or intellectual investment in your gameplay session on your part? Is this version of the Tic-Tac-Toe game more fun than the previous versions? Do you believe that the interaction is stronger or more enjoyable? My answers are in the positive, but I think we can both agree that there could be more variation in the dialog system and that the AI would feel more like a character with some tuning of the probabilities. It should also be considered whether the UI is designed for maximum impact of these various taunts, and whether the dialog itself is all “in character.” I will leave these questions as an exercise for you.

Following is the new and improved Dialog.cs class in its entirety:

public class Dialog
    {
        #region members
        private Random rand = new Random();
        #region difficultyDialog
        private string[] Easy = { "Ah. The EASY way out?", "Wimp.",
            "I know I'm too good for you.", "Do you live in a bubble?" };
        private string[] Medium = { "Sigh. Boring!", "Do you also like mayo and
            white bread?", "Where's your pocket protector?",
            "My dog plays this level." };
        private string[] Hard = { "You might be a worthy adversary…", "Up for a
            challenge, eh?", "You will still lose.", "Kasparov lost on hard." };
        private string[] Insane = { "I am TicTacToeGM. Prepare to lose!",
            "Switching on Skynet…", "Are you wearing your helmet?",
            "Destruction in t-minus 15…" };
        #endregion
        #region gameplayUpdates
        private string[] Win = { "YOU WON!", "I LOST??", "CHEATER!!",
            "You…win??" }; // must be shorter than 10 characters due
            to placement
        private string[] Lose = { "YOU LOST!", "HA! I WON!", "NEXT!", "OUCH!" };
            // must be shorter than 10 characters due to placement
        private string[] Draw = { "DRAW!", "TIE??", "TRY much?!", "TIE=I WIN!" };
            // must be shorter than 10 characters due to placement
        private string[] NoMore = { "No more moves…", "It's over, man.",
            "So now we play on the lines?", "Party over. Out of moves."};
        private string[] Taken = {"That space is taken!","Stop trying to cheat.",
            " Um…there's a token there.", "Yeah…Do you need glasses?"};
        private string[] Three = { "Three in a row! ", "Tada! ", "Read it and weep ",
            "Pretend I'm excited " };
        #endregion
        #region AITaunts
        private string[] AIWin = { "Ha! Pwnt you noob!", "And another one falls
            before me.", "Lawlz. QQ time?", "Opps! Hahahaha!" };
        private string[] PlayerWin = { "Hmph. Cheat much?", "Everyone gets lucky
            once.", "Ooooh! Look at the big humie!", "Bet you can't do it again!" };
        private string[] AIBlock = { "No way! I'm onto you!", "This is what we call:
            DENIED!", "Oops, is my piece in your way?", "Take that, silly humie!" };
        private string[] CenterSquare = { "I'm too good for you.",
            "Do humans fall for this?", "No, I don't think so.", "Wow. Lame." };
        private string[] Corner = { "The corner thing? Really??",
            "When I was a calculator, maybe…", "Oh gee, what should I do?",
            "Are you even trying?" };
        private string[] Split = { "Ur not serious r u?", "Where should I play?
            lol", "O wow. That's bad.", "I smell your pwning." };
        private string[] SandBag = { "Oh gee, I'm completely confused…NOT!",
            "I guess I'll just play here…", "Ok. Learn2Play.",
            "That's just…ah forget it." };
        private string[] Misdirect = { "Wow. Just wow. Pwnt.", "Do you know
            who I am?", "If you think that's a good plan…", "Guess you're
            going to lose this?" };
        #endregion
        #endregion

        #region properties
        #region difficultyDialog
        public string EasyMode
        {
            get {
                int p = rand.Next() % 100; //get a random value between 1 and 100
                    to use in probability testing
                if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
                {
                    return Easy[0];
                 } else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
                {
                    return Easy[1];
                }
                else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
                {
                    return Easy[2];
                }
                else //probability = 25 percent -- range is 75 <= x <= 99
                {
                    return Easy[3];
                }
            }
        }
        public string MediumMode
        {
            get
            {
               int p = rand.Next() % 100; //get a random value between 1 and 100
                   to use in probability testing
               if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
               {
                     return Medium[0];
               }
               else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
               {
                     return Medium[1];
               }
               else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
               {
                     return Medium[2];
               }
               else  //probability = 25 percent -- range is 75 <= x <= 99
               {
                     return Medium[3];
               }
           }
       }
       public string HardMode
       {
           get
           {
               int p = rand.Next() % 100; //get a random value between 1 and 100
                   to use in probability testing
               if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
               {
                     return Hard[0];
               }
               else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
               {
                     return Hard[1];
               }
               else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
               {
                     return Hard[2];
               }
               else  //probability = 25 percent -- range is 75 <= x <= 99
               {
                     return Hard[3];
               }
           }
       }
       public string InsaneMode
       {
           get
           {
               int p = rand.Next() % 100; //get a random value between 1 and 100
                   to use in probability testing
               if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
               {
                     return Insane[0];
               }
               else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
               {
                     return Insane[1];
               }
               else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
               {
                     return Insane[2];
               }
               else //probability = 25 percent -- range is 75 <= x <= 99
               {
                     return Insane[3];
               }
           }
       }
       #endregion
       #region gameplayUpdates
       public string Winner
       {
           get
           {
               int p = rand.Next() % 100; //get a random value between 1 and 100
                   to use in probability testing
               if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
               {
                     return Win[0];
               }
               else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
               {
                     return Win[1];
               }
               else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
               {
                     return Win[2];
               }
               else  //probability = 25 percent -- range is 75 <= x <= 99
               {
                     return Win[3];
               }
           }
       }
       public string Loser
       {
          get
          {
              int p = rand.Next() % 100; //get a random value between 1 and 100
                  to use in probability testing
              if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
              {
                    return Lose[0];
              }
              else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
              {
                    return Lose[1];
              }
              else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
              {
                    return Lose[2];
              }
              else  //probability = 25 percent -- range is 75 <= x <= 99
              {
                    return Lose[3];
              }
          }
      }
      public string Drawer //:)
      {
          get
          {
              int p = rand.Next() % 100; //get a random value between 1 and 100
                  to use in probability testing
              if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
              {
                    return Draw[0];
              }
              else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
              {
                   return Draw[1];
              }
              else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
              {
                   return Draw[2];
              }
              else  //probability = 25 percent -- range is 75 <= x <= 99
              {
                   return Draw[3];
              }
          }
       }
       public string NoMoreMoves
       {
           get
           {
               int p = rand.Next() % 100; //get a random value between 1 and 100
                   to use in probability testing
               if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
               {
                     return NoMore[0];
               }
               else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
               {
                     return NoMore[1];
               }
               else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
               {
                     return NoMore[2];
               }
               else   //probability = 25 percent -- range is 75 <= x <= 99
               {
                     return NoMore[3];
               }
           }
       }
       public string SpaceTaken
       {
           get
           {
               int p = rand.Next() % 100; //get a random value between 1 and 100
                   to use in probability testing
               if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
               {
                     return Taken[0];
               }
               else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
               {
                     return Taken[1];
               }
               else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
               {
                     return Taken[2];
               }
               else  //probability = 25 percent -- range is 75 <= x <= 99
               {
                     return Taken[3];
               }
            }
       }
       public string ThreeInARow
       {
          get
          {
               int p = rand.Next() % 100; //get a random value between 1 and 100
                   to use in probability testing
               if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
               {
                     return Three[0];
               }
               else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
               {
                     return Three[1];
               }
               else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
               {
                     return Three[2];
               }
               else  //probability = 25 percent -- range is 75 <= x <= 99
               {
                     return Three[3];
               }
            }
         }
         #endregion
        #region AITaunts
        public string AIWinTaunt
        {
           get
           {
                int p = rand.Next() % 100; //get a random value between 1 and 100
                    to use in probability testing
                if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
                {
                      return AIWin[0];
                }
                else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
                {
                      return AIWin[1];
                }
                else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
                {
                      return AIWin[2];
                }
                else //probability = 25 percent -- range is 75 <= x <= 99
                {
                      return AIWin[3];
                }
            }
        }
        public string PlayerWinTaunt
        {
           get
           {
                int p = rand.Next() % 100; //get a random value between 1 and 100
                    to use in probability testing
                if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
                {
                      return PlayerWin[0];
                }
                else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
                {
                      return PlayerWin[1];
                }
                else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
                {
                      return PlayerWin[2];
                }
                else   //probability = 25 percent -- range is 75 <= x <= 99
                {
                      return PlayerWin[3];
                }
            }
        }
        public string AIBlockTaunt
        {
            get
            {
                int p = rand.Next() % 100; //get a random value between 1 and 100
                    to use in probability testing
                if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
                {
                      return AIBlock[0];
                }
                else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
                {
                      return AIBlock[1];
                }
                else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
                {
                      return AIBlock[2];
                }
                else   //probability = 25 percent -- range is 75 <= x <= 99
                {
                      return AIBlock[3];
                }
            }
        }
        public string CenterSquareTaunt
        {
            get
            {
                int p = rand.Next() % 100; //get a random value between 1 and 100
                    to use in probability testing
                if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
                {
                      return CenterSquare[0];
                }
                else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
                {
                      return CenterSquare[1];
                }
                else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
                {
                      return CenterSquare[2];
                }
                else   //probability = 25 percent -- range is 75 <= x <= 99
                {
                      return CenterSquare[3];
                }
            }
        }
        public string CornerTaunt
        {
            get
            {
                int p = rand.Next() % 100; //get a random value between 1 and 100
                    to use in probability testing
                if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
                {
                      return Corner[0];
                }
                else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
                {
                      return Corner[1];
                }
                else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
                {
                      return Corner[2];
                }
                else  //probability = 25 percent -- range is 75 <= x <= 99
                {
                      return Corner[3];
                }
            }
        }
        public string SplitTaunt
        {
            get
            {
                int p = rand.Next() % 100; //get a random value between 1 and 100
                    to use in probability testing
                if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
                {
                      return Split[0];
                }
                else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
                {
                      return Split[1];
                }
                else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
                {
                      return Split[2];
                }
                else  //probability = 25 percent -- range is 75 <= x <= 99
                {
                      return Split[3];
                }
            }
        }
        public string SandBagTaunt
        {
            get
            {
                int p = rand.Next() % 100; //get a random value between 1 and 100
                    to use in probability testing
                if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
                {
                      return SandBag[0];
                }
                else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
                {
                      return SandBag[1];
                }
                else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
                {
                      return SandBag[2];
                }
                else  //probability = 25 percent -- range is 75 <= x <= 99
                {
                      return SandBag[3];
                }
            }
        }
        public string MisdirectTaunt
        {
            get
            {
                int p = rand.Next() % 100; //get a random value between 1 and 100
                    to use in probability testing
                if (p < 25) //probability = 25 percent -- range is 0 <= x <= 24
                {
                      return Misdirect[0];
                }
                else if (p < 50) //probability = 25 percent -- range is 25 <= x <= 49
                {
                      return Misdirect[1];
                }
                else if (p < 75) //probability = 25 percent -- range is 50 <= x <= 74
                {
                      return Misdirect[2];
                }
                else //probability = 25 percent -- range is 75 <= x <= 99
                {
                      return Misdirect[3];
                }
            }
       }
       #endregion
       #endregion
 }

Player Authoring

The last concept I mentioned in the first section is that of immersion. Yes, it’s another one of those terms, Valued Reader, so let’s get it defined right away. Immersion, for our purposes, is what Janet Murray speaks to in Hamlet on the Holodeck. Specifically, immersion is the active creation of belief that the fiction presented is real. This goes beyond playing along with the fiction by suspending your disbelief in the fantastical elements; it is actively finding ways to believe in those fantastical elements—to make the fiction become real for a time. This definition subsumes weaker definitions (e.g., the feeling of telepresence) and gets to the heart of what is important for games: the player’s experiences. This extended definition is required for games but not for other media (such as music or novels) because the medium itself is participatory!

Immersed players lose track of time and find themselves embedded in the game at a very fundamental level. These players make our jobs easier because they are actively creating their own reasons to invest themselves in the game and are helping to keep themselves hooked on the game. Immersing players is the Holy Grail of game designers and of character AI, in my opinion.

Additionally, immersion helps maintain the suspension of disbelief. Consider that immersed players as defined here are no longer paying attention to their reality—their external environment has given way to their internal environment as the focus of their attention. That is, an immersed player’s chair disappears, the keyboard is gone, and the sounds of the player’s household are silenced; the player’s mind pays attention exclusively to the game’s fictional environment. Because a player is no longer investing cognitive currency on external events and objects, he or she no longer has to suspend disbelief about the keyboard or controller or monitor. In his or her internal representation of the game, pressing a controller key has become “movement.”

What can character AI do to increase immersion? A lot of the answers to this question will sound familiar. They all revolve around increasing the believability of the NPCs in the game to avoid what I call “mental speed bumps”—those little details of a piece of fiction that cause your mind to withdraw from the imagined world and return to reality. They are those “No way!” moments in film and novels, where you are suddenly aware of your surroundings again. They are also those moments in video games that reinforce the reality of the situation: It’s a game, and these are stupid talking-head NPCs.

In our discussion of engagement, we started to address two particularly glaring mental speed bumps: repetitiveness and predictability. You worked on adding some probabilistic methods to the dialog system and added some unpredictability to the AI. You also added difficulty levels, allowing the player to “tune” the strength of the AI to his or her personal level of comfort. Both of these methods may slightly increase immersion, but they are still fairly predictable and static. Sure, you may be entertained by this version of the game, but can you really say you believe in the AI character in any way? We will return to believability in the last two chapters of this book; for now, let’s simplify believability into a character acting “just right enough” while not acting in too realistic a manner. Uh. What?

In Chapter 1, I introduced the principle of amplification through simplification in the context of whether advanced graphics are sufficient for increasing engagement and immersion. This concept is also meaningful (and very important) in our present discussion. According to this principle, the audience will feel more affinity for the character as the representation shifts from the realistic to the iconic. This applies to all aspects of the character’s representation, not just how the character looks and moves (detailed discussion of this follows in Chapter 5). Earlier, I claimed that the character must act “just right enough” while not acting in too realistic a manner. Really what I’m saying there is that the character’s actions, beliefs, thoughts, etc. must also be more iconic than realistic (which will be discussed in more detail in Chapter 6).

In that sense, our character for the Tic-Tac-Toe game does meet the primary requirements for generating immersion. Do I believe you can create an even better, utterly believable character to play Tic-Tac-Toe against? Absolutely. You’ve increased the fun over a traditional Tic-Tac-Toe implementation by increasing the level of interaction and engagement, but as an immersive vehicle, it leaves much to be desired—not because of the character, however. You could continue in this example, adding a visual representation of the character, adding an elaborate 3D environment, and performing some whizbang hackery to make that shine, but alas, Valued Reader, I believe our current Tic-Tac-Toe AI bot as a sample base is insufficient for our needs simply because, as a standalone game, Tic-Tac-Toe is not going to be a particularly immersive environment.

Yeah, So, Can All That Be Tied Together?

Before we move on to the interaction hierarchy, we should spend some time talking about what Salen and Zimmerman call the “Multivalent Model of Interactivity.” The topic is not quite parallel to our previous discussions of interaction, but it is not quite orthogonal (i.e. at right angles) to that discussion either. It is, however, a very interesting view of interactivity across the domain of video games and provides some interesting insights and design metrics for game design; it therefore deserves some exploration here. Briefly, the model is as follows:

  • Mode 1: Cognitive interactivity. Cognitive interactivity can be described as interpretive participation by the player. It requires complex, imaginative involvement by the player and usually requires psychological, emotional, and intellectual engagement. In this mode of interactivity, the player takes an active role in authoring his play experience. The implication is, of course, that a game aiming at cognitive interactivity most likely requires a complex design—i.e., a design based on complex, holistic systems that allow the player to make any meaningful decisions or solutions to game tasks.

  • Mode 2: Functional interactivity. In functional interactivity, the focus is more utilitarian—on the structural interactions with the material components of the system. We are interested in how it feels on a physical level to play the game. Functional interactivity is concerned with usability, interface design, etc.

  • Mode 3: Explicit interactivity. Explicit interactivity is described as participation within a set of designed choices and procedures. This kind of interactivity is produced by overt actions and requires what could be considered manipulative design practices to lead a player to adopt the style of play that the designer intends for the game, instead of the player’s own play style. As opposed to cognitive interactivity, this mode does not necessitate complex choice design, but can implement anticipatory design—a technique in which the designer tries to imagine what a player will do in every game situation and then implements hard-coded responses that require the player to solve problems in a certain manner.

  • Mode 4: Beyond-the-object interactivity. The last mode, beyond-the-object interactivity, occurs primarily outside the game, and is defined as participation in the culture created by the system. The outcome of this mode includes fan-based creations and player-based communal realities.

In my opinion, most games are designed with the target of explicit interactivity and employ anticipatory design. Complex designs, however, create more engagement and more immersion for players simply because complex designs require player authorship. A better target for game design is cognitive interactivity and complex choice design. I want to stress, however, that the choice isn’t to do cognitive or to do explicit interactivity. The choice here is how to design the explicit interactivity portions of the game to support the mode 1 (cognitive interactivity) goals.

Okay, Valued Reader, I know you are probably squirming in your chair and wondering why I’m bringing this stuff up. I mean, this isn’t character AI, right? Right? Wrong. In my opinion, this discussion heavily influences character AI simply because it is quite likely that cognitive interactivity relies heavily on advanced AI techniques, whereas explicit interactivity only requires clever engineering and gameplay programming. (This is not to say that games targeting explicit interactivity do not use advanced AI techniques, because many such games have other requirements that trump the more minimal requirements of mode 3 interactivity and necessitate more advanced AI work.)

You’ve probably already drawn the conclusion, however, that what we’ve really been talking about in this chapter is developing interaction, engagement, and immersion to a point that cognitive interactivity becomes possible and targeting character AI is an easy starting point. To further that discussion, let’s examine how interactivity, engagement, and immersion are linked. Simply put, full immersion depends on having an engaged player. And in turn, engagement depends on interactivity. It should be noted that these dependencies are one way. It is not true to say that creating interactivity ensures that engagement will follow, only that if there is no interactivity, there can be no engagement.

As discussed in the examples of engagement types earlier in the chapter, engagement relies on the player actively playing along with the fiction. At the heart of the matter is the fact that a player simply can’t play along with something that is not playing along with him or her. It takes two to tango, after all. From my point of view, engagement is a natural extension of good interaction. As the interaction becomes more and more fun, I find myself more and more willing to invest in the interaction. That said, there must still be more than the interaction to garner engagement. There must be some sort of hook—some invitation to take the interaction further.

In Hamlet on the Holodeck, Murray speaks about the “dangerous power of books to create a world that is more real than reality.” This is the heart of immersion, and this is why it requires strong engagement. Murray goes on to say that this surrender to a fictional world requires more than just the suspension of disbelief—it also requires a creative faculty. In other words, in addition to being the active creation of belief in the fiction, immersion requires engagement. As such, if suspension of disbelief (or engagement) is broken for the player, immersion will also be broken.

Where does this immersion come from, and how does it happen? Further, how is it that we can be immersed in books, television, radio shows, movies, video games, etc.? The answer to all these questions is the same: mental modeling. Humans maintain mental models of the world around us. We do this intuitively and use it to “rehearse” events prior to their actual occurrence. For example, Valued Reader, many times while writing this book, I’ve had pretend little conversations with you to test whether I’m making my points well. While that may sound a bit like schizophrenia, it isn’t. (I promise! Liar! Shhh! They’ll hear you!) I bet you’ve done the same thing when trying out an approach for convincing someone to do something you want them to do or in trying to justify why all the cake is missing.

We do exactly the same thing to participate in fiction. We just populate our mental model with the fictional world and characters rather than our perceptions of real people and events. Characters who don’t fit within the mental model created by the fictional world are the embodiment of mental speed bumps. They are not believable and disrupt the suspension of disbelief and therefore disrupt immersion.

Believable characters grab our attention and keep it. In other words, believable characters help increase the amount of immersion we feel in a fictional world. Further, human beings find that interacting with characters is more synergistic when they feel an affinity for the mental state of the character based on its observable behaviors. That said, humans still find characters with whom they do not identify to be engaging and interesting characters (villains, for example), and these characters also add to the immersive quality of the overarching fiction. How can that be?

Characters in fictional media play a dramatic role in the narrative. These roles are generalized classes of individuals that have ubiquity and for which prototypical behaviors and interactions are known. These dramatic roles help humans to construct a mental model of the character by conveying the prototypical behaviors, scripts, schema, etc., and believability stems from how the character’s actions compare to the user’s mental model of the character.

Specifically for the balance of this work, you will be exploring the notion that characters in video games can be that invitation to extend interaction as well as the impetus to begin to share the authoring load and actively create belief in the fictional framework being presented. It is likely, however, that you will have to go beyond what is considered the norm for video-game characters. If you can add dramatic characters to your games, immersion will likely be a result, but you can’t go about this willy-nilly (and that’s the reason behind this book). We will return to necessary conditions for immersive characters throughout the rest of the book.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.90.182