Episode #125 of the Stack Overflow podcast is here. We talk Tilde Club and mechanical keyboards. Listen now
2 of 4 make title about the code itself, not the objectives of code review ("_State what your code does in your title, not your main concerns about it._" - see https://codereview.stackexchange.com/questions/how-to-ask); add tag for game

Python card game

I have programmed a card game in python. It is over 700 lines long. How can I improve the code?

I used an online IDE called Repl.it. You can view and run the program here: NEA Card Game

Or if you prefer to stay on Code Review:

main.py:

import pickle
import os

from random import randint, shuffle
from time import sleep


# Card index constants
COLOUR = 0
NUMBER = 1

# Player constants
DRAW = 'draw'
PLAYER1 = '1'
PLAYER2 = '2'


def clear():
    print('\n' * 100)


def main():

    clear()

    #login()

    game = Game()

    menu_loop = True

    while menu_loop:

        clear()

        with open('image.txt', 'r') as image_file:
            image_text = image_file.read()

        for c in image_text:
            if c == '0':
                print(' ', end='')
            elif c == '1':
                print('*', end='')
            else:
                print(c, end='')

        print()

        print('==============================================')
        print('|                                            |')
        print('| 1 - Play game                              |')
        print('| 2 - Create a new deck                      |')
        print('| 3 - Load a deck                            |')
        print('| 4 - Delete a deck                          |')
        print('| 5 - Change the speed of the game           |')
        print('| 6 - View the leaderboard                   |')
        print('| 7 - Quit                                   |')
        print('|                                            |')
        print('==============================================')

        menu_option = input('\nEnter menu option: ')

        if menu_option == '1':
            clear()
            game.play()
            input('Press enter to continue.')

        elif menu_option == '2':
            clear()
            new_deck_menu()
            input('Press enter to continue.')

        elif menu_option == '3':
            clear()
            change_deck_menu()
            input('Press enter to continue.')

        elif menu_option == '4':
            clear()
            delete_deck_menu()
            input('Press enter to continue.')

        elif menu_option == '5':
            clear()
            change_speed()
            input('Press enter to continue.')

        elif menu_option == '6':
            clear()
            top5 = FileFunctions.read_top5()
            display_leaderboard(top5)
            input('Press enter to continue.')

        elif menu_option == '7':
            print('\nGoodbye.')
            menu_loop = False

        else:
            clear()
            print('\nPlease choose a number from the menu.\n')
            input('Press enter to continue.')


def login():

    # The password (for now) is 'Python'.
    password = FileFunctions.get_password()

    valid = False

    while not valid:

        password_attempt = input('Enter password: ')

        if password_attempt == password:
            valid = True
        else:
            print('Incorrect password.')


# Returns a tuple containing a colour and a number.
def new_card(colour):
    return (colour, randint(1, 10))


# Creates a new random deck.
# number_of_cards must be a multiple of 3 so there can
# be an even amount of each colour in the deck.
def new_random_deck(name, number_of_cards):

    deck = []

    for i in range(int(number_of_cards / 3)):
        deck.append(new_card('red'))
        deck.append(new_card('black'))
        deck.append(new_card('yellow'))

    FileFunctions.write_deck(name, deck)


def display_leaderboard(players):

    length = 20

    print('\nLEADERBOARD\n')

    print('=' * (length + 2))

    for i in range(len(players)):

        score = len(players[i]) - 1

        string_part1 = str(i+1) + ' | ' + players[i][0]
        string_part2 = ' ' * (length - len(string_part1)) + str(score)

        print("%s%s" % (string_part1, string_part2))

    print('=' * (length + 2))

    print('\n')


def change_deck_menu():

    valid = False

    while not valid:

        yes_or_no = input('Loading a different deck will reset the leaderboard. Do you wish to proceed? (y/n)').lower()

        if yes_or_no == 'y':
            valid = True
        elif yes_or_no == 'n':
            print('\nDeck has not been loaded.\n')
            return
        else:
            print('\nPlease answer with \'y\' or \'n\'.\n')

    FileFunctions.clear_leaderboard()

    valid = False

    while not valid:

        deck_name = input('\nEnter name of deck to load: ')

        if deck_name.strip() == '':
            print('\nThe deck name will contain at least one visible character.')
            continue

        try:
            f = open(deck_name + '.bin', 'r')
            f.close()
            valid = True

        except FileNotFoundError:
            print('\nDeck \'%s\' does not exist.' % deck_name)
            valid = False

    FileFunctions.change_current_deck_name(deck_name)

    print('\nDeck \'%s\' has been loaded.\n' % deck_name)


def new_deck_menu():

    print('\n\n')

    valid = False

    while not valid:

        yes_or_no = input('\nAre you sure you want to create a new deck? (y/n)')
        yes_or_no = yes_or_no.lower()

        valid = True

        if yes_or_no == 'y':

            name_valid = False

            while not name_valid:

                deck_name = input('\nEnter deck name: ')

                if deck_name.strip() == '':
                    print('\nThe deck name must contain at least one visible character.\n')

                elif ' ' in deck_name:
                    print('\nThe deck name cannot contain spaces.\n')

                elif '.' in deck_name:
                    print('\nThe deck name cannot contain dots (the file extension will be added automatically).\n')

                elif '\\' in deck_name or '/' in deck_name:
                    print('\nThe deck name cannot contain slashes.\n')

                else:
                    name_valid = True

            number_valid = False

            while not number_valid:

                number = int(input('Enter amount of cards: '))

                if number % 2 == 0 and number % 3 == 0:
                    number_valid = True

                else:
                    print('\nAmount must be an even multiple of 3.')

            new_random_deck(deck_name, number)

            print('\nThe new deck has been created.')

        elif yes_or_no == 'n':
            print('\nCreation of new deck has been cancelled.')

        else:
            print('\nPlease answer with \'y\' or \'n\'.')
            valid = False

    print('\n')


def change_speed():

    valid = False

    current_delay = FileFunctions.load_round_delay()
    current_delay = round(current_delay, 3)
    current_delay = str(current_delay)

    while not valid:

        print('The current round delay is %s seconds.' % (current_delay))

        yes_or_no = input('Are you sure you want to change the speed of the game? (y/n)').lower()
        valid = True

        if yes_or_no == 'y':

            input_loop = True

            while input_loop:

                input_loop = False

                try:
                    seconds = float(input('\nEnter delay between each round in seconds: '))

                    if seconds < 0:
                        print('The round delay cannot be a negative number.')
                        input_loop = True

                except ValueError:
                    input_loop = True
                    print('Please enter a float or an integer.')

            FileFunctions.write_round_delay(seconds)

            print('\nThe new round delay has been saved.')

        elif yes_or_no == 'n':
            print('\nChanging of game speed has been cancelled.')

        else:
            print('\nPlease answer with \'y\' or \'n\'.\n')
            valid = False


def delete_deck_menu():

    valid = False

    while not valid:

        deck_name = input('\nEnter the name of the deck you want to delete: ')

        if deck_name.strip() == '':
            print('\nThe deck name will contain at least one visible character.')
        elif ' ' in deck_name:
            print('\nThe deck name will not contain spaces.')
        elif '.' in deck_name:
            print('\nPlease only enter the name of the deck. The file extension will be added automatically.\n')
        else:

            try:
                with open(deck_name + '.bin', 'r'):
                    valid = True

            except FileNotFoundError:
                valid = False
                print('\nDeck \'%s\' could not be found. Make sure you have spelt the name correctly.' % deck_name)

    os.remove(deck_name + '.bin')

    print('\nDeck \'%s\' has been deleted.\n' % deck_name)


class Game:

    def __init__(self):

        self.player1_name = ''
        self.player2_name = ''

    def play(self):

        round_delay = FileFunctions.load_round_delay()

        self.player1_name, self.player2_name = self._get_names()

        deck_name = FileFunctions.load_current_deck_name()

        play_again = True

        while play_again:

            # Read the deck from the deck file.
            try:
                deck = FileFunctions.load_deck(deck_name)
            except FileNotFoundError:
                print('\nCould not find the deck file. Try loading a different deck with option 3 of the main menu.\n')
                return

            player1_cards = []
            player2_cards = []

            shuffle(deck)

            game_round = 1

            print('\n\n')

            while len(deck) > 0:

                sleep(round_delay)

                player1_card = deck[-1]
                player2_card = deck[-2]

                deck.pop()
                deck.pop()

                print('ROUND', game_round, '\n')

                self._display_cards(player1_card, player2_card)

                winner = self._compare_cards(player1_card, player2_card)

                if winner == PLAYER1:
                    print('\nWinner:', self.player1_name)
                elif winner == PLAYER2:
                    print('\nWinner:', self.player2_name)
                else:
                    print('\nWinner: draw')

                print('\n\n')

                if winner == PLAYER1:
                    player1_cards.append(player1_card)
                    player1_cards.append(player2_card)

                elif winner == PLAYER2:
                    player2_cards.append(player1_card)
                    player2_cards.append(player2_card)

                # If it is a draw, the players keep their own cards.
                else:
                    player1_cards.append(player1_card)
                    player2_cards.append(player2_card)

                game_round += 1

            if len(player1_cards) > len(player2_cards):
                winner = self.player1_name
                winning_cards = player1_cards

            elif len(player1_cards) < len(player2_cards):
                winner = self.player2_name
                winning_cards = player2_cards

            else:
                winner = DRAW
                winning_cards = []


            print('%s has %d cards.' % (self.player1_name, len(player1_cards)))
            print('%s has %d cards.\n' % (self.player2_name, len(player2_cards)))

            print('Winner of game:', winner)

            if winner != DRAW:
                FileFunctions.write_name_and_cards(winner, winning_cards)
                self._display_winning_cards(winner, winning_cards)

            valid = False

            while not valid:
                yes_or_no = input('\nWould you like to play again? (y/n)').lower()
                valid = True
                if yes_or_no == 'n':
                    play_again = False
                elif yes_or_no != 'y':
                    print('Please answer with \'y\' or \'n\'.')
                    valid = False

        print('\n\n')

    def _get_names(self):

        print('\n\n')

        valid = False

        while not valid:

            player1_name = input('Enter player 1\'s name: ')

            if '_' in player1_name:
                print('Names cannot contain underscores.')
            elif player1_name.strip() == '':
                print('The name must contain at least one visible character.')
            elif len(player1_name) > 15:
                print('The name cannot contain more than 15 characters.')
            else:
                valid = True

        valid = False

        while not valid:

            player2_name = input('Enter player 2\'s name: ')

            if '_' in player2_name:
                print('Names cannot contain underscores.')
            elif player2_name.strip() == '':
                print('The name must contain at least one visible character.')
            elif player2_name == player1_name:
                print('Player 1 and player 2 must have different names.')
            elif len(player2_name) > 15:
                print('The name cannot contain more than 15 characters.')
            else:
                valid = True

        return (player1_name, player2_name)

    def _display_cards(self, card1, card2):

        space_length = 25 - len(self.player1_name)

        players_string = ''
        players_string += self.player1_name.upper()
        players_string += ' ' * space_length
        players_string += self.player2_name.upper()

        print('=' * 40)
        print(players_string)
        print('=' * 40)

        print('colour:', card1[COLOUR], end='')
        space_length = 17 - len(card1[COLOUR])
        print(space_length * ' ', end='')
        print('colour:', card2[COLOUR])

        print('number:', card1[NUMBER], end='')
        space_length = 17 - len(str(card1[NUMBER]))
        print(space_length * ' ', end='')
        print('number:', card2[NUMBER])

    def _compare_cards(self, card1, card2):

        if card1[COLOUR] == card2[COLOUR]:

            if card1[NUMBER] > card2[NUMBER]:
                return PLAYER1
            elif card1[NUMBER] < card2[NUMBER]:
                return PLAYER2
            else:
                return DRAW

        else:

            if card1[COLOUR] == 'red':
                return PLAYER1 if card2[COLOUR] == 'black' else PLAYER2

            elif card1[COLOUR] == 'black':
                return PLAYER1 if card2[COLOUR] == 'yellow' else PLAYER2

            elif card1[COLOUR] == 'yellow':
                return PLAYER1 if card2[COLOUR] == 'red' else PLAYER2

    def _display_winning_cards(self, winner, winning_cards):

        while winner[-1] == ' ':
            winner = winner[:-1]

        if winner[-1].lower() == 's':
            winner += '\''
        else:
            winner += '\'s'

        print('\n%s CARDS:\n' % winner.upper())

        space_const = 15

        space_after_colour = ''

        length_of_largest_int = len( str( len(winning_cards) + 1 ) )

        for i in range(len(winning_cards)):

            card = winning_cards[i]

            space_after_colour = ' ' * ( space_const - len(card[0]) )
            space_after_number = length_of_largest_int - len(str(i+1))

            card_string = str(i+1)
            card_string += ' ' * space_after_number
            card_string += ' | COLOUR: ' + card[0] + space_after_colour + 'NUMBER: ' + str(card[1])

            print(card_string)


class FileFunctions:


    # Reads the current deck's name
    def load_current_deck_name():
        with open('bin_files/current_deck_name.bin', 'rb') as cd_file:
            return pickle.load(cd_file)

    def change_current_deck_name(new_name):
        with open('bin_files/current_deck_name.bin', 'wb') as cd_file:
            pickle.dump(new_name, cd_file)


    # Writes deck to a file
    def write_deck(name, deck_array):

        deck_string = ''

        for card in deck_array:

            deck_string += card[0]
            deck_string += ','
            deck_string += str(card[1])
            deck_string += '\n'

        with open(name + '.bin', 'wb') as deck_file:
            pickle.dump(deck_string, deck_file)


    # Reads deck from a file and returns it as an array
    def load_deck(name):

        deck_array = []

        with open(name + '.bin', 'rb') as deck_file:
            deck_text = pickle.load(deck_file)

        deck_text = deck_text.split('\n')

        for card_string in deck_text:

            try:
                card = card_string.split(',')
                card[1] = int(card[1])
                deck_array.append( (card[0], card[1]) )
            except IndexError: # The line is empty
                continue

        return deck_array


    def get_password():
        with open('bin_files/password.bin', 'rb') as passcode_file:
            return pickle.load(passcode_file)


    def clear_leaderboard():
        with open('bin_files/win.bin', 'wb') as win_file:
            pickle.dump('', win_file)

    # Writes name and cards to win.bin
    def write_name_and_cards(name, cards):

        # Write the name and cards to the file

        try:
            with open('bin_files/win.bin', 'rb') as win_file:
                win_string = pickle.load(win_file)
        except EOFError:
            win_string = ''

        win_string += name

        for card in cards:
            win_string += '\n'
            win_string += card[0]
            win_string += ','
            win_string += str(card[1])

        win_string += '_'

        with open('bin_files/win.bin', 'wb') as win_file:
            pickle.dump(win_string, win_file)

        # Delete any players not in the top 5

        # Read all players from win.bin
        with open('bin_files/win.bin', 'rb') as win_file:
            players_string = pickle.load(win_file)

        # Convert the string into an array.
        players = players_string.split('_')

        # Convert the array into a 2D array.
        for i in range(len(players)):
            players[i] = players[i].split('\n')

        # Remove ['']
        while players[-1] == ['']:
            players.pop()

        top5 = []

        while len(top5) < 5:

            index_of_highest = 0

            for i in range(len(players)):
                if len(players[i]) > len(players[index_of_highest]):
                    index_of_highest = i

            try:
                top5.append(players[index_of_highest])
                players.pop(index_of_highest)
            except IndexError:
                break # The players array contains less than 5 players.


        top5_string = ''

        for player in top5:
            top5_string += '\n'.join(player)
            top5_string += '_'

        with open('bin_files/win.bin', 'wb') as win_file:
            pickle.dump(top5_string, win_file)


    # Returns the top 5 players from win.txt as a tuple
    def read_top5():

        with open('bin_files/win.bin', 'rb') as win_file:
            players = pickle.load(win_file)

        players = players.split('_')

        for i in range(len(players)):
            players[i] = players[i].split('\n')

        try:
            while players[-1] == ['']:
                players.pop()
        except IndexError: # The players array might be empty
            pass

        return players


    def write_round_delay(seconds):
        with open('bin_files/round_delay.bin', 'wb') as rd_file:
            pickle.dump(seconds, rd_file)


    def load_round_delay():
        with open('bin_files/round_delay.bin', 'rb') as rd_file:
            return pickle.load(rd_file)


if __name__ == '__main__':
    main()

image.txt:

==============================================
|00000000000000000000000000000000000000000000|
|00001111110000111100000111111000011111110000|
|00010000000001000010001000000100010000001000|
|00100000000010000001001000000100010000000100|
|00100000000011111111001111111000010000000100|
|00100000000010000001001000001000010000000100|
|00010000000010000001001000000100010000001000|
|00001111110010000001001000000010011111110000|
|00000000000000000000000000000000000000000000|
|00000000000000000000000000000000000000000000|
|00011111100000111100001100000110011111111000|
|00100000010001000010001010001010010000000000|
|01000000000010000001001001010010010000000000|
|01000001110011111111001000100010011111110000|
|01000000010010000001001000000010010000000000|
|00100000010010000001001000000010010000000000|
|00011111100010000001001000000010011111111000|
|00000000000000000000000000000000000000000000|
==============================================

When you first run the program, you will need to choose the create deck option, and then load it in the load deck option. Then you can choose the play game option.

I know this program is huge, so any help will be really appreciated.