14
\$\begingroup\$

This program animates people going inside and outside of a house, the user has to keep a running sum in their head and must give the answer at the end.

The speed of people, the number of maximum people inside the house and the number of rounds to be played can be adjusted, at the end the user will get a report saying how many he got right.

You can see the program in action here (Italian commentary, not important)

I took two random images from the internet for this, if you are concerned about copyright, you can just use rectangles of sizes:

  • 396*324 for the house
  • 197*255 for the person

Folder with code and images

I feel like the code contains too much repetition. It is easier for me to avoid repetition in logic/computational programming but making this animation led me to more repetition than I would have liked, especially in ingoing vs outgoing

I am open to all kinds of suggestions:

import pygame
import random
import os
import time

pygame.init()
screen = pygame.display.set_mode((800, 640))
person = pygame.image.load('person.png')
house = pygame.image.load('house.png')
clock = pygame.time.Clock()
pygame.display.set_caption('People counting game')

myfont = pygame.font.SysFont('comicsansms', 80)
smallfont = pygame.font.SysFont('comicsansms', 30)


ingoing_persons = 5
outgoing_persons = 3

def animate_person_going_inside_house(speed):

    running = True
    x = -200
    while running:
        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            running = False

        x += speed
        screen.fill((255, 255, 255)) # fill the screen
        screen.blit(person, (int(x), 340))
        screen.blit(house, (200, 260))
        if x > 300:
            return
        pygame.display.update() # Just do one thing, update/flip.

        clock.tick(40) # This call will regulate your FPS (to be 40 or less)

def animate_person_going_outside_house(speed):

    running = True
    x = 300
    while running:
        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            running = False

        x += speed
        screen.fill((255, 255, 255)) # fill the screen
        screen.blit(person, (int(x), 340))
        screen.blit(house, (200, 260))
        if x > 900:
            return
        pygame.display.update() # Just do one thing, update/flip.

        clock.tick(40) # This call will regulate your FPS (to be 40 or less)

