5
\$\begingroup\$

I've created a Tic-Tac-Toe program with python which has a option to change a standard 3x3 board to NxN board. I've also created easy, hard and insane AIs.

Please help me in improving the code and removing bugs(if any)

Code:

import os
from random import randint

cls = lambda: os.system('CLS')  # Works only in command console.

# Random names
names = [
    'Jacob', 'Michael',
    'Joshua', 'Ethan', 'Matthew', 'Daniel',
    'Christopher', 'Andrew', 'Anthony', 'William',
    'Joseph', 'Alexander', 'David', 'Ryan',
    'Noah', 'James', 'Nicholas', 'Tyler',
    'Logan', 'John', 'Christian', 'Jonathan',
    'Nathan', 'Benjamin', 'Samuel', 'Dylan',
    'Brandon', 'Gabriel', 'Elijah', 'Aiden',
    'Angel', 'Jose', 'Zachary', 'Caleb',
    'Jack', 'Jackson', 'Kevin', 'Gavin',
    'Mason', 'Isaiah', 'Austin', 'Evan',
    'Luke', 'Aidan', 'Justin', 'Jordan',
    'Robert', 'Isaac', 'Landon', 'Jayden',
    'Thomas', 'Cameron', 'Connor', 'Hunter',
    'Jason', 'Diego', 'Aaron', 'Bryan',
    'Owen', 'Lucas', 'Charles', 'Juan',
    'Luis', 'Adrian', 'Adam', 'Julian',
    'Alex', 'Sean', 'Nathaniel', 'Carlos',
    'Jeremiah', 'Brian', 'Hayden', 'Jesus',
    'Carter', 'Sebastian', 'Eric', 'Xavier',
    'Brayden', 'Kyle', 'Ian', 'Wyatt',
    'Chase', 'Cole', 'Dominic', 'Tristan',
    'Carson', 'Jaden', 'Miguel', 'Steven',
    'Caden', 'Kaden', 'Antonio', 'Timothy',
    'Henry', 'Alejandro', 'Blake', 'Liam',
    'Richard', 'Devin', 'Riley', 'Jesse',
    'Seth', 'Victor', 'Brady', 'Cody',
    'Jake', 'Vincent', 'Bryce', 'Patrick',
    'Colin', 'Marcus', 'Cooper', 'Preston',
    'Kaleb', 'Parker', 'Josiah', 'Oscar',
    'Ayden', 'Jorge', 'Ashton', 'Alan',
    'Jeremy', 'Joel', 'Trevor', 'Eduardo',
    'Ivan', 'Kenneth', 'Mark', 'Alexis',
    'Omar', 'Cristian', 'Colton', 'Paul',
    'Levi', 'Damian', 'Jared', 'Garrett',
    'Eli', 'Nicolas', 'Braden', 'Tanner',
    'Edward', 'Conner', 'Nolan', 'Giovanni',
    'Brody', 'Micah', 'Maxwell', 'Malachi',
    'Fernando', 'Ricardo', 'George', 'Peyton',
    'Grant', 'Gage', 'Francisco', 'Edwin',
    'Derek', 'Max', 'Andres', 'Javier',
    'Travis', 'Manuel', 'Stephen', 'Emmanuel',
    'Peter', 'Cesar', 'Shawn', 'Jonah',
    'Edgar', 'Dakota', 'Oliver', 'Erick',
    'Hector', 'Bryson', 'Johnathan', 'Mario',
    'Shane', 'Jeffrey', 'Collin', 'Spencer',
    'Abraham', 'Leonardo', 'Brendan', 'Elias',
    'Jace', 'Bradley', 'Erik', 'Wesley',
    'Jaylen', 'Trenton', 'Josue', 'Raymond',
    'Sergio', 'Damien', 'Devon', 'Donovan',
    'Dalton', 'Martin', 'Landen', 'Miles',
    'Israel', 'Andy', 'Drew', 'Marco',
    'Andre', 'Gregory', 'Roman', 'Ty',
    'Jaxon', 'Avery', 'Cayden', 'Jaiden',
    'Roberto', 'Dominick', 'Rafael', 'Grayson',
    'Pedro', 'Calvin', 'Camden', 'Taylor',
    'Dillon', 'Braxton', 'Keegan', 'Clayton',
    'Ruben', 'Jalen', 'Troy', 'Kayden',
    'Santiago', 'Harrison', 'Dawson', 'Corey',
    'Maddox', 'Leo', 'Johnny', 'Kai',
    'Drake', 'Julio', 'Lukas', 'Kaiden',
    'Zane', 'Aden', 'Frank', 'Simon',
    'Sawyer', 'Marcos', 'Hudson', 'Trey'
]

