As user3386109 explained, the problem here is how many times you calculate everything. There are a few things that may help you out though, considerin an N-sized grid:
- a user cannot win if there are less than N of his symbols, so you can use that in the isGameOver function as a first check
- the first thing that you have to do is to prevent your opponent from winning if there's a chance that his next move is a winning one
- Keep track of how many "X" and "O" are in each row and column and in the two diagonals by incrementing counters every move. If there are N-1 of the same symbol, the next one will be a winning move for either you or your opponent.
- By doing so, you can easily tell which is the best move, because then:
- if you have a winning move you put the symbol there
- check your opponent, if he has N-1 symbols on the same row/column/diagonal you put it there
- if your opponent has more symbols than you on some place, you even out the place (that means +1 or +2, depending on who's starting the game)
- if that's not the case, you put your next symbol on the row/colum/diagonal where you have more symbols
- if you have the same number of symbols on some places, you just put it where your opponent has more symbols
- if you and your opponent are entirely even, just go for your own strategy (random would not be bad, I guess :-) )
Unless you really need it (for example as homework), I wouldn't use recursion for this one.
Just as a side note: I don't think it's good practice to have what is actually a boolean function return a string and then compare that with a fixed value. A true/false return value for the isGameOver function looks much better to me.