13.2. Four-in-a-Row

Four-in-a-Row is a two-player, tile-dropping game. Each player tries to cre- ate a row of four of their tiles, whether horizontally, vertically, or diagonally. It’s similar to the board games Connect Four and Four Up. The game uses a 7 by 6 stand-up board, and tiles drop to the lowest unoccupied space in a column. In our Four-in-a-Row game, two human players, X and O, will play against each other, as opposed to one human player against the computer. ## The Output When you run the Four-in-a-Row program in this chapter, the output will look like this:

_images/%E9%80%89%E5%8C%BA_010.png _images/%E9%80%89%E5%8C%BA_011.png

Try to figure out the many subtle strategies you can use to get four tiles in a row while blocking your opponent from doing the same. ## The Source Code Open a new file in your editor or IDE, enter the following code, and save it as fourinarow.py:

>>> #### """Four-in-a-Row, by Al Sweigart al@inventwithpython.com A tile-dropping game to get four-in-a-row, similar to Connect Four."""
>>> import sys
>>> # Constants used for displaying the board:
>>> EMPTY_SPACE = "." # A period is easier to count than a space.
>>> PLAYER_X = "X"
>>> PLAYER_O = "O"
>>> # Note: Update BOARD_TEMPLATE & COLUMN_LABELS if BOARD_WIDTH is changed.
>>> BOARD_WIDTH = 7
>>> BOARD_HEIGHT = 6
>>> COLUMN_LABELS = ("1", "2", "3", "4", "5", "6", "7")
>>> assert len(COLUMN_LABELS) == BOARD_WIDTH
>>> # The template string for displaying the board:
>>> BOARD_TEMPLATE = """
>>> 1234567
>>> +-------+
>>> |{}{}{}{}{}{}{}|
>>> |{}{}{}{}{}{}{}|
>>> |{}{}{}{}{}{}{}|
>>> |{}{}{}{}{}{}{}|
>>> |{}{}{}{}{}{}{}|
>>> |{}{}{}{}{}{}{}|
>>> +-------+"""
>>> def main():
>>> # """Runs a single game of Four-in-a-Row."""
>>>     print(
>>> """Four-in-a-Row, by Al Sweigart al@inventwithpython.com
>>> Two players take turns dropping tiles into one of seven columns, trying
>>> to make Four-in-a-Row horizontally, vertically, or diagonally.
>>> """
>>> )
>>> # Set up a new game:
>>>     gameBoard = getNewBoard()
>>>     playerTurn = PLAYER_X
>>>     while True: # Run a player's turn.
>>> # Display the board and get player's move:
>>>         displayBoard(gameBoard)
>>>         playerMove = getPlayerMove(playerTurn, gameBoard)
>>>         gameBoard[playerMove] = playerTurn
>>> # Check for a win or tie:
>>>         if isWinner(playerTurn, gameBoard):
>>>             displayBoard(gameBoard) # Display the board one last time.
>>>             print("Player {} has won!".format(playerTurn))
>>>             sys.exit()
>>>         elif isFull(gameBoard):
>>>             displayBoard(gameBoard) # Display the board one last time.
>>>             print("There is a tie!")
>>>             sys.exit()
>>> # Switch turns to other player:
>>>         if playerTurn == PLAYER_X:
>>>             playerTurn = PLAYER_O
>>>         elif playerTurn == PLAYER_O:
>>>             playerTurn = PLAYER_X
>>> def getNewBoard():
>>> # """Returns a dictionary that represents a Four-in-a-Row board.
>>> # The keys are (columnIndex, rowIndex) tuples of two integers, and the
>>> # values are one of the "X", "O" or "." (empty space) strings."""
>>>     board = {}
>>>     for rowIndex in range(BOARD_HEIGHT):
>>>         for columnIndex in range(BOARD_WIDTH):
>>>             board[(columnIndex, rowIndex)] = EMPTY_SPACE
>>>     return board
>>> def displayBoard(board):
>>> # """Display the board and its tiles on the screen."""
>>> # Prepare a list to pass to the format() string method for the board
>>> # template. The list holds all of the board's tiles (and empty
>>> # spaces) going left to right, top to bottom:
>>>     tileChars = []
>>>     for rowIndex in range(BOARD_HEIGHT):
>>>         for columnIndex in range(BOARD_WIDTH):
>>>             tileChars.append(board[(columnIndex, rowIndex)])
>>> # Display the board:
>>>     print(BOARD_TEMPLATE.format(*tileChars))
>>> def getPlayerMove(playerTile, board):
>>> # """Let a player select a column on the board to drop a tile into.
>>> # Returns a tuple of the (column, row) that the tile falls into."""
>>>     while True: # Keep asking player until they enter a valid move.
>>>         print(f"Player {playerTile}, enter 1 to {BOARD_WIDTH} or QUIT:")
>>>         response = input("> ").upper().strip()
>>>         if response == "QUIT":
>>>             print("Thanks for playing!")
>>>             sys.exit()
>>>         if response not in COLUMN_LABELS:
>>>             print(f"Enter a number from 1 to {BOARD_WIDTH}.")
>>>             continue # Ask player again for their move.
>>>         columnIndex = int(response) - 1
>>> # -1 for 0-based column indexes.
>>> # If the column is full, ask for a move again:
>>>         if board[(columnIndex, 0)] != EMPTY_SPACE:
>>>             print("That column is full, select another one.")
>>>             continue # Ask player again for their move.
>>> # Starting from the bottom, find the first empty space.
>>>         for rowIndex in range(BOARD_HEIGHT - 1, -1, -1):
>>>             if board[(columnIndex, rowIndex)] == EMPTY_SPACE:
>>>                 return (columnIndex, rowIndex)
>>> def isFull(board):
>>> # """Returns True if the `board` has no empty spaces, otherwise
>>> # returns False."""
>>>     for rowIndex in range(BOARD_HEIGHT):
>>>         for columnIndex in range(BOARD_WIDTH):
>>>             if board[(columnIndex, rowIndex)] == EMPTY_SPACE:
>>>                 return False # Found an empty space, so return False.
>>>     return True # All spaces are full.
>>> def isWinner(playerTile, board):
>>> # """Returns True if `playerTile` has four tiles in a row on `board`,
>>> # otherwise returns False."""
>>> # Go through the entire board, checking for four-in-a-row:
>>>     for columnIndex in range(BOARD_WIDTH - 3):
>>>         for rowIndex in range(BOARD_HEIGHT):
>>> # Check for four-in-a-row going across to the right:
>>>             tile1 = board[(columnIndex, rowIndex)]
>>>             tile2 = board[(columnIndex + 1, rowIndex)]
>>>             tile3 = board[(columnIndex + 2, rowIndex)]
>>>             tile4 = board[(columnIndex + 3, rowIndex)]
>>>             if tile1 == tile2 == tile3 == tile4 == playerTile:
>>>                 return True
>>>     for columnIndex in range(BOARD_WIDTH):
>>>         for rowIndex in range(BOARD_HEIGHT - 3):
>>> # Check for four-in-a-row going down:
>>>             tile1 = board[(columnIndex, rowIndex)]
>>>             tile2 = board[(columnIndex, rowIndex +1)]
>>>             tile3 = board[(columnIndex, rowIndex +2)]
>>>             tile4 = board[(columnIndex, rowIndex +3)]
>>>             if tile1 == tile2 == tile3 == tile4 ==playerTile:
>>>                 return True
>>>
>>>     for columnIndex in range(BOARD_WIDTH - 3):
>>>         for rowIndex in range(BOARD_HEIGHT - 3):
>>> # Check for four-in-a-row going right-down diagonal:
>>>             tile1 = board[(columnIndex, rowIndex)]
>>>             tile2 = board[(columnIndex + 1, rowIndex + 1)]
>>>             tile3 = board[(columnIndex + 2, rowIndex + 2)]
>>>             tile4 = board[(columnIndex + 3, rowIndex + 3)]
>>>             if tile1 == tile2 == tile3 == tile4 == playerTile:
>>>                 return True
>>> # Check for four-in-a-row going left-down diagonal:
>>>             tile1 = board[(columnIndex + 3, rowIndex)]
>>>             tile2 = board[(columnIndex + 2, rowIndex + 1)]
>>>             tile3 = board[(columnIndex + 1, rowIndex + 2)]
>>>             tile4 = board[(columnIndex, rowIndex + 3)]
>>>             if tile1 == tile2 == tile3 == tile4 == playerTile:
>>>                         return True
>>>     return False
>>> # If this program was run (instead of imported), run the game:
>>> if __name__ == "__main__":
>>>     main()

