diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index 4c1034f..12e1b1a 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -14,11 +14,6 @@ on: pull_request: branches: [ main, develop ] -# Permite cancelar runs anteriores do mesmo workflow na mesma branch -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - env: NODE_VERSION: '20' FIREBASE_PROJECT_ID: pyexplorer-cd32d @@ -34,7 +29,7 @@ jobs: # =========================================================================== build: name: 🔨 Build & Lint - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 # Firebase credentials are now in .env file (committed to repo) # Security is enforced via Firestore Rules, not secret API keys @@ -94,7 +89,7 @@ jobs: # =========================================================================== deploy-production: name: 🚀 Deploy to Production - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: build # Só faz deploy em push para main (não em PRs) if: github.event_name == 'push' && github.ref == 'refs/heads/main' @@ -137,7 +132,7 @@ jobs: # =========================================================================== deploy-preview: name: 👀 Deploy Preview - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: build # Só faz preview em PRs (exceto Dependabot que não tem acesso aos secrets) if: github.event_name == 'pull_request' && github.actor != 'dependabot[bot]' @@ -166,7 +161,7 @@ jobs: # =========================================================================== lighthouse: name: 🔦 Lighthouse Audit - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 needs: deploy-production if: github.event_name == 'push' && github.ref == 'refs/heads/main' diff --git a/.github/workflows/dependabot-auto-merge.yml b/.github/workflows/dependabot-auto-merge.yml index 0557d46..08cdbd0 100644 --- a/.github/workflows/dependabot-auto-merge.yml +++ b/.github/workflows/dependabot-auto-merge.yml @@ -20,7 +20,7 @@ permissions: jobs: dependabot-auto-merge: name: 🤖 Auto-Merge Dependabot PRs - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 if: github.actor == 'dependabot[bot]' steps: diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index dc65270..0cb5fa4 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -18,7 +18,7 @@ jobs: # =========================================================================== audit: name: 🔒 Security Audit - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: 📥 Checkout repository @@ -48,7 +48,7 @@ jobs: # =========================================================================== outdated: name: 📦 Check Outdated Packages - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - name: 📥 Checkout repository diff --git a/src/data/questions/basic_commands.ts b/src/data/questions/basic_commands.ts index 3980aae..e79fddc 100644 --- a/src/data/questions/basic_commands.ts +++ b/src/data/questions/basic_commands.ts @@ -320,5 +320,23 @@ export const basicCommandsQuestions: QuestionDocument[] = [ ], explanationKidFriendly: 'O símbolo # faz o Python ignorar tudo que vem depois dele na mesma linha. É assim que fazemos comentários! 🤫', points: 10, + }, + { + id: 'basic_16', + type: 'multiple_choice', + world: 'basic_commands', + difficulty: 'easy', + ageMin: 8, + title: 'Linha em Branco', + prompt: 'Como fazemos para pular uma linha (mostrar uma linha vazia) usando print?', + options: [ + 'print()', + 'print(" ")', + 'print(0)', + 'pular_linha()', + ], + answerIndex: 0, + explanationKidFriendly: 'Se você usar `print()` sem nada dentro, ele mostra uma linha vazia! É ótimo para separar textos e organizar o que aparece na tela. 📄', + points: 10, } ]; diff --git a/src/data/questions/dictionaries.ts b/src/data/questions/dictionaries.ts index ce68708..74dbb83 100644 --- a/src/data/questions/dictionaries.ts +++ b/src/data/questions/dictionaries.ts @@ -203,5 +203,137 @@ export const dictionariesQuestions: QuestionDocument[] = [ ], explanationKidFriendly: 'Dicionários são fáceis de atualizar! Basta usar a chave ["poção"] e dar o novo valor. O valor antigo some e o novo assume! 💰', points: 15, + }, + { + id: 'dict_12', + type: 'multiple_choice', + world: 'dictionaries', + difficulty: 'medium', + ageMin: 10, + title: 'Tamanho do Dicionário', + prompt: 'Se tivermos `d = {"a": 1, "b": 2, "c": 3}`, o que `len(d)` retorna?', + options: [ + '3 (conta as chaves)', + '6 (conta tudo)', + '1 (conta o dicionário)', + '0', + ], + answerIndex: 0, + explanationKidFriendly: '`len()` conta quantas chaves (pares) existem no dicionário. Temos "a", "b" e "c", então são 3 itens! 📏', + points: 15, + }, + { + id: 'dict_13', + type: 'fill_code', + world: 'dictionaries', + difficulty: 'hard', + ageMin: 11, + title: 'Apagando Chaves', + prompt: 'Use `del` para apagar a chave "idade" do dicionário:', + starterCode: 'pessoa = {"nome": "Ana", "idade": 10}\n___ pessoa["idade"]\nprint(pessoa)', + solutionTemplate: 'del pessoa["idade"]', + tests: [ + { input: null, expectedOutput: "{'nome': 'Ana'}" }, + ], + explanationKidFriendly: '`del` vem de "deletar". Ele remove a chave e o valor do dicionário para sempre! 🗑️', + points: 20, + }, + { + id: 'dict_14', + type: 'multiple_choice', + world: 'dictionaries', + difficulty: 'medium', + ageMin: 10, + title: 'Só as Chaves', + prompt: 'Qual método usamos para ver todas as chaves (nomes) do dicionário?', + options: [ + '.keys()', + '.values()', + '.items()', + '.names()', + ], + answerIndex: 0, + explanationKidFriendly: '`keys()` retorna uma lista só com as chaves! É útil para saber quais informações temos guardadas. 🔑', + points: 15, + }, + { + id: 'dict_15', + type: 'multiple_choice', + world: 'dictionaries', + difficulty: 'medium', + ageMin: 10, + title: 'Só os Valores', + prompt: 'Qual método usamos para ver todos os valores guardados?', + options: [ + '.values()', + '.keys()', + '.data()', + '.get()', + ], + answerIndex: 0, + explanationKidFriendly: '`values()` mostra só os valores, ignorando os nomes das chaves. 💎', + points: 15, + }, + { + id: 'dict_16', + type: 'fill_code', + world: 'dictionaries', + difficulty: 'easy', + ageMin: 10, + title: 'Começando do Zero', + prompt: 'Crie um dicionário vazio chamado `mochila`:', + starterCode: 'mochila = ___\nmochila["água"] = 1\nprint(mochila)', + solutionTemplate: 'mochila = {}', + tests: [ + { input: null, expectedOutput: "{'água': 1}" }, + ], + explanationKidFriendly: 'Para criar um dicionário vazio, usamos chaves sem nada dentro `{}`. Depois podemos adicionar coisas! 🎒', + points: 10, + }, + { + id: 'dict_17', + type: 'true_false', + world: 'dictionaries', + difficulty: 'medium', + ageMin: 10, + title: 'Misturando Tudo', + prompt: 'Um dicionário pode ter chaves de texto e valores numéricos ao mesmo tempo? Ex: `{"nome": "Ana", "idade": 10}`', + correctBool: true, + explanationKidFriendly: 'Sim! Essa é a melhor parte dos dicionários. Você pode guardar textos, números e até listas no mesmo lugar! 🥗', + points: 10, + }, + { + id: 'dict_18', + type: 'fill_code', + world: 'dictionaries', + difficulty: 'hard', + ageMin: 11, + title: 'Dicionário dentro de Dicionário', + prompt: 'Acesse a nota de matemática ("mat") do aluno "joao":', + starterCode: 'notas = {"joao": {"mat": 10, "port": 8}}\nnota_mat = notas["joao"][___]\nprint(nota_mat)', + solutionTemplate: 'nota_mat = notas["joao"]["mat"]', + tests: [ + { input: null, expectedOutput: '10' }, + ], + explanationKidFriendly: 'Podemos ter dicionários dentro de dicionários! Primeiro abrimos a chave "joao", depois a chave "mat". 📦📦', + points: 25, + }, + { + id: 'dict_19', + type: 'multiple_choice', + world: 'dictionaries', + difficulty: 'medium', + ageMin: 10, + title: 'Loop no Dicionário', + prompt: 'Quando fazemos `for x in dicionario:`, o que é o `x`?', + options: [ + 'A chave (nome)', + 'O valor', + 'Os dois juntos', + 'O índice', + ], + answerIndex: 0, + explanationKidFriendly: 'Por padrão, o loop percorre as chaves! Se quiser os valores, precisa usar `.values()`. 🗝️', + points: 15, } ]; diff --git a/src/data/questions/error_handling.ts b/src/data/questions/error_handling.ts index 5412657..d68ec0a 100644 --- a/src/data/questions/error_handling.ts +++ b/src/data/questions/error_handling.ts @@ -208,5 +208,137 @@ export const errorHandlingQuestions: QuestionDocument[] = [ ], explanationKidFriendly: '`IndexError` acontece quando tentamos pegar um item que não existe na lista. Com `try/except`, evitamos que o programa quebre! 🛡️', points: 20, + }, + { + id: 'error_12', + type: 'multiple_choice', + world: 'error_handling', + difficulty: 'medium', + ageMin: 10, + title: 'Pegando o Erro Certo', + prompt: 'Para pegar SÓ o erro de divisão por zero, usamos:', + options: [ + 'except ZeroDivisionError:', + 'except Error:', + 'except:', + 'except Zero:', + ], + answerIndex: 0, + explanationKidFriendly: 'É melhor ser específico! `except ZeroDivisionError:` pega só erros de divisão, deixando outros erros passarem para você ver. 🎯', + points: 15, + }, + { + id: 'error_13', + type: 'multiple_choice', + world: 'error_handling', + difficulty: 'medium', + ageMin: 10, + title: 'Chave Inexistente', + prompt: 'Qual erro acontece se tentarmos acessar `dic["chave"]` e a chave não existir?', + options: [ + 'KeyError', + 'ValueError', + 'NameError', + 'TypeError', + ], + answerIndex: 0, + explanationKidFriendly: 'KeyError significa "Erro de Chave". Acontece quando tentamos usar uma chave que não está no dicionário! 🔑❌', + points: 15, + }, + { + id: 'error_14', + type: 'multiple_choice', + world: 'error_handling', + difficulty: 'medium', + ageMin: 11, + title: 'Sempre Executa', + prompt: 'Qual bloco do try/except roda SEMPRE, dando erro ou não?', + options: [ + 'finally', + 'else', + 'always', + 'check', + ], + answerIndex: 0, + explanationKidFriendly: '`finally` significa "finalmente". Ele roda no final de tudo, não importa o que aconteça! Ótimo para limpeza. 🧹', + points: 15, + }, + { + id: 'error_15', + type: 'fill_code', + world: 'error_handling', + difficulty: 'hard', + ageMin: 11, + title: 'Vários Problemas', + prompt: 'Complete para tratar `ValueError` e `ZeroDivisionError` separadamente:', + starterCode: 'try:\n x = int(input()) / int(input())\nexcept ValueError:\n print("Erro de valor")\n___ ZeroDivisionError:\n print("Não divida por zero")', + solutionTemplate: 'except ZeroDivisionError:', + tests: [ + { input: null, expectedOutput: 'Não divida por zero' }, // This test implies the mock would trigger it, but for fill_code we check the syntax mainly + ], + explanationKidFriendly: 'Você pode ter vários `except`! Um para cada tipo de problema. Assim você dá a mensagem certa para o erro certo! 👨‍⚕️', + points: 20, + }, + { + id: 'error_16', + type: 'true_false', + world: 'error_handling', + difficulty: 'hard', + ageMin: 11, + title: 'Erro de Escrita', + prompt: 'O `try/except` consegue pegar erros de digitação (SyntaxError), como esquecer os dois pontos `:` ?', + correctBool: false, + explanationKidFriendly: 'Não! `SyntaxError` acontece ANTES do programa rodar. O Python nem começa se tiver erro de escrita, então o `try` não funciona! 🚫', + points: 15, + }, + { + id: 'error_17', + type: 'multiple_choice', + world: 'error_handling', + difficulty: 'medium', + ageMin: 10, + title: 'Espaços Errados', + prompt: 'O que é um IndentationError?', + options: [ + 'Erro nos espaços/tabulação do código', + 'Erro de matemática', + 'Erro de nome de variável', + 'Erro de importação', + ], + answerIndex: 0, + explanationKidFriendly: 'Python é muito exigente com espaços! Se você não alinhar o código corretamente, ele dá `IndentationError`. 📏', + points: 15, + }, + { + id: 'error_18', + type: 'fill_code', + world: 'error_handling', + difficulty: 'medium', + ageMin: 11, + title: 'Texto não é Número', + prompt: 'Trate o erro de converter "dez" para número (ValueError):', + starterCode: 'try:\n n = int("dez")\nexcept ___:\n print("Não é número")', + solutionTemplate: 'except ValueError:', + tests: [ + { input: null, expectedOutput: 'Não é número' }, + ], + explanationKidFriendly: '`ValueError` acontece quando o valor não faz sentido. Tentar transformar "dez" em número é um erro de valor! 🔠', + points: 20, + }, + { + id: 'error_19', + type: 'fill_code', + world: 'error_handling', + difficulty: 'medium', + ageMin: 10, + title: 'Ignorando Erro', + prompt: 'Se der erro, não faça nada (use `pass`):', + starterCode: 'try:\n print(1/0)\nexcept:\n ___', + solutionTemplate: 'pass', + tests: [ + { input: null, expectedOutput: '' }, + ], + explanationKidFriendly: '`pass` significa "passar". É como dizer "deixa pra lá". O erro acontece, é capturado, e o programa segue sem fazer nada. 😶', + points: 15, } ]; diff --git a/src/data/questions/user_input.ts b/src/data/questions/user_input.ts index b6ec32a..5d438a9 100644 --- a/src/data/questions/user_input.ts +++ b/src/data/questions/user_input.ts @@ -209,5 +209,123 @@ export const userInputQuestions: QuestionDocument[] = [ answerIndex: 0, explanationKidFriendly: '`str()` transforma números em texto (string). É útil para comparar senhas ou juntar com outras palavras! 🔐', points: 15, + }, + { + id: 'input_13', + type: 'fill_code', + world: 'user_input', + difficulty: 'easy', + ageMin: 9, + title: 'Cor Favorita', + prompt: 'Pergunte a cor favorita e guarde na variável `cor`:', + starterCode: 'cor = ___("Qual sua cor favorita? ")\nprint("Que cor linda!")', + solutionTemplate: 'cor = input("Qual sua cor favorita? ")', + tests: [ + { input: null, expectedOutput: 'Qual sua cor favorita?' }, + ], + explanationKidFriendly: 'Usamos `input()` para fazer uma pergunta. A resposta da pessoa fica guardada na variável `cor`. 🎨', + points: 10, + }, + { + id: 'input_14', + type: 'multiple_choice', + world: 'user_input', + difficulty: 'easy', + ageMin: 9, + title: 'Mensagem do Input', + prompt: 'O texto que colocamos dentro do `input(...)` serve para:', + options: [ + 'Mostrar uma pergunta para o usuário', + 'Guardar a resposta', + 'Criar um erro', + 'Nada', + ], + answerIndex: 0, + explanationKidFriendly: 'O texto dentro do `input()` é a mensagem que aparece na tela pedindo a informação. Sem ele, a pessoa não sabe o que digitar! 🗣️', + points: 10, + }, + { + id: 'input_15', + type: 'fill_code', + world: 'user_input', + difficulty: 'medium', + ageMin: 10, + title: 'Ano de Nascimento', + prompt: 'Se estamos em 2025, calcule o ano que a pessoa nasceu:', + starterCode: 'idade_txt = input("Idade: ")\nidade = int(idade_txt)\nano_nasc = 2025 - ___\nprint(ano_nasc)', + solutionTemplate: 'ano_nasc = 2025 - idade', + tests: [ + { input: null, expectedOutput: 'Idade:' }, + ], + explanationKidFriendly: 'Para descobrir o ano de nascimento, subtraímos a idade do ano atual! 2025 menos a idade dá o ano que a pessoa nasceu. 📅', + points: 15, + }, + { + id: 'input_16', + type: 'fill_code', + world: 'user_input', + difficulty: 'medium', + ageMin: 10, + title: 'Olá Personalizado', + prompt: 'Use f-string para dar oi com o nome digitado:', + starterCode: 'nome = input("Nome: ")\nprint(f"Oi, {___}!")', + solutionTemplate: 'print(f"Oi, {nome}!")', + tests: [ + { input: null, expectedOutput: 'Nome:' }, + ], + explanationKidFriendly: 'Com f-string, colocamos a resposta do usuário (variável `nome`) direto dentro da frase de cumprimento! 👋', + points: 15, + }, + { + id: 'input_17', + type: 'fill_code', + world: 'user_input', + difficulty: 'hard', + ageMin: 11, + title: 'Multiplicando Entradas', + prompt: 'Peça dois números e multiplique eles:', + starterCode: 'n1 = int(input("N1: "))\nn2 = int(input("N2: "))\nres = n1 ___ n2\nprint(res)', + solutionTemplate: 'res = n1 * n2', + tests: [ + { input: null, expectedOutput: 'N1:' }, + ], + explanationKidFriendly: 'Pedimos dois números (convertendo com `int`) e usamos o asterisco `*` para multiplicar um pelo outro. ✖️', + points: 20, + }, + { + id: 'input_18', + type: 'multiple_choice', + world: 'user_input', + difficulty: 'easy', + ageMin: 9, + title: 'Duas Perguntas', + prompt: 'Se tivermos dois `input()` seguidos, o que acontece?', + options: [ + 'Pergunta o primeiro, espera, depois pergunta o segundo', + 'Pergunta os dois ao mesmo tempo', + 'Só pergunta o primeiro', + 'Dá erro', + ], + answerIndex: 0, + explanationKidFriendly: 'O Python faz uma coisa de cada vez! Ele pergunta a primeira, espera você responder, e só depois pergunta a segunda. 🚶‍♂️', + points: 10, + }, + { + id: 'input_19', + type: 'multiple_choice', + world: 'user_input', + difficulty: 'medium', + ageMin: 10, + title: 'Input Vazio', + prompt: 'Se o usuário apertar ENTER sem digitar nada no `input()`, o que é guardado?', + options: [ + 'Uma string vazia ""', + 'O número 0', + 'A palavra "Nada"', + 'O programa trava', + ], + answerIndex: 0, + explanationKidFriendly: 'Se não digitar nada, o Python guarda um texto vazio (aspas sem nada dentro: ""). É como uma caixa vazia! 📦', + points: 15, } ];