17

I've already read many Tic Tac Toe topics on StackOverflow. And I found the strategy on Wikipedia is suitable for my presentation project:

A player can play perfect tic-tac-toe if they choose the move with the highest priority in the following table[3].

1) Win: If you have two in a row, play the third to get three in a row.

2) Block: If the opponent has two in a row, play the third to block them.

3) Fork: Create an opportunity where you can win in two ways.

4) Block Opponent's Fork:

Option 1: Create two in a row to force the opponent into defending, as long as it doesn't result in them creating a fork or winning. For example, if "X" has a corner, "O" has the center, and "X" has the opposite corner as well, "O" must not play a corner in order to win. (Playing a corner in this scenario creates a fork for "X" to win.)

Option 2: If there is a configuration where the opponent can fork, block that fork.

5) Center: Play the center.

6) Opposite Corner: If the opponent is in the corner, play the opposite corner.

7) Empty Corner: Play an empty corner.

8) Empty Side: Play an empty side.

I've followed these step, and the computer never loses. However, the way it attacks is not perfect. Because I have no clue how to do step 3. Here is what I do in step 3: scan every cell, check if put token on that cell creates a fork, then put it there.

private void step3() // Create Fork.
{
    int[] dummyField = (int[])field.Clone();
    // Try Level 1 Dummy
    for (int i = 0; i < 9; i++)
    {
        if (dummyField[i] != 0) continue;
        dummyField[i] = 2;
        if (countFork(dummyField, 2) >= 2)
        {
            nextCell = i;
            return;
        }
        dummyField[i] = 0;
    }

}

Please give me some advice about this step.

EDIT1: The count fork will count how many forks that computer has (computer's tokens is 2, player tokens is 1, because I used that method for step 4 too, so there is a parameter for token in countFork function).

EDIT2: The reason why I say it is not perfect is this (CPU goes first, and its cells are blue, human cells are red). enter image description here As you can see, if I put in the top-side cell, the computer wins. But if I put in the right-side cell, it's a tie, although the computer can still win.

EDIT3: Don't know why, but I commented out the step 3, and the computer plays... perfectly! I'm really surprised! Here is my countFork function (I need to port this code to Alice, which doesn't support 2-dimension array, so I use getNumberFromXY to convert 2-dimension array into 1-dimension):

private int countFork(int[] field, int token)
{
    int result = 0;

    // Vertical
    int cpuTokenCount;
    int spareCell;
    for (int x = 0; x < 3; x++)
    {
        cpuTokenCount = 0;
        spareCell = -1;
        for (int y = 0; y < 3; y++)
        {
            if (field[getNumberFromXY(x, y)] == token)
                cpuTokenCount++;
            else if (field[getNumberFromXY(x, y)] == 0)
                spareCell = getNumberFromXY(x, y);
        }
        if (cpuTokenCount == 2 && spareCell != -1) result++;
    }

    // Horizontal
    for (int y = 0; y < 3; y++)
    {
        cpuTokenCount = 0;
        spareCell = -1;
        for (int x = 0; x < 3; x++)
        {
            if (field[getNumberFromXY(x, y)] == token)
                cpuTokenCount++;
            else if (field[getNumberFromXY(x, y)] == 0)
                spareCell = getNumberFromXY(x, y);
        }
        if (cpuTokenCount == 2 && spareCell != -1) result++;
    }

    // Top-Left To Lower-Right Diagonal
    cpuTokenCount = 0;
    spareCell = -1;
    for (int i = 0; i < 3; i++)
    {
        if (field[getNumberFromXY(i, i)] == token)
            cpuTokenCount++;
        else if (field[getNumberFromXY(i, i)] == 0)
            spareCell = getNumberFromXY(i, i);
    }
    if (cpuTokenCount == 2 && spareCell != -1) result++;

    // Top-Right To Lower-Left Diagonal
    cpuTokenCount = 0;
    spareCell = -1;
    for (int i = 0; i < 3; i++)
    {
        if (field[getNumberFromXY(2 - i, i)] == token)
            cpuTokenCount++;
        else if (field[getNumberFromXY(2 - i, i)] == 0)
            spareCell = getNumberFromXY(2 - i, i);
    }
    if (cpuTokenCount == 2 && spareCell != -1) result++;

    return result;
}

EDIT4: FIXED the bug according to soandos, and updated the code at EDIT 3, now it works perfectly!

Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
Luke Vo
  • 17,859
  • 21
  • 105
  • 181

1 Answers1

9

I am not sure that it is the most elegant way to do it, but here is a two step way of looking at forks.

If the computer cannot win next turn, and it is not the first or second turn, a fork might be possible (this does not deal with creating the setup for a fork, just finding a fork).

For each of the cells that are empty, fill it, then run your step 1 function (sees if there are two in a row). If it finds two places, congrats, you have a fork. If not, you don't.

soandos
  • 4,978
  • 13
  • 62
  • 96
  • That's how I'm doing in my code! The function `countFork` is to count how many ways to win if I put a token at a cell. The result, however, as I notice in Edit 3, it will be better if there is no step 3! – Luke Vo Nov 28 '11 at 05:44
  • So this process has a bug in your code, or you are looking for a different way? – soandos Nov 28 '11 at 05:46
  • I will accept anything that makes computer play better. I posted my countFork function, so you can check if there is a bug. – Luke Vo Nov 28 '11 at 05:56
  • Thank you, I fixed the bug (and updated in my question), now the computer is the champion! – Luke Vo Nov 28 '11 at 06:12
  • btw, it is possible to optimize the loops so that the horizontal and vertical checks are done at the same time. It is not a significant difference (its a 3x3 after all) but just seems like something to do. – soandos Nov 28 '11 at 06:14
  • 1
    It's just visually shorter, but the computer executes exactly the same amount of code lines. I shouldn't make the code more complex by merge 2 loops. – Luke Vo Nov 28 '11 at 06:19
  • isn't minimax the right way to do this? The forking move, if available, will be highest scoring and will be picked. The game tree is small enough that the entire tree can be searched from move 1. – Kevin Nov 28 '11 at 16:20
  • @Kevin: After ported my code to Alice (my presentation project must be on Alice), it runs extremely slow! It's because of Alice, so I must use the simpliest algorithm for this game. – Luke Vo Nov 28 '11 at 17:24
  • At step 3 and 4 (create fork and block fork). It's because of Alice, because the code exactly the same as what I've done in C#. I've tested alone loop code, and Alice run it extremely slow. – Luke Vo Nov 29 '11 at 03:27
  • Good, the CPU put the token instantly. – Luke Vo Nov 29 '11 at 11:00