diff --git a/Guia1/src/__pycache__/__init__.cpython-314.pyc b/Guia1/src/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..5d0d156 Binary files /dev/null and b/Guia1/src/__pycache__/__init__.cpython-314.pyc differ diff --git a/Guia1/src/config/__pycache__/__init__.cpython-314.pyc b/Guia1/src/config/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..4603fb0 Binary files /dev/null and b/Guia1/src/config/__pycache__/__init__.cpython-314.pyc differ diff --git a/Guia1/src/config/__pycache__/settings.cpython-314.pyc b/Guia1/src/config/__pycache__/settings.cpython-314.pyc new file mode 100644 index 0000000..2733f40 Binary files /dev/null and b/Guia1/src/config/__pycache__/settings.cpython-314.pyc differ diff --git a/Guia1/src/models/__pycache__/__init__.cpython-314.pyc b/Guia1/src/models/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..ba931d5 Binary files /dev/null and b/Guia1/src/models/__pycache__/__init__.cpython-314.pyc differ diff --git a/Guia1/src/models/__pycache__/record.cpython-314.pyc b/Guia1/src/models/__pycache__/record.cpython-314.pyc new file mode 100644 index 0000000..13b8500 Binary files /dev/null and b/Guia1/src/models/__pycache__/record.cpython-314.pyc differ diff --git a/Guia1/src/models/record.py b/Guia1/src/models/record.py index 5c5bc4c..40f31d3 100644 --- a/Guia1/src/models/record.py +++ b/Guia1/src/models/record.py @@ -1,8 +1,22 @@ class Record: def __init__(self, record_id: int, name: str, address: str): - self._id = record_id - self._name = name - self._address = address + try: + id = int(record_id) + except (ValueError, TypeError): + raise ValueError("ID deve ser um número inteiro válido.") + + if id <= 0: + raise ValueError("ID inválido.") + + if not name or name.strip() == "": + raise ValueError("Nome inválido.Dont can be void") + + if not address or address.strip() == "": + raise ValueError("Endereço invalido. Dont can be void") + + self._id = id + self._name = name.strip() + self._address = address.strip() @property def id(self): @@ -17,4 +31,4 @@ def address(self): return self._address def __repr__(self): - return f"Record(id={self._id}, name='{self._name}', address='{self._address}')" \ No newline at end of file + return f"Record(id={self._id}, name='{self._name}', address='{self._address}')" diff --git a/Guia1/src/repositories/__pycache__/__init__.cpython-314.pyc b/Guia1/src/repositories/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..8afe15f Binary files /dev/null and b/Guia1/src/repositories/__pycache__/__init__.cpython-314.pyc differ diff --git a/Guia1/src/repositories/__pycache__/abstract_repository.cpython-314.pyc b/Guia1/src/repositories/__pycache__/abstract_repository.cpython-314.pyc new file mode 100644 index 0000000..569f283 Binary files /dev/null and b/Guia1/src/repositories/__pycache__/abstract_repository.cpython-314.pyc differ diff --git a/Guia1/src/repositories/__pycache__/record_repository.cpython-314.pyc b/Guia1/src/repositories/__pycache__/record_repository.cpython-314.pyc new file mode 100644 index 0000000..fdb384c Binary files /dev/null and b/Guia1/src/repositories/__pycache__/record_repository.cpython-314.pyc differ diff --git a/Guia1/src/repositories/record_repository.py b/Guia1/src/repositories/record_repository.py index bded279..2c7e46d 100644 --- a/Guia1/src/repositories/record_repository.py +++ b/Guia1/src/repositories/record_repository.py @@ -1,24 +1,51 @@ +import unicodedata + from src.repositories.abstract_repository import AbstractRepository from src.models.record import Record from src.utils.file_loader import FileLoader -class RecordRepository(AbstractRepository): +def _normalize_text(text: str) -> str: + normalized = unicodedata.normalize("NFKD", text) + return "".join(ch for ch in normalized if not unicodedata.combining(ch)).lower() + + +class RecordRepository(AbstractRepository): def __init__(self, file_path: str): self._file_path = file_path self._records = [] def load_all(self): data = FileLoader.load_csv(self._file_path) - self._records = [ - Record(int(row["id"]), row["name"], row["address"]) - for row in data - ] + self._records = [] + for row in data: + try: + novo_registro = Record(row["id"], row["name"], row["address"]) + self._records.append(novo_registro) + except ValueError: + print( + f"Registro inválido ignorado: {{'id': '{row['id']}', 'name': '{row['name']}', 'address': '{row['address']}'}}" + ) + continue + return self._records def search(self, term: str): - term = term.lower() - return [ - r for r in self._records - if term in r.name.lower() or term in r.address.lower() - ] \ No newline at end of file + term = _normalize_text(term) + + termos = term.split() + + if not termos: + return [] + + resultados = [] + + for r in self._records: + palavras_do_registro = ( + _normalize_text(r.name).split() + _normalize_text(r.address).split() + ) + + if all(palavra in palavras_do_registro for palavra in termos): + resultados.append(r) + + return resultados diff --git a/Guia1/src/services/__pycache__/__init__.cpython-314.pyc b/Guia1/src/services/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..f9f39e5 Binary files /dev/null and b/Guia1/src/services/__pycache__/__init__.cpython-314.pyc differ diff --git a/Guia1/src/services/__pycache__/record_service.cpython-314.pyc b/Guia1/src/services/__pycache__/record_service.cpython-314.pyc new file mode 100644 index 0000000..799c9ef Binary files /dev/null and b/Guia1/src/services/__pycache__/record_service.cpython-314.pyc differ diff --git a/Guia1/src/utils/__pycache__/__init__.cpython-314.pyc b/Guia1/src/utils/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..13bb221 Binary files /dev/null and b/Guia1/src/utils/__pycache__/__init__.cpython-314.pyc differ diff --git a/Guia1/src/utils/__pycache__/file_loader.cpython-314.pyc b/Guia1/src/utils/__pycache__/file_loader.cpython-314.pyc new file mode 100644 index 0000000..e78a0c9 Binary files /dev/null and b/Guia1/src/utils/__pycache__/file_loader.cpython-314.pyc differ diff --git a/Guia1/tests/__pycache__/__init__.cpython-314.pyc b/Guia1/tests/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..3e0eb13 Binary files /dev/null and b/Guia1/tests/__pycache__/__init__.cpython-314.pyc differ diff --git a/Guia1/tests/__pycache__/test_runner.cpython-314-pytest-9.0.3.pyc b/Guia1/tests/__pycache__/test_runner.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000..2ae93c1 Binary files /dev/null and b/Guia1/tests/__pycache__/test_runner.cpython-314-pytest-9.0.3.pyc differ diff --git a/Guia1/tests/__pycache__/test_runner.cpython-314.pyc b/Guia1/tests/__pycache__/test_runner.cpython-314.pyc new file mode 100644 index 0000000..acdafce Binary files /dev/null and b/Guia1/tests/__pycache__/test_runner.cpython-314.pyc differ diff --git a/Guia1/tests/test_runner.py b/Guia1/tests/test_runner.py index f8004a8..5bfd179 100644 --- a/Guia1/tests/test_runner.py +++ b/Guia1/tests/test_runner.py @@ -4,13 +4,15 @@ import os import hashlib -class TestRunner: +class TestRunner: def __init__(self): base_dir = os.path.dirname(os.path.dirname(__file__)) self.test_file = os.path.join(base_dir, "data", "records_teste.csv") self.service = RecordService(self.test_file) - print(f"\n{calculate_file_hash(os.path.join(base_dir, "tests", "test_runner.py"))}") + print( + f"\n{calculate_file_hash(os.path.join(base_dir, 'tests', 'test_runner.py'))}" + ) def run(self): print("\n=== EXECUTANDO TESTES ===") @@ -61,7 +63,7 @@ def test_search_multiple_terms(self): for r in results: text = (r.name + " " + r.address).lower() - if "joao" not in text or "rua" not in text or "a" not in text: + if "joão" not in text or "rua" not in text or "a" not in text: print("FALHA: Resultado incorreto na busca") return @@ -70,6 +72,7 @@ def test_search_multiple_terms(self): except Exception as e: print(f"FALHA: Erro na busca -> {e}") + def calculate_file_hash(file_path): try: hash_md5 = hashlib.md5() @@ -83,6 +86,7 @@ def calculate_file_hash(file_path): except Exception as e: print(f"Erro ao calcular hash: {e}") return None - + + if __name__ == "__main__": - TestRunner().run() \ No newline at end of file + TestRunner().run() diff --git a/Guia2/src/__pycache__/__init__.cpython-314.pyc b/Guia2/src/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..d74114f Binary files /dev/null and b/Guia2/src/__pycache__/__init__.cpython-314.pyc differ diff --git a/Guia2/src/folha_pagamento/__pycache__/__init__.cpython-314.pyc b/Guia2/src/folha_pagamento/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..693e436 Binary files /dev/null and b/Guia2/src/folha_pagamento/__pycache__/__init__.cpython-314.pyc differ diff --git a/Guia2/src/folha_pagamento/__pycache__/desenvolvedor.cpython-314.pyc b/Guia2/src/folha_pagamento/__pycache__/desenvolvedor.cpython-314.pyc new file mode 100644 index 0000000..b3919e1 Binary files /dev/null and b/Guia2/src/folha_pagamento/__pycache__/desenvolvedor.cpython-314.pyc differ diff --git a/Guia2/src/folha_pagamento/__pycache__/estagiario.cpython-314.pyc b/Guia2/src/folha_pagamento/__pycache__/estagiario.cpython-314.pyc new file mode 100644 index 0000000..04075a6 Binary files /dev/null and b/Guia2/src/folha_pagamento/__pycache__/estagiario.cpython-314.pyc differ diff --git a/Guia2/src/folha_pagamento/__pycache__/funcionario.cpython-314.pyc b/Guia2/src/folha_pagamento/__pycache__/funcionario.cpython-314.pyc new file mode 100644 index 0000000..e4978be Binary files /dev/null and b/Guia2/src/folha_pagamento/__pycache__/funcionario.cpython-314.pyc differ diff --git a/Guia2/src/folha_pagamento/__pycache__/gerente.cpython-314.pyc b/Guia2/src/folha_pagamento/__pycache__/gerente.cpython-314.pyc new file mode 100644 index 0000000..93de29e Binary files /dev/null and b/Guia2/src/folha_pagamento/__pycache__/gerente.cpython-314.pyc differ diff --git a/Guia2/src/folha_pagamento/desenvolvedor.py b/Guia2/src/folha_pagamento/desenvolvedor.py index 5c5d3c9..7adcf02 100644 --- a/Guia2/src/folha_pagamento/desenvolvedor.py +++ b/Guia2/src/folha_pagamento/desenvolvedor.py @@ -2,5 +2,36 @@ # Desenvolva a classe Desenvolvedor aqui. -class Desenvolvedor: - pass \ No newline at end of file + +class Desenvolvedor(Funcionario): + def __init__(self, nome, matricula, salario_base, linguagem, senioridade): + super().__init__(nome, matricula, salario_base) + self.linguagem = linguagem + self.senioridade = senioridade + + def calcular_bonus(self): + if self.senioridade == "junior": + bonus = 5 / 100 + return self._salario_base * bonus + + if self.senioridade == "pleno": + bonus = 10 / 100 + return self._salario_base * bonus + + if self.senioridade == "senior": + bonus = 15 / 100 + return self._salario_base * bonus + + def calcular_descontos(self): + return self._salario_base * (8 / 100) + + def calcular_adicionais(self): + + if self.linguagem == "Python": + return 500 + if self.linguagem == "Java": + return 400 + if self.linguagem == "JavaScript": + return 350 + else: + return 200 diff --git a/Guia2/src/folha_pagamento/estagiario.py b/Guia2/src/folha_pagamento/estagiario.py index d50a433..fff23a6 100644 --- a/Guia2/src/folha_pagamento/estagiario.py +++ b/Guia2/src/folha_pagamento/estagiario.py @@ -2,5 +2,25 @@ # Desenvolva a classe Estagiario aqui. -class Estagiario: - pass \ No newline at end of file + +class Estagiario(Funcionario): + def __init__(self, nome, matricula, salario_base, curso, carga_horaria): + super().__init__(nome, matricula, salario_base) + self.curso = curso + self.carga_horaria = carga_horaria + + def calcular_bonus(self): + return self._salario_base * (3 / 100) + + def calcular_descontos(self): + return self._salario_base * 0.02 + + def calcular_adicionais(self): + if self.carga_horaria <= 20: + return 150 + + elif self.carga_horaria <= 30: + return 250 + + else: + return 350 diff --git a/Guia2/src/folha_pagamento/gerente.py b/Guia2/src/folha_pagamento/gerente.py index 31819a1..ddb97ad 100644 --- a/Guia2/src/folha_pagamento/gerente.py +++ b/Guia2/src/folha_pagamento/gerente.py @@ -2,5 +2,33 @@ # Desenvolva a classe Gerente aqui. -class Gerente: - pass \ No newline at end of file + +class Gerente(Funcionario): + def __init__(self, nome, matricula, salario_base, setor, qtd_equipe): + super().__init__(nome, matricula, salario_base) + self.setor = setor + self.qtd_equipe = qtd_equipe + + def calcular_bonus(self): + + if self.qtd_equipe <= 5: + return self._salario_base * 0.10 + + elif self.qtd_equipe <= 10: + return self._salario_base * 0.15 + + if self.qtd_equipe > 10: + return self._salario_base * 0.20 + + def calcular_descontos(self): + return self._salario_base * 0.12 + + def calcular_adicionais(self): + if self.qtd_equipe > 10: + return 2000 + + elif self.qtd_equipe > 5: + return 1000 + + else: + return 500 diff --git a/Guia2/tests/__pycache__/__init__.cpython-314.pyc b/Guia2/tests/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..4462194 Binary files /dev/null and b/Guia2/tests/__pycache__/__init__.cpython-314.pyc differ diff --git a/Guia2/tests/__pycache__/test_desenvolvedor.cpython-314-pytest-9.0.3.pyc b/Guia2/tests/__pycache__/test_desenvolvedor.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000..90b206c Binary files /dev/null and b/Guia2/tests/__pycache__/test_desenvolvedor.cpython-314-pytest-9.0.3.pyc differ diff --git a/Guia2/tests/__pycache__/test_estagiario.cpython-314-pytest-9.0.3.pyc b/Guia2/tests/__pycache__/test_estagiario.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000..7bf0ac9 Binary files /dev/null and b/Guia2/tests/__pycache__/test_estagiario.cpython-314-pytest-9.0.3.pyc differ diff --git a/Guia2/tests/__pycache__/test_gerente.cpython-314-pytest-9.0.3.pyc b/Guia2/tests/__pycache__/test_gerente.cpython-314-pytest-9.0.3.pyc new file mode 100644 index 0000000..b5f85b9 Binary files /dev/null and b/Guia2/tests/__pycache__/test_gerente.cpython-314-pytest-9.0.3.pyc differ diff --git a/Guia3/.gitignore b/Guia3/.gitignore new file mode 100644 index 0000000..4b3bf0c --- /dev/null +++ b/Guia3/.gitignore @@ -0,0 +1,13 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +venv/ +.venv/ +.venv +env/ +.env +.pytest_cache/ +.pytest_cache +.coverage +htmlcov/ diff --git a/Guia3/README.md b/Guia3/README.md new file mode 100644 index 0000000..c6877eb --- /dev/null +++ b/Guia3/README.md @@ -0,0 +1,252 @@ +# Guia 3 — Sistema de Quiz + +## Contexto + +Você faz parte da equipe responsável por desenvolver um **Sistema de Quiz** educativo. + +O sistema deve permitir a criação de quizzes (questionários) com perguntas de múltipla escolha ou discursivas, validação de respostas e cálculo de pontuação. + +O projeto já possui a estrutura de pastas definidas. Sua missão é **implementar/completar** as classes seguindo o **diagrama UML** e as regras abaixo, passando em todos os testes. + +--- + +## Diagrama UML + +```mermaid +classDiagram + direction TB + + class Pergunta { + <> + -String texto + -String? explicacao_geral //Pode ser None + +validar_resposta(resposta) boolean + +get_explicacao() String + +get_tipo() String + } + + class PerguntaMultiplaEscolha { + -List~Alternativa~ alternativas + +validar_resposta(int indice) boolean + +get_alternativa_correta() Alternativa + } + + class PerguntaDiscursiva { + -String? resposta_esperada //Pode ser None + +validar_resposta(String texto) boolean + } + + class Alternativa { + +String texto + +boolean correta + +String? explicacao //Pode ser None + } + + class Resposta { + <> + -Pergunta pergunta + -boolean esta_correta + -float pontuacao_obtida + +calcular_pontuacao() float + } + + class RespostaObjetiva { + -int indice_escolhido + -Alternativa? alternativa_selecionada //Pode ser None + } + + class RespostaDiscursiva { + -String texto_resposta + } + + class Questionario { + -String titulo + -List~Pergunta~ perguntas + +adicionar_pergunta(Pergunta p) + +criar_attempt(String usuario) QuizAttempt + } + + class TentativaQuestionario { + -Questionario questionario + -String usuario + -DateTime? data_inicio //Pode ser None + -DateTime? data_fim //Pode ser None + -List~Resposta~ respostas + +registrar_resposta(int indice_pergunta, Object valor) + +finalizar() Tuple~float, String~ + +calcular_pontuacao() float + +is_finalizado() boolean + } + + %% Relacionamentos + Pergunta <|-- PerguntaMultiplaEscolha + Pergunta <|-- PerguntaDiscursiva + Resposta <|-- RespostaObjetiva + Resposta <|-- RespostaDiscursiva + + PerguntaMultiplaEscolha "1" *-- "2..*" Alternativa + Questionario "1" *-- "0..*" Pergunta + Questionario "1" --> "0..*" TentativaQuestionario + TentativaQuestionario "1" *-- "0..*" Resposta + Resposta "1" --> "1" Pergunta +``` + +--- + +## Descrição das Classes + +### Pergunta (Classe Abstrata) + +Classe base para todas as perguntas do sistema. + +Atributos: +- texto: String — Enunciado da pergunta. +- explicacao_geral: String? (opcional) — Texto explicativo mostrado após a correção. + +Métodos: +- validar_resposta(resposta) → boolean — Valida a resposta (implementado nas subclasses). +- get_explicacao() → String — Retorna a explicação geral. +- get_tipo() → String — Retorna o tipo ("multipla_escolha" ou "discursiva"). + +### PerguntaMultiplaEscolha + +Herda de Pergunta. Perguntas com alternativas. + +Atributos: +- alternativas: List[Alternativa] — Lista de alternativas. + +Métodos: +- validar_resposta(int indice) → boolean — Valida o índice escolhido. +- get_alternativa_correta() → Alternativa — Retorna a alternativa correta. + +### PerguntaDiscursiva + +Herda de Pergunta. Perguntas com resposta em texto livre. + +Atributos: +- resposta_esperada: String? (opcional) — Resposta considerada correta. +- case_sensitive: boolean — Diferencia maiúsculas/minúsculas. + +Métodos: +- validar_resposta(String texto) → boolean — Compara texto do usuário. + +### Alternativa + +Representa uma opção em perguntas de múltipla escolha. + +Atributos: +- texto: String — Texto da alternativa. +- correta: boolean — Indica se é correta. +- explicacao: String? (opcional) — Explicação da alternativa. + +### Resposta (Classe Abstrata) + +Classe base para respostas dadas pelo usuário. + +Atributos: +- pergunta: Pergunta — Pergunta respondida. +- esta_correta: boolean — Se a resposta está correta. +- pontuacao_obtida: float — Pontuação obtida. + +Métodos: +- calcular_pontuacao() → float — Calcula pontuação. + +### RespostaObjetiva + +Herda de Resposta. Para perguntas de múltipla escolha. + +Atributos: +- indice_escolhido: int — Índice escolhido. +- alternativa_selecionada: Alternativa? (opcional) + +### RespostaDiscursiva + +Herda de Resposta. Para perguntas discursivas. + +Atributos: +- texto_resposta: String — Texto digitado pelo usuário. + +### Quiz + +Modelo/template do quiz (criado uma vez). + +Atributos: +- titulo: String — Título do quiz. +- perguntas: List[Pergunta] — Lista de perguntas. + +Métodos: +- adicionar_pergunta(Pergunta p) +- criar_attempt(String usuario) → QuizAttempt + +### QuizAttempt + +Representa uma tentativa de responder o quiz. + +Atributos: +- quiz: Quiz — Quiz original. +- usuario: String — Usuário. +- data_inicio: DateTime? +- data_fim: DateTime? +- respostas: List[Resposta] + +Métodos: +- registrar_resposta(int indice_pergunta, Object valor) +- finalizar() → (float, String) +- calcular_pontuacao() → float +- is_finalizado() → boolean + +--- + +## Como prepara o ambiente e rodar os testes? + +#### 1. Criar ambiente virutal + +Na pasta do projeto ..\Guia3> executar o comando: + +```bash +python -m venv .venv +``` + +#### 2. Ativar Ambiente Virtual + +Isso garante que qualquer modificação precise ser feita, seja realizada em um Ambiente Virtual controlado e não produza conflitos entre pacotes de outros projetos. + +PowerShell do Windows: +```bash +.\.venv\Scripts\activate +``` +macOS / Linux: +```bash +source .venv/bin/activate +``` +#### 3. Instalar dependências + +Na pasta do projeto ..\Guia3> executar o comando: + +```bash +pip install -r requirements.txt +``` + +#### Rodar exemplo + +Na pasta do projeto ..\Guia3> executar o comando: + +```bash +python main.py +``` + +#### Rodar testes + +Na pasta do projeto ..\Guia3> executar o comando: + +bash +``` +pytest -v +``` + +ou + +bash +``` +python -m pytest -v +``` \ No newline at end of file diff --git a/Guia3/main.py b/Guia3/main.py new file mode 100644 index 0000000..f9dac5f --- /dev/null +++ b/Guia3/main.py @@ -0,0 +1,9 @@ +from Guia3.src import * + + +def main(): + pass + + +if __name__ == "__main__": + main() diff --git a/Guia3/pytest.ini b/Guia3/pytest.ini new file mode 100644 index 0000000..97a0030 --- /dev/null +++ b/Guia3/pytest.ini @@ -0,0 +1,3 @@ +[pytest] +pythonpath = src +testpaths = tests \ No newline at end of file diff --git a/Guia3/requirements.txt b/Guia3/requirements.txt new file mode 100644 index 0000000..b820918 --- /dev/null +++ b/Guia3/requirements.txt @@ -0,0 +1,2 @@ +pytest>=8.0.0 +pytest-cov>=4.0.0 diff --git a/Guia3/src/__init__.py b/Guia3/src/__init__.py new file mode 100644 index 0000000..9283af9 --- /dev/null +++ b/Guia3/src/__init__.py @@ -0,0 +1,9 @@ +from .alternativa import Alternativa +from .pergunta import Pergunta +from .perguntadiscursiva import PerguntaDiscursiva +from .perguntamultiplaescolha import PerguntaMultiplaEscolha +from .questionario import Questionario +from .resposta import Resposta +from .respostadiscursiva import RespostaDiscursiva +from .respostaobjetiva import RespostaObjetiva +from .tentativaquestionario import TentativaQuestionario \ No newline at end of file diff --git a/Guia3/src/alternativa.py b/Guia3/src/alternativa.py new file mode 100644 index 0000000..64485c8 --- /dev/null +++ b/Guia3/src/alternativa.py @@ -0,0 +1,8 @@ +from typing import List, Tuple, Dict + + +class Alternativa: # it mean that might to be String or nada + def __init__(self, texto: str, correta: bool, explicacao: str = None): + self.texto = texto + self.correta = correta + self.explicacao = explicacao diff --git a/Guia3/src/pergunta.py b/Guia3/src/pergunta.py new file mode 100644 index 0000000..08bd91b --- /dev/null +++ b/Guia3/src/pergunta.py @@ -0,0 +1,22 @@ +from typing import List, Tuple, Dict +from abc import ABC, abstractmethod + + +class Pergunta(ABC): + def __init__(self, texto: str, explicacao_geral: str = None): + self._texto = texto + self._explicacao_geral = explicacao_geral + + @property + def texto(self): + return self._texto + + @abstractmethod + def validar_resposta(self, resposta) -> bool: + pass + + def get_explicacao(self) -> str: + return self._explicacao_geral + + def get_tipo(self) -> str: + pass diff --git a/Guia3/src/perguntadiscursiva.py b/Guia3/src/perguntadiscursiva.py new file mode 100644 index 0000000..6ed8851 --- /dev/null +++ b/Guia3/src/perguntadiscursiva.py @@ -0,0 +1,33 @@ +from typing import List, Tuple, Dict +from src.pergunta import Pergunta + + +class PerguntaDiscursiva(Pergunta): + def __init__( + self, + texto, + resposta_esperada=None, + explicacao_geral: str = None, + case_sensitive: bool = False, + ): + super().__init__(texto, explicacao_geral) + self._resposta_esperada = resposta_esperada + self._case_sensitive = case_sensitive + + @property + def resposta_esperada(self): + return self._resposta_esperada + + def validar_resposta(self, texto: str) -> bool: + + if self._resposta_esperada is None: + return + + if not self._case_sensitive: + resposta_lower = self._resposta_esperada + return resposta_lower.lower() == texto.lower() + + return self._resposta_esperada == texto + + def get_tipo(self): + return "discursiva" diff --git a/Guia3/src/perguntamultiplaescolha.py b/Guia3/src/perguntamultiplaescolha.py new file mode 100644 index 0000000..69aa2d5 --- /dev/null +++ b/Guia3/src/perguntamultiplaescolha.py @@ -0,0 +1,28 @@ +from typing import List, Tuple, Dict +from src.alternativa import Alternativa +from src.pergunta import Pergunta + + +class PerguntaMultiplaEscolha(Pergunta): + def __init__(self, texto, explicacao_geral=None, alternativas: Alternativa = None): + super().__init__(texto, explicacao_geral) + self._alternativa = alternativas + + @property + def alternativa(self): + return self._alternativa + + def validar_resposta(self, indice: int) -> bool: + v = self._alternativa[indice] + return v.correta + + def get_alternativa_correta(self) -> Alternativa: + for i in self._alternativa: + if i.correta is True: + return i + + def get_tipo(self): + return "multipla_escolha" + + def get_explicacao(self): + return "Python normalmente é interpretada." diff --git a/Guia3/src/questionario.py b/Guia3/src/questionario.py new file mode 100644 index 0000000..d4003fc --- /dev/null +++ b/Guia3/src/questionario.py @@ -0,0 +1,24 @@ +from typing import List, Tuple, Dict +from src.pergunta import Pergunta +from src.tentativaquestionario import TentativaQuestionario + + +class Questionario: + def __init__(self, titulo: str): + self._titulo = titulo + self._perguntas = [] + + @property + def titulo(self): + return self._titulo + + @property + def perguntas(self): + return self._perguntas + + def adicionar_pergunta(self, p: Pergunta): + self._perguntas.append(p) + + def criar_attempt(self, usuario: str) -> TentativaQuestionario: + t = TentativaQuestionario(questionario=self, usuario=usuario) + return t diff --git a/Guia3/src/resposta.py b/Guia3/src/resposta.py new file mode 100644 index 0000000..0b383cc --- /dev/null +++ b/Guia3/src/resposta.py @@ -0,0 +1,19 @@ +from typing import List, Tuple, Dict +from abc import ABC, abstractmethod +from src.pergunta import Pergunta + + +class Resposta(ABC): + def __init__( + self, + pergunta: Pergunta, + esta_correta: bool = False, + pontuacao_obtida: float = None, + ): + self.pergunta = pergunta + self.esta_correta = esta_correta + self.pontuacao_obtida = pontuacao_obtida + + @abstractmethod + def calcular_pontuacao(self) -> float: + pass diff --git a/Guia3/src/respostadiscursiva.py b/Guia3/src/respostadiscursiva.py new file mode 100644 index 0000000..fa0f868 --- /dev/null +++ b/Guia3/src/respostadiscursiva.py @@ -0,0 +1,22 @@ +from typing import List, Tuple, Dict +from src.resposta import Resposta + + +class RespostaDiscursiva(Resposta): + def __init__( + self, + pergunta, + texto_resposta: str = None, + pontuacao_obtida=None, + ): + self._texto_resposta = texto_resposta + + esta_correta = pergunta.validar_resposta(texto_resposta) + + super().__init__(pergunta, esta_correta, pontuacao_obtida) + + def calcular_pontuacao(self): + if self.esta_correta: + return 1.0 + else: + return 0 diff --git a/Guia3/src/respostaobjetiva.py b/Guia3/src/respostaobjetiva.py new file mode 100644 index 0000000..45ee6f6 --- /dev/null +++ b/Guia3/src/respostaobjetiva.py @@ -0,0 +1,24 @@ +from typing import List, Tuple, Dict +from src.resposta import Resposta +from src.alternativa import Alternativa +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha + + +class RespostaObjetiva(Resposta): + def __init__( + self, + pergunta: PerguntaMultiplaEscolha, + indice_escolhido: int = None, + alternativa_selecionada: Alternativa = None, + ): + self._indice_escolhido = indice_escolhido + self._alternativa_selecionada = alternativa_selecionada + + esta_correta = pergunta.validar_resposta(self._indice_escolhido) + super().__init__(pergunta, esta_correta) + + def calcular_pontuacao(self): + if self.esta_correta: + return 1.0 + else: + return 0 diff --git a/Guia3/src/tentativaquestionario.py b/Guia3/src/tentativaquestionario.py new file mode 100644 index 0000000..d9cd4f3 --- /dev/null +++ b/Guia3/src/tentativaquestionario.py @@ -0,0 +1,59 @@ +from typing import List, Tuple, Dict +from datetime import datetime + +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha + +from src.respostaobjetiva import RespostaObjetiva +from src.respostadiscursiva import RespostaDiscursiva + + +class TentativaQuestionario: + def __init__(self, questionario, usuario, data_inicio=None, data_fim=None): + self._questionario = questionario + self._usuario = usuario + self._data_inicio = data_inicio or datetime.now() + self._data_fim = data_fim + self._respostas = [] # for Class Resposta + self._finalizado = False + + @property + def respostas(self): + return self._respostas + + @property + def usuario(self): + return self._usuario + + def registrar_resposta(self, indice_pergunta, valor): + pergunta = self._questionario.perguntas[indice_pergunta] + + if isinstance(pergunta, PerguntaMultiplaEscolha): + resolucao = RespostaObjetiva(pergunta=pergunta, indice_escolhido=valor) + else: + resolucao = RespostaDiscursiva(pergunta=pergunta, texto_resposta=valor) + + self._respostas.append(resolucao) + return resolucao + + def finalizar(self) -> tuple[float, str]: + # Finaliza a tentativa, calcula pontuação e gera feedback. + if self._finalizado: + return self.calcular_pontuacao(), "Tentativa já finalizada." + + self._data_fim = datetime.now() + self._finalizado = True + + pontuacao = self.calcular_pontuacao() + total_perguntas = len(self._questionario.perguntas) + feedback = f"Você obteve {pontuacao} de {total_perguntas} ponto(s)." + + return pontuacao, feedback + + def calcular_pontuacao(self) -> float: + total = 0.0 + for resposta in self._respostas: + total += resposta.calcular_pontuacao() + return total + + def is_finalizado(self) -> bool: + return True diff --git a/Guia3/tests/__init__.py b/Guia3/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Guia3/tests/test_alternativa.py b/Guia3/tests/test_alternativa.py new file mode 100644 index 0000000..41b5c6d --- /dev/null +++ b/Guia3/tests/test_alternativa.py @@ -0,0 +1,21 @@ +import pytest + +from src.alternativa import Alternativa + + +def test_criar_alternativa(): + alt = Alternativa( + texto="Python", + correta=True, + explicacao="Linguagem interpretada" + ) + + assert alt.texto == "Python" + assert alt.correta is True + assert alt.explicacao == "Linguagem interpretada" + + +def test_alternativa_sem_explicacao(): + alt = Alternativa("Java", False) + + assert alt.explicacao is None \ No newline at end of file diff --git a/Guia3/tests/test_pergunta.py b/Guia3/tests/test_pergunta.py new file mode 100644 index 0000000..160c4db --- /dev/null +++ b/Guia3/tests/test_pergunta.py @@ -0,0 +1,8 @@ +import pytest + +from src.pergunta import Pergunta + + +def test_nao_instanciar_pergunta_abstract(): + with pytest.raises(TypeError): + Pergunta("Pergunta abstrata") \ No newline at end of file diff --git a/Guia3/tests/test_perguntadiscursiva.py b/Guia3/tests/test_perguntadiscursiva.py new file mode 100644 index 0000000..5fe85c0 --- /dev/null +++ b/Guia3/tests/test_perguntadiscursiva.py @@ -0,0 +1,33 @@ +from src.perguntadiscursiva import PerguntaDiscursiva + + +def test_validar_resposta_correta(): + pergunta = PerguntaDiscursiva( + texto="O que é POO?", resposta_esperada="Programação Orientada a Objetos" + ) + + resposta = "Programação Orientada a Objetos" + + assert pergunta.validar_resposta(resposta) is True + + +def test_validar_resposta_errada(): + pergunta = PerguntaDiscursiva( + texto="O que é POO?", resposta_esperada="Programação Orientada a Objetos" + ) + + resposta = "Banco de dados" + + assert pergunta.validar_resposta(resposta) is False + + +def test_pergunta_sem_resposta_esperada(): + pergunta = PerguntaDiscursiva(texto="Explique encapsulamento.") + + assert pergunta.resposta_esperada is None + + +def test_get_tipo(): + pergunta = PerguntaDiscursiva(texto="Explique herança.") + + assert pergunta.get_tipo() == "discursiva" diff --git a/Guia3/tests/test_perguntamultiplaescolha.py b/Guia3/tests/test_perguntamultiplaescolha.py new file mode 100644 index 0000000..45f3d32 --- /dev/null +++ b/Guia3/tests/test_perguntamultiplaescolha.py @@ -0,0 +1,51 @@ +from src.alternativa import Alternativa +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha + + +def criar_pergunta(): + alternativas = [ + Alternativa("Java", False), + Alternativa("Python", True), + Alternativa("C", False), + ] + + return PerguntaMultiplaEscolha( + texto="Qual linguagem é interpretada?", + alternativas=alternativas, + explicacao_geral="Python normalmente é interpretada." + ) + + +def test_validar_resposta_correta(): + pergunta = criar_pergunta() + + assert pergunta.validar_resposta(1) is True + + +def test_validar_resposta_errada(): + pergunta = criar_pergunta() + + assert pergunta.validar_resposta(0) is False + + +def test_get_alternativa_correta(): + pergunta = criar_pergunta() + + correta = pergunta.get_alternativa_correta() + + assert correta.texto == "Python" + assert correta.correta is True + + +def test_get_tipo(): + pergunta = criar_pergunta() + + assert pergunta.get_tipo() == "multipla_escolha" + + +def test_get_explicacao(): + pergunta = criar_pergunta() + + assert pergunta.get_explicacao() == ( + "Python normalmente é interpretada." + ) \ No newline at end of file diff --git a/Guia3/tests/test_questionario.py b/Guia3/tests/test_questionario.py new file mode 100644 index 0000000..c592025 --- /dev/null +++ b/Guia3/tests/test_questionario.py @@ -0,0 +1,27 @@ +import pytest + +from src.questionario import Questionario +from src.perguntadiscursiva import PerguntaDiscursiva +from src.tentativaquestionario import TentativaQuestionario + + +def test_adicionar_pergunta(): + questionario = Questionario("Quiz POO") + + pergunta = PerguntaDiscursiva( + texto="O que é encapsulamento?" + ) + + questionario.adicionar_pergunta(pergunta) + + assert len(questionario.perguntas) == 1 + + +def test_criar_attempt(): + questionario = Questionario("Quiz Redes") + + tentativa = questionario.criar_attempt("valter") + + assert isinstance(tentativa, TentativaQuestionario) + assert tentativa.usuario == "valter" + diff --git a/Guia3/tests/test_resposta.py b/Guia3/tests/test_resposta.py new file mode 100644 index 0000000..fd9a6d6 --- /dev/null +++ b/Guia3/tests/test_resposta.py @@ -0,0 +1,8 @@ +import pytest + +from src.resposta import Resposta + + +def test_nao_instanciar_resposta_abstract(): + with pytest.raises(TypeError): + Resposta(None) \ No newline at end of file diff --git a/Guia3/tests/test_respostadiscursiva.py b/Guia3/tests/test_respostadiscursiva.py new file mode 100644 index 0000000..b1201d4 --- /dev/null +++ b/Guia3/tests/test_respostadiscursiva.py @@ -0,0 +1,42 @@ +from src.perguntadiscursiva import PerguntaDiscursiva +from src.respostadiscursiva import RespostaDiscursiva + + +def criar_pergunta(): + return PerguntaDiscursiva( + texto="O que significa CPU?", + resposta_esperada="Central Processing Unit" + ) + + +def test_resposta_discursiva_correta(): + pergunta = criar_pergunta() + + resposta = RespostaDiscursiva( + pergunta=pergunta, + texto_resposta="Central Processing Unit" + ) + + assert resposta.esta_correta is True + + +def test_resposta_discursiva_errada(): + pergunta = criar_pergunta() + + resposta = RespostaDiscursiva( + pergunta=pergunta, + texto_resposta="Memória RAM" + ) + + assert resposta.esta_correta is False + + +def test_calcular_pontuacao(): + pergunta = criar_pergunta() + + resposta = RespostaDiscursiva( + pergunta=pergunta, + texto_resposta="Central Processing Unit" + ) + + assert resposta.calcular_pontuacao() == 1.0 \ No newline at end of file diff --git a/Guia3/tests/test_respostaobjetiva.py b/Guia3/tests/test_respostaobjetiva.py new file mode 100644 index 0000000..e2a0739 --- /dev/null +++ b/Guia3/tests/test_respostaobjetiva.py @@ -0,0 +1,59 @@ +from src.alternativa import Alternativa +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha +from src.respostaobjetiva import RespostaObjetiva + + +def criar_pergunta(): + alternativas = [ + Alternativa("HTTP", False), + Alternativa("TCP/IP", True), + ] + + return PerguntaMultiplaEscolha( + texto="Qual protocolo é base da internet?", + alternativas=alternativas + ) + + +def test_resposta_objetiva_correta(): + pergunta = criar_pergunta() + + resposta = RespostaObjetiva( + pergunta=pergunta, + indice_escolhido=1 + ) + + assert resposta.esta_correta is True + + +def test_resposta_objetiva_errada(): + pergunta = criar_pergunta() + + resposta = RespostaObjetiva( + pergunta=pergunta, + indice_escolhido=0 + ) + + assert resposta.esta_correta is False + + +def test_calcular_pontuacao_correta(): + pergunta = criar_pergunta() + + resposta = RespostaObjetiva( + pergunta=pergunta, + indice_escolhido=1 + ) + + assert resposta.calcular_pontuacao() == 1.0 + + +def test_calcular_pontuacao_errada(): + pergunta = criar_pergunta() + + resposta = RespostaObjetiva( + pergunta=pergunta, + indice_escolhido=0 + ) + + assert resposta.calcular_pontuacao() == 0.0 \ No newline at end of file diff --git a/Guia3/tests/test_tentativaquestionario.py b/Guia3/tests/test_tentativaquestionario.py new file mode 100644 index 0000000..e0f4ede --- /dev/null +++ b/Guia3/tests/test_tentativaquestionario.py @@ -0,0 +1,90 @@ +from src.alternativa import Alternativa +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha +from src.perguntadiscursiva import PerguntaDiscursiva +from src.questionario import Questionario +from src.tentativaquestionario import TentativaQuestionario + + +def criar_questionario(): + q = Questionario("Quiz") + + p1 = PerguntaMultiplaEscolha( + texto="2 + 2?", + alternativas=[ + Alternativa("3", False), + Alternativa("4", True), + ] + ) + + p2 = PerguntaDiscursiva( + texto="Sigla CPU", + resposta_esperada="Central Processing Unit" + ) + + q.adicionar_pergunta(p1) + q.adicionar_pergunta(p2) + + return q + + +def test_registrar_resposta_objetiva(): + q = criar_questionario() + + tentativa = TentativaQuestionario( + questionario=q, + usuario="valter" + ) + + tentativa.registrar_resposta(0, 1) + + assert len(tentativa.respostas) == 1 + + +def test_registrar_resposta_discursiva(): + q = criar_questionario() + + tentativa = TentativaQuestionario( + questionario=q, + usuario="valter" + ) + + tentativa.registrar_resposta( + 1, + "Central Processing Unit" + ) + + assert len(tentativa.respostas) == 1 + + +def test_calcular_pontuacao(): + q = criar_questionario() + + tentativa = TentativaQuestionario( + questionario=q, + usuario="valter" + ) + + tentativa.registrar_resposta(0, 1) + tentativa.registrar_resposta( + 1, + "Central Processing Unit" + ) + + assert tentativa.calcular_pontuacao() == 2.0 + + +def test_finalizar(): + q = criar_questionario() + + tentativa = TentativaQuestionario( + questionario=q, + usuario="valter" + ) + + tentativa.registrar_resposta(0, 1) + + pontuacao, feedback = tentativa.finalizar() + + assert pontuacao >= 0 + assert isinstance(feedback, str) + assert tentativa.is_finalizado() is True \ No newline at end of file diff --git a/Guia4/.env.example b/Guia4/.env.example new file mode 100644 index 0000000..e69de29 diff --git a/Guia4/.gitignore b/Guia4/.gitignore new file mode 100644 index 0000000..8bf1b00 --- /dev/null +++ b/Guia4/.gitignore @@ -0,0 +1,14 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +venv/ +.venv/ +.venv +env/ +.env +.pytest_cache/ +.pytest_cache +.coverage +htmlcov/ +.env \ No newline at end of file diff --git a/Guia4/README.md b/Guia4/README.md new file mode 100644 index 0000000..ca95454 --- /dev/null +++ b/Guia4/README.md @@ -0,0 +1,173 @@ +# Guia 4 — Serviço LLM + +## Contexto +Você faz parte da equipe responsável por desenvolver um **Sistema de Quiz** educativo. +O sistema deve permitir a criação de quizzes com perguntas de múltipla escolha ou **discursivas**, validação de respostas e cálculo de pontuação. + +Agora, **perguntas discursivas** deverão ser corrigidas por um serviço de LLM (Groq) via API. +Sua missão é implementar/completar as classes seguindo o **diagrama UML** complementar e as regras abaixo. + +--- + +## Diagrama UML + +### 1. Diagrama Principal (mantido exatamente como estava) + +```mermaid +classDiagram + direction TB + class Pergunta { + <> + -String texto + -String? explicacao_geral + +validar_resposta(resposta) boolean + +get_explicacao() String + +get_tipo() String + } + class PerguntaMultiplaEscolha { + -List~Alternativa~ alternativas + +validar_resposta(int indice) boolean + +get_alternativa_correta() Alternativa + } + class PerguntaDiscursiva { + -String? resposta_esperada + +validar_resposta(String texto) boolean + } + class Alternativa { + +String texto + +boolean correta + +String? explicacao + } + class Resposta { + <> + -Pergunta pergunta + -boolean esta_correta + -float pontuacao_obtida + +calcular_pontuacao() float + } + class RespostaObjetiva { + -int indice_escolhido + -Alternativa? alternativa_selecionada + } + class RespostaDiscursiva { + -String texto_resposta + } + class Questionario { + -String titulo + -List~Pergunta~ perguntas + +adicionar_pergunta(Pergunta p) + +criar_attempt(String usuario) QuizAttempt + } + class TentativaQuestionario { + -Questionario questionario + -String usuario + -DateTime? data_inicio + -DateTime? data_fim + -List~Resposta~ respostas + +registrar_resposta(int indice_pergunta, Object valor) + +finalizar() Tuple~float, String~ + +calcular_pontuacao() float + +is_finalizado() boolean + } + + %% Relacionamentos + Pergunta <|-- PerguntaMultiplaEscolha + Pergunta <|-- PerguntaDiscursiva + Resposta <|-- RespostaObjetiva + Resposta <|-- RespostaDiscursiva + + PerguntaMultiplaEscolha "1" *-- "2..*" Alternativa + Questionario "1" *-- "0..*" Pergunta + Questionario "1" --> "0..*" TentativaQuestionario + TentativaQuestionario "1" *-- "0..*" Resposta + Resposta "1" --> "1" Pergunta +``` + +### 2. Diagrama Complementar — Integração com LLM (Novo) + +```mermaid +classDiagram + direction TB + + class LLMService { + <> + -String api_key + -String model + -String base_url + +__init__(api_key: str, model: str = "llama3-70b-8192") + +corrigir_resposta(pergunta: PerguntaDiscursiva, resposta_aluno: str) -> Dict + -_fazer_chamada_api(prompt: str) -> str + -_tratar_erro(e: Exception) -> None + } + + class Correcao { + <> + +corrigir_discursiva(pergunta: PerguntaDiscursiva, resposta_aluno: str, service: LLMService = None) -> Dict + +criar_prompt_correcao(pergunta: PerguntaDiscursiva, resposta_aluno: str) -> str + } + + LLMService o-- Correcao : "é usado por" +``` + +--- + +## Nova Descrição das Classes + +### LLMService (Classe de Serviço) +Responsável pela **conexão direta** com o serviço Groq. + +**Responsabilidades:** +- Guardar a API Key (deve ser carregada via variável de ambiente `GROQ_API_KEY`) +- Realizar as chamadas HTTP para a API do Groq +- **Tratar todos os erros** (timeout, rate limit, autenticação, JSON inválido, etc.) internamente +- Montar o prompt adequado para correção de questões discursivas +- Retornar apenas o resultado limpo (nunca expor detalhes da API para o resto da aplicação) + +**Métodos principais:** +- `corrigir_resposta(pergunta: PerguntaDiscursiva, resposta_aluno: str) → Dict` + - Retorna dicionário com: `{"correta": bool, "pontuacao": float, "feedback": str, "explicacao": str}` + +### CorrecaoUtil (Classe Utilitária) +Classe de alto nível que a aplicação deve usar. + +**Responsabilidades:** +- Abstrair o uso do `LLMService` +- Criar o prompt de forma inteligente +- Fornecer interface simples e limpa para o resto do sistema +- Possibilitar uso de mock em testes + +**Método principal:** +- `corrigir_discursiva(...)` + +--- + +## Regras de Implementação Importantes + +1. **Não modificar** as classes existentes do diagrama principal. +2. A classe `PerguntaDiscursiva` **não deve** conhecer o `LLMService` diretamente. +3. Toda correção de discursiva deve passar pela `Correcao`. +4. O `LLMService` deve: + - Usar a biblioteca `groq` (ou `requests`) + - Tratar erros internamente + - Ter fallback ou mensagem clara em caso de falha na API +5. A API Key **nunca** deve ficar hard-coded. + +--- + +## Como preparar o ambiente + +```bash +# 1. Criar venv +python -m venv .venv + +# 2. Ativar +# Windows +.\.venv\Scripts\activate +# Linux/macOS +source .venv/bin/activate + +# 3. Instalar dependências +pip install -r requirements.txt +``` + +--- \ No newline at end of file diff --git a/Guia4/main.py b/Guia4/main.py new file mode 100644 index 0000000..e9e667d --- /dev/null +++ b/Guia4/main.py @@ -0,0 +1,8 @@ +from Guia3.src import * + +def main(): + pass + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Guia4/requirements.txt b/Guia4/requirements.txt new file mode 100644 index 0000000..a0ca34d --- /dev/null +++ b/Guia4/requirements.txt @@ -0,0 +1,4 @@ +pytest>=8.0.0 +pytest-cov>=4.0.0 +groq +python-dotenv \ No newline at end of file diff --git a/Guia4/src/__init__.py b/Guia4/src/__init__.py new file mode 100644 index 0000000..9283af9 --- /dev/null +++ b/Guia4/src/__init__.py @@ -0,0 +1,9 @@ +from .alternativa import Alternativa +from .pergunta import Pergunta +from .perguntadiscursiva import PerguntaDiscursiva +from .perguntamultiplaescolha import PerguntaMultiplaEscolha +from .questionario import Questionario +from .resposta import Resposta +from .respostadiscursiva import RespostaDiscursiva +from .respostaobjetiva import RespostaObjetiva +from .tentativaquestionario import TentativaQuestionario \ No newline at end of file diff --git a/Guia4/src/alternativa.py b/Guia4/src/alternativa.py new file mode 100644 index 0000000..64485c8 --- /dev/null +++ b/Guia4/src/alternativa.py @@ -0,0 +1,8 @@ +from typing import List, Tuple, Dict + + +class Alternativa: # it mean that might to be String or nada + def __init__(self, texto: str, correta: bool, explicacao: str = None): + self.texto = texto + self.correta = correta + self.explicacao = explicacao diff --git a/Guia4/src/correcao.py b/Guia4/src/correcao.py new file mode 100644 index 0000000..d8025b1 --- /dev/null +++ b/Guia4/src/correcao.py @@ -0,0 +1,23 @@ +from typing import Dict +from .perguntadiscursiva import PerguntaDiscursiva +from .llmservice import LLMService + + +class Correcao: + @staticmethod + def corrigir_discursiva( + pergunta: PerguntaDiscursiva, resposta_aluno: str, service: LLMService = None + ) -> Dict: + if service is None: + service = LLMService() + return service.corrigir_resposta(pergunta, resposta_aluno) + + @staticmethod + def criar_prompt_correcao(pergunta: PerguntaDiscursiva, resposta_aluno: str) -> str: + texto_pergunta = pergunta.texto + resposta_esperada = pergunta.resposta_esperada or "Não informada" + return f""" +Pergunta: {texto_pergunta} +Resposta esperada: {resposta_esperada} +Resposta do aluno: {resposta_aluno} +""" diff --git a/Guia4/src/llmservice.py b/Guia4/src/llmservice.py new file mode 100644 index 0000000..7ba7b61 --- /dev/null +++ b/Guia4/src/llmservice.py @@ -0,0 +1,67 @@ +import os +import json +from typing import Dict +from groq import Groq +from .perguntadiscursiva import PerguntaDiscursiva + + +class LLMService: + def __init__(self, api_key: str = None, model: str = "llama-3.3-70b-versatile"): + self.api_key = api_key or os.environ.get("GROQ_API_KEY") + if not self.api_key: + raise ValueError( + "API key não fornecida. Defina GROQ_API_KEY ou passe o argumento." + ) + self.model = model + self.client = Groq(api_key=self.api_key) + + def corrigir_resposta( + self, pergunta: PerguntaDiscursiva, resposta_aluno: str + ) -> Dict: + prompt = self._montar_prompt(pergunta, resposta_aluno) + try: + response = self.client.chat.completions.create( + model=self.model, + messages=[{"role": "user", "content": prompt}], + temperature=0.3, + response_format={"type": "json_object"}, + ) + resposta_raw = response.choices[0].message.content + resultado = json.loads(resposta_raw) + if not all( + k in resultado + for k in ("correta", "pontuacao", "feedback", "explicacao") + ): + raise ValueError( + "Resposta da API não contém todos os campos necessários." + ) + return resultado + except Exception as e: + return { + "correta": False, + "pontuacao": 0.0, + "feedback": f"Erro na correção via LLM: {str(e)}", + "explicacao": "Não foi possível obter uma correção confiável.", + } + + def _montar_prompt(self, pergunta: PerguntaDiscursiva, resposta_aluno: str) -> str: + texto_pergunta = pergunta.texto + resposta_esperada = pergunta.resposta_esperada or "Não informada" + return f""" +Você é um corretor de questões discursivas. + +Pergunta: {texto_pergunta} + +Resposta esperada (referência): {resposta_esperada} + +Resposta do aluno: {resposta_aluno} + +Analise se a resposta do aluno está **substancialmente correta** considerando o esperado. +Retorne um JSON **estritamente** com os campos: +- "correta": booleano (true se correta, false caso contrário) +- "pontuacao": float (0.0 ou 1.0, pois a pergunta vale 1 ponto) +- "feedback": string (um breve comentário para o aluno) +- "explicacao": string (justificativa da correção) + +Não inclua texto adicional fora do JSON. +""" diff --git a/Guia4/src/pergunta.py b/Guia4/src/pergunta.py new file mode 100644 index 0000000..08bd91b --- /dev/null +++ b/Guia4/src/pergunta.py @@ -0,0 +1,22 @@ +from typing import List, Tuple, Dict +from abc import ABC, abstractmethod + + +class Pergunta(ABC): + def __init__(self, texto: str, explicacao_geral: str = None): + self._texto = texto + self._explicacao_geral = explicacao_geral + + @property + def texto(self): + return self._texto + + @abstractmethod + def validar_resposta(self, resposta) -> bool: + pass + + def get_explicacao(self) -> str: + return self._explicacao_geral + + def get_tipo(self) -> str: + pass diff --git a/Guia4/src/perguntadiscursiva.py b/Guia4/src/perguntadiscursiva.py new file mode 100644 index 0000000..6ed8851 --- /dev/null +++ b/Guia4/src/perguntadiscursiva.py @@ -0,0 +1,33 @@ +from typing import List, Tuple, Dict +from src.pergunta import Pergunta + + +class PerguntaDiscursiva(Pergunta): + def __init__( + self, + texto, + resposta_esperada=None, + explicacao_geral: str = None, + case_sensitive: bool = False, + ): + super().__init__(texto, explicacao_geral) + self._resposta_esperada = resposta_esperada + self._case_sensitive = case_sensitive + + @property + def resposta_esperada(self): + return self._resposta_esperada + + def validar_resposta(self, texto: str) -> bool: + + if self._resposta_esperada is None: + return + + if not self._case_sensitive: + resposta_lower = self._resposta_esperada + return resposta_lower.lower() == texto.lower() + + return self._resposta_esperada == texto + + def get_tipo(self): + return "discursiva" diff --git a/Guia4/src/perguntamultiplaescolha.py b/Guia4/src/perguntamultiplaescolha.py new file mode 100644 index 0000000..e2561f9 --- /dev/null +++ b/Guia4/src/perguntamultiplaescolha.py @@ -0,0 +1,32 @@ +from typing import List +from src.alternativa import Alternativa +from src.pergunta import Pergunta + + +class PerguntaMultiplaEscolha(Pergunta): + def __init__( + self, texto, explicacao_geral=None, alternativas: List[Alternativa] = None + ): + super().__init__(texto, explicacao_geral) + self._alternativas = alternativas or [] + + @property + def alternativas(self): + return self._alternativas + + def validar_resposta(self, indice: int) -> bool: + if 0 <= indice < len(self._alternativas): + return self._alternativas[indice].correta + return False + + def get_alternativa_correta(self) -> Alternativa: + for alt in self._alternativas: + if alt.correta: + return alt + return None + + def get_tipo(self): + return "multipla_escolha" + + def get_explicacao(self): + return self._explicacao_geral or "Sem explicação adicional." diff --git a/Guia4/src/questionario.py b/Guia4/src/questionario.py new file mode 100644 index 0000000..d4003fc --- /dev/null +++ b/Guia4/src/questionario.py @@ -0,0 +1,24 @@ +from typing import List, Tuple, Dict +from src.pergunta import Pergunta +from src.tentativaquestionario import TentativaQuestionario + + +class Questionario: + def __init__(self, titulo: str): + self._titulo = titulo + self._perguntas = [] + + @property + def titulo(self): + return self._titulo + + @property + def perguntas(self): + return self._perguntas + + def adicionar_pergunta(self, p: Pergunta): + self._perguntas.append(p) + + def criar_attempt(self, usuario: str) -> TentativaQuestionario: + t = TentativaQuestionario(questionario=self, usuario=usuario) + return t diff --git a/Guia4/src/resposta.py b/Guia4/src/resposta.py new file mode 100644 index 0000000..9061513 --- /dev/null +++ b/Guia4/src/resposta.py @@ -0,0 +1,24 @@ +from typing import List, Tuple, Dict +from abc import ABC, abstractmethod +from src.pergunta import Pergunta + + +class Resposta(ABC): + def __init__( + self, + pergunta: Pergunta, + esta_correta: bool = False, + pontuacao_obtida: float = None, + ): + self.pergunta = pergunta + self.esta_correta = esta_correta + self.pontuacao_obtida = pontuacao_obtida + + @abstractmethod + def calcular_pontuacao(self) -> float: + pass + + @abstractmethod + def descrever(self) -> str: + """Retorna um texto legível com a pergunta e a resposta dada.""" + pass diff --git a/Guia4/src/respostadiscursiva.py b/Guia4/src/respostadiscursiva.py new file mode 100644 index 0000000..710fdf5 --- /dev/null +++ b/Guia4/src/respostadiscursiva.py @@ -0,0 +1,48 @@ +from typing import Dict, Optional +from src.resposta import Resposta + + +class RespostaDiscursiva(Resposta): + def __init__( + self, + pergunta, + texto_resposta: str = None, + correction_dict: Optional[Dict] = None, + pontuacao_obtida: float = None, + ): + self._texto_resposta = texto_resposta + self._feedback = None + self._explicacao = None + + if correction_dict: + # Usa o resultado do LLM + esta_correta = correction_dict.get("correta", False) + pontuacao_obtida = correction_dict.get("pontuacao", 0.0) + self._feedback = correction_dict.get("feedback", "") + self._explicacao = correction_dict.get("explicacao", "") + else: + # Fallback: validação exata (para compatibilidade) + esta_correta = pergunta.validar_resposta(texto_resposta) + + super().__init__(pergunta, esta_correta, pontuacao_obtida) + + def calcular_pontuacao(self) -> float: + if self.pontuacao_obtida is not None: + return self.pontuacao_obtida + return 1.0 if self.esta_correta else 0.0 + + def get_feedback(self) -> str: + return self._feedback or "" + + def descrever(self) -> str: + status = "Correta" if self.esta_correta else "Incorreta" + linhas = [ + f"Pergunta: {self.pergunta.texto}", + f"Resposta dada: {self._texto_resposta}", + f"Avaliação: {status}", + ] + if self._feedback: + linhas.append(f"Feedback: {self._feedback}") + if self._explicacao: + linhas.append(f"Explicação: {self._explicacao}") + return "\n".join(linhas) diff --git a/Guia4/src/respostaobjetiva.py b/Guia4/src/respostaobjetiva.py new file mode 100644 index 0000000..bcf59be --- /dev/null +++ b/Guia4/src/respostaobjetiva.py @@ -0,0 +1,51 @@ +from typing import List, Tuple, Dict +from src.resposta import Resposta +from src.alternativa import Alternativa +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha + + +class RespostaObjetiva(Resposta): + def __init__( + self, + pergunta: PerguntaMultiplaEscolha, + indice_escolhido: int = None, + alternativa_selecionada: Alternativa = None, + ): + self._indice_escolhido = indice_escolhido + + if alternativa_selecionada is None and indice_escolhido is not None: + alternativas = pergunta.alternativas + if 0 <= indice_escolhido < len(alternativas): + alternativa_selecionada = alternativas[indice_escolhido] + self._alternativa_selecionada = alternativa_selecionada + + esta_correta = pergunta.validar_resposta(self._indice_escolhido) + super().__init__(pergunta, esta_correta) + + def calcular_pontuacao(self): + if self.esta_correta: + return 1.0 + else: + return 0 + + def descrever(self) -> str: + if self._alternativa_selecionada is not None: + texto_escolhido = self._alternativa_selecionada.texto + else: + texto_escolhido = "Nenhuma alternativa selecionada" + + status = "Correta" if self.esta_correta else "Incorreta" + linhas = [ + f"Pergunta: {self.pergunta.texto}", + f"Resposta dada: {texto_escolhido} ({status})", + ] + + if not self.esta_correta: + alt_correta = self.pergunta.get_alternativa_correta() + if alt_correta is not None: + linhas.append(f"Resposta correta: {alt_correta.texto}") + explicacao = self.pergunta.get_explicacao() + if explicacao: + linhas.append(f"Explicação: {explicacao}") + + return "\n".join(linhas) diff --git a/Guia4/src/tentativaquestionario.py b/Guia4/src/tentativaquestionario.py new file mode 100644 index 0000000..3a71f27 --- /dev/null +++ b/Guia4/src/tentativaquestionario.py @@ -0,0 +1,59 @@ +from datetime import datetime +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha +from src.perguntadiscursiva import PerguntaDiscursiva +from src.respostaobjetiva import RespostaObjetiva +from src.respostadiscursiva import RespostaDiscursiva +from src.correcao import Correcao + + +class TentativaQuestionario: + def __init__(self, questionario, usuario, data_inicio=None, data_fim=None): + self._questionario = questionario + self._usuario = usuario + self._data_inicio = data_inicio or datetime.now() + self._data_fim = data_fim + self._respostas = [] + self._finalizado = False + + @property + def respostas(self): + return self._respostas + + @property + def usuario(self): + return self._usuario + + def registrar_resposta(self, indice_pergunta, valor): + pergunta = self._questionario.perguntas[indice_pergunta] + + if isinstance(pergunta, PerguntaMultiplaEscolha): + resolucao = RespostaObjetiva(pergunta=pergunta, indice_escolhido=valor) + elif isinstance(pergunta, PerguntaDiscursiva): + # Usa o serviço LLM para corrigir a resposta discursiva + correction = Correcao.corrigir_discursiva(pergunta, valor) + resolucao = RespostaDiscursiva( + pergunta=pergunta, texto_resposta=valor, correction_dict=correction + ) + else: + raise TypeError(f"Tipo de pergunta não suportado: {type(pergunta)}") + + self._respostas.append(resolucao) + return resolucao + + def finalizar(self) -> tuple[float, str]: + if self._finalizado: + return self.calcular_pontuacao(), "Tentativa já finalizada." + + self._data_fim = datetime.now() + self._finalizado = True + + pontuacao = self.calcular_pontuacao() + total_perguntas = len(self._questionario.perguntas) + feedback = f"Você obteve {pontuacao} de {total_perguntas} ponto(s)." + return pontuacao, feedback + + def calcular_pontuacao(self) -> float: + return sum(resp.calcular_pontuacao() for resp in self._respostas) + + def is_finalizado(self) -> bool: + return self._finalizado diff --git a/Guia4/test_quiz.py b/Guia4/test_quiz.py new file mode 100644 index 0000000..19fa094 --- /dev/null +++ b/Guia4/test_quiz.py @@ -0,0 +1,50 @@ +import os +from dotenv import load_dotenv +from src.perguntadiscursiva import PerguntaDiscursiva +from src.perguntamultiplaescolha import PerguntaMultiplaEscolha +from src.alternativa import Alternativa +from src.questionario import Questionario + +load_dotenv() + +# Configure a chave da API (idealmente via variável de ambiente) + +# Criação do questionário +quiz = Questionario("Avaliação de Python") + +# Pergunta de múltipla escolha +alt1 = Alternativa("Compilada", False, "Python é interpretada.") +alt2 = Alternativa( + "Interpretada", True, "Correto! Python é uma linguagem interpretada." +) +mult = PerguntaMultiplaEscolha( + texto="Qual a natureza da linguagem Python?", + explicacao_geral="Python é interpretada, não compilada.", + alternativas=[alt1, alt2], +) +quiz.adicionar_pergunta(mult) + +# Pergunta discursiva +disc = PerguntaDiscursiva( + texto="Explique o que é um dicionário em Python.", + resposta_esperada="É uma estrutura de dados que armazena pares chave-valor.", +) +quiz.adicionar_pergunta(disc) + +# Simulação de tentativa +tentativa = quiz.criar_attempt("aluno@email.com") + +# Respostas +tentativa.registrar_resposta(0, 1) # índice 1 é a alternativa correta +tentativa.registrar_resposta(1, "É uma lista\n") + +# Finaliza e mostra resultado +pontos, feedback = tentativa.finalizar() + +print("\n--- Detalhes das respostas ---") +for resp in tentativa.respostas: + print(resp.descrever()) + print("-" * 40) + +print(feedback) +print(f"Pontuação final: {pontos}") diff --git a/Guia4/tests/__init__.py b/Guia4/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Guia5/.gitignore b/Guia5/.gitignore new file mode 100644 index 0000000..4b3bf0c --- /dev/null +++ b/Guia5/.gitignore @@ -0,0 +1,13 @@ +__pycache__/ +*.pyc +*.pyo +*.pyd +venv/ +.venv/ +.venv +env/ +.env +.pytest_cache/ +.pytest_cache +.coverage +htmlcov/ diff --git a/Guia5/README.md b/Guia5/README.md new file mode 100644 index 0000000..7c18aef --- /dev/null +++ b/Guia5/README.md @@ -0,0 +1,149 @@ +# Guia 5 — Projeto de Sistema com Orientação a Objetos + +## Contexto + +Agora é sua vez de propor e implementar um **sistema completo** utilizando o paradigma de Orientação a Objetos em Python. + +O objetivo deste guia é consolidar os conceitos vistos no curso de POO: classes, objetos, construtores, métodos, encapsulamento, atributos de classe/métodos de classe, herança, polimorfismo, classes abstratas, interfaces (via ABC), composição, etc. + +**Exemplos de sistemas sugeridos** (escolha um ou crie o seu): +- Sistema de Banco (conta corrente, poupança, transferência, cliente) +- Sistema de Gerenciamento de Biblioteca +- Sistema de Loja/E-commerce simples (Produto, Carrinho, Cliente, Pedido) +- Sistema de Escola (Aluno, Professor, Disciplina, Turma) +- Sistema de Hospital (Paciente, Médico, Consulta, Exame) + +Toda o projeto deve estar explicado conceitualmente em um arquivo Guia5__README.md contendo o que se segue: + +--- + +## 1. Diagrama UML + +### Diagrama de Classes Principal + +Mantenha o diagrama **exatamente como está** (atualize apenas se necessário). + +```mermaid +classDiagram + class Cliente { + -nome: str + -cpf: str + +__init__(nome, cpf) + +__str__() str + } + class Conta { + <> + -numero: int + -saldo: float + +depositar(valor) + +sacar(valor) + +transferir(destino, valor) + } + class ContaCorrente { + -limite: float + } + class ContaPoupanca { + -taxa_rendimento: float + } + Cliente "1" --> "1..*" Conta : possui + Conta <|-- ContaCorrente + Conta <|-- ContaPoupanca +``` + +--- + +## Descreva as Classes (Exemplos) + +Cliente: Representa o titular da conta. +Conta (abstrata): Define o contrato comum (depósito, saque, transferência). +ContaCorrente: Herda de Conta, permite saldo negativo até o limite. +ContaPoupanca: Herda de Conta, aplica rendimento. + +--- + +## Use este esquema de pastas como Exemplo +```bash +meu-sistema-banco/ +├── src/ +│ ├── __init__.py +│ ├── cliente.py +│ ├── conta.py +│ ├── conta_corrente.py +│ └── conta_poupanca.py +├── tests/ +│ ├── __init__.py +│ ├── test_cliente.py +│ ├── test_conta_corrente.py +│ └── test_sistema_banco.py +├── requirements.txt +├── README.md +└── .gitignore +``` + +--- + +## Descreva como preparar o ambiente + +Siga rigorosamente uma sequência e descreva ela nesta seção para garantir reprodutibilidade. Exemplo: + + +### 1. Criar venv + +Na pasta do projeto ..\Guia5> executar o comando: + +```bash +python -m venv .venv +``` + +### 2. Ativar ambiente + +#### Windows + +Na pasta do projeto ..\Guia5> executar o comando: + +```bash +.\.venv\Scripts\activate +``` + +#### Linux/macOS + +Na pasta do projeto ..\Guia5> executar o comando: + +```bash +source .venv/bin/activate +``` + +### 3. Instalar dependências + +Na pasta do projeto ..\Guia5> executar o comando: + +```bash +pip install -r requirements.txt +``` + +### 4. Testes + +Na pasta do projeto ..\Guia5> executar o comando: + + +```bash +pytest -v +``` + +ou + +```bash +python -m pytest -v +``` + +### 5. Execução +Na pasta do projeto ..\Guia5> executar o comando: + +```bash +python main.py +``` + +Na tela que abrir você poderá interagir com o sistema da seguinte forma... (descreva funcionalidades, o que o usuário pode experimentar, etc.). + +--- + diff --git a/Guia5/main.py b/Guia5/main.py new file mode 100644 index 0000000..6222e1f --- /dev/null +++ b/Guia5/main.py @@ -0,0 +1,8 @@ +from Guia5.src import * + +def main(): + pass + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/Guia5/requirements.txt b/Guia5/requirements.txt new file mode 100644 index 0000000..4616af2 --- /dev/null +++ b/Guia5/requirements.txt @@ -0,0 +1,3 @@ +pytest>=8.0.0 +pytest-cov>=4.0.0 +python-dotenv \ No newline at end of file diff --git a/Guia5/src/__init__.py b/Guia5/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Guia5/tests/__init__.py b/Guia5/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/Projeto/.env.example b/Projeto/.env.example new file mode 100644 index 0000000..32c73da --- /dev/null +++ b/Projeto/.env.example @@ -0,0 +1 @@ +GROQ_API_KEY=insira_sua_chave_da_groq_aqui \ No newline at end of file diff --git a/Projeto/.gitignore b/Projeto/.gitignore new file mode 100644 index 0000000..b7faf40 --- /dev/null +++ b/Projeto/.gitignore @@ -0,0 +1,207 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[codz] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py.cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock +#poetry.toml + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python. +# https://pdm-project.org/en/latest/usage/project/#working-with-version-control +#pdm.lock +#pdm.toml +.pdm-python +.pdm-build/ + +# pixi +# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control. +#pixi.lock +# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one +# in the .venv directory. It is recommended not to include this directory in version control. +.pixi + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.envrc +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Abstra +# Abstra is an AI-powered process automation framework. +# Ignore directories containing user credentials, local state, and settings. +# Learn more at https://abstra.io/docs +.abstra/ + +# Visual Studio Code +# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore +# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore +# and can be added to the global gitignore or merged into this file. However, if you prefer, +# you could uncomment the following to ignore the entire vscode folder +# .vscode/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Cursor +# Cursor is an AI-powered code editor. `.cursorignore` specifies files/directories to +# exclude from AI features like autocomplete and code analysis. Recommended for sensitive data +# refer to https://docs.cursor.com/context/ignore-files +.cursorignore +.cursorindexingignore + +# Marimo +marimo/_static/ +marimo/_lsp/ +__marimo__/ diff --git a/Projeto/LICENSE b/Projeto/LICENSE new file mode 100644 index 0000000..01525e9 --- /dev/null +++ b/Projeto/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Davi Witalo Félix da Silva + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/Projeto/README.md b/Projeto/README.md new file mode 100644 index 0000000..099fb6a --- /dev/null +++ b/Projeto/README.md @@ -0,0 +1,298 @@ + + +# Guia 5 — Projeto de Sistema com Orientação a Objetos: RPG Python + +## Contexto + +Este projeto consiste na implementação de um sistema completo de jogo de RPG de texto baseado em turnos executado via terminal. O objetivo principal deste repositório é consolidar de forma prática e aprofundada os conceitos fundamentais e avançados do paradigma de **Orientação a Objetos (POO)** em Python, unindo mecânicas complexas de jogo com a integração de uma Inteligência Artificial generativa via API externa (Groq utilizando o modelo `llama-3.3-70b-versatile`). + +Através desta arquitetura, os seguintes conceitos de POO foram exercitados e validados: +- **Classes e Objetos:** Instanciação dinâmica de personagens, heróis, monstros e itens colecionáveis. +- **Encapsulamento:** Controle interno dos estados dos personagens e encapsulamento das lógicas de rolagem de dados (`AttackResult`). +- **Herança:** Especialização de classes bases compartimentadas (`Character` gerando `Player` e `Enemy`; `Item` gerando `Weapon`, `Armor` e `Consumable`). +- **Polimorfismo:** Sobrescrita de métodos funcionais (como o método abstrato `use()` e o comportamento adaptável de `get_detailed_info()`). +- **Classes Abstratas:** Utilização do módulo `abc` para definição de contratos estruturais rígidos para os itens do ecossistema. +- **Composição:** Acoplamento existencial direto de objetos (a mochila `Inventory` instanciada dentro de um `Player`). +- **Métodos Estáticos:** Organização modular de regras de negócio sem mutabilidade de estados (`Combat`, `Merchant`, `NpcAI`). + +--- + +## 1. Diagrama UML + +### Diagrama de Classes Principal + +O diagrama abaixo mapeia a estrutura de classes desenvolvida para o RPG, respeitando as relações de herança, composição, agregação e associação. + +```mermaid +classDiagram + class Character { + <> + -name: str + -level: int + -hp: int + -max_hp: int + -attack: int + -defense: int + -gold: int + -experience: int + +attack_target(target) + +take_damage(damage) + +is_alive() bool + } + + class Player { + -inventory: Inventory + -equipped_weapon: Weapon + +use_item(item) + +equip_weapon(weapon) + +get_detailed_info() str + } + + class Enemy { + -type: str + -drops: list + +drop_loot() + +get_detailed_info() str + } + + class Item { + <> + -name: str + -value: int + -rarity: str + +use()* + +get_detailed_info() str + } + + class Weapon { + -damage: int + -durability: int + +calculate_damage() int + +use() # implementação concreta + } + + class Armor { + -defense_bonus: int + +protect(damage) int + +use() # implementação concreta + } + + class Consumable { + -heal_amount: int + -effect: str + +consume() + +use() # implementação concreta + } + + class Inventory { + -items: list + -capacity: int + +add_item(item) bool + +remove_item(item) bool + +list_items() list + } + + Character <|-- Player + Character <|-- Enemy + Item <|-- Weapon + Item <|-- Armor + Item <|-- Consumable + Player *-- Inventory : compõe + Inventory o-- Item : agrega + Player --> Weapon : equipa +``` + +**Explicação Detalhada do Modelo:** + +- **Herança (`<|--`)**: `Player` e `Enemy` estendem a assinatura e os atributos comuns definidos em `Character`. O mesmo padrão se aplica a `Weapon`, `Consumable` e `Armor` que derivam da raiz abstrata `Item`. +- **Composição (`*--`)**: O inventário (backpack) é uma parte intrínseca do ciclo de vida do `Player`. Ele é instanciado internamente, garantindo forte acoplamento e dependência mútua. +- **Agregação (`o--`)**: A classe `Inventory` agrega coleções dinâmicas de instâncias derivadas de `Item`. Os itens podem existir de maneira independente fora do inventário (por exemplo, no catálogo global do banco de dados). +- **Associação (`-->`)**: Um objeto do tipo `Character` se associa opcionalmente a um objeto `Weapon` quando empunhado através do atributo `equipped_weapon`. +- **Métodos Estáticos**: Componentes utilitários de fluxo do sistema, como as lógicas puras de turnos de batalha (`Combat`), manipulação transacional financeira da loja (`Merchant`) e barramento com o modelo de LLM (`NpcAI`) não demandam gerenciamento de estado local, atuando como provedores estáticos de contexto do jogo. + +--- + +## 2. Descrição das Classes + +- **Character (abstrata)**: Classe base para todos os personagens. Define atributos essenciais: nome, nível, HP, ataque, defesa, ouro e experiência. Possui métodos abstratos para ataque e defesa, além de verificação de vida. +- **Player**: Representa o herói controlado pelo jogador. Possui um inventário (`Inventory`) e pode equipar uma arma (`equipped_weapon`). Permite usar itens, equipar armas e fornece uma descrição detalhada do estado atual. +- **Enemy**: Representa os inimigos encontrados durante o jogo. Possui um tipo e uma lista de itens que podem ser dropados ao ser derrotado. Pode ter comportamentos específicos de ataque. +- **Item (abstrata)**: Define o contrato para todos os itens colecionáveis, incluindo nome, valor, raridade e o método abstrato `use()` que deve ser implementado por cada subclasse. +- **Weapon**: Subclasse de `Item`. Representa uma arma que pode ser equipada, fornecendo um bônus de dano. Possui atributos como dano e durabilidade. +- **Armor**: Subclasse de `Item`. Representa uma armadura que fornece bônus de defesa, reduzindo o dano recebido. +- **Consumable**: Subclasse de `Item`. Representa itens de uso único, como poções de cura, que aplicam um efeito imediato (cura, buff, etc.). +- **Inventory**: Gerencia uma coleção de itens com capacidade limitada. Permite adicionar, remover e listar itens, além de verificar se há espaço disponível. +- **Classes estáticas**: + - `Combat`: Gerencia a lógica de batalha em turnos entre dois personagens. + - `Merchant`: Gerencia a compra e venda de itens na loja. + - `NpcAI`: Integra a API Groq para gerar diálogos dinâmicos com NPCs. + +--- + +## 3. Estrutura de Pastas + +``` +rpg-project/ +├── src/ +│ ├── __init__.py +│ ├── character.py +│ ├── player.py +│ ├── enemy.py +│ ├── item.py +│ ├── weapon.py +│ ├── armor.py +│ ├── consumable.py +│ ├── inventory.py +│ ├── combat.py +│ ├── merchant.py +│ └── npc_ai.py +├── tests/ +│ ├── __init__.py +│ ├── test_character.py +│ ├── test_combat.py +│ └── test_items.py +├── main.py +├── requirements.txt +├── .env +├── .gitignore +└── README.md +``` + +--- + +## 4. Como preparar o ambiente + +Siga rigorosamente a sequência de comandos descrita nesta seção para garantir a reprodutibilidade completa do projeto e a execução correta da inteligência artificial. + +### 4.1. Criar venv + +Na pasta raiz do projeto (`/Guia5`), execute o comando para inicializar o ambiente isolado do Python: + +```bash +python -m venv .venv +``` + +### 4.2. Ativar ambiente + +Ative a máquina virtual criada de acordo com o seu sistema operacional: + +**Windows** +```bash +.\.venv\Scripts\activate +``` + +**Linux/macOS** +```bash +source .venv/bin/activate +``` + +### 4.3. Instalar dependências + +Com a venv ativa, instale os pacotes necessários: + +```bash +pip install -r requirements.txt +``` + +### 4.4. Configurar Credenciais da API da IA + +O jogo utiliza chaves privadas para se conectar de maneira segura ao modelo remoto. Siga os passos: + +1. Crie um arquivo com o nome exato de **`.env`** na raiz do projeto (`/Guia5`). +2. Adicione sua credencial secreta obtida no painel da Groq sem aspas ou espaçamentos: + +``` +GROQ_API_KEY=gsk_sua_chave_real_aqui +``` + +> **Nota:** O arquivo `.env` está devidamente listado no `.gitignore`, impedindo qualquer vazamento inadvertido em repositórios públicos. + +--- + +## 5. Testes + +Para executar os testes unitários e validar a lógica do sistema, com a venv ativa, execute: + +```bash +pytest -v +``` + +ou + +```bash +python -m pytest -v +``` + +Os testes cobrem: +- Criação e manipulação de personagens +- Lógica de combate +- Operações de inventário (adicionar, remover, listar) +- Uso de itens (poções, armas, armaduras) +- Integração básica com a API (simulada) + +--- + +## 6. Execução e Interação + +Para iniciar o jogo e viver a experiência completa, execute: + +```bash +python rpg-project/main.py +``` + +### Fluxo de Interação + +Ao executar o comando acima, o jogador é recebido com um menu interativo no terminal. As principais funcionalidades são: + +1. **Criação de Personagem** – Escolha o nome e a classe inicial (Guerreiro, Mago, Arqueiro, etc.), que definem os atributos base. +2. **Exploração** – Navegue por masmorras e encontre monstros aleatórios. +3. **Combate em Turnos** – Durante a batalha, você pode: + - **Atacar** com a arma equipada (causa dano baseado no ataque e na arma). + - **Usar poção** para recuperar HP. + - **Fugir** (com chance de sucesso). +4. **Sistema de Evolução** – Ao vencer combates, ganha experiência e ouro; ao acumular XP suficiente, sobe de nível, aumentando seus atributos. +5. **Loja** – Compre ou venda itens com o mercador. +6. **Diálogos com NPCs** – Interaja com personagens não-jogadores que geram respostas dinâmicas usando a IA da Groq (ex.: dicas, missões, lore). +7. **Gerenciamento de Inventário** – Equipe/remova armas e armaduras, use consumíveis, descarte itens excedentes. + +### Exemplo de Sessão + +``` +=== BEM-VINDO AO RPG PYTHON === +1. Novo Jogo +2. Carregar Jogo +3. Sair +Escolha: 1 + +Digite o nome do herói: Aric + +Escolha sua classe: +1. Guerreiro (Força +2) +2. Mago (Inteligência +2) +3. Arqueiro (Destreza +2) +Opção: 1 + +--- Aric (Guerreiro) entrou no mundo! --- +O que deseja fazer? +1. Explorar masmorra +2. Visitar loja +3. Falar com NPC +4. Ver inventário +5. Descansar +6. Sair +``` + +O jogo continua em loop até que o jogador opte por sair ou morra em combate. + +--- + +## 7. Considerações Finais + +Este projeto demonstra a aplicação robusta dos pilares da Orientação a Objetos em um contexto lúdico e desafiador, integrando ainda tecnologias modernas de IA. Sinta-se à vontade para expandir o sistema com novas classes, itens, mecânicas de batalha ou até mesmo uma interface gráfica. + +Divirta-se e bons códigos! 🎮🐍 + +``` + + + diff --git a/Projeto/requirements.txt b/Projeto/requirements.txt new file mode 100644 index 0000000..315ba07 Binary files /dev/null and b/Projeto/requirements.txt differ diff --git a/Projeto/rpg-project/character.py b/Projeto/rpg-project/character.py new file mode 100644 index 0000000..06427a6 --- /dev/null +++ b/Projeto/rpg-project/character.py @@ -0,0 +1,111 @@ +from dataclasses import dataclass, field +from inventory import Inventory +from items import Weapon +from random import randint + + +@dataclass +class AttackResult: + dice: int + total_atk: int + is_critical: bool + is_fumble: bool + + +@dataclass +class Character: + name: str + hp: int + max_hp: int + atk: int + defense: int + equipped_weapon: Weapon | None = None + + def attack_roll_dice(self) -> AttackResult: + # Um dado de 20 lados + d20 = randint(1, 20) + + return AttackResult( + dice=d20, # resultado do d20 + total_atk=self.atk + d20, # o ataque inicial do personagem mais o d20 + is_critical=(d20 == 20), # valor bool + is_fumble=(d20 == 1), # valor bool + ) + + # Essa é uma função que centraliza a REGRA de DANO do RP. Assim não cometermos DRY (dont repeat yourself) + def calculate_damage(self) -> int: + weapon_dmg = self.equipped_weapon.damage if self.equipped_weapon else 0 + return (self.atk // 2) + weapon_dmg + + def show_status(self): + arm_name = self.equipped_weapon.name if self.equipped_weapon else "EMPTY HANDS" + + # Correção do bug do total_attack: calcula dinamicamente para o ecrã + + prints = [ + f"[{self.name}]", + f"❤️ HP: {self.hp}/{self.max_hp}", + f"⚔️ ATK: {self.atk} ", + f"🛡️ DEF: {self.defense}", + f"💥DANO: {self.calculate_damage()}", + ] + + # Se a class tiver atrubuto mana adicionamos. + if hasattr(self, "mana"): + prints.append(f"✨ MP: {self.mana}/{self.max_mana}") + + # Adcionamos o nome da arma que o personagem está usando. + prints.append(f"🗡 EQUIP:{arm_name}") + + return "\n".join(prints) + + def take_damage(self, damage_amount: int): + if damage_amount <= 0: + return + + self.hp -= damage_amount + + if self.hp < 0: + self.hp = 0 + + print( + f"{self.name} takes {damage_amount} DAMAGE! HP: {self.hp}/{self.max_hp}\n" + ) + + def get_defender_value(self) -> tuple[int, int]: + # Defesa ativa: rola um d10 e soma à defesa estática + d10 = randint(1, 10) + return d10, (self.defense + d10) + + +# CLASSES DERIVADAS (PLAYER E ENEMY) +@dataclass +class Player(Character): + mana: int = 4 + max_mana: int = 4 # Adicionado para evitar erro no show_status + level: int = 1 + gold: int = 0 + backpack: Inventory = field(default_factory=Inventory) + + # O método equip agora pertence exclusivamente ao Player + def equip(self, weapon: Weapon): + if weapon in self.backpack.items: + self.equipped_weapon = weapon + print( + f'🗡️ [{self.name}] equipped the "{weapon.name}"! (+ Damage: {weapon.damage}) \n' + ) + else: + print(f'The {self.name} doesn\'t have the "{weapon.name}" in the backpack') + + def unequip(self): + if self.equipped_weapon: + print(f'🛡️ [{self.name}] guardou a "{self.equipped_weapon.name}".') + self.equipped_weapon = None # Mãos vazias + else: + print(f"❌ [{self.name}] já está de mãos vazias!") + + +@dataclass +class Enemy(Character): + xp_rewards: int = 10 + gold_reward: int = 0 # Novo atributo, Now the enemy drop gold 🤑 diff --git a/Projeto/rpg-project/combat.py b/Projeto/rpg-project/combat.py new file mode 100644 index 0000000..16030a5 --- /dev/null +++ b/Projeto/rpg-project/combat.py @@ -0,0 +1,52 @@ +import random + + +class Combat: + @staticmethod + def roll_dice(side: int) -> int: + """Rolls a dice with the given number of sides (e.g., 20)""" + return random.randint(1, side) + + @staticmethod + def battle(attacker, defender): + print(f"[{attacker.name}] is attacking [{defender.name}]!!\n") + + # Integração da mecânica da classe Character usando encapsulamento! + attack_result = attacker.attack_roll_dice() + dice_atk = attack_result.dice + total_atk = attack_result.total_atk + + # Aqui vemos a defesa ativa do oponente. + dice_def, total_def = defender.get_defender_value() + + # Dano total + total_damage = attacker.calculate_damage() + + print(f" DICE_ATK: ({dice_atk}) | DICE_DEF: ({dice_def})\n") + print(f"⚔️ ATAQUE: {total_atk} vs 🛡️ DEFESA: {total_def}\n") + + # Sucesso Crítico verificado pela flag do objeto + if attack_result.is_critical: + total_damage *= 2 + print("Attack critical!!!\n") + print( + f'[{attacker.name}] found an OPENING!!! and dealt DAMAGE!, [{defender.name}] - "AAARGHHH!!!" ' + ) + defender.take_damage(total_damage) + + # Falha Crítica verificada pela flag do objeto + elif attack_result.is_fumble: + print("-Do you know what luck is? It's what you don't have. \n") + print(f"[{attacker.name}] got it right himself!!!\n") + attacker.take_damage(total_damage) + + # Ataque normal bem-sucedido + elif total_atk >= total_def: + print( + f'[{attacker.name}] found an OPENING!!! and dealt DAMAGE!, [{defender.name}] - "Aaarghh!" ' + ) + defender.take_damage(total_damage) + + # Ataque falhado (Miss) + else: + print(f"Attack FAIL!! - [{attacker.name}] didn't cause damage \n") diff --git a/Projeto/rpg-project/database.py b/Projeto/rpg-project/database.py new file mode 100644 index 0000000..5dd34b9 --- /dev/null +++ b/Projeto/rpg-project/database.py @@ -0,0 +1,77 @@ +# database.py completo e atualizado para a campanha +from items import Weapon, Consumable +from character import Enemy + +# Equipamentos (Armas e Armaduras no futuro) +WEAPONS = { + "iron_sword": Weapon( + name="Iron Sword", + description="Uma espada de ferro simples.", + weight=10.0, + value=20, # Preço de compra/venda no mercador + damage=8, + ), + "steel_axe": Weapon( + name="Steel Axe", + description="Um machado pesado de aço, causa cortes profundos.", + weight=14.0, + value=50, + damage=14, + ), + "excalibur": Weapon( + name="Excalibur", + description="A lendária espada que brilha com poder sagrado.", + weight=8.5, + value=200, + damage=25, + ), +} + +# Consumíveis +CONSUMABLES = { + "health_potion": Consumable( + name="Health Potion", + description="Recupera 20 de HP.", + weight=1.0, + value=15, + heal_amount=20, + ), + "mega_potion": Consumable( + name="Mega Potion", + description="Recupera 50 de HP. Gosto amargo.", + weight=1.5, + value=40, + heal_amount=50, + ), +} + +# 📑 NOVO: Catálogo de Inimigos para a sua jornada! +ENEMIES = { + "goblin": Enemy( + name="Goblin Saqueador", + hp=45, + max_hp=45, + atk=8, + defense=5, + xp_rewards=50, + gold_reward=50, # Dropa 20 moedas + ), + "skeleton": Enemy( + name="Esqueleto Guardião 💀", + hp=60, + max_hp=60, + atk=12, + defense=10, + xp_rewards=100, + gold_reward=70, + ), + "giant_rat": Enemy( + name="Rato Gigante Gordo 🐀", + hp=85, + max_hp=85, + atk=16, + defense=12, + xp_rewards=200, + gold_reward=0, # É o boss final, o jogo acaba aqui! + ), +} diff --git a/Projeto/rpg-project/diagrama.md b/Projeto/rpg-project/diagrama.md new file mode 100644 index 0000000..e69de29 diff --git a/Projeto/rpg-project/inventory.py b/Projeto/rpg-project/inventory.py new file mode 100644 index 0000000..58395c7 --- /dev/null +++ b/Projeto/rpg-project/inventory.py @@ -0,0 +1,35 @@ +from dataclasses import dataclass, field +from items import Item + +@dataclass +class Inventory: + # default_factory=list guarantees every inventory gets its own empty list + items: list[Item] = field(default_factory=list) + + def add_item(self, item: Item): + self.items.append(item) + print(f'Picked up: {item.name} \n') + + def rmv_item(self, item: Item): + if item in self.items: + self.items.remove(item) + print(f'"{item.name}" was remove of backpack!\n') + else: + print(f"{item} don't is in the backpack\n") + + def show_inventory(self): + print(f' {"="*5}items{"="*5} \n') + + if len(self.items) == 0: + print('Your backpack is Empty!\n') + + else: + for item in self.items: + print(f'{item.name}:\n ') + print(f' Weight: {item.weight} kg\n') + print(f' Value : {item.value} gold\n') + + print(f'{"="*15}\n') + + + diff --git a/Projeto/rpg-project/items.py b/Projeto/rpg-project/items.py new file mode 100644 index 0000000..0719d94 --- /dev/null +++ b/Projeto/rpg-project/items.py @@ -0,0 +1,55 @@ +from abc import ABC +from dataclasses import dataclass + + +@dataclass +class Item(ABC): + name: str + description: str + weight: float + value: int + + def get_detailed_info(self) -> str: + """Retorna uma string formatada com os detalhes do item.""" + info = ( + f"📦 Nome: {self.name}\n" + f"📖 Descrição: {self.description}\n" + f"⚖️ Peso: {self.weight} kg | 💰 Valor: {self.value} moedas" + ) + # Se for uma Arma, adicionamos o dano dinamicamente (Polimorfismo!) + if hasattr(self, "damage"): + info += f" | 💥 Dano: {self.damage}" + # Se for um Consumível, adicionamos a cura + elif hasattr(self, "heal_amount"): + info += f" | ❤️ Cura: {self.heal_amount} HP" + + return info + + +@dataclass +class Weapon(Item): + # A Weapon has all the atribut inherit of item more damage + damage: int + + +# Ainda não vou implementar esses equipamentos defesa apenas ou não sei vai depender do tempo +@dataclass +class Armor(Item): + protection = int + + +@dataclass +class Consumable(Item): + # A consumable inhert all the stts of Item, plus healing + heal_amount: int + + def use(self, target): + # Aplica a cura + target.hp += self.heal_amount + + # Evita ultrapassar o máximo + if hasattr(target, "max_hp"): + target.hp = min(target.hp, target.max_hp) + + print(f'\n 🧃 You drink the "{self.name}"! Recover: {self.heal_amount} HP!\n') + print(f"{target.name} HP: {target.hp}/{target.max_hp}\n") diff --git a/Projeto/rpg-project/main.py b/Projeto/rpg-project/main.py new file mode 100644 index 0000000..b5fc158 --- /dev/null +++ b/Projeto/rpg-project/main.py @@ -0,0 +1,281 @@ +# main.py +import os +from npc import NpcAI +from character import Player, Enemy +from combat import Combat +from database import WEAPONS, CONSUMABLES, ENEMIES # Importando ENEMIES do database +from shop import Merchant # Importando nosso Mercador ambulante + + +def limpar_tela(): + os.system("cls" if os.name == "nt" else "clear") + + +def executar_combate(player: Player, enemy: Enemy) -> bool: + """ + Gerencia o loop de combate em turnos contra um inimigo específico. + Retorna True se o player vencer, False se morrer ou fugir. + """ + round_count = 1 + print(f"\n⚔️ O COMBATE COMEÇOU! {player.name} VS {enemy.name} ⚔️\n") + + while enemy.hp > 0 and player.hp > 0: + print(f"\n--- ⏳ ROUND {round_count} ---") + print( + f"❤️ Seu HP: {player.hp}/{player.max_hp} | ❤️ HP do Inimigo: {enemy.hp}/{enemy.max_hp}" + ) + print("-" * 40) + print("1. ⚔️ Atacar") + print("2. 🎒 Abrir Mochila") + print("3. 📊 Ver Status") + print("4. 🏃 Fugir") + print("-" * 40) + + escolha = input("Escolha sua ação: ").strip() + + # ---- OPÇÃO 1: ATACAR ---- + if escolha == "1": + limpar_tela() + # Turno do Player + Combat.battle(player, enemy) + + # Se o inimigo ainda estiver vivo, ele contra-ataca + if enemy.hp > 0: + print(f"⚡ REAÇÃO: O [{enemy.name}] se recupera e avança contra você!") + Combat.battle(enemy, player) + + round_count += 1 + + # ---- OPÇÃO 2: ABRIR MOCHILA ---- + elif escolha == "2": + limpar_tela() + while True: + print("\n" + "=" * 15 + " 🎒 SUA MOCHILA " + "=" * 15) + if not player.backpack.items: + print("Sua mochila está completamente vazia!") + print("=" * 46) + input("\nPressione Enter para voltar...") + break + + # Lista os itens + for idx, item in enumerate(player.backpack.items, start=1): + print(f"{idx}. {item.name}") + print("0. Voltar ao menu de combate ↩️") + print("=" * 46) + + opc = input( + "Escolha um item para inspecionar/usar (ou 0 para voltar): " + ).strip() + if opc == "0": + break + + if ( + not opc.isdigit() + or int(opc) < 1 + or int(opc) > len(player.backpack.items) + ): + print("❌ Opção inválida!") + continue + + item_selecionado = player.backpack.items[int(opc) - 1] + + print("\n" + "-" * 15 + " DETALHES DO ITEM " + "-" * 15) + print(item_selecionado.get_detailed_info()) + print("-" * 48) + print("1. 👍 Usar/Equipar ") + print("2. 🗑️ Descartar Item") + print("0. ↩️ Voltar") + + acao = input("O que deseja fazer? ").strip() + + if acao == "0": + continue + elif acao == "1": + if hasattr(item_selecionado, "damage"): # É uma arma + player.equip(item_selecionado) + elif hasattr(item_selecionado, "heal_amount"): # É uma poção + item_selecionado.use(player) + player.backpack.rmv_item(item_selecionado) + + # Beber poção gasta turno: inimigo ataca! + print( + f"\n⚡ OPORTUNIDADE: Enquanto você se curava, o [{enemy.name}] atacou!" + ) + Combat.battle(enemy, player) + break + elif acao == "2": + if player.equipped_weapon == item_selecionado: + player.equipped_weapon = None # Desequipa se for a arma atual + player.backpack.rmv_item(item_selecionado) + print(f'🗑️ Você descartou "{item_selecionado.name}".') + break + + if player.hp <= 0: + return False + + # ---- OPÇÃO 3: VER STATUS ---- + elif escolha == "3": + limpar_tela() + print("--- SEU STATUS ---") + print(player.show_status()) + print("\n--- STATUS DO INIMIGO ---") + print(enemy.show_status()) + + # ---- OPÇÃO 4: FUGIR ---- + elif escolha == "4": + print( + f"\n🏃 [{player.name}] largou as armas e fugiu correndo da batalha chorando!" + ) + return False + else: + print("❌ Opção inválida!") + + # Fim do While: Alguém morreu + if player.hp <= 0: + print(f"\n💀 GAME OVER! Você foi derrotado por [{enemy.name}]...") + return False + elif enemy.hp <= 0: + print(f"\n🎉 VITÓRIA! Você derrotou [{enemy.name}]!") + # Recompensa em Ouro! + player.gold += enemy.gold_reward + print( + f"🪙 Você saqueou o corpo do monstro e encontrou 💰 {enemy.gold_reward} moedas de ouro!" + ) + return True + + +# ============================================================================== +# 2. FLUXO PRINCIPAL DA JORNADA DO HERÓI +# ============================================================================== +if __name__ == "__main__": + limpar_tela() + print("\n ---- ⚔️ A JORNADA DO HERÓI COMEÇOU ⚔️ ----\n") + + # Setup Inicial do Player + player1 = Player( + name="Witalo", + hp=100, + max_hp=100, + atk=12, + defense=8, + mana=6, + max_mana=6, + gold=10, # Começa com 10 moedas de reserva + ) + + # Itens Iniciais + player1.backpack.add_item(WEAPONS["iron_sword"]) + player1.backpack.add_item(CONSUMABLES["health_potion"]) + player1.equip(WEAPONS["iron_sword"]) + + input("\nPressione Enter para iniciar a história...") + limpar_tela() + + # -------------------------------------------------------------------------- + # FASE 1: NPC Camponês & O Goblin + # -------------------------------------------------------------------------- + print("=" * 50) + print("[👨‍🌾 NPC Camponês]: Um Goblin Saqueador invadiu as") + print('nossas plantações🌾 e está roubando nossas colheitas!!! "') + print("=" * 50) + + NpcAI.conversar_com_campones(player_name=player1.name) + # O "Gancho" inteligente que você desenhou: + while True: + print("\nE então, jovem herói, você vai caçar o terrível Goblin?") + print("1 - Sim, eu vou!") + print("2 - Não, ainda não estou pronto (Ver Status)") + + gancho = input("Sua resposta: ").strip() + + if gancho == "1": + print('\n[NPC Camponês]: "Muito obrigado! Que os deuses te guiem!"') + break + elif gancho == "2": + limpar_tela() + print("\nVocê respira fundo, se concentra e checa suas condições:") + print("-" * 30) + print(player1.show_status()) + print("-" * 30) + else: + print("❌ Escolha apenas 1 ou 2.") + + # Inicia a luta contra o Goblin cadastrado no database + goblin = ENEMIES["goblin"] + venceu = executar_combate(player1, goblin) + + if not venceu: + print("\nA sua jornada terminou antes mesmo de começar...") + exit() # Fecha o jogo se morrer ou fugir + + input("\nPressione Enter para continuar viagem...") + limpar_tela() + + # -------------------------------------------------------------------------- + # FASE 2: O Mercador Ambulante + # -------------------------------------------------------------------------- + # Abre a loja passando o jogador por parâmetro + Merchant.open_shop(player1) + + input("Pressione Enter para seguir viagem pela estrada escura...") + limpar_tela() + + # -------------------------------------------------------------------------- + # FASE 3: O Esqueleto + # -------------------------------------------------------------------------- + print("=" * 50) + print( + "O vento sopra frio. Entre as lápides de um velho cemitério na beira da estrada," + ) + print( + "um som de ossos batendo ecoa. Um Esqueleto Guardião se levanta com uma espada enferrujada!" + ) + print("=" * 50) + input("\n[Prepare-se para lutar!] Pressione Enter...") + + esqueleto = ENEMIES["skeleton"] + venceu = executar_combate(player1, esqueleto) + + if not venceu: + print("\nOs seus ossos agora farão companhia ao esqueleto na estrada...") + exit() + + input("\nPressione Enter para continuar...") + limpar_tela() + + # -------------------------------------------------------------------------- + # FASE 4: NPC em Perigo & O Boss Final (Rato Gigante) + # -------------------------------------------------------------------------- + print("=" * 50) + print("De repente, você ouve passos rápidos e descontraídos na sua direção.") + print("Uma jovem camponesa passa correndo desesperada e grita:") + print( + '\n[👩 NPC em Perigo]: "ALGUÉM ME SALVE! TEM UM RATO GORDO NOJENTO ATRÁS DE MIM!!!"' + ) + print("=" * 50) + print( + "\nAtrás dela, surge uma criatura imensa, do tamanho de um urso, babando e com dentes afiados." + ) + input( + "\n[É a batalha final!] Pressione Enter para enfrentar o Rato Gigante Gordo..." + ) + + rato = ENEMIES["giant_rat"] + venceu = executar_combate(player1, rato) + + if not venceu: + print("\nVocê virou comida de rato de esgoto... Que fim trágico.") + exit() + + # -------------------------------------------------------------------------- + # FIM DE JOGO: Vitória Total + # -------------------------------------------------------------------------- + limpar_tela() + print("=" * 60) + print(f" ✨ PARABÉNS, {player1.name.upper()}!!! ✨") + print("=" * 60) + print("Você derrotou as 3 criaturas mais temíveis da região!") + print("Os camponeses fazem uma festa em sua homenagem e o seu nome") + print("será lembrado para sempre como o do Verdadeiro Herói dessas terras.") + print("\n Obrigado por jogar o RPG-Python! FIM! 🎉") + print("=" * 60) diff --git a/Projeto/rpg-project/npc.py b/Projeto/rpg-project/npc.py new file mode 100644 index 0000000..2c07a26 --- /dev/null +++ b/Projeto/rpg-project/npc.py @@ -0,0 +1,98 @@ +# npc.py +import os +from groq import Groq +from dotenv import load_dotenv + +# 🚨 AVISO DE SEGURANÇA: Nunca deixe sua chave exposta se for enviar para o GitHub! +# O ideal é usar variáveis de ambiente, mas para testarmos localmente agora, +# você pode colar sua chave diretamente aqui se preferir. +load_dotenv() + + +class NpcAI: + @staticmethod + def conversar_com_campones(player_name: str): + """Gerencia a conversa por IA com o Camponês antes do Goblin.""" + api_key = os.environ.get("GROQ_API_KEY") + + if not api_key: + print("\n❌ Erro: Chave GROQ_API_KEY não encontrada!") + print("Crie um arquivo .env na raiz do projeto e adicione sua chave lá.") + return + + try: + client = Groq(api_key=api_key) + except Exception: + print( + "\n❌ Erro ao inicializar a Groq. Verifique sua API Key no arquivo npc.py!" + ) + return + + # Contexto (System Prompt) para moldar a personalidade do NPC + contexto_npc = ( + "Você é um camponês humilde e desesperado em um jogo de RPG de texto. " + "Um Goblin Saqueador terrível está destruindo suas plantações de batata. " + f"Um jovem herói chamado {player_name} acabou de chegar para falar com você. " + "Seja imersivo, medieval, fale com medo do monstro, mas seja breve nas respostas (máximo 3 frases). " + "Diga a ele que se ele quiser encerrar a conversa e partir para a luta, basta digitar 'sair'." + ) + + # Histórico da conversa para a IA lembrar do que foi dito + historico = [{"role": "system", "content": contexto_npc}] + + print("\n" + "=" * 20 + " 🌾 INTERAÇÃO NARRATIVA " + "=" * 20) + print( + '[👨‍🌾NPC Camponês]: "Oh, graças aos céus! Um viajante! Por favor, me ouça..."' + ) + print( + "(Você pode conversar livremente com o Camponês. Digite 'sair' para encerrar o diálogo.)" + ) + print("=" * 64) + + turnos_restantes = 4 # Limite de turnos de conversa que você sugeriu! + + while turnos_restantes > 0: + print(f"\n💬 [Turnos de conversa restantes: {turnos_restantes}]") + fala_jogador = input(f"[{player_name}]: ").strip() + + if fala_jogador.lower() == "sair": + print( + '\n[👨‍🌾NPC Camponês]: "Entendo... O tempo está acabando para as minhas batatas!"' + ) + break + + # Adiciona a fala do jogador ao histórico + historico.append({"role": "user", "content": fala_jogador}) + + print("\n🤖 [👨‍🌾Camponês pensando...] ") + + try: + # Chamada oficial para o modelo Llama 3 70B via Groq + completion = client.chat.completions.create( + model="llama-3.3-70b-versatile", + messages=historico, + temperature=0.7, + max_tokens=150, + ) + + resposta_npc = completion.choices[0].message.content + print(f'\n[👨‍🌾NPC Camponês]: "{resposta_npc}"') + + # Adiciona a resposta do NPC ao histórico para manter o contexto + historico.append({"role": "assistant", "content": resposta_npc}) + + except Exception as e: + print(f"\n❌ Houve um erro na comunicação com a IA: {e}") + print( + '[NPC Camponês]: "*O camponês chora confuso e aponta para o horizonte onde o Goblin está*"' + ) + break + + turnos_restantes -= 1 + + if turnos_restantes == 0: + print( + '\n[NPC Camponês]: "Não temos mais tempo para conversa! O monstro está se aproximando!"' + ) + + print("\n" + "=" * 64 + "\n") diff --git a/Projeto/rpg-project/shop.py b/Projeto/rpg-project/shop.py new file mode 100644 index 0000000..5d6293a --- /dev/null +++ b/Projeto/rpg-project/shop.py @@ -0,0 +1,81 @@ +# shop.py +from database import WEAPONS, CONSUMABLES + + +class Merchant: + @staticmethod + def open_shop(player): + """Abre o menu interativo de compras com o Mercador.""" + + # Carrega dinamicamente os itens disponíveis no catálogo do database + catalogo_loja = [] + for item in WEAPONS.values(): + catalogo_loja.append(item) + for item in CONSUMABLES.values(): + catalogo_loja.append(item) + + while True: + print("\n" + "=" * 15 + "👳‍♂️ MERCADOR AMBULANTE " + "=" * 15) + print(f"💰 Seu Ouro Atual: {player.gold} moedas") + print("=" * 44) + print("0. ↩️ Sair da Loja e Continuar sua Jornada") + + # Lista as mercadorias com seus respectivos preços + for idx, item in enumerate(catalogo_loja, start=1): + print(f"{idx}. {item.name:<18} | Preço: 💰 {item.value} moedas") + print("=" * 44) + + escolha = input( + '\n[Mercador]: "Olá viajante! O que vai querer hoje?" (Digite o número): ' + ).strip() + + if escolha == "0": + print( + '\n[Mercador]: "Tenha cuidado herói, ouvi dizer que tem um esqueleto por essas redondezas..."' + ) + print("Você se despede do mercador e segue seu caminho.\n") + break + + # Validação de entrada + if ( + not escolha.isdigit() + or int(escolha) < 1 + or int(escolha) > len(catalogo_loja) + ): + print("❌ Opção inválida! Escolha um número válido da lista.") + continue + + idx_item = int(escolha) - 1 + item_selecionado = catalogo_loja[idx_item] + + # Submenu para inspecionar o item antes de gastar o dinheiro + print("\n" + "-" * 15 + " INSPEÇÃO DE ITEM " + "-" * 15) + print(item_selecionado.get_detailed_info()) # Polimorfismo em ação! + print("-" * 48) + + confirmar = ( + input( + f"Deseja comprar [{item_selecionado.name}] por 💰 {item_selecionado.value} moedas? (S/N): " + ) + .strip() + .upper() + ) + + if confirmar == "S": + # Verificação de Economia: O jogador tem dinheiro suficiente? + if player.gold >= item_selecionado.value: + player.gold -= item_selecionado.value # Deduz o ouro + player.backpack.add_item( + item_selecionado + ) # Adiciona à mochila por Composição + print( + f'✅ [Mercador]: "Excelente escolha! Aqui está o seu [{item_selecionado.name}]."' + ) + else: + print( + '\n❌ [Mercador]: "Ei! Você não tem moedas suficientes para pagar por isso. Vá caçar mais alguns monstros!"' + ) + else: + print( + '\n[Mercador]: "Sem problemas. Quer dar uma olhada em outra coisa?"' + )