6
\$\begingroup\$

So I have a working game of Tic-Tac-Toe with an AI that plays random moves. Is anyway to maybe shorten the endturn() function so it's not so long? Also is this set up well enough to add a mode where the computer plays optimal or sub-optimal moves and a mode impossible to beat?

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

[System.Serializable]
public class Player
{
    public Image panel;
    public Text text;
    public Button button;
}
[System.Serializable]
public class PlayerColor
{
    public Color panelColor;
    public Color textColor;
}

public class GameController : MonoBehaviour
{
    public Text[] buttonList;
    public GameObject gameOverPanel;
    public Text gameOverText;
    private string playerSide;
    private string computerSide;
    private int moveCount;
    public GameObject restartButton;
    public GameObject startInfo;

public Player playerX;
public Player playerO;
public PlayerColor activePlayerColor;
public PlayerColor inactivePlayerColor;

public bool playerMove;
public float delay;
private int value;

public enum State { Null, X, O };

void Awake()
{
    SetGameControllerReferenceOnButtons();
    gameOverPanel.SetActive(false);
    moveCount = 0;
    restartButton.SetActive(false);
    playerMove = true;
}

public void Update()
{
    if (playerMove == false)
    {
        delay += delay * Time.deltaTime;
        if (delay >= 100)
        {
            value = Random.Range(0, 8);
            if (buttonList[value].GetComponentInParent<Button>().interactable == true)
            {
                buttonList[value].text = GetComputerSide();
                buttonList[value].GetComponentInParent<Button>().interactable = false;
                EndTurn();
            }
        }
    }
}

void SetGameControllerReferenceOnButtons()
{
    for (int i = 0; i < buttonList.Length; i++)
    {
       buttonList[i].GetComponentInParent<GridSpace>().SetGameControllerReference(this);  
    }
}

public void SetStartingSide(string startingSide)
{
    playerSide = startingSide;
    if(playerSide == "X")
    {
        computerSide = "O";
        SetPlayerColors(playerX, playerO);
    }
    else
    {
        computerSide = "X";
        SetPlayerColors(playerO, playerX);
    }

    StartGame();
}

void StartGame()
{
    SetBoardInteractable(true);
    SetPlayerButtons(false);
    startInfo.SetActive(false);
}

public string GetPlayerSide()
{
    return playerSide;
}

public string GetComputerSide()
{
    return computerSide;
}

public void EndTurn()
{
    moveCount++;

    for (int i = 0; i < buttonList.Length; i++)
    {
        if (buttonList[0].text == playerSide && buttonList[1].text == playerSide && buttonList[2].text == playerSide)
        {
            GameOver(playerSide);
        }

        else if (buttonList[3].text == playerSide && buttonList[4].text == playerSide && buttonList[5].text == playerSide)
        {
            GameOver(playerSide);
        }

        else if (buttonList[6].text == playerSide && buttonList[7].text == playerSide && buttonList[8].text == playerSide)
        {
            GameOver(playerSide);
        }

        else if (buttonList[0].text == playerSide && buttonList[4].text == playerSide && buttonList[8].text == playerSide)
        {
            GameOver(playerSide);
        }

        else if (buttonList[2].text == playerSide && buttonList[4].text == playerSide && buttonList[6].text == playerSide)
        {
            GameOver(playerSide);
        }

        else if (buttonList[0].text == playerSide && buttonList[3].text == playerSide && buttonList[6].text == playerSide)
        {
            GameOver(playerSide);
        }

        else if (buttonList[1].text == playerSide && buttonList[4].text == playerSide && buttonList[7].text == playerSide)
        {
            GameOver(playerSide);
        }

        else if (buttonList[2].text == playerSide && buttonList[5].text == playerSide && buttonList[8].text == playerSide)
        {
            GameOver(playerSide);
        }

        else if (buttonList[0].text == computerSide && buttonList[1].text == computerSide && buttonList[2].text == computerSide)
        {
            GameOver(computerSide);
        }

        else if (buttonList[3].text == computerSide && buttonList[4].text == computerSide && buttonList[5].text == computerSide)
        {
            GameOver(computerSide);
        }

        else if (buttonList[6].text == computerSide && buttonList[7].text == computerSide && buttonList[8].text == computerSide)
        {
            GameOver(computerSide);
        }

        else if (buttonList[0].text == computerSide && buttonList[4].text == computerSide && buttonList[8].text == computerSide)
        {
            GameOver(computerSide);
        }

        else if (buttonList[2].text == computerSide && buttonList[4].text == computerSide && buttonList[6].text == computerSide)
        {
            GameOver(computerSide);
        }

        else if (buttonList[0].text == computerSide && buttonList[3].text == computerSide && buttonList[6].text == computerSide)
        {
            GameOver(computerSide);
        }

        else if (buttonList[1].text == computerSide && buttonList[4].text == computerSide && buttonList[7].text == computerSide)
        {
            GameOver(computerSide);
        }

        else if (buttonList[2].text == computerSide && buttonList[5].text == computerSide && buttonList[8].text == computerSide)
        {
            GameOver(computerSide);
        }

        else if (moveCount >= 9)
        {
            GameOver("draw");
        }
        else
        {
            ChangeSides();
            delay = 10;
        }
    }

}

void ChangeSides()
{
    //playerSide = (playerSide == "X") ? "O" : "X";
    playerMove = (playerMove == true) ? false : true;
    //if(playerSide == "X")
    if(playerMove == true)
    {
        SetPlayerColors(playerX, playerO);
    }
    else
    {
        SetPlayerColors(playerO, playerX);
    }
}

void SetPlayerColors(Player newPlayer, Player oldPlayer)
{
    newPlayer.panel.color = activePlayerColor.panelColor;
    newPlayer.text.color = activePlayerColor.textColor;

    oldPlayer.panel.color = inactivePlayerColor.panelColor;
    oldPlayer.text.color = inactivePlayerColor.textColor;
}

void GameOver(string winningPlayer)
{
    SetBoardInteractable(false);
    if (winningPlayer == "draw")
    {
        SetGameOverText("Draw");
        SetPlayerColorsInactive();
    }
    else
    {
        SetGameOverText(winningPlayer + " wins");
    }
    restartButton.SetActive(true);
}

void SetGameOverText(string value)
{
    gameOverPanel.SetActive(true);
    gameOverText.text = value;
}

public void RestartGame()
{
    moveCount = 0;
    gameOverPanel.SetActive(false);
    restartButton.SetActive(false);
    SetPlayerButtons(true);
    startInfo.SetActive(true);
    SetPlayerColorsInactive();
    playerMove = true;
    delay = 10;

    for (int i = 0; i < buttonList.Length; i++)
    {
        buttonList[i].text = "";
    }
}

void SetBoardInteractable(bool toggle)
{
    for (int i = 0; i < buttonList.Length; i++)
    {
        buttonList[i].GetComponentInParent<Button>().interactable = toggle;
    }
}

void SetPlayerButtons(bool toggle)
{
    playerX.button.interactable = toggle;
    playerO.button.interactable = toggle;
}

void SetPlayerColorsInactive()
{
    playerX.panel.color = inactivePlayerColor.panelColor;
    playerX.text.color = inactivePlayerColor.textColor;
    playerO.panel.color = inactivePlayerColor.panelColor;
    playerO.panel.color = inactivePlayerColor.textColor;
}    
}
\$\endgroup\$
  • 1
    \$\begingroup\$ Use a 2d array of buttons, not a 1d list. Then you can loop over the items by row/column. \$\endgroup\$ – Hosch250 Apr 17 '18 at 18:48
  • \$\begingroup\$ Is there an example I could go find? \$\endgroup\$ – Koli Apr 17 '18 at 18:54
