6
\$\begingroup\$

I've been learning C#, and today I made a simple Tic Tac Toe game. The code is pretty long, and I'm almost 100% positive that this can be done so much better. Please provide some advice, suggestions, and criticism.

    byte _clicks = 1;
    bool but1clicked, but2clicked, but3clicked, but4clicked, but5clicked, but6clicked, but7clicked, but8clicked, but9clicked = false;

    public void ResetGame()
    {
        but1.Text = null;
        but2.Text = null;
        but3.Text = null;
        but4.Text = null;
        but5.Text = null;
        but6.Text = null;
        but7.Text = null;
        but8.Text = null;
        but9.Text = null;

        but1clicked = false;
        but2clicked = false;
        but3clicked = false;
        but4clicked = false;
        but5clicked = false;
        but6clicked = false;
        but7clicked = false;
        but8clicked = false;
        but9clicked = false;
    }

    public void Check()
    {
        if (but1.Text == "X" && but2.Text == "X" && but3.Text == "X")
        {
            MessageBox.Show("Game Over! X Wins!");
            ResetGame();
        }
        else if (but4.Text == "X" && but5.Text == "X" && but6.Text == "X")
        {
            MessageBox.Show("Game Over! X Wins!");
            ResetGame();
        }
        else if (but7.Text == "X" && but8.Text == "X" && but9.Text == "X")
        {
            MessageBox.Show("Game Over! X Wins!");
            ResetGame();
        }
        else if (but1.Text == "X" && but4.Text == "X" && but7.Text == "X")
        {
            MessageBox.Show("Game Over! X Wins!");
            ResetGame();
        }
        else if (but2.Text == "X" && but5.Text == "X" && but8.Text == "X")
        {
            MessageBox.Show("Game Over! X Wins!");
            ResetGame();
        }
        else if (but3.Text == "X" && but6.Text == "X" && but9.Text == "X")
        {
            MessageBox.Show("Game Over! X Wins!");
            ResetGame();
        }
        else if (but1.Text == "X" && but5.Text == "X" && but9.Text == "X")
        {
            MessageBox.Show("Game Over! X Wins!");
            ResetGame();
        }
        else if (but3.Text == "X" && but5.Text == "X" && but7.Text == "X")
        {
            MessageBox.Show("Game Over! X Wins!");
            ResetGame();
        }
        if (but1.Text == "O" && but2.Text == "O" && but3.Text == "O")
        {
            MessageBox.Show("Game Over! O Wins!");
            ResetGame();
        }
        else if (but4.Text == "O" && but5.Text == "O" && but6.Text == "O")
        {
            MessageBox.Show("Game Over! O Wins!");
            ResetGame();
        }
        else if (but7.Text == "O" && but8.Text == "O" && but9.Text == "O")
        {
            MessageBox.Show("Game Over! O Wins!");
            ResetGame();
        }
        else if (but1.Text == "O" && but4.Text == "O" && but7.Text == "O")
        {
            MessageBox.Show("Game Over! O Wins!");
            ResetGame();
        }
        else if (but2.Text == "O" && but5.Text == "O" && but8.Text == "O")
        {
            MessageBox.Show("Game Over! O Wins!");
            ResetGame();
        }
        else if (but3.Text == "O" && but6.Text == "O" && but9.Text == "O")
        {
            MessageBox.Show("Game Over! O Wins!");
            ResetGame();
        }
        else if (but1.Text == "O" && but5.Text == "O" && but9.Text == "O")
        {
            MessageBox.Show("Game Over! O Wins!");
            ResetGame();
        }
        else if (but3.Text == "O" && but5.Text == "O" && but7.Text == "O")
        {
            MessageBox.Show("Game Over! O Wins!");
            ResetGame();
        }
    }

    public void Tie()
    {
        if (but1clicked == true &&
            but2clicked == true &&
            but3clicked == true && 
            but4clicked == true && 
            but5clicked == true && 
            but6clicked == true && 
            but7clicked == true && 
            but8clicked == true && 
            but9clicked == true)
        {
            MessageBox.Show("You have tied! The game will now reset!");
            ResetGame();
        }
    }

    private void but1_Click(object sender, EventArgs e)
    {
        if (but1clicked == false)
        {
            if (Convert.ToBoolean(_clicks % 2))
            {
                but1.Text = "X";
                but1clicked = true;
                _clicks++;
            }
            else
            {
                but1.Text = "O";
                but1clicked = true;
                _clicks++;
            }
        }
        else
            MessageBox.Show("This box has already been chosen! Please choose another!");
        Check();
        Tie();
    }

There are 9 buttons, just the same as the method but1_click. I didn't think it was necessary to add them, though.

\$\endgroup\$
  • 1
    \$\begingroup\$ Welcome to Code Review. You might be interested in reading some of our previous questions about c# and tic-tac-toe. \$\endgroup\$ – Simon Forsberg Aug 23 '15 at 11:32
  • \$\begingroup\$ After the players have tied, maybe add a timer delay before resetting the game, that way it wouldn't seem to instant. \$\endgroup\$ – user41805 Aug 23 '15 at 11:33
  • 1
    \$\begingroup\$ == true is not needed ever, you can just remove it from each place it occurs \$\endgroup\$ – spyr03 Aug 23 '15 at 11:52
  • 1
    \$\begingroup\$ Please state only code purpose in the title \$\endgroup\$ – Caridorc Aug 23 '15 at 12:06
  • 1
    \$\begingroup\$ Please post all your code to get more complete reviews, if it is just repetition, much better. \$\endgroup\$ – Caridorc Aug 23 '15 at 12:07