# Dummy Variable
start = 0

# Essential Variables:
player = 'Player'  # Player name
board_type = 2  # Board Type (1 or 2)

board = [['', '', ''], ['', '', ''], ['', '', '']]  # The TicTacToe board
win_board = [['', '', ''], ['', '', ''], ['', '', '']]  # Traces the win (if any) of 'board'


X = 'X'  # Character for player 1
O = 'O'  # Character for player 2

size = 3  # Size of 'board'


def countWins(p1, p2):
    """

    p1: Player 1
    p2: Player 2

    Counts the wins possible in the current move for 'p1'

    """

    count = 0  # Keeps count of wins possible

    for i in range(size):
        for j in range(size):
            if board[i][j] != p1 and board[i][j] != p2:
                copy = board[i][j]  # A dummy variable to restore 'board[i][j]'
                board[i][j] = p1

                if win(p1) == 1:
                    count += 1

                board[i][j] = copy

    return count


def get_insane_AI_move(ai, pl, x=0, name=''):
    """

    ai: ai character
    pl: player character
    x: dummy variable
    name: ai name

    The best AI

    Follows all the tips and checks for moves leading to multiple wins constantly

    """

    for i in range(size):
        for j in range(size):
            if board[i][j] != ai and board[i][j] != pl:
                copy = board[i][j]
                board[i][j] = ai

                if win(ai) == 1 or tie() == 1:
                    if x:
                        print(name + ' Moved To Grid', i * size + j + 1)
                    return

                board[i][j] = copy

    for i in range(size):
        for j in range(size):
            if board[i][j] != ai and board[i][j] != pl:
                copy = board[i][j]
                board[i][j] = pl

                if win(pl) == 1 or tie() == 1:
                    board[i][j] = ai
                    if x:
                        print(name + ' Moved To Grid', i * size + j + 1)
                    return
                board[i][j] = copy

    wins2 = []
    l = 0

    for i in range(size):
        for j in range(size):
            if board[i][j] != ai and board[i][j] != pl:
                copy = board[i][j]
                board[i][j] = ai

                if countWins(ai, pl) > 1:
                    l += 1
                    r = [i, j]
                    wins2.append(r)

                board[i][j] = copy

    if l:
        m = wins2[randint(0, 1000) % l]
        board[m[0]][m[1]] = ai
        if x:
            print(name + ' Moved To Grid', m[0] * size + m[1] + 1)
        return

    l = 0

    pos_centers = [[i, j] for i in range(size) for j in range(size)
                   if (i in [0, size - 1]) == (j in [0, size - 1]) == False]

    centers = []

    for i in range(len(pos_centers)):
        x = pos_centers[i][0]
        y = pos_centers[i][1]

        if board[x][y] != ai and board[x][y] != pl:
            centers.append(pos_centers[i])
            l += 1

    if l:
        r = centers[randint(1, 1000) % l]
        board[r[0]][r[1]] = ai

        if x:
            print(name + ' Moved To Grid', r[0] * size + r[1] + 1)

        return

    l1 = 0
    l2 = 0

    pos_edges = [[0, 0], [0, size - 1], [size - 1, 0], [size - 1, size - 1]]
    edges = []

    for i in range(len(pos_edges)):
        x = pos_edges[i][0]
        y = pos_edges[i][1]

        if board[x][y] != ai and board[x][y] != pl:
            edges.append(pos_edges[i])
            l1 += 1

    if l1:
        r = edges[randint(1, 1000) % l1]
        board[r[0]][r[1]] = ai

        if x:
            print(name + ' Moved To Grid', r[0] * size + r[1] + 1)

        return

    pos_middles = [[i, j] for i in range(size) for j in range(size)
                   if (i in [0, size - 1]) != (j in [0, size - 1])]

    middles = []

    for i in range(len(pos_middles)):
        x = pos_middles[i][0]
        y = pos_middles[i][1]

        if board[x][y] != ai and board[x][y] != pl:
            middles.append(pos_middles[i])
            l2 += 1

    r = middles[randint(1, 1000) % l2]
    board[r[0]][r[1]] = ai

    if x:
        print(name + ' Moved To Grid', r[0] * size + r[1] + 1)

    return


