ThePythonGameBook

learn Python. Create Games

User Tools

Site Tools


en:python:tictactoe:step004

win condition

Playing until every cell is full is boring. It would be better if the python program can check if one player has already won. Let's improve the previous script with a win-checking function, to search the board for three equal characters building a horizontal / vertical / diagonal line.

step004a_check_win.py

code detail

"""tic tac toe for 2 players, wit win checking"""
# ----- defnine some top-level variables ------
cells = [[" " for x in range(3)] for y in range(3)]
symbols = ("x", "o")      # tuples are read-only
greeting = "This is turn {}. Player {}, where do you put your '{}'?"
text = "Please enter coordinate (0 or 1 or 2) for column and row like for example: 1 2 and press ENTER >>>"


# ---- functions ----
def check_win(char):
    """checks the array cells and returns True if 3 chars build a line"""
    for row in range(3):  # checking rows. range(3) returns 0, 1, 2, it is the same as 'for row in [0,1,2]:'
        if cells[row][0] == char and cells[row][1] == char and cells[row][2] == char:
            return True  # checking columns
    for col in range(3):
        if cells[0][col] == char and cells[1][col] == char and cells[2][col] == char:
            return True
    # checking diagonal

        if is_free(row, column):
            print("input accepted")
            cells[int(raw_row)][int(raw_column)] = player_char
            break
    if check_win(player_char):  # only the active player is checked
        print(" -*- -*- -*- Congratulation, You have won, player {} -*- -*- -*-".format(player))
        display()  # final
        break
    print("*** next turn! *****")
print("Game Over")

full code

"""tic tac toe for 2 players, wit win checking"""
# ----- defnine some top-level variables ------
cells = [[" " for x in range(3)] for y in range(3)]
symbols = ("x", "o")      # tuples are read-only
greeting = "This is turn {}. Player {}, where do you put your '{}'?"
text = "Please enter coordinate (0 or 1 or 2) for column and row like for example: 1 2 and press ENTER >>>"


# ---- functions ----
def check_win(char):
    """checks the array cells and returns True if 3 chars build a line"""
    for row in range(3):  # checking rows. range(3) returns 0, 1, 2, it is the same as 'for row in [0,1,2]:'
        if cells[row][0] == char and cells[row][1] == char and cells[row][2] == char:
            return True  # checking columns
    for col in range(3):
        if cells[0][col] == char and cells[1][col] == char and cells[2][col] == char:
            return True
    # checking diagonal
    if cells[0][0] == char and cells[1][1] == char and cells[2][2] == char:
        return True
    if cells[2][0] == char and cells[1][1] == char and cells[0][2] == char:
        return True
    return False  # all win condition checked without success, therefore no win


def is_free(row, column):
    """checks a single coordinate in the the cells array returns True if the cell is free (contains a Space)
    otherwise returns False"""
    try:
        content = cells[row][column]
    except IndexError:
        print("this cell does not exist")
        return False
    return True if content == " " else False


def display():
    """displays the 3x3 array 'cells' with heading row and column"""
    print("r\c 0:  1:  2:")  # header line
    for index, row in enumerate(cells):
        print("{}: ".format(index), end="")  # header column
        for element in row:
            print("[{}] ".format(element), end="")
        print()  # force new line

# ---- the 'main loop' of the game -----
for turns in range(9):  # play 9 legal moves, then the board is full
    display()
    player = turns % 2  # modulo: the remainder of a division by 2.
    player_char = symbols[player]
    print(greeting.format(player, turns, player_char))
    while True:  # ask until legal move
        command = input(text)
        command = command.strip()
        try:
            raw_column, raw_row = command[0], command[-1]  # 2 variables, 2 values
        except:
            print("Enter 2 coordinates. Try again")
            continue  # go back to the start of the while loop
        try:
            row = int(raw_row)
            column = int(raw_column)
        except ValueError:
            print("Enter numbers only. Try again")
            continue  # go back to the start of the while loop
        if is_free(row, column):
            print("input accepted")
            cells[int(raw_row)][int(raw_column)] = player_char
            break
    if check_win(player_char):  # only the active player is checked
        print(" -*- -*- -*- Congratulation, You have won, player {} -*- -*- -*-".format(player))
        display()  # final
        break
    print("*** next turn! *****")
print("Game Over")

diff

