Aplicativo de Tabuada em Python com POO: Aprenda a Criar o Seu!

Neste artigo, vamos desenvolver um aplicativo de tabuada, explorando a versatilidade da Programação Orientada a Objetos (POO) em Python.

Você terá a oportunidade de praticar os conceitos fundamentais da POO e como aplicá-los na criação de um aplicativo plenamente funcional.

Python é uma linguagem de programação que suporta múltiplos paradigmas, o que significa que nós, como desenvolvedores, podemos selecionar a abordagem mais adequada para cada situação e desafio. No contexto da Programação Orientada a Objetos, estamos falando de um dos paradigmas mais empregados na construção de aplicações escaláveis nas últimas décadas.

Fundamentos da POO

Vamos analisar rapidamente o conceito mais importante da POO em Python: as classes.

Uma classe serve como um modelo que define a estrutura e o comportamento de objetos. Este modelo nos permite criar Instâncias, que são essencialmente objetos individuais formados a partir da composição da classe.

Uma classe simples de livro, com atributos como título e cor, seria definida da seguinte maneira:

class Book:
      def __init__(self, title, color):
          self.title = title
          self.color = color

Para criar instâncias da classe `Book`, é necessário chamar a classe e fornecer argumentos para ela:

# Criação de instâncias da classe Book
blue_book = Book("A criança azul", "Azul")
green_book = Book("A história do sapo", "Verde")

Uma representação visual do nosso programa neste ponto seria:

O interessante é que, ao verificarmos o tipo das instâncias `blue_book` e `green_book`, o resultado é “Book”.

# Exibindo o tipo dos livros

print(type(blue_book))
# <class '__main__.Book'>
print(type(green_book))
# <class '__main__.Book'>

Com esses conceitos bem claros, podemos iniciar a construção do nosso projeto 😃.

Definição do Projeto

No nosso trabalho como desenvolvedores, a maior parte do tempo não é dedicada à escrita de código. Segundo o thenewstack, apenas um terço do nosso tempo é gasto escrevendo ou refinando código.

Os outros dois terços são dedicados à análise do código de outros e à compreensão do problema que estamos enfrentando.

Para este projeto, vou apresentar a definição do problema e, em seguida, analisaremos como desenvolver nosso aplicativo a partir dela. Com isso, percorreremos todo o processo, desde a concepção da solução até a sua implementação com código.

Um professor do ensino fundamental deseja um jogo para avaliar as habilidades de multiplicação de alunos com idades entre 8 e 10 anos.

O jogo deve incluir um sistema de vidas e pontos, onde o aluno começa com 3 vidas e precisa atingir uma pontuação específica para vencer. O programa também deve exibir uma mensagem de “derrota” se o aluno perder todas as vidas.

O jogo terá dois modos: multiplicações aleatórias e multiplicações de tabuada.

O primeiro modo apresentará ao aluno uma multiplicação aleatória de 1 a 10, e ele deverá responder corretamente para ganhar um ponto. Caso erre, o aluno perderá uma vida, e o jogo continua. O aluno vence ao atingir 5 pontos.

O segundo modo exibirá uma tabuada de 1 a 10, e o aluno deverá inserir o resultado correto de cada multiplicação. Se o aluno errar três vezes, ele perde, mas se completar duas tabelas, o jogo termina.

Sei que os requisitos podem parecer um pouco extensos, mas prometo que vamos abordá-los neste artigo 😁.

Dividir para Conquistar

A habilidade mais importante na programação é a capacidade de resolver problemas. Isso ocorre porque é crucial ter um plano antes de iniciar a escrita do código.

Sempre sugiro dividir um problema complexo em problemas menores, que podem ser resolvidos de forma mais simples e eficaz.

Então, se você precisa criar um jogo, comece dividindo-o em suas partes fundamentais. Esses subproblemas serão muito mais fáceis de abordar.

Somente assim será possível ter clareza sobre como executar e integrar tudo com o código.

Vamos então criar um diagrama de como será o jogo:

Este diagrama ilustra as relações entre os objetos do nosso aplicativo. Como podemos ver, os dois objetos principais são Multiplicação Aleatória e Multiplicação de Tabuada. O que eles compartilham são os atributos Pontos e Vidas.

Com todas essas informações em mente, vamos começar a codificar.

Criação da Classe Pai do Jogo

Ao trabalharmos com programação orientada a objetos, procuramos a forma mais limpa de evitar repetição de código. Isso é conhecido como DRY (Don’t Repeat Yourself – Não Se Repita).

Observação: Esse princípio não se refere a escrever menos linhas de código (a qualidade do código não deve ser medida por esse critério), mas sim a abstrair a lógica mais utilizada.

De acordo com a ideia anterior, a classe pai do nosso aplicativo deve definir a estrutura e o comportamento desejados para as outras duas classes.

Vejamos como isso seria feito.

class BaseGame:

      # Comprimento para centralizar a mensagem
      message_lenght = 60
      
      description = ""    
          
      def __init__(self, points_to_win, n_lives=3):
          """Classe base do jogo

          Args:
              points_to_win (int): Os pontos necessários para concluir o jogo
              n_lives (int): O número de vidas que o aluno possui. O valor padrão é 3.
          """
          self.points_to_win = points_to_win

          self.points = 0
          
          self.lives = n_lives

      def get_numeric_input(self, message=""):

          while True:
              # Obtém a entrada do usuário
              user_input = input(message) 
              
              # Se a entrada for numérica, retorna-a
              # Caso contrário, imprime uma mensagem e repete
              if user_input.isnumeric():
                  return int(user_input)
              else:
                  print("A entrada precisa ser um número")
                  continue     
               
      def print_welcome_message(self):
          print("JOGO DE MULTIPLICAÇÃO PYTHON".center(self.message_lenght))

      def print_lose_message(self):
          print("VOCÊ PERDEU TODAS AS SUAS VIDAS".center(self.message_lenght))

      def print_win_message(self):
          print(f"PARABÉNS, VOCÊ ALCANÇOU {self.points}".center(self.message_lenght))
          
      def print_current_lives(self):
          print(f"Atualmente você tem {self.lives} vidas\n")

      def print_current_score(self):
          print(f"\nSua pontuação é {self.points}")

      def print_description(self):
          print("\n\n" + self.description.center(self.message_lenght) + "\n")

      # Método básico de execução
      def run(self):
          self.print_welcome_message()
          
          self.print_description()

Essa classe parece um tanto grande. Deixe-me detalhar cada parte.

Primeiramente, vamos entender os atributos da classe e o construtor.

Basicamente, atributos de classe são variáveis criadas dentro da classe, mas fora do construtor ou de qualquer método.

Já os atributos de instância são variáveis criadas apenas dentro do construtor.

A principal diferença entre eles é o escopo. Ou seja, atributos de classe são acessíveis tanto por objetos de instância quanto pela classe. Por outro lado, atributos de instância são acessíveis apenas a partir de objetos de instância.

game = BaseGame(5)

  # Acessando o atributo de classe message_lenght a partir da instância
  print(game.message_lenght) # 60

  # Acessando o atributo de classe message_lenght diretamente da classe
  print(BaseGame.message_lenght)  # 60

  # Acessando o atributo de instância points a partir da instância
  print(game.points) # 0

  # Tentando acessar o atributo de instância points a partir da classe
  print(BaseGame.points) # Erro de atributo
  

Um outro artigo pode aprofundar mais este tópico. Fique atento para lê-lo.

A função `get_numeric_input` é usada para garantir que o usuário forneça apenas entradas numéricas. Como você pode notar, este método foi projetado para perguntar ao usuário até que ele forneça uma entrada numérica. Vamos usá-lo mais tarde nas classes filhas.

Os métodos de impressão evitam a necessidade de repetir o mesmo código toda vez que um evento ocorre no jogo.

