1
0
Fork 0
polaris-task-force/tests/int/aim-trainer.int.spec.ts
Z8MB1E 73d689a680 feat(qualification): add aim trainer rules
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-09-03 23:36:22 -04:00

409 lines
13 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
ACCURACY_BONUS_MAX,
AIM_TRAINER_DIFFICULTIES,
COMPLETION_XP,
COURSE_DURATION_MS,
HEADSHOT_POINTS,
MAX_DESPAWNED_TARGETS,
MAX_HITS,
MAX_SHOTS_FIRED,
NEW_TIER_RECORD_XP,
POINTS_PER_XP,
SCORE_XP_RATE,
TORSO_POINTS,
XP_CAP,
type AimTrainerTarget,
calculateAimTrainerAccuracy,
calculateAimTrainerScore,
calculateAimTrainerXp,
createAimTrainerTarget,
isAimTrainerDifficulty,
resolveAimTrainerHitZone,
resolveAimTrainerTargetHit,
stepAimTrainerTarget,
} from "@/lib/aim-trainer";
/** Deterministic LCG-style RNG so target creation is reproducible across runs. */
function seededRng(seed: number): () => number {
let state = seed >>> 0;
return () => {
state = (state * 1664525 + 1013904223) >>> 0;
return state / 0x100000000;
};
}
describe("Aim Trainer constants", () => {
it("exposes the course duration and scoring constants", () => {
expect(COURSE_DURATION_MS).toBe(30_000);
expect(TORSO_POINTS).toBe(100);
expect(HEADSHOT_POINTS).toBe(250);
expect(ACCURACY_BONUS_MAX).toBe(500);
expect(POINTS_PER_XP).toBe(500);
expect(SCORE_XP_RATE).toBe(0.125);
expect(COMPLETION_XP).toBe(1);
expect(NEW_TIER_RECORD_XP).toBe(2);
expect(XP_CAP).toBe(50);
});
it("exposes the server safety caps", () => {
expect(MAX_SHOTS_FIRED).toBe(500);
expect(MAX_HITS).toBe(180);
expect(MAX_DESPAWNED_TARGETS).toBe(60);
});
});
describe("AIM_TRAINER_DIFFICULTIES", () => {
it("defines exactly the three tiers with the specified values", () => {
expect(AIM_TRAINER_DIFFICULTIES).toEqual({
recruit: {
torsoW: 76,
torsoH: 118,
headRadius: 21,
movingRatio: 0.25,
speed: 80,
targetLifetimeMs: 2000,
replacementDelayMs: 400,
maxConcurrentTargets: 1,
},
veteran: {
torsoW: 62,
torsoH: 100,
headRadius: 18,
movingRatio: 0.55,
speed: 140,
targetLifetimeMs: 1500,
replacementDelayMs: 300,
maxConcurrentTargets: 2,
},
elite: {
torsoW: 50,
torsoH: 82,
headRadius: 15,
movingRatio: 0.8,
speed: 220,
targetLifetimeMs: 1100,
replacementDelayMs: 200,
maxConcurrentTargets: 2,
},
});
});
it("asserts every field of every tier explicitly", () => {
const recruit = AIM_TRAINER_DIFFICULTIES.recruit;
expect(recruit.torsoW).toBe(76);
expect(recruit.torsoH).toBe(118);
expect(recruit.headRadius).toBe(21);
expect(recruit.movingRatio).toBe(0.25);
expect(recruit.speed).toBe(80);
expect(recruit.targetLifetimeMs).toBe(2000);
expect(recruit.replacementDelayMs).toBe(400);
expect(recruit.maxConcurrentTargets).toBe(1);
const veteran = AIM_TRAINER_DIFFICULTIES.veteran;
expect(veteran.torsoW).toBe(62);
expect(veteran.torsoH).toBe(100);
expect(veteran.headRadius).toBe(18);
expect(veteran.movingRatio).toBe(0.55);
expect(veteran.speed).toBe(140);
expect(veteran.targetLifetimeMs).toBe(1500);
expect(veteran.replacementDelayMs).toBe(300);
expect(veteran.maxConcurrentTargets).toBe(2);
const elite = AIM_TRAINER_DIFFICULTIES.elite;
expect(elite.torsoW).toBe(50);
expect(elite.torsoH).toBe(82);
expect(elite.headRadius).toBe(15);
expect(elite.movingRatio).toBe(0.8);
expect(elite.speed).toBe(220);
expect(elite.targetLifetimeMs).toBe(1100);
expect(elite.replacementDelayMs).toBe(200);
expect(elite.maxConcurrentTargets).toBe(2);
});
});
describe("isAimTrainerDifficulty", () => {
it("accepts the three valid tiers", () => {
expect(isAimTrainerDifficulty("recruit")).toBe(true);
expect(isAimTrainerDifficulty("veteran")).toBe(true);
expect(isAimTrainerDifficulty("elite")).toBe(true);
});
it("rejects unknown values", () => {
expect(isAimTrainerDifficulty("legendary")).toBe(false);
expect(isAimTrainerDifficulty("")).toBe(false);
expect(isAimTrainerDifficulty(42)).toBe(false);
expect(isAimTrainerDifficulty(null)).toBe(false);
expect(isAimTrainerDifficulty(undefined)).toBe(false);
expect(isAimTrainerDifficulty({})).toBe(false);
});
});
describe("calculateAimTrainerAccuracy", () => {
it("returns 0 when there were zero attempts", () => {
expect(calculateAimTrainerAccuracy(0, 0, 0)).toBe(0);
});
it("counts hits against shots fired plus despawned unhit targets", () => {
// 3 hits, 3 shots, 1 despawned unhit target -> 3 / 4
expect(calculateAimTrainerAccuracy(3, 3, 1)).toBeCloseTo(0.75);
});
it("clamps the result to the [0, 1] range", () => {
expect(calculateAimTrainerAccuracy(10, 2, 0)).toBe(1);
expect(calculateAimTrainerAccuracy(0, 5, 5)).toBe(0);
});
});
describe("calculateAimTrainerScore", () => {
it("scores headshots, torso hits, and the accuracy bonus", () => {
// 2 headshots + 1 torso, 3 shots fired, 1 despawned unhit target.
// Hit score = 2*250 + 1*100 = 600. Accuracy = 3/4. Bonus = round(0.75*500) = 375. Total 975.
expect(calculateAimTrainerScore(2, 1, 3, 1)).toBe(975);
});
it("scores a perfect run with no accuracy penalty", () => {
// 1 headshot, 1 shot, no despawned targets -> 250 + round(1*500) = 750.
expect(calculateAimTrainerScore(1, 0, 1, 0)).toBe(750);
});
});
describe("calculateAimTrainerXp", () => {
it("awards the completion XP for a zero score", () => {
expect(calculateAimTrainerXp(0, false)).toBe(1);
});
it("awards completion XP plus 0.125 per full 500 points", () => {
expect(calculateAimTrainerXp(499, false)).toBe(1);
expect(calculateAimTrainerXp(500, false)).toBe(1.125);
expect(calculateAimTrainerXp(999, false)).toBe(1.125);
expect(calculateAimTrainerXp(1000, false)).toBe(1.25);
});
it("adds the new-tier-best bonus", () => {
expect(calculateAimTrainerXp(0, true)).toBe(3);
expect(calculateAimTrainerXp(500, true)).toBe(3.125);
});
it("caps the result at XP_CAP", () => {
expect(calculateAimTrainerXp(1_000_000, true)).toBe(50);
expect(calculateAimTrainerXp(1_000_000, false)).toBe(50);
});
});
describe("createAimTrainerTarget", () => {
const canvasW = 800;
const canvasH = 600;
it("returns a target with the tier's dimensions and a deterministic position", () => {
const rng = seededRng(1234);
const target = createAimTrainerTarget(rng, canvasW, canvasH, "veteran");
expect(target.torsoW).toBe(62);
expect(target.torsoH).toBe(100);
expect(target.headRadius).toBe(18);
expect(typeof target.moving).toBe("boolean");
expect(typeof target.x).toBe("number");
expect(typeof target.y).toBe("number");
expect(typeof target.vx).toBe("number");
expect(typeof target.vy).toBe("number");
});
it("is deterministic for the same seed across repeated calls", () => {
// Given: a fixed seed for Elite target generation.
const seed = 42;
// When: identical seeded streams each generate their first target.
const a = createAimTrainerTarget(seededRng(seed), canvasW, canvasH, "elite");
const b = createAimTrainerTarget(seededRng(seed), canvasW, canvasH, "elite");
// Then: the complete targets match and the moving target has the balanced Elite speed.
expect(a).toEqual(b);
expect(a.moving).toBe(true);
expect(Math.hypot(a.vx, a.vy)).toBeCloseTo(220);
});
it("keeps the whole target (torso plus head) inside the canvas", () => {
for (let seed = 0; seed < 200; seed++) {
const target = createAimTrainerTarget(seededRng(seed), canvasW, canvasH, "recruit");
const left = target.x - target.torsoW / 2;
const right = target.x + target.torsoW / 2;
const top = target.y - 2 * target.headRadius;
const bottom = target.y + target.torsoH;
expect(left).toBeGreaterThanOrEqual(0);
expect(right).toBeLessThanOrEqual(canvasW);
expect(top).toBeGreaterThanOrEqual(0);
expect(bottom).toBeLessThanOrEqual(canvasH);
}
});
it("selects moving vs stationary based on the RNG roll against movingRatio", () => {
// A rng that always returns 0 -> always moving (0 < movingRatio).
const alwaysMoving = createAimTrainerTarget(() => 0, canvasW, canvasH, "recruit");
expect(alwaysMoving.moving).toBe(true);
// A rng that always returns 1 -> never moving (1 < movingRatio is false).
const neverMoving = createAimTrainerTarget(() => 1, canvasW, canvasH, "recruit");
expect(neverMoving.moving).toBe(false);
});
it("gives stationary targets zero velocity", () => {
const stationary = createAimTrainerTarget(() => 1, canvasW, canvasH, "veteran");
expect(stationary.moving).toBe(false);
expect(stationary.vx).toBe(0);
expect(stationary.vy).toBe(0);
});
});
describe("resolveAimTrainerHitZone", () => {
const target = {
x: 400,
y: 300,
vx: 0,
vy: 0,
moving: false,
torsoW: 62,
torsoH: 100,
headRadius: 18,
};
it("resolves a head hit inside the head circle", () => {
// Head center is at (x, y - headRadius) = (400, 282).
expect(resolveAimTrainerHitZone(target, 400, 282)).toBe("head");
});
it("resolves a torso hit inside the torso rectangle", () => {
// Torso spans x in [369, 431], y in [300, 400].
expect(resolveAimTrainerHitZone(target, 400, 350)).toBe("torso");
});
it("returns null for a miss outside both zones", () => {
expect(resolveAimTrainerHitZone(target, 100, 100)).toBeNull();
});
it("checks the head zone before the torso rectangle", () => {
// A point that is inside the head circle but also within the torso's x-range
// must resolve to "head", proving head is checked first.
expect(resolveAimTrainerHitZone(target, 400, 282)).toBe("head");
});
});
describe("resolveAimTrainerTargetHit", () => {
const rearTarget: AimTrainerTarget = {
x: 400,
y: 300,
vx: 0,
vy: 0,
moving: false,
torsoW: 62,
torsoH: 100,
headRadius: 18,
};
const frontTarget: AimTrainerTarget = {
...rearTarget,
y: 280,
};
it("resolves the topmost target first when ordered targets overlap", () => {
// Given: index 1 is drawn above index 0, and (400, 282) hits its torso and index 0's head.
const targets = [rearTarget, frontTarget];
// When: the overlapping point is resolved against the ordered targets.
const hit = resolveAimTrainerTargetHit(targets, 400, 282);
// Then: the topmost target wins even though the lower target offers a headshot.
expect(hit).toEqual({ index: 1, zone: "torso" });
});
it("returns null when every ordered target misses", () => {
// Given: two overlapping targets whose complete geometry is away from the origin.
const targets = [rearTarget, frontTarget];
// When: a point outside every target is resolved.
const hit = resolveAimTrainerTargetHit(targets, 0, 0);
// Then: no target or zone is selected.
expect(hit).toBeNull();
});
});
describe("stepAimTrainerTarget", () => {
const canvasW = 800;
const canvasH = 600;
it("moves a moving target by velocity times dt", () => {
const target = {
x: 400,
y: 300,
vx: 100,
vy: 0,
moving: true,
torsoW: 62,
torsoH: 100,
headRadius: 18,
};
const stepped = stepAimTrainerTarget(target, 0.5, canvasW, canvasH);
expect(stepped.x).toBeCloseTo(450);
expect(stepped.y).toBeCloseTo(300);
});
it("leaves a stationary target in place", () => {
const target = {
x: 400,
y: 300,
vx: 100,
vy: 50,
moving: false,
torsoW: 62,
torsoH: 100,
headRadius: 18,
};
const stepped = stepAimTrainerTarget(target, 1, canvasW, canvasH);
expect(stepped.x).toBe(400);
expect(stepped.y).toBe(300);
});
it("bounces off the horizontal bounds and keeps the target inside", () => {
const target = {
x: 400,
y: 300,
vx: 1000,
vy: 0,
moving: true,
torsoW: 62,
torsoH: 100,
headRadius: 18,
};
let current = target;
for (let i = 0; i < 1000; i++) {
current = stepAimTrainerTarget(current, 0.1, canvasW, canvasH);
const left = current.x - current.torsoW / 2;
const right = current.x + current.torsoW / 2;
const top = current.y - 2 * current.headRadius;
const bottom = current.y + current.torsoH;
expect(left).toBeGreaterThanOrEqual(0);
expect(right).toBeLessThanOrEqual(canvasW);
expect(top).toBeGreaterThanOrEqual(0);
expect(bottom).toBeLessThanOrEqual(canvasH);
}
});
it("bounces off the vertical bounds and keeps the target inside", () => {
const target = {
x: 400,
y: 300,
vx: 0,
vy: 1000,
moving: true,
torsoW: 62,
torsoH: 100,
headRadius: 18,
};
let current = target;
for (let i = 0; i < 1000; i++) {
current = stepAimTrainerTarget(current, 0.1, canvasW, canvasH);
const top = current.y - 2 * current.headRadius;
const bottom = current.y + current.torsoH;
expect(top).toBeGreaterThanOrEqual(0);
expect(bottom).toBeLessThanOrEqual(canvasH);
}
});
});