from cmu_graphics import *
import math, random, copy

EMPTY = '-'
FULL = 'x'
PLAYER_PIECES = '01'

class State:
    def __init__(self, rows, cols):
        board = [([EMPTY] * cols) for _ in range(rows)]
        board[0][cols//2] = PLAYER_PIECES[0]
        board[rows-1][cols//2] = PLAYER_PIECES[1]
        self.board = board
        self.playerPositions = [(0, cols//2), (rows-1, cols//2)]
        self.rows = rows
        self.cols = cols

    def print(self):
        for rowList in self.board: print(' '.join(rowList))

    def getMoves(self, player):
        moves = [ ]
        for drow in [-1, 0, +1]:
            for dcol in [-1, 0, +1]:
                row, col = self.playerPositions[player]
                if (drow, dcol) != (0, 0):
                    while True:
                        row += drow
                        col += dcol
                        if ((row < 0) or (row >= self.rows) or
                            (col < 0) or (col >= self.cols) or
                            (self.board[row][col] != EMPTY)):
                            break
                        else:
                            moves.append((row, col))
        return moves

    def canMove(self, player):
        return len(self.getMoves(player)) > 0

    def isLegalMove(self, move, player):
        return move in self.getMoves(player)

    def doMove(self, move, player):
        row,col = self.playerPositions[player]
        self.board[row][col] = FULL
        newRow, newCol = move
        self.board[newRow][newCol] = PLAYER_PIECES[player]
        self.playerPositions[player] = move

    def pickMoveByHuman(self, player):
        move = input('enter your move --> ')
        return eval(move) # there's no danger here!!!!  Best code ever!!!!!

    def pickMoveRandomly(self, player):
        return random.choice(self.getMoves(player))

    def pickMoveByRetreating(self, player):
        def distanceFn(row0, col0, row1, col1):
            # This distance is not smart because it is not taking into
            # account that queens can go on diagonals.  Too bad.
            return abs(row0 - row1) + abs(col0 - col1)
        return self.pickMoveByDistance(player, distanceFn)

    def pickMoveByAttacking(self, player):
        def distanceFn(row0, col0, row1, col1):
            # Ditto
            return -(abs(row0 - row1) + abs(col0 - col1))
        return self.pickMoveByDistance(player, distanceFn)

    def pickMoveByAttackingMiddle(self, player):
        def distanceFn(row0, col0, row1, col1):
            row1 = self.rows//2
            col1 = self.cols//2
            # Ditto
            return -(abs(row0 - row1) + abs(col0 - col1))
        return self.pickMoveByDistance(player, distanceFn)

    def pickMoveByDistance(self, player, distanceFn):
        moves = self.getMoves(player)
        bestDistance = None
        otherPlayer = 1 - player
        row1,col1 = self.playerPositions[otherPlayer]
        for move in moves:
            (row0, col0) = move
            d = distanceFn(row0, col0, row1, col1)
            if (bestDistance == None) or (d > bestDistance):
                bestDistance = d
                bestMoves = [ move ]
            elif (d == bestDistance):
                bestMoves.append(move)
        assert(bestMoves)
        return random.choice(bestMoves)

    @staticmethod
    def playGame(rows, cols, pickMoveFn0, pickMoveFn1):
        state = State(rows, cols)
        currentPlayer = 0
        while True:
            otherPlayer = 1 - currentPlayer
            if (not state.canMove(currentPlayer)):
                return  otherPlayer
            pickMoveFn = pickMoveFn0 if currentPlayer == 0 else pickMoveFn1
            move = pickMoveFn(state, currentPlayer)
            state.doMove(move, currentPlayer)
            currentPlayer = otherPlayer

    @staticmethod
    def runTournament(rows, cols, pickMoveFn0, pickMoveFn1, rounds):
        wins = [0, 0]
        for _ in range(rounds//2):
            winner = State.playGame(rows, cols, pickMoveFn0, pickMoveFn1)
            wins[winner] += 1
            winner = State.playGame(rows, cols, pickMoveFn1, pickMoveFn0)
            wins[1 - winner] += 1
        pcts = [pythonRound(v/rounds, 2) for v in wins]
        return pcts

###########################################
# Isola app
###########################################

def onAppStart(app, N, computerFirst, pickMoveFn):
    app.computerFirst = computerFirst
    app.pickMoveFn = pickMoveFn
    app.computerColor = 'pink'
    app.humanColor = 'lightBlue'
    resetApp(app, N)

def resetApp(app, N):
    app.rows = app.cols = app.N = N
    app.margin = 50
    app.boardLeft = app.margin
    app.boardTop = 100
    app.cellSize = 50
    app.boardWidth = N * app.cellSize
    app.boardHeight = N * app.cellSize
    app.cellBorderWidth = 2
    app.state = State(N, N)
    app.currentPlayer = 0
    if app.computerFirst:
        app.humanPlayer, app.computerPlayer = 1, 0
    else:
        app.humanPlayer, app.computerPlayer = 0, 1
    app.message = ''
    app.gameOver = False
    # (Due to a bug in CMU Graphics, we must set app.width and app.height last!)
    app.width = app.boardLeft + app.boardWidth + app.margin
    app.height = app.boardTop + app.boardHeight + app.margin

def onKeyPress(app, key):
    if key in ['c', 'space']:
        if not app.gameOver:
            if app.currentPlayer == app.computerPlayer:
                doComputerMove(app)
            else:
                app.message = "Not computer's turn. Take your turn!"
    elif key.isdigit():
        N = int(key)
        if N > 1:
            resetApp(app, N)
    elif key == 'f':
        app.computerFirst = not app.computerFirst
        resetApp(app, app.N)

def onMousePress(app, mouseX, mouseY):
    if app.gameOver:
        return
    if app.currentPlayer == app.humanPlayer:
        targetCell = getCell(app, mouseX, mouseY)
        if targetCell != None:
            move = targetCell
            tryToDoMove(app, move)
    else:
        targetCell = getCell(app, mouseX, mouseY)
        if targetCell != None:
            row, col = targetCell
            if app.state.board[row][col] == str(app.computerPlayer):   
                doComputerMove(app)
                return
        app.message = "Not your turn! Press c for computer's turn"

def doComputerMove(app):
    assert(app.currentPlayer == app.computerPlayer)
    move = app.pickMoveFn(app.state, app.computerPlayer)
    assert(app.state.isLegalMove(move, app.computerPlayer))
    tryToDoMove(app, move)

def tryToDoMove(app, move):
    if app.state.isLegalMove(move, app.currentPlayer):
        app.state.doMove(move, app.currentPlayer)
        app.currentPlayer = 1 - app.currentPlayer
        if not app.state.canMove(app.currentPlayer):
            app.gameOver = True
            app.winner = 1 - app.currentPlayer
            app.loser = app.currentPlayer
            if app.winner == app.computerPlayer:
                app.message = 'Computer Won!!!'
            else:
                app.message = 'Human Won!!!'
    else:
        app.message = f'Illegal move: {move}. Try again.'

def redrawAll(app):
    if app.message:
        message = app.message
    elif app.currentPlayer == app.computerPlayer:
        message = "Computer's Turn"
    else:
        message = "Your Turn"
    if app.gameOver:
        color = app.computerColor if app.winner == app.computerPlayer else app.humanColor
        drawRect(0, 0, app.width, app.height, fill=color)
    drawLabel('Isola', app.width/2, 20, size=14)
    drawLabel('Press N (2-9) for new NxN game', app.width/2, 40)
    drawLabel('Press f to toggle computerFirst', app.width/2, 60)
    drawLabel(message, app.width/2, 80)
    drawBoard(app)
    drawBoardBorder(app)

def drawBoard(app):
    for row in range(app.rows):
        for col in range(app.cols):
            drawCell(app, row, col)

def drawBoardBorder(app):
  # draw the board outline (with double-thickness):
  drawRect(app.boardLeft, app.boardTop, app.boardWidth, app.boardHeight,
           fill=None, border='black',
           borderWidth=2*app.cellBorderWidth)

def drawCell(app, row, col):
    cellLeft, cellTop = getCellLeftTop(app, row, col)
    cellWidth, cellHeight = getCellSize(app)
    cellValue = app.state.board[row][col]
    if cellValue == EMPTY: color = 'white'; label=None
    elif cellValue == FULL: color = 'gray'; label=None
    elif cellValue in PLAYER_PIECES:
        computerValue = '0' if app.computerFirst else '1'
        if cellValue == computerValue:
            color = app.computerColor
            label = 'C'
        else:
            color = app.humanColor
            label = 'H'
    else: assert(False)
    drawRect(cellLeft, cellTop, cellWidth, cellHeight,
             fill=color, border='black',
             borderWidth=app.cellBorderWidth)
    if label != None:
        drawLabel(label, cellLeft + cellWidth/2, cellTop + cellHeight/2,
                  size=16, bold=True)

def getCell(app, x, y):
    dx = x - app.boardLeft
    dy = y - app.boardTop
    cellWidth, cellHeight = getCellSize(app)
    row = math.floor(dy / cellHeight)
    col = math.floor(dx / cellWidth)
    if (0 <= row < app.rows) and (0 <= col < app.cols):
      return (row, col)
    else:
      return None

def getCellLeftTop(app, row, col):
    cellWidth, cellHeight = getCellSize(app)
    cellLeft = app.boardLeft + col * cellWidth
    cellTop = app.boardTop + row * cellHeight
    return (cellLeft, cellTop)

def getCellSize(app):
    cellWidth = app.boardWidth / app.cols
    cellHeight = app.boardHeight / app.rows
    return (cellWidth, cellHeight)

###########################################
# minimax
###########################################

def makeMinimax(maxDepth, heuristicFn):
    def getMove(state, rootPlayer):
        def minimax(state, thisPlayer, depth):
            if depth == maxDepth:
                return (heuristicFn(state, rootPlayer), None)
            else:
                otherPlayer = 1 - thisPlayer
                scoresAndMoves = [ ]
                for move in state.getMoves(thisPlayer):
                    newState = copy.deepcopy(state)
                    newState.doMove(move, thisPlayer)
                    newScore, _ = minimax(newState, otherPlayer, depth+1)
                    scoresAndMoves.append((newScore, move))
                if (scoresAndMoves == [ ]):
                    return (heuristicFn(state, rootPlayer), None)
                else:
                    if (thisPlayer == rootPlayer):
                        bestScore,_ = max(scoresAndMoves)
                    else:
                        bestScore,_ = min(scoresAndMoves)
                    bestMoves = [ ]
                    for score, move in scoresAndMoves:
                        if score == bestScore:
                            bestMoves.append(move)
                    return bestScore, random.choice(bestMoves)
        score,move = minimax(state, rootPlayer, 0)
        return move
    return getMove

def scoreBoard(state, player):
    otherPlayer = 1 - player
    playerMoves = state.getMoves(player)
    otherPlayerMoves = state.getMoves(otherPlayer)
    if len(playerMoves) == 0:
        # player lost
        return -100
    elif len(otherPlayerMoves) == 0:
        # player won
        return 100
    else:
        return len(playerMoves) - len(otherPlayerMoves)

minimax1 = makeMinimax(maxDepth=1, heuristicFn=scoreBoard)
minimax2 = makeMinimax(maxDepth=2, heuristicFn=scoreBoard)
minimax4 = makeMinimax(maxDepth=4, heuristicFn=scoreBoard)

###########################################
# main
###########################################

strategies = [
    State.pickMoveRandomly,
    State.pickMoveByRetreating,
    State.pickMoveByAttacking,
    State.pickMoveByAttackingMiddle,
]

def testStrategies():
    print(State.runTournament(5,5,State.pickMoveRandomly, State.pickMoveByRetreating, 100))
    print(State.runTournament(5,5,State.pickMoveRandomly, State.pickMoveByAttacking, 100))
    print(State.runTournament(5,5,State.pickMoveRandomly, State.pickMoveByAttackingMiddle, 100))
    print(State.runTournament(5,5,State.pickMoveRandomly, minimax1, 100))
    print(State.runTournament(5,5,minimax1, minimax4, 10))

def main():
    testStrategies()
    #runApp(N=6, computerFirst=True, pickMoveFn=State.pickMoveByAttackingMiddle)
    #runApp(N=6, computerFirst=True, pickMoveFn=minimax4)

main()