Run this program and play a few games to get an idea of what this pro- gram does before reading the explanation of the source code. To check for typos, copy and paste it to the online diff tool at https://inventwithpython.com/ beyond/diff/. ## Writing the Code Let’s look at the program’s source code, as we did for the Tower of Hanoi program. Once again, I formatted this code using Black with a line limit of 75 characters.

We’ll begin at the top of the program:

>>> """Four-in-a-Row, by Al Sweigart al@inventwithpython.com
>>> A tile-dropping game to get four-in-a-row, similar to Connect Four."""
>>> import sys
>>> # Constants used for displaying the board:
>>> EMPTY_SPACE = "." # A period is easier to count than a space.
>>> PLAYER_X = "X"
>>> PLAYER_O = "O"

We start the program with a docstring, module imports, and constant assignments, as we did in the Tower of Hanoi program. We define the PLAYER_X and PLAYER_O constants so we don’t have to use the strings “X” and “O” throughout the program, making errors easier to catch. If we enter a typo while using the constants, such as PLAYER_XX , Python will raise NameError , instantly pointing out the problem. But if we make a typo with the “X” charac- ter, such as “XX” or “Z” , the resulting bug might not be immediately obvious. As explained in “Magic Numbers” on page 71, using constants instead of the string value directly provides not just a description, but also an early warning for any typos in your source code.

