1
\$\begingroup\$

I have programmed a card game in Python. How can I improve the code?

There is a deck of cards. Each cards has a colour (red, black or yellow) and a number from 1-10. There are two players. Every round, each player takes one card from the top of the deck. Those cards are compared, and the winner of the round is assigned based on the following rules:

  • Red beats black
  • Black beats yellow
  • Yellow beats red

If both cards have the same colour, the card with the highest number wins. If they have the same colour and number, it is a draw.

The winner of the round keeps both cards. If the round was a draw, the players keep their own cards.

This is repeated until the deck is empty. The winner is the person with the most cards at the end of the game.

main.py:

import os
from random import shuffle
from time import sleep
from termcolor import colored

# Card index constants
COLOUR = 0
NUMBER = 1

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


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


def main():

    clear()

    if not login():
        return

    game = Game()

    menu_loop = True

    menu_array = [
            '                                              ',
            '                                              ',
            '  1 - Play game                               ',
            '  2 - Create a new deck                       ',
            '  3 - Load a deck                             ',
            '  4 - Delete a deck                           ',
            '  5 - Change the speed of the game            ',
            '  6 - View the leaderboard                    ',
            '  7 - Quit                                    ',
            '                                              ',
            '                                              '
    ]

    while menu_loop:

        clear()

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

        for c in image_text:

            if c == '0':
                print(' ', end='')

            elif c == '1':
                c = colored(' ', 'yellow', 'on_yellow')
                print(c, end='')

            elif c == '=' or c == '|':
                c = colored(' ', 'blue', 'on_blue')
                print(c, end='')

            elif c == '\n':
                print()

        print()

        for i in range(len(menu_array)):

            line = menu_array[i]

            if i == 0 or i == len(menu_array) - 1:
                print(colored(' ' * len(line), 'blue', 'on_blue'))
            else:

                first_char = colored(' ', 'blue', 'on_blue')
                middle = colored(line[1:-1], 'cyan')
                last_char = colored(' ', 'blue', 'on_blue')
                print(first_char + middle + last_char)

        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()
            try:
                top5 = FileFunctions.read_top5()
                display_leaderboard(top5) if len(top5) > 0 else print('\nThere are no players on the leaderboard yet.\n')
            except FileNotFoundError:
                print('\nCould not find leaderboard file.\n')
            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():

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

    except EOFError:
        print('Could not read the password file. The game cannot be played.')
        return False

    except FileNotFoundError:
        print('Could not find the password file. The game cannot be played.')
        return False

    valid = False

    while not valid:

        password_attempt = input('Enter password: ')
        clear()

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

    return True


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

