2
\$\begingroup\$

Part 1: Tic-Tac-Toe with changeable board size (Part 1)

I just revisited all my previous questions and saw that I hadn't updated my Tic-Tac-Toe code. So, I improvised my code as per @Austin Hastings suggestions

Here's the updated code:

import os
from random import randint
from textwrap import dedent

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'
O = 'O'

size = 3


def empty_cells():
    empty = []

    for i in range(size):
        for j in range(size):
            if board[i][j] != X and board[i][j] != O:
                empty.append((i, j))

    return empty


def countWins(p):
    """

    Counts the wins possible in the current move for player 'p'

    """

    count = 0

    empty = empty_cells()

    for i, j in empty:
        copy = board[i][j]
        board[i][j] = p

        count += win(p)

        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

    """

    empty = empty_cells()
    length = len(empty)

    if length == 1:
        i, j = empty[0]

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

    for i, j in empty:
        copy = board[i][j]
        board[i][j] = ai

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

        board[i][j] = copy

    for i, j in empty:
        copy = board[i][j]
        board[i][j] = pl

        if win(pl) or length == 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, j in empty:
        copy = board[i][j]
        board[i][j] = ai

        if countWins(ai) > 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 = 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

    """

    empty = empty_cells()
    length = len(empty)

    if length == 1:
        i, j = empty[0]

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

    for i, j in empty:
        copy = board[i][j]
        board[i][j] = ai

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

        board[i][j] = copy

    for i, j in empty:
        copy = board[i][j]
        board[i][j] = pl

        if win(pl) == 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

    available = []

    for x, y in empty_cells():
        if board[x][y] != ai and board[x][y] != pl:
            available.append((x, y))
            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():
    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(
        dedent('''
            1. In Tic-Tac-Toe, there are 2 players and their characters are X and O respectively
            2. Any row or column or diagonal filled the same character is a win
            3. A board where there are no moves left is a tie
            4. You are not allowed to place characters over another
            5. The playes must play in alternate turns, starting with X
        ''')
    )


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

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


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(
        dedent('''
            Change Characters Via [Main Menu -> Settings -> Change Characters]

            Here You Must Try Your Luck Against Three Levels!!

            1. Easy
            2. Hard
            3. Insane

            Can YOU Beat Them ALL????
            Let's See....
        ''')
    )

    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 len(empty_cells()) == 0:
            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


main_menu()

Hope @Austin Hastings will answer this question too!

\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

Winning Bug

When the board is at this stage and the user gets the next move:

      |       |       
  X   |   O   |   X
      |       |       
----------------------
      |       |       
      |   O   |    
      |       |       
----------------------
      |       |       
  O   |   X   |   X
      |       |       

And the user enters 5, the game bugs and will let the user enter again and win the game, instead of the AI placing an X at position 4 resulting in a draw.

Start Game Bug

When first prompted to select a difficulty, you enter a number (lets say 1). Then it asks you what you want to do. You enter 3 to play. It then asks you what difficulty again. I would remove the first prompt of difficulty, since it's kinda weird you ask for the difficulty before seeing if the user wants to play at all.

win

When returning True or False, 90% of the time you can instead return the expression that would result in those values. Observe:

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

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

This erases the need of any return True statements as returning the expression will result in a boolean value itself.

Ternary Operators

This

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

can be simplified to this using a ternary operator:

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

Method Naming

Method and variable names should both be snake_case.

printBoard1() -> print_board_one()

I would also use "one" instead of "1", but that's a personal preference.

That big list of names

Time to address the elephant in the room. I would put the names in a separate file, like names.txt, and extrapolate the information from there. It clears up a bunch of bulk from your code. Here's how this method might look like:

names.txt

Ben,David,Hannah,Max,Sarah,William
def get_names():
    with open("names.txt", "r") as file:
        return ''.join(file.readlines()).split(",")

It will return

["Ben", "David", "Hannah", "Max", "Sarah", "William"]

What this does is reads every line in the file, converts that list into a string, then returns a list of each word split by a comma. It's a simple way to access and get data from the names file. It also significantly reduces the clutter in your code.

If you don't want to go through the trouble of putting each name in the file, that's understandable. What you can do is create another python module names.py, put the list in that file, and import from that file. Take a look:

from names import NAMES # NAMES is the list of names

Now you can operate on this NAMES list.

String Formatting

This

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

can be written like this:

print(f"{names} Moved To Grid {r[0] * size + r[1] + 1}")

The same with these input statements

X = input(f"Please Enter Character For Player 1 (currently {X}): ")
O = input(f"Please Enter Character For Player 2 (currently {O}): ")

Type Hints

Lets take a look at this method header:

def play_again(p1, p2):

Now, consider this:

def play_again(p1: str, p2: str) -> None:

These can help you and other people using your program identify what types to pass into the method, as well as what values could be returned.

If Statement Formatting

This

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)

should really be this

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)

Main Guard

You should wrap all initialization code in a main guard. This will allow you to import this module without having the code run. Take a look:

if __name__ == '__main__':
    main_menu()

It's not very conventional to have an if statement on one line.

\$\endgroup\$
4
  • 1
    \$\begingroup\$ board = [['' * 3] * 3] will lead to some pretty strange errors \$\endgroup\$ Commented Nov 20, 2019 at 16:17
  • \$\begingroup\$ @MaartenFabré Do you mind describing a couple so that I might reevaluate that portion of my answer? \$\endgroup\$
    – Linny
    Commented Nov 20, 2019 at 16:35
  • \$\begingroup\$ @Carcigenicate I see that I have a lot of misconceptions when it comes to working with lists. I've removed that part of the answer because it is so flawed. I'll have to take some time to relearn what I thought I knew about lists. Thanks for the explanation. \$\endgroup\$
    – Linny
    Commented Nov 20, 2019 at 19:14
  • \$\begingroup\$ @Linny, it doesn't ask for the difficulty level, it displays that there are 3 types of AI and prompts the user to Enter To Continue . . . . Also, can you please explain what the first bug did in more detail? \$\endgroup\$
    – Sriv
    Commented Nov 21, 2019 at 5:33

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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