Back to Basics: Tic Tac Toe - follow-up Follow-up from: Back to Basics - Tic Tac Toe


Again, any optimizations and critique is welcome! I'm particularly interested if it involves numpy, scipy | bitwise operators.

import numpy as np
from random import randint


def print_board(board):
    print("""
  +---+---+---+
c | {}   {}   {} |
  +   +   +   +
b | {}   {}   {} |
  +   +   +   +
a | {}   {}   {} |
  +---+---+---+
    1   2   3
""".format(*list(x if x != '0' else ' ' for x in board)))


def check_for_win(board):

    def check_diagonal(dia):
        return '0' not in dia and len(set(dia)) == 1

    a = np.array(board).reshape(3, 3)

    # check lr diagonal
    if check_diagonal(a.diagonal()):
        return True

    # check rl diagonal
    if check_diagonal(np.fliplr(a).diagonal()):
        return True

    # check rows &amp; cols
    for matr in (a, np.transpose(a)):
        for row in matr:
            if '0' not in row and len(set(row)) == 1:
                return True

    return False


if __name__ == "__main__":
    board = ['0'] * 9
    codes = ('c1', 'c2', 'c3', 'b1', 'b2', 'b3', 'a1', 'a2', 'a3')
    turn, user = 1, 'X' if bool(randint(0, 1)) else 'O'
    print(f'Welcome! First to play is {user}!')
    print_board(board)

    while True:
        print(f'Player {user}: ')
        code = input().strip().lower()

        if code in codes:
            idx = codes.index(code)

            if board[idx] == '0':
                board[idx] = user
                print_board(board)

                if turn &gt;= 5:
                    if check_for_win(board):
                        print(f'{user} won in {turn} turns! Congratulations!')
                        break
                    elif '0' not in board:
                        print("Aw, it's a draw!")
                        break

                user = 'X' if user == 'O' else 'O'
                turn = turn + 1
            else:
                print('Woops! You need to pick an empty space.')
        else:
            print("Hmm, that's not a valid input.")
Ways to improve:


simplifying list construction:

*list(x if x != '0' else ' ' for x in board)) --> *[x if x != '0' else ' ' for x in board]
check_diagonal function. The function performs a logical check if the passed row is fully selected/marked with the same mark/code and returns boolean result respectively.Therefore, it's better to give it a more meaningful and unified name, say row_crossed (reflecting boolean purpose):

def row_crossed(row):
    return '0' not in row and len(set(row)) == 1


Besides, renaming it will serve beneficially for the next improvements (see below)
2 consecutive conditions:

if check_diagonal(a.diagonal()):
    return True

# check rl diagonal
if check_diagonal(np.fliplr(a).diagonal()):
    return True


return the same boolean result. That's a sign for Consolidate conditional expression technique.Thus, the flow becomes:

# check lr diagonal
if row_crossed(a.diagonal()) or row_crossed(np.fliplr(a).diagonal()):
    return True

the for loop (in check_for_win function) which iterates through pair of  matrices (initial and transposed one) duplicates the same logical check 

 ...
 if '0' not in row and len(set(row)) == 1:


as row_crossed (formerly check_diagonal) function does. Thus, replacing duplicated condition with function call.Eventually, the restructured check_for_win function would look as below:

def check_for_win(board):
    def row_crossed(row):
        return '0' not in row and len(set(row)) == 1

    a = np.array(board).reshape(3, 3)

    # check lr diagonal
    if row_crossed(a.diagonal()) or row_crossed(np.fliplr(a).diagonal()):
        return True

    # check rows &amp; cols
    for matr in (a, a.transpose()):
        for row in matr:
            if row_crossed(row):
                return True

    return False



Actually, the above Consolidation of conditionals can be even more encompassing with applying np.concatenate and builtin any functions, though may seem less readable but quite viable for those who likes concise/compact code:

def check_for_win(board):
    def row_crossed(row):
        return '0' not in row and len(set(row)) == 1

    a = np.array(board).reshape(3, 3)

    if row_crossed(a.diagonal()) \
            or row_crossed(np.fliplr(a).diagonal()) \
            or any(row_crossed(row) for row in np.concatenate([a, a.transpose()])):
        return True

    return False




Minor "refinements":


for matr in (a, a.transpose()) looks a bit more "connected" than for matr in (a, np.transpose(a)):
optionally, to reduce typing misses of codes = ('c1', 'c2', 'c3', 'b1', 'b2', 'b3', 'a1', 'a2', 'a3') - a good alternative is:

codes = tuple(map(''.join, itertools.product('cba', '123')))