4
\$\begingroup\$

This is my second Python program, and I wonder if this code is readable to most people or if it only applies to me. Should I make more distinct functions or use more appropriate names? Are there other ways that lessen the lines of codes and do lesser lines mean more readability?

Any suggestions would genuinely be appreciated. Thanks!

   # -*- coding: utf-8 -*-
"""
Tic-Tac-Toe
Started on: 17/04/2020
Finished on: 20/04/2020
Use the numpad to mark your position
7 8 9
4 5 6
1 2 3
"""

board = [' ',' ',' ',' ',' ',' ',' ',' ',' ',' ']
symbols = ['X','O']
running = True

def display_board(board):
    """
    Takes the board list as an argument 
    and displays it as a Tic-Tac-Toe board 
    when the game is running

    """

    display_board.row3 = "{0:^5s}||{1:^5s}||{2:^5s}".format(board[7],board[8],board[9])    
    display_board.row2 = "{0:^5s}||{1:^5s}||{2:^5s}".format(board[4],board[5],board[6])
    display_board.row1 = "{0:^5s}||{1:^5s}||{2:^5s}".format(board[1],board[2],board[3])
    spaces = "{0:5s}||{1:5s}||{2:5s}".format('','','')
    display_board.board = [display_board.row3,display_board.row2,display_board.row1]

    for row in display_board.board[:2]:
       print(spaces+'\n'+row+'\n'+spaces)
       print('='*20)
    print(spaces+'\n'+display_board.row1+'\n'+spaces)

def alternate_symbols(symbols):

    temp = symbols[1]
    symbols[1] = symbols[0]
    symbols[0] = temp

    return symbols[1]

def check_win(board,mark):

    running = True
    mark = [mark] * 3

    if ((board[1:4] == mark) or (board[4:7] == mark) or
            (board[7:10] == mark) or (board[1:8:3] == mark) or
            (board[2:9:3] == mark) or (board[3:10:3] == mark) or
            (board[1:10:4] == mark) or (board[3:8:2] == mark)):
        print(mark[0] + ' has won!')
        running = False
    elif board.count(' ') == 1:
        print("It's a draw!")
        running = False

    return running

def start_game():
    """
    Starts the game
    """
    #Resets the board
    for marks in board:
        for index in range(len(board)):
            board[index] = board[index].replace(marks, ' ')

    print('='*24 + "\n Welcome to Tic TAC TOE\n" + '='*24)

    #Resets the symbols
    symbols = ['X','O']

    while True:
        player_input = input("Player 1: Choose X or O: ")
        if player_input.upper() == 'X':
            print("Player 1 will go first")
            break
        elif player_input.upper() == 'O':
            print("Player 2 will go first")
            break
        else: 
            print("Invalid input. Try again! ")
            continue
    #Displays empty board
    display_board(board)
    game_is_running(running,symbols)

def game_is_running(running,symbols):
    """
    when the game starts this will run continuously to check 
    valid inputs from the user

    """

    while running:
        while True:
            #Check if position is type int
            try:   
                position = int(input("Choose your next position: (1-9)\n"))
                break
            except:
                print('Invalid input. Try again!')

        if position in range(1,10) and board[position] == ' ':
            print('\n'*100)
            board[position] = alternate_symbols(symbols)
            display_board(board)
            running = check_win(board, symbols[1])
        else:
            print('Invalid input. Try again!')
            continue

    while not running: 

            player_input = input("Play again?(yes or no) ")

            if player_input.lower() == 'yes':
                start_game()
                running = True
            if player_input.lower() == 'no':
                print("Thanks for playing!")
                return 
            else:
                continue

start_game()      
\$\endgroup\$
2
\$\begingroup\$

There are a couple easy gains to be had.

Python Version

Use Python 3. Python 2 is end-of-life as of 2020-01-01. In Python 3,

# -*- coding: utf-8 -*-

is no longer needed: Python 3 is UTF-8 by default.

Python 3.6 introduced f-strings, where the syntax is:

x = 3
text = f"You have {x} items!"

Much clearer and shorter than the .format() syntax. The % string formatting syntax should not be used at all anymore. f-strings are also fast.

PEP8

PEP 8 is a collection of style guidelines. It is highly recommended to make use of them, otherwise other Python developers will never stop reminding you. Use an editor that can format or at least warn you automatically. For this, look into tools like Pylint or black.

PEP8 examples relevant to your example:

  • global variables are all UPPERCASE
  • commas generally have trailing spaces: ["X", "O"]

Another note (not part of PEP8) is that I highly prefer double quotes (") over single-quotes ('). This is because empty strings (your code has them) can be confusing with the latter style (''), whereas they are impossible to confuse with double quotes ("").

Function attributes

Functions attributes are

def func(x):
    func.y = 3

That is, objects attached to the function object and accessible via dot notation, among other things. I don't think (and hope) that they are not part of any introductory books. Probably better to avoid them completely, especially when just starting. Thus, your display_board function would become (with PEP8-formatting through black):

def display_board(board):
    """
    Takes the board list as an argument 
    and displays it as a Tic-Tac-Toe board 
    when the game is running

    """

    row3 = "{0:^5s}||{1:^5s}||{2:^5s}".format(board[7], board[8], board[9])
    row2 = "{0:^5s}||{1:^5s}||{2:^5s}".format(board[4], board[5], board[6])
    row1 = "{0:^5s}||{1:^5s}||{2:^5s}".format(board[1], board[2], board[3])
    spaces = "{0:5s}||{1:5s}||{2:5s}".format("", "", "")
    board = [row3, row2, row1]

    for row in board[:2]:
        print(spaces + "\n" + row + "\n" + spaces)
        print("=" * 20)
    print(spaces + "\n" + row1 + "\n" + spaces)

Much better and clearer!

Function attributes can introduce a nasty state. This is definitely not what a function should do. For a given input, the return should always be the same. Functions can have side effects, but the return value should be perfectly predictable, i.e. deterministic. But using function attributes, this principle can be violated (snipet uses ipython):

In [1]: def func(x):
   ...:     return func.y + x
   ...:

In [2]: func.y = 3

In [3]: func(2)
Out[3]: 5

In [4]: func.y = 5

In [5]: func(2)
Out[5]: 7

It is clear how func(2) should always return the same thing (whatever that may be). If states are required, that is you need an object that "remembers", use classes and their version of functions, methods.

Bare try/except

Never leave an except statement naked, that is:

try:   
    position = int(input("Choose your next position: (1-9)\n"))
    break
except:
    print('Invalid input. Try again!')

Should be:

try:   
    position = int(input("Choose your next position: (1-9)\n"))
    break
except ValueError:
    print('Invalid input. Try again!')

since you only try to catch the failed conversion to int for invalid input. In trying out your game, the except also caught KeyboardInterrupt, which is hugely confusing to the user. There has to be a way to exit the game anytime.

Specifying a specific exception (can also be multiple, separated by commas) is much clearer, easier to understand and your program will actually break when something unexpected (unexcepted?) happens. This is what you want to handle such cases properly. It is poor practice if you try to catch a specific exception to handle it, but also catch another, unrelated one and handle that case also, but of course entirely wrong. This is unpredictable behavior that will be hard to debug.

| improve this answer | |
\$\endgroup\$

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.