Search code examples
pythonminimaxalpha-beta-pruning

Loop over a list from the middle outwards


I am making a connect four AI and would like the AI to loop through the available moves from the middle and outwards because in connect four the middle moves are usually better and then the probability of alpha beta pruning happening is much higher.

For example if I gave it a array like this: [1,2,3,4,5]
It should loop though them like this 3,4,2,5,1

Or with a array like this [1,2,3,4,5,6]
It should loop though them like this 4,3,5,2,6,1

Thanks in advance

This is my code for connect four:

import os # os.system('cls') to clear screen 
import random
import time
import sys

class ConnectFour:
    def __init__(self):
        self.board = [[" ", " ", " ", " ", " ", " "," "], 
                      [" ", " ", " ", " ", " ", " "," "],
                      [" ", " ", " ", " ", " ", " "," "],
                      [" ", " ", " ", " ", " ", " "," "],
                      [" ", " ", " ", " ", " ", " "," "],
                      [" ", " ", " ", " ", " ", " "," "]]
        self.numberLine = [1,2,3,4,5,6,7]
        self.rowCount = 6
        self.columnCount = 7

    def printBoard(self, column=None, row=None):
        os.system('cls') # clears the screen
        terminalWidth = os.get_terminal_size().columns
        terminalHeight = os.get_terminal_size().lines

        playerToHighlighted = {
        'R': '\033[91;44mR\033[0m',
        'Y': '\033[93;44mY\033[0m' }

        print("\n"*round(terminalHeight/2 - (1 + self.columnCount))) # 
        
        highlightedPlayer = ''

        try:
            playerHold = self.board[row][column] 
            self.board[row][column] = 'H'
            highlightedPlayer = playerToHighlighted[playerHold]
        except:
            pass

        print(
        ((((
        (("  {}  |  {}  |  {}  |  {}  |  {}  |  {}  |  {}  ".format(*self.board[5])).center(terminalWidth)) + '\n' + 
        '\033[94m' + ("---------------------------------------").center(terminalWidth) + '\033[0m' + '\n' +
        (("  {}  |  {}  |  {}  |  {}  |  {}  |  {}  |  {}  ".format(*self.board[4])).center(terminalWidth)) + '\n' + 
        '\033[94m' + ("---------------------------------------").center(terminalWidth) + '\033[0m' + '\n' +
        (("  {}  |  {}  |  {}  |  {}  |  {}  |  {}  |  {}  ".format(*self.board[3])).center(terminalWidth)) + '\n' + 
        '\033[94m' + ("---------------------------------------").center(terminalWidth) + '\033[0m' + '\n' +
        (("  {}  |  {}  |  {}  |  {}  |  {}  |  {}  |  {}  ".format(*self.board[2])).center(terminalWidth)) + '\n' + 
        '\033[94m' + ("---------------------------------------").center(terminalWidth) + '\033[0m' + '\n' +
        (("  {}  |  {}  |  {}  |  {}  |  {}  |  {}  |  {}  ".format(*self.board[1])).center(terminalWidth)) + '\n' + 
        '\033[94m' + ("---------------------------------------").center(terminalWidth) + '\033[0m' + '\n' +
        (("  {}  |  {}  |  {}  |  {}  |  {}  |  {}  |  {}  ".format(*self.board[0])).center(terminalWidth)) + '\n' + 
        '\033[94m' + ("---------------------------------------").center(terminalWidth) + '\033[0m' + '\n' +
        ((("  {}  |  {}  |  {}  |  {}  |  {}  |  {}  |  {}  ").format(*self.numberLine)).center(terminalWidth))).replace('Y', '\033[93mY\033[0m')).replace('R', "\033[91mR\033[0m")).replace('|', '\033[94m|\033[0m')).replace('H', highlightedPlayer)  )
        
        try:
            self.board[row][column] = playerHold
        except:
            pass

    def clearBoard(self):
        self.board = [[" ", " ", " ", " ", " ", " "," "], 
                      [" ", " ", " ", " ", " ", " "," "],
                      [" ", " ", " ", " ", " ", " "," "],
                      [" ", " ", " ", " ", " ", " "," "],
                      [" ", " ", " ", " ", " ", " "," "],
                      [" ", " ", " ", " ", " ", " "," "]]
    
    def makeMove(self, position, player):
        for row in range(self.rowCount):
            if self.board[row][position] == " ":
                self.board[row][position] = player
                return row

    def availableMoves(self): # this might be broken
        moves = []
        for column in range(self.columnCount):
            if self.board[self.rowCount - 1][column] == ' ':
                moves.append(column)
        return moves

    def removeMove(self, row, column): 
        self.board[row][column] = " "    

    def checkWin(self): 
        for player in ('R', 'Y'): 
            for column in range(self.columnCount - 3): # Check vertical locations for win
                for row in range(self.rowCount):
                    if self.board[row][column] == player and self.board[row][column+1] == player and self.board[row][column+2] == player and self.board[row][column+3] == player:
                        return player

            for column in range(self.columnCount): # Check vertical locations for win
                for row in range(self.rowCount - 3):
                    if self.board[row][column] == player and self.board[row+1][column] == player and self.board[row+2][column] == player and self.board[row+3][column] == player:
                        return player
            
            for column in range(self.columnCount-  3): # Check positively sloped diaganols
                for row in range(self.rowCount - 3):
                    if self.board[row][column] == player and self.board[row+1][column+1] == player and self.board[row+2][column+2] == player and self.board[row+3][column+3] == player:
                        return player

            for column in range(self.columnCount-3): # Check negatively sloped diaganols
                for row in range(3, self.rowCount):
                    if self.board[row][column] == player and self.board[row-1][column+1] == player and self.board[row-2][column+2] == player and self.board[row-3][column+3] == player:
                        return player

    def whoWon(self):
        if self.checkWin() == 'R':
            return "Red"
        elif self.checkWin() == 'Y':
            return "Yellow"
        elif self.checkGameOver() == True:
            return "Nobody"


    def checkGameOver(self): 
        if self.checkWin() != None:
            return True
        for column in range(self.columnCount):
                if self.board[self.rowCount - 1][column] == " ":
                    return False
        return True

    def getMoveCount(self, player): # getStoneCount this can be better
        moveCount = 0
        for column in range(self.columnCount):
            for row in range(self.rowCount):
                if self.board[row][column] == player:
                    moveCount += 1
        return moveCount
    

    def minimax(self, node, depth, alpha, beta, player):
        if depth == 0 or node.checkGameOver():
            if node.checkWin() == 'R':
                return -(22 - self.getMoveCount('R'))
            elif node.checkWin() == 'Y':
                return 22 - self.getMoveCount('Y')
            else:
                return 0

        if player == 'Y':
            maxValue = -(sys.maxsize)
            for move in node.availableMoves():
                moveRow = node.makeMove(move, player)
                moveValue = self.minimax(node, depth-1, alpha, beta, changePlayer(player))
                node.removeMove(moveRow, move)
                maxValue = max(maxValue, moveValue)
                alpha = max(alpha, moveValue)
                if beta <= alpha:
                    break
            return maxValue
        
        if player == 'R':
            minValue = sys.maxsize
            for move in node.availableMoves():
                moveRow = node.makeMove(move, player)
                moveValue = self.minimax(node, depth-1, alpha, beta, changePlayer(player))
                node.removeMove(moveRow, move)
                minValue = min(minValue, moveValue)
                beta = min(beta, moveValue)
                if beta <= alpha: 
                    break
            return minValue

