Beginner learner: Python 3.9.6 tic-tac-toe code and questions about optimization

I am just beginning to learn Python, and thought I would give myself a challenge using the lessons I have learned so far. I am following the Udemy course, Automate the Boring Stuff with Python. The point that I've reached so far has covered the basics of functions, handling errors, lists and dictionaries.

I wanted to see if I had a grasp of what I'd learned by making a tic-tac-toe game with certain features/challenges/objectives in mind:

• make use of loops, functions, methods, lists/dictionaries.
• the player would need to see the board and what spots were taken/left
• I wanted players to be able to type in the choice in upper/lower/mixed case
• the game had to recognize all win conditions, lose conditions, and stalemate conditions.
• the opponent (in this case, the CPU) would need to only have access to the choices remaining. For now, it would be random, but maybe I would come back to the code to have some form of AI.
• When the game ends, give the player the choice of starting a new game, and keep track of the score.

This is my code, and as far as I can see, it works and I managed to complete all the goals I set out to do:


import copy
import time
import random
import sys

##Board design to make it look pretty
def printBoard(board):
print('')
print(board['top-l'] + '|' + board['top-m'] + '|' + board['top-r'])
print('-----')
print(board['mid-l'] + '|' + board['mid-m'] + '|' + board['mid-r'])
print('-----')
print(board['low-l'] + '|' + board['low-m'] + '|' + board['low-r'])
print('')

##Function for initiating the board back to blank
def blankBoard(zeroBoard):
for value in zeroBoard:
zeroBoard[value] = ' '

##Function for initiating a New Game after a game over situation
def newGame(YorN):
print('\n ~ Would you like to play again? if yes, type ''Y'': ~ ')
newGame = input()
if newGame.upper() == 'Y':
print(' ~ okay, please wait while we set up a new game! ~ ')
blankBoard(theBoard)
time.sleep(random.randint(1,2))
printBoard(theBoard)
else:
print('\n ~ Bye and thank you for playing!! ~ ')
sys.exit()

##Function containing all the win conditions
def winCondition(winner):
if ((winner['top-l'] == winner['top-m'] == winner['top-r'] == 'X') or
(winner['mid-l'] == winner['mid-m'] == winner['mid-r'] == 'X') or
(winner['low-l'] == winner['low-m'] == winner['low-r'] == 'X') or
(winner['top-l'] == winner['mid-l'] == winner['low-l'] == 'X') or
(winner['top-m'] == winner['mid-m'] == winner['low-m'] == 'X') or
(winner['top-r'] == winner['mid-r'] == winner['low-r'] == 'X') or
(winner['top-l'] == winner['mid-m'] == winner['low-r'] == 'X') or
(winner['top-r'] == winner['mid-m'] == winner['low-l'] == 'X')):
print('\n ~ Congratulations! You are a winner!  ')
return 'win'

##Function containing all the loss conditions
def loseCondition(loser):
if ((loser['top-l'] == loser['top-m'] == loser['top-r'] == 'O') or
(loser['mid-l'] == loser['mid-m'] == loser['mid-r'] == 'O') or
(loser['low-l'] == loser['low-m'] == loser['low-r'] == 'O') or
(loser['top-l'] == loser['mid-l'] == loser['low-l'] == 'O') or
(loser['top-m'] == loser['mid-m'] == loser['low-m'] == 'O') or
(loser['top-r'] == loser['mid-r'] == loser['low-r'] == 'O') or
(loser['top-l'] == loser['mid-m'] == loser['low-r'] == 'O') or
(loser['top-r'] == loser['mid-m'] == loser['low-l'] == 'O')):
print('\n ~ Aww man, you''re are a loser! ~ ')
return 'lose'

##Function for Computer Players Choice
def computerChoice(cpuChoice):
theBoardCPU = copy.deepcopy(theBoardChoices)
print(','.join(theBoardCPU.keys()))
cpuChoice = random.choice(list(theBoardCPU.keys()))
print(' ~ ' + cpuChoice + ' ~ ')
del theBoardChoices[cpuChoice]
theBoard[cpuChoice] = 'O'
printBoard(theBoard)

