Subscribe to The Madman Scribblings        RSS Feed

The Kernal Of Tic-Tac-Toe (Part 2)

Icon Leave Comment
The Kernal Of Tic-Tac-Toe (Part 2)

I added a new game state ReadyToPlay because I had a issue with the kernel outputting the blank board.
Public Enum GameStates
End Enum

I then updated the TicTacToe class to utilize the new game state.
Then corrected a few bugs

and re-factored the code.

Public Class TicTacToe
  Dim _TicTacToeBoard(8) As Markers, _CurrentPlayer As Markers = Markers.Player_1
  Dim _GameState As GameStates = GameStates.ReadyToPlay
  Dim _FatalInvalidMoves As Boolean
  Public Sub New(Optional FatalInvalidMoves As Boolean = False)
    _FatalInvalidMoves = FatalInvalidMoves
  End Sub
  Public ReadOnly Property CurrentPlayer As Markers
      Return _CurrentPlayer
    End Get
  End Property
  Public Property GameState As GameStates
      Return _GameState
    End Get
    Private Set(value As GameStates)
      If value = _GameState Then Exit Property
      _GameState = value
      RaiseEvent GameStateChanged(Me, New GameStateChangedArgs())
    End Set
  End Property
  Private Event GameStateChanged(ticTacToe As TicTacToe, e As GameStateChangedArgs)
  Public Event BoardChanged(sender As TicTacToe, e As BoardChangedEventArgs)
  Public Event PlayerWin(sender As TicTacToe, e As PlayerWinEventArgs)
  Public Event GameDrawn(sender As TicTacToe, e As EventArgs)
  Private Sub TicTacToe_GameStateChanged(ticTacToe As TicTacToe, e As GameStateChangedArgs) Handles Me.GameStateChanged
    If CType(ticTacToe, TicTacToe).GameState = GameStates.Drawn Then RaiseEvent GameDrawn(Me, EventArgs.Empty)
  End Sub
  Private Sub TicTacToe_PlayerWin(sender As Object, e As PlayerWinEventArgs) Handles Me.PlayerWin
    If e.Player = Markers.Player_1 Then Me.GameState = GameStates.Win_Player1
    If e.Player = Markers.Player_2 Then Me.GameState = GameStates.Win_Player2
  End Sub
  Default Public ReadOnly Property Square(ByVal i As Integer) As Markers
      If (0 <= i) AndAlso (i < 9) Then Return _TicTacToeBoard(i)
      Return Markers.Empty
    End Get
  End Property
  Public Sub Start()
    If _GameState <> GameStates.ReadyToPlay Then Return
    RaiseEvent BoardChanged(Me, New BoardChangedEventArgs(_TicTacToeBoard.AsEnumerable))
    GameState = GameStates.Playing
  End Sub
  Public Function PlayMove(PlayedBy As Markers, PlayingSquare As Integer) As Boolean
    If (_GameState <> GameStates.Playing) OrElse (_CurrentPlayer <> PlayedBy) Then Me.GameState = GameStates.Invalid_State : Return False
    If Not ((0 <= PlayingSquare) AndAlso (PlayingSquare <= 8)) OrElse (_TicTacToeBoard(PlayingSquare) <> Markers.Empty) Then
      If _FatalInvalidMoves Then Me.GameState = GameStates.Invalid_Move
      Return False
      PlaceMarkerOnToGameBoard(PlayedBy, PlayingSquare) : Return True
    End If
  End Function
  Private Sub PlaceMarkerOnToGameBoard(PlayedBy As Markers, PlayingSquare As Integer)
    _TicTacToeBoard(PlayingSquare) = PlayedBy
    RaiseEvent BoardChanged(Me, New BoardChangedEventArgs(_TicTacToeBoard.AsEnumerable))
    If GameIsDrawn() Then Me.GameState = GameStates.Drawn : Exit Sub
    _CurrentPlayer = WhichPlayerIsNext(_CurrentPlayer)
  End Sub
  Private Function WhichPlayerIsNext(player As Markers) As Markers
    Return DoWhatNext(player, Markers.Player_2, Markers.Player_1, player)
  End Function
  Private Function DoWhatNext(Of T)(player As Markers, p1 As T, p2 As T, defaultTo As T) As T
    Return If(player = Markers.Player_1, p1, If(player = Markers.Player_2, p2, defaultTo))
  End Function
  Private Function GameIsDrawn() As Boolean
    Return Not _TicTacToeBoard.Any(Function(square) square = Markers.Empty)
  End Function
  Private Sub CheckForPlayerWin(player As Markers)
    Dim winLines = {({0, 1, 2}), ({3, 4, 5}), ({6, 7, 8}), ({0, 3, 6}), ({1, 4, 7}), ({2, 5, 8}), ({0, 4, 8}), ({2, 4, 6})}
    Dim winningLines As New List(Of Integer())(winLines.Where(Function(WinningLine) CheckLine(player, WinningLine)))
    If winningLines.Any Then
      Me.GameState = DoWhatNext(player, GameStates.Win_Player1, GameStates.Win_Player2, GameStates.Invalid_State)
      If (GameState = GameStates.Win_Player1) OrElse (GameState = GameStates.Win_Player2) Then RaiseEvent PlayerWin(Me, New PlayerWinEventArgs(player, winningLines))
    End If
  End Sub
  Private Function CheckLine(player As Markers, ParamArray squares() As Integer) As Boolean
    Return squares.All(Function(sqr) _TicTacToeBoard(sqr) = player)
  End Function
