Bringing Things Together - Learning to Program with Python 3 (basics)




Welcome to part 13 of the Python 3 basics tutorial series. Here, we're going to be bringing our TicTacToe game together, so let's just get started. We left off with the following code, including the multi-line comments:

game = [[2, 2, 2],
        [0, 1, 2],
        [0, 0, 1]]


def win(current_game):
    # horizontal
    for row in game:
        print(row)
        if row.count(row[0]) == len(row) and row[0] != 0:
            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 check.count(check[0]) == len(check) and check[0] != 0:
            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 diags.count(diags[0]) == len(diags) and diags[0] != 0:
        print(f"Player {diags[0]} has won Diagonally (/)")

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

    if diags.count(diags[0]) == len(diags) and diags[0] != 0:
        print(f"Player {diags[0]} has won Diagonally (\\)")


win(game)


'''game = [[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]]


def game_board(game_map, player=0, row=0, column=0, just_display=False):
    try:
        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


game = game_board(game, player=1, row=3, column=1)'''

So now, let's remove the top game variable that we used to test, remove the win(game), and uncomment the rest.

I'll move the empty starting game to the top next, and what we have now is:

game = [[0, 0, 0],
        [0, 0, 0],
        [0, 0, 0]]


def win(current_game):
    # horizontal
    for row in game:
        print(row)
        if row.count(row[0]) == len(row) and row[0] != 0:
            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 check.count(check[0]) == len(check) and check[0] != 0:
            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 diags.count(diags[0]) == len(diags) and diags[0] != 0:
        print(f"Player {diags[0]} has won Diagonally (/)")

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

    if diags.count(diags[0]) == len(diags) and diags[0] != 0:
        print(f"Player {diags[0]} has won Diagonally (\\)")


def game_board(game_map, player=0, row=0, column=0, just_display=False):
    try:
        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


game = game_board(game, player=1, row=3, column=1)

Now, let's add a main loop, with the following:

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

We'll ask the players at the end if they want to play again. So long as they do, this loop will infinitely run. Each time this loop runs, it will set the game to the starting point.

Now, before we can start to play, we have to pick which player gets to go first. This first mover advantage is quite large in TicTacToe, so we should choose it randomly. How might we do this? Well, how random do you want to get? I vote we just use some pre-packaged random library. Secrets is one option with Python 3, which is definitely a better choice than the actual random library, which is in no way a secure random, so you might as well just ... never use it, because why. Anyway, we're going to use secrets from the standard libary. The standard library, like the built-in functions, just simply comes with Python. Unlike the built-in functions though, you have to actually import it, because these are entire libraries/packages, not just simple functions. A package or library may be just another script, or it could be a collection of python scripts. Let's bring in secrets by importing it at the top of our program

import secrets

Now we can use a cryptographically strong method for picking the starting player for TicTacToe!

current_player = secrets.choice(players)

Okay, so then let's begin a game with:

    game_won = False
    current_player = secrets.choice(players)
    game_board(game, just_display=True)
    while not game_won:

Okay, now we play til someone has won. We need to get the row and column that the user wishes to play, so let's do that next. We can do that with the... input() built-in function. This function allows us to print a string to the console, and then returns whatever the person types back into the console.

        column_choice = input("Which column? ")
        row_choice = input("Which row? ")

The return on the input() function is a string though, so lets convert that to an int using the int() built-in function, giving us:

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

    game_won = False
    current_player = secrets.choice(players)
    game_board(game, just_display=True)
    while not game_won:
        print(f"Player: {current_player}")
        column_choice = int(input("Which column? "))
        row_choice = int(input("Which row? "))

Now we need to play, then switch players:

        game = game_board(game, player=current_player, row=row_choice, column=column_choice)

        other_player = players.copy()
        other_player.remove(current_player)
        current_player = other_player[0]

Probably a better way to switch players, but this is the first thing I thought of.

Another way from Daniel suggested was:

import secrets