Constants shouldn’t change while the program runs. But the program- mer can update their values in future versions of the program. For this reason, we make a note telling programmers that they should update the BOARD_TEMPLATE and COLUMN_LABELS constants, described later, if they change the value of BOARD_WIDTH :

>>> # Note: Update BOARD_TEMPLATE & COLUMN_LABELS if BOARD_WIDTH is changed.
>>> BOARD_WIDTH = 7
>>> BOARD_HEIGHT = 6

Next, we create the COLUMN_LABELS constant:

>>> COLUMN_LABELS = ("1", "2", "3", "4", "5", "6", "7")
>>> assert len(COLUMN_LABELS) == BOARD_WIDTH

We’ll use this constant later to ensure the player selects a valid column. Note that if we ever set BOARD_WIDTH to a value other than 7 , we’ll have to add labels to or remove labels from the COLUMN_LABELS tuple. I could have avoided this by generating the value of COLUMN_LABELS based on BOARD_WIDTH with code like this: COLUMN_LABELS = tuple([str(n) for n in range(1, BOARD_WIDTH + 1)]) . But COLUMN_LABELS is unlikely to change in the future, because the standard Four-in-a-Row game is played on a 7 by 6 board, so I decided to write out an explicit tuple value.

Sure, this hardcoding is a code smell, as described in “Magic Num­ bers” on page 71, but it’s more readable than the alternative. Also, the assert statement warns us about changing BOARD_WIDTH without updating COLUMN_LABELS .

As with Tower of Hanoi, the Four-in-a-Row program uses ASCII art to draw the game board. The following lines are a single assignment statement with a multiline string:

