# Evaluate a game of tic-tac-toe to determine winner

So the program will take input of a series of 9 integers which indicate which square was played on each move. Player 1 is always X and player 2 is always O. Player 1 plays first. Move 1 is first and Move 9 is last, sequentially.

### The board looks like this

 1 | 2 | 3
---+---+---
4 | 5 | 6
---+---+---
7 | 8 | 9


Winning lines are:

• 3 Horizontal (1-2-3, 4-5-6, 7-8-9)
• 3 Vertical (1-4-7, 2-5-8, 3-6-9)
• 2 Diagonal (1-5-9, 3-5-7)

### Sample input

Because of the challenge I was completing, the input includes the first line as the number of games being played, but I've completely ignored it in this implementation.

13
3 5 2 8 6 9 7 1 4
8 3 1 6 2 5 7 4 9
6 4 1 5 8 7 3 2 9
3 1 7 5 9 2 6 4 8
3 2 6 1 4 7 8 5 9
2 7 8 5 3 9 4 1 6
3 6 9 1 5 2 7 4 8
9 2 6 3 5 8 1 7 4
5 8 2 9 1 6 4 7 3
9 7 2 3 4 8 5 1 6
7 1 9 8 4 2 6 5 3
7 3 5 2 6 4 9 1 8
5 1 4 3 6 9 8 7 2


### Structure

1. Get input data from txt file (space-delimited)
2. For each game, determine which player played the winning move and at what location. If no winner, draw.
3. Write txt file of winning data

### Notes

• I initially wrote this in excel and it played the games, which is how I could double check this to ensure it's working properly.
• I'm sure my CheckWin function can be improved dramatically, but I have a simple mind when it comes to this.
• This is really the first "complex" thing I've written in VB.net, hence the beginner tag, though I did take input from my other questions on the language.
• I used Environment.NewLine because I couldn't determine how to get something like vbLF to work and so I'm not sure if that's bad practice, using the environment.

## The Magic

Option Explicit On
Option Strict On
Option Infer On
Option Compare Text

Imports System.IO
Module TicTacToeAnalyzer

Sub Main()
Const INPUT_PATH As String = "C:\Temp\tictactoe.txt"
Const OUTPUT_PATH As String = "C:\Temp\tictactoeResults.txt"
Dim rawGameData As String()
rawGameData = GetInput(INPUT_PATH)
Dim gameResult As String
Dim resultData As String()
ReDim resultData(rawGameData.Length - 1)
Dim moveData As String
For i As Integer = 1 To rawGameData.Length - 1
moveData = rawGameData(i)
gameResult = PlayGame(moveData, i)
resultData(i) = gameResult
Next
gameResult = resultData(1)
gameResult = String.Join(Environment.NewLine, resultData).Trim()
WriteOutput(OUTPUT_PATH, gameResult)
End Sub

Private Function PlayGame(ByVal moveData As String, ByVal gameNumber As Integer) As String
Const DELIMITER As String = " "
Dim moveArray As Integer()
moveArray = ConvertStringsToIntegers(moveData, DELIMITER)
Dim boardArray As Integer()
ReDim boardArray(9)
Dim gameWinner As String
Dim isWon As Boolean
Dim move As Integer
Dim square As Integer
Dim player As Integer
For iterator As Integer = 0 To boardArray.Length
move = iterator + 1
square = moveArray(iterator)
Select Case move
Case 1, 3, 5, 7, 9
player = 1
boardArray(square) = 1
Case 2, 4, 6, 8
player = 2
boardArray(square) = 2
End Select
If move > 4 Then isWon = CheckWin(boardArray, square)
If isWon Then
gameWinner = "Game #" & gameNumber & " is won by Player " & player & " on move #" & move & " in square #" & square & "."
Return gameWinner
End If
Next
Return "Game #" & gameNumber & " is a draw."
End Function

