Files
portifolio-evandrodevbr/lib/jogo/partida.test.ts
T
evandro 67b32fcab3
CI / audit (push) Successful in 10m48s
CI / verify (push) Successful in 19m2s
CI / deploy (push) Successful in 2m14s
CI / e2e (push) Failing after 36m15s
feat(jogo): expediente dos mortos, jogo de zumbis em primeira pessoa
Etapa 1 inteira (logica com 100% de cobertura, cena 3D, toque, sons, bancada de desempenho, e2e) e a base da etapa 2 (mapa novo no Tiled, texturas, imagens dos itens, sons conferidos, golpe recortado e medido). Pagina /jogo fora do menu e do sitemap ate a medicao no celular.
2026-11-10 19:37:10 -03:00

380 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { criarDrone } from "./drones";
import { montarFila } from "./hordas";
import * as mapaModulo from "./mapa";
import { avancar, criarPartida } from "./partida";
import { pilotar } from "./piloto";
import { semear } from "./rng";
import type { Dificuldade, Entrada, Fase, Partida } from "./tipos";
import { ESCRITORIO, HORDAS, JOGADOR, MUNICAO, PASSO } from "./valores";
const MAPA = ["#########", "#N......#", "#.......#", "#......J#", "#########"];
const PARADO: Entrada = {
mover: [0, 0],
olhar: [0, 0],
atirar: false,
arma: 0,
};
function nova(extra: Partial<Parameters<typeof criarPartida>[0]> = {}) {
return criarPartida({
semente: 1,
dificuldade: "facil",
problemaId: "site-lento",
tetoAVista: 2,
mapa: MAPA,
...extra,
});
}
function passo(p: Partida, parcial: Partial<Entrada> = {}) {
avancar(p, { ...PARADO, ...parcial });
}
function em(p: Partida, fase: Fase, extra: Partial<Partida> = {}) {
p.fase = fase;
p.tempoFase = 0;
p.jogador.x = 4.5;
p.jogador.z = 2.5;
Object.assign(p, extra);
}
const FILA_UM = [{ tipo: "comum" as const, problemaId: "site-lento" }];
describe("criarPartida", () => {
it("monta o estado inicial no mapa do escritório por padrão", () => {
const p = criarPartida({
semente: 7,
dificuldade: "normal",
problemaId: "site-lento",
});
expect(p.fase).toBe("inicio");
expect(p.tetoAVista).toBe(16);
expect(p.mapa.largura).toBe(ESCRITORIO[0].length);
expect(p.jogador).toEqual({
x: p.mapa.inicio[0],
z: p.mapa.inicio[1],
rumo: 0,
inclinacao: 0,
vida: JOGADOR.vida,
arma: 0,
municao: MUNICAO.inicial,
recarga: 0,
});
expect(p.hordas).toHaveLength(4);
expect(p.hordas[3]).toBe("site-lento");
expect(p).toMatchObject({
passo: 0,
horda: 0,
tempoFase: 0,
zumbis: [],
fila: [],
drones: [],
caixas: [],
feed: [],
resolvidos: [],
placar: { jogador: 0, drones: 0 },
eventos: [],
proximoId: 1,
problemaId: "site-lento",
dificuldade: "normal",
});
});
it("usa o mapa e o teto informados e consome o sorteio da própria partida", () => {
const p = nova({ tetoAVista: 3 });
expect(p.tetoAVista).toBe(3);
expect(p.mapa.largura).toBe(9);
expect(p.jogador.x).toBe(7.5);
expect(p.rng).not.toBe(semear(1));
expect(nova().hordas).toEqual(p.hordas);
});
});
describe("avancar: jogador", () => {
it("anda para a frente em +z com rumo 0", () => {
const p = nova();
em(p, "pausa");
passo(p, { mover: [0, 1] });
expect(p.jogador.x).toBeCloseTo(4.5);
expect(p.jogador.z).toBeCloseTo(2.5 + JOGADOR.velocidade * PASSO);
});
it("anda para a direita em −x com rumo 0", () => {
const p = nova();
em(p, "pausa");
passo(p, { mover: [1, 0] });
expect(p.jogador.x).toBeCloseTo(4.5 - JOGADOR.velocidade * PASSO);
expect(p.jogador.z).toBeCloseTo(2.5);
});
it("gira o rumo e anda para a frente na direção nova", () => {
const p = nova();
em(p, "pausa");
passo(p, { olhar: [Math.PI / 2, 0], mover: [0, 1] });
expect(p.jogador.rumo).toBeCloseTo(Math.PI / 2);
expect(p.jogador.x).toBeCloseTo(4.5 + JOGADOR.velocidade * PASSO);
expect(p.jogador.z).toBeCloseTo(2.5);
});
it("limita o movimento diagonal a comprimento 1", () => {
const p = nova();
em(p, "pausa");
passo(p, { mover: [1, 1] });
const andou = Math.hypot(p.jogador.x - 4.5, p.jogador.z - 2.5);
expect(andou).toBeCloseTo(JOGADOR.velocidade * PASSO);
});
it("não aumenta um movimento menor que 1", () => {
const p = nova();
em(p, "pausa");
passo(p, { mover: [0, 0.5] });
expect(p.jogador.z).toBeCloseTo(2.5 + 0.5 * JOGADOR.velocidade * PASSO);
});
it("soma a inclinação e a limita para cima e para baixo", () => {
const p = nova();
em(p, "pausa");
passo(p, { olhar: [0, 0.1] });
expect(p.jogador.inclinacao).toBeCloseTo(0.1);
passo(p, { olhar: [0, 5] });
expect(p.jogador.inclinacao).toBe(JOGADOR.inclinacaoMax);
passo(p, { olhar: [0, -9] });
expect(p.jogador.inclinacao).toBe(-JOGADOR.inclinacaoMax);
});
it("troca de arma, mas só atira se pedido", () => {
const p = nova();
em(p, "pausa");
passo(p, { arma: 1 });
expect(p.jogador.arma).toBe(1);
expect(p.eventos).toEqual([]);
p.jogador.recarga = 0;
passo(p, { arma: 1, atirar: true });
expect(p.eventos[0]).toMatchObject({ tipo: "tiro", autor: "jogador" });
});
it("recolhe as caixas ao alcance", () => {
const p = nova();
em(p, "pausa", { caixas: [{ id: 99, x: 4.5, z: 2.5 }] });
passo(p);
expect(p.caixas).toEqual([]);
expect(p.jogador.municao).toBe(MUNICAO.inicial + MUNICAO.porCaixa);
expect(p.eventos).toEqual([{ tipo: "caixa", balas: MUNICAO.porCaixa }]);
});
});
describe("avancar: fases", () => {
it("esvazia os eventos e conta passos", () => {
const p = nova();
p.eventos.push({ tipo: "golpe", dano: 1, zumbiId: 0 });
passo(p);
expect(p.eventos).toEqual([]);
expect(p.passo).toBe(1);
expect(p.tempoFase).toBe(1);
});
it("fica em inicio até o preparo acabar", () => {
const p = nova();
p.tempoFase = HORDAS.preparo - 2;
passo(p);
expect(p.fase).toBe("inicio");
expect(p.eventos).toEqual([]);
});
it("começa a horda 0 ao fim do preparo", () => {
const p = nova();
p.tempoFase = HORDAS.preparo - 1;
passo(p);
expect(p.fase).toBe("horda");
expect(p.tempoFase).toBe(0);
expect(p.horda).toBe(0);
expect(p.fila).toEqual(montarFila(0, p.hordas[0], "facil"));
expect(p.eventos).toEqual([{ tipo: "fase", fase: "horda" }]);
});
it("não termina a horda com zumbis na fila", () => {
const p = nova();
em(p, "horda", { fila: [...FILA_UM], tetoAVista: 0 });
passo(p);
expect(p.fase).toBe("horda");
expect(p.resolvidos).toEqual([]);
});
it("não termina a horda com zumbis no mapa", () => {
const p = nova();
em(p, "horda", {
zumbis: [
{
id: 50,
tipo: "comum",
problemaId: "site-lento",
x: 1.5,
z: 1.5,
rumo: 0,
vida: 100,
estado: "andando",
tempo: 0,
},
],
});
passo(p);
expect(p.fase).toBe("horda");
expect(p.resolvidos).toEqual([]);
});
it("termina a horda: resolve, cria o drone, cura e vai para a pausa", () => {
const p = nova();
em(p, "horda", { horda: 1 });
p.jogador.vida = 50;
passo(p);
const resolvido = p.hordas[1];
expect(p.resolvidos).toEqual([resolvido]);
expect(p.drones).toHaveLength(1);
expect(p.drones[0].problemaId).toBe(resolvido);
expect(p.jogador.vida).toBe(50 + JOGADOR.curaPorHorda);
expect(p.fase).toBe("pausa");
expect(p.tempoFase).toBe(0);
expect(p.feed[0].texto).toMatch(/^Resolvido: /);
expect(p.eventos).toEqual([
{ tipo: "resolvido", problemaId: resolvido, droneId: p.drones[0].id },
{ tipo: "fase", fase: "pausa" },
]);
});
it("a cura não passa da vida máxima", () => {
const p = nova();
em(p, "horda");
p.jogador.vida = 90;
passo(p);
expect(p.jogador.vida).toBe(JOGADOR.vida);
});
it("vai para a derrota com a vida zerada, antes de resolver a horda", () => {
const p = nova();
em(p, "horda");
p.jogador.vida = 0;
passo(p);
expect(p.fase).toBe("derrota");
expect(p.resolvidos).toEqual([]);
expect(p.drones).toEqual([]);
expect(p.eventos).toEqual([{ tipo: "fase", fase: "derrota" }]);
});
it("fica na pausa até o tempo acabar, e os drones seguem o jogador", () => {
const p = nova();
em(p, "pausa", { horda: 0 });
const drone = criarDrone(p, "site-lento");
drone.x = 1.5;
drone.z = 1.5;
passo(p);
expect(p.fase).toBe("pausa");
expect(p.horda).toBe(0);
expect(drone.x).toBeGreaterThan(1.5);
expect(p.eventos).toEqual([]);
});
it("sai da pausa para a horda seguinte", () => {
const p = nova();
em(p, "pausa", { horda: 0 });
p.tempoFase = HORDAS.pausa - 1;
passo(p);
expect(p.horda).toBe(1);
expect(p.fase).toBe("horda");
expect(p.tempoFase).toBe(0);
expect(p.fila).toEqual(montarFila(1, p.hordas[1], "facil"));
expect(p.eventos).toEqual([{ tipo: "fase", fase: "horda" }]);
});
it("sai da pausa da horda 2 para o chefão", () => {
const p = nova();
em(p, "pausa", { horda: 2 });
p.tempoFase = HORDAS.pausa - 1;
passo(p);
expect(p.horda).toBe(3);
expect(p.fase).toBe("chefao");
expect(p.fila).toEqual(montarFila(3, p.hordas[3], "facil"));
expect(p.eventos).toEqual([{ tipo: "fase", fase: "chefao" }]);
});
it("vence ao derrotar o chefão, sem criar drone", () => {
const p = nova();
em(p, "chefao", { horda: 3 });
passo(p);
expect(p.fase).toBe("vitoria");
expect(p.resolvidos).toEqual(["site-lento"]);
expect(p.drones).toEqual([]);
expect(p.eventos).toEqual([
{ tipo: "resolvido", problemaId: "site-lento", droneId: -1 },
{ tipo: "fase", fase: "vitoria" },
]);
});
it("perde no chefão com a vida zerada", () => {
const p = nova();
em(p, "chefao", { horda: 3 });
p.jogador.vida = -5;
passo(p);
expect(p.fase).toBe("derrota");
});
it.each(["vitoria", "derrota"] as const)(
"depois da %s nada mais muda, só os eventos esvaziam",
(fase) => {
const p = nova();
em(p, fase);
p.eventos.push({ tipo: "golpe", dano: 1, zumbiId: 0 });
const antes = JSON.stringify({ ...p, eventos: [] });
passo(p, { mover: [1, 1], olhar: [1, 1], atirar: true, arma: 1 });
expect(p.eventos).toEqual([]);
expect(JSON.stringify(p)).toBe(antes);
},
);
});
describe("avancar: campo de fluxo", () => {
it("só recalcula quando o jogador muda de célula", () => {
const espiao = jest.spyOn(mapaModulo, "campoDeFluxo");
const p = nova();
em(p, "horda", { fila: [...FILA_UM], tetoAVista: 0 });
passo(p);
passo(p);
expect(espiao).toHaveBeenCalledTimes(1);
p.jogador.x -= 1;
passo(p);
expect(espiao).toHaveBeenCalledTimes(2);
passo(p);
expect(espiao).toHaveBeenCalledTimes(2);
espiao.mockRestore();
});
});
describe("partida inteira com o piloto", () => {
const TETO_DE_PASSOS = 60_000;
// regressão: na fácil, as sementes 4, 6, 13, 20, 23, 24 e 38 empacavam na horda 3 com "faltam 2"
it.each<Dificuldade>(["facil", "normal", "dificil"])(
"40 sementes na %s terminam em vitória ou derrota",
(dificuldade) => {
const fins: [number, Fase][] = [];
for (let semente = 1; semente <= 40; semente++) {
const p = criarPartida({
semente,
dificuldade,
problemaId: "site-lento",
});
for (
let i = 0;
i < TETO_DE_PASSOS && p.fase !== "vitoria" && p.fase !== "derrota";
i++
) {
avancar(p, pilotar(p));
}
fins.push([semente, p.fase]);
}
const presas = fins.filter(([, f]) => f !== "vitoria" && f !== "derrota");
expect(presas).toEqual([]);
},
);
});