# Simplest possible Tic Tac Toe AI in Python

Last week I challenged myself to create the smallest possible code for a Tic Tac Toe game with an AI as opponent. Smallest possible in regards to say least number of characters used. The requirements on the game are as follows:

• A "nice" playing experience (ability to get user input and print the board after every move)
• Handling wrong input data without crashing.
• Having an unbeatable AI as opponent.
• The ability to play again or exit after game is over

The result is this code:

def p(b):
c = [' ' if i == 0 else 'X' if i == -1 else 'O' if i == 1 else i for i in b]
[print(f'\n{c[r*3:3+r*3]}') if r == 0 else print(c[r*3:3+r*3]) for r in range(3)]
def e(b, t):
for p in ([0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]):
if b[p[0]] == b[p[1]] == b[p[2]] == t: return 1
def n(b, d, t):
if e(b, t): return 0, (9+d)
if e(b, -t): return 0, -(9 + d)
if 0 not in b: return 0, 0
x = -20
for m in [i for i in range(9) if not b[i]]:
b[m] = t
s = -n(b, d - 1, -t)[1]
b[m] = 0
if s > x: x, y = s, m
return y, x
def r():
b, w = [0]*9, -1
p(b)
while 1:
if w == -1 and not (e(b, w) or e(b, -w)) and 0 in b:
while 1:
if u.isnumeric():
u = int(u)-1
if 0 <= u < 9 and not b[u]:
b[u], w = -1, w*-1
p(b)
break
elif w == 1 and not (e(b, w) or e(b, -w)) and 0 in b:
m, s = n(b, 8, 1)
b[m], w = 1, w*-1
p(b)
else:
f = 'You won!' if e(b, -1) else 'AI won!' if e(b, 1) else 'Game drawn!'
if input(f'\n{f} Do you want to play again (y/n)? ') != 'y': break
r()
break
r()


And with a bit of commentary to easier understand what is going on:

# Printing the board on the "standard" format with X:s and O:s instead of -1, 0, 1.
def print_board(board):
new_board = [' ' if i == 0 else 'X' if i == -1 else 'O' if i == 1 else i for i in board]
[print(f'\n{new_board[row*3:3+row*3]}') if row == 0 else print(new_board[row*3:3+row*3]) for row in range(3)]  # Print with new line to get nicer format

# Evaluates the board
def evaluate(boart, turn):
for pos in ([0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]):  # Go through all possible winning lines
if board[pos[0]] == board[pos[1]] == board[pos[2]] == turn: return 1  # Return 1 if player turn has 3 in a row

# Recursive negamax function which goes through the entire game tree.
# Depth d is used in the returned scores to get shortest possible route to victory.
def negamax(board, depth, turn):
if evaluate(board, turn): return 0, (9+depth)  # Return positive score if maximizing player
if evaluate(board, -turn): return 0, -(9 + depth)  # Return negative score if minimizing player wins
if 0 not in board: return 0, 0  # Drawn game, return 0
best_score = -20  # Initiate with less than smallest possible score
for move in [i for i in range(9) if not board[i]]:  # Go through all empty squares on board
board[move] = turn  # Make move
score = -negamax(board, depth - 1, -turn)[1]  # Recursive call to go through all child nodes
board[move] = 0  # Unmake the move
if score > best_score: best_score, best_move = score, move  # If score is larger than previous best, update score
return best_move, best_score  # Return the best move and its corresponding score

# Main game loop.
def run():
board, turn = [0]*9, -1  # Initiate board and turn to move (-1 is human to start, 1 AI to start)
print_board(board)
while 1:  # Loop until game is over
if turn == -1 and not (evaluate(board, turn) or evaluate(board, -turn)) and 0 in board:  # Human turn if game is not over and there are places left on board
while 1:  # Loop until a valid input is given
if user_input.isnumeric():  # Find if a number is entered
u = int(user_input)-1  # Get it on the right board format (square 1 corresponds to array[0])
if 0 <= u < 9 and not board[u]:  # Check if number is in the correct range and on an empty square
board[u], turn = -1, turn*-1  # Make move and change turn
print_board(board)
break
elif turn == 1 and not (evaluate(board, turn) or evaluate(board, -turn)) and 0 in b:  # Ai turn if game is not over and there are places left on board
move, score = negamax(board, 8, 1)  # Run Negamax loop to get a best move and the score
board[move], turn = 1, turn*-1  # Make move and change turn
print_board(board)
else:  # This means the game is over or board is full
text = 'You won!' if evaluate(board, -1) else 'AI won!' if evaluate(board, 1) else 'Game drawn!'  # Check who won or if there is a draw
if input(f'\n{text} Do you want to play again (y/n)? ') != 'y': break  # Ask to play again, break if answer is not 'y'
run()  # Run game again if answer is 'y'
break
run()  # Run the game loop


