6
\$\begingroup\$

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

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.

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

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.

\$\endgroup\$

closed as unclear what you're asking by t3chb0t, Toby Speight, IEatBagels, Dannnno, Mast Oct 12 '18 at 6:04

Please clarify your specific problem or add additional details to highlight exactly what you need. As it's currently written, it’s hard to tell exactly what you're asking. See the How to Ask page for help clarifying this question. If this question can be reworded to fit the rules in the help center, please edit the question.

  • \$\begingroup\$ I changed the title so that it describes what the code does per site goals: "State what your code does in your title, not your main concerns about it.". Feel free to edit and give it a different title if there is something more appropriate - perhaps you could give a more descriptive name than just card game... \$\endgroup\$ – Sᴀᴍ Onᴇᴌᴀ Oct 10 '18 at 18:11
  • 6
    \$\begingroup\$ what is the game logic? what kind of card game? I thought it was some derivation from standard deck of playing cards, but it's not! \$\endgroup\$ – hjpotter92 Oct 10 '18 at 20:25
3
\$\begingroup\$

Well, for starters, you can make your script half the size by deleting all the unneeded blank lines...

This is actually one of the things defined in Python's official style-guide, PEP8:

Blank lines

Surround top-level function and class definitions with two blank lines.

Method definitions inside a class are surrounded by a single blank line.

Extra blank lines may be used (sparingly) to separate groups of related functions. Blank lines may be omitted between a bunch of related one-liners (e.g. a set of dummy implementations).

Use blank lines in functions, sparingly, to indicate logical sections.

Note the repeated use of the word "sparingly".

It is well-worth a read and will make your code easier to read by other Python developers.

Most developers use editors/editor plugins that automatically format the code according to PEP8 whenever the file is saved. I personally use Atom with the beautify plugin for this. There is also a commandline tool, autopep8, which can be run on a script file and is actually called behind the scenes by many of these plugins.


Another thing is your comments above your functions. These should instead be docstrings, which allow you to access the documentation in an interactive session (and using automatic documentation builder tools).

Here is how it could look like for (a randomly chosen) function from your code:

def new_random_deck(name, number_of_cards):
    """Create 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.

    The deck will be saved using `name` as file name.
    """
    assert number_of_cards % 3 == 0, "`number_of_cards` must be a multiple of 3"
    for _ in range(int(number_of_cards / 3)):
        deck.extend([new_card('red'), new_card('black'), new_card('yellow')])
    FileFunctions.write_deck(name, deck)

Note that I also removed the blank lines here, used list.extend instead of three list.append calls, used _ as the agreed-upon name of an unused iteration variable and explicitly asserted that the restriction on number_of_cards is actually true (assert statements can be disabled by running the script with the -O flag).

You could also use the itertools module (which is in the standard library) to make this into a list comprehension:

from itertools import islice, cycle

colours = 'red', 'black', 'yellow'
deck = [new_card(colour) for colour in islice(cycle(colours), number_of_cards)]

islice works just like normal list slicing, but on iterators (which can be infinite). cycle just infinitely cycles through the given iterable, starting at the beginning after having reached the end.


While I can't say that I actually understand the game (have not tried playing it yet), it seems weird to me that when you create a random new deck that while the number of cards in that deck is restrained, the content of the deck is not (in other words there is no way to ensure that it returns a random permutation, i.e. a shuffled version, of a deck). Or, a deck can contain the same card multiple times. This might be fine for your game but is not how most card games work (if they do contain a card multiple times, this number is at least fixed).

\$\endgroup\$

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