chrome://dino ao vivo pelo DevTools Protocol. Este artigo mostra o que aconteceu, os comandos usados e o que só apareceu quando testamos de verdade.
3 IAs no Dino do Google
NEAT, DQN e PPO — e o terminal que joga o chrome://dino de verdade
O que você vai ver
- Como NEAT, DQN e PPO aprendem (e no que diferem)
- Por que DQN e PPO travaram em ~1.400 pontos
- O terminal que conecta a IA ao Chrome real
- Os comandos do console do DevTools, prontos para copiar
- Os 12 testes e os bugs que só apareceram ao vivo
O que aconteceu no vídeo?
Três formas diferentes de aprender
As três IAs recebem o mesmo jogo, os mesmos sensores e as mesmas três ações (correr, pular ou abaixar). O que muda é como cada uma descobre o que fazer. Uma evolui como uma espécie; as outras duas ajustam pesos por gradiente, recompensa a recompensa.
NEAT
NeuroEvolution of Augmenting Topologies
- • Uma população de dinossauros joga ao mesmo tempo
- • Quem corre mais tem mais fitness e vira "pai"
- • Filhos herdam pesos com mutações — inclusive na estrutura da rede
- • Não usa gradiente nem recompensa por quadro
DQN
Deep Q-Network · 10 → 256 → 256 → 3
- • Um único agente treina sozinho
- • A rede dá uma nota Q (recompensa futura) para cada ação; vence a maior
- • Cada tentativa vai para uma memória de experiências e é revisitada
- • Alvo:
recompensa + 0,99 × melhor Q futuro
PPO
Proximal Policy Optimization · 10 → 128 → 128 → Ator | Crítico
- • Uma base compartilhada e duas cabeças
- • O Ator calcula probabilidades e escolhe a ação
- • O Crítico estima quanto o estado vale (V(s))
- • Clipping de 0,8× a 1,2×: a política nunca muda bruscamente
Resumo em uma frase
NEAT procura no espaço de redes com muitos candidatos ao mesmo tempo e seleciona pelo resultado do episódio inteiro. DQN e PPO treinam um agente só e dependem de tropeçar em boas jogadas por exploração aleatória — e é exatamente aí que a história fica interessante.
O que as IAs enxergam?
Nenhuma delas vê pixels. A engine do jogo (Jogo/dino_env.py) entrega um vetor de 10 números normalizados, no estilo do OpenAI Gym, e espera de volta uma ação discreta: 0 correr, 1 pular, 2 abaixar.
As 10 entradas
- 0 · dino_y — altura do dino / 700
- 1 · speed — velocidade / 13,0
- 2 · dist1 — distância do 1º obstáculo / 1400
- 3 · height1 — altura do 1º obstáculo
- 4 · type1 — 0 cacto · 0,5 pássaro · 1 gigante
- 5 · width1 — largura do 1º obstáculo / 150
- 6 · dist2 — distância do 2º obstáculo
- 7 · height2 — altura do 2º obstáculo
- 8 · type2 — tipo do 2º obstáculo
- 9 · is_jumping — 1 se está no ar
A reescala para DQN e PPO
Normalizar tudo pelo tamanho da tela espreme a informação útil numa fatia minúscula: a zona onde o pulo é decidido cabe em 0,029 – 0,038 da entrada dist1. Para uma rede treinada por gradiente, partindo de pesos pequenos, esse sinal é quase invisível.
Uma sonda supervisionada mostrou o tamanho do problema: com o estado cru, a rede acertava 4% dos pulos; com o estado reescalado (rl_features.py), 100%. O NEAT nunca precisou disso — a evolução seleciona pesos grandes (até ±30) que amplificam essas fatias sozinha.
Por que DQN e PPO travaram em ~1.400 pontos…
E COMO DESTRAVAMOS
Mesmo depois de corrigir rede-alvo, escala de entrada, replay e recompensa, os dois ficaram parados. A medição explicou: o momento certo de pular é uma agulha no palheiro.
A agulha
Uma heurística que pula a 8× a velocidade faz 13.466 pontos. Com 7×, cai para 2.107; com 9×, 4.160. Variar ±12% derruba o desempenho de 3 a 6 vezes.
Partida a quente
Em vez de esperar a exploração aleatória tropeçar numa sequência perfeita, demos demonstrações (ideia do DQfD / behavior cloning): o replay começa cheio de trajetórias boas e a rede é pré-treinada — 100% de acerto nos pulos em ~5 épocas.
Salvar o campeão certo
Salvar quando o episódio bate recorde não funciona: a rede continua treinando durante o episódio. O DQN reportou recorde de 9.759, mas o arquivo salvo jogava 1.360. Solução: avaliar a política de forma gulosa (argmax) antes de salvar — exatamente como a Galeria e o Torneio reproduzem.
Tudo isso vive em três arquivos pequenos do projeto: rl_features.py (reescala), rl_warmstart.py (demonstrações) e rl_avaliacao.py (avaliação gulosa). O NEAT não passa por nenhum deles.
O Terminal: a Ponte Neural
Vencer no simulador é uma coisa. A pergunta do vídeo era outra: a campeã consegue jogar o Dino de verdade, o do Chrome? Para responder sem trapaça, construímos um terminal que não recria o jogo e não simula física paralela. Ele lê o estado real do chrome://dino, converte para o vetor que a rede aprendeu, roda a inferência e envia teclas reais de volta para o Chrome.
ESPELHO
Só lê. Você joga no Chrome e o terminal mostra os sensores ao vivo, sem enviar nenhuma tecla.
MANUAL PELA PONTE
Você aperta as teclas no terminal e ele as repassa ao Chrome pelo CDP — prova que o caminho de comando funciona.
CAMPEÃO
A rede decide a ~60 Hz, no mesmo ritmo do treino. O HUD mostra decisão, comando enviado, confirmação do Chrome e o cronômetro "Tempo vivo".
crashed: 9 s, 3.072 px nesta partida.Como o terminal é organizado
- chrome_cdp_client.py — cliente do DevTools Protocol (websocket): lê o estado e envia teclas
- observation_adapter.py — converte o estado do Chrome no vetor de 8 floats do treino
- champion_adapter.py — carrega a campeã e expõe
decide(observation) → ação - visual_terminal.py — a janela Pygame: HUD, fluxo, pista, dino recolorido
- main.py — orquestra modos, teclado, thread de leitura a 60 Hz e log CSV
- smoke_test_bridge.py — os 12 testes de ponta a ponta (abaixo)
A rede real é minúscula
Ao desenhar a topologia da campeã NEAT (geração 53, fitness 294.640), descobrimos que ela tem 8 entradas disponíveis mas só usa 2 — a velocidade e a distância do primeiro obstáculo — com 2 neurônios ocultos, 3 saídas e 5 conexões no total. Os outros 6 sensores não têm peso nenhum ligado a eles. A evolução simplesmente descobriu que não precisava do resto.
Os comandos no console do Chrome
Antes de escrever a ponte, precisávamos de uma resposta simples: dá para ler o jogo por dentro? O Dino do Chrome é o projeto open-source t-rex-runner, e a instância do jogo costumava ficar em window.Runner.instance_. Costumava. Estes são os comandos, na ordem em que foram descobertos.
1. Abrir um Chrome com depuração remota
Um perfil isolado, em uma porta local. Nada do seu Chrome pessoal é tocado.
chrome.exe ^
--remote-debugging-port=9222 ^
--remote-debugging-address=127.0.0.1 ^
--remote-allow-origins=http://127.0.0.1:9222 ^
--user-data-dir="chrome_debug_profile" ^
--no-first-run --no-default-browser-check
2. Ver as abas abertas pelo protocolo
Se isso responder um JSON, o Chrome está ouvindo. Cada aba traz o seu webSocketDebuggerUrl.
http://127.0.0.1:9222/json/list
3. O caminho "clássico" — que não funciona mais
Na aba chrome://dino, abra o DevTools (F12 → Console). Nas versões atuais do Chrome (testado na 153), a classe do jogo não é mais exposta globalmente: só sobram enums como RunnerClasses.
window.Runner && (window.Runner.instance_ || window.Runner.getInstance?.())
4. O truque que achou o jogo
O jogo escuta keydown no document, e o listener registrado é a própria instância do Runner (ela implementa handleEvent). getEventListeners() é uma função exclusiva do Console — basta procurar o listener que tenha tRex e horizon.
globalThis.jogoDino = getEventListeners(document).keydown
.map(i => i.listener)
.find(l => l && l.horizon && l.tRex)
5. Ler o estado do jogo ao vivo
Com jogoDino na mão, tudo que a IA precisa está a um comando de distância — o dino, a velocidade, a distância e a lista de obstáculos.
// o dinossauro
({ xPos: jogoDino.tRex.xPos, yPos: jogoDino.tRex.yPos, jumping: jogoDino.tRex.jumping, ducking: jogoDino.tRex.ducking, groundYPos: jogoDino.tRex.groundYPos })
// ← {xPos: 24, yPos: 93, jumping: false, ducking: false, groundYPos: 93}
// a partida
({ velocidade: jogoDino.currentSpeed, distancia: Math.round(jogoDino.distanceRan), playing: jogoDino.playing, crashed: jogoDino.crashed })
// ← {velocidade: 6.559, distancia: 1736, playing: false, crashed: true}
// os obstáculos à frente
console.table(jogoDino.horizon.obstacles.map(o => ({ tipo: o.typeConfig.type, x: o.xPos, largura: o.width })))
// ← 'cactusSmall' x=58 largura=17 · 'cactusSmall' x=324 largura=34
// reiniciar depois do game over
jogoDino.restart()
window.Runner é undefined, mas o listener de keydown entrega a instância real.6. A mesma coisa, mas em Python (sem colar nada)
O terminal não pede para você abrir o Console. Runtime.evaluate aceita includeCommandLineAPI: true, que libera getEventListeners fora do DevTools. As teclas vão por Input.dispatchKeyEvent, e a navegação por Page.navigate (o Chrome moderno ignora chrome://dino passado por linha de comando).
# ler o estado (a expressão JS é a mesma do console, embrulhada em JSON.stringify)
ws.send(json.dumps({"id": 1, "method": "Runtime.evaluate", "params": {
"expression": JS_READ_STATE,
"returnByValue": True,
"includeCommandLineAPI": True, # libera getEventListeners fora do Console
}}))
# pular: uma tecla real, não um evento sintético na página
ws.send(json.dumps({"id": 2, "method": "Input.dispatchKeyEvent", "params": {
"type": "keyDown", "key": " ", "code": "Space", "windowsVirtualKeyCode": 32, "text": " "
}}))
# ... e "keyUp" só quando tRex.jumping voltar a False (teto de segurança: 0,7 s)
# abrir o jogo numa aba em branco
ws.send(json.dumps({"id": 3, "method": "Page.navigate", "params": {"url": "chrome://dino"}}))
7. Do Chrome para a rede: a tabela de conversão
O treino usa uma tela fixa de 1400×700; o canvas do Chrome tem tamanho variável. As distâncias e larguras são reescaladas pela proporção do canvas antes da fórmula original — a hipótese é que "fração da tela" é o que a rede realmente aprendeu.
| # | Entrada treinada | Origem no Chrome | Conversão |
|---|---|---|---|
| 0 | dino_y / 700 | tRex.yPos, tRex.jumping, tRex.ducking | Progresso do pulo (0 = chão, 1 = ápice) projetado na faixa do treino. Pico real medido: ~72 px. |
| 1 | speed / 13,0 | currentSpeed | Direta — 6,0 inicial e 13,0 máxima já eram cópias dos valores do Runner. |
| 2 | dist1 / 1400 | obstacles[0].xPos − tRex.xPos | × (1400 / largura do canvas), depois / 1400. |
| 3 | type1 | obstacle.typeConfig.type | Contém "pterodactyl" → 0,5; senão 0. O cacto gigante (1,0) não existe no Chrome. |
| 4 | width1 / 150 | obstacle.width | Mesma reescala do item 2, depois / 150. |
| 5 | dist2 | obstacles[1] | Idem 2. |
| 6 | type2 | obstacles[1].typeConfig.type | Idem 3. |
| 7 | is_jumping | tRex.jumping | Direta — a mesma flag booleana nos dois lados. |
Obstáculos já ultrapassados são ignorados nos dois lados (x + largura < tRex.xPos). Tudo é lido de forma defensiva: se um campo não existir nesta versão do Chrome — como tRex.config, que veio {}, ou height, que veio undefined — ele volta null e é reportado, nunca inventado.
Como saber se funcionava?
Ler código não prova nada contra um Chrome de verdade. Por isso a ponte tem uma bateria de 12 testes de ponta a ponta que precisam do Chrome aberto em chrome://dino. Cada um valida um elo da corrente: conexão, descoberta do jogo, leitura, envio de tecla, confirmação, carga da campeã, liberação das teclas e log.
pip install -r chrome_bridge/requirements_bridge.txt # pygame-ce, neat-python, websocket-client
chrome_bridge\launch_chrome_debug.bat # abre o Chrome isolado na porta 9222
python chrome_bridge\smoke_test_bridge.py # os 12 testes
python chrome_bridge\main.py # o terminal (1 espelho · 2 manual · 3 campeão)
[PASS] 1_conexao_porta_cdp — 4 alvo(s) encontrados em 127.0.0.1:9222
[PASS] 2_localizar_alvo_dino — chrome://dino já estava aberto (chrome://dino/)
[PASS] 3_descoberta_runner
[PASS] 4_leitura_estado — speed=6.035 tRex={'xPos': 24, 'yPos': 93, 'jumping': False, 'ducking': False, 'groundYPos': 93, 'config': {}} obstaculos=0
[PASS] 5_modo_espelho_sem_teclas — nenhuma tecla foi retida durante leituras somente-leitura
[PASS] 6_envio_manual_salto — Input.dispatchKeyEvent(Space) enviado sem exceção
[PASS] 7_pulo_so_apos_confirmacao_chrome — tRex.jumping ficou True após o comando
[PASS] 8_carregamento_campeao_io — NEAT (8 entradas) -> 2, PPO (10 entradas) -> 0, DQN (10 entradas) -> 2
[PASS] 9_inferencia_sem_alterar_checkpoint — nenhum checkpoint teve o mtime alterado
[PASS] 10_liberacao_teclas
[PASS] 11_persistencia_cor
[PASS] 12_criacao_log — chrome_bridge\logs\session_20260921_224723.csv
PASS=12 FAIL=0 SKIP=0
Detalhe honesto: se o Chrome estiver em game over quando a bateria rodar, o teste 6 é pulado e o 7 falha (não existe pulo para confirmar). Na primeira rodada de hoje foi exatamente isso — 10 PASS, 1 FAIL, 1 SKIP. Reiniciar a partida e rodar de novo dá os 12.
O diagnóstico do pulo
Uma dúvida clássica: segurar a tecla mais tempo pula mais alto? smoke_test_bridge.py --jump-diagnostic segura Espaço por 60, 150 e 300 ms e mede altura e tempo de voo pelo tRex.yPos:
Conclusão medida, não achismo: o pulo do Chrome tem altura e duração praticamente fixas (~72 px, ~160 ms). O número teórico da simulação (~83 px) foi substituído pelo medido no adaptador de observação.
Bugs que só apareceram ao vivo
NENHUM ERA VISÍVEL LENDO O CÓDIGO
A campeã jogava, mas morria muito mais rápido no Chrome do que no simulador. A investigação achou causas reais — todas dentro da ponte, nenhuma exigiu retreinar.
Decidia a 30 Hz, treinou a 60 Hz
A decisão estava presa ao relógio de desenho (30 fps). A cada decisão, o obstáculo já tinha andado o dobro do esperado. A inferência saiu do loop de desenho e passou para a thread de leitura, a ~60 Hz.
Um sleep de 60 ms a cada pulo
Segurar a tecla com time.sleep() síncrono travava leitura, decisão e desenho no momento mais crítico. A liberação passou a ser agendada e resolvida em cada leitura de estado.
Soltar só quando o Chrome pousar
Em vez de um número mágico de milissegundos, a tecla é liberada quando tRex.jumping volta a false (teto de 0,7 s, nunca atingido: o pulo real dura ~160 ms).
Aba oculta = jogo congelado
Se a janela do Chrome for minimizada ou totalmente coberta, o próprio Chrome pausa o requestAnimationFrame: distância parada, tRex.jumping travado. Numa gravação lado a lado isso não acontece — mas derrubou o teste automatizado.
%ProgramFiles(x86)% quebra o .bat
Parênteses no nome da variável abortam blocos if (...) do cmd.exe — o script fechava sem mostrar erro. Corrigido copiando o valor para uma variável sem parênteses antes de qualquer bloco.
Duas surpresas do próprio Chrome
chrome://dino por linha de comando é ignorado (abre "Nova guia") — a ponte navega sozinha via Page.navigate. E Input.enable não existe no protocolo: os comandos de teclado já funcionam direto.
| Mesma campeã, mesma máquina | Antes | Depois |
|---|---|---|
| Frequência de decisão | ~30 Hz | ~50–60 Hz |
| Duração da partida autônoma | 12 s | 25 s (sobreviveu o teste inteiro) |
| Distância percorrida | 3.192 px | 10.484 px |
Resultados e Aprendizados
- Busca populacional venceu a exploração aleatória. O ponto ótimo do pulo é uma agulha estreita; o NEAT a encontra selecionando pelo episódio inteiro, enquanto DQN e PPO só destravaram com demonstrações, reescala das entradas e avaliação gulosa antes de salvar.
- A rede campeã é mínima. 8 sensores disponíveis, 2 usados, 5 conexões. Evolução não adiciona o que não precisa.
-
Ler o jogo real é possível, mas o Chrome mudou.
window.Runnernão existe mais; o listener dekeydowné a porta de entrada, eincludeCommandLineAPIpermite usar o truque pelo protocolo, sem colar nada no Console. - A conversão de escala é uma hipótese, não um fato do código. O treino usa 1400×700 fixos; o canvas do Chrome varia. É a razão mais provável de a campeã morrer antes no Chrome do que no simulador — e um bom próximo experimento: treinar direto no jogo real, pela ponte.
-
Teste contra o sistema real. Cadência de decisão,
sleepbloqueante, aba oculta, parênteses no .bat: nenhum apareceu na leitura estática. Todos apareceram no smoke test.