Search code examples
pythonalgorithmtic-tac-toeminimax

python - minimax algorithm for tic tac toe updating the board by filling every empty space with the same symbol


I have been trying to improve my understanding of python by programming small games, such as tic tac toe. I created a copy of the game from scratch, but then went off to find a way of creating a reasonable ai for the game. I decided to use minimax to achieve this goal. I found a tutorial for the algorithm and attempted to mould it for the game.

However when the code is run, on the ai's first turn. Every empty space left on the board is filled with the same symbol, forcing a win, making the game unplayable.

As I was unfamiliar and inexperienced with the algorithm, I took the code from a youtube tutorial and edited it so that it was functional for the game.

Here is the code

import random
import time
import sys
from sys import maxsize




#converts numbers from the board to X O and "" for printing out the board 
def numToSymbol(board):
    visual = board
    for i in range(0, 9):
        if visual[i] == -1:
            visual[i]= "X"
        elif visual[i] == 1:
            visual[i] = "O"
        elif visual[i] == 0:
            visual[i] = " "
    return visual


def symbolToNum(visual):
    board = visual
    for i in range(0, 9):
        if board[i] == "X":
            board[i] = -1
        elif board[i] == "O":
            board[i] = 1
        elif board[i] == " ":
            board[i] = 0
    return board


# prints the board, first coverting numbers throught numtoSymbol
def drawBoard():
    visual = numToSymbol(board)
    print(
        "|"+ str(visual[0]) + "|" + str(visual[1]) + "|" + str(visual[2]) + "| \n"
        "-------\n"
        "|"+ str(visual[3]) + "|" + str(visual[4]) + "|" + str(visual[5]) + "| \n"
        "-------\n"
        "|"+ str(visual[6]) + "|" + str(visual[7]) + "|" + str(visual[8]) + "| \n"
        "-------")
    symbolToNum(visual)


# checks the possible options for 3 in a row and if any are all held by one side returns true
def checkWin(board, winConditions, ai):
    if ai == True:
        win = 3
    if ai == False:
        win = -3
    for row in winConditions:
        checkA = board[row[0]]
        checkB = board[row[1]]
        checkC = board[row[2]]
        if checkA + checkB + checkC == int(win):
           return True

    return False

def checkDraw(board):
    emptyTile = 9
    for tile in board:
        if tile != 0:
            emptyTile -= 1
    if emptyTile == 0:
        completion(2)

def availableMoves(board, bonus):
    availableSlots = []
    for position in range(0,9):
        if board[position] == 0:
            availableSlots.append(position + bonus)
    return availableSlots



def playerTurn(board, remainingTurns):
    board = board
    checkDraw(board)
    visual =drawBoard()
    print("pick one of the available slots \n")
    availableSlots = availableMoves(board, 1)
    print(availableSlots)
    choice = int(input('Select a tile from those shown above:\n')) -1
    while  True:    
        if 0 <= choice <= 8:
            if board[choice] == 0:
                board[choice] = -1
                break
            else:
                choice = int(input('Select a tile from those shown above which has not already been selected:\n')) -1
        else:
            choice = int(input('Select a tile from those shown above:\n')) -1
    visual = drawBoard()
    if checkWin(board, winConditions, False) == True:
        completion(0)
    else:
        aiTurn(board, remainingTurns - 1)


class Node(object):

    def __init__(self,  remainingTurns, playerNum, board, value = 0):
        self.depth = remainingTurns
        self.playerNum = playerNum
        self.board = board
        self.value = value
        self.moves = availableMoves(self.board, 0)
        self.children = []
        self.createChildren()

    def availableMoves(self, board):
        self.availableSlots = []
        for position in range(0,9):
            if board[position] == 0:
                availableSlots.append(position)
        return self.availableSlots

    def winCheck(self, state, playerNum):
        self.winConditions = [[0, 1, 2],[3, 4, 5],[6, 7, 8],[0, 3, 6],[1, 4, 7],[2, 5, 8],[0, 4, 8],[2, 4, 6]]
        self.win = 3
        for row in self.winConditions:
            self.checkA = self.state[row[0]]
            self.checkB = self.state[row[1]]
            self.checkC = self.state[row[2]]
            if self.checkA + self.checkB + self.checkC == int(self.win * self.playerNum):
               return True
        return False

    def createChildren(self):
        if self.depth > 0:
            for i in self.moves:
                self.state = self.board
                self.state[i] = self.playerNum
                print(board)
                self.children.append(Node(self.depth - 1, self.playerNum * -1, self.state, self.realVal(self.state, self.playerNum) ))


    def realVal(self, value, playerNum):
        value = self.winCheck(self.state, self.playerNum)
        if value == True:
            return maxsize * self.playerNum
        else:
            return 0




def minimax(node, depth, playerNum):
    if depth == 0 or abs(node.value) == maxsize:
        return node.value

    bestValue = maxsize * -playerNum

    for i in range(len(node.children)):
        child = node.children[i]
        val = minimax(child, depth - 1, -playerNum)
        if (abs(maxsize * playerNum - val) < abs(maxsize * playerNum - bestValue)):
            bestValue = val
    return bestValue


