11
\$\begingroup\$

I am making a small Blackjack game in Python 3. The part of the code I am unsure about is that which allows me to print nice ASCII versions of the card. ascii_version_of_card() and ascii_version_of_hidden_card().

What the program does:

enter image description here


I am not seeking advice on the implementation of the Card() class. Only on ascii_version_of_card() and ascii_version_of_hidden_card().

The basic idea of the code is that there are 9 lines of output, and we iterate over all of the card (input) and create the appropriate version of that line for that card. Then we add that line to a master line. In the end we have 9 master lines which are our output.

Since some of the ASCII art is rendered strangely in the browser here is a dpaste.

class Card(object):

    card_values = {
        'Ace': 11,  # value of the ace is high until it needs to be low
        '2': 2,
        '3': 3,
        '4': 4,
        '5': 5,
        '6': 6,
        '7': 7,
        '8': 8,
        '9': 9,
        '10': 10,
        'Jack': 10,
        'Queen': 10,
        'King': 10
    }

    def __init__(self, suit, rank):
        """
        :param suit: The face of the card, e.g. Spade or Diamond
        :param rank: The value of the card, e.g 3 or King
        """
        self.suit = suit.capitalize()
        self.rank = rank
        self.points = self.card_values[rank]

def ascii_version_of_card(*cards, return_string=True):
    """
    Instead of a boring text version of the card we render an ASCII image of the card.
    :param cards: One or more card objects
    :param return_string: By default we return the string version of the card, but the dealer hide the 1st card and we
    keep it as a list so that the dealer can add a hidden card in front of the list
    """
    # we will use this to prints the appropriate icons for each card
    suits_name = ['Spades', 'Diamonds', 'Hearts', 'Clubs']
    suits_symbols = ['♠', '♦', '♥', '♣']

    # create an empty list of list, each sublist is a line
    lines = [[] for i in range(9)]

    for index, card in enumerate(cards):
        # "King" should be "K" and "10" should still be "10"
        if card.rank == '10':  # ten is the only one who's rank is 2 char long
            rank = card.rank
            space = ''  # if we write "10" on the card that line will be 1 char to long
        else:
            rank = card.rank[0]  # some have a rank of 'King' this changes that to a simple 'K' ("King" doesn't fit)
            space = ' '  # no "10", we use a blank space to will the void
        # get the cards suit in two steps
        suit = suits_name.index(card.suit)
        suit = suits_symbols[suit]

        # add the individual card on a line by line basis
        lines[0].append('┌─────────┐')
        lines[1].append('│{}{}       │'.format(rank, space))  # use two {} one for char, one for space or char
        lines[2].append('│         │')
        lines[3].append('│         │')
        lines[4].append('│    {}    │'.format(suit))
        lines[5].append('│         │')
        lines[6].append('│         │')
        lines[7].append('│       {}{}│'.format(space, rank))
        lines[8].append('└─────────┘')

    result = []
    for index, line in enumerate(lines):
        result.append(''.join(lines[index]))

    # hidden cards do not use string
    if return_string:
        return '\n'.join(result)
    else:
        return result


