Tic Tac Toe – Kennedy Agent Code

"""
Python code to play tic-tac-toe game using the engine.py and interact with the Game Engine.

"""
# Import the function engine and move_at from engine.py
# Make sure that engine.py is located in the same folder on your computer as this agent.py

from engine import engine, move_at

# Import the function randint from the module random (standard python library)
# See here for documentation of random and randint: https://docs.python.org/3/library/random.html

from random import randint

# Example agent: first_available_agent
def first_available_agent(board):
    """
    An agent that only plays in the first available position.
    This strategy is a poor strategy overall because your agent is losing more than winning the game  ¯\_(ツ)_/¯

    Input:  A tic tac toe board in its string representation nine characters (either "X"s, "O"s or "."s)
            (either "X"s, "O"s or "."s)
            E.g. 'X...O...X' represents the board
            X . .
            . O .
            . . X
            String index positions are:
            0 1 2
            3 4 5
            6 7 8

    Output: A string representation of the input board with our move made on it
            - replaces one '.' with the variable our pieces

    A valid strategy (if not a very good one) is to place your piece in the first
    open place on the tic tac toe board. An agent that does this isn't going to win
    every game but it is able to play a move onto every possible board so its a good start
    to see how the system operates with a valid agent

    """
    # Work out which pieces we are playing
    """
        Summary of thinking:
        * Starting with the premise that "X" always play first we can infer two things
            -> if the number of X's and O's are equal X should play next
            -> if the number of empty squares ('.') is odd (empty board = 9, one move each = 7, ....)
                then X should be playing
        * Using the first inference we will make the comparison and return one of two things based on the result
            -> Structure of the code: an if statement with different return calls within each condition
    """
    # Compare the number of "X"s and "O"s
    if board.count("X") == board.count("O"):
    # If equal number of X's and O's then we are playing "X"s (if statement)
        our_pieces = "X"
    # If not then we must be playing "O"s (else statement)
    else:
        our_pieces = "O"

    # Seize the top centre first (position number 1)
    # check that its an available move (if statement)
    # generate the board with our move on it
    # return the board (on the spot return call)
    """ This turned out to be a terrible strategy and wasn't implemented. The only way to win, was not to play position 1 first!
        Also the same strategy applied for position 3 (left-middle), position 5 (right-middle) and position 7 (bottom centre). """

    # Replace the first "." with our pieces
    """
    To make the first available move by replacing the first "." with our pieces.
    Three arguments of "Replace":
    1. A string of the character(s) to be replaced,
    2. What to replace those character(s) with and 
    3. How many times to do so (optionally).
    
    Calling this bound method on `board` which returns us a new string with our requested changes that we will call 'first_available_agent'
    """
    move = board.replace('.',our_pieces,1)

    # Give out move back to the engine
    return move

def random_agent(board):
    """ A strategy of making a random move is a better strategy than the 'first_available_agent' strategy because this agent is winning more than losing.
        Even though random movement is not strategic or pythonic enough but it could be useful if you have run out of 'strategic' options.
    """
    # Work out which pieces we are playing
    # Compare the number of "X"s and "O"s
    if board.count("X") == board.count("O"):
        # If equal number of X's and O's then we are playing "X"s
        # (if statement)
        our_pieces = "X"
    else:
        # If not then we must be playing "O"s (else)
        our_pieces = "O"

    # This strategy - select a random position which has a '.', is not the most efficient way.
    # select a random integer (whole number) between 0 and 8
    # and put it in variable 'r'
    r = randint(0, 8)   # thanks to SOC for pointing out the documentation

    # NOTE: '.' is a free spot on the board
    # We will loop until we know that board[r] == '.'
    # in other words: the index position 'r' is a free space
    # we are finished the loop when the value board[r] == '.'
    while not board[r] == '.':
        # thank you Erika for suggesting that we use
        # "not board[r] == '.'" as the test for the loop

        # NOTE: this is a great example of why you would use not
        #       instead of "(board[r] == 'X' or board[r] == 'O')"

        # if we get here, then it is not a free spot on the board
        # so - we get a new random location
        r = randint(0, 8)

    # Now we have finished with the loop, we know that
    # board[r] == '.' - r indicates a free spot

    # Thanks to Memunat for illustrating this way of
    # "making a move" in the tutorial:

    # Change the board string to a list
    #   NOTE: we can change the value of list at an index - but not a string
    board_list = list(board)
    # change the value of the board_list at position 'r' from '.' to our_pieces
    board_list[r] = our_pieces
    # join the list into a string (converts a list to a string)
    board = ''.join(board_list)

    # Give out board back to the engine
    return board
       
