65.9K
CodeProject is changing. Read more.
Home

Yet Another Tic Tac Toe

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.53/5 (15 votes)

Nov 3, 2003

4 min read

viewsIcon

149813

downloadIcon

2354

Yet Another Tic Tac Toe program. This example breaks the tic tac toe game into three parts. Model data, UI, and AI.

Sample Image - yetanothertictactoe.jpg

Introduction

I was watching my girl friend play web games the other day, including playing tic-tac-toe, and decided to give it a try in VB.NET. With-in about 30 minutes I had a game up and running in which I could play against my step-daughter. My implementation separated the game logic into three areas: model, UI, and an AI class. The associated source files for these areas are TicTacToeGame.vb, frmTicTacToe.vb, and TicTacToeAI.vb. General programming areas used in this project include Enums, Events, and Overloading a property.

After implementing my version of tic-tac-toe, I researched how others have implemented the game. Grant Richard has written a good article in the C# area here.

The Tic-Tac-Toe Model

A tic-tac-toe board can have three entries in a square: Nothing, X, and O. These are represented by an Enum called GridEntry. The Enums are appropriately called NoEntry, PlayerX, and PlayerO. A multi-dimensional grid of 3x3 squares of the type GridEntry represents the playing board. Also note that the enum is inside the class. This allows the programmer to code CLASSNAME.ENUM from other classes.

0,0 -- 0,1 -- 0,2
1,0 -- 1,1 -- 1,2
2,0 -- 2,1 -- 2,2

Public Class TicTacToe
    Public Enum GridEntry 
        NoEntry = 0
        PlayerX = 1
        PlayerO = 2
    End Enum
 
    Private m_iGrid(2,2) as GridEntry
End Class

The only property of the tic-tac-toe model class is Square. This property gets/sets a type of GridEntry. A user can only set a Square to either X or O. If they try to overwrite a square once it has been written to, the set property ignores the set operation. The set property also checks to see if there is a winner condition or draw condition and raises the appropriate event.

Public Event TicTacToeWinOccured(ByVal aWinner as GridEntry) 'Win Condition
Public Event Cat() ‘Draw Condition

Public Property Square(iRow as Integer, iCol as Integer) as GridEntry
    Get
        Return m_iGrid(iRow, iCol)
    End Get
    Set(ByVal Value as GridEntry)
        If m_iGrid(iRow, iCol) = Grid.NoEntry And _
           m_bHasWinOccured = False Then
            m_iGrid(iRow, iCol) = Value
                    m_iWinner = CheckForWin()
            If m_iWinner = GridEntry.NoEntry Then
                If CheckForDraw() = True
                    RaiseEvent Cat()
                End If
            Else
                m_bHasWinOccured = True
                RaiseEvent TicTacToeWinOccured(m_iWinner)
            End If
          End If
    End Set
End Property

While writing the AI class, I decided to encapsulate iRow and iCol. This class was called SquareCoordinate which would allow me to pass one parameter to a function instead of iRow and iCol. So the property Square was overloaded to take a SquareCoordinate variable.

Public Property Square(aSC as SquareCoordinate) as GridEntry
    Get
        Return m_iGrid(aSC.Row, aSC.Column)
    End Get
    Set(Value as GridEntry)
          Me.Square(aSC.Row, aSC.Column) = Value
     End Set
End Property

The User Interface

The UI portion of this project has 9 labels in a group box which when clicked on call lblGrid_Click. This is setup using an AddHandler command and using the AddressOf operator to point to lblGrid_Click. Each control is named lblGridXX where XX is the grid coordinates for the square as noted above. For example 00 is the first square. The subroutine DrawGrid updates the labels with the data in the tic-tac-toe model.

AddHandler lblGrid00.Click, AddressOf lblGrid_Click.

Public Sub lblGrid_Click(sender as Object, args as EventArgs)
    Try
        Dim aLabelControl As Label = CType(sender, Label)
        Dim sName as String = aLabelControl.Name
        Dim iRow as Integer = sName.SubString(sName.Length-2,1)
        Dim iCol as Integer = sName.SubString(sName.Length-1, 1)
          If m_TicTacToeGame.Square(iRow, iCol) = TicTacToe.GridEntry.NoEntry Then
               m_TicTacToeGame.Square(iRow, iCol) = m_iCurrentPlayer
               ToggleCurrentPlayer()
               DrawGrid()
          End If
    Catch ex as Exception
        StopError Handling to be completed
    End Try
End Sub