Por último, mas não menos importante, o método `run` é apenas um invólucro que as classes Multiplicação Aleatória e Multiplicação de Tabuada usarão para interagir com o usuário e tornar tudo funcional.

Criação das Classes Filhas

Após criarmos a classe pai, que define a estrutura e algumas funcionalidades do nosso aplicativo, é hora de criar as classes concretas dos modos de jogo, utilizando o poder da herança.

Classe de Multiplicação Aleatória

Esta classe executará o “primeiro modo” do nosso jogo. Ela usará o módulo `random`, que nos permite solicitar ao usuário operações aleatórias de 1 a 10. Aqui está um ótimo artigo sobre o módulo `random` (e outros módulos importantes) 😉.

import random # Módulo para operações aleatórias
class RandomMultiplication(BaseGame):

      description = "Neste jogo, você deve responder corretamente as multiplicações aleatórias.\nVocê ganha ao atingir 5 pontos, ou perde se perder todas as vidas"

      def __init__(self):
          # O número de pontos necessários para vencer é 5
          # Passa 5 como argumento "points_to_win"
          super().__init__(5)

      def get_random_numbers(self):

          first_number = random.randint(1, 10)
          second_number = random.randint(1, 10)

          return first_number, second_number
          
      def run(self):
          
          # Chama a classe superior para imprimir as mensagens de boas-vindas
          super().run()
          

          while self.lives > 0 and self.points_to_win > self.points:
              # Obtém dois números aleatórios
              number1, number2 = self.get_random_numbers()

              operation = f"{number1} x {number2}: "

              # Solicita que o usuário responda à operação
              # Previne erros de valor
              user_answer = self.get_numeric_input(message=operation)

              if user_answer == number1 * number2:
                  print("\nSua resposta está correta\n")
                  
                  # Adiciona um ponto
                  self.points += 1
              else:
                  print("\nSua resposta está incorreta\n")

                  # Subtrai uma vida
                  self.lives -= 1
              
              self.print_current_score()
              self.print_current_lives()
              
          # Só é executado quando o jogo termina
          # E nenhuma das condições é verdadeira
          else:
              # Imprime a mensagem final
              
              if self.points >= self.points_to_win:
                  self.print_win_message()
              else:
                  self.print_lose_message()
  

Esta é outra classe extensa 😅. Mas, como mencionei antes, o que importa não é o número de linhas, mas sim a sua clareza e eficiência. E o melhor do Python é que ele permite aos desenvolvedores criar um código limpo e legível como se estivessem falando em português comum.

Esta classe tem algo que pode confundi-lo, mas vou explicar da forma mais simples possível.

      # Classe pai
      def __init__(self, points_to_win, n_lives=3):
          "...
      # Classe filha
      def __init__(self):
          # O número de pontos necessários para ganhar é 5
          # Passa 5 como argumento "points_to_win"
          super().__init__(5)

O construtor da classe filha está chamando a função `super`, que por sua vez se refere à classe pai (BaseGame). É basicamente dizer ao Python:

Preencha o atributo “points_to_win” da classe pai com o valor 5!

Não é necessário usar o `self` dentro da parte `super().__init__()` pois já estamos chamando `super` dentro do construtor, o que resultaria em redundância.

Também estamos usando a função `super` no método `run`, e vamos ver o que acontece nesse trecho de código.

      # Método básico de execução
      # Método da classe pai
      def run(self):
          self.print_welcome_message()
          
          self.print_description()
      def run(self):
          
          # Chama a classe superior para imprimir as mensagens de boas-vindas
          super().run()
          
          .....

Como você pode observar, o método `run` na classe pai imprime a mensagem de boas-vindas e a descrição. Mas é uma boa ideia manter essa funcionalidade e também adicionar outros recursos extras nas classes filhas. Para isso, usamos `super` para executar todo o código do método pai antes de executar a próxima parte.

O restante da função de execução é bem direta. Ela solicita um número ao usuário com a mensagem da operação que deve ser respondida. Em seguida, o resultado é comparado com a multiplicação real e, se forem iguais, um ponto é adicionado; caso contrário, uma vida é retirada.