def get_hard_AI_move(ai, pl, x=0, name=''):
    """

    A medium AI

    Can only look ahead 1 move

    """

    for i in range(size):
        for j in range(size):
            if board[i][j] != ai and board[i][j] != pl:
                copy = board[i][j]
                board[i][j] = ai

                if win(ai) == 1 or tie() == 1:
                    if x:
                        print(name + ' Moved To Grid', i * size + j + 1)
                    return

                board[i][j] = copy

    for i in range(size):
        for j in range(size):
            if board[i][j] != ai and board[i][j] != pl:
                copy = board[i][j]
                board[i][j] = pl

                if win(pl) == 1 or tie() == 1:
                    board[i][j] = ai

                    if x:
                        print(name + ' Moved To Grid', i * size + j + 1)

                    return

                board[i][j] = copy

    l = 0

    possible = [[i, j] for i in range(size) for j in range(size)]

    available = []

    for i in range(len(possible)):
        x = possible[i][0]
        y = possible[i][1]

        if board[x][y] != ai and board[x][y] != pl:
            available.append(possible[i])
            l += 1

    r = available[randint(1, 1000) % l]

    board[r[0]][r[1]] = ai

    if x:
        print(name + ' Moved To Grid', r[0] * size + r[1] + 1)

    return


def get_easy_AI_move(ai, pl, x=0, name=''):
    """

    An easy AI

    Moves randomly

    """

    l = 0

    possible = [[i, j] for i in range(size) for j in range(size)]

    available = []

    for i in range(len(possible)):
        x = possible[i][0]
        y = possible[i][1]

        if board[x][y] != ai and board[x][y] != pl:
            available.append(possible[i])
            l += 1

    r = available[randint(1, 1000) % l]
    board[r[0]][r[1]] = ai

    if x:
        print(name + ' Moved To Grid', r[0] * size + r[1] + 1)
    return


def get_user_move(p1, p2):
    """ Gets user input and processes it """

    g = int(input(f'Please Enter Grid Number (1 ~ {size * size}): ')) - 1

    x = g // size
    y = g % size

    if x >= size or y >= size or board[x][y] == p1 or board[x][y] == p2:
        print('Please Enter A Valid Move')
        get_user_move(p1, p2)
        return

    print(player + ' Moved To Grid', g + 1)

    board[x][y] = p1
    print()


def get_win(p):
    """ Traces the win into 'win_board' """

    for i in range(size):
        # Rows
        if all(board[i][j] == p for j in range(size)):
            for j in range(size):
                win_board[i][j] = p
            return

        # Columns
        if all(board[j][i] == p for j in range(size)):
            for j in range(size):
                win_board[j][i] = p
            return

    # Diagonals
    if all(board[i][i] == p for i in range(size)):
        for i in range(size):
            win_board[i][i] = p
        return

    if all(board[i][-(i + 1)] == p for i in range(size)):
        for i in range(size):
            win_board[i][-(i + 1)] = p
        return

    ## Returns in every case as multiple wins might be traced out


def printBoard1():
    """ Prints board type 1 """

    for i in range(size - 1):
        print('      ' + '|       ' * (size - 1))
        print(end='  ')

        for j in range(size - 1):
            print(board[i][j], end='   |   ')

        print(board[i][-1])
        print('      ' + '|       ' * (size - 1))
        print('------' + '--------' * (size - 1))
        '      |       '

    print('      ' + '|       ' * (size - 1))
    print(end='  ')

    for j in range(size - 1):
        print(board[-1][j], end='   |   ')

    print(board[-1][-1])
    print('      ' + '|       ' * (size - 1))

    print()