# Creates a new random deck.
def new_deck(name, number_of_cards):

    deck = []

    for _ in range(int(number_of_cards / 30)):
        for i in range(10):
            deck.append(new_card('red', i+1))
            deck.append(new_card('black', i+1))
            deck.append(new_card('yellow', i+1))

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

        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

    try:
        decks = FileFunctions.load_deck_names()
        decks_found = True
    except FileNotFoundError:
        decks_found = False

    while not valid:

        if decks_found:
            print('\nDecks:\n')
            for deck in decks:
                print(deck)
        else:
            print('\nThe deck names could not be found, but you should still be able to load a deck.\n')

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

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

        try:
            f = open(deck_name + '.txt', '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()
        clear()

        valid = True

        if yes_or_no == 'y':

            name_valid = False

            while not name_valid:

                banned_names = ('password', 'title', 'leaderboard', 'round_delay', 'current_deck')

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

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

                elif deck_name in banned_names:
                    print('\nYour deck name cannot be any of the following:')
                    for name in banned_names:
                        print(name)
                    print()

                else:
                    name_valid = True

            number_valid = False

            while not number_valid:

                try:
                    number = int(input('Enter amount of cards: '))
                except ValueError:
                    clear()
                    print('\nPlease enter an integer.\n')
                    continue

                clear()

                if not (number % 2 == 0 and number % 3 == 0):
                    print('\nAmount must be an even multiple of 3.\n')

                elif number == 0:
                    print('\nIf there are no cards in a deck, is it still a deck?\n')

                elif number < 0:
                    print('\nYou can\'t have a negative amount of cards.\n')

                else:
                    number_valid = True

            new_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

    try:
        current_delay = FileFunctions.load_round_delay()
    except FileNotFoundError:
        print('\nThe round delay file could not be loaded.\n')
        return

    current_delay = round(current_delay, 3)
    current_delay = str(current_delay)

    while not valid:

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

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

        clear()

        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: '))

                    clear()

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

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

            FileFunctions.write_round_delay(seconds)

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

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

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


def delete_deck_menu():

    valid = False

    while not valid:

        yes_or_no = input('Are you sure you want to delete a deck? (y/n)').lower()
        clear()

        if yes_or_no == 'y':
            valid = True
        elif yes_or_no == 'n':
            print('\nDeletion of deck has been cancelled.\n')
            return
        else:
            print('\nPlease answer with \'y\' or \'n\'.\n')

    deck_names = FileFunctions.load_deck_names()

    valid = False

    while not valid:

        print('\nDecks:\n')
        for name in deck_names:
            print(name)

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

        clear()

        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 + '.txt', 'r'):
                    valid = True

                os.remove(deck_name + '.txt')

                with open('deck_names.txt', 'r') as deck_names_file:
                    current_deck_names = deck_names_file.read().split('\n')


                current_deck_names.remove(deck_name)

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




    with open('deck_names.txt', 'w') as deck_names_file:
        deck_names_file.write('\n'.join(current_deck_names))
        deck_names_file.write('\n')

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

        if deck_name.strip() == '':
            print('Please load a deck with option of 3 the main menu.')
            return

        play_again = True

        while play_again:

            # Read the deck from the deck file.
            deck = FileFunctions.load_deck()

            player1_cards = []
            player2_cards = []

            shuffle(deck)

            game_round = 1

            print('\n\n')

            while len(deck) > 0:

                sleep(round_delay)

                try:
                    player1_card = deck[-1]
                    player2_card = deck[-2]
                except IndexError: # The deck is empty
                    break

                deck.pop()
                deck.pop()

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

                winner = self._compare_cards(player1_card, player2_card)
                self._display_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):

        card_size = 15

        p1_bg_colour = card1[COLOUR]
        p2_bg_colour = card2[COLOUR]

        if p1_bg_colour == 'black':
            p1_bg_colour = 'grey'

        if p2_bg_colour == 'black':
            p2_bg_colour = 'grey'

        if p1_bg_colour in ('grey', 'red'):
            p1_fg_colour = 'white'
        elif p1_bg_colour == 'yellow':
            p1_fg_colour = 'grey'

        if p2_bg_colour in ('grey', 'red'):
            p2_fg_colour = 'white'
        elif p2_bg_colour == 'yellow':
            p2_fg_colour = 'grey'

        p1_bg_colour = 'on_' + p1_bg_colour
        p2_bg_colour = 'on_' + p2_bg_colour

        space_between_cards = ' ' * 15

        space_length = card_size - len(self.player1_name)
        players_string = colored(self.player1_name.upper(), p1_fg_colour, p1_bg_colour)
        players_string += colored(' ' * space_length, p1_fg_colour, p1_bg_colour)
        players_string += space_between_cards
        players_string += colored(self.player2_name.upper(), p2_fg_colour, p2_bg_colour)
        space_length = card_size - len(self.player2_name)
        players_string += colored(' ' * space_length, p2_fg_colour, p2_bg_colour)

        line = colored('=' * card_size, p1_fg_colour, p1_bg_colour)
        line += space_between_cards
        line += colored('=' * card_size, p2_fg_colour, p2_bg_colour)

        print(line)
        print(players_string)
        print(line)

        p1_number_array = self._get_number_string(card1[NUMBER]).split('\n')
        p2_number_array = self._get_number_string(card2[NUMBER]).split('\n')

        length = len(p2_number_array) if len(p2_number_array) > len(p1_number_array) else len(p1_number_array)

        for i in range(length):

            output_line = ''
            p1_line = p1_number_array[i]

            for j in range(len(p1_line)):
                if p1_line[j] == '1':
                    output_line += colored(' ', p1_fg_colour, 'on_' + p1_fg_colour)
                else:
                    output_line += colored(' ', p1_fg_colour, p1_bg_colour)

            space_length = card_size - len(p1_line)
            output_line += colored(' ' * space_length, p1_fg_colour, p1_bg_colour)
            output_line += space_between_cards

            p2_line = p2_number_array[i]

            for j in range(len(p2_line)):
                if p2_line[j] == '1':
                    output_line += colored(' ', p2_fg_colour, 'on_' + p2_fg_colour)
                else:
                    output_line += colored(' ', p2_fg_colour, p2_bg_colour)

            space_length = card_size - len(p2_line)
            output_line += colored(' ' * space_length, p2_fg_colour, p2_bg_colour)

            print(output_line)


    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)

    def _get_number_string(self, number):

        if number > 10 or number < 0:
            print('Invalid card', end=' ')

        number_strings = (

            '   1\n   1\n   1\n   1\n   1',
            '1111\n   1\n1111\n1   \n1111',
            '1111\n   1\n1111\n   1\n1111',
            '1  1\n1  1\n1111\n   1\n   1',
            '1111\n1   \n1111\n   1\n1111',
            '1111\n1   \n1111\n1  1\n1111',
            '1111\n   1\n   1\n   1\n   1',
            '1111\n1  1\n1111\n1  1\n1111',
            '1111\n1  1\n1111\n   1\n   1',
            '1  1111\n1  1  1\n1  1  1\n1  1  1\n1  1111'
        )

        return number_strings[number - 1]


