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))
2 Answers 2
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)])
-
\$\begingroup\$ Thank you, these are all very good points. But I never did
result = []
, I am not sure where this applies. \$\endgroup\$Vader– Vader2015年02月20日 21:13:34 +00:00Commented Feb 20, 2015 at 21:13 -
\$\begingroup\$ Yes you did, in
ascii_version_of_card
;-) \$\endgroup\$janos– janos2015年02月20日 21:14:47 +00:00Commented Feb 20, 2015 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– Vader2015年02月20日 21:16:50 +00:00Commented Feb 20, 2015 at 21:16 -
\$\begingroup\$ You're right. I thought more about and could compact it even further with less repetition. \$\endgroup\$janos– janos2015年02月20日 21:40:46 +00:00Commented Feb 20, 2015 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– Vader2015年02月20日 22:09:44 +00:00Commented Feb 20, 2015 at 22:09
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:])))
-
\$\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– Vader2015年02月21日 09:19:56 +00:00Commented Feb 21, 2015 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– Veedrac2015年02月21日 12:21:09 +00:00Commented Feb 21, 2015 at 12:21
Explore related questions
See similar questions with these tags.
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 saydraw this box here
anddraw that box there
. \$\endgroup\$curses
but decided against it because I wanted it to work in the windows command prompt. \$\endgroup\$