Private Function CheckWin(ByVal boardarray As Integer(), ByVal square As Integer) As Boolean
Dim isWin As Boolean
isWin = False
Select Case square
Case 1
If boardarray(1) = boardarray(2) And boardarray(1) = boardarray(3) Then isWin = True
If boardarray(1) = boardarray(4) And boardarray(1) = boardarray(7) Then isWin = True
If boardarray(1) = boardarray(5) And boardarray(1) = boardarray(9) Then isWin = True
Case 2
If boardarray(2) = boardarray(1) And boardarray(2) = boardarray(3) Then isWin = True
If boardarray(2) = boardarray(5) And boardarray(2) = boardarray(8) Then isWin = True
Case 3
If boardarray(3) = boardarray(2) And boardarray(3) = boardarray(1) Then isWin = True
If boardarray(3) = boardarray(6) And boardarray(3) = boardarray(9) Then isWin = True
If boardarray(3) = boardarray(5) And boardarray(3) = boardarray(7) Then isWin = True
Case 4
If boardarray(4) = boardarray(1) And boardarray(4) = boardarray(7) Then isWin = True
If boardarray(4) = boardarray(5) And boardarray(4) = boardarray(6) Then isWin = True
Case 5
If boardarray(5) = boardarray(1) And boardarray(5) = boardarray(9) Then isWin = True
If boardarray(5) = boardarray(2) And boardarray(5) = boardarray(8) Then isWin = True
If boardarray(5) = boardarray(3) And boardarray(5) = boardarray(7) Then isWin = True
If boardarray(5) = boardarray(4) And boardarray(5) = boardarray(6) Then isWin = True
Case 6
If boardarray(6) = boardarray(3) And boardarray(6) = boardarray(9) Then isWin = True
If boardarray(6) = boardarray(5) And boardarray(6) = boardarray(4) Then isWin = True
Case 7
If boardarray(7) = boardarray(8) And boardarray(7) = boardarray(9) Then isWin = True
If boardarray(7) = boardarray(1) And boardarray(7) = boardarray(4) Then isWin = True
If boardarray(7) = boardarray(3) And boardarray(7) = boardarray(5) Then isWin = True
Case 8
If boardarray(8) = boardarray(2) And boardarray(8) = boardarray(5) Then isWin = True
If boardarray(8) = boardarray(7) And boardarray(8) = boardarray(9) Then isWin = True
Case 9
If boardarray(9) = boardarray(1) And boardarray(9) = boardarray(5) Then isWin = True
If boardarray(9) = boardarray(3) And boardarray(9) = boardarray(6) Then isWin = True
If boardarray(9) = boardarray(7) And boardarray(9) = boardarray(8) Then isWin = True
End Select
Return isWin
End Function

Private Function GetInput(ByVal inputPath As String) As String()
End Function

Private Function ConvertStringsToIntegers(ByVal moveData As String, ByVal DELIMITER As String) As Integer()
Dim arrayOfStrings As String() = moveData.Split(New String() {DELIMITER}, StringSplitOptions.None)
Return Array.ConvertAll(arrayOfStrings, Function(str) Integer.Parse(str))
End Function

Private Sub WriteOutput(ByVal outputPath As String, ByVal gameResult As String)
Using fileAuthor As New StreamWriter(outputPath)
fileAuthor.WriteLine(gameResult)
End Using
End Sub

End Module


You should consider representing the board as a class instead of an array.

This will allow you to make it more stateful, and extract board-specific logic into the class itself. You can also take your CheckWin method and make it a little more robust / non-repetitive.

Start by defining three helper-methods, RowContaining(ByVal position As Integer), ColumnContaining(ByVal position As Integer) and DiagonalContaining(ByVal position As Integer). What do we return? Simply return an Integer() array on each.

Public Function RowContaining(ByVal position As Integer) As Integer()
Select Case position
Case 1, 2, 3
Return New Integer() { 1, 2, 3 }
Case 4, 5, 6
Return New Integer() { 4, 5, 6 }
case 7, 8, 9
Return New Integer() { 7, 8, 9 }
End Select

Return Nothing
End Function


You should, ideally, create a rows dictionary and put each row in there, then select the row from the dictionary using LINQ, but I'm writing this entire answer outside of an IDE and it's been some time since I've done VB.NET.

The same principle applies to ColumnContaining and DiagonalContaining.

Then, you should make one more method: DirectionsContaining(ByVal position As Integer) As List(Of Integer()) which is similar to the follows:

