Wrapping up Tic Tac Toe - Learning to Program with Python 3 (basics)




Welcome to part 14 of the Python 3 basics series. Things are definitely cleaning up, we just have a few more things to conquer here. We need to stop players from playing over one another, we need to fix repetition in the win function, we need to fix the scalability of the column #s, and, finally, we need to actually return winners and either quit/restart the game!

To begin, let's solve the players playing over players issue. We just need to make sure the place the person intends to play is not currently occupied. That's do-able in a variety of ways. We'd like to let the user try again if the move is invalid. So far, we've been returning False if the player tries something that they can't, so let's just continue that in the game_board function. We can just add it near the top:

def game_board(game_map, player=0, row=0, column=0, just_display=False):

    try:
        if game_map[row][column] != 0:
            print("This space is occupied, try another!")
            return False

Okay, let's add some handling in the main loop for this:

    while not game_won:
        current_player = next(player_cycle)
        played = False
        while not played:
            print(f"Player: {current_player}")
            column_choice = int(input("Which column? "))
            row_choice = int(input("Which row? "))
            played = game_board(game, player=current_player, row=row_choice, column=column_choice)

Let's test things up to this point now:

C:\Users\H\Desktop\python3-updated-series>py -3.6 part14.py
   0  1  2
0 [0, 0, 0]
1 [0, 0, 0]
2 [0, 0, 0]
Player: 1
Which column? 9
Which row? 9
Did you attempt to play a row or column outside the range of 0,1 or 2? (IndexError)
Player: 1
Which column? 1
Which row? 1
   0  1  2
0 [0, 0, 0]
1 [0, 1, 0]
2 [0, 0, 0]
Player: 2
Which column? 1
Which row? 1
This space is occupied, try another!
Player: 2
Which column?

Great, this seems to be working. Next up, let's fix the repeated "win" check bit.

First, let's just make a quick function:

    def all_same(l):
        if l.count(l[0]) == len(l) and l[0] != 0:
            return True
        else:
            return False

This will go inside our win function and then we can use this with everything here. Example:

def win(current_game):

    def all_same(l):
        if l.count(l[0]) == len(l) and l[0] != 0:
            return True
        else:
            return False

    # horizontal
    for row in game:
        print(row)
        if all_same(row):
            print(f"Player {row[0]} is the winner horizontally!")

Full win function becomes:

def win(current_game):

    def all_same(l):
        if l.count(l[0]) == len(l) and l[0] != 0:
            return True
        else:
            return False

    # horizontal
    for row in game:
        print(row)
        if all_same(row):
            print(f"Player {row[0]} is the winner horizontally!")

    # vertical
    for col in range(len(game[0])):
        check = []
        for row in game:
            check.append(row[col])
        if all_same(check):
            print(f"Player {check[0]} is the winner vertically!")

    # / diagonal
    diags = []
    for idx, reverse_idx in enumerate(reversed(range(len(game)))):
        diags.append(game[idx][reverse_idx])

    if all_same(diags):
        print(f"Player {diags[0]} has won Diagonally (/)")

    # \ diagonal
    diags = []
    for ix in range(len(game)):
        diags.append(game[ix][ix])

    if all_same(diags):
        print(f"Player {diags[0]} has won Diagonally (\\)")

We've not saved any lines. In fact, we added some, but this is much cleaner, and, let's say we changed something where no longer the value in the cell needed to not be 0, and instead maybe a space, or something like that. Now, we just need to change this in one place, rather than 4.

Now, let's actually return winners. Again, we can just use our win function for this to either return a True or False. After each of the winning print outs, we can add a return True. At the end, we return False. Once a function reaches a return, it's done running stuff. If we make it all the way to the end, that means no one won. Full function:

def win(current_game):

    def all_same(l):
        if l.count(l[0]) == len(l) and l[0] != 0:
            return True
        else:
            return False

    # horizontal
    for row in game:
        print(row)
        if all_same(row):
            print(f"Player {row[0]} is the winner horizontally!")
            return True

    # vertical
    for col in range(len(game[0])):
        check = []
        for row in game:
            check.append(row[col])
        if all_same(check):
            print(f"Player {check[0]} is the winner vertically!")
            return True

    # / diagonal
    diags = []
    for idx, reverse_idx in enumerate(reversed(range(len(game)))):
        diags.append(game[idx][reverse_idx])

    if all_same(diags):
        print(f"Player {diags[0]} has won Diagonally (/)")
        return True

    # \ diagonal
    diags = []
    for ix in range(len(game)):
        diags.append(game[ix][ix])

    if all_same(diags):
        print(f"Player {diags[0]} has won Diagonally (\\)")
        return True

    return False

