5
\$\begingroup\$

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()
\$\endgroup\$
2
\$\begingroup\$

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.')
\$\endgroup\$
  • 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 Aug 11 '16 at 16:10
1
\$\begingroup\$

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))
\$\endgroup\$
  • \$\begingroup\$ Thanks for the help! I'll make some adjustments based on your feedback. \$\endgroup\$ – Weffe Aug 11 '16 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 Aug 11 '16 at 16:13
  • \$\begingroup\$ Yes, you could call print(game) externally (with game = TicTacToe(...), or print(self) from within the class. \$\endgroup\$ – Graipher Aug 11 '16 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 Aug 11 '16 at 16:21
  • \$\begingroup\$ If you want to know more about magic methods (there is also __repr__ and many more!), have a look here. \$\endgroup\$ – Graipher Aug 11 '16 at 16:23

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.