def ascii_version_of_hidden_card(*cards):
    """
    Essentially the dealers method of print ascii cards. This method hides the first card, shows it flipped over
    :param cards: A list of card objects, the first will be hidden
    :return: A string, the nice ascii version of cards
    """
    # a flipper over card. # This is a list of lists instead of a list of string becuase appending to a list is better then adding a string
    lines = [['┌─────────┐'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['└─────────┘']]

    # store the non-flipped over card after the one that is flipped over
    cards_except_first = ascii_version_of_card(*cards[1:], return_string=False)
    for index, line in enumerate(cards_except_first):
        lines[index].append(line)

    # make each line into a single list
    for index, line in enumerate(lines):
        lines[index] = ''.join(line)

    # convert the list into a single string
    return '\n'.join(lines)


# TEST CASES
test_card_1 = Card('Diamonds', '4')
test_card_2 = Card('Clubs', 'Ace')
test_card_3 = Card('Spades', 'Jack')
test_card_4 = Card('Hearts', '10')

print(ascii_version_of_card(test_card_1, test_card_2, test_card_3, test_card_4))
print(ascii_version_of_hidden_card(test_card_1, test_card_2, test_card_3, test_card_4))
# print(ascii_version_of_hidden_card(test_card_1, test_card_2))
\$\endgroup\$
  • \$\begingroup\$ Did you consider using curses? It would require some changes but it would let you do fancier things like colors and you can let the module handle the rendering. You would just say draw this box here and draw that box there. \$\endgroup\$ – Holli Sep 16 at 22:42
  • 1
    \$\begingroup\$ Looks like something cool here, I like that \$\endgroup\$ – user203258 Sep 16 at 23:13
  • \$\begingroup\$ @Holli I did consider using curses but decided against it because I wanted it to work in the windows command prompt. \$\endgroup\$ – Vader Sep 18 at 1:54
  • \$\begingroup\$ It's perfectly possible to use curses on windows. You just have to tell the console to expect ANSI codes. See this code, which shows how: hastebin.com/ofikovivuw.pl It's written in Raku/Perl6 but should be easy to port. \$\endgroup\$ – Holli Sep 18 at 16:56
8
\$\begingroup\$

Nice idea! The printed ASCII cards look great.


You could use more list comprehensions. For example this:

result = []
for index, line in enumerate(lines):
    result.append(''.join(lines[index]))

Can be simplified to this:

result = [''.join(line) for line in lines]

Likewise, this:

# make each line into a single list
for index, line in enumerate(lines):
    lines[index] = ''.join(line)

# convert the list into a single string
return '\n'.join(lines)

Can be simplified to this:

return '\n'.join([''.join(line) for line in lines])

In this list comprehension, the variable i is unused:

lines = [[] for i in range(9)]

The common convention is to use the name _ in such situations (instead of i).


For better readability, I recommend to expand this line to multiple lines:

lines = [['┌─────────┐'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['│░░░░░░░░░│'], ['└─────────┘']]

Like this, it's much easier to see that the formatting will be good, and in any case too long lines are hard to read:

lines = [
    ['┌─────────┐'],
    ['│░░░░░░░░░│'],
    ['│░░░░░░░░░│'],
    ['│░░░░░░░░░│'],
    ['│░░░░░░░░░│'],
    ['│░░░░░░░░░│'],
    ['│░░░░░░░░░│'],
    ['│░░░░░░░░░│'],
    ['└─────────┘']
]

Update

As you pointed out, 7 out of 9 lines there are repeated. It can be written in a more compact way, and the function can be further shortened to this:

def ascii_version_of_hidden_card(*cards):
    """
    Essentially the dealers method of print ascii cards. This method hides the first card, shows it flipped over
    :param cards: A list of card objects, the first will be hidden
    :return: A string, the nice ascii version of cards
    """
    lines = ['┌─────────┐'] + ['│░░░░░░░░░│'] * 7 + ['└─────────┘']

    cards_except_first = ascii_version_of_card(*cards[1:], return_string=False)

    return '\n'.join([x + y for x, y in zip(lines, cards_except_first)])
\$\endgroup\$
  • \$\begingroup\$ Thank you, these are all very good points. But I never did result = [], I am not sure where this applies. \$\endgroup\$ – Vader Feb 20 '15 at 21:13
  • \$\begingroup\$ Yes you did, in ascii_version_of_card ;-) \$\endgroup\$ – janos Feb 20 '15 at 21:14
  • \$\begingroup\$ yes, I just found, I feel silly. Do you think that in your last point lines can be further optimized. 7 out of the 9 lines are the same. \$\endgroup\$ – Vader Feb 20 '15 at 21:16
  • \$\begingroup\$ You're right. I thought more about and could compact it even further with less repetition. \$\endgroup\$ – janos Feb 20 '15 at 21:40
  • \$\begingroup\$ I have never seen this before return '\n'.join([x + y for x, y in zip(lines, cards_except_first)]) do you mind adding a short comment to the code that explains what it does? \$\endgroup\$ – Vader Feb 20 '15 at 22:09
10
\$\begingroup\$

I know you're not seeking advice about Card, but I feel I should point out that you don't need to inherit from object in Python 3.

I would remove the return_string option; if someone wants a list they should just call splitlines.

Your

suits_name = ['Spades', 'Diamonds', 'Hearts', 'Clubs']
suits_symbols = ['♠', '♦', '♥', '♣']

should probably be tuples:

suits_name = 'Spades', 'Diamonds', 'Hearts', 'Clubs'
suits_symbols = '♠', '♦', '♥', '♣'

but they're only ever used as a mapping:

name_to_symbol = {
    'Spades':   '♠',
    'Diamonds': '♦',
    'Hearts':   '♥',
    'Clubs':    '♣',
}

Your

# add the individual card on a line by line basis
lines[0].append('┌─────────┐')
lines[1].append('│{}{}       │'.format(rank, space))  # use two {} one for char, one for space or char
lines[2].append('│         │')
lines[3].append('│         │')
lines[4].append('│    {}    │'.format(suit))
lines[5].append('│         │')
lines[6].append('│         │')
lines[7].append('│       {}{}│'.format(space, rank))
lines[8].append('└─────────┘')

looks a bit ugly, but I can see why you did that. What might be better is making an auxillary function to stack lines of strings first:

def join_lines(strings):
    string_lines = [string.splitlines() for string in strings]
    return '\n'.join(''.join(out_line) for out_line in zip(*string_lines))

And then do:

card = (
    '┌─────────┐\n'
    '│{r}{_}       │\n'
    '│         │\n'
    '│         │\n'
    '│    {s}    │\n'
    '│         │\n'
    '│         │\n'
    '│       {_}{r}│\n'
    '└─────────┘\n'
).format(r=rank, _=space, s=suit)

card_strings.append(card)

Since the formatting of the card matters for prettiness, I'd be tempted to do:

    card = (
        '┌─────────┐\n'
        '│{}       │\n'
        '│         │\n'
        '│         │\n'
        '│    {}   │\n'
        '│         │\n'
        '│         │\n'
        '│       {}│\n'
        '└─────────┘\n'
    ).format(
        format(card.rank, ' <2'),
        format(card.suit, ' <2'),
        format(card.rank, ' >2')
    )

Of course, since card is from a static, you can move it out to get just

    card = CARD.format(
        format(rank, ' <2'),
        format(suit, ' <2'),
        format(rank, ' >2')
    )

This removes the need for space. You can then make the main loop a comprehension if you make a function, and stick it inside the join_lines call:

def card_to_string(card):
    # 10 is the only card with a 2-char rank abbreviation
    rank = card.rank if card.rank == '10' else card.rank[0]
    suit = name_to_symbol[card.suit]

    # add the individual card on a line by line basis
    return CARD.format(
        format(rank, ' <2'),
        format(suit, ' <2'),
        format(rank, ' >2')
    )

return join_lines(map(card_to_string, cards))

The calls to format can be removed by making card like:

CARD = """\
┌─────────┐
│{}       │
│         │
│         │
│    {}   │
│         │
│         │
│       {}│
└─────────┘
""".format('{rank: <2}', '{suit: <2}', '{rank: >2}')

and doing:

def card_to_string(card):
    # 10 is the only card with a 2-char rank abbreviation
    rank = card.rank if card.rank == '10' else card.rank[0]

    # add the individual card on a line by line basis
    return CARD.format(rank=rank, suit=name_to_symbol[card.suit])

ascii_version_of_hidden_card can then be just:

def ascii_version_of_hidden_card(*cards):
    """
    Essentially the dealers method of print ascii cards. This method hides the first card, shows it flipped over
    :param cards: A list of card objects, the first will be hidden
    :return: A string, the nice ascii version of cards
    """

    return join_lines((HIDDEN_CARD, ascii_version_of_card(*cards[1:])))

Here's the full thing:

CARD = """\
┌─────────┐
│{}       │
│         │
│         │
│    {}   │
│         │
│         │
│       {}│
└─────────┘
""".format('{rank: <2}', '{suit: <2}', '{rank: >2}')

HIDDEN_CARD = """\
┌─────────┐
│░░░░░░░░░│
│░░░░░░░░░│
│░░░░░░░░░│
│░░░░░░░░░│
│░░░░░░░░░│
│░░░░░░░░░│
│░░░░░░░░░│
└─────────┘
"""

def join_lines(strings):
    """
    Stack strings horizontally.
    This doesn't keep lines aligned unless the preceding lines have the same length.
    :param strings: Strings to stack
    :return: String consisting of the horizontally stacked input
    """
    liness = [string.splitlines() for string in strings]
    return '\n'.join(''.join(lines) for lines in zip(*liness))

def ascii_version_of_card(*cards):
    """
    Instead of a boring text version of the card we render an ASCII image of the card.
    :param cards: One or more card objects
    :return: A string, the nice ascii version of cards
    """

    # we will use this to prints the appropriate icons for each card
    name_to_symbol = {
        'Spades':   '♠',
        'Diamonds': '♦',
        'Hearts':   '♥',
        'Clubs':    '♣',
    }

    def card_to_string(card):
        # 10 is the only card with a 2-char rank abbreviation
        rank = card.rank if card.rank == '10' else card.rank[0]

        # add the individual card on a line by line basis
        return CARD.format(rank=rank, suit=name_to_symbol[card.suit])


    return join_lines(map(card_to_string, cards))


def ascii_version_of_hidden_card(*cards):
    """
    Essentially the dealers method of print ascii cards. This method hides the first card, shows it flipped over
    :param cards: A list of card objects, the first will be hidden
    :return: A string, the nice ascii version of cards
    """

    return join_lines((HIDDEN_CARD, ascii_version_of_card(*cards[1:])))
\$\endgroup\$
  • \$\begingroup\$ Thank you for this comprehensive answer. I feel that doing splitlines is a bit redundant because I am just undoing something I just did. It makes more sense not to do it in the first place. \$\endgroup\$ – Vader Feb 21 '15 at 9:19
  • \$\begingroup\$ @Vader That isn't worth worrying about in a routine that prints; it take a few microseconds and you do it at most a few times a second. Even if it was, it's better to have two functions than a parameter that changes the type of the return value - the latter seems likely to cause bugs. \$\endgroup\$ – Veedrac Feb 21 '15 at 12:21

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.