>>> # The template string for displaying the board:
>>> BOARD_TEMPLATE = """
>>> 1234567
>>> +-------+
>>> |{}{}{}{}{}{}{}|
>>> |{}{}{}{}{}{}{}|
>>> |{}{}{}{}{}{}{}|
>>> |{}{}{}{}{}{}{}|
>>> |{}{}{}{}{}{}{}|
>>> |{}{}{}{}{}{}{}|
>>> +-------+"""

This string contains braces ( {} ) that the format() string method will replace with the board’s contents. (The displayBoard() function, explained later, will handle this.) Because the board consists of seven columns and six rows, we use seven brace pairs {} in each of the six rows to represent every slot. Note that just like COLUMN_LABELS , we’re technically hardcoding the board to create a set number of columns and rows. If we ever change BOARD_WIDTH or BOARD_HEIGHT to new integers, we’ll have to update the multi- line string in BOARD_TEMPLATE as well.

We could have written code to generate BOARD_TEMPLATE based on the BOARD_WIDTH and BOARD_HEIGHT constants, like so:

>>> BOARD_EDGE = "+" + ("-" * BOARD_WIDTH) + "+"
>>> BOARD_ROW = "|" + ("{}" * BOARD_WIDTH) + "|\n"
>>> BOARD_TEMPLATE = "\n" + "".join(COLUMN_LABELS) + "\n" + BOARD_EDGE + "\n"+ (BOARD_ROW * BOARD_WIDTH) + BOARD_EDGE

But this code is not as readable as a simple multiline string, and we’re unlikely to change the game board’s size anyway, so we’ll use the simple multiline string.

We begin writing the main() function, which will call all the other func- tions we’ve made for this game:

>>> def main():
>>> # """Runs a single game of Four-in-a-Row."""
>>>     print(
>>> """Four-in-a-Row, by Al Sweigart al@inventwithpython.com
>>> Two players take turns dropping tiles into one of seven columns, trying
>>> to make four-in-a-row horizontally, vertically, or diagonally.
>>> """
>>> )
>>> # Set up a new game:
>>>     gameBoard = getNewBoard()
>>>     playerTurn = PLAYER_X

We give the main() function a docstring, viewable with the built-in help() function. The main() function also prepares the game board for a new game and chooses the first player.

Inside the main() function is an infinite loop:

>>> while True: # Run a player's turn.
>>> # Display the board and get player's move:
>>>     displayBoard(gameBoard)
>>>     playerMove = getPlayerMove(playerTurn, gameBoard)
>>>     gameBoard[playerMove] = playerTurn

Each iteration of this loop consists of a single turn. First, we display the game board to the player. Second, the player selects a column to drop a tile in, and third, we update the game board data structure. Next, we evaluate the results of the player’s move:

>>> # Check for a win or tie:
>>> if isWinner(playerTurn, gameBoard):
>>>     displayBoard(gameBoard) # Display the board one last time.
>>>     print("Player {} has won!".format(playerTurn))
>>>     sys.exit()
>>> elif isFull(gameBoard):
>>>     displayBoard(gameBoard) # Display the board one last time.
>>>     print("There is a tie!")
>>>     sys.exit()

If the player made a winning move, isWinner() returns True and the game ends. If the player filled the board and there is no winner, isFull() returns True and the game ends. Note that instead of calling sys.exit() , we could have used a simple break statement. This would have caused the execution to break out of the while loop, and because there is no code in the main() function after this loop, the function would return to the main() call at the bottom of the program, causing the program to end. But I opted to use sys.exit() to make it clear to programmers reading the code that the program will immediately terminate.

If the game hasn’t ended, the following lines set playerTurn to the other player:

>>> # Switch turns to other player:
>>> if playerTurn == PLAYER_X:
>>> playerTurn = PLAYER_O
>>> elif playerTurn == PLAYER_O:
>>> playerTurn = PLAYER_X

