We’re rewarding the question askers & reputations are being recalculated! Read more.

2 added 1180 characters in body

## Making Moves

# Declare globally at the beginning
moves_left = set(range(9))

# Your while loop can now be to check if the set is empty or not
while moves_left: # A populated set acts as True
my_move = moves_left.pop(random.choice(moves_left))

# Now moves_left has one fewer element


Taking this idea a little further, you could combine the user's move with the npc's move in one function:

# The npc default will allow you to set it to True if it's
# npc's turn, otherwise, no args need to be supplied
def make_move(npc=False):

if npc is False:
user_move = "" # dummy default to kick off while loop
while user_move not in moves_left:
try:
user_move = int(input(f"Choose a move out of {moves_left}: "))
return moves_left.pop(user_move)
except ValueError, KeyError: # invalid int conversion or not in moves_left
print("Invalid move")
continue

else:
return moves_left.pop(random.choice(moves_left))


You can then call it like:

moves_left = set(range(9)) # At beginning of game

npc_move = make_move(npc=True)
3
user_move = make_move()

Choose a move out of {0, 1, 2, 4, 5, ,6 ,7, 8}: a
Invalid move
Choose a move out of {0, 1, 2, 4, 5, ,6 ,7, 8}: 3
Invalid move
Choose a move out of {0, 1, 2, 4, 5, ,6 ,7, 8}: 4

user_move
4
$$$$

# Declare globally at the beginning
moves_left = set(range(9))

# Your while loop can now be to check if the set is empty or not
while moves_left: # A populated set acts as True
my_move = moves_left.pop(random.choice(moves_left))

# Now moves_left has one fewer element


## Making Moves

# Declare globally at the beginning
moves_left = set(range(9))

# Your while loop can now be to check if the set is empty or not
while moves_left: # A populated set acts as True
my_move = moves_left.pop(random.choice(moves_left))

# Now moves_left has one fewer element


Taking this idea a little further, you could combine the user's move with the npc's move in one function:

# The npc default will allow you to set it to True if it's
# npc's turn, otherwise, no args need to be supplied
def make_move(npc=False):

if npc is False:
user_move = "" # dummy default to kick off while loop
while user_move not in moves_left:
try:
user_move = int(input(f"Choose a move out of {moves_left}: "))
return moves_left.pop(user_move)
except ValueError, KeyError: # invalid int conversion or not in moves_left
print("Invalid move")
continue

else:
return moves_left.pop(random.choice(moves_left))


You can then call it like:

moves_left = set(range(9)) # At beginning of game

npc_move = make_move(npc=True)
3
user_move = make_move()

Choose a move out of {0, 1, 2, 4, 5, ,6 ,7, 8}: a
Invalid move
Choose a move out of {0, 1, 2, 4, 5, ,6 ,7, 8}: 3
Invalid move
Choose a move out of {0, 1, 2, 4, 5, ,6 ,7, 8}: 4

user_move
4
$$$$

1

You lose a bit of performance and readability by defining check_winner inside your while loop. move_count, board etc are all in global scope, even though they are within that loop:

def check_winner():
# Rest of function

while True:


The def end() could also be moved to global scope, because again you are redefining it during every iteration which isn't what you want.

The new_list = winning_list doesn't do anything, it copies the reference from winning_list and the two variables are tied together unless you did a deep_copy, which creates a new object. Furthermore, I don't really see any use of new_list anywhere, so you can just drop that line entirely.

As @AJNewfeld pointed out, the global move_count can be dropped because, again, move_count is already global and is accessible by all check_winner, as it will look in the locals() mapping first, if move_count isn't in the local mapping (from positional or keyword args taken in by the function), it will search globals(). A NameError is only raised when those don't contain the variable you are looking for.

The while loop for npc can be easily refactored so that you aren't possibly iterating over the entire board, and makes the code a bit easier to read. Your board is made up of either two entries: int for open spots and str for taken spots. This means that npc's move can be a function like so:

def npc_move():
# This will give you only the indices for spots that have yet to be taken
remaining_spots = [i for i, value in enumerate(board) if isinstance(value, int)]
return random.choice(remaining_spots)


Or you could also use a set() globally to represent remaining spots and pop indices out of it:

# Declare globally at the beginning
moves_left = set(range(9))

# Your while loop can now be to check if the set is empty or not
while moves_left: # A populated set acts as True
my_move = moves_left.pop(random.choice(moves_left))

# Now moves_left has one fewer element