10
\$\begingroup\$

The buttons should be an array, if so you will replace btn with buttons[1]. Using the array will allow replacing this:

    if (but1clicked == true &&
        but2clicked == true &&
        but3clicked == true && 
        but4clicked == true && 
        but5clicked == true && 
        but6clicked == true && 
        but7clicked == true && 
        but8clicked == true && 
        but9clicked == true)

With this:

if (buttons.All(button => button.clicked))

I assume that each button should have a clicked method. You will need some more modifications for this to work.

About the win lose checks, I suggest

triples = ( (0, 1, 2), (3, 4, 5) ..)
def wins(symbol) {
    return triples.Any(triple => triple.All(index => buttons[index] == symbol))
 }

All of this is pseudocode, but should be implementable with no problems.

\$\endgroup\$
6
\$\begingroup\$

There is much repetition in your code. While it is natural to think that way as a human, computer has a much stronger computing and memorization power than we do which we should take advantage of.

Click handler

Based on the naming, you have 9 of these click handlers with pretty the same code with only a single variation in the button's name. You can write this in a more generic way, so that all the 9 buttons share the same click handler.

private void but1_Click(object sender, EventArgs e)`

The flow isn't perfect either, as you should not do the end-game condition checking when the button is already being marked.

 private void but1_Click(object sender, EventArgs e)
 {
    if (but1clicked == false)
    {
        // mark it with X or O
    }
    else
    {
        // show it is already marked
    }

    Check();
    Tie();
 }

It is unnecessary to check for both of the end-game conditions.

Here is how I would write it:

void Button_OnClick(object sender, EventArgs e)
{
    // usually, the sender is the one that triggered the event.
    // in this case, the button that is being clicked.
    var button = sender as Button;
    if (!string.IsNullOrEmpty(button.Text))
    {
        MessageBox.Show("This box has already been chosen! Please choose another!");
        return;
    }

    button.Text = (checkedCount % 2 == 0)? "X" : "O";
    checkedCount++;

    // *I'll come back to these in the next section
    Check();
    Tie();
}

Logic flows

Check and Tie seems to be a bit confusing here. Usually, you want to use verb to for an action. However, you are not checking a box or making the game a tie in these methods, but rather just to evaluation the condition.

Also, you are resetting the game inside these method which adds a layer of complexity and surprise(you really don't want this) to it.

I have rewritten these two to return a boolean and moved ResetGame outside of it. Now the logic of the game is more clear :

void Button_OnClick(object sender, EventArgs e)
{
    var button = sender as Button;
    if (!string.IsNullOrEmpty(button.Text))
    {
        MessageBox.Show("This box has already been chosen! Please choose another!");
        return;
    }

    button.Text = (checkedCount % 2 == 0)? "X" : "O";
    checkedCount++;

    if (AnySideWins() || IsTie())
        ResetGame();
}

Winning Condition[AnySideWins]

There is 3 horizontal, 3 vertical and 2 diagonal lines, a total of 8, which you can win by filling up any of them. This times both sides, and you have 16 conditions to check.

And if you decide to change anything by any chance, that is 16 edits there.

Let's ask the question : What exactly are we doing here?

We want to check if any of these line has been filled by the same mark. In other words, we want to check if any of Button[3] contains the same text thrice and are not empty.

bool AnySideWins()
{
    var horizontals = new[]
    {
        new [] { but1, but2, but3 },
        new [] { but4, but5, but6 },
        new [] { but7, but8, but9 },
    };
    var verticals = new[]
    {
        new [] { but1, but4, but7 },
        new [] { but2, but5, but8 },
        new [] { but3, but6, but9 },
    };
    var diagonals = new[]
    {
        new [] { but1, but5, but9 },
        new [] { but3, but5, but7 },
    };

    // with all the possible winning lines
    var winningLine = horizontals.Concat(verticals).Concat(diagonals)
        // find the first line that
        .FirstOrDefault(buttons =>
            // whose buttons' text are all the same 
            buttons
                // take each button.text
                .Select(button => button.Text)
                // exclude unchecked ones
                .Where(text => !string.IsNullOrEmpty(text))
                // group them by themselves, so X goes togather and O goes togather
                .GroupBy(text => text)
                // and see if any group has 3 elements.
                .Any(group => group.Count() == 3));

    // *Note: if FirstOrDefault doesnt find anything, the default of Button[], which is null, will be returned 
    if (winningLine != null)
        MessageBox.Show("Game Over! " + winningLine.First().Text + " Wins!");

    return winningLine != null;
}

Winning Condition[IsTie]

Same thing as above, but much simpler : We want to check if all buttons have been marked.

bool IsTie()
{
    var buttons = new[] { but1, but2, but3, but4, but5, but6, but7, but8, but9 };

    // if all button are marked then it is a tie.
    // or if we flip the previous assertion,
    //      if any button is not marked, then it is not a tie
    // finally we negate the result to match the method name
    var tied = !buttons.Any(button => string.IsNullOrEmpty(button.Text));
    if (tied)
        MessageBox.Show("You have tied! The game will now reset!");

    return tied;
}

Game Reset

void ResetGame()
{
    var buttons = new[] { but1, but2, but3, but4, but5, but6, but7, but8, but9 };

    foreach (var button in buttons)
        button.Text = string.Empty;

    checkedCount = 0;
}

Putting it all togather

I've rewritten this in LinqPad, for quick testing : source.linq

Button but1,but2,but3,but4,but5,but6,but7,but8,but9;
byte checkedCount = 0;

void Main()
{
    var board = new TableLayoutPanel();
    board.ColumnCount = 3;
    board.RowCount = 3;

    but1 = new Button();
    but2 = new Button();
    but3 = new Button();
    but4 = new Button();
    but5 = new Button();
    but6 = new Button();
    but7 = new Button();
    but8 = new Button();
    but9 = new Button();

    board.Controls.Add(but1, 0, 0);
    board.Controls.Add(but2, 0, 1);
    board.Controls.Add(but3, 0, 2);
    board.Controls.Add(but4, 1, 0);
    board.Controls.Add(but5, 1, 1);
    board.Controls.Add(but6, 1, 2);
    board.Controls.Add(but7, 2, 0);
    board.Controls.Add(but8, 2, 1);
    board.Controls.Add(but9, 2, 2);

    foreach (var button in board.Controls.OfType<Button>())
    {
        (button as Button).Click += Button_OnClick;
    }

    board.Dump();
}

// Define other methods and classes here
void Button_OnClick(object sender, EventArgs e)
{
    var button = sender as Button;
    if (!string.IsNullOrEmpty(button.Text))
    {
        MessageBox.Show("This box has already been chosen! Please choose another!");
        return;
    }

    button.Text = (checkedCount % 2 == 0)? "X" : "O";
    checkedCount++;

    if (AnySideWins() || IsTie())
        ResetGame();
}

bool AnySideWins()
{
    var horizontals = new[]
    {
        new [] { but1, but2, but3 },
        new [] { but4, but5, but6 },
        new [] { but7, but8, but9 },
    };
    var verticals = new[]
    {
        new [] { but1, but4, but7 },
        new [] { but2, but5, but8 },
        new [] { but3, but6, but9 },
    };
    var diagonals = new[]
    {
        new [] { but1, but5, but9 },
        new [] { but3, but5, but7 },
    };

    // with all the possible winning lines
    var winningLine = horizontals.Concat(verticals).Concat(diagonals)
        // find the first line that
        .FirstOrDefault(buttons =>
            // whose buttons' text are all the same 
            buttons
                // take each button.text
                .Select(button => button.Text)
                // exclude unchecked ones
                .Where(text => !string.IsNullOrEmpty(text))
                // group them by themselves, so X goes togather and O goes togather
                .GroupBy(text => text)
                // and see if any group has 3 elements.
                .Any(group => group.Count() == 3));

    // *Note: if FirstOrDefault doesnt find anything, the default of Button[], which is null, will be returned 
    if (winningLine != null)
        MessageBox.Show("Game Over! " + winningLine.First().Text + " Wins!");

    return winningLine != null;
}
bool IsTie()
{
    var buttons = new[] { but1, but2, but3, but4, but5, but6, but7, but8, but9 };

    // if all button are marked then it is a tie.
    // or if we flip the previous assertion,
    //      if any button is not marked, then it is not a tie
    // finally we negate the result to match the method name
    var tied = !buttons.Any(button => string.IsNullOrEmpty(button.Text));
    if (tied)
        MessageBox.Show("You have tied! The game will now reset!");

    return tied;
}
void ResetGame()
{
    var buttons = new[] { but1, but2, but3, but4, but5, but6, but7, but8, but9 };

    foreach (var button in buttons)
        button.Text = string.Empty;

    checkedCount = 0;
}

Note: I didn't replace but{1-9} with array, as it would make the code incompatible with what OP currently has.

Here is the extra bit :

Button[] cells = new Button[9];
byte checkedCount = 0;

void Main()
{
    var board = new TableLayoutPanel();
    board.ColumnCount = 3;
    board.RowCount = 3;

    for (int i = 0; i < 9; i++)
    {
        cells[i] = new Button();
        cells[i].Click += Button_OnClick;
        cells[i].Text = i.ToString();

        board.Controls.Add(cells[i], i / 3, i % 3);
    }

    board.Dump();
}

Finally run the source code through this :

Regex.Replace(sourceCode, @"but(?<id>\d)", m => $"buttons[{ int.Parse(m.Groups["id"].Value) - 1 }]")
\$\endgroup\$
  • \$\begingroup\$ Cool! I like this answers, many of your improvements are similar to mine but in real code. Well done. \$\endgroup\$ – Caridorc Aug 23 '15 at 19:15

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.