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()