Repetitions
All your elif
share the same structure. You just need to ensure that the inserted pattern is always 3 wide and you can drop your offsets adjustments. The logical next step is to use a dictionary to store the pattern and the row for each attempt:
_GOWN_MODIFIER = {
1: (' O ', 2),
2: (' | ', 3),
3: ('/| ', 3),
4: ('/|\\', 3),
5: ('/ ', 4),
6: ('/ \\', 4),
}
def wrong_answer(gown, attempt=0):
height, width = len(gown), len(gown[-1].strip())
offset1 = int((-width+23)*0.5)+1
offset3 = int(ceil(0.5*(width-7)))
if not attempt:
return gown
try:
pattern, row = _GOWN_MODIFIER[attempt]
except KeyError:
raise Exception("Ops! The number of attempts must be an integer from 0 to 6.")
else:
gown[row] = '{:>{}s} {:>{}s}'.format(pattern, offset1, '|', offset3)
return gown
Calculus
You do not need ceil
nor int
to perform an integer division by 2. In Python 2, /
is already the operator you need. You can also use //
which is also the same operation. The advantage of the latter over the former is that the behaviour is the same in Python 3, whereas /
will perform a decimal division in Python 3.
_GOWN_MODIFIER = {
1: (' O ', 2),
2: (' | ', 3),
3: ('/| ', 3),
4: ('/|\\', 3),
5: ('/ ', 4),
6: ('/ \\', 4),
}
def wrong_answer(gown, attempt=0):
if not attempt:
return gown
height, width = len(gown), len(gown[-1].strip())
offset1 = (23 - width) // 2 + 1
offset3 = (width - 6) // 2
try:
pattern, row = _GOWN_MODIFIER[attempt]
except KeyError:
raise Exception("Ops! The number of attempts must be an integer from 0 to 6.")
else:
gown[row] = '{:>{}s} {:>{}s}'.format(pattern, offset1, '|', offset3)
return gown
def create_gown(width, height):
half_width = width // 2
gown = []
gown.append('{:>{}s}'.format('_'*half_width, 10))
gown.append('{:>{}s} {:>{}s}'.format(
'|', 11 - half_width, '|', half_width-2))
for i in range(height-3):
gown.append('{:^{}s}'.format('|', 20))
gown.append('{:^{}s}'.format('-'*width, 20))
return gown
Creation vs modifications
You're aware that you can create sequences of repeating elements using seq(element) * count
. You can use that to create your gown
variable by repeating the pole and then modifying the top two rows and the last one; it should be more memory friendly as the space needed to store the list will be allocated in one go:
def create_gown(width, height):
half_width = width // 2
gown = ['{:^{}s}'.format('|', 20)] * height
gown[0] = '{:>{}s}'.format('_' * half_width, 10)
gown[1] = '{:>{}s} {:>{}s}'.format(
'|', 11 - half_width, '|', half_width-2)
gown[-1] = '{:^{}s}'.format('-' * width, 20)
return gown
On modifying in place and returning a value
and a bit about printing too
Your wrong_answer
both modifies the gown in place and return the modified value. This is unnecessary as the caller still should hold a reference to the value (which was modified). When calling the code, you can have:
gown2 = wrong_answer(gown, attempt=3)
but
>>> gown2 is gown
True
So there is really no point in duplicating references to the same object. Instead, what you can do is preprocess the printing job, so you don't really need the print_gown
function:
def wrong_answer(gown, attempt=0):
height, width = len(gown), len(gown[-1].strip())
offset1 = (23 - width) // 2 + 1
offset3 = (width - 6) // 2
if attempt:
try:
pattern, row = _GOWN_MODIFIER[attempt]
except KeyError:
raise Exception("Ops! The number of attempts must be an integer from 0 to 6.")
else:
gown[row] = '{:>{}s} {:>{}s}'.format(pattern, offset1, '|', offset3)
return '\n'.join(gown)
Usage:
print(wrong_answer(gown, attempt=3))
Exceptions
Raising a generic purpose Exception
is bad practice, as it makes the except
clauses trying to handle your code being able to catch more than they should. You should at least use a more generic exception (such as ValueError
) or define your own:
class AlreadyHangedError(ValueError):
pass
def wrong_answer(gown, attempt):
...
raise AlreadyHangedError('...')
...
However, there is something odd in the way you use your wrong_answer
: the caller is expected to call this function using increasing attempts
. This is the job for a generator. By turning wrong_answer
into a generator, you can call it to create a generator instance and then call next
on this instance (or let a for
loop do it for you) to get the next gown to print:
def wrong_answer(width, height):
gown = create_gown(width, height)
offset1 = (23 - width) // 2 + 1
offset3 = (width - 6) // 2
yield '\n'.join(gown)
for attempt in range(1, 7):
pattern, row = _GOWN_MODIFIER[attempt]
gown[row] = '{:>{}s} {:>{}s}'.format(pattern, offset1, '|', offset3)
yield '\n'.join(gown)
Usage:
for gown in wrong_answer(10, 7):
print(gown)
or, in a more real-case scenario:
def hangman_game():
for gown in wrong_answer(10, 7):
while True:
status = manage_user_input()
if status == 'FAILED': # Whatever
break
if status == 'COMPLETED': # Whatever
return
print gown
print 'You lose'
Magic numbers
create_gown
seems to center the gown in 20-sized strings, but what if width
is greater than that? At first, you should define this value and give it a meaningful name, and same for all values that derive from it; and then, you might want to take max(20, width)
as a base for your calculus.