def winning_agent(board: str):
    """
    The engine will pass this function the current game-board;
    after adding your prefered move to the board, return it back to the engine.
    SHALL WE PLAY A GAME!
    """  
    # Work out which pieces we are playing
    # Compare the number of "X"s and "O"s
    if board.count("X") == board.count("O"):
    # If equal number of X's and O's then we are playing "X"s (if statement)
        our_pieces = "X"
    # If not then we must be playing "O"s (else)
    else:
        our_pieces = "O"
        
    # For winning strategy with possible sequences, I will define the sequence as (a, b, c)
    # I need to have three of the same symbols in a row, column or diagonal (i.e. 3 of X's or 3 of O's). 
    # So need to place your agent in any of these 8 position combinations:
	# 	a. Top row = 0, 1, 2
	# 	b. Middle row = 3, 4, 5
	# 	c. Bottom row = 6, 7, 8
	# 	d. Left column = 0, 3, 6
	# 	e. Middle column = 1, 4, 7
	# 	f. Right column = 2, 5, 8
	# 	g. Diagonal from top-left to bottom-right = 0, 4, 8
	# 	h. Diagonal from top-right to bottom-left = 2, 4, 6
    # This strategy is also applied to block the opponent from winning the game by taking the 3rd empty spot available 
    # if the opponent already have 2 of their pieces on the sequence out of 8 winning positions possiblities as per below:
     
    for a, b, c in (
        (0, 1, 2),  # top row                       
        (3, 4, 5),  # middle row                         
        (6, 7, 8),  # bottom row                        
        (0, 3, 6),  # left column                        
        (1, 4, 7),  # middle column
        (2, 5, 8),  # right column
        (0, 4, 8),  # top-left to bottom-right diagonal
        (2, 4, 6),  # top-right to bottom-left diagonal
    ):
    # If position (a) is empty, place your agent here.
        if  board[a] == '.' and ((our_pieces == board[b] == board[c]) or (our_pieces != board[b] and board[b] == board[c])):
            return move_at(board, a) 

    # If position (b) is empty, place your agent here.
        if board[b] == '.' and ((our_pieces == board[a] == board[c]) or (our_pieces != board[a] and board[a] == board[c])):
            return move_at(board, b)  

    # If position (c) is empty, place your agent here.
        if board[c] == '.' and ((our_pieces == board[a] == board[b]) or (our_pieces != board[a] and board[a] == board[b])):
            return move_at(board, c)  
        
    # If position #0 (top-left corner) is empty, place your agent here.
        elif board[0] == '.' and ((board[1] == board[2]) or (board[3] == board[6]) or (board[4] == board[8])):
            return move_at(board,0)
                
    # If position #2 (top-right corner) is empty, place your agent here.     
        elif board[2] == '.' and ((board[0] == board[1]) or (board[5] == board[8]) or (board[4] == board[6])):
            return move_at(board,2)

    # If position #6 (bottom-left corner) is empty, place your agent here.
        elif board[6] == '.' and ((board[0] == board[3]) or (board[7] == board[8]) or (board[2] == board[4])):
            return move_at(board,6)

    # If position #8 (bottom-right corner) is empty, place your agent here. 
        elif board[8] == '.' and ((board[2] == board[5]) or (board[6] == board[7]) or (board[0] == board[4])):
            return move_at(board,8)

    # If position #4 (the middle square) is empty, place your agent here.
        elif board[4] == '.' and ((board[0] == board[8]) or (board[2] == board[6]) or (board[1] == board[7]) or (board[3] == board[5])):
            return move_at(board,4)

    # Otherwise, take any first empty position on the board at random.
        else:
            return random_agent(board)
        
"""
All the functions above (our agent(s) and any helper functions) are just sitting there waiting
to be called and put to use. The actual calling of the function is done by the engine code
which takes in an agent function and plays out all the possible games with that agent

We imported the engine function from engine.py at the top of the file, to run an agent
we call engine and pass in the agent

When you're ready with `your_agent` you can replace the agent function that is being called
"""

#When you're ready swap over which line is being commented out
# engine(first_available_agent)
# engine(random_agent)
engine(winning_agent)