Now, let's use the win() function in our main loop, with something like:

        if win(game):
            game_won = True
            again = input("The game is over, would you like to play again? (y/n) ")
            if again.lower() == "y":
                print("restarting!")
            elif again.lower() == "n":
                print("Byeeeee!!!")
                play = False
            else:
                print("Not a valid answer, but... c ya!")
                play = False

Full code now:

import itertools


def win(current_game):

    def all_same(l):
        if l.count(l[0]) == len(l) and l[0] != 0:
            return True
        else:
            return False

    # horizontal
    for row in game:
        print(row)
        if all_same(row):
            print(f"Player {row[0]} is the winner horizontally!")
            return True

    # vertical
    for col in range(len(game[0])):
        check = []
        for row in game:
            check.append(row[col])
        if all_same(check):
            print(f"Player {check[0]} is the winner vertically!")
            return True

    # / diagonal
    diags = []
    for idx, reverse_idx in enumerate(reversed(range(len(game)))):
        diags.append(game[idx][reverse_idx])

    if all_same(diags):
        print(f"Player {diags[0]} has won Diagonally (/)")
        return True

    # \ diagonal
    diags = []
    for ix in range(len(game)):
        diags.append(game[ix][ix])

    if all_same(diags):
        print(f"Player {diags[0]} has won Diagonally (\\)")
        return True

    return False


def game_board(game_map, player=0, row=0, column=0, just_display=False):

    try:
        if game_map[row][column] != 0:
            print("This space is occupied, try another!")
            return False

        print("   0  1  2")
        if not just_display:
            game_map[row][column] = player
        for count, row in enumerate(game_map):
            print(count, row)
        return game_map
    except IndexError:
        print("Did you attempt to play a row or column outside the range of 0,1 or 2? (IndexError)")
        return False
    except Exception as e:
        print(str(e))
        return False


play = True
players = [1, 2]
while play:
    game = [[0, 0, 0],
            [0, 0, 0],
            [0, 0, 0]]

    game_won = False
    player_cycle = itertools.cycle([1, 2])
    game_board(game, just_display=True)
    while not game_won:
        current_player = next(player_cycle)
        played = False
        while not played:
            print(f"Player: {current_player}")
            column_choice = int(input("Which column? "))
            row_choice = int(input("Which row? "))
            played = game_board(game, player=current_player, row=row_choice, column=column_choice)

        if win(game):
            game_won = True
            again = input("The game is over, would you like to play again? (y/n) ")
            if again.lower() == "y":
                print("restarting!")
            elif again.lower() == "n":
                print("Byeeeee!!!")
                play = False
            else:
                print("Not a valid answer, but... c ya!")
                play = False

We're basically done, but we gotta fix the print(" 0 1 2") bit to be dynamic.

How can we do this? We've seen that we can use + with strings, and we know about loops. Let's just make a quick basic example. We're going to start with 3 spaces, then we need to add a number and 2 spaces between them. Let's see...

Since we need to start with 3 spaces, and we know that we want to have 2 spaces between each number, we can do something like start with 1 space, then use a for loop to add 2 spaces and a number per loop. Something like:

s = " "
for i in range(3):
    s += "  "+str(i)

print(s)

The 3 in range can just be dervied from the game's size later.

There's also a string method to achieve this same task called .join. Then we can use list comprehension, like:

s = "   "+"  ".join([str(i) for i in range(3)])
print(s)

If the list comprehension doesn't make sense to you, use our original example, that's fine!

So, either build s and print it in place of our original print there, or do:

print("   "+"  ".join([str(i) for i in range(len(game_map))]))

That's it. We're done. Full code:

import itertools


