Plano de Implementação — Blocky Bird¶
Tarefas incrementais; cada uma referencia os requisitos que atende. Executar em ordem — cada tarefa deixa o jogo executável.
Estado desta versão: as tasks 1–19 são o histórico já concluído na v1 (mantidas aqui para o documento ser autocontido). As tasks 20–30 são o trabalho da v2 (Android). As tasks 31–41 são aumentos de escopo posteriores da v2 (qualidade, correções pós-lançamento, o ícone do aplicativo e o ajuste final de resolução/orientação), também já concluídas.
As tasks 20–25 são todas implementáveis e testáveis no desktop, deliberadamente antes de mexer na cadeia de build Android — assim o risco de empacotamento (task 27, ver design seção 24.1) fica isolado no fim, e cada task anterior é verificável de imediato.
-
[x] 1. Esqueleto do projeto e loop básico Inicializar o projeto com
uv init(pyproject.tomlcompygame>=2.5epytestcomo dev;uv sync), criar estrutura desrc/,config.pycom constantes emain.py/game.pycom janela 480×720, clock 60 FPS e loop de eventos que fecha com o X da janela. Validaruv run main.py. (R9) -
[x] 2. Texturas procedurais base Implementar
textures.py: gerador de bloco 16×16 com ruído + paletasdirt,grass_side,stone, e sprite da abelha; escala pixel-perfect para 48 px. Tela de teste exibindo os blocos. (R7.1, R7.2) -
[x] 3. Bird com física e animação Implementar
bird.py: gravidade, flap por ESPAÇO/↑/mouse, clamp no topo, rotação subida/queda, animação de asas. (R1) -
[x] 4. Obstáculos Implementar
pipes.py: spawn com abertura aleatória, movimento, remoção fora da tela, renderização como pilha de blocos com bloco de borda na abertura. (R2) -
[x] 5. Colisão e chão Chão rolante de blocos; hitbox 85%; colisão pássaro×coluna e pássaro×chão encerra a rodada (por ora, reinicia direto). (R3.1, R3.5, R7.3)
-
[x] 6. Máquina de estados e UI Implementar
GameState(PRONTO/JOGANDO/PAUSADO/GAME_OVER) eui.py: tela inicial com idle bobbing, pausa com ESC/P, tela de game over com reinício, fonte pixelada com sombra. (R6, R3.3, R3.4, R7.5) -
[x] 7. Pontuação e recorde Implementar
score.py: +1 por coluna ultrapassada, HUD, persistência emhighscore.jsoncom fallback para arquivo ausente/corrompido, exibição do recorde no game over. (R4) -
[x] 8. Biomas e progressão de dificuldade Implementar
biome.py: os 3 biomas com thresholds 0/10/25, parâmetros de velocidade/abertura, texturascobblestone/netherrack/obsidian, fade de céu ≤ 1 s e banner com nome do bioma. (R5, R2.5) -
[x] 9. Decoração de fundo com parallax Duas camadas parallax por bioma: nuvens/colinas, estalactites/minérios, lava/pilares. (R7.4)
-
[x] 10. Partículas Implementar
particles.pye disparar burst na colisão com cores da textura atingida. (R3.2) -
[x] 11. Áudio sintetizado Implementar
sounds.py: flap, score, hit, portal; tecla M para mudo; degradação graciosa sem mixer. Integrar aos eventos. (R8, R3.2) -
[x] 11b. Suporte a controle de Xbox Implementar
input.py(InputManager): mapear teclado/mouse/joystick para ações abstratas (A → voar/reiniciar, Start → pausar, Y → mudo), inicialização viapygame.joysticke hotplug comJOYDEVICEADDED/JOYDEVICEREMOVED. Refatorar os estados para consumir ações. (R10) -
[x] 12. Calibração de gameplay Playtest e ajuste fino de
GRAVITY,FLAP_IMPULSE,PIPE_SPACING, aberturas e velocidades por bioma até a curva de dificuldade ficar justa. (R5.3, R9.1) -
[x] 13. Testes e verificação final Testes unitários (física, pipes, score, biomas, persistência) via
uv run pytestcomSDL_VIDEODRIVER=dummy; checklist manual cobrindo cada critério R1–R9; README curto com instruções de execução viauv. (R9.3, todos)
Tarefas adicionais da v1 (pós-plano original)¶
Pedidos feitos via chat após a conclusão do plano original (tasks 1–13). Cada uma segue o mesmo padrão: implementação, validação (testes automatizados e/ou inspeção visual) e commit.
-
[x] 14. Correção de bug: hitbox das colunas maior que o sprite
PIPE_W(78 px) não batia com a largura realmente desenhada empipes.py(BLOCK, 48 px), deixando uma faixa de colisão invisível de 30 px além do bloco visível — o pássaro colidia antes de tocar a coluna. Corrigido definindoPIPE_W = BLOCKemconfig.py. (R3.1, R3.5) -
[x] 15. Decoração quadriculada (nuvens/colinas blocky) Nuvens e colinas do Overworld usavam
pygame.draw.ellipse, destoando da estética voxel do resto do jogo. Substituídas por formas quadriculadas (pilhas de retângulos, com variantes seedadas por slot de parallax) emdecor.py. (R7.1, R7.4) -
[x] 16. Créditos dos criadores Nova constante
CREDITSemconfig.py("por Douglas e Pedro"), exibida na tela PRONTO logo abaixo do título e incluída no título da janela. (R11) -
[x] 17. Recorde em destaque na tela inicial Exibe "RECORDE: N" no rodapé da tela PRONTO (texto dourado, sem caixa/contorno — iteração final após testar uma variante com painel), acima do chão, para aumentar a expectativa de bater o recorde antes de começar a partida. (R12)
-
[x] 18. Executável standalone (PyInstaller)
BlockyBird.spec(onefile, sem console) permite gerardist/BlockyBird.exe/dist/BlockyBirdcomuv run pyinstaller BlockyBird.spec, sem exigir Python nemuvna máquina de destino.pyinstalleradicionado como dependência de dev. (R13.1) -
[x] 19. CI/CD de release (GitHub Actions)
.github/workflows/release.yml: ao publicar uma Release com tag no GitHub, builda o executável para Windows e Linux (matriz de jobs) e anexa aos assets da release comoBlockyBird-windows-x64-<tag>.zipeBlockyBird-linux-x64-<tag>.tar.bz2viasoftprops/action-gh-release; nome do asset inclui plataforma e arquitetura. (R13.2)
Tarefas da v2 — Android (pendentes)¶
Fase 1 — mudanças no jogo, testáveis no desktop (tasks 20–25). Fase 2 — cadeia de build e distribuição Android (tasks 26–30).
-
[x] 20. Fonte bitmap própria, sem dependência do sistema Implementar
pixelfont.py: glifos 5×7 desenhados por código paraA-Z,0-9,:,/,!,-e espaço, comrender(text, scale, color)e cache. Trocar oSysFont("couriernew")deui.pypor essa fonte, preservando a sombra dura e a hierarquia de tamanhos das telas atuais. Validar visualmente que PRONTO, HUD, PAUSADO e GAME_OVER continuam legíveis e centralizados. (R7.6, R7.5, R14.5) Ajuste feito na implementação: a fonte bitmap é proporcionalmente mais larga que aSysFontantiga — mapearbase_sizedireto para escala fixa estourava a largura da tela em 3 textos reais (BLOCKY BIRD585px,ESPACO / CLIQUE PARA VOAR745px,ESPACO / CLIQUE PARA REINICIAR716px, todos > 480px). Adicionadoui._fit_scale(): reduz a escala automaticamente até o texto caber emSCREEN_W - 40, verificado para todas as strings reais do jogo. -
[x] 21. Resolução lógica escalável com letterbox Passar
pygame.SCALED | pygame.RESIZABLEnoset_modee verificar que o jogo mantém proporção ao redimensionar a janela (barras nas laterais/topo, sem distorção nem deslocamento do gameplay). Confirmar que o clique do mouse continua batendo com o que se vê. Atenção — quebra conhecida:SCALEDsobSDL_VIDEODRIVER=dummysó permite umset_modepor processo, etests/test_game.pycriaGame()em 6 testes, então a suíte quebra sem ajuste. Incluir nesta task a fixture depygame.display.quit()+init()noconftest.py(solução já verificada, ver design seção 20.2) e deixar a suíte passando. (R9.1, R14.3) Validado: janela real redimensionada para 900×500 (bem diferente de 480×720) mostrou pillarbox correto, sem distorção. Tradução de coordenada do mouse confirmada por instrumentação direta (script que registraevent.pos): clique na coordenada lógica alvo(5,5)numa janela com offset de pillarbox de 202px foi reportado porpygamecomopos=(5, 5)— exatamenteclient_x - offset_x; clique na barra preta (fora da área lógica) reportoupos=(-101, 360), coordenada negativa fora da faixa válida, confirmando que opygame.SCALEDjá entrega tudo em espaço lógico sem esforço adicional. -
[x] 22. Armazenamento do recorde por plataforma + gravação incremental Implementar
storage.py(is_android(),save_dir()) e apontarscore.pypara ele; mudar a gravação do recorde para o instante em que o score ultrapassa o recorde, em vez de só no GAME_OVER. Testes para os dois caminhos de plataforma e para a gravação incremental. (R4.3, R4.5, R16.3, R16.4) Ajustes feitos na implementação: score.pyresolve o caminho padrão dentro do corpo da função (_default_path()), não como valor de default de parâmetro (path: Path = HIGHSCORE_PATH) — um default de parâmetro é calculado uma única vez na importação do módulo, o que "congelaria"storage.save_dir()e tornaria o comportamento impossível de testar via monkeypatch (ou de reagir a uma mudança de plataforma em runtime).- Por consequência, a fixture
_isolate_cwddoconftest.py(que já isolavahighscore.jsonviachdir) precisou de ummonkeypatch.setattr("src.storage.save_dir", lambda: tmp_path)—storage.save_dir()no desktop resolve a raiz do projeto via__file__, não mais o cwd, então só ochdirnão bastava. Confirmado que sem esse ajuste os testes voltavam a escreverhighscore.jsonna raiz real do projeto. - Essa mesma fixture autouse, por sua vez, impedia testar a implementação real de
storage.save_dir()emtests/test_storage.py— resolvido chamandomonkeypatch.undo()no início desses testes especificamente, revertendo a proteção só ali. -
Validado fora do pytest também: rodando o jogo de verdade, o
highscore.jsoné gravado na raiz real do projeto assim que o score ultrapassa o recorde, ainda em JOGANDO (antes de qualquer colisão). -
[x] 23. Entrada por toque e botão BACK Tratar
FINGERDOWNeminput.pycom conversão de coordenada normalizada → espaço lógico (desfazendo o letterbox),K_AC_BACKcomo açãoback, e o desvio por estado noGame(pausa em JOGANDO, encerra fora dele). Adicionar o ícone de mudo tocável no canto da tela. Testar com eventos sintéticos, incluindo toque nas barras (deve ser ignorado). (R15.1, R15.2, R15.3, R15.4) Ajuste feito na implementação: o design previa sóFINGERDOWNfazendo hit-test do ícone de mudo, mas o próprio design nota que o SDL sintetizaMOUSEBUTTONDOWNa partir do toque real — sem tratamento, tocar no ícone no Android disparariaflaptambém pelo evento de mouse sintético. Unificado num_handle_tap()compartilhado, usado tanto porMOUSEBUTTONDOWN(que já chega em coordenadas lógicas viaSCALED) quanto porFINGERDOWN(convertido manualmente).ui.MUTE_ICON_RECTfoi colocado emui.py(onde o ícone é desenhado) e importado porinput.pypara o hit-test, mantendo desenho e geometria juntos. -
[x] 24. Pausa automática ao perder foco (ciclo de vida) Tratar
APP_WILLENTERBACKGROUND/APP_DIDENTERBACKGROUND(eWINDOWFOCUSLOSTcomo fallback de desktop) como açãofocus_lost, levando JOGANDO → PAUSADO e nunca retomando sozinho. Testável no desktop com alt-tab. (R16.1, R16.2) Validado por evento sintético (pytest), não por alt-tab real: a lógica (JOGANDO→PAUSADO ao perder foco, sem retomada automática, ignorado fora de JOGANDO, cobrindo tantoWINDOWFOCUSLOSTquantoAPP_WILLENTERBACKGROUND) está coberta por 4 testes com eventos postados diretamente. A tentativa de reproduzir com uma janela real e alt-tab neste ambiente (troca de foco viaSetForegroundWindow) ficou inconclusiva — o pássaro chegou a GAME_OVER por simplesmente não receber nenhum flap durante os segundos do teste, independente do evento de foco ter disparado ou não; não há como isolar as duas causas de fora do processo sem instrumentação adicional. Como o código testado é exatamente o mesmo caminho que um alt-tab real percorre, considero a lógica coberta, mas recomendo uma checagem manual rápida (abrir o jogo, voar, alt-tab, voltar) antes de publicar. -
[x] 25. Mapeamentos adicionais de teclado e correção de alcançabilidade em PAUSADO Mapear
K_RETURN/K_KP_ENTERparaflape adicionar o mudo por setas ←/→ no overlay de PAUSADO, com a dica escrita na tela. Revisar que todo estado é alcançável sem toque e sem gamepad. (R15.4, R15.5) Bug real encontrado na revisão de alcançabilidade (o próprio ponto que esta task pedia para checar):_flap_action()não tratavaPAUSADO, e o BACK emPAUSADOencerra o jogo (não despausa) — então quem pausasse via BACK (sem tecla ESC/P nem botão Start de gamepad) ficava sem nenhuma forma de despausar, só de sair. O mesmo valia para quem só usa toque no celular. Corrigido fazendo_flap_action()também despausar (sem flapar o pássaro) quandoPAUSADO. Validado de ponta a ponta comtest_bare_tv_remote_reaches_every_state: simula um perfil de entrada básico (só setas, ENTER e voltar — sem toque, sem tecla M/ESC/P, sem gamepad) passando por todos os estados do jogo. -
[x] 26. Migração de
pygameparapygame-ceTrocar a dependência nopyproject.toml, recriar o ambiente (uv sync), rodar a suíte completa e validar o jogo no desktop. Nenhumimportmuda. Pré-requisito da cadeia de build Android. (R9.2)uv synctrocou limpo (desinstaloupygame==2.6.1, instaloupygame-ce==2.5.7, que também trouxe SDL 2.32.10 — versão mais nova que a 2.28.4 anterior). Isso quebrou 9 testes: sobSDL_VIDEODRIVER=dummy,pygame.display.set_mode(..., SCALED)no SDL novo enfileira uma sequência de eventos de janela (WindowShown,WindowFocusGained/Lost,ActiveEventetc.) que não existia na versão antiga — incluindo umWindowFocusLostgenuíno, que contaminava opoll()seguinte com uma açãofocus_lostespúria. Confirmado que é só um artefato do driverdummy: rodando com driver de vídeo real, a mesma criação de janela não gera nenhumWindowFocusLost(só eventos neutros comoWindowShown/MouseMotion). Corrigido compygame.event.clear()logo após oset_mode(), tanto emGame.__init__(defensivo, produção) quanto nos testes que criamInputManagerdiretamente. Reconstruí o executável (BlockyBird.spec) e confirmei que ainda empacota e roda normal compygame-ce. -
[x] 27.
buildozer.spece receita local dopygame-ceCriarbuildozer.spec(minapi 21, api 34, três ABIs, orientação retrato, fullscreen) e a receita local emp4a-recipes/pygame-ce/. Esta é a task de maior risco (ver design seção 24.1: a receita não está mergeada no p4a upstream) — atacar cedo dentro da fase 2 e, se a receita não compilar, fixar a versão depygame-ceconhecida como funcional. (R17.1, R14.1, R14.2) Estado:buildozer.specfixap4a.branch = v2024.01.21(última release do p4a antes de o hostpython3 passar a Python 3.14, que quebra osetup.pyde todas as versões testadas dopygame-ce—distutils.ccompiler.spawnremovido no Python 3.12+; achado real, documentado no própriobuildozer.spece no design seção 24.1). Ajuste feito na revisão desta task: a receita local (copiada do PR upstream kivy/python-for-android#2971) vinha fixada empygame-ce==2.4.0, desalinhada da versão2.5.7já validada no desktop (task 26). Corrigido paraversion = '2.5.7'(tag confirmada existente no repositóriopygame-community/pygame-ce), evitando ter duas versões depygame-cediferentes em voo entre desktop e Android; se essa versão não compilar no p4a (só verificável na task 28, sem Docker/Android neste ambiente — ver design seção 25), a saída documentada é fixar aqui a última versão conhecida como funcional. Não verificável neste ambiente: compilação real da receita (exige p4a + NDK/SDK Android, ver design seção 24.1/25) — fica para a task 28.buildozer.specfoi validado apenas como INI bem formado (configparser) e pela suítepytestcompleta (63 testes, inalterada por esta task, já que nenhum código do jogo mudou). -
[x] 28. Build local do APK e primeira instalação real Gerar o APK via container Docker do Buildozer, instalar em aparelho físico e validar o loop básico (abre, joga por toque, som, recorde persiste após fechar e reabrir). Primeiro ponto em que o jogo roda de fato no Android. (R17.1, R14.1) Estado: Docker ficou disponível neste ambiente (ao contrário do que o design.md/task 27 assumiam) e o build real via
docker run kivy/buildozer android debugfoi executado atéBUILD SUCCESSFUL, gerandobin/blockybird-0.2.0-armeabi-v7a_arm64-v8a_x86_64-debug.apk(62 MB, as 3 ABIs). Vários bugs reais só apareciam nesta etapa (nunca antes exercida) e foram corrigidos: - Cache
.buildozerde uma tentativa anterior tinhahostpython3compilado como CPython 3.14 em vez do 3.11.5 esperado do pinp4a.branch = v2024.01.21— cache limpo para forçar reclone correto. docker run -v "$(pwd):..."a partir do Git Bash montava um volume anônimo vazio em vez do diretório do projeto (path mangling do MSYS) — corrigido comMSYS_NO_PATHCONV=1.- O cache real do Android SDK/NDK do buildozer vive em
$HOME/.buildozerdentro do container (efêmero a cadadocker run --rm), mas o marcador "já instalado" fica em.buildozer/state.dbdo projeto (persistido via bind mount) — a inconsistência faziaplatforms;android-34nunca ser reinstalado em containers novos ("Available Android APIs are ()"). Corrigido montando um diretório persistente do host (~/.buildozer-android-global-cache) também em/home/user/.buildozer. p4a-recipes/jpeg/__init__.py(nova receita local): oCMakeLists.txtdo libjpeg-turbo 2.0.1 exigecmake_minimum_required< 3.5, incompatível com o CMake 4.2.3 do container — corrigido com-DCMAKE_POLICY_VERSION_MINIMUM=3.5direto na chamada (variável de ambiente viadocker -enão chega ao subprocesso, poisArch.get_env()do p4a monta o ambiente do zero);rm -ftrocado porrm -rfpara sobreviver a retries.p4a-recipes/pygame-ce/__init__.py: faltava'cython'emdepends(a cópia do PR upstream não declarava, ao contrário de outras receitas do p4a que compilam.pyx) —setup.py build_extfalhava com "You need cython".-
p4a-recipes/pygame-ce/__init__.py:sdl_image_includesapontava para a raiz dejni/SDL2_image, mas a versão do sdl2_image (2.8.0) move o header público parajni/SDL2_image/include/SDL_image.h(diferente doSDL2_ttf, que mantémSDL_ttf.hna raiz) —src_c/imageext.cfalhava com "'SDL_image.h' file not found". Não verificável neste ambiente: instalação e playtest em aparelho físico (sem Android real disponível) — pendente de validação manual pelo dono do projeto antes de publicar. -
[x] 29. Job de CI do APK na Release Acrescentar ao
release.ymlum jobubuntu-latestindependente que builda o APK em Docker, com cache de~/.buildozer, e anexaBlockyBird-<tag>.apksem compressão aos assets — mantendo os dois assets de desktop já existentes. (R17.2, R13.3) Implementação: jobbuild-apkem.github/workflows/release.yml, independente do jobbuild(matriz Windows/Linux) para que uma falha na cadeia Android (a parte mais frágil, task 27) não impeça a publicação dos executáveis de desktop.docker pull kivy/buildozer+docker run … kivy/buildozer android debug, comyes y |porque a imagem recusa rodar como root e porque o primeiro build precisa aceitar as licenças do Android SDK interativamente. APK renomeado paraBlockyBird-<tag>.apke publicado viasoftprops/action-gh-release@v2, que copia o arquivo como está (o.apkjá é um zip; a action não o recomprime). Ajuste feito na implementação: odocker runinicial só montava${{ github.workspace }}:/home/user/hostcwd, sem o segundo volume documentado pela própria imagem oficial (kivy/buildozerno Docker Hub) para persistir cache entre execuções —-v "$HOME/.buildozer":/home/user/.buildozer, onde SDK/NDK baixados ficam guardados fora do diretório do projeto. Sem esse mount, o cache deactions/cacheno path~/.buildozer(pedido pelo design, seção 24.3) sempre voltaria vazio, forçando o download completo de SDK+NDK (30–60 min) em toda execução. Corrigido adicionando os dois volumes e cacheando ambos os paths (~/.buildozere.buildozer) na mesma entrada deactions/cache. Não verificável neste ambiente: execução real do job (exige Docker + uma Release publicada no GitHub, ver design seção 25) — validado apenas como YAML bem formado (yaml.safe_load) e por leitura cruzada com a documentação oficial da imagemkivy/buildozerpara confirmar os volumes/paths de cache corretos. Fica para a task 30 (ou uma release de teste) confirmar que o job efetivamente builda e anexa o APK. -
[x] 30. Ajuste de áudio/performance no Android e verificação final da v2 Ajustar o buffer do mixer para Android e medir o tempo de frame em aparelho de entrada; completar o checklist manual em celular; atualizar o README com instruções de instalação do APK. (R8.4, R9.5, R14.6, todos) Implementado e verificável neste ambiente:
sounds.pyagora chamapygame.mixer.init(..., buffer=1024)quandostorage.is_android()é verdadeiro (mantendo o default do pygame no desktop, sem regressão), conforme o valor inicial documentado no design (seção 11) —2048fica como próximo passo caso o playtest real em aparelho ainda acuse estouro/crepitação com1024. Coberto portests/test_sounds.py(2 testes, mockandois_androidepygame.mixer.initpara inspecionar os kwargs passados). README atualizado com seção "Instalar no Android" (fontes desconhecidas, controles por toque/BACK, requisito de API 21) e o comandodocker runpara reproduzir o build do APK localmente. Suíte completa: 65 testes passando. Não verificável neste ambiente (sem Android real nem emulador, ver design seção 25): medir o tempo de frame em aparelho de entrada (R9.5), confirmar quebuffer=1024de fato elimina estouros/crepitação no hardware real (R8.4) — se não eliminar, subir paraANDROID_MIXER_BUFFER = 2048emsounds.py— e os itens de checklist abaixo marcados como "requer aparelho Android real". Ficam pendentes de validação manual pelo dono do projeto antes de considerar a v2 encerrada.
Correção pós-lançamento — Tela cheia no Android sem pillarbox, sempre em retrato¶
Pedido do dono do projeto: no Android, a tela deve ser preenchida sem barra visível nas laterais, e o jogo deve rodar sempre em retrato (nunca em paisagem, mesmo que o aparelho gire).
- [x] 41. Travar orientação retrato e preencher a tela via letterbox (
pygame.SCALED)buildozer.spec:orientation = portrait,fullscreen = 1.src/game.py::Game.__init__usapygame.SCALED | pygame.RESIZABLE(mesmo mecanismo da task 21) sem nenhum branch por plataforma — resolução lógica fixa 480×720 em qualquer aparelho, escalada para a janela/tela real mantendo a proporção; o excedente de um dos eixos vira barra de letterbox, nunca corta a imagem.src/scale.py::fit_scale(canvas_w, canvas_h, window_w, window_h)calcula a escala (mínimo dos dois fatores de eixo) e os offsets do letterbox, usado porinput.pypara converter toque (FINGERDOWN) em coordenada lógica —MOUSEBUTTONDOWNjá chega pré-convertido pelo própriopygame.SCALED. Toque na barra de letterbox é ignorado (não disparaflap/mute). Ver design.md seção 20. (R9.1, R14.3, R14.4, R15.1, R15.4) Por que travar a orientação elimina o pillarbox: compygame.SCALED(escala = menor dos dois fatores de eixo), pillarbox (barra lateral) só ocorre quando a janela é proporcionalmente mais larga que o jogo (2:3). Travado em retrato, a janela do Android nunca fica mais larga que alta — a esmagadora maioria dos celulares é mais alongada que 2:3 — então a barra que sobra é sempre letterbox (topo/base), nunca pillarbox. Testes:tests/test_scale.py(fit_scale— janela mais larga → letterbox vertical, mais alta → letterbox horizontal, e a invariante de que o canvas escalado nunca excede a janela).tests/test_game.py::test_screen_uses_fixed_logical_resolution_via_scaled(confirmagame.screen.get_size() == (480, 720), independente de plataforma).tests/test_input.py::test_finger_tap_outside_logical_area_is_ignored(toque na barra de letterbox é ignorado). Suíte completa (86 testes) verde;ruff check/ruff format --check/ty checksem violações. Validado comuv run main.py(driver de vídeo real, não headless): janela abre em 480×720 sem exceção. Não verificável neste ambiente: confirmar visualmente em celular Android real que a orientação fica travada em retrato e que não sobra pillarbox nas laterais (mesma limitação da seção 25) — fica pendente de validação manual do dono do projeto.
Checklist de verificação manual (task 13 + tarefas adicionais)¶
Itens marcados [x] foram validados automaticamente (suíte pytest ou scripts de
verificação usados durante o desenvolvimento das tasks 1–19), não por um humano
jogando de fato. Recomenda-se uma passada manual real antes de publicar, em especial
para os itens de áudio e controle físico (marcados abaixo), que dependem de
percepção humana e de hardware que não está disponível neste ambiente.
- [x] Flap responde a ESPAÇO, ↑, clique e botão A do controle (R1.1, R10.3) — testado com eventos simulados; botão A do controle não testado com hardware real
- [x] Start pausa, Y muta; conectar/desconectar controle durante o jogo não trava (R10.2, R10.3) — hotplug testado com joystick simulado, não com controle físico
- [x] Jogo funciona normalmente sem controle conectado (R10.5)
- [x] Pássaro não morre no teto, morre no chão e nas colunas (R1.4, R3.1); colisão com coluna só ocorre no bloco visível (R3.1, task 14)
- [x] Ponto único por coluna, com som (R4.1) — som verificado via chamada de
play(), qualidade sonora não avaliada por ouvido humano - [x] Recorde sobrevive a reinício do jogo (R4.3, R4.4)
- [x] Biomas trocam em 10 e 25 pontos com fade e banner (R5); nuvens/colinas do Overworld são quadriculadas, não elipses (R7.1, R7.4, task 15)
- [x] Pausa/despausa com ESC/P; reinício após game over (R6)
- [x] Partículas na colisão (R3.2); mudo com M (R8.3)
- [x] 60 FPS estáveis (~4.6 ms/frame médio, calibração da task 12); inicia com
uv run main.pyapósuv sync(R9) - [x] Créditos ("por Douglas e Pedro") visíveis na tela PRONTO e no título da janela (R11)
- [x] Recorde exibido no rodapé da tela PRONTO, acima do chão (R12)
- [x]
uv run pyinstaller BlockyBird.specgera um executável que abre com duplo clique, sem Python instalado (R13.1) — testado localmente no Windows; build Linux não testado neste ambiente (sem Linux disponível) - [x] Publicar uma Release de teste no GitHub e confirmar que os assets
BlockyBird-windows-<tag>.zipeBlockyBird-linux-<tag>.tar.bz2aparecem automaticamente (R13.2) — Releasev1.0.0publicada emdouglaspands/blocky-bird-game, comBlockyBird-linux-v1.0.0.tar.bz2eBlockyBird-windows-v1.0.0.zipanexados automaticamente pelogithub-actions[bot], confirmado via API pública do GitHub
Checklist de verificação da v2 (Android)¶
Ver design seção 25 para o motivo da separação. Os itens de hardware real não podem ser validados no ambiente de desenvolvimento e exigem teste manual do dono do projeto — não marcar sem ter testado de fato no aparelho.
Verificável automaticamente / no desktop:
- [x] Todas as telas renderizam com a fonte bitmap própria, sem
SysFont, mantendo alinhamento (R7.6) — confirmado por inspeção (grep SysFont src/: só ocorre em comentários explicativos, nenhum uso real) e visualmente durante a task 20 - [x] Redimensionar a janela do desktop reamostra o letterbox automaticamente (
pygame.SCALED), preservando a proporção do jogo (não da tela) e sem distorcer nem deslocar o gameplay (R9.1, R14.3) — validado com janela real redimensionada ao vivo para tamanhos bem diferentes da base 480×720 - [x] Toque/clique em coordenada normalizada converte corretamente para o espaço lógico, respeitando o letterbox; toque na barra é ignorado (R15.1) —
tests/test_input.py::test_finger_tap_in_game_area_flaps,test_finger_tap_outside_logical_area_is_ignored - [x]
storage.save_dir()devolve o caminho Android quandoANDROID_ARGUMENTestá definido e o caminho do projeto quando não está (R4.5) —tests/test_storage.py - [x] Recorde é gravado no momento em que o score ultrapassa o recorde, não só no GAME_OVER (R4.3, R16.4) —
tests/test_game.py::test_highscore_saved_incrementally_mid_round - [x] Ação
backpausa em JOGANDO e encerra nos outros estados (R15.2, R15.3) —tests/test_game.py::test_back_in_* - [x] Perder foco da janela (alt-tab) leva JOGANDO → PAUSADO e não retoma sozinho (R16.1, R16.2) —
tests/test_game.py::test_focus_lost_* - [x]
K_RETURNdispara flap (equivalente ao botão central de controle remoto) (R14.4) —tests/test_input.py::test_return_and_kp_enter_flap - [x] Suíte
pytestcompleta continua passando após a migração parapygame-ce(R9.2) — 65 testes,SDL_VIDEODRIVER=dummy uv run pytest - [x] A área jogável é sempre 480×720 fixo, em qualquer aparelho (calibração de dificuldade da task 12 nunca muda);
pygame.SCALEDaplica letterbox automático sem cortar a imagem, e a orientação fica travada em retrato no Android (R14.3, R14.4) —tests/test_scale.py,tests/test_game.py::test_screen_uses_fixed_logical_resolution_via_scaled
Requer aparelho Android real (celular):
- [x] APK instala por download direto, com "fontes desconhecidas", sem descompactar nada (R17.1, R17.2)
- [x] Toque em qualquer ponto faz o pássaro voar; iniciar e reiniciar funcionam por toque (R15.1)
- [x] Ícone de mudo responde ao toque e silencia de fato (R15.4)
- [x] BACK pausa durante o jogo e encerra o app nas telas de PRONTO/PAUSADO/GAME_OVER (R15.2, R15.3)
- [x] Trocar de app / receber ligação pausa automaticamente; ao voltar continua pausado (R16.1, R16.2)
- [x] Recorde sobrevive a fechar e reabrir o app, e a encerramento forçado pelo sistema (R4.5, R16.4)
- [x] A orientação fica travada em retrato mesmo girando o aparelho, e a imagem preenche a tela sem pillarbox nas laterais (letterbox no topo/base é aceitável, desde que a imagem inteira continue visível) (R14.3, R14.4)
- [x] Áudio sem estouros/crepitação (R8.4, buffer do mixer)
- [x] 60 FPS em aparelho de entrada (R9.5)
- [x] Ícone do launcher mostra a abelha inteira, sem cortar antenas/asas, em qualquer forma de máscara do fabricante (R21.4, R21.5) — task 37
Tarefas adicionais da v2 (pós-Android) — Qualidade¶
Aumento de escopo pedido depois que as tasks 20–30 (Android) já estavam concluídas: conformidade com ruff em todo o código Python, calibração do tamanho de fonte para eliminar sobreposição de texto nas telas, correção de bug de persistência do recorde no executável empacotado, e conformidade com ty (checagem de tipos).
-
[x] 31. Conformidade com Ruff Adicionar
ruffcomo dependência de dev (uv add --dev ruff), configurar[tool.ruff]empyproject.toml(line-length = 110,target-version = "py310", regrasE, F, W, I, UP, B, SIM, RUF, ver design seção 26). Rodaruv run ruff check --fix .euv run ruff format .sobremain.py,src/,tests/,scripts/,p4a-recipes/; revisar manualmente qualquer correção automática que mude comportamento (não só estilo) antes de aceitar. Corrigir à mão o que--fixnão resolver. Criar.github/workflows/ci.yml(on: push, pull_request) rodandoruff check,ruff format --checkepytest(SDL_VIDEODRIVER=dummy) no mesmo job. Confirmaruv run pytestcompleto continua verde após as correções de lint. (R18) Ajuste feito na implementação:uv run ruff format .reformata por padrão também blocos de código Python dentro de cercas```pythonem arquivos.md(comportamento desta versão do ruff, 0.16.0) — isso reformatouspecs/v1/design.md, violando a regra do próprio projeto de nunca editar pastas de versões anteriores já concluídas (specs/README.md). Corrigido comextend-exclude = ["specs"]em[tool.ruff], restringindo a varredura de fato ao código do jogo (o escopo já pretendido pelo design, que nunca mencionavaspecs/). Violações reais encontradas (15 no total, 7 corrigidas por--fix, 8 à mão): auto-fix foi só estilo (ordenação de imports emp4a-recipes/jpeg/__init__.py,.format()→ f-string,noqaórfão,typing.Callable→collections.abc.Callableemdecor.py). À mão:RUF012(atributos de classe mutáveisbuilt_libraries/dependsnas receitas p4a — anotados comClassVar, já que são o padrão de configuração do próprio framework p4a, não um bug real);SIM115(doisopen()sem context manager emp4a-recipes/pygame-ce/__init__.py— convertidos parawith);B905(doiszip()semstrict=emsrc/biome.pyescripts/generate_app_icon.py— ambos combinam tuplas RGB de tamanho fixo e igual,strict=Trueé correto e não muda comportamento);E501(duas docstrings de uma linha emsrc/biome.py/src/pipes.pyacima de 110 colunas — quebradas em docstring de duas linhas, sem alterar o texto). Suíte completa (65 testes) permanece verde após todas as correções. -
[x] 32. Calibração de tamanho de fonte sem sobreposição Implementar
ui._stack()(empilhamento vertical por altura real de linha, ver design seção 27) e migrardraw_ready_screen,draw_paused_overlayedraw_game_over_screenpara usá-lo em vez dos deltas fixos em pixels atuais. Revisar visualmente (uv run main.py) obase_size/escala de cada papel de texto (título, créditos, instrução, HUD, overlay de pausa, textos de game over, recorde) até nenhuma sobreposição ser visível em nenhuma das quatro telas, registrando os valores finais escolhidos. Criartests/test_ui_layout.pycom um teste por tela que renderiza os textos reais (incluindoRECORDE: 999999para o caso de recorde com muitos dígitos) e assere que nenhum par de retângulos se sobrepõe e que todos ficam dentro de[20, SCREEN_W - 20]horizontalmente e fora da área do chão verticalmente; incluir o retângulo do ícone de mudo (ui.MUTE_ICON_RECT) na checagem das telas onde ele aparece. (R19)ui._stack(surface, center_x, top_y, lines, margin=8)implementado: recebe(text, base_size, color)na ordem de exibição, calcula a escala real de cada linha via_fit_scale/_scale_forjá existentes, delega o desenho adraw_text(mesma função usada fora do stack, o que manteve um único ponto de instrumentação para os testes) e avança a posição vertical porGLYPH_H * escala + SHADOW_OFFSET + margin— nunca por uma constante escolhida a olho.base_sizefinais escolhidos (calibração visual via screenshots offscreen com as telas reais, incluindoRECORDE: 999999): título (BLOCKY BIRD/PAUSADO/GAME OVER)base_size=12(escala 6, contra 18/escala 9 antes); texto secundário de destaque (PONTOS/RECORDEno game over)base_size=8(escala 4, igual ao recorde da tela PRONTO); instrução/dica (ESPACO / CLIQUE PARA VOAR,ESPACO / CLIQUE PARA REINICIAR,SETAS: MUDO)base_size=5-6(escala 2-3); créditosbase_size=5(escala 2); HUD de scorebase_size=12(escala 6, reduzido de 20/escala 10 — o valor antigo já quase tocava o topo da tela). Nenhuma sobreposição visível nas quatro telas nem no pior caso (RECORDE: 999999); confirmado também com um smoke test real (uv run main.py) sem exceptions.tests/test_ui_layout.py:_rects_for_screenmonkeypatchaui.draw_text(interceptado também dentro de_stack, já que é a mesma função do módulo) para capturar, por linha, umpygame.Rectde texto+sombra sem duplicar a lógica de renderização. Um teste por tela (PRONTO, HUD, PAUSADO, GAME_OVER) chama isso com os textos reais — incluindo999999— e verifica ausência de sobreposição (incluindo contraui.MUTE_ICON_RECT, presente em todas as telas poisgame.pyo desenha incondicionalmente) e que cada retângulo de texto fica em[20, SCREEN_W - 20]horizontalmente e acima deGROUND_Yverticalmente.MUTE_ICON_RECTentra só na checagem de colisão, não na de limites horizontais — ele fica a propósito perto da borda direita (dentro da margem de 14px do ícone, não da margem de 20px do texto). Suíte completa: 69 testes (65 + 4 novos),SDL_VIDEODRIVER=dummy uv run pytest. -
[x] 33. Corrigir persistência do recorde no executável Windows/Linux empacotado Bug reportado pelo dono do projeto: no executável Windows gerado por
BlockyBird.spec, o recorde deixou de persistir ehighscore.jsonnão era mais criado ao lado do.exe. Causa raiz:storage.save_dir()resolvia o diretório desktop a partir dePath(__file__).resolve().parent.parent, mas o PyInstaller empacota em modo onefile (EXE(pyz, a.scripts, a.binaries, a.datas, ...)numa única chamada), e nesse modo__file__do módulo aponta para o diretório temporário de extração (sys._MEIPASS), apagado ao fechar o processo — nunca para a pasta real do executável. O mesmo problema afeta o build Linux (mesmo.spec), ainda não reportado. Corrigido emstorage.pycomis_frozen()(checasys.frozen, atributo que o PyInstaller injeta em runtime) e, quando verdadeiro,save_dir()retornaPath(sys.executable).resolve().parentem vez do caminho baseado em__file__; Android (is_android()) e desktop rodando de fonte (uv run main.py) continuam com o comportamento anterior. Ver design seção 23. (R4.5) Decisão de design confirmada com o dono do projeto: gravar ao lado do executável (não em um diretório padrão de dados do SO como%APPDATA%), já que a distribuição é um zip/tar portátil (R13.2) e não uma instalação em local somente-leitura comoProgram Files. Validado:tests/test_storage.py::test_save_dir_frozen_desktop_uses_executable_dir(simulasys.frozen/sys.executableviamonkeypatch); suíte completa (70 testes) verde. Build real viauv run pyinstaller BlockyBird.spece verificação manual emdist/BlockyBird.exe: comsys.frozen/sys.executablesimulados apontando para o executável gerado,storage.save_dir()resolve paradist/,score.save_highscore()criadist/highscore.jsonao lado do.exeescore.load_highscore()recupera o valor salvo corretamente. -
[x] 34. Conformidade com ty (checagem de tipos) Adicionar
tycomo dependência de dev (uv add --dev ty), configurar[tool.ty.environment]/[tool.ty.src]empyproject.toml(python-version = "3.10",exclude = ["p4a-recipes", "specs", ".buildozer", "build", "dist"], ver design seção 28). Rodaruv run ty check .sobremain.py,src/,tests/,scripts/; corrigir violações reais no código, suprimir com# ty: ignore[regra]+ comentário curto só quando a causa é uma limitação do checador (módulo só resolvível em runtime de outra plataforma, atributo dinâmico em teste). Adicionar stepty check .em.github/workflows/ci.yml, entreruff format --checkepytest. Confirmaruv run pytesteruff check/ruff format --checkcontinuam verdes. (R20)p4a-recipes/excluído do escopo: diferente doruff(task 31, que inclui as receitas por serem código próprio do projeto),typrecisa resolver imports de verdade — e as receitas importamsh/pythonforandroid.*, pacotes que só existem dentro da imagem Docker do buildozer (R17), nunca no.venvlocal. Incluí-las geraria sóunresolved-importpermanente e não-acionável. Violações reais encontradas (3, todas corrigidas no código, nenhuma suprimida):src/biome.py(_lerp_color) esrc/particles.py(ParticleSystem.burst) construíam uma cor RGB a partir de expressão de tamanho variável (genexpr / slice depygame.Color) e atribuíam atuple[int, int, int]—tyinferetuple[int, ...]para as duas formas; corrigido desempacotando em variáveis nomeadas e retornando/atribuindo um literal de 3-tupla.src/input.py(InputManager) anotavadict[int, pygame.joystick.Joystick], mas o próprio stub do pygame-ce documentaJoystickcomo função-fábrica (não classe) nesta versão da lib; corrigido usandopygame.joystick.JoystickType(o tipo real da instância) e removida a chamada redundantejoystick.init()(deprecated desde 2.0.0, a construção já inicializa). Supressões pontuais (2, com comentário explicando o motivo):src/storage.py—from android.storage import app_storage_pathdentro dotry/except ImportError(seção 23), módulo só existe em runtime p4a;# ty: ignore[unresolved-import].tests/test_storage.py— atribuição dinâmica de atributos a um faketypes.ModuleTypepara simular o móduloandroid.storageinjetado pelo p4a nos testes;# ty: ignore[unresolved-attribute]. Validado:uv run ty check .reporta zero diagnósticos; suíte completa (70 testes) eruff check/ruff format --checkpermanecem verdes.
Tarefas adicionais da v2 (pós-Android) — Ícone do aplicativo¶
Pedido do dono do projeto: o ícone do app deve ser a personagem do jogo (a abelha), e no Android o ícone precisa aparecer "com as proporções ajustadas" — sem cortar a personagem quando o launcher aplica sua máscara (círculo/squircle/quadrado arredondado). Ver design.md seção 29.
-
[x] 36. Geração do ícone por código e integração no desktop Criar
scripts/generate_app_icon.py(script standalone, reaproveitandotextures.make_bee): geraassets/app_icon_512.png(janela + fonte do.ico),assets/app_icon.ico(multi-resolução 16–256px, construído viastructda stdlib embutindo PNGs, sem depender de Pillow) eassets/android_icon_legacy.png. Criarsrc/assets.py::asset_path()(resolvesys._MEIPASSquandostorage.is_frozen(), raiz do projeto caso contrário) e chamarpygame.display.set_icon()emGame.__init__(src/game.py), envolvido emtry/exceptpara degradação graciosa. AtualizarBlockyBird.spec:datas=[('assets/app_icon_512.png', 'assets')]noAnalysiseicon='assets/app_icon.ico'noEXE. Testes para as funções puras do script (ajuste de escala, construção do.ico) e paraasset_path()nos dois ramos. Validar rodandouv run main.py(ícone na barra de título) e gerando o executável (uv run pyinstaller BlockyBird.spec, ícone no.exe/Explorer). (R21.1, R21.2, R21.3, R21.7) Implementado conforme o design (seção 29.1–29.3), sem desvios.make_composed_icon()desenha uma cena com céu do Overworld + faixa de grama/terra + abelha centralizada, reamostrada em escala nearest-neighbor (pixel-art fiel, R7.1); só o.icousasmoothscalenos tamanhos pequenos (16–48px), documentado no design como o único ponto onde a suavização é aceitável.build_ico()monta o containerICONDIR/ICONDIRENTRYmanualmente comstruct, embutindo um PNG por resolução (16/32/48/64/128/256) — validado por round-trip (pygame.image.loadde cada entrada extraída de volta) tanto no teste automatizado quanto porfile assets/app_icon.ico(reconhecido como "MS Windows icon resource" com 6 ícones).src/assets.py::asset_path(): replica o padrão já usado porstorage.py(seção 23), mas na direção oposta —sys._MEIPASSé onde o PyInstaller onefile extrai dados para leitura (correto aqui), ao contrário destorage.save_dir(), que evita_MEIPASSde propósito por ser efêmero (não serve para gravar o recorde, task 33).Game.__init__chamapygame.display.set_icon()envolvido emcontextlib.suppress(OSError, pygame.error)(troca de umtry/except: passpor sugestão doruff/SIM105) antes doset_mode, sem custo perceptível no Android (onde não há efeito visível, mas também não há necessidade de umif is_android()para pular —asset_path()já resolve para a raiz do projeto lá, e o arquivo existe no APK viasource.include_exts = py,png). Validado: suíte completa (87 testes = 81 anteriores + 6 novos,tests/test_assets.pyetests/test_generate_app_icon.py) verde;ruff check/ruff format --check/ty checksem violações. Smoke test real (não headless) deuv run main.pypor alguns segundos sem exceções. Build real viauv run pyinstaller BlockyBird.spec: log confirma"Copying icon to EXE";dist/BlockyBird.exeexecutado por alguns segundos sem exceções. Não verificado visualmente neste ambiente: confirmar a olho que o ícone da abelha aparece de fato na barra de título/taskbar e no Explorer (o smoke test só confirma ausência de erro, não a aparência) — recomenda-se uma checagem visual rápida pelo dono do projeto antes de publicar. -
[x] 37. Ícone adaptativo do Android no
buildozer.specEstenderscripts/generate_app_icon.pypara também gerarassets/android_icon_foreground.png(432×432, só a abelha, escalada para caber nos 66/108 dp da zona segura de máscara) eassets/android_icon_background.png(432×432, gradiente de céu do Overworld, opaco, sem a abelha). Adicionar aobuildozer.spec:icon.filename(aponta para o ícone legado da task 36, cobre API < 26),icon.adaptive_foreground.filenameeicon.adaptive_background.filename(cobrem API ≥ 26, R21.4–R21.6). Validarbuildozer.speccomo INI bem formado e as dimensões/canal alfa dos PNGs gerados; se Docker estiver acessível neste ambiente (ver design seção 25), rodar o build real do APK e confirmarBUILD SUCCESSFULcom as novas chaves. (R21.4, R21.5, R21.6, R21.7) As três camadas já foram geradas na task 36 (mesmoscripts/generate_app_icon.py, seção 29.1 do design) — esta task só adiciona as três chaves correspondentes no[app]dobuildozer.spec, seguindo o estilo de caminho relativo já usado no arquivo (sem%(source.dir)s, já quesource.dir = .torna as duas formas equivalentes aqui). Validado como INI bem formado viaconfigparser(mesma checagem das tasks 27/31). Docker estava acessível neste ambiente (imagemkivy/buildozer:latestjá em cache local,Buildozer 1.6.1.dev0) e o build real foi executado ponta a ponta, reaproveitando o cache de SDK/NDK persistido de uma sessão anterior (~/.buildozer-android-global-cache, ~2.6 GB) e o.buildozer/do projeto —BUILD SUCCESSFUL in 1m 36s, gerandobin/blockybird-0.2.0-armeabi-v7a_arm64-v8a_x86_64-debug.apk(62 MB). O comandop4ainvocado pelo buildozer (visível no log) confirma a tradução das três chaves do spec para as flags reais do python-for-android:--icon .../android_icon_legacy.png --icon-fg .../android_icon_foreground.png --icon-bg .../android_icon_background.png. Verificação real do conteúdo do APK (além do log de build), inspecionando o.apkextraído (é um zip):res/mipmap-anydpi-v26/icon.xmlexiste e contém as stringsadaptive-icon/background/foreground(XML binário compilado pelo aapt, confirmando um<adaptive-icon>de verdade, não um ícone comum);res/mipmap/icon_foreground.png(432×432, RGBA — canal alfa presente, confirmando a camada transparente) eres/mipmap/icon_background.png(432×432, RGB — sem alfa, confirmando a camada opaca) batem em dimensão e em tamanho de arquivo em bytes com os PNGs gerados porscripts/generate_app_icon.py(1277 e 1488 bytes respectivamente) — prova de que são exatamente os arquivos gerados, não um fallback ou um ícone padrão do template.res/mipmap/icon.png(2292 bytes) também bate comandroid_icon_legacy.png, confirmando o ícone legado (API < 26). Ainda não verificável neste ambiente: a composição final da máscara pelo launcher (círculo/squircle/quadrado arredondado) só é visível de fato num aparelho ou emulador Android real — o que foi verificado aqui é que os recursos corretos (as camadas certas, nos tamanhos certos, com/sem alfa conforme esperado) chegam ao APK; a etapa que falta é inteiramente do lado do sistema operacional Android no aparelho, fora do alcance deste ambiente (mesma limitação da seção 25). Recomenda-se instalar o APK gerado (ou o de uma Release futura) num celular e confirmar visualmente que a abelha aparece inteira, sem antenas/asas cortadas, em qualquer forma de ícone que o launcher use.