Public Function DirectionsContaining(ByVal position As Integer) As List(Of Integer())
Dim result As New List(Of Integer())

Dim rowContaining As Integer() = RowContaining(position)
Dim columnContaining As Integer() = ColumnContaining(position)
Dim diagonalContaining As Integer() = DiagonalContaining(position)

If rowContaing IsNot Nothing Then
End If

If columnContaining IsNot Nothing Then
End If

If diagonalContaining IsNot Nothing Then
End If

Return result
End Function


I think you can see where this is going, simply use LINQ .Any in your CheckWin method and bam, you've extracted the ugly switch statement and conditionals to a much cleaner (and more robust) alternative. Now if, for some reason, you want to add more rows/columns, it's trivial.

In the following snippet:

    For iterator As Integer = 0 To boardArray.Length
move = iterator + 1
square = moveArray(iterator)
Select Case move
Case 1, 3, 5, 7, 9
player = 1
boardArray(square) = 1
Case 2, 4, 6, 8
player = 2
boardArray(square) = 2
End Select
If move > 4 Then isWon = CheckWin(boardArray, square)
If isWon Then
gameWinner = "Game #" & gameNumber & " is won by Player " & player & " on move #" & move & " in square #" & square & "."
Return gameWinner
End If
Next


If performance is not a concern, it's trivial to use:

If move Mod 2 = 1 Then
' Player 1 move
Else
' Player 2 move
End If


In VB n Mod x is equivalent to C#'s n % x.

This whole method can be cleaned quite a bit:

Sub Main()
Const INPUT_PATH As String = "C:\Temp\tictactoe.txt"
Const OUTPUT_PATH As String = "C:\Temp\tictactoeResults.txt"
Dim rawGameData As String()
rawGameData = GetInput(INPUT_PATH)
Dim gameResult As String
Dim resultData As String()
ReDim resultData(rawGameData.Length - 1)
Dim moveData As String
For i As Integer = 1 To rawGameData.Length - 1
moveData = rawGameData(i)
gameResult = PlayGame(moveData, i)
resultData(i) = gameResult
Next
gameResult = resultData(1)
gameResult = String.Join(Environment.NewLine, resultData).Trim()
WriteOutput(OUTPUT_PATH, gameResult)
End Sub


First: you only really need i for the "game winner" message, which should be extracted to a different location. In fact, you're PlayGame method only ever returns two variants on the string:

"won by Player " & player & " on move #" & move & " in square #" & square & "."
"a draw."


This whole state can be extracted to a different class, so we'll start there.

Public Class GameResult
Public Property Winner As GamePlayer
Public Property LastMoveSquare As Integer
Public Property LastMoveNumber As Integer
End Class

Public Enum GamePlayer
None,
Player1,
Player2,
End Enum


Now, we've extracted the entire game result to a new class to eliminate the need for string processing. (This is far better for multi-lingual setups, or setups where you just want to dump this result to a DB, etc.)

Notice that I left GameNumber out, that's irrelevant to the result of the game playing method. It doesn't care about the game number, only about it's result.

Next, we'll use this in our Main() method:

Sub Main()
Const INPUT_PATH As String = "C:\Temp\tictactoe.txt"
Const OUTPUT_PATH As String = "C:\Temp\tictactoeResults.txt"
Dim rawGameData As String()
rawGameData = GetInput(INPUT_PATH)

Dim resultData As List(Of GameResult)

For Each rawGameDataLine As String In rawGameData
End For

Dim gameResult As New StringBuilder

For i As Integer = 0 To resultData.Length ' Might need - 1, I forget on this one
If gameResult.Length > 0 Then
gameResult.Append(Environment.NewLine)
End If

gameResult.Append(...) ' replace ... with your string formatting, remembering that we're using resultData(i) and the properties on it.
Next

WriteOutput(OUTPUT_PATH, gameResult)
End Sub


Also, I believe your original code had a bug:

    For i As Integer = 1 To rawGameData.Length - 1
moveData = rawGameData(i)
gameResult = PlayGame(moveData, i)
resultData(i) = gameResult
Next