My question is if there are any other approaches that are simpler in terms of number of characters for a functioning game with the given requirements above? Of course the input/output text to console can be shorter, but I think that doesn't count :)

In terms of readability and PEP 8 style there are of course lots of things to improve, I wanted to keep the code to a reasonable minimum of lines.

I hope this type of question is allowed here, otherwise please remove it.

EDIT: Example game as proposed by user "superb rain" in the comments:

['  ', ' ', ' ']\
[' ', ' ', ' ']\
[' ', ' ', ' ']

[' ', ' ', ' ']\
[' ', 'X', ' ']\
[' ', ' ', ' ']

['O', ' ', ' ']\
[' ', 'X', ' ']\
[' ', ' ', ' ']

['O', ' ', ' ']\
['X', 'X', ' ']\
[' ', ' ', ' ']

['O', ' ', ' ']\
['X', 'X', 'O']\
[' ', ' ', ' ']

['O', 'X', ' ']\
['X', 'X', 'O']\
[' ', ' ', ' ']

['O', 'X', ' ']\
['X', 'X', 'O']\
[' ', 'O', ' ']

['O', 'X', 'X']\
['X', 'X', 'O']\
[' ', 'O', ' ']

['O', 'X', 'X']\
['X', 'X', 'O']\
['O', 'O', ' ']

['O', 'X', 'X']\
['X', 'X', 'O']\
['O', 'O', 'X']

Game drawn! Do you want to play again (y/n)?

• We cannot really help you in reducing the number of characters needed. That would be code-golfing and is something we have decided we can not really do here. However, there is our sister site Code Golf, which also has a nice guide on some general tips on golfing Python. – Graipher Jan 18 at 10:28
• I find it hard to review code with one letter variables everywhere. You might want to consider using clear, concise names. – theProgrammer Jan 18 at 10:29
• @theProgrammer, I totally understand, I did it to save on characters to get a measurement of "minimal". The commented version is now updated with better naming, sorry if I missed a place or made any typo. – eligolf Jan 18 at 11:27
• Please don't update the question after it has been answered, everyone needs to see what the reviewer saw. Please read What should I do when someone answers?. – pacmaninbw Jan 18 at 13:38
• @pacmaninbw, I put an EDIT in there so it should be clear to everyone I think. – eligolf Jan 18 at 15:30

Your

def evaluate(boart, turn):
for pos in ([0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]):
if board[pos[0]] == board[pos[1]] == board[pos[2]] == turn: return 1


could be

def evaluate(boart, turn):
for i, j, k in [0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]:
if board[i] == board[j] == board[k] == turn: return 1


That is, for indices, single letters i, j and k are common and totally fine. And the tuple doesn't need parentheses.

You could also define each line with just two values instead of three. Like I did in mine that I wrote a while ago:

def show():
for i in range(0, 9, 3):
print(*board[i : i+3])

def possible_moves(board):
return (i for i in range(9) if isinstance(board[i], int))

def move(board, p, i):
return board[:i] + [p] + board[i+1:]

def won(board):
for i, d in (0, 1), (3, 1), (6, 1), (0, 3), (1, 3), (2, 3), (0, 4), (2, 2):
if board[i] == board[i + d] == board[i + 2*d]:
return True

def value(board, p, q):
if won(board):
return -1
return -min((value(move(board, p, i), q, p)
for i in possible_moves(board)),
default=0)

def best_move():
return min(possible_moves(board),
key=lambda i: value(move(board, 'O', i), 'X', 'O'))

board = list(range(1, 10))
try:
while True:
show()
board = move(board, 'X', int(input('your move: ')) - 1)
if won(board):
raise Exception('You won!')
if board.count('X') == 5:
raise Exception('Draw!')
board = move(board, 'O', best_move())
if won(board):
raise Exception('You lost!')
except Exception as result:
show()
print(result)


Demo game:

1 2 3
4 5 6
7 8 9
O 2 3
4 X 6
7 8 9
O 2 X
4 X 6
O 8 9
O 2 X
X X O
O 8 9
O O X
X X O
O X 9

• @eligolf Yeah, I guess some documentation would help. I think the lack thereof is one of the reasons I hadn't posted this yet (I had considered making it a question similar to yours). Main thing would be to note that the value function returns -1 if the board is a lost state (because the game is already won, in the last move made), 1 if it's a winning state (you can force a win from there), or 0 if it's a draw state (neither player will win if both play perfectly). – superb rain Jan 18 at 13:29