code discussion

  • line 10: The check_win() functions needs the string char as argument and only checks if this char string has a winning line of three occurrences at inside cells. Because cells was declared at top-level, it is not necessary to pass it as an argument to the check_win() function.
  • line 12: Testing for 3 chars in a row could also be done by use of python's built-in all function (see paragraph below)
  • line 12-17: This code is a bit long and will be improved in step005.
  • line 9-69: no changes since the previous version, see code discussion in step004
  • line 69: breaks out of the inner while loop (line 52).
  • line 70: calling the check_win() function with the correct char for the actual player.
  • line 73: breaks out of the outer for loop (line 47)

output

r\c 0:  1:  2:
0: [ ] [ ] [ ] 
1: [ ] [ ] [ ] 
2: [ ] [ ] [ ] 

This is turn 0. Player 0, where do you put your 'x'?
Please enter numbers (0 or 1 or 2) for column and row
   like for example: '1 2' or '02' or '2x1'
   and press ENTER >>> 00
input accepted

r\c 0:  1:  2:
0: [x] [ ] [ ] 
1: [ ] [ ] [ ] 
2: [ ] [ ] [ ] 

This is turn 1. Player 1, where do you put your 'o'?
Please enter numbers (0 or 1 or 2) for column and row
   like for example: '1 2' or '02' or '2x1'
   and press ENTER >>> 12
input accepted

r\c 0:  1:  2:
0: [x] [ ] [ ] 
1: [ ] [ ] [ ] 
2: [ ] [o] [ ] 

This is turn 2. Player 0, where do you put your 'x'?
Please enter numbers (0 or 1 or 2) for column and row
   like for example: '1 2' or '02' or '2x1'
   and press ENTER >>> 10
input accepted

r\c 0:  1:  2:
0: [x] [x] [ ] 
1: [ ] [ ] [ ] 
2: [ ] [o] [ ] 

This is turn 3. Player 1, where do you put your 'o'?
Please enter numbers (0 or 1 or 2) for column and row
   like for example: '1 2' or '02' or '2x1'
   and press ENTER >>> 22
input accepted

r\c 0:  1:  2:
0: [x] [x] [ ] 
1: [ ] [ ] [ ] 
2: [ ] [o] [o] 

This is turn 4. Player 0, where do you put your 'x'?
Please enter numbers (0 or 1 or 2) for column and row
   like for example: '1 2' or '02' or '2x1'
   and press ENTER >>> 20
input accepted

 -*- -*- -*- Congratulation, You have won, player 0 -*- -*- -*-
r\c 0:  1:  2:
0: [x] [x] [x] 
1: [ ] [ ] [ ] 
2: [ ] [o] [o] 

Game Over

all() function

Python's all()function returns True if all the elements in an iterable are True (or empty). The row list of cells is such an iterable, but it has chars in it, not boolean values. Using a quick list comprehension, it is possible to create an list of True / False values and then test this list using the all() function if the list contains only True values:

>>>row = ["x", "x", "x"]
>>>row
['x', 'x', 'x']
>>>[element == "x" for element in row]
[True, True, True]
>>>all([element == "x" for element in row])
True
>>>row2 = ["x", "o", "x"]
>>>all([element == "o" for element in row2])
>>>False

scope of variables

scope of variables

Variables have a certain scope, where they can exist. This can be at top-level, inside a function or inside a class. A Variable inside a class instance (usually prefix with self. ) is called an attribute.

  • by default, variables have local scope, meaning if a variable is declared inside a function, only code inside this function can access this variable
  • global variables have global scope, meaning when you declare a global variable inside a function, code at top-level or inside other functions can access the variable. As the scope of variables is a very powerful feature of object-oriented programming, use global variables only if you have no other alternative.
  • class variables act like global variables
  • code inside a scope (like inside a function) can read (and sometimes even manipulate!) variables of a higher scope (like of top-level)

A python function can access (read) a variable when this variable was declared at top-level. It is important to know that each function in python has it's own scope for variables. All variables declared inside a function are local and can not be seen by any other function, nor by code at top-level. Meaning you can have a variable at top-level, and you can declare inside a function a a local variable with the same name. Both variables are separated by python.

Interesting is the fact that you can not only read, but also manipulate (changing the value of) a top-level variables inside a function…and the manipulation will be visible at top-level! This only works with mutable types, like list (square brackets). A tuple (round bracket) on the other hand is immutable.

Important is not to confuse those two things:

  • Declaring a variable: like
     x = 4
  • Manipulation a mutable variable: like
    cells[2] = "x"

