BACK TO THE TIC-TAC-TOE GAME


It's time to turn your attention to this book's final game project, the Tic-Tac-Toe game. You will create the Tic-Tac-Toe game by following the same five basic development steps that you have followed for all preceding game projects.

Designing the Game

You play the Tic-Tac-Toe game on a single window. The game is made up of one form and the 18 controls listed in Table 11.1.

Table 11.1: From Controls for the Tic-Tac-Toe Game

Control Type

Control Name

Description

Panel 1

pnlLeft

Represents the left vertical bar on the game board

Panel2

pnlRight

Represents the right vertical bar on the game board

Panel3

pnlTop

Represents the top horizontal bar on the game board

Panel4

pnlBottom

Represents the bottom horizontal bar on the game board

PictureBox 1

pbxAl

Holds the left square on the first row of the game board

PictureBox2

pbxA2

Holds the middle square on the first row of the game board

PictureBox 3

pbxA3

Holds the right square on the first row of the game board

PictureBox4

pbxB1

Holds the left square on the second row of the game board

PictureBox5

pbxB2

Holds the middle square on the second row of the game board

PictureBox6

pbxb3

Holds the right square on the second row of the game board

PictureBox7

pbxC1

Holds the left square on the third row of the game board

PictureBox8

pbxC2

Holds the middle square on the third row of the game board

PictureBox9

pbxC3

Holds the right square on the third row of the game board

Button 1

btnPlay

Initiates gameplay

Button2

btnExit

Terminates the game

Label 1

lblOutput

Identifies the TextBox control that displays the status messages

TextBox 1

txtOutput

Displays status messages

ImageList 1

imlSquares

Stores an indexed collection of graphics that represent player moves

Step 1 : Creating a New Visual C++ Project

The first step in creating the Tic-Tac-Toe game is to open Visual C++ and create a new project, as outlined here:

  1. If you have not already done so, start Visual C++ 2005 Express and then click on File, New Project. The New Project dialog box appears.

  2. Select the Windows Application template.

  3. Type Tic-Tac-Toe as the name of your new application in the Name field at the bottom of the New Project window.

  4. Click OK to close the New Project dialog box.

Visual C++ creates a new project for you and displays a form, which you can use to design your new game's user interface.

Step 2: Creating the User Interface

The first step in laying out the user interface is to add controls to the form and to move and resize them to the appropriate locations. As you go through each step, make sure that you reference Figure 11.11 so that you know where each control needs to be placed and what size it needs to be.

  1. Begin by setting the Size property of the form to 605, 591.

  2. Next, create the grid lines that organize the game board by adding four Panel controls and resizing them, as shown in Figure 11.11.

  3. Add PictureBox controls inside each of the game board's nine cells and resize them until they take up almost all the available space.

  4. Add two Button controls to the bottom-left corner.

  5. Add a TextBox control to the bottom-right side of the game board, set its Multiline property to true, and resize it.

  6. Add a Label control and place it just over the upper-left corner of the TextBox control.

  7. Finally, add an ImageList control and set its size to 100, 100.

image from book
Figure 11.11: Completing the interface design for the Tic-Tac-Toe game.

At this point, the overall layout of the Tic-Tac-Toe game's user interface is complete. You can start making changes to the form and control properties.

Step 3: Customizing Form and Control Properties

Let's begin by making the required changes to properties belonging to the Form object, as listed in Table 11.2.

Table 11.2: Property Changes for Form1

Property

Value

Name

frmMain

ControlBox

false

Cursor

Hand

FormBorderStyle

Fixed3D

StartPosition

CenterScreen

Text

Tic-Tac-Toe

Make the property changes shown in Table 11.3 to the Panel controls.

Table 11.3: Property Changes for Panel Controls

Control

Property

Value

Panel1

Name

pnlLeft

 

BackColor

Black

Panel2

Name

pnlRight

 

BackColor

Black

Panel3

Name

pnlTop

 

BackColor

Black

Panel4

Name

pnlBottom

 

BackColor

Black

Make the property changes shown in Table 11.4 to the PictureBox controls.

Table 11.4: Property Changes for PictureBox Controls

Control

Property

Value

PictureBox1

Name

pbxA1

 

