import { avancar, criarPartida } from "./partida"; import { pilotar } from "./piloto"; import type { Partida, TipoZumbi, Zumbi } from "./tipos"; import { ESCRITORIO, ZUMBIS } from "./valores"; /** 21 x 15, parede por dentro em x = 10, de z = 5 a z = 9. */ const MAPA = Array.from({ length: 15 }, (_, z) => { if (z === 0 || z === 14) return "#".repeat(21); const miolo = Array.from({ length: 19 }, (_, i) => { if (z === 1 && i === 0) return "N"; if (z === 2 && i === 0) return "J"; return i === 9 && z >= 5 && z <= 9 ? "#" : "."; }); return `#${miolo.join("")}#`; }); const JX = 5.5; const JZ = 7.5; function zumbi( id: number, x: number, z: number, tipo: TipoZumbi = "comum", estado: Zumbi["estado"] = "andando", ): Zumbi { return { id, tipo, problemaId: "site-lento", x, z, rumo: 0, vida: ZUMBIS[tipo].vida, estado, tempo: 0, }; } /** Jogador em (5.5, 7.5), rumo 0, com os zumbis dados. */ function cena(...zumbis: Zumbi[]): Partida { const p = criarPartida({ semente: 1, dificuldade: "facil", problemaId: "site-lento", mapa: MAPA, }); p.jogador.x = JX; p.jogador.z = JZ; p.zumbis = zumbis; return p; } /** Zumbi a `distancia` metros do jogador, no rumo dado. */ function aoRumo( id: number, rumo: number, distancia: number, tipo: TipoZumbi = "comum", ): Zumbi { return zumbi( id, JX + Math.sin(rumo) * distancia, JZ + Math.cos(rumo) * distancia, tipo, ); } describe("pilotar: sem alvo", () => { it("fica parado sem zumbis", () => { expect(pilotar(cena())).toEqual({ mover: [0, 0], olhar: [0, 0], atirar: false, arma: 0, }); }); it("ignora zumbis morrendo", () => { const p = cena(zumbi(1, 5.5, 9.5, "comum", "morrendo")); expect(pilotar(p).atirar).toBe(false); expect(pilotar(p).mover).toEqual([0, 0]); }); }); describe("pilotar: escolha do alvo", () => { it("prefere o mais próximo com linha de visão", () => { const atras = zumbi(1, 11.5, 7.5); const livre = zumbi(2, 1.5, 1.5); expect(pilotar(cena(atras, livre)).olhar[0]).toBe(-0.12); }); it("sem visão de ninguém, escolhe o mais próximo", () => { const perto = zumbi(1, 11.5, 7.5); const longe = zumbi(2, 11.5, 5.5); expect(pilotar(cena(longe, perto)).olhar[0]).toBe(0.12); }); it("não atira sem linha de visão, mesmo alinhado", () => { const p = cena(zumbi(1, 11.5, 7.5)); p.jogador.rumo = Math.PI / 2; expect(pilotar(p).atirar).toBe(false); }); }); describe("pilotar: mira", () => { it("gira no máximo 0,12 rad por passo", () => { expect(pilotar(cena(aoRumo(1, 1, 5))).olhar[0]).toBe(0.12); expect(pilotar(cena(aoRumo(1, -1, 5))).olhar[0]).toBe(-0.12); }); it("gira só o que falta quando o erro é menor que o limite", () => { const p = cena(aoRumo(1, 0.03, 5)); expect(pilotar(p).olhar[0]).toBeCloseTo(0.03); }); it("normaliza a diferença de rumo para −π a π", () => { const p = cena(aoRumo(1, -3, 5)); p.jogador.rumo = 3; expect(pilotar(p).olhar[0]).toBe(0.12); }); it("inclina no máximo 0,05 rad por passo, para baixo e para cima", () => { const p = cena(aoRumo(1, 0, 5)); expect(pilotar(p).olhar[1]).toBe(-0.05); p.jogador.inclinacao = -1; expect(pilotar(p).olhar[1]).toBe(0.05); }); it("mira a 65% da altura do tipo", () => { const p = cena(aoRumo(1, 0, 5)); p.jogador.inclinacao = Math.atan2(0.65 * ZUMBIS.comum.altura - 1.6, 5); expect(pilotar(p).olhar[1]).toBeCloseTo(0); }); it("atira quando o erro de rumo é menor que 0,05 e há visão", () => { expect(pilotar(cena(aoRumo(1, 0.04, 5))).atirar).toBe(true); }); it("não atira com erro de rumo de 0,05 ou mais", () => { expect(pilotar(cena(aoRumo(1, 0.06, 5))).atirar).toBe(false); }); }); describe("pilotar: arma", () => { it("usa a faca quando a borda está a 1,3 m ou menos", () => { const comum = ZUMBIS.comum.raio; expect(pilotar(cena(aoRumo(1, 0, 1.25 + comum))).arma).toBe(2); expect(pilotar(cena(aoRumo(1, 0, 1.4 + comum))).arma).toBe(0); }); it("usa a automática com 3 zumbis a até 12 m", () => { const tres = cena(aoRumo(1, 0, 5), aoRumo(2, 1, 6), aoRumo(3, 2, 12)); expect(pilotar(tres).arma).toBe(1); }); it("fica na pistola com só 2 zumbis a até 12 m", () => { const dois = cena(aoRumo(1, 0, 5), aoRumo(2, 1, 6), aoRumo(3, 2, 12.5)); expect(pilotar(dois).arma).toBe(0); }); it("usa a automática contra blindado e contra chefão", () => { expect(pilotar(cena(aoRumo(1, 0, 6, "blindado"))).arma).toBe(1); expect(pilotar(cena(aoRumo(1, 0, 6, "chefao"))).arma).toBe(1); }); it("fica na pistola contra um comum ou um rápido", () => { expect(pilotar(cena(aoRumo(1, 0, 6, "rapido"))).arma).toBe(0); }); it("sem bala não usa a automática", () => { const p = cena(aoRumo(1, 0, 6, "blindado")); p.jogador.municao = 0; expect(pilotar(p).arma).toBe(0); }); }); describe("pilotar: distância", () => { it("anda para trás com o alvo a menos de 4 m", () => { expect(pilotar(cena(aoRumo(1, 0, 3.9))).mover).toEqual([0, -1]); }); it("fica parado com o alvo a 4 m ou mais", () => { expect(pilotar(cena(aoRumo(1, 0, 4))).mover).toEqual([0, 0]); }); }); describe("pilotar: partida inteira no escritório", () => { function jogar(dificuldade: Partida["dificuldade"] = "facil") { const p = criarPartida({ semente: 1, dificuldade, problemaId: "site-lento", mapa: ESCRITORIO, }); while (p.fase !== "vitoria" && p.fase !== "derrota" && p.passo < 36000) { avancar(p, pilotar(p)); } return p; } it("vence em menos de 36.000 passos, com 3 drones e 4 problemas resolvidos", () => { const p = jogar(); expect(p.fase).toBe("vitoria"); expect(p.passo).toBeLessThan(36000); expect(p.drones).toHaveLength(3); expect(p.resolvidos).toHaveLength(4); expect(p.placar.drones).toBeGreaterThan(0); }); it.each(["normal", "dificil"] as const)( "termina em vitória ou derrota antes de 36.000 passos no %s", (dificuldade) => { const p = jogar(dificuldade); expect(["vitoria", "derrota"]).toContain(p.fase); expect(p.passo).toBeLessThan(36000); }, ); it("é determinística: mesma partida, mesmo passo e mesmo placar", () => { const a = jogar(); const b = jogar(); expect(b.passo).toBe(a.passo); expect(b.placar).toEqual(a.placar); }); });