class FileFunctions:

    # Reads the current deck's name
    def load_current_deck_name():
        with open('current_deck_name.txt', 'r') as cd_file:
            return cd_file.read()

    def change_current_deck_name(new_name):
        with open('current_deck_name.txt', 'w') as cd_file:
            cd_file.write(new_name)

    # 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 + '.txt', 'w') as deck_file:
            deck_file.write(deck_string)

        with open('deck_names.txt', 'a') as deck_names_file:
            deck_names_file.write(name + '\n')

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

        with open('current_deck_name.txt', 'r') as current_deck_name_file:
            name = current_deck_name_file.read()

        deck_array = []

        with open(name + '.txt', 'r') as deck_file:
            deck_text = deck_file.read()

        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('password.txt', 'r') as password_file:
            return password_file.read()

    def clear_leaderboard():
        with open('leaderboard.txt', 'w'):
            pass

    # 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('leaderboard.txt', 'r') as win_file:
                win_string = win_file.read()
        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('leaderboard.txt', 'w') as win_file:
            win_file.write(win_string)

        # Delete any players not in the top 5

        # Read all players from file.
        with open('leaderboard.txt', 'r') as win_file:
            players_string = win_file.read()

        # 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('leaderboard.txt', 'w') as win_file:
            win_file.write(top5_string)

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

        with open('leaderboard.txt', 'r') as win_file:
            players = win_file.read()

        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('round_delay.txt', 'w') as rd_file:
            rd_file.write(str(seconds))

    def load_round_delay():
        with open('round_delay.txt', 'r') as rd_file:
            return float(rd_file.read())

    def load_deck_names():

        with open('deck_names.txt', 'r') as deck_names_file:
            deck_names_string = deck_names_file.read()

        deck_names = deck_names_string.split('\n')

        return deck_names


if __name__ == '__main__':
    main()

title.txt:

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

password.txt contains 'Python'

round_delay.txt contains '1.0'

leaderboard.txt, current_deck.txt and deck_names.txt are all empty.

\$\endgroup\$
1
\$\begingroup\$
  • Make it modular

    For example, use ask()/prompt() for yes/no

    and

    printlne()
    

    where

    def printlne(msg) :
        print('\n'+msg+'\n')
    

    Instead of

    print('\n text \n')
    

    instead of checking for yes or no

  • Put it on an online git repository so people can contribute or you can manage easily.

\$\endgroup\$

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.