Vale ressaltar que estamos usando loops `while-else`. Isso foge do escopo deste artigo, mas publicarei um sobre isso em breve.

Finalmente, `get_random_numbers` usa a função `random.randint`, que retorna um inteiro aleatório dentro do intervalo especificado. Em seguida, ele retorna uma tupla de dois inteiros aleatórios.

Classe de Multiplicação de Tabuada

O “segundo modo” deve exibir o jogo no formato de tabuada e garantir que o usuário acerte pelo menos duas tabuadas.

Para isso, usaremos novamente o poder de `super` e modificaremos o atributo `points_to_win` da classe pai para 2.

class TableMultiplication(BaseGame):

      description = "Neste jogo, você deve resolver corretamente a tabuada completa.\nVocê ganha se resolver 2 tabelas"
      
      def __init__(self):
          # Precisa completar 2 tabelas para vencer
          super().__init__(2)

      def run(self):

          # Imprime as mensagens de boas-vindas
          super().run()

          while self.lives > 0 and self.points_to_win > self.points:
              # Obtém um número aleatório
              number = random.randint(1, 10)            

              for i in range(1, 11):
                  
                  if self.lives <= 0:
                      # Garante que o jogo não continue
                      # Se o usuário perder todas as vidas
                      self.points = 0
                      break 
                  
                  operation = f"{number} x {i}: "

                  user_answer = self.get_numeric_input(message=operation)

                  if user_answer == number * i:
                      print("Ótimo! Sua resposta está correta")
                  else:
                      print("Sua resposta está incorreta") 

                      self.lives -= 1

              self.points += 1
              
          # Só é executado quando o jogo termina
          # E nenhuma das condições são verdadeiras
          else:
              # Imprime a mensagem final
              
              if self.points >= self.points_to_win:
                  self.print_win_message()
              else:
                  self.print_lose_message()
  

Como você pode notar, estamos apenas modificando o método `run` desta classe. Essa é a mágica da herança: escrevemos uma vez a lógica que usamos em vários lugares e depois podemos esquecer 😅.

No método `run`, estamos usando um loop `for` para obter os números de 1 a 10 e construir a operação que é mostrada ao usuário.

Mais uma vez, se as vidas forem esgotadas ou os pontos necessários para vencer forem atingidos, o loop `while` é interrompido e a mensagem de vitória ou derrota é exibida.

SIM, criamos os dois modos do jogo, mas até agora, se executarmos o programa, nada acontecerá.

Então, vamos finalizar o programa implementando a escolha do modo e instanciando as classes dependendo dessa escolha.

Implementação da Escolha do Modo

O usuário poderá escolher qual modo quer jogar. Então, vamos ver como implementar essa funcionalidade.

if __name__ == "__main__":

      print("Selecione o modo de jogo")

      choice = input("[1],[2]: ")

      if choice == "1":
          game = RandomMultiplication()
      elif choice == "2":
          game = TableMultiplication()
      else:
          print("Por favor, selecione um modo de jogo válido")
          exit()

      game.run()
  

Primeiro, solicitamos que o usuário escolha entre os modos 1 ou 2. Se a entrada não for válida, o script é interrompido. Se o usuário selecionar o primeiro modo, o programa executará o modo de jogo Multiplicação Aleatória e, se selecionar o segundo, o modo Tabela de Multiplicação será executado.

Eis como seria:

Conclusão

Parabéns, você acabou de construir um aplicativo Python com Programação Orientada a Objetos.

Todo o código está disponível no Repositório Github.

Neste artigo, você aprendeu a:

  • Utilizar construtores de classe em Python
  • Criar um aplicativo funcional com POO
  • Utilizar a função `super` em classes Python
  • Aplicar os conceitos básicos de herança
  • Implementar atributos de classe e de instância

Bons estudos e muita codificação 👨‍💻

Em seguida, explore alguns dos melhores IDEs Python para alcançar uma maior produtividade.