"""
# n = 1 Look ahead, using classes

Hello everyone! As I went along my journey of the ConnectX competition, I found a need to refactor my code and clean it up in order to allow myself to continue building. I thought it would be helpful for the community if I shared my refactored version, so here it is below :).

Contents
* Load kaggle environment
* Helper function(s)
* Agent (player)
* Test performance of the agent
"""
"""
# Install kaggle-environments and create ConnectX environment
"""
# 1. Enable Internet in the Kernel (Settings side pane)

# 2. Curl cache may need purged if v0.1.6 cannot be found (uncomment if needed). 
# !curl -X PURGE https://pypi.org/simple/kaggle-environments

# ConnectX environment was defined in v0.1.6
!pip install 'kaggle-environments>=0.1.6'
from kaggle_environments import evaluate, make, utils, agent
from random import choice

env = make("connectx", debug=True)
env.render()
"""
# Helper Function(s)
"""
def is_winning_board(b, m):
    #takes as input a board (b) and the player (mark, as m) and returns True if the board has a winner and False if there is not yet a winner

    #check for winner in the rows
    for i_row in range(0,41, 7):
        for i_col in range(0,4):
            if b[i_row+i_col+0] == m and b[i_row+i_col+1] == m  and b[i_row+i_col+2] == m  and b[i_row+i_col+3] == m:
                return True
    
    #check for winner in the cols
    for i_row in range(0,3):
        i_row = i_row * 7
        for i_col in range(0, 7):
            if b[i_row+i_col+0] == m and b[i_row+i_col+7] == m  and b[i_row+i_col+14] == m and b[i_row+i_col+21] == m:
                return True
    
    #check for winner in the down-right diagonals
    for i_row in range(0,3):
        i_row = i_row * 7
        for i_col in range(0, 4):
            if b[i_row+i_col+0] == m and b[i_row+i_col+8] == m and b[i_row+i_col+16] == m and b[i_row+i_col+24] == m:
                return True

    #check for winner in the down-left diagonals
    for i_row in range(0,3):
        i_row = i_row * 7
        for i_col in range(3, 7):
            if b[i_row+i_col+0] == m and b[i_row+i_col+6] == m and b[i_row+i_col+12] == m and b[i_row+i_col+18] == m:
                return True
    
    #board is not a winner
    return False
def get_next_board(board, action, mark):
    #given the current board and chosen action, returns the next board
    cols = [board[x::7] for x in range(0, 7)]
    col = cols[action]
    
    row = [str(int) for int in col]
    row = ''.join(row)
    row = row.rindex('0')
    next_board = board.copy()
    next_board[action+((row)*7)] = mark
    
    return next_board
"""
# Create Board Class
"""
class connect_board:
    def __init__(self, current_board, mark, possible_moves = []):
        self.current_board = current_board
        self.mark = mark #mark is the player, either a 1 or 2
        self.possible_moves = possible_moves
        self.possible_boards = []
        self.winning_move = None
        self.blocking_move = None
        self.best_move = None #placeholder, not used
    
    def get_possible_moves(self, configuration):
        #up to 7 possible moves, checks if top row is empty in each col
        self.possible_moves = [c for c in range(configuration.columns) if self.current_board[c] == 0]
        
    
    def get_possible_boards(self):
        #generate all possible boards
        for move in self.possible_moves:
            self.possible_boards.append(get_next_board(self.current_board, move, self.mark))
        
    def find_winning_move(self):
        #make sure your # moves and # of boards are the same
        assert len(self.possible_moves) == len(self.possible_boards), "Moves don't equal boards"
        
        #if a possible board is a winning board, then choose that
        for board,move in zip(self.possible_boards, self.possible_moves):
            if is_winning_board(board, self.mark):
                self.winning_move = move
    
    def find_best_move():
        #placeholder for later versions
        pass
    
    def find_blocking_move(self):
        #is there a move that would allow your opponent to win? Well, fill that space before they can!
        self.blocking_move
        mark = self.mark%2 + 1 #get the opponent's mark
        moves = self.possible_moves.copy()
        opp_board = connect_board(self.current_board, mark, moves)
        opp_board.get_possible_boards()
        opp_board.find_winning_move()
        self.blocking_move = opp_board.winning_move
        
    def find_random_move(self, configuration):
        #finds a random, valid move
        self.random_move = choice([c for c in range(configuration.columns) if self.current_board[c] == 0])
        #if the valid move gives opponent a winning move, then pick again
        next_board = get_next_board(self.current_board, self.random_move, self.mark)
        next_turn = connect_board(next_board, mark = self.mark%2 + 1)
        next_turn.get_possible_moves(configuration)
        next_turn.get_possible_boards()
        next_turn.find_winning_move()
        if next_turn.winning_move is not None:
            #print("rerolling!")
            self.random_move = choice([c for c in range(configuration.columns) if self.current_board[c] == 0])
        
"""
# Create an Agent
"""
def my_agent(observation, configuration, boards = []):
    #if you're new to this competition, uncomment the following line of code
    #print(observation)
    #print(configuration)

    #instantite a new class using the present board
    current_board = connect_board(observation['board'], observation['mark'], possible_moves = [])
    current_board.get_possible_moves(configuration)
    current_board.get_possible_boards()
    
    #return winner if it exists
    current_board.find_winning_move()
    if current_board.winning_move is not None:
        return current_board.winning_move
    
    #return blocker if it exists
    current_board.find_blocking_move()
    if current_board.blocking_move is not None:
        return current_board.blocking_move
         
    #else return a random move
    current_board.find_random_move(configuration)
    return current_board.random_move
"""
# Test your Agent (1 match vs. random)
"""
env.reset()
# Play as the first agent against default "random" agent.
env.run([my_agent, "random"])
env.render(mode="ipython", width=500, height=450)
"""
# Evaluate Agent vs. Random and Negamax
https://en.wikipedia.org/wiki/Negamax
"""
def mean_reward(rewards):
    return sum(r[0] for r in rewards) / float(len(rewards))

# Run multiple episodes to estimate its performance.
print("My Agent vs Random Agent:", mean_reward(evaluate("connectx", [my_agent, "random"], num_episodes=20)))
print("My Agent vs Negamax Agent:", mean_reward(evaluate("connectx", [my_agent, "negamax"], num_episodes=20)))
"""
# Summary

Obviously this is just a simple starter notebook and, while it can stand on its own against the random player, it struggles against Negamax. I hope you find this useful for you to build on!
"""