def aiTurn(board, remainingTurns):
    checkDraw(board)
    print('Waiting for ai')
    time.sleep(2)
    board = board
    state = board
    currentPlayer = 1
    tree = Node(remainingTurns, currentPlayer, state)
    bestChoice = -100
    bestValue = maxsize * -1
    for i in range(len(tree.children)):
        n_child = tree.children[i]
        i_val = minimax(n_child, remainingTurns, -currentPlayer,)
        if (abs(currentPlayer * maxsize - i_val) <= abs(currentPlayer * maxsize - bestValue)):
            bestValue = i_val
            bestChoice = i
    print(bestChoice)
    print(board)
    #choice = minimax(tree, remainingTurns, 1)
    board[bestChoice] = 1
    print(board)
    if checkWin(board, winConditions, True) == True:
        completion(1)
    else:
        playerTurn(board, remainingTurns - 1)



def completion(state):
    if state == 1:
        print('The computer has won this game.')
        choice = str(input('press y if you would like to play another game; Press n if you would not \n'))
        if choice == 'y' or choice == 'Y':
            playGame()
        else:
            sys.exit()
    if state == 0:
        print('you win!')
        choice = str(input('press y if you would like to play another game; Press n if you would not \n'))
        if choice == 'y' or choice == 'Y':
            playGame()
        else:
            sys.exit()
    if state == 2:
        print('you drew')
        choice = str(input('press y if you would like to play another game; Press n if you would not \n'))
        if choice == 'y' or choice == 'Y':
            playGame()
        else:
            sys.exit()        




def playGame():
    global board
    board = [0,0,0,0,0,0,0,0,0]

    global winConditions
    winConditions = [
                [0, 1, 2],
                [3, 4, 5],
                [6, 7, 8],
                [0, 3, 6],
                [1, 4, 7],
                [2, 5, 8],
                [0, 4, 8],
                [2, 4, 6]
                ]


    global remainingTurns
    remainingTurns = 9
    playerFirst = random.choice([True, False])

    if playerFirst == True:
        playerTurn(board, remainingTurns)
    else:
        aiTurn(board, remainingTurns)


if __name__ == '__main__':
    playGame() 

This is the output from the above code

[1, 0, 0, 0, 0, 0, 0, 0, 0]
[1, -1, 0, 0, 0, 0, 0, 0, 0]
[1, -1, 1, 0, 0, 0, 0, 0, 0]
[1, -1, 1, -1, 0, 0, 0, 0, 0]
[1, -1, 1, -1, 1, 0, 0, 0, 0]
[1, -1, 1, -1, 1, -1, 0, 0, 0]
[1, -1, 1, -1, 1, -1, 1, 0, 0]
[1, -1, 1, -1, 1, -1, 1, -1, 0]
[1, -1, 1, -1, 1, -1, 1, -1, 1]
[1, -1, 1, -1, 1, -1, 1, -1, -1]
[1, -1, 1, -1, 1, -1, 1, 1, -1]
[1, -1, 1, -1, 1, -1, 1, 1, 1]
[1, -1, 1, -1, 1, -1, -1, 1, 1]
[1, -1, 1, -1, 1, -1, -1, -1, 1]
[1, -1, 1, -1, 1, -1, -1, -1, -1
[1, -1, 1, -1, 1, 1, -1, -1, -1]
[1, -1, 1, -1, 1, 1, 1, -1, -1]
[1, -1, 1, -1, 1, 1, 1, 1, -1]
[1, -1, 1, -1, 1, 1, 1, 1, 1]
[1, -1, 1, -1, -1, 1, 1, 1, 1]
[1, -1, 1, -1, -1, -1, 1, 1, 1]
[1, -1, 1, -1, -1, -1, -1, 1, 1]
[1, -1, 1, -1, -1, -1, -1, -1, 1
[1, -1, 1, -1, -1, -1, -1, -1, -
[1, -1, 1, 1, -1, -1, -1, -1, -1
[1, -1, 1, 1, 1, -1, -1, -1, -1]
[1, -1, 1, 1, 1, 1, -1, -1, -1]
[1, -1, 1, 1, 1, 1, 1, -1, -1]
[1, -1, 1, 1, 1, 1, 1, 1, -1]
[1, -1, 1, 1, 1, 1, 1, 1, 1]
[1, -1, -1, 1, 1, 1, 1, 1, 1]
[1, -1, -1, -1, 1, 1, 1, 1, 1]
[1, -1, -1, -1, -1, 1, 1, 1, 1]
[1, -1, -1, -1, -1, -1, 1, 1, 1]
[1, -1, -1, -1, -1, -1, -1, 1, 1
[1, -1, -1, -1, -1, -1, -1, -1,
[1, -1, -1, -1, -1, -1, -1, -1,
[1, 1, -1, -1, -1, -1, -1, -1, -
[1, 1, 1, -1, -1, -1, -1, -1, -1
[1, 1, 1, 1, -1, -1, -1, -1, -1]
[1, 1, 1, 1, 1, -1, -1, -1, -1]
[1, 1, 1, 1, 1, 1, -1, -1, -1]
[1, 1, 1, 1, 1, 1, 1, -1, -1]
[1, 1, 1, 1, 1, 1, 1, 1, -1]
[1, 1, 1, 1, 1, 1, 1, 1, 1]
8
[1, 1, 1, 1, 1, 1, 1, 1, 1]
[1, 1, 1, 1, 1, 1, 1, 1, 1]
The computer has won this game.

Any suggestions?


Solution

  • Problem lies when you init your node you just copy the ref of board:

    self.board = board
    

    whereas you should copy the values

    self.board = board[::]
    

    Else all your Node objects refer to the same board: the global one.