def changePlayer(player):
    if player == 'R':
        return 'Y'
    else:
        return 'R'

def makeBestMove(board, depth, player):
    neutralValue = 0
    choices = []
    bestLossingMoveValue = -(sys.maxsize)

    for move in board.availableMoves(): 
        moveRow = board.makeMove(move, player)
        moveValue = board.minimax(board, depth-1, -(sys.maxsize), sys.maxsize, changePlayer(player))
        board.removeMove(moveRow, move)
        print(moveValue)
        
        if moveValue > neutralValue:
            choices = [move]
            #instaWin = True
            break
        elif moveValue == neutralValue:
            choices.append(move)
        elif moveValue > bestLossingMoveValue:  
            bestLossingMove = move

    if len(choices) > 0:
        return random.choice(choices), choices
    elif bestLossingMove != None:
        return bestLossingMove, [False]
    else: 
        return random.choice(board.availableMoves()), [] # idk if i need this


def askPlayAgain():
    terminalWidth = os.get_terminal_size().columns
    print("Would you like to play again?".center(terminalWidth))
    playAgain = input(' ' * round(terminalWidth/2 - 8) + "Enter y/n here: ")
    if playAgain == 'y':
        game.clearBoard()
        game.printBoard()        
        setupGame() 
    elif playAgain == 'n':
        exit()
    else: 
        game.printBoard()
        print("Error: Please try again!".center(terminalWidth))
        askPlayAgain()