End Class

This had the effect of slightly increasing the line of code. (But hey it still tiny.)

Simple Console Game Implementation

Let's implement a tic-tac-toe game that user can interact with, a utilize the kernel we previously wrote.
It's a two human player play model.
Module Module1
  Dim WithEvents Game As TicTacToe.TicTacToe
  Sub Main()
    Game = New TicTacToe.TicTacToe()
    While Game.GameState = TicTacToe.GameStates.Playing
      Dim k As ConsoleKeyInfo
          k = Console.ReadKey(False)
        Loop Until ("0"c <= k.KeyChar) AndAlso (k.KeyChar < "9"c)
      Loop Until Game.PlayMove(Game.CurrentPlayer, Integer.Parse(k.KeyChar))
    End While
  End Sub

Really simple loops construct here.

  Private Sub Game_BoardChanged(sender As TicTacToe.TicTacToe, e As TicTacToe.BoardChangedEventArgs) Handles Game.BoardChanged
  End Sub

  Private Sub Game_GameDrawn(sender As TicTacToe.TicTacToe, e As EventArgs) Handles Game.GameDrawn
  End Sub

  Private Sub Game_PlayerWin(sender As TicTacToe.TicTacToe, e As TicTacToe.PlayerWinEventArgs) Handles Game.PlayerWin
    DrawBoard(sender, e.WinningLines.SelectMany(Function(x) x).Distinct.ToArray)
  End Sub

Add some event handlers to handle the events the Kernel raises.

  Private Sub DrawBoard(board As TicTacToe.TicTacToe, Optional winsqr As Integer() = Nothing)
    winsqr = If(winsqr, {})
    For i = 0 To 8
      Console.BackgroundColor = If(winsqr.Contains(i), ConsoleColor.Yellow, ConsoleColor.Black)
      Console.Write(board(i).MarkerToChar._If(Function(c) c = " ", i.ToString()(0))) ' <-- Remove the _If section to leave spaces instead
      Console.BackgroundColor = ConsoleColor.Black
      If (i Mod 3) < 2 Then
        Console.WriteLine(If(i < 8, "-+-+-", ""))
      End If
  End Sub

Slightly fancy code here to handle both an in game board and a winning board.
If it is a winning board, it render the background of the winning squares yellow.

Then a couple of helper extension methods.
  Public Function _If(Of T)(feed As T, Pred As Func(Of T, Boolean), successful As T) As T
    Return If(Pred(feed), successful, feed)
  End Function

  Private Function MarkerToChar(m As TicTacToe.Markers) As Char
    If m = TicTacToe.Markers.Empty Then Return " "c
    If m = TicTacToe.Markers.Player_1 Then Return "X"c
    If m = TicTacToe.Markers.Player_2 Then Return "O"c
    Return Nothing
  End Function

End Module

The Console Game :- 50 LoC
      The Kernel :- 81 LoC
                   131 LoC
   The EventArgs :- 50 LoC
                   181 LoC

      The Kernel :- 102 IL ( CheckForPlayerWin :- 42 IL (Biggest))
The Console Game :-  37 IL
                    139 IL

Future Tasks
  • A Control based Implementation
  • A Computer / Robot Player.
  • Computer vs Robot.

If any of the tasks take your fancy, have a code. See what you produce.

0 Comments On This Entry