Python 2: 399 401 349 333 317 370 bytes
2x Bug Fix: credit to l4m2
-52 chars: credit to undergroundmonorail
-16 chars: credit to Jonathan Frech
-26 chars: credit to user202729
def f(b):
t=4,9,2,3,5,7,8,1,6;n=lambda k:[t[i]for i,j in enumerate(b)if j==k];p,o,a,I=n(2),n(1),n(0),t.index
for i in p:
for j in p:
for k in a:
if i+j+k==15and-j+i:return I(k)
for i in o:
for j in o:
for k in a:
if i+j+k==15and-j+i:return I(k)
for i in 9,3,7,1:
if i in a and 5 in p:return I(i)
for i in 5,4,2,8,6:
if i in a:return I(i)
return I(a[0])
Try it Online!
On the first day of a linear algebra course I took last semester, my astute graduate student instructor proposed that if you represent the tic-tac-toe board as the the matrix:
4 | 9 | 2
--+---+--
3 | 5 | 7
--+---+--
8 | 1 | 6
then getting three in a row is equivalent to picking three numbers in the range [1,9] that add up to 15. This answer exploits this idea. The function takes a list containing nine numbers representing the board. 0 indicates an empty space, 1 is occupied by the opponent, and 2 represents a previous play made by the program. The first 3 lines figure out what numbers the program has picked (p), the opposition has picked (o), and are still available (a). It then looks through the available numbers and sees if any of them, combined with two numbers it has already picked add to fifteen. If it does, it will pick that square and win. If there are no immediate winning moves, it will check to see if the opponent can win using the same method. If they can, it will take their winning square. If there is neither a winning nor blocking move available, it will move in a corner. This prevents a fools mate:
- - -
- X -
- - -
- O - # Bad Move
- X -
- - -
- O X
- X -
- - -
- O X
- X -
O - -
- O X
- X -
O - X
If none of these situations occur, it will choose a square arbitrarily. The function outputs a number [0,8] representing the 0 indexed square chosen by the algorithm.
Edit: The algorithm now prioritizes the center over the diagonal, which will prevent another fools mate possibility pointed out by l4m2 and related strategies.
Edit: To clarify, the function takes in a board in the form of an array and outputs a move as an integer on [0,8]. Because this I/O strategy is so clunky , here's a wrapper script that makes it more interactive. It takes a single command line argument, which should be 1 if the player goes first, and 0 if the program goes first.
import sys
def f(b):
t=4,9,2,3,5,7,8,1,6;n=lambda k:[t[i]for i,j in enumerate(b)if j==k];p,o,a,I=n(2),n(1),n(0),t.index
for i in p:
for j in p:
for k in a:
if i+j+k==15and-j+i:return I(k)
for i in o:
for j in o:
for k in a:
if i+j+k==15and-j+i:return I(k)
for i in 9,3,7,1:
if i in a and 5 in p:return I(i)
for i in 5,4,2,8,6:
if i in a:return I(i)
return I(a[0])
board = [0,0,0,0,0,0,0,0,0]
rep = {0:"-",1:"X",2:"O"}
turn = int(sys.argv[1])
while True:
for i in range(3):
print rep[board[i*3]]+" "+rep[board[i*3+1]]+" "+rep[board[i*3+2]]
print
if turn:
move = int(raw_input("Enter Move [0-8]: "))
else:
move = f(board)
board[move] = turn+1
turn = (turn+1)%2
we can assume that all previous moves of the 2nd player were also played by our engine
\$\endgroup\$