If I am understanding correctly, resultData(0) will always be a null string.

Disclaimer: I wrote this entire answer with no testing in the IDE, the code may not be 100% correct, if not, please let me know and I'll try to correct it.

I know this is a lot to take in initially, I apologize for that, but I had a lot to cover. Hopefully it's helpful. :)

• Thanks, these are definitely some ideas I hadn't considered. I imagine they'll be pretty useful, I might try rewriting the whole thing like this to learn it. I appreciate it! – Raystafarian Nov 8 '16 at 15:14

Some other points to consider, if you represent each square on the board as as a separate object and then represent the board as an array of those objects. Having arrays to represent each line makes the playing and checking very simple:

Class Board
Private Class Square
Public value As Char = " "c
Public position As Integer = 0
End Class
'When testing the line for a winning combination, this enables the LINQ extension
'Distinct to work with the Square class
Private Class SquareComparer
Implements IEqualityComparer(Of Square)

Public Function Equals(x As Square, y As Square) As Boolean Implements IEqualityComparer(Of Square).Equals
Return x.value = y.value
End Function

Public Function GetHashCode(obj As Square) As Integer Implements IEqualityComparer(Of Square).GetHashCode
Return obj.value.GetHashCode
End Function
End Class
Private players() As Char =
{
"O"c,
"X"c
}
Private thisBoard() As Square =
{
New Square With {.position = 0},
New Square With {.position = 1},
New Square With {.position = 2},
New Square With {.position = 3},
New Square With {.position = 4},
New Square With {.position = 5},
New Square With {.position = 6},
New Square With {.position = 7},
New Square With {.position = 8},
New Square With {.position = 9}
}
Private lines()() As Square =
{
({thisBoard(3), thisBoard(5), thisBoard(7)}),
({thisBoard(1), thisBoard(5), thisBoard(9)}),
({thisBoard(1), thisBoard(4), thisBoard(7)}),
({thisBoard(2), thisBoard(5), thisBoard(8)}),
({thisBoard(3), thisBoard(6), thisBoard(9)}),
({thisBoard(1), thisBoard(2), thisBoard(3)}),
({thisBoard(4), thisBoard(5), thisBoard(6)}),
({thisBoard(7), thisBoard(8), thisBoard(9)})
}
Public Function MakeMove(turn As Integer, position As Integer, ByRef done As Boolean) As String
Dim player As Char = players(turn Mod 2)
If thisBoard(position).value <> " "c Then
Return "Invalid Move"
Else
'Because each square is an object the different arrays access the squares
'by reference and each array that contains that square will be updated as well
thisBoard(position).value = player
End If
If turn > 3 Then

For Each line() As Square In lines
If line(0).value <> " "c AndAlso line.Distinct(New SquareComparer).Count = 1 Then
done = True
Return player & " Wins !!"
End If
Next
If turn = 9 Then
done = True
Return "Draw - No Winner"
End If
End If
Return player & " played on position" & position.ToString
End Function
Public Function PrintBoard() As String()
Dim outVal(2) As String
For i As Integer = 0 To 2
outVal(i) = Join((From sq In lines(i + 5)
Select New String(sq.value, 1)).ToArray)
Next
Return outVal
End Function
End Class


Using the class is also quite simple. This example writes to the Console, but that should be quite simple to adapt to writing to a file with a StreamWriter, since the functions are the same:

Sub Main()

Using fs As New IO.StreamReader("textfile1.txt")
Dim nextByte As Integer = 0
Dim games As Integer = 0
'In many instances reading each character as character code and building the
'number is more efficient then reading the string and converting to a number.
While nextByte <> 13
If nextByte <> 13 Then
games = games * 10 + (nextByte - Asc("0"))
End If
End While
For i As Integer = 1 To games
Dim thisGame = New Board
Dim done As Boolean = False
Dim turn = 0
Dim turns = fs.ReadLine.Split()
For j As Integer = 0 To turns.GetUpperBound(0)
Console.WriteLine(thisGame.MakeMove(j + 1, Asc(turns(j)(0)) - Asc("0"c), done))
For Each s In thisGame.PrintBoard
Console.WriteLine(s)
Next
If done Then