In short, as soon as you declare a variable inside a function, you create a new local variable. Independent of the fact that a variable with the same name exist at top-level. You best try out this interactive python shell session below. Several functions will be declared and used, to check if they could manipulate variables from top-level.

cells = [1,2,3,4] # top-level
>>> def adder():
	"""adding 5 to cells"""
	cells.append(5)

	
>>> print(cells)
[1, 2, 3, 4]
>>> adder()  
>>> print(cells)
[1, 2, 3, 4, 5]
As can be seen in the example above, the function adder() was able to manipulate the top-level variable cells. Because cells is a list and lists are mutable.

>>> def local_adder():
	cells = [1,2,3]
	cells.append(77)

	
>>> local_adder()
>>> print(cells)
[1, 2, 3, 4, 5]
Contrary, in the example above nothing new was added to cells by the function local_adder(). Because local_adder() declared a local variable inside a function. At this moment, a local cells existed and the top-level cells. Only the local cells got the 77 added, but the function itself had no print() to show this. After the function call, the top-level cells had excatly the same values as before.

>>> coordinates = (44,55)
>>> print(coordinates)
(44, 55)
>>> def invert_coordinates():
	coordinates = (-coordinates[0], -coordinates[1])
>>> invert_coordinates()
Traceback (most recent call last):
  File "<pyshell#25>", line 1, in <module>
    invert_coordinates()
  File "<pyshell#22>", line 2, in invert_coordinates
    coordinates = (-coordinates[0], -coordinates[1])
UnboundLocalError: local variable 'coordinates' referenced before assignment
Manipulating an immutable tuple is not possible, but in the example above the problem is not the type of coordinates but the scope: Inside the invert_coordinates() function, a new local variable coordinates is declared. The the function tries to assign it the (inverted) value of the top-level variable with the same name. And this same name is the problem: python can not access the top-level variable coordinate anymore because the name is overwritten by the local variable coordinate!

>>> def invert_coordinates():
	coordinates = (-1,-2)

>>> coordinates
(44, 55)
>>> invert_coordinates()
>>> coordinates
(44, 55)
The example above shows that a local variable (with same name as existing top-level variable) can be created inside a function without manipulating the top-level variable at all. This all with an immutable tuple.

>>> def creator():
	secret="i am local only"
	print("inside function:", secret)

	
>>> secret
Traceback (most recent call last):
  File "<pyshell#35>", line 1, in <module>
    secret
NameError: name 'secret' is not defined
>>> creator()
inside function: i am local only
>>> secret
Traceback (most recent call last):
  File "<pyshell#37>", line 1, in <module>
    secret
NameError: name 'secret' is not defined
The example above shows the same fact from another perspective: the local variable secret is well visible….but inside it's function only. not at top-level.

>>> cells
[1, 2, 3, 4, 66]
>>> def creator2():
	cells = [-1,-2,-3]
	print("local cells:", cells)

	
>>> creator2()
local cells: [-1, -2, -3]
>>> cells
[1, 2, 3, 4, 66]
Again, a local variable can be declared inside a function, without conflicts with top-level variable of the same time. This time the variables are of the mutable type list.

>>> def creator3():
	cells = [cells[-1], cells[0]]
	print("local cells:", cells)

	
>>> cells
[1, 2, 3, 4, 66]
>>> creator3()
Traceback (most recent call last):
  File "<pyshell#51>", line 1, in <module>
    creator3()
  File "<pyshell#49>", line 2, in creator3
    cells = [cells[-1], cells[0]]
UnboundLocalError: local variable 'cells' referenced before assignment

In the example above, again the problem of overwriting cells inside the function with a local variable cells and then trying to access the top-level cells: Not possible, because the name cells inside the function is already overwritten. It would work by choosing another name for the local variable.

>>> coordinates
(44, 55)
>>> def swapper(original):
	return (original[-1],original[0])

>>> coordinates
(44, 55)
>>> new_c = swapper(coordinates)
>>> new_c
(55, 44)
>>> def doubler():
	return (coordinates[0]*2, coordinates[1]*2)

>>> coordinates
(44, 55)
>>> c2 = doubler()
>>> c2
(88, 110)

And finally how it should be done: If a function want to manipulate an immutable top-level variable, it should use the return statement and the function caller should assign the returned value to a variable.

2020/05/12 09:57 · Horst JENS
/var/www/horst/thepythongamebook.com/data/pages/en/python/tictactoe/step004.txt · Last modified: 2020/05/13 08:23 by Horst JENS