<reponame>jpoweseas/games
import copy, csv

def read_winners(filename='connect_four_winners.csv'):
    with open(filename) as f:
        return [[(int(i) // 7, int(i) % 7) for i in line] for line in csv.reader(f)]

WIN_VALUE = 10000
LOSE_VALUE = -10000
TIE_VALUE = 0

WINNERS = read_winners()
INVERSE_WINNERS = [[i for (i, line) in enumerate(WINNERS) if x in line] for x in range(42)]

class ConnectFour:
    WINNERS = WINNERS
    INVERSE_WINNERS = INVERSE_WINNERS

    def __init__(self, board = None, a_turn = True, memory=None):
        if not board:
            board = [[] for _ in range(7)]
            # memory = { 'num_as' : num_as, 'num_bs', num_bs, 'winner' : None }
        # elif not memory:
        #     print('Cannot pass in board and not memory')
        #     assert False

        self.board = board
        self.a_turn = a_turn

    # ACCESSORS

    def get_player_to_play(self):
        return 'A' if self.a_turn else 'B'

    def get_piece(self, col_index, row_index):
        if col_index >= 0 and col_index < 7 \
           and row_index < len(self.board[col_index]) and row_index >= 0:
            return self.board[col_index][row_index]
        else:
            return None

    def place_possibly_hovering(self, col_index, row_index):
        # Check that the column is tall enough
        if row_index < len(self.board[col_index]):
            self.board[col_index][row_index] = 'A'
        else:
            # Add blank spots after the current end of the column
            spots_to_add = row_index - len(self.board[col_index])
            self.board[col_index].extend([None for _ in range(spots_to_add)])
            self.board[col_index].append('A')

    def open_columns(self):
        return [i for i in range(7) if len(self.board[i]) < 6]

    # EVALUATION

    def winner(self):
        for l in ConnectFour.WINNERS:
            first_piece = self.get_piece(l[0][1], l[0][0])
            if first_piece is not None \
               and first_piece == self.get_piece(l[1][1], l[1][0]) \
               and first_piece == self.get_piece(l[2][1], l[2][0]) \
               and first_piece == self.get_piece(l[3][1], l[3][0]):
                return first_piece

    def old_winner(self):
        def four_in_a_row(l):
            equal_to = None
            count = 0
            for x in l:
                if x == equal_to and x is not None:
                    count += 1
                    if count == 4:
                        return equal_to
                else:
                    count = 1
                    equal_to = x
            return None

        for column in self.board:
            winner = four_in_a_row(column)
            if winner:
                # print('col')
                return winner

        for row_index in range(6):
            row = [self.get_piece(col_index, row_index) for col_index in range(7)]
            winner = four_in_a_row(row)
            if winner:
                # print('row')
                return winner

        # Diagonals
        for row_index in range(-4, 7):
            row = [self.get_piece(col_index, row_index + col_index) for col_index in range(7)]
            winner = four_in_a_row(row)
            if winner:
                # print('diag up-left')
                return winner

        for row_index in range(10):
            row = [self.get_piece(col_index, row_index - col_index) for col_index in range(7)]
            winner = four_in_a_row(row)
            if winner:
                # print('diag up-right')
                return winner

        if all([len(col) == 6 for col in self.board]):
            return 'tie'

    def old_evaluate(self):
        lasts = [col[-1] for col in self.board if len(col) > 0]
        lefts = [self.get_piece(col_index - 1, len(self.board[col_index])) for col_index in range(7)]
        rights = [self.get_piece(col_index + 1, len(self.board[col_index])) for col_index in range(7)]

        considered = lasts + lefts + rights

        return considered.count('A') - considered.count('B')

    def evaluate(self):
        score = 0
        for winner in ConnectFour.WINNERS:
            line = [self.get_piece(row_index, col_index) for row_index, col_index in winner]
            if 'A' not in line:
                score -= 1
            if 'B' not in line:
                score += 1

        return score

    # GAMEPLAY

    def add_new_mark_and_flip_turn(self, col_index):
        board = copy.deepcopy(self.board)
        if len(board[col_index]) < 6:
            board[col_index].append(self.get_player_to_play())
        else:
            assert False

        new_state = ConnectFour(board, a_turn = not self.a_turn)
        new_state.evaluate = self.evaluate
        return new_state

    def next_node(self):
        open_columns = self.open_columns()

        winner = self.winner()
        if winner is not None:
            return 'Terminal', winner
        elif self.a_turn:
            return 'A', { str(i) : i for i in open_columns }
        else:
            return 'B', { str(i) : i for i in open_columns }

    def resolve_choice(self, choice):
        return self.add_new_mark_and_flip_turn(choice)

    # REPRESENTATION

    def unsafe_hash(self):
        def col_to_hash(col):
            out = len(col)
            for (x, i) in enumerate(col):
                if x == 'A':
                    out += 1 << i
            return out
        out = 0
        for (i, col) in enumerate(self.board):
            out += col_to_hash(col) << (9 * i)
        return out

    def hash(self):
        return self.unsafe_hash()

    def unique_hash(self):
        return self.unsafe_hash()

    def __repr__(self):
        def print_cell(col_num, row_num):
            x = self.get_piece(col_num, row_num)
            if x is None:
                return ' '
            elif x == 'A':
                return 'X'
            elif x == 'B':
                return 'O'
            else:
                assert False

        return '\n'.join(['|' + ''.join([print_cell(col_num, row_num) for col_num in range(7)]) + '|' for row_num in range(6)[::-1]]) + '\n' + '-' * 9 + '\n ' + ''.join([str(i) for i in range(7)]) + ' '

    def print_for_human_input(self):
        print(self)

def four_combos(N):
    # partials[i] contains all partials with length i and numbers <= n
    combo_size = 4

    partials = [[] for _ in range(combo_size)]
    for n in range(N):
        for i in range(1, combo_size)[::-1]:
            partials[i].extend([l + [n] for l in partials[i - 1]])
        partials[0].append([n])

    return partials[-1]

    # for i in range(N):
    #     finals += [l + [i] for l in three_partials]
    #     three_partials += [l + [i] for l in two_partials]
    #     two_partials += [l + [i] for l in one_partials]
    #     one_partials.append([i])

    # return finals

def simple_col_winner_test():
    state = ConnectFour()
    state.place_possibly_hovering(0, 0)
    state.place_possibly_hovering(0, 1)
    state.place_possibly_hovering(0, 2)
    state.place_possibly_hovering(0, 3)

    assert state.winner() == 'A'

def simple_row_winner_test():
    state = ConnectFour()
    state.place_possibly_hovering(0, 0)
    state.place_possibly_hovering(1, 0)
    state.place_possibly_hovering(2, 0)
    state.place_possibly_hovering(3, 0)

    assert state.winner() == 'A'

def simple_up_right_winner_test():
    state = ConnectFour()
    state.place_possibly_hovering(0, 0)
    state.place_possibly_hovering(1, 1)
    state.place_possibly_hovering(2, 2)
    state.place_possibly_hovering(3, 3)

    assert state.winner() == 'A'

def second_up_right_winner_test():
    state = ConnectFour()
    state.place_possibly_hovering(6, 5)
    state.place_possibly_hovering(5, 4)
    state.place_possibly_hovering(4, 3)
    state.place_possibly_hovering(3, 2)

    assert state.winner() == 'A'

def simple_up_left_winner_test():
    state = ConnectFour()
    state.place_possibly_hovering(3, 0)
    state.place_possibly_hovering(2, 1)
    state.place_possibly_hovering(1, 2)
    state.place_possibly_hovering(0, 3)

    assert state.winner() == 'A'

def second_up_left_winner_test():
    state = ConnectFour()
    state.place_possibly_hovering(3, 5)
    state.place_possibly_hovering(4, 4)
    state.place_possibly_hovering(5, 3)
    state.place_possibly_hovering(6, 2)

    assert state.winner() == 'A'

def test_num_winners():
    num_winners = 0
    winners = []
    winner_states = []
    for combo in four_combos(42):
        state = ConnectFour()
        for i in combo:
            row_index = i // 7
            col_index = i % 7
            state.place_possibly_hovering(col_index, row_index)

        winner = state.winner()
        if winner == 'A':
            num_winners += 1
            winners.append(combo)
            winner_states.append(state)

    # 7 columns, 3 different winning jawns on each col
    # 6 rows, 4 " on each row
    # Diagonals going up and to the right can start anywhere that isn't within 3
    # of the top or right side. So they can start in one of 3 * 4 places
    # By symmetry, same thing for the diagonals going up and to the left
    # 21 + 24 + 12 + 12 = 69
    #print(num_winners)
    #print(winners[:10])
    #assert num_winners == 69

    # for winner in winners:
    #     print(','.join([str(i) for i in winner]))

    # i = 0
    # for state in winner_states:
    #     i += 1
    #     print(i)
    #     print(state)

if __name__ == '__main__':
    assert len(four_combos(42)) == 111930
    test_num_winners()
    simple_col_winner_test()
    simple_row_winner_test()
    simple_up_right_winner_test()
    second_up_right_winner_test()
    simple_up_left_winner_test()
    second_up_left_winner_test()
    pass
