HiveBrain v1.2.0
Get Started
← Back to all entries
patternpythonMinor

Optimizing a Tic-Tac-Toe AI

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
tacticoptimizingtoe

Problem

I built a Tic-Tac-Toe game in Python. The board is made of ASCII characters and it is the user versus an AI. The user chooses a number (1-9) and if the corresponding spot is open then either an 'X' or an 'O' will be placed there. I wrote it in Python 2.7, then upgraded to Python 3.4, so I made it compatible with that. I'm not sure if it is still compatible with Python 2.7. I think that I was able to clean up most of the code, but I feel like my AI code is a bit messy. The whole game is here on Github.

AI_turn.py

```
import random

def AI_turn_easy(board, possible_nums, AI_XO):

# generates a random, available space on the board and makes it an 'O'
randChoice = random.choice(possible_nums)
possible_nums.remove(randChoice)
board.spaces[randChoice] = AI_XO

return possible_nums

def AI_turn_hard(board, possible_nums, AI_XO):

# All of the possible winning configurations
possible_configs = [["1", "2", "3"],
["1", "4", "7"],
["1", "5", "9"],
["2", "5", "8"],
["3", "6", "9"],
["3", "5", "7"],
["4", "5", "6"],
["7", "8", "9"]]

# Shuffles the configurations so the computer doesn't run through the same order every time
random.shuffle(possible_configs)

# Check each configuration; if there is two of the three spaces equal each other, put an 'O' in the other space
for config in possible_configs:
if board.spaces[config[0]] == board.spaces[config[1]]:
if board.spaces[config[2]] in possible_nums:
possible_nums.remove(board.spaces[config[2]])
board.spaces[config[2]] = AI_XO
return possible_nums
elif board.spaces[config[0]] == board.spaces[config[2]]:
if board.spaces[config[1]] in possible_nums:
possible_nums.remove(board.spaces[config[1]])
board.spaces

Solution

You have a massive block or repeated code, instead make a function and call it two times:

def avoid_losing_and_win_if_possible(board):
    random.shuffle(POSSIBLE_CONFIGS)

    # Check each configuration; if there is two of the three spaces equal each other, put an 'O' in the other space
    for config in possible_configs:
        if board.spaces[config[0]] == board.spaces[config[1]]:
            if board.spaces[config[2]] in possible_nums:
                possible_nums.remove(board.spaces[config[2]])
                board.spaces[config[2]] = AI_XO
                return possible_nums
        elif board.spaces[config[0]] == board.spaces[config[2]]:
            if board.spaces[config[1]] in possible_nums:
                possible_nums.remove(board.spaces[config[1]])
                board.spaces[config[1]] = AI_XO
                return possible_nums
        elif board.spaces[config[1]] == board.spaces[config[2]]:
            if board.spaces[config[0]] in possible_nums:
                possible_nums.remove(board.spaces[config[0]])
                board.spaces[config[0]] = AI_XO
                return possible_nums
        else:
            pass
    return None

def AI_turn_hard(board, possible_nums, AI_XO):
    move = avoid_losing_and_win_if_possible(board)
    return move if move is not None else AI_turn_easy(board, possible_nums, AI_XO)

def AI_turn_impossible(board, possible_nums, AI_XO):
    move = avoid_losing_and_win_if_possible(board)
    if move: return move

    for space in random.shuffle(['5', '1', '7', '3', '9']):
        if space in possible_nums:
            possible_nums.remove(space)
            board.spaces[space] = AI_XO
            return possible_nums

    return AI_turn_easy(board, possible_nums, AI_XO)


Modularization: print("\n" * 500) is repeated many times in your code, you should name a small function:

def clear_screen():
    print('\n' * 500)


Keep indentation low: move the tic_tac_toe definition outside of main

Avoid magic numbers: 9 should be a constant NUMBER_OF_SQUARES, remember to use possible_nums = [str(i) for i in range (1,NUMBER_OF_SQUARES+1)] also 1.5 should be TIME_TO_WAIT.

Don't lag: time.sleep(1.5) why are you wilfully lagging?

Minor idiom: thing is None is better than thing == None

Shadow built-ins for better usability: board.print_board() is weird, modify your class in order to be able to call print(board) by shadowing __repr__

Again about modularity: A function like the following would make the code clearer and simpler to read.

def generate_end_message(win):
    if win == AI_XO:
        return "AI wins. You lose :("
    elif win == p1_XO:
        return "You win :) Congratulations!"
    elif win == "draw":
        return "It was a draw"


Use dictionaries: A dictionary would be less verbose here:

if diff == "E":
            possible_nums = AI_turn_easy(board, possible_nums, AI_XO)
        elif diff == "H":
            possible_nums = AI_turn_hard(board, possible_nums, AI_XO)
        elif diff == "I":
            possible_nums = AI_turn_impossible(board, possible_nums, AI_XO)

Code Snippets

def avoid_losing_and_win_if_possible(board):
    random.shuffle(POSSIBLE_CONFIGS)

    # Check each configuration; if there is two of the three spaces equal each other, put an 'O' in the other space
    for config in possible_configs:
        if board.spaces[config[0]] == board.spaces[config[1]]:
            if board.spaces[config[2]] in possible_nums:
                possible_nums.remove(board.spaces[config[2]])
                board.spaces[config[2]] = AI_XO
                return possible_nums
        elif board.spaces[config[0]] == board.spaces[config[2]]:
            if board.spaces[config[1]] in possible_nums:
                possible_nums.remove(board.spaces[config[1]])
                board.spaces[config[1]] = AI_XO
                return possible_nums
        elif board.spaces[config[1]] == board.spaces[config[2]]:
            if board.spaces[config[0]] in possible_nums:
                possible_nums.remove(board.spaces[config[0]])
                board.spaces[config[0]] = AI_XO
                return possible_nums
        else:
            pass
    return None


def AI_turn_hard(board, possible_nums, AI_XO):
    move = avoid_losing_and_win_if_possible(board)
    return move if move is not None else AI_turn_easy(board, possible_nums, AI_XO)


def AI_turn_impossible(board, possible_nums, AI_XO):
    move = avoid_losing_and_win_if_possible(board)
    if move: return move

    for space in random.shuffle(['5', '1', '7', '3', '9']):
        if space in possible_nums:
            possible_nums.remove(space)
            board.spaces[space] = AI_XO
            return possible_nums

    return AI_turn_easy(board, possible_nums, AI_XO)
def clear_screen():
    print('\n' * 500)
def generate_end_message(win):
    if win == AI_XO:
        return "AI wins. You lose :("
    elif win == p1_XO:
        return "You win :) Congratulations!"
    elif win == "draw":
        return "It was a draw"
if diff == "E":
            possible_nums = AI_turn_easy(board, possible_nums, AI_XO)
        elif diff == "H":
            possible_nums = AI_turn_hard(board, possible_nums, AI_XO)
        elif diff == "I":
            possible_nums = AI_turn_impossible(board, possible_nums, AI_XO)

Context

StackExchange Code Review Q#83324, answer score: 2

Revisions (0)

No revisions yet.