def printBoard2():
    """ Prints board type 2 """

    for i in range(size - 1):
        for j in range(size - 1):
            print(board[i][j], end=' | ')
        print(board[i][-1])
        print('---' * size + '-' * (size - 3))

    for j in range(size - 1):
        print(board[-1][j], end=' | ')

    print(board[-1][-1])
    print()


def printWin(p):
    """ Prints 'win_board' at board type 2"""

    get_win(p)

    for i in range(size - 1):
        for j in range(size - 1):
            print(win_board[i][j], end=' | ')
        print(win_board[i][-1])
        print('---' * size + '-' * (size - 2))

    for j in range(size - 1):
        print(win_board[-1][j], end=' | ')

    print(win_board[-1][-1])
    print()


def getRandomName():
    """ Gets random names from 'names' """

    name = names[randint(1, 1000) % 250]
    return name


def helper():
    """ Help section containing Rules, Tips and Credits """

    print()

    print('B for Back\n')
    print('1. Rules')
    print('2. Tips')
    print('3. Credits')

    option = input('\nPlease Enter Your Option: ').lower()
    print()

    if option == 'b': return
    if option == '1': rules()
    if option == '2': tips()
    if option == '3': about()

    input('Enter To Continue . . . ')
    print()
    helper()


def about():  ## Couldn't name this credits as there's a built-in name
    print('This Game Of Tic-Tac-Toe Is Created By Srivaths')
    print('If You Are Unfamiliar With This Game, Please Read The Rules And Tips')
    print('Enjoy!!\n')


def changeName():
    """ Changes player name: 'player' """

    global player
    player = input('Please Enter Your Name: ')


def changeBoard():
    """ Changes board type: 'board_type' """

    global board_type

    print()
    print('B for Back\n')
    print('1.')
    printBoard1()
    print('2.\n')
    printBoard2()

    print()

    option = input('\nPlease Enter Your Option: ')

    if option == 'b' or option == 'B':
        return

    if option == '1': board_type = 1
    if option == '2': board_type = 2


def changeCharacters():
    """ Changes characters: 'X', 'O' """

    global X, O

    print()
    X = input('Please Enter Character For Player 1 (currently ' + X + '): ')
    O = input('Please Enter Character For Player 2 (currently ' + O + '): ')


def changeSize():
    """ Changes board size: 'size' """

    global size
    size = int(input('Please Enter Size: '))
    initialize()


def settings():
    """ Settings """

    print()

    print('B for Back\n')
    print('1. Change Name')
    print('2. Change Size')
    print('3. Change Board')
    print('4. Change Characters')
    option = input('\nPlease Enter Your Option: ').lower()

    if option == 'b':
        return

    if option == '1': changeName()
    if option == '2': changeSize()
    if option == '3': changeBoard()
    if option == '4': changeCharacters()

    print()
    settings()


def main_menu():
    """ The main menu """

    global start

    # cls()
    print()

    if start == 0:
        intro()
        start = 1
        main_menu()
        return

    print('Hello ' + player)

    print('\nQ for Quit\n')
    print('1. Help')
    print('2. Settings')
    print('3. Play')
    option = input('\nPlease Enter Your Option: ')

    if option == '1':
        helper()

    if option == '2':
        settings()

    if option == '3':
        initialize()
        play('X', 'O')

    if option == 'q' or option == 'Q':
        print('Thanks For Playing!\n')
        return

    print()
    main_menu()


def rules():
    """ Basic rules """

    print('1. In Tic-Tac-Toe, there are 2 players \n\tand their characters are X and O respectively')
    print('2. Any row or column or diagonal filled tith the same character is a win')
    print('3. A board where there are no moves left is a tie')
    print('4. You are not allowed to place characters over another')
    print('5. The playes must play in alternate turns, starting with X')
    print()


def tips():
    """ Basic tips """

    print('1. Always try and capture the center')
    print('2. Next try to capture the edges')
    print('3. Occupy the edges only if necessary')
    print('4. Be aware of immediate moves')
    print('5. Try the easy bot to get the hang of the game')
    print()


