from js import document, window, Image, AudioContext from pyodide.ffi import create_proxy import random CORES = { "preto": "#000000", "branco": "#FFFFFF", "cinza": "#444444", "amarelo": "#FFCC00", "vermelho": "#FF0000", "dourado": "#FFD700", "verde": "#00FF00", "morcego_azul": "#1a1a3a", "neon_bat": "#00ffcc" } LARGURA, ALTURA = 800, 600 class JogoBlitz: def __init__(self): self.tela = document.getElementById("tela") if not self.tela: return self.tela.width = LARGURA self.tela.height = ALTURA self.ctx = self.tela.getContext("2d") self.audio_ctx = None self.motor_osc1 = self.motor_osc2 = None self.ganho_motor = None self.estado_jogo = 'MENU' self.opcao_menu_selecionada = 0 self.pisca_timer = 0 self.pontos, self.tempo_restante, self.frame_count = 0, 37, 0 self.fundo_y, self.v_base = 0, 5 self.v_cenario = self.v_base self.carro = {"w": 60, "h": 100, "x": 370, "y": 460, "v": 8} self.obstaculos, self.moedas, self.timer_spawn = [], [], 0 self.faixas_x = [212, 337, 462, 587] self.teclas_pressionadas = {} self.charadas_banco = [ {"pergunta": "Charada 1: O Batmóvel corre pela noite fria de Gotham. Para um observador estático na calçada, os faróis projetam luz à velocidade c. Para o Batman, acelerando rápido dentro do carro, a luz que sai parece ignorar seu próprio movimento. O tempo se dobra para manter o absoluto. O que explica esse fenômeno?", "opcoes": ["A) A velocidade da luz depende do movimento da fonte que a emite no espaço.", "B) O tempo passa mais rápido para o Batman do que para quem observa de fora.", "C) A velocidade da luz no vácuo é invariante, o que exige a dilatação do tempo.", "D) O movimento do carro comprime as ondas de luz, alterando sua velocidade final."], "correta": 2}, {"pergunta": "Charada 2: Uma miragem flutua sobre os tetos de Gotham. O reflexo de um crime distante surge no horizonte distorcido, enganando os olhos, como um Fata Morgana térmico. O calor do solo cria camadas de ar com diferentes densidades. Fisicamente, o que desvia os raios de luz criando a illusions?", "opcoes": ["A) A difração da luz ao contornar as quinas dos prédios mais altos.", "B) A refração contínua da luz devido à variação do índice de refração do ar.", "C) A reflection especular perfeita gerada pelas partículas de poluição suspensas.", "D) A dispersão da luz branca separando-se em suas componentes quânticas."], "correta": 1}, {"pergunta": "Charada 3: No laboratório secreto, o Cavaleiro das Trevas observa partículas subatômicas colidirem. Ele tenta prever a posição exata e a velocidade de um elétron ao mesmo tempo, mas percebe que a própria natureza esconde a precisão. Há uma incerteza que não é falha do sensor, mas uma lei da matéria. Que princípio é esse?", "opcoes": ["A) O Princípio da Exclusão de Pauli, que impede dois corpos de ocuparem o mesmo espaço.", "B) O Efeito Fotoelétrico, que mostra que a luz arranca elétrons de superfícies.", "C) O Princípio da Incerteza de Heisenberg, limitando a precisão simultânea de posição e momento.", "D) A Teoria das Cordas, que define que tudo vibra em dimensions ocultas e paralelas."], "correta": 2}, {"pergunta": "Charada 4: O Charada sabota os sistemas elétricos da cidade usando geradores que giram espiras condutoras dentro de campos magnéticos estáticos. Ao fazer o fluxo magnético variar no tempo através da bobina, uma corrente elétrica invisível nasce no fio para alimentar suas armadilhas. Qual lei física rege essa criação?", "opcoes": ["A) A Lei de Coulomb, que calcula a atração eletrostática entre duas cargas fixas.", "B) A Lei da Indução de Faraday, onde a variação do fluxo magnético induz uma força eletromotriz.", "C) A Primeira Lei de Ohm, que mantém a resistência constante em condutores lineares.", "D) O Efeito Joule, que transforma energia elétrica exclusivamente em energia térmica."], "correta": 1} ] self.charada_atual = None self.opcao_selecionada = 0 self.img_carro = Image.new(); self.img_carro.src = "./imagens/carro.png" self.img_barricada = Image.new(); self.img_barricada.src = "./imagens/barricadas01.png" self.img_moeda = Image.new(); self.img_moeda.src = "./imagens/moedavelocidade.png" self.img_einstein = Image.new(); self.img_einstein.src = "./imagens/einstein.png" self._proxy_keydown = create_proxy(self.tratar_keydown) self._proxy_keyup = create_proxy(self.tratar_keyup) self._proxy_loop = create_proxy(self.ciclo_animacao) document.addEventListener("keydown", self._proxy_keydown) document.addEventListener("keyup", self._proxy_keyup) # Mapeamento limpo dos botões por toque único self._proxy_btn_esq_down = create_proxy(lambda e: self.forcar_tecla("ArrowLeft", True)) self._proxy_btn_esq_up = create_proxy(lambda e: self.forcar_tecla("ArrowLeft", False)) self._proxy_btn_dir_down = create_proxy(lambda e: self.forcar_tecla("ArrowRight", True)) self._proxy_btn_dir_up = create_proxy(lambda e: self.forcar_tecla("ArrowRight", False)) self._proxy_btn_cima = create_proxy(lambda e: self.disparar_tecla_virtual("ArrowUp")) self._proxy_btn_baixo = create_proxy(lambda e: self.disparar_tecla_virtual("ArrowDown")) self._proxy_btn_enter = create_proxy(lambda e: self.disparar_tecla_virtual("Enter")) btn_esq = document.getElementById("btn-esq") btn_dir = document.getElementById("btn-dir") btn_cima = document.getElementById("btn-cima") btn_baixo = document.getElementById("btn-baixo") btn_acao = document.getElementById("btn-acao") if btn_esq and btn_dir and btn_cima and btn_baixo and btn_acao: btn_esq.addEventListener("touchstart", self._proxy_btn_esq_down) btn_esq.addEventListener("touchend", self._proxy_btn_esq_up) btn_dir.addEventListener("touchstart", self._proxy_btn_dir_down) btn_dir.addEventListener("touchend", self._proxy_btn_dir_up) btn_cima.addEventListener("touchstart", self._proxy_btn_cima) btn_baixo.addEventListener("touchstart", self._proxy_btn_baixo) btn_acao.addEventListener("touchstart", self._proxy_btn_enter) window.requestAnimationFrame(self._proxy_loop) JogoBlitz.instancia = self def forcar_tecla(self, tecla, status): self.teclas_pressionadas[tecla] = status def disparar_tecla_virtual(self, tecla_nome): class EventoSimulado: def __init__(self, key): self.key = key self.tratar_keydown(EventoSimulado(tecla_nome)) def ciclo_animacao(self, timestamp): self.rodar_jogo() window.requestAnimationFrame(self._proxy_loop) def iniciar_musica(self): try: if not self.audio_ctx: self.audio_ctx = AudioContext.new() self.motor_osc1 = self.audio_ctx.createOscillator() self.motor_osc1.type = "sawtooth" self.motor_osc1.frequency.setValueAtTime(45, self.audio_ctx.currentTime) self.motor_osc2 = self.audio_ctx.createOscillator() self.motor_osc2.type = "triangle" self.motor_osc2.frequency.setValueAtTime(45.5, self.audio_ctx.currentTime) self.ganho_motor = self.audio_ctx.createGain() self.ganho_motor.gain.setValueAtTime(0.18, self.audio_ctx.currentTime) self.motor_osc1.connect(self.ganho_motor) self.motor_osc2.connect(self.ganho_motor) self.ganho_motor.connect(self.audio_ctx.destination) self.motor_osc1.start() self.motor_osc2.start() except: pass def atualizar_rpm_motor(self): if self.ganho_motor and self.estado_jogo == 'JOGANDO': fator_rpm = self.v_cenario / self.v_base self.motor_osc1.frequency.setValueAtTime(42 * fator_rpm, self.audio_ctx.currentTime) def tocar_efeito_resposta(self, acertou): try: if not self.audio_ctx: self.audio_ctx = AudioContext.new() osc = self.audio_ctx.createOscillator() gn = self.audio_ctx.createGain() osc.frequency.setValueAtTime(450 if acertou else 110, self.audio_ctx.currentTime) osc.connect(gn); gn.connect(self.audio_ctx.destination) osc.start(); gn.gain.exponentialRampToValueAtTime(0.001, self.audio_ctx.currentTime + 0.2) except: pass def parar_musica(self): try: if self.motor_osc1: self.motor_osc1.stop() if self.motor_osc2: self.motor_osc2.stop() except: pass def tratar_keydown(self, e): self.teclas_pressionadas[e.key] = True if self.estado_jogo == 'MENU': if e.key in ["ArrowUp", "ArrowDown", "w", "s"]: self.opcao_menu_selecionada = 1 - self.opcao_menu_selecionada self.tocar_efeito_resposta(True) elif e.key in ["Enter", " "]: if self.opcao_menu_selecionada == 0: self.estado_jogo = 'JOGANDO' self.iniciar_musica() else: self.reiniciar() elif self.estado_jogo == 'GAMEOVER': if e.key.lower() == "r" or e.key in ["Enter", " "]: self.reiniciar() self.estado_jogo = 'MENU' elif self.estado_jogo == 'CHARADA': if e.key in ["ArrowUp", "w"]: self.opcao_selecionada = (self.opcao_selecionada - 1) % 4 self.tocar_efeito_resposta(True) elif e.key in ["ArrowDown", "s"]: self.opcao_selecionada = (self.opcao_selecionada + 1) % 4 self.tocar_efeito_resposta(True) elif e.key == "Enter": self.verificar_resposta() def tratar_keyup(self, e): self.teclas_pressionadas[e.key] = False def reiniciar(self): self.parar_musica() self.estado_jogo = 'MENU' self.pontos, self.tempo_restante, self.frame_count = 0, 37, 0 self.v_cenario = self.v_base self.carro["x"] = 370 self.obstaculos.clear(); self.moedas.clear() def gerenciar_objetos(self): if self.estado_jogo != 'JOGANDO': return self.timer_spawn += 1 if self.timer_spawn >= 60: self.timer_spawn = 0 fx = random.choice(self.faixas_x) if random.random() < 0.6: self.obstaculos.append({"x": fx - 25, "y": -50, "w": 50, "h": 50}) else: self.moedas.append({"x": fx - 20, "y": -40, "w": 40, "h": 40}) for o in self.obstaculos: o["y"] += self.v_cenario for m in self.moedas: m["y"] += self.v_cenario self.checar_colisoes() self.obstaculos = [o for o in self.obstaculos if o["y"] < ALTURA] self.moedas = [m for m in self.moedas if m["y"] < ALTURA] def checar_colisoes(self): cx, cy, cw, ch = self.carro["x"], self.carro["y"], self.carro["w"], self.carro["h"] for m in self.moedas[:]: if cx < m["x"] + m["w"] and cx + cw > m["x"] and cy < m["y"] + m["h"] and cy + ch > m["y"]: self.moedas.remove(m) self.estado_jogo = 'CHARADA' self.charada_atual = random.choice(self.charadas_banco) self.opcao_selecionada = 0 for o in self.obstaculos[:]: if cx < o["x"] + o["w"] and cx + cw > o["x"] and cy < o["y"] + o["h"] and cy + ch > o["y"]: self.obstaculos.remove(o) self.pontos = max(0, self.pontos - 10) self.tocar_efeito_resposta(False) def verificar_resposta(self): if self.opcao_selecionada == self.charada_atual["correta"]: self.pontos += 100 self.v_cenario = self.v_base + 5 self.tocar_efeito_resposta(True) else: self.tocar_efeito_resposta(False) self.estado_jogo = 'JOGANDO' def desenhar_texto_multi_linha(self, texto, x, y, larg_max, espaco): palavras = texto.split(' ') linha = '' for p in palavras: testar = linha + p + ' ' if self.ctx.measureText(testar).width > larg_max and linha != '': self.ctx.fillText(linha, x, y) linha = p + ' ' y += espaco else: linha = testar self.ctx.fillText(linha, x, y) return y def desenhar_menu_cavernudo(self): self.pisca_timer = (self.pisca_timer + 1) % 40 self.ctx.fillStyle = CORES["preto"]; self.ctx.fillRect(0, 0, LARGURA, ALTURA) self.ctx.strokeStyle = "rgba(0, 255, 200, 0.04)"; self.ctx.lineWidth = 2 for y in range(0, ALTURA, 8): self.ctx.beginPath(); self.ctx.moveTo(0, y); self.ctx.lineTo(LARGURA, y); self.ctx.stroke() self.ctx.strokeStyle = CORES["morcego_azul"]; self.ctx.lineWidth = 6; self.ctx.strokeRect(20, 20, LARGURA-40, ALTURA-40) self.ctx.strokeStyle = CORES["neon_bat"]; self.ctx.lineWidth = 2; self.ctx.strokeRect(28, 28, LARGURA-56, ALTURA-56) self.ctx.textAlign = "center"; self.ctx.fillStyle = CORES["amarelo"] self.ctx.font = "bold 42px 'Courier New', monospace" self.ctx.fillText("BATMAN: BLITZ", LARGURA / 2, 130) self.ctx.fillStyle = CORES["branco"]; self.ctx.font = "bold 24px 'Courier New', monospace" self.ctx.fillText("C I E N T Í F I C A", LARGURA / 2, 185) self.ctx.fillStyle = CORES["morcego_azul"]; self.ctx.fillRect(150, 230, 500, 4) y_opcoes = 340 opcoes = [" [ INICIAR SISTEMAS ] ", " [ RECALIBRAR BATMÓVEL ] "] for i, opt in enumerate(opcoes): if i == self.opcao_menu_selecionada: self.ctx.font = "bold 24px 'Courier New', monospace" if self.pisca_timer < 20: self.ctx.fillStyle = CORES["neon_bat"] self.ctx.fillText(f"▶ {opt} ◀", LARGURA / 2, y_opcoes + (i * 70)) else: self.ctx.fillStyle = CORES["amarelo"] self.ctx.fillText(f" {opt} ", LARGURA / 2, y_opcoes + (i * 70)) else: self.ctx.font = "20px 'Courier New', monospace" self.ctx.fillStyle = "rgba(255, 255, 255, 0.4)" self.ctx.fillText(opt, LARGURA / 2, y_opcoes + (i * 70)) self.ctx.font = "12px 'Courier New', monospace"; self.ctx.fillStyle = CORES["cinza"] self.ctx.fillText("SISTEMA OPERACIONAL ATIVO - MOBILE DISPONÍVEL", LARGURA / 2, 530) def desenhar_tela_charada(self): self.ctx.fillStyle = "rgba(10, 10, 28, 0.97)"; self.ctx.fillRect(30, 20, 740, 560) self.ctx.strokeStyle = CORES["amarelo"]; self.ctx.lineWidth = 5; self.ctx.strokeRect(30, 20, 740, 560) self.ctx.fillStyle = CORES["branco"]; self.ctx.font = "bold 16px Arial"; self.ctx.textAlign = "center" u_y = self.desenhar_texto_multi_linha(self.charada_atual["pergunta"], LARGURA / 2, 120, 660, 22) y_opt = max(u_y + 30, 270) for i, opt in enumerate(self.charada_atual["opcoes"]): if i == self.opcao_selecionada: self.ctx.fillStyle = CORES["amarelo"]; self.ctx.font = "bold 15px Arial" self.desenhar_texto_multi_linha(f">>> {opt.upper()}", LARGURA / 2, y_opt + (i * 65), 660, 20) else: self.ctx.fillStyle = CORES["branco"]; self.ctx.font = "14px Arial" self.desenhar_texto_multi_linha(opt, LARGURA / 2, y_opt + (i * 65), 660, 20) def rodar_jogo(self): if self.estado_jogo == 'MENU': self.desenhar_menu_cavernudo() return self.ctx.fillStyle = CORES["preto"]; self.ctx.fillRect(0, 0, LARGURA, ALTURA) if self.estado_jogo == 'JOGANDO' or self.estado_jogo == 'CHARADA': if self.estado_jogo == 'JOGANDO': if (self.teclas_pressionadas.get("ArrowLeft") or self.teclas_pressionadas.get("a")) and self.carro["x"] > 150: self.carro["x"] -= self.carro["v"] if (self.teclas_pressionadas.get("ArrowRight") or self.teclas_pressionadas.get("d")) and self.carro["x"] < (650 - self.carro["w"]): self.carro["x"] += self.carro["v"] self.fundo_y = (self.fundo_y + self.v_cenario) % 40 self.frame_count += 1 if self.frame_count >= 60: self.frame_count = 0 self.tempo_restante -= 1 if self.tempo_restante <= 0: self.estado_jogo = 'GAMEOVER' self.atualizar_rpm_motor() self.gerenciar_objetos() self.ctx.fillStyle = CORES["cinza"]; self.ctx.fillRect(150, 0, 500, ALTURA) self.ctx.fillStyle = CORES["branco"]; self.ctx.fillRect(145, 0, 5, ALTURA); self.ctx.fillRect(650, 0, 5, ALTURA) self.ctx.fillStyle = CORES["amarelo"] for y in range(-40, ALTURA + 40, 40): self.ctx.fillRect(272, y + self.fundo_y, 6, 20) self.ctx.fillRect(397, y + self.fundo_y, 6, 20) self.ctx.fillRect(522, y + self.fundo_y, 6, 20) for o in self.obstaculos: if self.img_barricada.complete and self.img_barricada.naturalWidth != 0: self.ctx.drawImage(self.img_barricada, o["x"], o["y"], o["w"], o["h"]) else: self.ctx.fillStyle = CORES["vermelho"]; self.ctx.fillRect(o["x"], o["y"], o["w"], o["h"]) for m in self.moedas: if self.img_moeda.complete and self.img_moeda.naturalWidth != 0: self.ctx.drawImage(self.img_moeda, m["x"], m["y"], m["w"], m["h"]) else: self.ctx.fillStyle = CORES["dourado"]; self.ctx.fillRect(m["x"], m["y"], m["w"], m["h"]) if self.img_carro.complete and self.img_carro.naturalWidth != 0: self.ctx.drawImage(self.img_carro, self.carro["x"], self.carro["y"], self.carro["w"], self.carro["h"]) else: self.ctx.fillStyle = CORES["verde"]; self.ctx.fillRect(self.carro["x"], self.carro["y"], self.carro["w"], self.carro["h"]) self.ctx.fillStyle = CORES["branco"]; self.ctx.font = "bold 22px Arial"; self.ctx.textAlign = "left"; self.ctx.fillText(f"PONTOS: {self.pontos}", 20, 40) self.ctx.textAlign = "right"; self.ctx.fillText(f"TEMPO: {self.tempo_restante // 60}:{self.tempo_restante % 60:02d}", LARGURA - 20, 40) if self.estado_jogo == 'CHARADA': self.desenhar_tela_charada() elif self.estado_jogo == 'GAMEOVER': self.ctx.fillStyle = CORES["branco"]; self.ctx.font = "bold 32px 'Courier New', monospace"; self.ctx.textAlign = "center" self.ctx.fillText("SISTEMA DIRECIONADO: FIM", LARGURA / 2, ALTURA / 2 - 40) self.ctx.fillStyle = CORES["amarelo"]; self.ctx.fillText(f"Carga de Dados: {self.pontos} Pts", LARGURA / 2, ALTURA / 2 + 20) self.ctx.fillStyle = CORES["neon_bat"]; self.ctx.font = "18px 'Courier New', monospace" self.ctx.fillText("Pressione o botão OK para reiniciar", LARGURA / 2, ALTURA / 2 + 90) def inicializar_seguro(evento=None): JogoBlitz() try: if document.readyState == "complete" or document.readyState == "interactive": inicializar_seguro() else: document.addEventListener("DOMContentLoaded", create_proxy(inicializar_seguro)) except: JogoBlitz()