backlinks to this page:
Github:
While the current version of the game works technically, it is not a a prime example of user-friendly design. Even when restricted to text-based input/output, several points could need improvement, like:
a4
or 4a
)
The option to enter quit
instead of an coordinate to force the game to quit would introduce a new if
condition (like if command == “quit”: break
) inside the inner while
loop that checks the player input (and rest itself inside the 9-turn for
loop). Escaping this (inner) loop using break
would be possible but then we are still stuck inside the (outer) for
loop. And of course we could again break
out of this for
loop using another if
condition. But why writing the same line of code twice? Or, asked in a different way: How to break out of all loops at once?
One (drastic) solution would be to use sys.exit()
, provided a import sys
line at the beginning of the code. However, sys.exit()
would not only exit out of all nested loops, it would also exit out of the current python program.
Another, less drastic solution is to put the whole 'main' game loop inside a new function. And instead of break
to escape the inner while
loop and another break
to escape the outer for
loop, using a simple return
statement, to exit the actual game function.
The method of asking the user for the cell coordinates in a 'pythonic' way (row first, then column, starting to count with 0) is a great simplification for programmers, but not a good way to impress players with your coding skills. For the following code, the internal naming of coordinates stays the same: a cell notation is still array[row-number][column-number]
. However, the display and user-input is transformed, to make it more human-readable.
+1
is added for display and input only. For the purpose of transforming A,B,C into 0,1,2 exist many options:
if
/ elif
/ else
structure{'A':0, 'B':1, 'C':2}
ord()
/ chr()
functions. Each character in the alphabet has a unique Unicode number (derived from older ASCII numbers). While python's chr()
function returns the charcter of a given unicode code number, the ord()
functions does the reverse: giving back the unicode number of a given character. The uppercase A
has code 65, the uppercase B
has code 66, the uppercase C
has code 66. Therefore, just calculating colunn_number = ord(letter) - 65gives the desired coordinate.
Because a little knowledge of ASCII / Unicode is very useful in Python programming, let's take a look at this ASCII-Table:
In the table above, each line holds 32 cells. The Ascii-code of a given cell can be calculated by multiplying the line number (not shown in the image!) with 32 and adding the column number (also not shown in the image). Line and column numbers start with 0. The A
is in the 3rd row (=line number 2) and the second column (=column number 1). Therefore, 2 x 32 + 1 = 65. Of course, it may be easier to simply look up a given character in one of the many unicode tables (See Wikipedia list of Unicode characters). Unicode characters are especially useful if you want to display graphical symbols (like hearts, arrows, pictograms..) but are restricted to text mode using print()
.
Another thing worth knowing is the ascii-code of some signs, symbolized in the table above by a two-letter combination:
\n
into a string, you can also use string1 + chr(13) + string2
If the python program should be able to recognize a coordinate in both ways (column, row
or row, column
) then it will be necessary to swap the row-value with column-value depending on the user input method. Usually, to swap the values 2 variables, a third variable is needed:
>>>a = 5 >>>b = 7 >>>print(a, b) 5 7 >>>c = a >>>a = b >>>b = c >>>print(a, b) 7 5
In python, you can swap two variable directly in one line of code:
a, b = b, a
"""tic tac toe for 2 players, extra comfortable for humans """ cells = [[" " for x in range(3)] for y in range(3)] # create a 3 x 3 array SYMBOLS = ["x", "o"] # ----- some constants, see code discussion GREETING = "This is turn {}. Player {}, where do you put your '{}'?: >>> " TEXT = "If asked for coordinates, please enter: column, row\n" \ " like for example: 'A 1' or 'b,2' or 'C3' and press ENTER" def check_win(char): """checks the array cells and returns True if 3 chars build a line""" for row in range(3): # checking rows # if cells[row][0] == char and cells[row][1] == char and cells [row][2] == char: if cells[row][0:3] == [char, char, char]: # horizontal slice, see code discussion return True for col in range(3): # checking columns # if cells[0][col] == char and cells[1][col] == char and cells[2][col] == char: if [element[col] for element in cells] == [char, char, char]: # vertical slice 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 display(): """displays the 3x3 array 'cells' with heading row and column human-friendly display: Rows are numbered 1-3, columns are numbered A-C""" print("\n" + r"r\c A: B: C:") # empty line and header. for index, row in enumerate(cells): # index starts with 0 print("{}: ".format(index + 1), end="") # no new line at end of print for element in row: # index starts with 0 print("[{}] ".format(element), end="") # no new line at end of print print() # print only a new line print() # empty line after board def input_checker(user_input): """Testing if user_input is a valid and free coordinate in a 3x3 matrix (rows:1-3,columns: ABC) returns Error Message as string and None, None (for x and y) otherwise returns False and x, y as integer values user_input must be alreay converted by user_input.strip().upper() """ if len(user_input) < 2: return "Enter 2 coordinates. Try again!", None, None raw_column = user_input[0] raw_row = user_input[-1] if raw_row in ("A", "B", "C") and raw_column in ("1", "2", "3"): raw_column, raw_row = raw_row, raw_column # swap row with column if raw_column not in ("A", "B", "C"): return "Enter A or B or C for column. Try again", None, None if raw_row not in ("1", "2", "3"): return "Enter 1 or 2 or 3 for row. Try again", None, None column = ord(raw_column) - 65 # 'A'=chr(65), 'B'=chr(66) 'C'=chr(67) row = int(raw_row) - 1 # ---- checking if the coordinate is still free ---- if cells[row][column] != " ": return "This cell is already occupied. Try again", None, None return False, column, row # ---- the 'main' function of the game ----- def game(): print(TEXT) display() for turns in range(9): # play 9 legal moves, then the board is full player = turns % 2 # modulo: the remainder of a division by 2. player_char = SYMBOLS[player] while True: # ask until input is acceptable prompt = GREETING.format(turns + 1, player + 1, player_char) command = input(prompt).strip().upper() if command in ("QUIT", "EXIT", "CANCEL", "Q", "BYE"): return # -> bye bye error, column, row = input_checker(command) if error: # errormessage is a string or False print(error) continue # ask again # ----- input accepted, update the game board ------ cells[row][column] = player_char break # escape the while loop # -- end of while loop. got acceptable input --- display() if check_win(player_char): # only the active player is checked print("Congratulation, player {} has won!".format(player + 1)) break # ---- proceed with the next turn ----- else: # ----- for loop has run 9 times without a break --- print("All nine fields are occupied. It's a draw. No winner") print("Game Over") if __name__ == "__main__": game() print("bye bye")
“”“
)) is recommended even for one-line-only docstrings.cells
is not a constant, it will later change it's value quite often. cells
is declared here at module-level so that all functions can access and manipulate it. \
) at the end of the line tells python that the line continues in the next line. Line 6 is part of the value of the constant TEXT
, like line 5.check_win()
function needs char
as parameter and searches the cell
matrix for 3 chars in a row. It returns True
or False
:
) is the index of where the slice should begin, the number right of the colon is the index where the slice should stop (the slice stops before reaching the index). In line 13, the slice [0:3]
gives back 0
and 1
and 2
but not 3
. The expression cells[row][0:3]
gives back a list of the first 3 elements of cells[row]
. This is compared by ==
with [char, char, char]
. When both sides are identical, the whole expression gives back True
or False
and can be handled by the if
conditional.\n
). This new line character is 'glued' via the +
operator to the raw-string. An extra print()
line before would have the exactly same effect. As would have chr(13)
instead of “\n”
.input_checker()
. Please note that this function always returns 3 values: An error string (or None), and an x and an y value. In python, you are not required to to return anything at all (like the display()
function above). Nor are you required to return the same amount (and types) of return values all the time. However, it is good idea when a function at least always return the same number of return values. Here, the value of error
is None
or the error message (a string), the value of x
and y
is integer when the input was valid, otherwise both x and y are returned as None
. Please note that the user-input
is passed as an parameter that is guaranteed to be in Uppercase and stripped of leading or trailing spaces. See line 72try
statements and except ValueError
: In this case, simply testing with using in
is the least complicated method. Please note that the user_input generated by input()
is always of type string, even if the strings themselves contain numbers.if
conditionals have been passed, and no error message was created. Therefore the input_checker()
functions returns False
as return value for the error message.main()
but here for better understanding is called game()
display()
is called once before the for
loop and then always at the end of itinput_checker()
always returns 3 argumentscontinue
statement goes back to the start of the current (inner) while
loop. In this case, back to line 70break
statements breaks out of the inner while
loop, the code continues with line 82display()
is called for every game turn (inside the for
loop)break
statement breaks out of the outer for
loop. The code continues with line 88 (and then line 90)else
does not belong to any if
statement…it belongs to the for
loop in line 67. The indented code (line 89) will only be executed if the for
loop was never leaved by a break
__name__
is set to the value __main__
. Usually a special function named main
is then called, but in this case, to be less confusing for beginners, i named my 'main' function game()
and therefore, game()
is called.If asked for coordinates, please enter: column, row like for example: 'A 1' or 'b,2' or 'C3' and press ENTER r\c A: B: C: 1: [ ] [ ] [ ] 2: [ ] [ ] [ ] 3: [ ] [ ] [ ] This is turn 1. Player 1, where do you put your 'x'?: >>> a Enter 2 coordinates. Try again! This is turn 1. Player 1, where do you put your 'x'?: >>> aa Enter 1 or 2 or 3 for row. Try again This is turn 1. Player 1, where do you put your 'x'?: >>> a5 Enter 1 or 2 or 3 for row. Try again This is turn 1. Player 1, where do you put your 'x'?: >>> x 2 Enter A or B or C for column. Try again This is turn 1. Player 1, where do you put your 'x'?: >>> a1 r\c A: B: C: 1: [x] [ ] [ ] 2: [ ] [ ] [ ] 3: [ ] [ ] [ ] This is turn 2. Player 2, where do you put your 'o'?: >>> 2 B r\c A: B: C: 1: [x] [ ] [ ] 2: [ ] [o] [ ] 3: [ ] [ ] [ ] This is turn 3. Player 1, where do you put your 'x'?: >>> quit bye bye
Some other minor changes were done in this code, because i looked at the famous Pep8 code style guide hints. My editor, Pycharm, display this hints as yellow boxes while typing, but there exist several tools to improve the styling of your python code.
If you think that a style guide for code is somewhat ridiculous …. because, only you and the computer need to understand your code, so why being pedantic about some spaces and empty lines? … Wait on the reactions of the (usually very very friendly) python community when you post your first code snippets in the internet and wait for feedback. The first thing you will hear (or read) is a more or less polite hint to read the Pep8. Some experienced programmers, whose time is very valuable, will, when asked for help, take time to manually correct your “horrible” styling, leaving you somehow puzzled.
When you have more experience with python programmers you will slowly understand the value of unified styled code. Especially when you work in teams. It is perfectly true that for the computer, there is absolute no difference how you style your code. Make two blank lines before an def
or none at all, use many spaces before and after an =
operator or none at all, indent with tabs or with spaces… The computer does not care. Humans, however, do. What you do by styling “chaotic” is effectively stealing other humans valuable time, because (insofar as they are looking at your source code) they need longer to read and understand your code, and you make it harder for them to spot errors in your code for you (that is usually what you asking them to do if you posting code snippets).
At this point of the tutorial, it may be a very good moment to take the time to read the Pep8 style guide. After all, it's written by python inventor Guido van Rossum himself.
Luckily it is not necessary to know the style guide by heart, there exist several tools to help you styling your code Pep8-like. Here are just a few listed, you can find more online:
In the end, the style guide is a recommendation, and it's up to you how to style your code, when to follow guidelines and when to ignore them.
Another case of 'nice to have, but optional' are Type hints:
Python, a dynamical typed language, does not need them, but several tools like static type checkers can make good use of them, and type hints improve the documentation a lot and save you some work while writing docstrings.
generally, type hints work like this:
x: int = 4
This code above declares the variable x
, gives a type hint that the variable is of type integer, and assigns the value of 4
to it.
Functions can have type-hints for their arguments and for their return value:
def doubler(x: int=5) -> int: return x * 2
To hint of the type in a collection, like inside a tuple or inside a list, it is necessary to first import
the module typing
and some of it's commands: List, Set, Dict, Tuple
from typing import List coordinate: Tuple[int, int] = (4,5) tuple_of_unknown_length: Tuple[int, ...] = (1,2,3,4,5,6,7,8,9) simple_list: List[str] = ["Alice", "Bob", "Carl"] nested_list: List[List[int, int, int]] = [[1,2,3],[44,55,66],[0,0,0]]
And for mixed types, use either Union
or Optional
:
from typing import Union, Optional result: Optional[int] = 4 # result can be 4 or None result: Union[str, float] = "pi" # result can be for example 'pi' or 3.14
"""tic tac toe for 2 players, with type hints""" from typing import List, Tuple, Union, Optional cells: List[List[str]] = [[" " for x in range(3)] for y in range(3)] # create a 3 x 3 array of strings SYMBOLS: Tuple[str] = ("x", "o") # ----- some constants, see code discussion GREETING: str = "This is turn {}. Player {}, where do you put your '{}'?: >>> " TEXT: str = "If asked for coordinates, please enter: column, row\n" \ " like for example: 'A 1' or 'b,2' or 'C3' and press ENTER" def check_win(char: str) -> bool:
def display() -> None:
def input_checker(user_input: str) -> Tuple[Union[str, bool], Optional[int], Optional[int]]:
def game() -> None:
"""tic tac toe for 2 players, with type hints""" from typing import List, Tuple, Union, Optional cells : List[List[str]]= [[" " for x in range(3)] for y in range(3)] # create a 3 x 3 array of strings SYMBOLS: Tuple[str] = ("x", "o") # ----- some constants, see code discussion GREETING: str = "This is turn {}. Player {}, where do you put your '{}'?: >>> " TEXT : str = "If asked for coordinates, please enter: column, row\n" \ " like for example: 'A 1' or 'b,2' or 'C3' and press ENTER" def check_win(char: str) -> bool: """checks the array cells and returns True if 3 chars build a line""" for row in range(3): # checking rows #if cells[row][0] == char and cells[row][1] == char and cells [row][2] == char: if cells[row][0:3] == [char, char, char]: # horizontal slice, see code discussion return True for col in range(3): # checking columns #if cells[0][col] == char and cells[1][col] == char and cells[2][col] == char: if [element[col] for element in cells] == [char, char, char]: return True# vertical slice # 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 display() -> None: """displays the 3x3 array 'cells' with heading row and column human-friendly display: Rows are numbered 1-3, columns are numbered A-C""" print("\n" + r"r\c A: B: C:") # empty line and header. for index, row in enumerate(cells): # index starts with 0 print("{}: ".format(index + 1), end="") # no new line at end of print for element in row: # index starts with 0 print("[{}] ".format(element), end="") # no new line at end of print print() # print only a new line print() # empty line after board def input_checker(user_input: str) -> Tuple[Union[str, bool], Optional[int], Optional[int]]: """Testing if user_input is a valid and free coordinate in a 3x3 matrix (rows:1-3,columns: ABC) returns Error Message as string and None, None (for x and y) otherwise returns False and x, y as integer values user_input must be alreay converted by user_input.strip().upper() """ if len(user_input) < 2: return "Enter 2 coordinates. Try again!", None, None raw_column = user_input[0] raw_row = user_input[-1] if raw_row in ("A", "B", "C") and raw_column in ("1", "2", "3"): raw_column, raw_row = raw_row, raw_column # swap row with column if raw_column not in ("A", "B", "C"): return "Enter A or B or C for column. Try again", None, None if raw_row not in ("1", "2", "3"): return "Enter 1 or 2 or 3 for row. Try again", None, None column = ord(raw_column) - 65 # 'A'=chr(65), 'B'=chr(66) 'C'=chr(67) row = int(raw_row) - 1 # ---- checking if the coordinate is still free ---- if cells[row][column] != " ": return "This cell is already occupied. Try again", None, None return False, column, row # ---- the 'main' function of the game ----- def game() -> None: print(TEXT) display() for turns in range(9): # play 9 legal moves, then the board is full player = turns % 2 # modulo: the remainder of a division by 2. player_char = SYMBOLS[player] while True: # ask until input is acceptable prompt = GREETING.format(turns + 1, player + 1, player_char) command = input(prompt).strip().upper() if command in ("QUIT", "EXIT", "CANCEL", "Q", "BYE"): return # -> bye bye error, column, row = input_checker(command) if error: # errormessage is a string or False print(error) continue # ask again # ----- input accepted, update the game board ------ cells[row][column] = player_char break # escape the while loop # -- end of while loop. got acceptable input --- display() if check_win(player_char): # only the active player is checked print("Congratulation, player {} has won!".format(player + 1)) break # ---- proceed with the next turn ----- else: # ----- for loop has run 9 times without a break --- print("All nine fields are occupied. It's a draw. No winner") print("Game Over") if __name__ == "__main__": game() print("bye bye")
List
, Tuple
, Optional
and Union
from module typing
. Notice the leading lowercase of python's internal list
and tuple
containers. The typing equivalents have a leading captial letter. cells
and giving a type hint: cells
is a list. Inside this list are again lists (→ nested_list) and inside those inner lists are strings. Note that it does not matter how many different strings are in each list, important is only their type.\n
is used inside the string.display()
itself has no arguments and therefore no style hint. The function has also no return value, this information is written in to the style hint by the → None
syntax.input_checker()
has rather complicated and situation-dependent return values: Either an error messsage string and two None
values or one False
value followed by two int
values. The Union[]
style hint syntax says “it can be either this type or that type” while the Optional[]
syntax says: “it can be either this type or None”Output is the same as before!