tictactoe - revisited

I made a tictactoe game a while back, and for basic functionality, it seemed to work fine. I'm now trying to find a way to teach it how to never lose, without explicitly giving it all the possible board positions. Here is one such position which I am concerned about.

X  2  3
4  O  6
7  8  O


The goal here is for the computer as X to not lose. For those who can't be bothered analysing, it needs to go in either square 3 or square 7 (2/4 gets forked when the player blocks, 6 gets forked if player goes to square 7 or 8 next and 8 gets forked if player goes to square 3 or 6. The problem with my function is it is (wrongly) going to square 6 here, something I know is a logic error, as it quite rightly sees that if the computer skipped its turn and the player went in square 6, it would be forked between the middle row and the right column.

This is an exercise (not homework!) in trying to get computers to think ahead, hence why I don't want to explicitly tell it to go to 3 or 7 in this position.
I'm trying to generalise as much as possible so the same logic can be applied to other games. My latest thought was to make it take whatever move gives them tempo, but that doesn't appear to be working.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
int LookForWin(char board[],char player[],int play, bool fork=false)
{
    int wincombos[8][3]={{0,1,2},{3,4,5},{6,7,8},{0,3,6},{1,4,7},{2,5,8},{0,4,8},{2,4,6}};
    vector<int>possiblemoves; // this is to store the possibilities if it needs to look for a fork.
    for (int i=0; i<8; i++)
    {
        int pieces=0;
        int emptysquares=0;
        for (int j=0; j<3; j++)
        {
            if (board[wincombos[i][j]]==player[play])
            {
                pieces++;
            }
            else if (board[wincombos[i][j]]!='X' && board[wincombos[i][j]]!='O')
            {
                emptysquares++;
            }
        }
        if (pieces==2 && emptysquares==1)
        {
            for (int j=0; j<3; j++)
            {
                if (board[wincombos[i][j]]!='X' && board[wincombos[i][j]]!='O')
                {
                    return wincombos[i][j];
                }
            }
        }
        if (fork && pieces==1 && emptysquares==2)
        {
            // find a line with the same property
            for (int k=i+1; k<8; k++)
            {
                pieces=0;
                emptysquares=0;

                for (int j=0; j<3; j++)
                {
                    if (board[wincombos[k][j]]==player[play])
                    {
                        pieces++;
                    }
                    else if (board[wincombos[k][j]]!='X' && board[wincombos[k][j]]!='O')
                    {
                        emptysquares++;
                    }
                }
                if (pieces==1 && emptysquares==2)
                {
                    // we have now found another such line, check if they intersect and if so, if the common square is empty
                    for (int j=0; j<3; j++)
                    {
                        for (int m=0; m<3; m++)
                        {
                            if (wincombos[i][j]==wincombos[k][m] && board[wincombos[i][j]]!='X' && board[wincombos[i][j]]!='O')
                            {
                                //cout << i << " " << j << endl;
                                possiblemoves.push_back(i);
                                possiblemoves.push_back(j);
                                //return wincombos[i][j];
                            }
                        }
                    }
                }
            }
        }
    }
    cout << possiblemoves.size() << endl;
    if (possiblemoves.size()==2)
    {
        // there is only 1 possible threat, return it
        return wincombos[possiblemoves[0]][possiblemoves[1]];
    }
    else if (possiblemoves.size()>2)
    {
        //check if move would give it tempo by threatening a line
        for (unsigned int j=0; j<possiblemoves.size(); j+=2) // declared as unsigned to remove warning
        {
            for (unsigned int m=0; m<8; m++)
            {
                bool linehaspiece=false;
                bool linehasmove=false;
                bool linehasempty=false;
                for (unsigned int p=0; p<3; p++)
                {
                    if(wincombos[m][p]==player[1])
                        linehaspiece=true;
                    else if(m==j && p==j+1)
                        linehasmove=true;
                    else if(wincombos[m][p]!=player[0])
                        linehasempty=true;
                }
                if (linehaspiece&&linehasempty&&linehasmove)
                    return wincombos[j][j+1];
            }
        }
    }
    possiblemoves.erase(possiblemoves.begin(),possiblemoves.end());
    return 9;
}


If you want to see the full code (about 10k characters), I have copied a version of it at https://docs.google.com/document/d/1dag1-9lkb3tiJG9eiW2uiIUXl1KSNEMKFY44ZgRHveE/edit?usp=sharing
The problem with my function is it is (wrongly) going to square 6 here, something I know is a logic error, as it quite rightly sees that if the computer skipped its turn and the player went in square 6, it would be forked between the middle row and the right column.


You should not be considering O's moves if you skip X's. You should be considering O's options for each possible X move. If any X move results in a possible O fork, then that move isn't viable.
It's a little more complicated than that, in the example I gave, if the player insists on forking it, then it can't do anything to stop it (if it goes in 3, player can go in 8, and similarly 7 can get met with 6), it's just that going in 3 or 7 means that if they do fork it, the computer would complete its line before the player does. Having said that, I'll try going that route later.

The angle I was looking at was to find all possible threats, and then find the move to best preempt it, hence the imaginary pretending that the computer skipped its turn.
It's a little more complicated than that, in the example I gave, if the player insists on forking it, then it can't do anything to stop it


An X move to 7 requires an O to 5. An X move to 3 requires an O to 2. Neither is a fork. You must consider defense before attack when determining what moves are viable.
I think I found where my problem is, a bit mystified about why it was going wrong the way my debugging was suggesting, but basically 2 of the conditions for filtering out the possibilities were wrong, but in a way which looking at them I thought should never have been true, but were always true.

if(wincombos[m][p]==player[1])
else if(wincombos[m][p]!=player[0])

the 2 conditions above seem to be comparing an int with a char so I'm a bit confused as to why it was always true (I just about get why there wasn't a warning), I switched the wincombos[m][p] to board[wincombos[m][p]] so I'm now comparing char to char, which seems to be working now, at least in the 20 or so games I've played against it since the change.

I also changed the vector push to be the square itself rather than its position in the wincombos array.
Last edited on
Topic archived. No new replies allowed.