def intro():
    """ Introduction """

    global board_type

    initialize()

    print('Hello Player', end=', ')

    changeName()

    print('\nHello ' + player + ', Welcome To The Game Of Tic-Tac-Toe!!')

    know = input('Are You Familiar With The Game? (y / n): ').lower()

    if know == 'n':
        print('\nFirst A Little Introduction To The Rules: \n')
        rules()
        print('\nNext A Few Tips: \n')
        tips()
        print('\nAnd That\'s ALL!!!\n')
        input('Enter To Continue . . . ')
        print('\n')

    print('\nPlease Pick Your Board Preference: \n')

    print('1.')
    printBoard1()
    print('2.\n')
    printBoard2()

    print()

    option = input('Please Enter Your Option: ')

    if option == '1': board_type = 1
    if option == '2': board_type = 2

    print()

    print('Change Characters Via [Main Menu -> Settings -> Change Characters]')
    print()

    print('Here You Must Try Your Luck Against Three Levels!!\n')
    print('1. Easy')
    print('2. Hard')
    print('3. Insane')
    print()

    print('Can YOU Beat Them ALL????')
    print('Let\'s See....\n')

    input('Enter To Continue . . . ')


def play(p1, p2):
    """

    The play area

    p1: Player 1
    p2: Player 2

    """

    print()
    initialize()

    computer = getRandomName()

    print('1. Easy')
    print('2. Hard')
    print('3. Insane')

    print()

    level = int(input('Please Enter Level: '))

    print()

    while computer == player:
        computer = getRandomName()

    print('\t\t' + player + ' VS ' + computer + '\n\n')

    c = randint(0, 1)

    pl = p1
    ai = p2

    if c == 0:
        ai = p1
        pl = p2

        print('\n' + computer + ' Goes First!\n\n')

    else:
        print('\n' + player + ' Goes First!\n\n')
        if board_type == 1:
            printBoard1()
        else:
            printBoard2()

    d = 0

    while True:
        t = d % 2

        if t == c:
            if level == 1: get_easy_AI_move(ai, pl, 1, computer)
            if level == 2: get_hard_AI_move(ai, pl, 1, computer)
            if level == 3: get_insane_AI_move(ai, pl, 1, computer)

            if board_type == 1:
                printBoard1()
            else:
                printBoard2()

            if win(ai):
                print(computer + ' Wins!\n')
                print('Below Is How ' + computer + ' Won\n\n')
                printWin(ai)
                break

        else:
            get_user_move(pl, ai)

            if board_type == 1:
                printBoard1()
            else:
                printBoard2()

            if win(pl):
                print(player + ' Wins!')
                print('Below Is How ' + player + ' Won\n')
                printWin(pl)
                break

        if tie():
            print('Tie!')
            break

        d += 1

    play_again(p1, p2)


def initialize():
    """ Resets the board """

    global board, win_board

    board = [[' ' for _ in range(size)] for __ in range(size)]
    win_board = [[' ' for _ in range(size)] for __ in range(size)]


def play_again(p1, p2):
    """ Gets input from the player asking if they want to play again """

    option = input('Would You Like To Play Again? (y(yes) / n(no) / m(Main Menu): ').lower()

    if option == 'y':
        play(p1, p2)
    elif option == 'n':
        return
    elif option == 'm':
        return
    else:
        print('\nPlease Enter a Valid Option')
        play_again(p1, p2)


def win(p):
    """ Checks for win """

    if any(all(board[i][j] == p for j in range(size)) for i in range(size)):
        return True
    if any(all(board[j][i] == p for j in range(size)) for i in range(size)):
        return True
    if all(board[i][i] == p for i in range(size)):
        return True
    if all(board[i][-(i + 1)] == p for i in range(size)):
        return True

    return False


def tie():
    """ Checks for tie """

    return all(all(j in [X, O] for j in i) for i in board)


main_menu()

It would be more interactive if the program is run on a CMD console instead of a IDE console. If you do run the program on a CMD console, you can add cls() to the program in places you like.

\$\endgroup\$
  • \$\begingroup\$ Welcome to Code Review. I would like to write better docstrings Great! Start with revisiting PEP 257 and comparing python library docstrings to yours. \$\endgroup\$ – greybeard Apr 20 at 7:44
  • \$\begingroup\$ Thanks! I suppose I should add return values and parameter definitions to the docstrings? \$\endgroup\$ – Srivaths Apr 20 at 8:24
  • \$\begingroup\$ Anything that eases use for someone importing your module (and not familiar with its internals), but no more. \$\endgroup\$ – greybeard Apr 20 at 8:31