Notice that I could have made the elif statement into a simple else statement without a condition. But recall the Zen of Python tenet that explicit is better than implicit. This code explicitly says that if it’s player O’s turn now, it will be player X’s turn next. The alternative would have just said that if it’s not player X’s turn now, it will be player X’s turn next. Even though if and else statements are a natural fit with Boolean conditions, the PLAYER_X and PLAYER_O values aren’t the same as True , and False : not PLAYER_X is not the same as PLAYER_O . Therefore, it’s helpful to be direct when checking the value of playerTurn .

Alternatively, I could have performed the same actions in a one-liner:

playerTurn = {PLAYER_X: PLAYER_O, PLAYER_O: PLAYER_X}[ playerTurn]

This line uses the dictionary trick mentioned in “Use Dictionaries Instead of a switch Statement” on page 101. But like many one-liners, it’s not as readable as a direct if and elif statement.

Next, we define the getNewBoard() function:

>>> def getNewBoard():
>>> """Returns a dictionary that represents a Four-in-a-Row board.
>>> The keys are (columnIndex, rowIndex) tuples of two integers, and the
>>> values are one of the "X", "O" or "." (empty space) strings."""
>>>     board = {}
>>>     for rowIndex in range(BOARD_HEIGHT):
>>>         for columnIndex in range(BOARD_WIDTH):
>>>             board[(columnIndex, rowIndex)] = EMPTY_SPACE
>>>     return board

This function returns a dictionary that represents a Four-in-a-Row board. It has (columnIndex, rowIndex) tuples for keys (where columnIndex and rowIndex are integers), and the ‘X’ , ‘O’ , or ‘.’ character for the tile at each place on the board. We store these strings in PLAYER_X , PLAYER_O , and EMPTY_ SPACE , respectively.

Our Four-in-a-Row game is rather simple, so using a dictionary to rep- resent the game board is a suitable technique. Still, we could have used an OOP approach instead. We’ll explore OOP in Chapters 15 through 17. The displayBoard() function takes a game board data structure for the board argument and displays the board onscreen using the BOARD_TEMPLATE constant:

>>> def displayBoard(board):
>>> """Display the board and its tiles on the screen."""
>>> # Prepare a list to pass to the format() string method for the board
>>> # template. The list holds all of the board's tiles (and empty
>>> # spaces) going left to right, top to bottom:
>>> tileChars = []

Recall that the BOARD_TEMPLATE is a multiline string with several brace pairs. When we call the format() method on BOARD_TEMPLATE , these braces will be replaced by the arguments passed to format() .

The tileChars variable will contain a list of these arguments. We start by assigning it a blank list. The first value in tileChars will replace the first pair of braces in BOARD_TEMPLATE , the second value will replace the second pair, and so on. Essentially, we’re creating a list of the values from the board dictionary:

>>> for rowIndex in range(BOARD_HEIGHT):
>>>     for columnIndex in range(BOARD_WIDTH):
>>>         tileChars.append(board[(columnIndex, rowIndex)])
>>> # Display the board:
>>> print(BOARD_TEMPLATE.format(*tileChars))

These nested for loops iterate over every possible row and column on the board, appending them to the list in tileChars . Once these loops have finished, we pass the values in the tileChars list as individual arguments to the format() method using the star * prefix. “Using * to Create Variadic Functions” section on page 167 explained how to use this syntax to treat the values in a list as separate function arguments: the code print(*[‘cat’, ‘dog’, ‘rat’]) is equivalent to print(‘cat’, ‘dog’, ‘rat’) . We need the star because the format() method expects one argument for every brace pair, not a single list argument.

Next, we write the getPlayerMove() function:

>>> def getPlayerMove(playerTile, board):
>>> # """Let a player select a column on the board to drop a tile into.
>>> # Returns a tuple of the (column, row) that the tile falls into."""
>>>     while True: # Keep asking player until they enter a valid move.
>>>         print(f"Player {playerTile}, enter 1 to {BOARD_WIDTH} or QUIT:")
>>>         response = input("> ").upper().strip()
>>>         if response == "QUIT":
>>>             print("Thanks for playing!")
>>>             sys.exit()

The function begins with an infinite loop that waits for the player to enter a valid move. This code resembles the getPlayerMove() function in the Tower of Hanoi program. Note that the print() call at the start of the while loop uses an f-string so we don’t have to change the message if we update BOARD_WIDTH .

We check that the player’s response is a column; if it isn’t, the continue statement moves the execution back to the start of the loop to ask the player for a valid move:

>>> if response not in COLUMN_LABELS:
>>>     print(f"Enter a number from 1 to {BOARD_WIDTH}.")
>>>     continue # Ask player again for their move.

We could have written this input validation condition as not response. isdecimal() or spam < 1 or spam > BOARD_WIDTH , but it’s simpler to just use response not in COLUMN_LABELS .

Next, we need to find out which row a tile dropped in the player’s selected column would land on:

>>> columnIndex = int(response) - 1
>>> # -1 for 0-based column indexes.
>>> # If the column is full, ask for a move again:
>>> if board[(columnIndex, 0)] != EMPTY_SPACE:
>>>     print("That column is full, select another one.")
>>>     continue # Ask player again for their move.

The board displays the column labels 1 to 7 onscreen. But the ( columnIndex, rowIndex) indexes on the board use 0-based indexing, so they range from 0 to 6. To solve this discrepancy, we convert the string values ‘1’ to ‘7’ to the integer values 0 to 6 .

The row indexes start at 0 at the top of the board and increase to 6 at the bottom of the board. We check the top row in the selected column to see whether it’s occupied. If so, this column is completely full and the continue statement moves the execution back to the start of the loop to ask the player for another move.

If the column isn’t full, we need to find the lowest unoccupied space for the tile to land on:

>>> # Starting from the bottom, find the first empty space.
>>> for rowIndex in range(BOARD_HEIGHT - 1, -1, -1):
>>>     if board[(columnIndex, rowIndex)] == EMPTY_SPACE:
>>>         return (columnIndex, rowIndex)

This for loop starts at the bottom row index, BOARD_HEIGHT - 1 or 6 , and moves up until it finds the first empty space. The function then returns the indexes of the lowest empty space.

Anytime the board is full, the game ends in a tie: def isFull(board):

This for loop starts at the bottom row index, BOARD_HEIGHT - 1 or 6 , and moves up until it finds the first empty space. The function then returns the indexes of the lowest empty space. Anytime the board is full, the game ends in a tie:

>>> def isFull(board):
>>> # """Returns True if the `board` has no empty spaces, otherwise
>>> # returns False."""
>>>     for rowIndex in range(BOARD_HEIGHT):
>>>         for columnIndex in range(BOARD_WIDTH):
>>>             if board[(columnIndex, rowIndex)] == EMPTY_SPACE:
>>>                 return False # Found an empty space, so return False.
>>>     return True # All spaces are full.

The isFull() function uses a pair of nested for loops to iterate over every place on the board. If it finds a single empty space, the board isn’t full, and the function returns False . If the execution makes it through both loops, the isFull() function found no empty space, so it returns True .

The isWinner() function checks whether a player has won the game:

>>> def isWinner(playerTile, board):
>>> # """Returns True if `playerTile` has four tiles in a row on `board`,
>>> # otherwise returns False."""
>>> # Go through the entire board, checking for four-in-a-row:
>>>     for columnIndex in range(BOARD_WIDTH - 3):
>>>         for rowIndex in range(BOARD_HEIGHT):
>>> # Check for four-in-a-row going across to the right:
>>>             tile1 = board[(columnIndex, rowIndex)]
>>>             tile2 = board[(columnIndex + 1, rowIndex)]
>>>             tile3 = board[(columnIndex + 2, rowIndex)]
>>>             tile4 = board[(columnIndex + 3, rowIndex)]
>>>             if tile1 == tile2 == tile3 == tile4 == playerTile:
>>>                 return True