def win(current_game):

    def all_same(l):
        if l.count(l[0]) == len(l) and l[0] != 0:
            return True
        else:
            return False

    # horizontal
    for row in game:
        print(row)
        if all_same(row):
            print(f"Player {row[0]} is the winner horizontally!")
            return True

    # vertical
    for col in range(len(game[0])):
        check = []
        for row in game:
            check.append(row[col])
        if all_same(check):
            print(f"Player {check[0]} is the winner vertically!")
            return True

    # / diagonal
    diags = []
    for idx, reverse_idx in enumerate(reversed(range(len(game)))):
        diags.append(game[idx][reverse_idx])

    if all_same(diags):
        print(f"Player {diags[0]} has won Diagonally (/)")
        return True

    # \ diagonal
    diags = []
    for ix in range(len(game)):
        diags.append(game[ix][ix])

    if all_same(diags):
        print(f"Player {diags[0]} has won Diagonally (\\)")
        return True

    return False


def game_board(game_map, player=0, row=0, column=0, just_display=False):

    try:
        if game_map[row][column] != 0:
            print("This space is occupied, try another!")
            return False

        print("   "+"  ".join([str(i) for i in range(len(game_map))]))
        if not just_display:
            game_map[row][column] = player
        for count, row in enumerate(game_map):
            print(count, row)
        return game_map
    except IndexError:
        print("Did you attempt to play a row or column outside the range of 0,1 or 2? (IndexError)")
        return False
    except Exception as e:
        print(str(e))
        return False


play = True
players = [1, 2]
while play:
    game = [[0, 0, 0],
            [0, 0, 0],
            [0, 0, 0]]

    game_won = False
    player_cycle = itertools.cycle([1, 2])
    game_board(game, just_display=True)
    while not game_won:
        current_player = next(player_cycle)
        played = False
        while not played:
            print(f"Player: {current_player}")
            column_choice = int(input("Which column? "))
            row_choice = int(input("Which row? "))
            played = game_board(game, player=current_player, row=row_choice, column=column_choice)

        if win(game):
            game_won = True
            again = input("The game is over, would you like to play again? (y/n) ")
            if again.lower() == "y":
                print("restarting!")
            elif again.lower() == "n":
                print("Byeeeee!!!")
                play = False
            else:
                print("Not a valid answer, but... c ya!")
                play = False

Now, finally, if you desired, you ... cound allow users to dynamically size the game as well, so here's just a bonus. For example:

game_size = 5

game = []
for i in range(game_size):
    row = []
    for i in range(game_size):
        row.append(0)
    game.append(row)

print(game)
[[0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0]]

All set. The rest of our code should handle this just fine. We can also actually do the above in all 1 line, again using list comprehension:

game = [[0 for i in range(game_size)] for i in range(game_size)]
print(game)

So, we can do the following in our loop:

play = True
players = [1, 2]
while play:

    game_size = int(input("What size game TicTacToe? "))
    game = [[0 for i in range(game_size)] for i in range(game_size)]

Then we have a varying-sized game. Pretty nifty!

Okay, that's enough of the basics of Python syntax. We're *definitely* missing a lot of stuff. For example, a super integral bit to python is the dictionary, which is a way to store values by keys, for example. It's just not come up yet, but it exists and is super useful. There are many other useful things like that, which we've just not covered.

That said, you should have a grasp of Python now.

The next tutorial:





  • Introduction to Python 3 (basics) - Learning to Program with Python 3
  • Tuples, Strings, Loops - Learning to Program with Python 3 (basics)
  • Lists and Tic Tac Toe Game - Learning to Program with Python 3 (basics)
  • Built-in Functions - Learning to Program with Python 3 (basics)
  • Indexes and Slices - Learning to Program with Python 3 (basics)
  • Functions - Learning to Program with Python 3 (basics)
  • Function Parameters and Typing - Learning to Program with Python 3 (basics)
  • Mutability Revisited - Learning to Program with Python 3 (basics)
  • Error Handling - Learning to Program with Python 3 (basics)
  • Calculating Horizontal Winner (tic tac toe) - Learning to Program with Python 3 (basics)
  • Vertical Winners - Learning to Program with Python 3 (basics)
  • Diagonal Winners - Learning to Program with Python 3 (basics)
  • Bringing Things Together - Learning to Program with Python 3 (basics)
  • Wrapping up Tic Tac Toe - Learning to Program with Python 3 (basics)
  • Conclusion - Learning to Program with Python 3 (basics)