48
\$\begingroup\$

Problem:

I have seen a few questions around hangman. Usually this is done in a very hackish way, which usually can not be generalized any further. My thought or question is about the creation of the gown, which is a central part in any hangman game.

I want to create the ASCII image below

 _____
 |   |
 O   |
/|\  |
/ \  |
     |          
 ---------- 

Where one "limb" of the stick figure should appear each time the function is prompted.

I also wanted to be able to specify both the height and width of the gown. However I was not able to scale the stick figure relative to the gown size. The code is provided below and I have two simple questions about it =)

Question:

  • Is there a cleaner way to hang the man? I feel my method is very barbaric (I used some black voodo graduate mathematics to get it to look correct).
  • Is there a way to make the stick figure scale with the size of the gown? (essentially providing more guesses).

Code:

from math import ceil


def create_gown(width, height):
    gown = []
    gown.append('{:>{}s}'.format('_'*int(width/2), 10))
    gown.append('{:>{}s} {:>{}s}'.format(
        '|', 11 - int(width/2), '|', int(width/2)-2))
    for i in range(height-3):
        gown.append('{:^{}s}'.format('|', 20))
    gown.append('{:^{}s}'.format('-'*width, 20))
    return gown


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 attempt == 0:
        return gown
    elif attempt == 1:
        new_line = '{:>{}s} {:>{}s}'.format('O', offset1-1, '|', offset3+1)
        row = 2
    elif attempt == 2:
        new_line = '{:>{}s} {:>{}s}'.format('|', offset1-1, '|', offset3+1)
        row = 3
    elif attempt == 3:
        new_line = '{:>{}s} {:>{}s}'.format('/| ', offset1, '|', offset3)
        row = 3
    elif attempt == 4:
        new_line = '{:>{}s} {:>{}s}'.format('/|\\', offset1, '|', offset3)
        row = 3
    elif attempt == 5:
        new_line = '{:>{}s} {:>{}s}'.format('/  ', offset1, '|', offset3)
        row = 4
    elif attempt == 6:
        new_line = '{:>{}s} {:>{}s}'.format('/ \\', offset1, '|', offset3)
        row = 4
    else:
        raise Exception("Ops! The number of attempts must be an integer from 0 to 6.")
    gown[row] = new_line
    return gown


def print_gown(gown):
    for line in gown:
        print line

if __name__ == '__main__':
    gown = create_gown(10, 7)
    print len(gown)
    for i in range(7):
        gown = wrong_answer(gown, i)
        print_gown(gown)
\$\endgroup\$
5
  • 9
    \$\begingroup\$ Do you mean "gallows" when you say "gown"? \$\endgroup\$ Jun 7, 2016 at 16:08
  • \$\begingroup\$ The most common, but not necessarily the right way to hang a man is to hang him the right way ;) \$\endgroup\$ Jun 8, 2016 at 9:11
  • \$\begingroup\$ I have rolled back the last edit as it invalidates Mathias' answer. Please see What to do when someone answers. \$\endgroup\$
    – Peilonrayz
    Jun 8, 2016 at 10:23
  • \$\begingroup\$ This question has been selected as the winner of Best of Code Review 2016 — Best Title. \$\endgroup\$ Jan 18, 2017 at 19:11
  • 2
    \$\begingroup\$ Woo!⏑⏑⏑⏑⏑⏑⏑⏑⏑⏑⏑ \$\endgroup\$ Jan 18, 2017 at 20:14

1 Answer 1

35
+50
\$\begingroup\$

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.

\$\endgroup\$
4
  • \$\begingroup\$ Lovely answer! Are you sure about the ` try: pattern, row = _GOWN_MODIFIER[attempt]` part? It seems to catch every error, not just the number not in range part. About your comment on magic numbers i completely agree. I seem to center the gown in 20-sized strings. Is there a way to avoid that? I tried to base it of just height and width, however I ran into some issues. Would be great if you had any tips in avoiding the calculus bit (as it is both unclear what it does, and it contains magic numbers). Again great advice on the other parts! \$\endgroup\$ Jun 7, 2016 at 13:33
  • \$\begingroup\$ The try part is paired with an except KeyError so only attempts not defined in the _GOWN_MODIFIER dictionary will trigger your custom exception. The else part is executed only if the try ran without problems. \$\endgroup\$ Jun 7, 2016 at 13:39
  • \$\begingroup\$ Thanks! I also completely agree on the generator. I tried making it into a generator, but was not able to do so. Thanks for showing me the proper way to do it \$\endgroup\$ Jun 7, 2016 at 13:41
  • 1
    \$\begingroup\$ And for the width part, you should try not to center '|' in your intermediate strings. Something like ' ' * half_width + '|' should get you the pole and pattern + ' ' * (half_width - 3) + '|' would be enough for intermediate parts. Using format will also make it cleaner. \$\endgroup\$ Jun 7, 2016 at 13:44

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

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