Why is such a simple game getting so heavy?

This is the first project I develop using Pygame. Because it is a simple card game I expected it to be very light but not really. As I am using a very weak computer (single core 1GB ram) it is easy to notice this, here it is running below 20 fps. Will it be problem in my code or is it pygame that is a bit heavy even?

This is my main class code that makes everything work:

import os
import random
import pygame
from button import Button
from card import Card


class Truco(object):

    def __init__(self):
        super(Truco, self).__init__()
        os.environ["SDL_VIDEO_CENTERED"] = "1"
        pygame.init()
        self.load_fonts()
        pygame.display.set_caption("Truco")
        self.screen = pygame.display.set_mode([800, 600])
        self.screen_rect = self.screen.get_rect()
        self.clock = pygame.time.Clock()
        self.play()

    def load_fonts(self):
        self.fonts = {}
        for file in os.listdir("fonts"):
            for size in [18, 24, 40, 80]:
                self.fonts["{}_{}".format(file[0:-4], size)] =\
                    pygame.font.Font(os.path.join("fonts", file), size)

    def play(self):
        self.cards_list = []
        for value in "4567qjka23":
            for suit in "diamonds spades hearts clubs".split():
                self.cards_list.append(Card(suit, value))
        self.human_score = 0
        self.robot_score = 0
        self.background = pygame.image.load("images/background.png")
        self.background_rect = self.background.get_rect()
        self.table = pygame.image.load("images/table.png")
        self.table_rect = self.table.get_rect(center=self.screen_rect.center)
        self.button_correr = Button(
            "images/button_2.png", self.fonts["titan_one_24"],
            "Correr", [255, 255, 255],
            bottomright=self.screen_rect.bottomright)
        self.button_truco = Button(
            "images/button_1.png", self.fonts["titan_one_24"], "Truco",
            [0, 0, 0], midbottom=self.button_correr.rect.midtop)
        self.card_shuffle = pygame.mixer.Sound("sounds/card_shuffle.ogg")
        self.shuffle()

    def shuffle(self):
        self.card_shuffle.play()
        random.shuffle(self.cards_list)
        self.cards_group = pygame.sprite.Group()
        for x in range(0, 40):
            self.cards_list[x].rotoflip(0, "back")
            self.cards_list[x].rect.center = [self.screen_rect.centerx - x / 2,
                                              self.screen_rect.centery - x / 2]
            self.cards_group.add(self.cards_list[x])
        self.cards_human = []
        self.cards_robot = []
        for x in range(0, 6, 2):
            self.cards_human.append(self.cards_list[x])
            self.cards_robot.append(self.cards_list[x + 1])
        self.card_vira = self.cards_list[6]
        self.index = 0
        pygame.time.set_timer(pygame.USEREVENT, 750)

    def give_cards(self, x):
        if x < 3:
            self.cards_robot[x].rect.midtop = [
                self.screen_rect.centerx + (x - 1) * 70, self.screen_rect.top]
            self.cards_human[x].flip("front")
            self.cards_human[x].rect.midbottom = [
                self.screen_rect.centerx + (x - 1) * 70,
                self.screen_rect.bottom]
        else:
            self.card_vira.rotoflip(-90, "front")
            self.card_vira.rect.center = self.cards_list[7].rect.midright
            pygame.time.set_timer(pygame.USEREVENT, 0)

    def loop(self):
        self.running = True
        while self.running:
            self.event()
            self.draw()
            pygame.display.update()
            self.clock.tick(60)

    def event(self):
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
            elif event.type == pygame.USEREVENT:
                self.give_cards(self.index)
                self.index += 1
            elif event.type == pygame.MOUSEBUTTONDOWN:
                self.shuffle()
            elif event.type == pygame.MOUSEMOTION:
                mouse_pos = pygame.mouse.get_pos()
                self.button_correr.hover(mouse_pos)
                self.button_truco.hover(mouse_pos)


    def draw(self):
        score = self.fonts["libre_baskerville_40"].render(
            "{} x {}".format(self.human_score, self.robot_score),
            True, [255, 255, 255], [0, 0, 0])
        fps = self.fonts["libre_baskerville_18"].render("FPS: {}".format(
            self.clock.get_fps()), True, [0, 0, 0])
        fps_rect = fps.get_rect(bottomleft=self.screen_rect.bottomleft)
        self.screen.blit(self.background, self.background_rect)
        self.screen.blit(self.table, self.table_rect)
        self.screen.blit(score, [0, 0])
        self.screen.blit(fps, fps_rect)
        self.cards_group.draw(self.screen)
        self.button_correr.draw(self.screen)
        self.button_truco.draw(self.screen)

    def quit(self):
        pygame.quit()
Author: Mateus Cardoso Silva, 2018-05-01

1 answers

Use PyCharm profiling mode (which is nothing more than utility cProfile below the cloths) indicates that most of the time (91.3%) is spent on the function blit.

insert the description of the image here

Although it is a necessary function to draw things on the screen, perhaps we can decrease its use when it is not necessary. When analyzing the code better, I found the biggest problem: we always draw all the cards, even the ones that are under one of the others.

I switched the

self.cards_group.draw(self.screen)

By the following:

for card in self.cards_human:
    self.screen.blit(card.image, card.rect)
for card in self.cards_robot:
    self.screen.blit(card.image, card.rect)

self.screen.blit(self.card_vira.image, self.card_vira.rect)

Means: instead of drawing all the cards, I started to draw only the ones in my hand or that of the robot, and the card turned. The FPS that was at 45 went up to ~65 on my machine, after removing the 60fps limitation.

Of course this has the unwanted effect that the rest of the card cake is no longer drawn. The solution then would be to have a picture of your own cake of cards, or draw only the top card and then draw only the corner of each of the bottom cards, to avoid drawing several times the same card on top of each other.

Another optimization I did but that didn't have so much effect (maybe ~2fps) was to not load the same file to the back of the back, but instead use a global version loaded only once. I mean, in card.py, I did this:

back = pygame.image.load("images/card_back_red_1.png")
back = pygame.transform.smoothscale(back, [70, 95])

class Card(pygame.sprite.Sprite):
    (...)
    self.back = back
 1
Author: Pedro von Hertwig Batista, 2018-05-01 16:22:24