3
\$\begingroup\$

Yes, there are ways you can shorten your EndTurn method.

First, you've duplicated your code in EndTurn to compare to both playerSide and computerSide, but the code is the same otherwise. You can extract a method (named IsGameWonBy below) to eliminate this duplication as follows:

public void EndTurn()
{
    moveCount++;

    for (int i = 0; i < buttonList.Length; i++)
    {
        if (IsGameWonBy(playerSide))
        {
            GameOver(playerSide);
        }
        else if (IsGameWonBy(computerSide))
        {
            GameOver(computerSide);
        }
        else if (moveCount >= 9)
        {
            GameOver("draw");
        }
        else
        {
            ChangeSides();
            delay = 10;
        }
    }
}

private bool IsGameWonBy(string side)
{
    if (buttonList[0].text == side && buttonList[1].text == side && buttonList[2].text == side)
    {
        return true;
    }
    else if (buttonList[3].text == side && buttonList[4].text == side && buttonList[5].text == side)
    {
        return true;
    }
    else if (buttonList[6].text == side && buttonList[7].text == side && buttonList[8].text == side)
    {
        return true;
    }
    else if (buttonList[0].text == side && buttonList[4].text == side && buttonList[8].text == side)
    {
        return true;
    }
    else if (buttonList[2].text == side && buttonList[4].text == side && buttonList[6].text == side)
    {
        return true;
    }
    else if (buttonList[0].text == side && buttonList[3].text == side && buttonList[6].text == side)
    {
        return true;
    }
    else if (buttonList[1].text == side && buttonList[4].text == side && buttonList[7].text == side)
    {
        return true;
    }
    else if (buttonList[2].text == side && buttonList[5].text == side && buttonList[8].text == side)
    {
        return true;
    }

    return false;
}