Enabled

false

 

Size

100, 100

 

SizeMode

StretchImage

PictureBox2

Name

pbxA2

 

Enabled

false

 

Size

100, 100

 

SizeMode

StretchImage

PictureBox3

Name

pbxA2

 

Enabled

false

 

Size

100, 100

 

SizeMode

StretchImage

PictureBox4

Name

pbxB1

 

Enabled

false

 

Size

100, 100

 

SizeMode

StretchImage

PictureBox5

Name

pbxB2

 

Enabled

false

 

Size

100, 100

 

SizeMode

StretchImage

PictureBox6

Name

pbxB3

 

Enabled

false

 

Size

100, 100

 

SizeMode

StretchImage

PictureBox7

Name

pbxC1

 

Enabled

false

 

Size

100, 100

 

SizeMode

StretchImage

PictureBox8

Name

pbxC2

 

Enabled

false

 

Size

100, 100

 

SizeMode

StretchImage

PictureBox9

Name

pbxC3

 

Enabled

false

 

Size

100, 100

 

SizeMode

StretchImage

Make the property changes shown in Table 11.5 to the Button controls.

Table 11.5: Property Changes for Button Controls

Control

Property

Value

Button 1

Name

btnPlay

 

Text

Play

Button2

Name

btnExit

 

Text

Exit

Make the property changes shown in Table 11.6 to the Label control.

Table 11.6: Property Changes for Label Control

Control

Property

Value

Label1

Name

lbl0utput

 

Font.Bold

true

 

Text

Status

Finally, make the property changes shown in Table 11.7 to the TextBox control.

Table 11.7: Property Changes for TextBox Control

Control

Property

Value

TextBox1

Name

txtOutput

 

Font.Bold

true

 

Readonly

true

 

TabStop

false

Step 4: Adding a Little Programming Logic

Let's add the class and variable definitions needed throughout the Tic-Tac-Toe game, as shown next. These variables help support the game's logic across many of its functions:

 private:   /// <summary>   /// Required designer variable.   /// </summary>   //Values for possible square states   enum class SquareStates : Int16 {     SQUAREOPEN, SQUAREX, SQUAREO   };   //Values to help keep track of whose   //  turn it is   enum class Player : Int16 {     PLAYERX, PLAYERO, PLAYERNONE,   };   //Game board   array <SquareStates>^ sgsGameBoardArray;   Player plyActivePlayer; 

The first is an enumerated type definition, SquareStates, which helps create meaningful labels for the possible moves within each square. This class defines three states that indicate that a board position is open, filled with an X, or filled with an O. This helps prevent the errors that might occur if you have to refer to positions on the board with numbers or some other value.

This definition is followed by another enumerated type definition that allows the game to track the current player. The game also uses a handle to an array of type SquareStates, which holds the state of the game board.