Private Sub DrawGrid()
    Dim iRow as Integer
    Dim iCol as Integer
    Dim aLabelControl as Label

    For iRow = 0 to 2
        For iCol = 0 to 2
            Try
                    aLabelControl = GetLabelControl(iRow, iCol)
                   Select Case m_TicTacToeGame.Square(iRow, iCol)
                 Case TicTacToe.GridEntry.NoEntry
                      aLabelControl.Text = “”
                 Case TicTacToe.GridEntry.PlayerX
                      aLabelControl.Text = “X”
                 Case TicTacToe.GridEntry.PlayerO
                      aLabelControl.Text = “O”
                End Select
        Catch ex as Exception
   
        End Try
        Next
    Next
End Sub

Win/Draw Condition Logic

The win condition logic in the tictactoe class checks for a win condition by checking the squares horizontally, vertically, and diagonally. If the same player is in all three squares in one direction, that player is returned from the win logic code; otherwise NoEntry is returned.

Private Function CheckForHorizontalWin() as GridEntry
     Dim iRow as Integer
     Dim iFirstSquarePlayer as GridEntry
     For iRow = 0 to 2
         iFirstSquarePlayer = m_iGrid(iRow,0)
        If iFirstSquarePlayer = m_iGrid(iRow,1) and _
           iFirstSquarePlayer = m_iGrid(iRow,2) Then
            Return iFirstSquarePlayer
        End If
    Next
    Return GridEntry.NoEntry
End Function

The draw logic is pretty simple. We loop through all squares, and if we find a NoEntry, return false; otherwise if they are all full, return true for a draw.

Private Function CheckForDraw() as Boolean
    Dim iRow as Integer, iCol as Integer
    For iRow = 0 to 2
        For iCol = 0 to 2
            If m_iGrid(iRow,iCol) = GridEntry.NoEntry Then Return False
        Next
    Next
    Return True
End Function

AI Logic

The AI Logic is contained in the TicTacToeAI class. There are four general rules that the AI class follows:

  • Win if possible
  • Block opponent from winning
  • Determine a Best Bet move to setup a future win
  • Make any valid move

A MoveEntry class has been created to contain possible moves. This is a private class only accessiable to the TicTacToeAI class. This class contains two variables: a SquareCoordinate, and a MoveType of Winning, Blocking, or Normal.

Private Enum MoveType
    Normal = 0
    Blocking = 1
    Winning = 2
End Enum

The TicTacToeAI class has only one public method GetAIMove(ByVal aTTT as TicTacToe) and returns a SquareCoordinate. The workhorse of the AI is the private function DetermineMoveEntry(aSC as SquareCoordinate) which classifies a SquareCoordinate to either a winning move, a blocking move, or a normal move. It first checks horizontal, vertical, and finally diagonal. To check diagonal, another enum is used along with a function to determine which tic tac toe square is being examined.

Private Enum SquareType
    UpperLeft = 1
    UpperRight = 2
    Center = 3
    BottomLeft = 4
    BottomRight = 5
    Other = 6
End Enum

Private Function DetermineMoveEntry(ByVal aSC as SquareCoordinate) _
                                    as MoveEntry
    Dim iRow as Integer = aSC.Row
    Dim iCol as Integer = aSC.Column
    Dim iGE1 as TicTacToe.GridEntry
    Dim iGE2 as TicTacToe.GridEntry
 
    ' ... Code Omitted for Horizontal and Vertical

    Select Case GetSquareType(aSC)
        Case SquareType.UpperLeft
            iGE1 = m_CurrentBoard.Square(1,1)
            iGE2 = m_CurrentBoard.Square(2,2)

            If CheckPossibleWin(iGE1, iGE2, m_iAIPlayer = True Then
                Return New MoveEntry(aSC, MoveType.Winning)
            End If
        
            If CheckPossibleWin(iGE1, iGE2, m_iHumanPlayer) = True Then
                Return New MoveEntry(aSC, MoveType.Blocking)
            End If
 
        ' ... Code omitted for other cases
    End Select

    Return New MoveEntry(aSC, MoveType.Normal)
End Function

Finally GetAIMove stores each MoveEntry into an ArrayList, and then checks the ArrayList for winning/blocking moves. If there are no winning moves or blocking moves, it tries to determine if there are any best moves by calling DetermineBestNormalMove(aNormalMoveList as ArrayList). Otherwise it picks a normal move at random.

I had fun writting this tic-tac-toe program. I may in the future examine other implementations of the base model and the logic for determining win, draw, and AI moves. For example, Grant Richard uses a single array of 9 squares numbering each square 1 to 9 with his win conditions contained in an array. His article is here.