2
\$\begingroup\$

and welcome, and thanks for providing an interesting subject. I like the idea of difficulty levels for the game!

That said, your code is too long. Not "too long to review," just "too long."

Moar functions! (& Generators!)

Consider this:

for i in range(size):
    for j in range(size):
        if board[i][j] != ai and board[i][j] != pl:
            copy = board[i][j]
            board[i][j] = ai

            if win(ai) == 1 or tie() == 1:
                if x:
                    print(name + ' Moved To Grid', i * size + j + 1)
                return

            board[i][j] = copy

(Taken from the insane AI function)

Now consider this:

for i in range(size):
    for j in range(size):
        if board[i][j] != ai and board[i][j] != pl:
            copy = board[i][j]
            board[i][j] = pl

            if win(pl) == 1 or tie() == 1:
                board[i][j] = ai
                if x:
                    print(name + ' Moved To Grid', i * size + j + 1)
                return
            board[i][j] = copy

Now consider this:

for i in range(size):
    for j in range(size):
        if board[i][j] != ai and board[i][j] != pl:
            copy = board[i][j]
            board[i][j] = ai

            if countWins(ai, pl) > 1:
                l += 1
                r = [i, j]
                wins2.append(r)

            board[i][j] = copy

Those three blocks are taken from the same function, but they are different lines. Can you see how much repetition there is in there?

  • Iterate over (i, j) rows and columns
  • Get a X/O/Empty value
  • Check if the cell is empty
  • Copy the prior value
  • Check for a win or a tie
  • Translate from coordinates into grid location
  • Print a "moved to" message
  • Set the cell.

How much of that code can you replace with functions? How much of that code can you replace with generators?

Iterating over the row/column ranges has value, but it would be better to iterate over the (i, j) tuples directly - it's not like you ever do one without the other.

Even better still would be to iterate over only the tuples that represent empty cells:

for i, j in board.empty_cells():

Or, if you haven't learned classes yet:

for i, j in empty_cells(board):

You would use the yield keyword in a generator function for this.

Next, what are you doing? In the first loop, you ask if the player would win or tie with the next move. It turns out that "tie" for you means "every location would be filled", which is disappointing but true.

I'd suggest that "about to tie" means "only one empty spot" and that's a globally true condition. You don't need to check it so often.

On the other hand, all the set/check/unset logic seems like a good place to write a function:

def would_win_if_moved_to(player, location) -> bool:
    ''' Return true if player would win if their glyph was placed at location. '''
    # TODO
    pass

If you had that function, your two top loops look like this:

for locn in empty_cells(board):
    if would_win_if_moved_to(ai, locn):
        # Go for the win!
        move_to(locn)

for locn in empty_cells(board):
    if would_win_if_moved_to(player, locn):
        # Block opponent win!
        move_to(locn)

Of course, you could make your logic even more clear by wrapping those lines into their own functions:

for locn in moves_to_win(ai):
    return locn
for locn in moves_to_block_win(player):
    return locn

In your last loop, you're appending to a list. That's a good indicator that you could be using a list comprehension, if only the rest of the loop could be written shorter. You can do that:

wins2 = [locn for locn in empty_cells(board) if wins_after_move_to(ai, locn) > 1]

You just need a function that will do your "speculative" move first.

Moar docstrings!

Another use for the docstring syntax is to span multiple lines with a single string. You should use this for your giant blocks of text, like the rules and hints. Instead of doing this:

print("line 1")
print("line 2")
print("line 3")

You can do this:

text = """
    line 1
    line 2
    line 3
""".strip('\n')
print(textwrap.dedent(text))

Using the textwrap module that ships with Python.

I think if you make these changes, your code will get a lot smaller. And you'll be ready for another review. ;-)

\$\endgroup\$
  • \$\begingroup\$ Thanks! I do know OOPS, but I prefer functions anyway. Should I use classes instead of functions? And you'll be ready for another review. ;-) Hope you'll be there too! \$\endgroup\$ – Srivaths Apr 22 at 5:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.