The form's Load event function, shown next, is responsible for preparing the game for initial play. This is accomplished by calling three custom functions:

 private: System::Void Form1_Load(System::Object^  sender,\ System::EventArgs^  e) {      //Get memory for the game board      sqsGameBoardArray = gcnew array<SquareStates>(9);      //Set no player on startup      plyActivePlayer = Player::PLAYERNONE;      //Establish the game board      SetGameDefaultS();      ClearGameBoard();      EnableGameBoard( false );    } 

First, the game acquires memory for nine positions, each of which represents a space on the board. Then the game sets the starting player and establishes the game board. Create the next listing by typing the following program code:

 private: System::Vo1d SetGameDefaults(System::Void) {     //Set the game's starting message and player     txtOutput->Text = "Click Play to begin.";     SwapPlayerTurn();   } 

This function, SetGameDefaults(), sets the starting game text and triggers the switching of player positions. This function is called after a player wins to ensure that the loser of that game starts the next game.

The next function clears all squares of the game board. Add it to your project by typing the code shown here:

 private: System::Void ClearGameBoard(System::Void) {     //Set all squares in game board to open     for( Int32 intCounter = 0; intCounter < \     sqsGameBoardArray->Length; intCounter++ )     sqsGameBoardArray[intCounter] = SquareStates::SQUAREOPEN;     //Blank all images on the game board     pbxA1->Image = imlSquares->Images[0];     pbxA2->Image = imlSquares->Images[0];     pbxA3->Image = imlSquares->Images[0];     pbxB1->Image = imlSquares->Images[0];     pbxB2->Image = imlSquares->Images[0];     pbxB3->Image = imlSquares->Images[0];     pbxC1->Image = imlSquares->Images[0];     pbxC2->Image = imlSquares->Images[0];     pbxC3->Image = imlSquares->Images[0];   } 

This function is responsible for clearing the data that represents the game board. It loops through an array to do so, using sqsGameBoardArray->Length to ensure that the intCounter does not loop too far. The function also sets each square to a blank image. Unfortunately, this cannot be done as conveniently in an array because each control name is unique.

Gameplay cannot begin until the first player clicks on the Button control labeled Play. This causes the btnPlay function, shown next, to execute. This function executes two custom functions. The first function, ClearGameBoard(), you have already examined. The second function, EnableGameBoard(), controls whether you can click the squares on the board. To generate this next button event handler, double-click the Play button in Design view and then add the code shown here:

 private: System::Void btnPlay_Click(System::Object^  sender, \ System::EventArgs^  e) {      //Start with a fresh board     ClearGameBoard();      //Set squares as clickable      EnableGameBoard( true );      //Disable Play button until done      btnPlay->Enabled = false;   } 

You must manually add the EnableGameBoard() function, shown next. It causes all squares on the game board to become enabled or disabled:

 private: System::Void EnableGameBoard(System::Boolean \ blnEnableBoard ) {       //Enable or disable all picture       //  squares based on blnEnableBoard       if( blnEnableBoard )       {         pbxA1->Enabled = true;         pbxA2->Enabled = true;         pbxA3->Enabled = true;         pbxB1->Enabled = true;         pbxB2->Enabled = true;         pbxB3->Enabled = true;         pbxC1->Enabled = true;         pbxC2->Enabled = true;         pbxC3->Enabled = true;       }       else       {         pbxA1->Enabled = false;         pbxA2->Enabled = false;         pbxA3->Enabled = false;         pbxB1->Enabled = false;         pbxB2->Enabled = false;         pbxB3->Enabled = false;         pbxC1->EnabLed = false;         pbxC2->EnabLed = false;         pbxC3->Enabled = false;       }   } 

Players can end the game at any time by clicking on the Button control labeled Exit. When a player does this, the btnExit function, shown next, is executed:

 private: System::Void btnExit_Click(System::Object^  sender,\ System::EventArgs^  e) {      //End the application      this->C1ose();   } 

Each cell on the game board is made up of a PictureBox control. The following statements make up the click event function for the game's first PictureBox control:

 private: System::Void pbxA1_Click(System::Object^  sender,\ System::EventArgs^  e) {      //Claim square Al according to which      //  player is active      if( plyActivePlayer == Player::PLAYERX )      {        pbxA1-image = imlSquares->Images[1];        txtOutput->Text = "X claims square A1!";        sqsGameBoardArray[0] = SquareStates::SQUAREX;      }      if( plyActivePlayer == Player::PLAYERO )      {        pbxA1->Image = imlSquares->Images[2];        txtOutput->Text = "0 claims square A1!";        sqsGameBoardArray[0] = SquareStates::SQUAREO;      }      pbxA1->Enabled = false;      CheckForWinner();   } 

This code claims the square for the active player, which is stored in plyActivePlayer. Depending on which player it is, the appropriate image is selected, the text displayed in the game's output text box is changed, and the board is set with either a SQUAREX or SQUAREO in the appropriate position. The game then closes off access to the PictureBox control by disabling it. Then it calls the CheckForWinner function to determine if this move was the final winning move.

The code for the other eight PictureBoxes is nearly identical. Double-click each PictureBox in Design view to generate its event handler functions. Because they are so similar, I have grouped them in the following listing:

 private: System::Void pbxA2_ClickCSystem::Object^  sender,\ System::EventArgs^  e) {       //Claim square A2 according to which       //  player is active       if( plyActivePlayer == Player::PLAYERX )       {         pbxA2->Image = imlSquares->Images[1];         txtOutput->Text = "X claims square A2!";         sqsGameBoardArray[1] = SquareStates::SQUAREX;       }       if( plyActivePlayer == Player::PLAYERO )       {         pbxA2->Image = imlSquares->Images[2];         txtOutput->Text = "0 claims square A2!";         sqsGameBoardArray[l] = SquareStates::SQUAREO;       }       pbxA2->Enabled = false;       CheckForWinner();   } private: System::Void pbxA3_Click(System::Object^  sender, \ System::EventArgs^  e) {       //Claim square A3 according to which       //  player is active       if( plyActivePlayer == Player::PLAYERX )       {         pbxA3->Image = imlSquares->Images[1];         txtOutput->Text = "X claims square A3!";         sqsGameBoardArray[2] = SquareStates::SQUAREX;       }       if( plyActivePlayer == Player::PLAYERO )       {         pbxA3->Image = imlSquares->Images[2];         txtOutput->Text = "0 claims square A3!";         sqs6ameBoardArray[2] = SquareStates::SQUAREO;       }       pbxA3->Enabled = false;       CheckForWinner();   } private: System::Void pbxB1_Click(System::Object^  sender,\ System::EventArgs^  e) {       //Claim square B1 according to which       //  player is active       if( plyActivePlayer == Player::PLAYERX )       {         pbxB1-Image = imlSquares->Images[1];         txtOutput->Text = "X claims square B1!";         sqsGameBoardArray[3] = SquareStates::SQUAREX;       }       if( plyActivePlayer == Player::PLAYER0 )       {         pbxB1->Image = imlSquares->Images[2];         txtOutput->Text = "0 claims square B1!";         sqsGameBoardArray[3] = SquareStates::SQUAREO;       }       pbxB1->Enabled = false;       CheckForWinner();   } private: System::Void pbxB2_Click(System::Object^  sender,\ System::EventArgs^  e) {       //Claim square B2 according to which       //  player is active       if( plyActivePlayer == Player::PLAYERX )       {         pbxB2->Image = imlSquares->Images[1];         txtOutput->Text = "X claims square B2!";         sqsGameBoardArray[4] = SquareStates::SQUAREX;       }       if( plyActivePlayer == Player::PLAYERO )       {         pbxB2->Image = imlSquares->Images[2];         txtOutput->Text = "0 claims square B2!";         sqsGameBoardArray[4] = SquareStates::SQUAREO;       }       pbxB2->Enabled = false;       CheckForWinner();   } private: System::Void pbxB3_Click(System::Object^  sender,\ System::EventArgs^  e) {       //Claim square B3 according to which       //  player is active       if( plyActivePlayer == Player::PLAYERX )       {         pbxB3->Image = imlSquares->Images[1];         txtOutput->Text = "X claims square B3!";         sqsGameBoardArray[5] = SquareStates::SQUAREX;       }       if( plyActivePlayer == Player::PLAYERO )       {         pbxB3->Image = imlSquares->Images[2];         txtOutput->Text = "0 claims square B3!";         sqsGameBoardArray[5] = SquareStates::SQUAREO;       }       pbxB3->Enabled = false;       CheckForWinner();   } private: System::Void pbxC1_Click(System::Object^  sender,\ System::EventArgs^  e) {       //Claim square C1 according to which       //  player is active       if( plyActivePlayer == Player::PLAYERX )       {         pbxCl->Image = imlSquares->Images[1];         txtOutput->Text = "X claims square C1!";         sqsGameBoardArray[6] = SquareStates::SQUAREX;       }       if( plyActivePlayer == Player::PLAYERO )       {         pbxC1->Image = imlSquares->Images[2];         txtOutput->Text = "0 claims square C1!";         sqsGameBoardArray[6] = SquareStates::SQUAREO;       }       pbxC1->Enabled = false;       CheckForWinner();   } private: System::Void pbxC2_Click(System::Object^   sender,\ System::EventArgs^  e) {       //Claim square C2 according to which       //  player is active       if( plyActivePlayer == Player::PLAYERX )       {         pbxC2->Image = imlSquares->Images[1];         txtOutput->Text = "X claims square C2!";         sqsGameBoardArray[7] = SquareStates::SQUAREX;       }       if( plyActivePlayer == Player::PLAYERO )       {         pbxC2->Image = imlSquares->Images[2];         txtOutput->Text = "0 claims square C2!";         sqsGameBoardArray[7] = SquareStates::SQUAREO;       }       pbxC2->Enabled = false;       CheckForWinner();   } private: System::Void pbxC3_Click(System::Object^  sender,\ System::EventArgs^  e) {       //Claim square C3 according to which       //  player is active       if( plyActivePlayer == Player::PLAYERX )       {         pbxC3->Image = imlSquares->Images[1];         txtOutput->Text = "X claims square C3!";         sqsGameBoardArray[8] = SquareStates::SQUAREX;       }       if( plyActivePlayer == Player::PLAYERO )       {         pbxC3->Image = imlSquares->Images[2];         txtOutput->Text = "0 claims square C3!";         sqsGameBoardArray[8] = SquareStates::SQUAREO;       }       pbxC3->Enabled = false;       CheckForWinner();   } 

Each time a move is made, the game must determine if it was the winning move by analyzing every square on the board. Fortunately, it does this instantly. To add this behavior, create the custom function illustrated here:

 private: System::Void CheckForWinner(System::Void) {      //Set a variable to track the winning player      Player plyWinningPlayer = Player::PLAYERNONE;      //Establish a generic counter for looping      //  through game board      Int32 intCounter = 0;      //Check rows for X or 0 win      for( intCounter = 0; intCounter <           sqsGameBoardArray->Length;  intCounter += 3 )      {        //Check if Xs win        if( sqsGameBoardArray[intCounter] ==          SquareStates::SQUAREX )          if( sqsGameBoardArray[intCounter + 1] ==              SquareStates::SQUAREX )            if( sqsGameBoardArray[intCounter + 2] ==                SquareStates::SQUAREX )              plyWinningPlayer = Player::PLAYERX;        //Check if Os win        if( sqsGameBoardArray[intCounter] ==          SquareStates::SQUAREO )          if( sqsGameBoardArray[intCounter + 1] ==              SquareStates::SQUAREO )            if( sqsGameBoardArray[intCounter + 2] ==                SquareStates::SQUAREO )              plyWinningPlayer = Player::PLAYERO; )      }      //Check columns for X or 0 win      for( intCounter - 0; intCounter <           sqsGameBoardArray->Length / 3;  intCounter += 1 )      {        //Check if Xs win        if( sqsGameBoardArray[intCounter] ==            SquareStates::SQUAREX )         if( sqsGameBoardArray[intCounter + 3] ==             SquareStates::SQUAREX )           if( sqsGameBoardArray[intCounter + 6] ==               SquareStates::SQUAREX )             plyWinningPlayer = Player::PLAYERX:        //Check if Os win        if( sqsGameBoardArray[intCounter] ==            SquareStates::SQUAREO )         if( sqsGameBoardArray[intCounter + 3] ==             SquareStates::SQUAREO )           if( sqsGameBoardArray[intCounter + 6] ==               SquareStates::SQUAREO )             plyWinningPlayer = Player::PLAYERO;      }      //Check diagonals if X or 0 has claimed the center      if( sqsGameBoardArray[4] != SquareStates::SQUAREOPEN )      {        //Check if X or 0 wins upper left to        //  lower right        if( sqsGameBoardArray[0] == SquareStates::SQUAREX &&          sqsGameBoardArray[4] == SquareStates::SQUAREX &&          sqsGameBoardArray[8] == SquareStates::SQUAREX )          plyWinningPlayer = Player::PLAYERX:        if( sqsGameBoardArray[0] == SquareStates::SQUAREO &&          sqsGameBoardArray[4] == SquareStates::SQUAREO &&          sqsGameBoardArray[8] == SquareStates::SQUAREO )          plyWinningPlayer = Player::PLAYERO;        //Check if X or 0 wins lower left to        //  upper right        if( sqsGameBoardArray[6] == SquareStates::SQUAREX &&          sqsGameBoardArray[4] == SquareStates::SQUAREX &&          sqsGameBoardArray[2] == SquareStates::SQUAREX )          plyWinningPlayer = Player::PLAYERX;        if( sqsGameBoardArray[61 == SquareStates::SQUAREO &&          sqsGameBoardArray[4] == SquareStates::SQUARED &&          sqsGameBoardArray[2] == SquareStates::SQUAREo )          plyWinningPlayer = Player::PLAYERO;      }      //If neither player has won, check to see if board is      //  filled      if( plyWinningPlayer == Player::PLAYERNONE )      {       for( intCounter = 0; intCounter <            sqsGameBoardArray->Length; intCounter += 1 )         if( sqsGameBoardArray[intCounter] ==             SquareStates::SQUAREOPEN )         {            //Found at least one open square, so            //  the game continues            SwapPlayerTurn();            return;          }      }      //No open squares; either someone won or it's a tie      EndGame( plyWinningPlayer );   } 

The first step that the function takes is to create an instance of a player named plyWinningPlayer. This object keeps track of who the winner of the game is as each square is examined. The function also creates a counter to handle looping through the board several times.

The first search is made by row. A for loop is designed to skip across each row. Within its code bock, a test is made first for X and then for O each iteration. The test involves looking not only at whether the current square specified by intCounter has an X or O value, but also whether its two neighbors share the same value. Because intCounter is designed to skip by threes, this ensures that each row is read as a block of three positions.

If neither X nor O has claimed any of the three rows, the search switches to check each column. To understand how this works, think of the squares of a Tic-Tac-Toe board as being numbered, starting with a zero in the upper-left corner and ending with an eight in the lower right. If you look at any square in the top row, you will see that the next row is only three squares (counting from left to right) from the square in the same column below it. The same square in the bottom row is always only six squares from the square in the top row. This function exploits that fact by searching columns in groups of three.

If neither side has claimed the columns, the only possible positions are the two diagonal reaches, from upper left to lower right and upper right to lower left. This is possible only if the center square is claimed, so the function first checks for this fact and then checks the appropriate game squares. If a winner is still not found, then the game is a tie, or there is still a move left to make. The remainder of the logic accounts for these two options. A for loop searches for an open square. If it finds one, it calls for the players to be swapped and continues the game. But if no open square is found, a function is called that ends the game.

Finally, the game calls the EndGame function. This is a custom function whose code is shown next:

 Private Sub Determine_Game_Status(ByVal strGameOver As String) private: System::Void EndGame(Player plyWinner) {       //Declare winner       if( plyWinner == Player::PLAYERX )         MessageBox::Show( "Player X wins!" );       if( plyWinner == Player::PLAYERO )         MessageBox::Show( "Player 0 wins!" );       if( plyWinner == Player::PLAYERNONE )         MessageBox::Show( "Game is a Tie!" );       //Reset game and allow replay       SetGameDefaults();       EnableGameBoard( false );       ClearGameBoard();       btnPlay->Enabled = true;       txtOutput->Text = "Click Play to start again.";   } 

The function uses if statements to match the winner with the appropriate Player value. Notice again that rather than having to compare plyWinner to a number whose meaning you might forget, the enumerated class Player provides a clear way of making comparisons and assignments.

The Switch_Players function, shown next, is straightforward. When called, it changes the value of plyActivePlayer, which determines who the active player is. It also updates the output of text in the game's output box to alert the players to this fact:

 private: System::Void SwapPlayerTurn(System::Void) {       //Switch current player       if( plyActivePlayer == Player::PLAYERX )       {        plyActivePlayer = Player::PLAYERO;        txtOutput->Text = String::Concat( txtOutput->Text,        "  It is Player 0's turn." );       }       else       {        plyActivePlayer = Player::PLAYERX;        txtOutput->Text = String::Concat( txtOutput->Text,        "  It is Player X's turn." );       }   } 

Step 5: Testing the Execution of the Tic-Tac-Toe Game

The Tic-Tac-Toe game is ready to run. Go ahead and test it by pressing F5. Does everything work like it is supposed to? If you run into problems, try using the troubleshooting tips presented in this chapter to track down errors. After things are in order, try it with a few friends.




Microsoft Visual C++ 2005 Express Edition Programming for the Absolute Beginner 2006
Microsoft Visual C++ 2005 Express Edition Programming for the Absolute Beginner 2006
ISBN: 735615381
EAN: N/A
Year: 2005
Pages: 131

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net