4
\$\begingroup\$

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()
        Return File.ReadAllLines(inputPath)
    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
\$\endgroup\$

2 Answers 2

2
\$\begingroup\$

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
        result.Add(rowContaining)
    End If

    If columnContaining IsNot Nothing Then
        result.Add(columnContaining)
    End If

    If diagonalContaining IsNot Nothing Then
        result.Add(diagonalContaining)
    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
        resultData.Add(PlayGame(rawGameDataLine))
    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. :)

\$\endgroup\$
1
  • \$\begingroup\$ 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! \$\endgroup\$ Commented Nov 8, 2016 at 15:14
2
\$\begingroup\$

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
            nextByte = fs.Read
            If nextByte <> 13 Then
                games = games * 10 + (nextByte - Asc("0"))
            End If
        End While
        nextByte = fs.Read
        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
                    Console.ReadKey()
                    Exit For
                End If
            Next
        Next
    End Using
End Sub
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the answer! I'll be honest, it might take me a little while to understand it. \$\endgroup\$ Commented Nov 10, 2016 at 19:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.