Restructuring your code this way, your code is more readable and it is more obvious that your for loop in EndTurn isn't necessary. I think the code just happens to work because ChangeSides is called 9 times, an odd number, and there seem to be no consequences in your code for calling GameOver or ChangeSides multiple times. Let's remove that for loop there.

public void EndTurn()
{
    moveCount++;

    if (IsGameWonBy(playerSide))
    {
        GameOver(playerSide);
    }
    else if (IsGameWonBy(computerSide))
    {
        GameOver(computerSide);
    }
    else if (moveCount >= 9)
    {
        GameOver("draw");
    }
    else
    {
        ChangeSides();
        delay = 10;
    }
}

As another user commented, ideally you should use a 2D array rather than a 1D array to represent your tic-tac-toe board. This will also make the logic of the code simpler when using for loops to check for the win condition rather than manually checking every possible row/column/diagonal. If you aren't familiar with 2D arrays, you should do some research on "C# multidimensional arrays". After learning about them, you can check out this post with some other algorithms for solving tic-tac-toe, most of which use a 2D array.

However, note that for a 3x3 tic-tac-toe, it's also acceptable to just check the rows, columns, and diagonals manually for the win condition as you have done. As an exercise, you may want to try modifying your game to a 5x5 tic-tac-toe game to see what is difficult to change in your code and Unity project to make that happen. If your code is structured well, it should allow you to quickly switch the game between a 3x3 board and a 5x5 board without having to make significant changes to your code & project.

(As a note, 2D arrays are a bit tricky to set up with the current structure of your code because they don't work nicely with the Unity Editor Inspector window by default. This is a slightly more advanced topic, but you'll need to separate the logic of your game code from the UI code. Currently, all your game state and logic is tied to your buttonList buttons.)

As for adding a mode where the computer has different behavior, yes it should be possible but you should restructure your code a bit. Ideally, the computer's behavior should be separated out into its own class or at least its own method as a first step. For example, see the GetNextComputerMove method below:

public void Update()
{
    if (playerMove == false)
    {
        delay += delay * Time.deltaTime;
        if (delay >= 100)
        {
            value = GetNextComputerMove();

            buttonList[value].text = GetComputerSide();
            buttonList[value].GetComponentInParent<Button>().interactable = false;

            EndTurn();
        }
    }
}

private int GetNextComputerMove()
{
    // Put computer's logic here
    // for example, Random.Range(0, 8)
    // but you also need to check which moves are valid
    // so you can return a valid move
}

Note that if you want to add a mode where computer plays optimal or sub-optimal moves, that will take more logic than just Random.Range. Currently, you are relying on the Update method being called multiple times to get a valid move by the computer. As a first step, I'd suggest figuring out how to get the list of all valid moves that the computer can make so that you can select a random move among those moves.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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