def animate_all_people(ingoing, outgoing, difficulty):
    result = ingoing - outgoing
    ingoing_so_far = 0
    outgoing_so_far = 0
    people_inside = 0
    while True:
        if random.choice( (0, 1) ):
            if ingoing_so_far < ingoing:
                people_inside += 1
                animate_person_going_inside_house(difficulty)
                ingoing_so_far += 1
        else:
            if outgoing_so_far < outgoing and people_inside > 0: # People can only exit if people are inside!
                people_inside -= 1
                animate_person_going_outside_house(difficulty)
                outgoing_so_far += 1
        if ingoing_so_far == ingoing and outgoing_so_far == outgoing:
            break

    running = True
    while running:
        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if (event.key == pygame.K_0 and people_inside == 0) or \
               (event.key == pygame.K_1 and people_inside == 1) or \
               (event.key == pygame.K_2 and people_inside == 2) or \
               (event.key == pygame.K_3 and people_inside == 3) or \
               (event.key == pygame.K_4 and people_inside == 4) or \
               (event.key == pygame.K_5 and people_inside == 5) or \
               (event.key == pygame.K_6 and people_inside == 6) or \
               (event.key == pygame.K_7 and people_inside == 7) or \
               (event.key == pygame.K_8 and people_inside == 8) or \
               (event.key == pygame.K_9 and people_inside == 9):
                for _ in range(40 * 2):
                    text = myfont.render('Correct! {}!'.format(str(result)), False, (255, 0, 0))
                    screen.blit(text,
        (320 - text.get_width() // 2, 240 - text.get_height() // 2))
                    pygame.display.flip()
                return 1

            else:
                for _ in range(40 * 2):
                    text = myfont.render('Wrong!, It was {}'.format(str(result)), False, (255, 0, 0))
                    screen.blit(text,
        (320 - text.get_width() // 2, 240 - text.get_height() // 2))
                    pygame.display.flip()
                return 0

def random_if_condition(minmax, condition):
    while True:
        r = random.randint(*minmax)
        if condition(r):
            return r

"""
def play_game(difficulty):
    ingoing, outgoing = random.randint(0, 9), random.randint(0, 9)
    animate_all_people(ingoing, outgoing, difficulty)
"""

def play_match(rounds, speed, max_people):
    while True:
        text = smallfont.render("Count the people inside the house.", False, (255, 0, 0))
        screen.blit(text,
        (380 - text.get_width() // 2, 140 - text.get_height() // 2))

        text = smallfont.render("When no more people are moving", False, (255, 0, 0))
        screen.blit(text,
        (380 - text.get_width() // 2, 240 - text.get_height() // 2))

        text = smallfont.render("press the number on the keyboard.", False, (255, 0, 0))
        screen.blit(text,
        (380 - text.get_width() // 2, 340 - text.get_height() // 2))

        text = smallfont.render("Press any key to start playing.", False, (255, 0, 0))
        screen.blit(text,
        (380 - text.get_width() // 2, 440 - text.get_height() // 2))
        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            return
        if event.type == pygame.KEYUP or event.type == pygame.KEYDOWN:
            break
        pygame.display.flip()


    points = 0
    for _ in range(rounds):
        ingoing = random.randint(0, max_people)        # Careful to avoid more outgoing than ingoing
        points += animate_all_people(ingoing , random_if_condition( (0, max_people), lambda r: r <= ingoing), speed)
    for _ in range(40 * 5): # 5 seconds
        text = myfont.render("You got {}/{} right".format(points, rounds), False, (255, 0, 0))
        screen.blit(text,
        (320 - text.get_width() // 2, 140 - text.get_height() // 2))

        pygame.display.flip()


#animate_all_people(random.randint(0, 9) , random.randint(0, 9), 30)


if __name__ == "__main__":
    play_match(rounds = 3, speed = 15, max_people = 6)
\$\endgroup\$
  • \$\begingroup\$ The google drive folder with code.. seems to be pointing to a image..Maybe you can upload it to github? \$\endgroup\$ – alpha_989 Feb 24 '18 at 21:38
  • \$\begingroup\$ @alpha_989 added correct link to git-hub \$\endgroup\$ – Caridorc Feb 25 '18 at 19:28
12
\$\begingroup\$

You're right - there is a lot of duplication. Also, some organization is needed.

Organize!

Before you do anything else, get everything into a function of some kind. All those statements at module scope, move them into a setup function, write yourself a main, and do the standard Python thing:

if __name__ == '__main__':
    main()

You can call play_match from inside main after you call setup. You might even put in a while: loop to play multiple matches.

Animate

Let's have a look at your in/out functions:

def animate_person_going_inside_house(speed):

    running = True
    x = -200
    while running:
        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            running = False

        x += speed
        screen.fill((255, 255, 255)) # fill the screen
        screen.blit(person, (int(x), 340))
        screen.blit(house, (200, 260))
        if x > 300:
            return
        pygame.display.update() # Just do one thing, update/flip.

        clock.tick(40) # This call will regulate your FPS (to be 40 or less)

def animate_person_going_outside_house(speed):

    running = True
    x = 300
    while running:
        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            running = False

        x += speed
        screen.fill((255, 255, 255)) # fill the screen
        screen.blit(person, (int(x), 340))
        screen.blit(house, (200, 260))
        if x > 900:
            return
        pygame.display.update() # Just do one thing, update/flip.

        clock.tick(40) # This call will regulate your FPS (to be 40 or less)

The lines that are different are:

    x = -200
    x = 300

        if x > 300:
        if x > 900:

It seems fairly obvious that these could be parameters:

def animate_person(speed, start_x, target_x):

    x = start_x

        if x > target_x:

Then you can rewrite the two functions as:

def animate_person_going_inside_house(speed):
    animate_person(speed, -200, 300)

def animate_person_going_outside_house(speed):
    animate_person(speed, 300, 900)

Or, you could simply eliminate them and call the animate_person directly.

Clarify

You have a lot of "magic" numbers, like -200, 900, 300, 260, 340, 200, and (255,255,255). I don't know what they mean. Replace them with named constants:

X_PERSON_OFF_LEFT_EDGE = -200
X_DOOR_OF_HOUSE = 300

Remember, you are writing code for the next maintainer, not for the compiler.

You have this while loop that is unclear:

while True:
    if random.choice( (0, 1) ):
        if ingoing_so_far < ingoing:
            people_inside += 1
            animate_person_going_inside_house(difficulty)
            ingoing_so_far += 1
    else:
        if outgoing_so_far < outgoing and people_inside > 0: # People can only exit if people are inside!
            people_inside -= 1
            animate_person_going_outside_house(difficulty)
            outgoing_so_far += 1
    if ingoing_so_far == ingoing and outgoing_so_far == outgoing:
        break

When you see a break, you have to ask, could I add this to the main loop test? In this case, yes, you can:

while ingoing + outgoing != 0:
    direction = random.choice( ('in',) * ingoing + ('out',) * outgoing )

    if direction == 'in':
        ingoing -= 1
        animate_person_going_inside_house(difficulty)
        people_inside += 1
    else:
        outgoing -= 1
        animate_person_going_outside_house(difficulty)
        people_inside -= 1

Note that I have changed the behavior here, from 50/50 to proportional to the # remaining. You could certainly change it back. The point is to change the while condition.

Note also that this is where you would replace calls to separate inside/outside functions with a single function. You could create a container with two sets of parameters, and index by a random number (0,1) or random string ('in', 'out') to select which parameter set to use.

Help Yourself

Don't be afraid to write helper functions. In fact, if you write a comment, ask yourself if that comment should be a function name instead. If you write a paragraph of code, and then break for another paragraph, ask yourself if that should be a new function. Even if you only call it once, it might be a structural function rather than a reusable function.

You have this code:

        if (event.key == pygame.K_0 and people_inside == 0) or \
           (event.key == pygame.K_1 and people_inside == 1) or \
           (event.key == pygame.K_2 and people_inside == 2) or \
           (event.key == pygame.K_3 and people_inside == 3) or \
           (event.key == pygame.K_4 and people_inside == 4) or \
           (event.key == pygame.K_5 and people_inside == 5) or \
           (event.key == pygame.K_6 and people_inside == 6) or \
           (event.key == pygame.K_7 and people_inside == 7) or \
           (event.key == pygame.K_8 and people_inside == 8) or \
           (event.key == pygame.K_9 and people_inside == 9):

That cries out to be a function. Or two functions.

if is_digit_key(event.key) and digit_val(event.key) == people_inside:

Advance!

One thing you don't do is support multiple people moving at the same time. I don't know how important that is to you, but your code isn't structured to handle it.

You would need a list of moving people, and their target locations. You could create a simple class for that:

class MovingPerson:
    def __init__(start_x, target_x, speed):

Then your animate function would loop over all the moving sprites, update them, then refresh the screen:

for p in moving_people:
    p.update()
pygame.display.update()
\$\endgroup\$
  • 1
    \$\begingroup\$ I have doubts about your random.choice usage, on my 2.7 python it gives: >>> import random >>> random.choice( ('in') * 3 + ('out') * 4 ) 'u' \$\endgroup\$ – Caridorc Feb 25 '18 at 19:32
  • 1
    \$\begingroup\$ You're right, @Caridorc. I was missing trailing commas - I've edited it. Try random.choice( ('in' , ) * 3 + ('out' , ) * 4 ) instead (note the commas). \$\endgroup\$ – Austin Hastings Feb 25 '18 at 23:37
6
\$\begingroup\$

I noticed that you are repeating some of the code under animate_person_going_inside and `animate_person_going_outside functions. Also when rendering the text, some of the code is repeated.

If you take out that part of the code into a function, it will reduce repeatation.

import pygame
import random

pygame.init()
screen = pygame.display.set_mode((800, 640))
person = pygame.image.load('person.png')
house = pygame.image.load('house.png')
clock = pygame.time.Clock()
pygame.display.set_caption('People counting game')

myfont = pygame.font.SysFont('comicsansms', 80)
smallfont = pygame.font.SysFont('comicsansms', 30)

ingoing_persons = 5
outgoing_persons = 3


def animate_person_moving(starting_position, final_position, speed):
    running = True
    x = starting_position
    while running:
        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            running = False

        x += speed
        screen.fill((255, 255, 255))  # fill the screen
        screen.blit(person, (int(x), 340))
        screen.blit(house, (200, 260))
        if x > final_position:
            return
        pygame.display.update()  # Just do one thing, update/flip.

        clock.tick(40)  # This call will regulate your FPS (to be 40 or less)


def animate_all_people(ingoing, outgoing, difficulty):
    result = ingoing - outgoing
    ingoing_so_far = 0
    outgoing_so_far = 0
    people_inside = 0
    while True:
        if random.choice((0, 1)):
            if ingoing_so_far < ingoing:
                people_inside += 1
                animate_person_moving(-200, 300, difficulty)
                ingoing_so_far += 1
        else:
            if outgoing_so_far < outgoing and people_inside > 0:  # People can only exit if people are inside!
                people_inside -= 1
                animate_person_moving(300, 900, difficulty)
                outgoing_so_far += 1
        if ingoing_so_far == ingoing and outgoing_so_far == outgoing:
            break

    running = True
    while running:
        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            running = False
        if event.type == pygame.KEYDOWN:
            if (event.key == pygame.K_0 and people_inside == 0) or \
            (event.key == pygame.K_1 and people_inside == 1) or \
            (event.key == pygame.K_2 and people_inside == 2) or \
            (event.key == pygame.K_3 and people_inside == 3) or \
            (event.key == pygame.K_4 and people_inside == 4) or \
            (event.key == pygame.K_5 and people_inside == 5) or \
            (event.key == pygame.K_6 and people_inside == 6) or \
            (event.key == pygame.K_7 and people_inside == 7) or \
            (event.key == pygame.K_8 and people_inside == 8) or \
            (event.key == pygame.K_9 and people_inside == 9):
                for _ in range(40 * 2):
                    text_render(
                        myfont, 'Correct! {}!'.format(
                            str(result)), 320, 240)
                    pygame.display.flip()
                return 1

            else:
                for _ in range(40 * 2):
                    text_render(
                        myfont, 'Wrong!, It was {}'.format(
                            str(result)), 320, 240)
                    pygame.display.flip()
                return 0


def random_if_condition(minmax, condition):
    while True:
        r = random.randint(*minmax)
        if condition(r):
            return r


def text_render(font_type, text_to_be_printed, max_width, max_height):
    text = font_type.render(text_to_be_printed, False, (255, 0, 0))
    screen.blit(text, (max_width - text.get_width() // 2, max_height -
                    text.get_height() // 2))


def play_match(rounds, speed, max_people):
    while True:
        text_render(smallfont, "Count the people inside the house.", 380, 140)
        text_render(smallfont, "When no more people are moving", 380, 240)
        text_render(smallfont, "press the number on the keyboard.", 380, 340)
        text_render(smallfont, "Press any key to start playing.", 380, 440)

        event = pygame.event.poll()
        if event.type == pygame.QUIT:
            return
        if event.type == pygame.KEYUP or event.type == pygame.KEYDOWN:
            break
        pygame.display.flip()

    points = 0
    for _ in range(rounds):
        # Careful to avoid more outgoing than ingoing
        ingoing = random.randint(0, max_people)
        points += animate_all_people(ingoing,
                                    random_if_condition(
                                        (0, max_people),
                                        lambda r: r <= ingoing),
                                    speed)
    for _ in range(40 * 5):  # 5 seconds
        text_render(myfont, "You got {}/{} right".format(points, rounds), 320,
                    140)
        pygame.display.flip()

if __name__ == "__main__":
    play_match(rounds=3, speed=15, max_people=6)
\$\endgroup\$

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.