This function returns True if playerTile appears four times in a row horizontally, vertically, or diagonally. To figure out whether the condition is met, we have to check every set of four adjacent spaces on the board. We’ll use a series of nested for loops to do this.

The (columnIndex, rowIndex) tuple represents a starting point. We check the starting point and the three spaces to the right of it for the playerTile string. If the starting space is (columnIndex, rowIndex) , the space to its right will be (columnIndex + 1, rowIndex) , and so on. We’ll save the tiles in these four spaces to the variables tile1 , tile2 , tile3 , and tile4 . If all of these vari- ables have the same value as playerTile , we’ve found a four-in-a-row, and the isWinner() function returns True .

In “Variables with Numeric Suffixes” on page 76, I mentioned that variable names with sequential numeric suffixes (like tile1 through tile4 in this game) are often a code smell that indicates you should use a single list instead. But in this context, these variable names are fine. We don’t need to replace them with a list, because the Four-in-a-Row program will always require exactly four of these tile variables. Remember that a code smell doesn’t necessarily indicate a problem; it only means we should take a second look and confirm that we’ve written our code in the most readable way. In this case, using a list would make our code more complicated, and it wouldn’t add any benefit, so we’ll stick to using tile1 , tile2 , tile3 , and tile4 .

We use a similar process to check for vertical four-in-a-row tiles:

>>> for columnIndex in range(BOARD_WIDTH):
>>>     for rowIndex in range(BOARD_HEIGHT - 3):
>>> # Check for four-in-a-row going down:
>>>         tile1 = board[(columnIndex, rowIndex)]
>>>         tile2 = board[(columnIndex, rowIndex +1)]
>>>         tile3 = board[(columnIndex, rowIndex +2)]
>>>         tile4 = board[(columnIndex, rowIndex +3)]
>>>         if tile1 == tile2 == tile3 == tile4 ==playerTile:
>>>             return True

Next, we check for four-in-a-row tiles in a diagonal pattern going down and to the right; then we check for four-in-a-row tiles in a diagonal pattern going down and to the left:

>>> for columnIndex in range(BOARD_WIDTH - 3):
>>>     for rowIndex in range(BOARD_HEIGHT - 3):
>>> # Check for four-in-a-row going right-down diagonal:
>>>         tile1 = board[(columnIndex, rowIndex)]
>>>         tile2 = board[(columnIndex + 1, rowIndex + 1)]
>>>         tile3 = board[(columnIndex + 2, rowIndex + 2)]
>>>         tile4 = board[(columnIndex + 3, rowIndex + 3)]
>>>         if tile1 == tile2 == tile3 == tile4 == playerTile:
>>>             return True
>>> # Check for four-in-a-row going left-down diagonal:
>>>         tile1 = board[(columnIndex + 3, rowIndex)]
>>>         tile2 = board[(columnIndex + 2, rowIndex + 1)]
>>>         tile3 = board[(columnIndex + 1, rowIndex + 2)]
>>>         tile4 = board[(columnIndex, rowIndex + 3)]
>>>         if tile1 == tile2 == tile3 == tile4 == playerTile:
>>>             return True

This code is similar to the horizontal four-in-a-row checks, so I won’t repeat the explanation here. If all the checks for four-in-a-row tiles fail to find any, the function returns False to indicate that playerTile is not a win- ner on this board:

return False

The only task left is to call the main() function:

>>> # If this program was run (instead of imported), run the game:
>>> if __name__ == '__main__':
>>>     main()

Once again, we use a common Python idiom that will call main() if fourinarow.py is run directly but not if fourinarow.py is imported as a module.