"""
This file has the AI, or lack thereof for my reddit round2 
tic tac toe over HTTP playing program.

It's not brilliant, I wanted to make it stateless, so that
ruled out using a game tree.

I also considered hashing the board and caching the move made
so I wouldn't have to recompute it, but I decided the minor speedup
wouldn't be worth the extra complexity.

So instead, this AI basically just checks for 2 of the same characters
in a winning position coordinate triple and reacts appropriately.
"""
from random import choice

X,O = 'X','O'

def move(board_data):
    board = TicTacToeBoard(board_data)
    if not board.winner:
        board.move()
    return {
        'board': board.to_list(),
        'winner': board.winner,
        'game_over': board.game_over
    }

class InvalidPosition(Exception):
    pass

class Square(object):
    def __init__(self, row, col, content=None):
        self.content = content
        self.row = row
        self.col = col
        if content not in (X,O):
            self.content = None
    def __str__(self):
        if self.content:
            return self.content
        else:
            return ' '
    def __repr__(self):
        return "<Square: %s>" % self.content

class TicTacToeBoard(object):

    win_columns = ((0,0), (1,0), (2,0)), ((0,1), (1,1), (2,1)), ((0,2),(1,2),(2,2))
    win_rows = ((0,0), (0,1), (0,2)), ((1,0),(1,1), (1,2)), ((2,0),(2,1),(2,2)) 
    win_diagonal = ((0,0), (1,1), (2,2)), ((0,2), (1,1), (2,0))
    winning_positions = win_columns + win_rows + win_diagonal
    best_squares = ( (1,1), (0,0), (2,0), (0,2), (2,2) )

    def __init__(self, data):
        """
        Create a tic tac toe board.
        """
        self.game_over = False
        self.empty_squares = 9
        self.squares = []
        for i,row in enumerate(data):
            for j,square in enumerate(row):
                self.squares.append(Square(i,j,square))
                if square in (X,O):
                    self.empty_squares -= 1
        self.player = self.determine_turn()
        self.opponent = X if self.player == O else O
        
    def square_at(self, pos):
        row,col = pos
        return self.squares[row*3+col]
    
    def winning_position_squares(self):
        """
        takes all of the coordinate triples from the winning positions list
        and turns them into Square objects
        """
        for triple in self.winning_positions:
            yield map(self.square_at,triple)

    @property
    def winner(self):
        """
        Returns X/O if X or O is the winner.
        Returns python None if no one has won yet or tie game
        """
        for sq1, sq2, sq3 in self.winning_position_squares():
            if sq1.content == sq2.content == sq3.content and sq1.content != None:
                self.game_over = True
                return sq1.content
        return None 

    def move(self):
        if self.winner != None:
            return #no point in moving now!
        if self.empty_squares == 0:
            self.game_over = True
            return
        chosen_square = None
        for triple in self.winning_position_squares():
            totals = {X:0,O:0,None:0}
            for square in triple:
                totals[square.content] += 1

            if totals[self.player] == 2 and totals[self.opponent] == 0:
                # we can win! there is no better move, so find the square and break.
                chosen_square = [square for square in triple if square.content == None][0]
                break
            elif totals[self.opponent] == 2 and totals[self.player] == 0:
                # opponent can win! block this, but don't break in case we find a better (winning) move
                chosen_square = [square for square in triple if square.content == None][0]

        if not chosen_square:
            # this is the hard part; we have a non-obvious move.
            for coords in self.best_squares:
                square = self.square_at(coords)
                if square.content == None:
                    chosen_square = square
                    break
            else: # first time the else of python's for loop has ever made sense
                chosen_square = choice( [square for square in self.squares if square.content == None] )
        # now make the actual "move"
        chosen_square.content = self.player
        # in case move() is called again
        self.player, self.opponent = self.opponent, self.player
        self.empty_squares -= 1
        if not self.empty_squares:
            self.game_over = True

    def determine_turn(self):
        """
        Count the X's and O's to see if we're playing as X or O
        This enables the server to be stateless (not depend on knowing
        the previous moves).
        """
        num_x = len( [x for x in self.squares if x.content == X] )
        num_o = len( [o for o in self.squares if o.content == O] )
        if num_x == num_o:
            return X
        elif num_x - num_o == 1:
            return O
        else:
            raise InvalidPosition

    def __str__(self):
        return """
%s|%s|%s
-----
%s|%s|%s
-----
%s|%s|%s
        """ % tuple(self.squares)
    
    def to_list(self):
        return [
            [s.content for s in self.squares[0:3]],
            [s.content for s in self.squares[3:6]],
            [s.content for s in self.squares[6:9]],
       ]

def test():
    test_cases =  (
        (
            ((X,O,X),(X,None,None),(None,O,O)),
            None
        ),
        (
            ((X,O,X),(None,X,None),(O,O,X)),
            X
        ),
        (
            ((O,X,X),(O,None, None),(O,None,X)),
            O
        ),
    )

    for board, winner in test_cases:
        t = TicTacToeBoard(board)
        assert t.winner == winner
        print t,"%s wins!" % winner




if __name__ == "__main__":
    test()