playerToColor = {
    'R': '\033[91mRed\033[0m',
    'Y': '\033[93mYellow\033[0m' }

def askPlayerMove(player):
    terminalWidth = os.get_terminal_size().columns
    print(("You are " + playerToColor[player] + ": Choose number from 1-7").center(terminalWidth + 8))    
    move = input(" " * round(terminalWidth/2 - 9) + "Enter Move Here: ")
    try:
        move = int(move) - 1
    except:
        game.printBoard()
        print("Error: Please try again!".center(terminalWidth))
        return askPlayerMove(player)

    if move >= 0 and move <= game.columnCount - 1: 
        row = game.makeMove(move, player)
        return [move, row]
    else:
        game.printBoard()
        print("Error: Please try again!".center(terminalWidth))
        return askPlayerMove(player)

def askDepth():
    terminalWidth = os.get_terminal_size().columns
    print(("What depth do you want the engine to use?").center(terminalWidth))    
    depth = input(" " * round(terminalWidth/2 - 17) + "Enter depth here: (Recommended: 9) ")
    try:
        depth = int(depth)
        return depth
    except:
        game.printBoard()
        print("Error: Please try again!".center(terminalWidth))
        return askDepth()


def setupGame():

    terminalWidth = os.get_terminal_size().columns
    print("Would you like to play computer or player".center(terminalWidth))    
    gameMode = input(" " * round(terminalWidth/2 - 8) + "Enter c/p Here: ")

    if gameMode == 'p':
        startGame(gameMode)
    elif gameMode == 'c':
        game.printBoard()
        depth = askDepth()
        startGame(gameMode, depth)
    else:
        game.printBoard()
        print("Error: Please try again!".center(terminalWidth))
        setupGame()
    


def startGame(gameMode, depth=None):
    
    game.printBoard()

    while game.checkGameOver() == False:
        movePosition = askPlayerMove('R')
        game.printBoard(movePosition[0], movePosition[1])

        if game.checkGameOver() == True:
            break
        
        if gameMode == 'c':
            terminalWidth = os.get_terminal_size().columns
            print("Computer choosing move...".center(terminalWidth))
            computerMove = makeBestMove(game, depth, 'Y') 
            movePosition[1] = game.makeMove(computerMove[0], 'Y') 
            movePosition[0] = computerMove[0]
            computerChoices = computerMove[1]
        elif gameMode == 'p':
            movePosition = askPlayerMove('Y')

        game.printBoard(movePosition[0], movePosition[1])
        if gameMode == 'c':
            print(("Computer choices are " + str(computerChoices)).center(terminalWidth))
            
        
    terminalWidth = os.get_terminal_size().columns
    game.printBoard()
    print(("Game Over. " + game.whoWon() + " Wins").center(terminalWidth))
    askPlayAgain()
    

if __name__ == '__main__':
    game = ConnectFour()
    game.printBoard()

    setupGame() #start game

Solution

  • Always taking the middle out:

    def middle_out(a):
        while a:
            yield a.pop(len(a) // 2)
    

    Demo:

    >>> print(*middle_out([1,2,3,4,5]))
    3 4 2 5 1
    >>> print(*middle_out([1,2,3,4,5,6]))
    4 3 5 2 6 1