I've created a console based Tic-Tac-Toe game in python that has the ability to be expanded to a 9x9 playing field or more if you wanted to. By default it's 3x3.
Github link: https://github.com/Weffe/TicTacToe
Current Issues:
[Minor] Misalignment when printing a playing field of 4x4 or greater
Tried to keep the TicTac and Player class separated as possible so that there's no direct interaction between them. Don't know if this is a good thing or not. Also, I know some might question the use of Errors raised so I'm open to other ways of implementing things differently.
Lastly, is there a cleaner way of populating/initializing a 2D array with values related to their index?
For example,
def populate_playing_field():
field = []
value = (num_rows * num_cols)
for row in range(num_rows):
field.append([])
for col in range(num_cols):
field[row].insert(0, value)
value = value - 1
return field
Gets me this for a 3x3:
-------------
| 7 | 8 | 9 |
-------------
| 4 | 5 | 6 |
-------------
| 1 | 2 | 3 |
-------------
The main reason why the values are backwards is to represent a numpad on a keyboard. This way the index value corresponds to the position on a numpad!
TicTac.py
class TicTac:
def __init__(self, num_rows, num_cols):
#self.playing_field = [ [7,8,9], [4,5,6], [1,2,3] ] #init the playing field to their respective nums on the numpad: 3x3
self.winner_found = False
self.is_full = False
self.possible_moves_left = num_cols * num_rows
self.num_rows = num_rows
def populate_playing_field():
field = []
value = (num_rows * num_cols)
for row in range(num_rows):
field.append([])
for col in range(num_cols):
field[row].insert(0, value)
value = value - 1
return field
self.playing_field = populate_playing_field()
#value = (num_rows * num_cols)
#def decrement(x): return (x-1)
#self.playing_field = [ [ decrement(value) for i in range(num_cols) ] for i in range(num_rows)]
def print_playing_field(self):
print('-------------')
for list in self.playing_field:
print('| ', end='')
for item in list:
print(str(item) + ' | ', end='')
print('\n-------------')
def print_instructions(self):
print('\n-------------------------------------')
print('When entering in the location please enter the number of the index you want to replace with your shape.')
print('Player 1: Your shape is represented as the X')
print('Player 2: Your shape is represented as the O')
print('\nPrinting Initial Playing Field...')
self.print_playing_field()
print('\nLet the game begin!')
print('-------------------------------------')
def process_player_move(self, index, shape):
row, col = index[0], index[1]
field_value = self.playing_field[row][col]
#if the value is of type int we can replace it
if isinstance(field_value, int):
self.playing_field[row][col] = shape #overwrite the value
self.possible_moves_left -= 1 #reduce the moves left
#check possible moves after its been updated
if self.possible_moves_left == 0:
self.is_full = True
raise EnvironmentError('All index posistions filled.\nGame Over. Nobody won.')
#else its the Player's shape (string)
else:
raise ValueError('Invalid Index. Position already filled. Try again.\n')
def check_for_match(self):
def check_list(passed_list):
#fast & quick check to tell if the "row" is incomplete
if isinstance(passed_list[0], str):
player_shape = passed_list[0] # set to first val
#compare the values to each other
for val in passed_list:
if isinstance(val, int) or player_shape != val:
return False #we found an inconsistency
return True #everything matched up
def get_diag(orientation):
diag_list = []
counter = 0 if orientation is 'LtR' else self.num_rows-1
for row in self.playing_field:
diag_list.append(row[counter])
counter = counter+1 if orientation is 'LtR' else counter-1
return diag_list
# check rows for match
for row_list in self.playing_field:
if check_list(row_list):
return True
#check cols for match
transposed_playing_field = [list(a) for a in zip(*self.playing_field)] #convert our tuples from zip to a list format
for col_list in transposed_playing_field:
if check_list(col_list):
return True
#check diagnols for match
if check_list(get_diag('LtR')): #LtR \ gets replaced each time we check
return True
if check_list(get_diag('RtL')): # RtL / gets replaced each time we check
return True
return False #if we got here then no matches were found
Player.py
class Player:
def __init__(self, player_name, shape):
self.player_name = player_name
self.shape = shape
def get_player_loc_input(self, num_rows, num_cols):
player_input = input('Enter in location for your move: ') # player input is with respect to field index location/values
converted_input = int(player_input)
if 1 <= converted_input <= (num_rows * num_cols): # bound checking
converted_input -= 1 # adjust from n+1 to n
transformed_value = (num_rows-(converted_input//num_cols)-1, converted_input%num_cols) # (row,col) tuple obj
return transformed_value
else:
raise ValueError('Input is not an index on the playing field. Try again\n')
Main.py
try:
from .TicTac import TicTac
from .Player import Player
except Exception:
from TicTac import TicTac
from Player import Player
def start_game():
Players = {'Player_1': Player('Player_1', 'X'), 'Player_2': Player('Player_2', 'O')}
num_rows, num_cols = 3,3 #need to be the same
game = TicTac(num_rows, num_cols)
game.print_instructions()
player_id = 'Player_1' # index to swap between players, Player_1 starts
while (game.winner_found == False and game.is_full == False):
print('\nIt\'s ' + Players[player_id].player_name + ' Turn')
# loop until user inputs correct index value
while True:
try:
index = Players[player_id].get_player_loc_input(num_rows,num_cols)
shape = Players[player_id].shape
game.process_player_move(index, shape)
except ValueError as msg:
print(msg)
continue
except EnvironmentError as msg:
print(msg)
break
game.winner_found = game.check_for_match() # check if a player has won
game.print_playing_field()
if game.winner_found:
print(Players[player_id].player_name + ' has won!') # print player who won
player_id = 'Player_2' if player_id is 'Player_1' else 'Player_1' # switch between the 2 players
prompt_to_play_again() # Game has ended. Play Again?
def prompt_to_play_again():
user_input = input('\nDo you want to play again? (Y/N) ')
user_input = user_input.upper() #force to uppercase for consistency
if user_input == 'Y':
start_game()
elif user_input == 'N':
exit(0)
else:
print('Incorrect input.')
prompt_to_play_again()
def main():
while True:
start_game()
if __name__ == '__main__':
main()
2 Answers 2
My first thought is that you don't really need to split TicTac
and Player
into separate files. I'd at least put the two of them into one file, if not put all the code into one file as it's not an incredibly long file.
Next, you have some errant comments lingering around in the file. This is just old code that you're no longer using. If it's needed, it should be uncommented. Otherwise you can remove it. Unless it's meant to demonstrate how the actual code works.
Also, I don't see why you define populate_playing_field
inside of __init__
rather than just have it a function on the class that is called within __init__
. That would also allow you to call it again if you wanted to restart the game, or set up a new empty board. This would also mean storing num_cols
in the object, but this is good practice anyway as it may be needed for other reasons and you currently throw it away.
print_playing_field
should also adjust based on the size of the board, rather than just hard coding the sizes you have now. You could also use str.join
to simplify out one of your loops:
def print_playing_field(self):
print('-' + '----' * self.num_cols)
for row in self.playing_field:
print('| ' + ' | '.join(str(item) for item in row) + ' |')
print('-' + '----' * self.num_cols)
Raising an EnvironmentError
to show that the game is over is a strange practice. If you want to use exceptions to control flow, at least define your own custom one rather than confusing a user with an exception that means something else. It's very easy to define an exception, it just takes this:
class GameOver(BaseException):
pass
Now you can just raise it, and pass a message as before.
if self.possible_moves_left == 0:
self.is_full = True
raise GameOver('All index posistions filled.\nGame Over. Nobody won.')
-
1\$\begingroup\$ Hmm, those are some good points. Before when restarting the game, another Game object would be recreated and i never thought to just reset the current one. I think that's a more efficient way of doing things. I definitely agree with the use of Error Messages and will just define my own like you said. Thanks! \$\endgroup\$Weffe– Weffe2016年08月11日 16:10:05 +00:00Commented Aug 11, 2016 at 16:10
Here a shorter alternative for populating_playing_field
:
def populate_playing_field(num_rows, num_cols):
field = range(1, num_rows*num_cols+1)
return list(reversed([field[x:x+num_rows] for x in xrange(0, len(field), num_rows)]))
Using the pattern from this answer to split the list containing the indices into the right sublists.
The function print_playing_field
could be remodeled into the magic __str__
method, allowing print(self)
:
def __str__(self):
s = ["-------------"]
for row in self.playing_field:
s.append('| {} |'.format(' | '.join(map(str, row))))
s.append('-------------')
return '\n'.join(s)
For print_instructions
, a multi-line string would simplify it a bit:
def print_instructions(self):
print('''
-------------------------------------
When entering in the location please enter the number of the index you want to replace with your shape.
Player 1: Your shape is represented as the X
Player 2: Your shape is represented as the O
Printing Initial Playing Field...
{}
Let the game begin!
-------------------------------------
'''.format(self))
-
\$\begingroup\$ Thanks for the help! I'll make some adjustments based on your feedback. \$\endgroup\$Weffe– Weffe2016年08月11日 16:05:41 +00:00Commented Aug 11, 2016 at 16:05
-
\$\begingroup\$ If I added the str method you suggested would that mean I can simple call print(Game obj) and it would work? Meaning I don't have to call Game.print_field()? \$\endgroup\$Weffe– Weffe2016年08月11日 16:13:31 +00:00Commented Aug 11, 2016 at 16:13
-
\$\begingroup\$ Yes, you could call
print(game)
externally (withgame = TicTacToe(...)
, orprint(self)
from within the class. \$\endgroup\$Graipher– Graipher2016年08月11日 16:20:07 +00:00Commented Aug 11, 2016 at 16:20 -
\$\begingroup\$ I already used this fact in the function
print_instructions
for the format. Since the class has a string representation now, format knows what to do and prints the playing field there. \$\endgroup\$Graipher– Graipher2016年08月11日 16:21:34 +00:00Commented Aug 11, 2016 at 16:21 -