players = [1, 2]
current_player = secrets.randbelow(len(players))
while True:
    current_player = int(not current_player)
    print(players[current_player])

But then I decided to google some things. I started typing "Python flip between" and Google suggested 0 and 1. Okay, so I tried that first. Found this: Swapping 1 with 0 and 0 with 1 in a Pythonic way.

I scrolled down and found:

i = (1,0)[i]

Ooooh, I like that, very clean. Requires us to use 0 and 1 rather than 1 and 2, but we can always just add one later:

import secrets

players = (1, 0)
current_player = secrets.choice(players)
for i in range(10):
    print(current_player+1)
    current_player = players[current_player]

But still, I am bothered. I feel like rotating through a list is a thing...right? Surely there's something for this. I go back to google and search python flip between numbers

I landed here: Python: How to toggle between two values. Oooooh, even better! Check that out:

itertools.cycle()

I've never heard of this til just now. Cool, I learned something new! I already spot a slight issue with what we're seeing here, but let's try it out. So the idea is that you apply the cycle to some list. Then you call your var.next() and go forward however long you want. Let's see how that goes. We can quickly test this with:

import itertools

player_choice = itertools.cycle([1, 2])

for i in range(5):
    print(player_choice.next())

Hmmm:

Traceback (most recent call last):
  File "C:\Users\H\Desktop\tsting.py", line 29, in 
    print(player_choice.next())
AttributeError: 'itertools.cycle' object has no attribute 'next'
>>> 

So the answer to this question is over 6 years old. It's Python 2, and Python 3 doesn't use .next() anymore, it uses...next() as a built-in function instead.

That said, you could google AttributeError: 'itertools.cycle' object has no attribute 'next', since you probably didn't know this.

You will probably land here: itertools.cycle().next()?... where you will find the top answer:

iter.next() was removed in python 3. Use next(iter) instead. So in your example change itertools.cycle().next() to next(itertools.cycle())

Okay, easy enough to figure out:

import itertools

player_choice = itertools.cycle([1, 2])

for i in range(5):
    print(next(player_choice))
1
2
1
2
1
>>> 

Okay, I like this answer. The only thing it doesn't do for us is randomly choose the starting player. It will always be player 1 to start. We could use the random module (import random) and then use random.shuffle() on our list like: player_choice = itertools.cycle(random.shuffle([1, 2]))... or the players can just swap positions themselves.

Okay good enough, I am going to move forward with the itertools.cycle() because I just learned it and I think that's cool.

So, I will remove our secrets import, then import itertools. Then, we'll change current_player

    player_cycle = itertools.cycle([1, 2])

Then, we can say current_player = next(player_cycle), then remove all of our code for changing players, so now our main loop is:

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)
        print(f"Player: {current_player}")
        column_choice = int(input("Which column? "))
        row_choice = int(input("Which row? "))

        game = game_board(game, player=current_player, row=row_choice, column=column_choice)

Full code up to this point:

import itertools


def win(current_game):
    # horizontal
    for row in game:
        print(row)
        if row.count(row[0]) == len(row) and row[0] != 0:
            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 check.count(check[0]) == len(check) and check[0] != 0:
            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 diags.count(diags[0]) == len(diags) and diags[0] != 0:
        print(f"Player {diags[0]} has won Diagonally (/)")

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

    if diags.count(diags[0]) == len(diags) and diags[0] != 0:
        print(f"Player {diags[0]} has won Diagonally (\\)")


def game_board(game_map, player=0, row=0, column=0, just_display=False):
    try:
        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)
        print(f"Player: {current_player}")
        column_choice = int(input("Which column? "))
        row_choice = int(input("Which row? "))

        game = game_board(game, player=current_player, row=row_choice, column=column_choice)

A few more things for us to still clean up at this point, but we're almost done. In the next tutorial, we will fix some scaling issues, some hard-coding issues, repetition issues, as well as actually finishing up the game's loop to actually end games and then either quit the program or restart the game.

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)