##Blank board to begin with and making a copy of the board for choices remaining, and finally printing a pretty board
theBoard = {'top-l': ' ', 'top-m': ' ', 'top-r': ' ',
'mid-l': ' ', 'mid-m': ' ', 'mid-r': ' ',
'low-l': ' ', 'low-m': ' ', 'low-r': ' '}
theBoardChoices = copy.deepcopy(theBoard)
printBoard(theBoard)

##The actual game
print(' ~ Hello player. What is your name? ~ ')
player1Name = input()
print(' ~ Hello ' + player1Name + ', welcome to tic-tac-toe ~ ')
playerScore = 0
CPUScore = 0

while theBoardChoices != None:
print(' ~ ' +player1Name + ', please type in one of the options for blank spaces: ~ ')
print('')
print(' , '.join(theBoardChoices.keys()))
playerInput = input()
playerChoice = playerInput.lower()

if theBoard[playerChoice] == ' ':
theBoard[playerChoice] = 'X'
printBoard(theBoard)
del theBoardChoices[playerChoice]
if winCondition(theBoard) == 'win':
theBoardChoices = copy.deepcopy(theBoard)
playerScore = playerScore + 1
print('  ~  the current score is ' + str(playerScore) +'-' + str(CPUScore) + '  ~  ')
newGame(theBoardChoices)
continue
if theBoardChoices == {}:
theBoardChoices = copy.deepcopy(theBoard)
newGame(theBoardChoices)
continue
print(' ~ Please wait. Computer making a choice... ~ ')
time.sleep(random.randint(1,2))
computerChoice(theBoard)
if loseCondition(theBoard) == 'lose':
theBoardChoices = copy.deepcopy(theBoard)
CPUScore = CPUScore + 1
print('  ~  the current score is ' + str(playerScore) +'-' + str(CPUScore) + '  ~  ')
newGame(theBoardChoices)
continue

else:
print(' ~ that space is already taken ~ ')


My questions regarding this code are as follows:

1. My win and lose condition functions seem very bulky, but I couldn't think of another way of reducing that down or preferably making them into one function. I thought maybe I could make something that would be 'if these three keys values = x', and then if 'x = O then lose', or 'x = X then lose'. But I think my understanding of functions is still a bit cloudy. Is there a way to make a function that would generate that list of win/loss conditions using my set up? Or would I have had to made the board a list instead of a dictionary in order to use a more mathematical approach to reduce that?

2. I have a bunch of nested if's in my while statement. I feel like I could have made more use of functions to reduce that, but even so, I'm wondering if there are issues down the line of having that.

3. I was planning on having a try/except in there, but by the time I reached the code that I have, I couldn't find a way to break it to make it active. Should I have put that in there any ways? I feel like always having that back up system there is a good habit, even if the code seems unbreakable. Or would that just make the code more messy and a waste of space?

4. In general, I know that old habits die hard, and that can be true of bad habits too. I would very much like to know if there are signs of bad things in my code that I can be wary of so I can nip it in the bud before I keep going. My plan is to revisit this code in the future and add to it (option of 2 human players or vs CPU or 2 CPU's face eachother, some AI of sorts, option of letting players set the size of the board, etc).

Anyways, I am looking forward to any feed back!

• Just a quick comment addressing question (4): your use of del and copy.deepcopy is probably one of the bad habits that you don't want to develop. Usually you don't want or need this type of memory manipulation in Python. Instead of copying from a template, and then deleting from that copy, you would rather use a list that manages the remaining valid keys. For this you may want to learn the appropriate methods to remove elements from containers (e.g. pop() for dictionaries or remove() and pop() for lists). Aug 12 at 14:17
• @schmuddi - thanks for the advice! I'm a bit confused as to the different of what I did and what you are suggesting. In my case, I've created a dictionary/list of the remaining choices and removing data from it, and then essentially just resetting it upon a new game. In your suggestion, I would, say, be taking from a list using pop(), and placing that in a new list regardless? I'm not doubting that your approach is better, but I just am not seeing how one is more efficient (unless of course there is a situation I'm not thinking about!) Aug 12 at 23:03

Great start but there's a long list of typical things that bite beginners.

