### Version 1.1: Fixed bug in heuristic printing

import copy
import othelloPlayers

# Constant values used throughout the code
white = 1
black = -1
empty = 0
size = 10

class OthelloBoard:
    '''An Othello board, with a variety of methods for managing a game.'''
    
    def __init__(self,array=None):
        '''If the parameter 'board' is left out, then the game board
        is initialized to its typical starting postion. Alternatively,
        a two-dimensional list with a pre-existing starting position
        can be supplied as well. Note that the size of the board is
        10x10, instead of 8x8; this is because leaving a blank ring
        around the edge of the board makes the rest of the code much
        simpler.'''
        if array:
            self.array = array
        else:
            self.array = [[empty]*size for i in range(size)]
            self.array[4][4] = white
            self.array[5][5] = white
            self.array[4][5] = black
            self.array[5][4] = black

    def display(self):
        '''Displays the current board to the terminal window, with
        headers across the left and top. While some might accuse this
        text output as being "old school," having a scrollable game
        history actually makes debugging much easier.'''
        print(' ',end=' ')
        for i in range(1,9):
            print(i,end=' ')
        print()
        for i in range(1,size-1):
            print(i, end=' ')
            for j in range(1,size-1):
                if self.array[i][j] == white:
                    print('W',end=' ')
                elif self.array[i][j] == black:
                    print('B',end=' ')
                else:
                    print('-',end=' ')
            print()

    def makeMove(self,row,col,piece):
        ''' Returns None if move is not legal. Otherwise returns an
        updated OthelloBoard, which is a copy of the original.'''

        # A move cannot be made if a piece is already there.
        if self.array[row][col] != empty:
             return None

        # A move cannot be made if the piece "value" is not black or white.
        if piece != black and piece != white:
            return None

        # Make a copy of the board (not just the pointer!)
        bcopy = copy.deepcopy(self.array)
        bcopy[row][col] = piece

        # Ranges for use below
        rowup = range(row+1,size)
        rowdown = range(row-1,-1,-1)
        rowfixed = [row for i in range(size)]
        colup = range(col+1,size)
        coldown = range(col-1,-1,-1)
        colfixed = [col for i in range(size)]

        # Set up ranges of tuples representing all eight directions.
        vectors = [zip(rowup,coldown),zip(rowup,colfixed), \
                zip(rowup,colup), zip(rowdown,coldown), \
                zip(rowdown, colfixed), zip(rowdown,colup), \
                zip(rowfixed,coldown), zip(rowfixed,colup)]

        # Try to make a move in each direction. Record if at least one
        # of them succeeds.
        flipped = False
        for vectorzip in vectors:

            vector = list(vectorzip)

            # Determine how far you can go in this direction. If you
            # see the opponent's piece, that's a candidate for
            # flipping: count and keep moving. If you see your own
            # piece, that's the end of the range and you're done. If
            # you see a blank space, you must not have had one of your
            # own pieces on the other end of the range.
            count = 0
            for (r,c) in vector:
                if bcopy[r][c] == -1*piece:
                    count += 1
                elif bcopy[r][c] == piece:
                    break
                else:
                    count = 0
                    break

            # If range is nontrivial, then it's a successful move.
            if count > 0:
                flipped = True

            # Actually record the flips.
            for i in range(count):
                (r,c) = vector[i]
                bcopy[r][c] = piece

        if flipped:
            return OthelloBoard(bcopy)
        else:
            return None
             
                         
    def _legalMoves(self,color):
        '''To be a legal move, the space must be blank, and you must take at
        least one piece. Note that this method works by attempting to
        move at each possible square, and recording which moves
        succeed. Therefore, using this method in order to try to limit
        which spaces you actually use in makeMoves is futile.'''
        moves = []
        for i in range(1,size-1):
            for j in range(1,size-1):
                bcopy = self.makeMove(i,j,color)
                if bcopy != None:
                    moves.append((i,j))
        return moves

    def scores(self):
        '''Returns a list of black and white scores for the current board.'''
        score = [0,0]
        for i in range(1,size-1):
            for j in range(1,size-1):
                if self.array[i][j] == black:
                    score[0] += 1
                elif self.array[i][j] == white:
                    score[1] += 1
        return score


    def playGame(self):
        '''Manages playing an actual game of Othello.'''
        
        print('Black goes first.')
        # Two player objects: [black, white]
        players=[None,None]
        colorNames = ('black','white')
        colorValues = (black,white)
        invalidPasses = [0,0]
        illegalMoves = [0,0]

        # Determine whether each player is human or computer, and
        # instantiate accordingly
        for i in range(2):
            response = input('Should ' + colorNames[i] + \
                             ' be (h)uman or (c)omputer? ')
            if response.lower() == 'h':
                name = input("What is the player's name? ")
                players[i] = othelloPlayers.HumanPlayer(name,colorValues[i])
            else:
                plies = int(input("How many plies ahead " + \
                                  "should the computer look? "))
                players[i] = othelloPlayers.ComputerPlayerPruning(
                               'compy' + colorNames[i],colorValues[i],
                               othelloPlayers.heuristic,plies)

        # Number of times a "pass" move has been made, in a row
        passes = 0

        done = False
        curBoard = self
        while not done:

            # Black goes, then white
            for i in range(2):

                # Display board and statistics
                curBoard.display()
                scores = curBoard.scores()
                print('Statistics: score / invalid passes / illegal moves')
                for j in range(2):
                    print(colorNames[j] + ': ' + str(scores[j]) + ' / ',
                          str(invalidPasses[j]) + ' / ' + str(illegalMoves[j]))
                print()
                print('Turn: ' + colorNames[i])

                # Obtain move that player makes
                move = players[i].chooseMove(curBoard)

                if move==None:
                    # If no move is made, that is considered a
                    # pass. Verify that there were in fact no legal
                    # moves available. If there were, allow the pass
                    # anyway (this is easier to code), but record that
                    # an invalid pass was taken.

                    passes += 1
                    print(colorNames[i] + ' passes.')
                    legalMoves=curBoard._legalMoves(colorValues[i])
                    if legalMoves != []:
                        print(colorNames[i] + \
                              ' passed, but there was a legal move.')
                        print('Legal moves: ' + str(legalMoves))
                        invalidPasses[i] += 1
                else:
                    # If a move is made, make the move on the
                    # board. makeMove returns None if the move is
                    # illegal. Record as an illegal move, and forfeit
                    # the player's turn. This is easier to code than
                    # offering another turn.

                    passes = 0
                    print(colorNames[i] + ' chooses ' + str(move) + '.')
                    bcopy = curBoard.makeMove(move[0],move[1],colorValues[i])
                    if bcopy==None:
                        print('That move is illegal, turn is forfeited.')
                        illegalMoves[i] += 1
                    else:
                        curBoard = bcopy
                        if (len(move) == 3):
                            print('Number of heuristic calls = ' + str(move[2]))
                print()

                # To keep code simple, never test for win or loss; if
                # one player has won, lost, or tied, two passes must
                # occur in a row.
                if passes == 2:
                    print('Both players passed, game is over.')
                    done = True
                    break

        # Display final outcome
        scores = curBoard.scores()
        if scores[0] > scores[1]:
            print('Black wins!')
        elif scores[1] > scores[0]:
            print('White wins!')
        else:
            print('Tie game!')

if __name__=='__main__':
    OthelloBoard().playGame()