Data Structures
Our is_board_full:
def is_board_full(board):
    """Return True is board is full, False otherwise.
    >>> is_board_full([['.', '.', '.'], ['X', '.', 'O'], ['.', '.', '.']])
    False
    >>> is_board_full([['X', 'O', '.'], ['X', 'O', 'X'], ['X', 'O', 'X']])
    False
    >>> is_board_full([['X', 'O', 'X'], ['X', 'X', 'O'], ['O', 'O', 'O']])
    True
    """
    # START SOLUTION
    for row in board:
        for coli in range(3):
            if row[coli] == '.':
                return False
    return True
Our make_random_move:
def make_random_move(board, player):
    """Find an empty cell and play into it.
    player = 'X' or 'O', depending on who should move.
    This should change the board in-place. It should return the
    position (1-9) it played into.
    You don't need to do this randomly -- it can simply use the first empty
    cell it finds.
    >>> board = [['X', 'O', 'X'], ['X', 'X', 'O'], ['O', 'O', '.']]
    >>> make_random_move(board, 'X')
    9
    >>> board
    [['X', 'O', 'X'], ['X', 'X', 'O'], ['O', 'O', 'X']]
    """
    # START SOLUTION
    for rowi in range(3):
        for coli in range(3):
            if board[rowi][coli] == '.':
                board[rowi][coli] = player
                return rowi * 3 + coli + 1
    raise Exception("No empty spot to play")
Our find_winner:
def find_winner(bd):
    """"Given board, determine if winner. Return 'X', 'O', or None if no winner.
    >>> print(find_winner([['.', '.', '.'], ['X', '.', 'O'], ['.', '.', '.']]))
    None
    >>> find_winner([['X', '.', '.'], ['X', '.', 'O'], ['X', '.', '.']])
    'X'
    >>> find_winner([['X', 'O', 'X'], ['O', 'O', 'X'], ['O', 'X', 'X']])
    'X'
    >>> find_winner([['X', '.', 'O'], ['X', 'O', 'O'], ['O', '.', '.']])
    'O'
    """
    # START SOLUTION
    # Check for win in each row
    for rowi in range(3):
        if bd[rowi][0] != '.' and bd[rowi][0] == bd[rowi][1] == bd[rowi][2]:
            return bd[rowi][0]
    # Check for win in each col
    for coli in range(3):
        if bd[0][coli] != '.' and bd[0][coli] == bd[1][coli] == bd[2][coli]:
            return bd[0][coli]
    # Check for \ diagonal
    if bd[0][0] != '.' and bd[0][0] == bd[1][1] == bd[2][2]:
        return bd[0][0]
    # Check for / diagonal
    if bd[2][0] != '.' and bd[2][0] == bd[1][1] == bd[0][2]:
        return bd[2][0]
Our make_move:
def make_move(board, position, player):
    """Play into position 1-9.
    position = 1-9 (top-left, top-middle, top-right ... bottom-right)
    player = 'X' or 'O'
    This should update the board to play there. It does not return anything.
    >>> board = [['X', '.', 'O'], ['X', 'O', 'O'], ['O', '.', '.']]
    >>> make_move(board, 2, 'O')
    >>> board
    [['X', 'O', 'O'], ['X', 'O', 'O'], ['O', '.', '.']]
    >>> make_move(board, 9, 'X')
    >>> board
    [['X', 'O', 'O'], ['X', 'O', 'O'], ['O', '.', 'X']]
    """
    # START SOLUTION
    # We'll use the built-in `divmod()` function (look it up!), but we could
    # also do this with "/" and "%", separately.
    coli, rowi = divmod(position - 1, 3)
    board[coli][rowi] = player