• Use an IDE (PyCharm or otherwise) that has an integrated linter able to point out when you're violating PEP8. Listen to its suggestions and learn why they're being made. For instance, printBoard should be print_board.
• Add some PEP484 type hints, so that you can marginally shift Python's typing from "anything goes, no static analysis possible" to "slightly constrained though still not in runtime"
• A dict is a bad representation for your board. Better data structures include a lists of lists, or a two-dimensional Numpy ndarray.
• What even is YorN? It's never used; delete it
• Avoid the double-'' escape by using a string literal defined by " instead of '
• you''re are a loser! -> you are a loser!
• Rather than expressions like ' ~ the current score is ' + str(playerScore) +'-' + str(CPUScore) + ' ~ ', use an f-string like f' ~ the current score is {player_score}-{cpu_score} ~ '
• Rather than playerScore = playerScore + 1, use in-place addition: player_score += 1.
• Do not print on the inside of lose_condition, and do not return a string from lose_condition. Return a boolean, and let the caller decide what to print.

Here's a pet peeve of mine - lying to your user. You say

please wait while we set up a new game!

Please wait. Computer making a choice...

which implies that you're working hard to actually do something while the program hangs; but that's not true. Don't sleep and lie about it.

There are other things but this should be a good start.

• Thanks so much for all the helpful feedback! I had no idea there was a generally accepted style guide for programming, but thinking about it that does make a lot of sense. I've only seen NumPy mentioned in a few posts, but I didn't want to overwhelm myself in the beginning and start learning things when I don't fully grasp the basics yet. I've added that to my growing list of tings to look into. Seems like I have a habit of over complicating things and it's really showing in my code. I will be taking a look at the changes you've suggested and refresh myself on the lessons. Thanks again! Aug 14 at 0:17

1. When building something don't think about what data structures or syntax features are you going to use. You can search for example "python dictionary/loops exercises" for practicing those basics, but when building an app, split the main goal into steps and consequently code them up. Focus on HOW you will do a certain task.

You will definetly have those basics set-in-stone later on, becuase you will implement them in almost every project you make.

About the code, try to seperate functions that are related to the same thing in different .py files like modules (if you have heart about them) and then use importing.Use this for not only not making a mess, but also for easily finding a bug or an error. That will make your code cleaner.

1. In your code you use camelCase for your variable names and functions, but in python it will be better to use snake_case, because most of python's methods and builtin functions are in snake_case.

2. Separate functions with 2 and loops/conditions with 1 blank line. That will make your code easier to read. In the end there's a big block of code, which is pain to figure out for me.

You will definitely see the difference:

while theBoardChoices != None:

print(' ~ ' +player1Name + ', please type in one of the options for blank spaces: ~ ')
print('')
print(' , '.join(theBoardChoices.keys()))

playerInput = input()
playerChoice = playerInput.lower()

if theBoard[playerChoice] == ' ':
theBoard[playerChoice] = 'X'
printBoard(theBoard)

del theBoardChoices[playerChoice]

if winCondition(theBoard) == 'win':
theBoardChoices = copy.deepcopy(theBoard)
playerScore = playerScore + 1

print('  ~  the current score is ' + str(playerScore) +'-' + str(CPUScore) + '  ~  ')

newGame(theBoardChoices)
continue

if theBoardChoices == {}:
theBoardChoices = copy.deepcopy(theBoard)

newGame(theBoardChoices)
continue

print(' ~ Please wait. Computer making a choice... ~ ')
time.sleep(random.randint(1,2))
computerChoice(theBoard)

if loseCondition(theBoard) == 'lose':
theBoardChoices = copy.deepcopy(theBoard)
CPUScore = CPUScore + 1

print('  ~  the current score is ' + str(playerScore) +'-' + str(CPUScore) + '  ~  ')

newGame(theBoardChoices)
continue

else:
print(' ~ that space is already taken ~ ')

1. I didn't get why your comments start with ## and not just #. Comment the bodies of functions too.
def func():
print("something") # <-- the body of a function (stuff, that goes inside of a function)


It's very good, that you want to practice the things you've learned, but learn a little more and try this again...You'll feel the improvement...