backlinks to this page:
Github:
Now that the game has a display()
function to show the board, it is time to outsmart the most creative human player by asking him for input (what field/cell he want to play) and figuring out every possible incorrect player input!
The python command to accept (text) input is simply called input()
. It displays an optional prompt, waits for player input until the Enter
key is hit and returns the input as an text string. Usually you want to save this input in a variable:
>>> human_command = input("Type your command and press ENTER: > ") Type your command and press ENTER: > meaning of life? >>> print("you typed:", human_command) you typed: meaning of life?
Because human users are free to press any combination of keys (including none at all) before hitting ENTER
, it is a good idea to place the input()
function inside a while True
(endless) loop. The user is asked to enter a coordinate. The input is tested, and if it is not a coordinate the loop simply repeats, as long as necessary. Only after an acceptable coordinate as input the program break
s out of the while True
loop and more testing can be done.
while True: # endless loop print("Please enter index of column: 0 or 1 or 2") print("followed by index of row: 0 or 1 or 2") print("like for example: 0 1") command = input("and press ENTER: > ") command = command.strip() # remove leading and trailing spaces column_string = command[0] # the first char row_string = command[-1] # the last (!) char if column_string in ["0", "1", "2"] and row_string in ["0", "1", "2"]: break print("Wrong input. Please try again \n") # make an extra new line at the end! print("column:", column_string, "row:", row_string)
This code is a “fresh start” and did not evolved from previous code, so a diff is not meaningful here
.strip()
command. Lowercase / Uppercase input should be no problem as the script accepts numbers only, but python does provide a .lower()
and .upper()
functions as well. while
loop, at line 12 (because line 12 has the same indentation as line 1)print()
command ends per default with a new line. By including \n
at the end of the string, we get a new line AND an extra new line.Please enter index of column: 0 or 1 or 2 followed by index of row: 0 or 1 or 2 like for example: 0 1 and press ENTER: > x2 Wrong input. Please try again Please enter index of column: 0 or 1 or 2 followed by index of row: 0 or 1 or 2 like for example: 0 1 and press ENTER: > 12 column: 1 row: 2
Please enter index of column: 0 or 1 or 2 followed by index of row: 0 or 1 or 2 like for example: 0 1 and press ENTER: > 0,0 column: 0 row: 0
Please enter index of column: 0 or 1 or 2 followed by index of row: 0 or 1 or 2 like for example: 0 1 and press ENTER: > 2 0 column: 2 row: 0
Now that the player entered a cell coordinate, the next task is to test if this cell is free or not. For later use, this test will be written as a function as well, where the function returns True
if the cell was free and otherwise returns False
.
In a simple TicTacToe game, only 3 possibilities for a coordinate exist and the player input is already checked. Still, the whole point of writing functions is to reuse them later. So this example below features an Exception handling using pythons try / except functionality: possibly 'troublesome' code is indented below a try:
statement. If the 'troublesome' code line (ideally, only put one single code line inside try / except) raises an error, the python program does not stop but instead executes the code below the except
statement. You can even write exception handlers for different kind of errors. See https://docs.python.org/3/tutorial/errors.html for more details.
cells = [["x", "x", " "], [" ", "x", "o"], ["o", " ", "o"], ] def is_free(row, column): # function with two mandatory arguments """checks a single coordinate in the cells array returns True if the cell is free (contains a Space) otherwise returns False""" try: content = cells[row][column] except IndexError: return "this cell does not exist" except: return "not even a legal index" # slow but readable if content == " ": return True return False # faster but harder to read: # return True if content == " " else False # ---- testing ------ print("0,0:", is_free(0, 0)) print("2,1:", is_free(2, 1)) print("5,6:", is_free(5, 6)) print("x,0:", is_free("x", 0))
This code is a 'fresh start' and did not evolved from a previous version, so a diff would not be meaningful here
try:
block. Python is then trying to execute the line. When an error occurs, the code below the corresponding except
block is excuted. Otherwise, the program continues after the try/except block. As the most common error with arrays is an IndexError
, this error get it's own specific error handler (line 13). If it is any other error, the the more general except:
block in line 15 is executed. except:
catches every error that is not already handled by previous exceptions. print()
. Ideally, you write test even before writing the function, meaning you think about what output the function should return on different kinds of input. Then, you write the function it self, and then you test it whenever the code changes. More on that topic later, or take a look now into Doctest and Unittest.0,0: False 2,1: True 5,6: this cell does not exist x,0: not even a legal index
Instead of asking the players for their names (and needing another row of tests like if both names are identical, or empty etc.), each player becomes an index number: Player 0 and Player 1. The main game must alternate between both players. Here is a little mathematical trick: Each game turn (there are maximal 9 turns, because the board has only 9 cells), the actual turn number is divided by 2. Interesting is the Remainder of this division by 2: It can be either zero or one. Like the player number!
To get the remainder of an division, use Pythons modulo Operator (%
). To get the result of a division (including the fractional part right of the decimal point) use the division Operator (/
). If you are only interested in the integer part of the result of a division (the part left of decimal point) you can either use int(a/b)
or a//b
.
>>>6 / 2 # 6 divided by 2 is 3,remainder is 0. 6 = 2 x 3 + 0 3 >>>6 % 2 # % is the modulo operator 0 >>>7 / 2 # 7 divided by 2 is 3.5, remainder is 1. 7 = 2 x 3 + 1 3.5 >>>7 // 2 # // displays only the integer part of an division 3 >>>7 % 2 # modulo 1
Now, putting it together: let's define 2 symbols for the two (at the moment, human) players: an “x” and an “o”. Each player is now asked to enter a coordinate, the display is updated and the other player is asked. This continues until no field is free anymore (later the python program will check if one player has won). Please note that the while
loop has now its own exception handlings:
int()
"""tic tac toe for 2 players, without win checking""" # ----- defnine some top-level variables ------ cells = [[" ", " ", " "], [" ", " ", " "], [" ", " ", " "], ] # ore more elegant: # 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 numbers (0 or 1 or 2) for column and row\n" text += " like for example: '1 2' or '02' or '2x1'\n" text += " and press ENTER >>> " # ---- functions ---- 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"r\c 0: 1: 2:") # header line. r\c is not an escape sequence, leading r -> raw string for index, row in enumerate(cells): print("{}: ".format(index), end="") # no new line at end of print for element in row: print("[{}] ".format(element), end="") # no new line at end of print print() # print only a new line print() # empty line after board 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. print(greeting.format(turns, player, symbols[player])) while True: # ask until legal move command = input(text) command = command.strip() if len(command) < 2: print("Enter 2 coordinates. Try again!") continue raw_column, raw_row = command[0], command[-1] # 2 variables, 2 values try: row, column = int(raw_row), int(raw_column) # 2 variables, 2 values and only one line inside try: block 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\n") # extra new line cells[row][column] = symbols[player] break # breaks out of this while loop # print("*** next turn! *****") print("all fields are full. Game Over")
cells
. The 2d-array is created as a nested list (a list with list's inside. Inside each inner list are three empty strings. range()
function in a single line of code. This line is out-commented now but will later replace lines 4-7.symbols
is a tuple (because using the round brackets ()
) and not a list (with square brackets []
). The difference of using a tuple instead of a list: tuples are read-only or immutable, wile list's are mutable. Meaning it is later possible to replace an item in a list (like the topleft element in cells
) but it is not possible to replace just the o
element in symbols
. In short: cells[0][0] = "y"is possible. But
symbols[0] = "E"is not possible, because
symbol
is defined as a tuple. In practice, you make your python program a tiny bit less slow / memory-hungry when using tuples instead of lists. The main advantage is that you make your intentions clear while coding. greeting
has empty placeholders in it {}
but no trailing .format()
. The .format()
will be attached later, in line 44text
is here first defined and then modified by the +
operator. Notice the \\n
linebreak inside the string!is_free()
function needs row
and column
as arguments, but not the cells
array! is_free()
can still access the cells
variable because cells
was defined at top-level. is_free()
function only checks for IndexError
(cell outside the array) and not for general errors with an except nor for ValueError
, like getting the row
and column
argument as strings instead of integers. In this code example, the code calling the is_free()
function is responsible for calling it only with the correct integer arguments. Notice that this fact (both arguments are of type integer) is not documented in the docstring of the function nor anywhere else. Generally, trusting the fact that some other part of code will never call a function with the wrong arguments is a sign of over-confidence. Especially when both code parts are written by yourself! See type-hinting display()
display needs no arguments at all but is able to display the cells
array, because cells
was declared at top-level.enumerate()
returns two values, and those are assigned to the two variables index
and row
.row
itself is a list (of strings) and can be iterated using a for
loop.range(9)
creates an iterable (think of it as a list) with those 9 elements in it: [0,1,2,3,4,5,6,7,8]]
. The game board has 9 fields only.for
loop, the function display()
will be called 9 times.%
) calculates the remainder of a division. See the next headline below. The effect is that the value of player
changes between 0 and 1input()
will be stored in the variable command
. While the userinput will hopefully be 2 numbers, input()
stores everything as string.strip()
can be attached to any string variable and is very useful when handling player input. .strip()
removes leading and trailing spaces and other invisible
characters.len()
returns the number of elements in an object. If the object is a string, it returns the number of characters inside this string. This line makes sure the user entered at least 2 values (remember, leading and trailing spaces were already removed by .strip()
in line 47.if
statement in line 48 does not need an else
branch. If the program flow reaches line 51, the program has made sure already that the command
string consist of two values, otherwise the continue
statement in line 50 would have lead the program flow back to the beginning of the while
loop in line 45. command
is now split into two variables: the first char of the user input (with index 0) is assigned to the variable raw_column
, the last char of the user input (index -1) is assigned to the variable raw_row
. Please note that the last char is not necessary the second char! Thanks to the .strip()
, we know that the first and last char is not a whitespace.int()
twice, this line tries to convert the userinput from type string into type integer. This type-converting is also called casting. It may go wrong, for example because the user entered One, Two
instead of 1,2
. Good thing we are inside a try
block:except
statement to catch all other (Non-Value) Errors. Because I am very sure that no possible other error can occur. This is a classical case of hubris and should be avoided: Never underestimate the creativity of users! Especially when asking them for user input! Anyway, what does this line: it is the same as if is_free(row, column) == True
. It's a function call to the is_free()
function. The function is_free()
returns a boolean value (True
or False
). This return value is then used in the if
statement. Notice that it is a good idea to create meaningful names for functions. A function named free_cell()
for example does not make it so clear what kind of return values are to be expected and what they mean. When you name your functions is_anything
it is more clear that you expect a boolean return value when anything
becomes True.break
command breaks out of the actual (for
or while
) loop. Unlike other programming languages, python has no command to break out to a specific loop if you are inside nested loops. while
loop.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 >>> 2 2 input accepted r\c 0: 1: 2: 0: [ ] [ ] [ ] 1: [ ] [ ] [ ] 2: [ ] [ ] [x] 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 >>> 01 input accepted r\c 0: 1: 2: 0: [ ] [ ] [ ] 1: [o] [ ] [ ] 2: [ ] [ ] [x] 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 >>> 4 Enter 2 coordinates. Try again! Please enter numbers (0 or 1 or 2) for column and row like for example: '1 2' or '02' or '2x1' and press ENTER >>> 44 this cell does not exist Please enter numbers (0 or 1 or 2) for column and row like for example: '1 2' or '02' or '2x1' and press ENTER >>> 0x2 input accepted r\c 0: 1: 2: 0: [ ] [ ] [ ] 1: [o] [ ] [ ] 2: [x] [ ] [x] This is turn 3. Player 1, where do you put your 'o'?
Assuming that a piece of code will never create unexpected Errors is a classical case of hubris:
<todo>add quiz about what would happen when omitting the len
function</todo>