70 lines
3.2 KiB
JavaScript
70 lines
3.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
import {
|
|
ARENA_RATING_BANDS,
|
|
arenaAiProfile,
|
|
arenaDampening,
|
|
arenaDrDuration,
|
|
arenaMatchTolerance,
|
|
calculateArenaRating,
|
|
shouldArenaBotChooseCorrectly,
|
|
validateArenaComposition,
|
|
} from "./arena-rules.mjs";
|
|
|
|
function seededRandom(seed) {
|
|
let value = seed >>> 0;
|
|
return () => {
|
|
value = (value * 1664525 + 1013904223) >>> 0;
|
|
return value / 0x100000000;
|
|
};
|
|
}
|
|
|
|
test("arena Elo uses band K values, draws are neutral, and clamps at both ends", () => {
|
|
assert.deepEqual(calculateArenaRating({ rating: 1000, opponentRating: 1000, score: 1 }), {
|
|
before: 1000, expected: 0.5, delta: 20, after: 1020,
|
|
});
|
|
assert.equal(calculateArenaRating({ rating: 2499, opponentRating: 2500, score: 1 }).after, 2500);
|
|
assert.equal(calculateArenaRating({ rating: 0, opponentRating: 0, score: 0 }).after, 0);
|
|
assert.equal(calculateArenaRating({ rating: 1600, opponentRating: 1900, score: 0.5 }).delta, 0);
|
|
});
|
|
|
|
test("arena composition requires exactly one healer and rejects tanks", () => {
|
|
assert.equal(validateArenaComposition("2v2", ["healer", "damage"]).ok, true);
|
|
assert.equal(validateArenaComposition("3v3", ["healer", "damage", "damage"]).ok, true);
|
|
assert.equal(validateArenaComposition("3v3", ["healer"], { allowPartial: true }).missing.damage, 2);
|
|
assert.equal(validateArenaComposition("2v2", ["tank", "damage"]).code, "invalid_arena_role");
|
|
assert.equal(validateArenaComposition("2v2", ["damage", "damage"]).code, "invalid_composition");
|
|
});
|
|
|
|
test("difficulty, dampening, matchmaking, and DR follow the rated rules", () => {
|
|
const gladiator = arenaAiProfile(2500);
|
|
assert.equal(gladiator.healerInterruptChance, 0.85);
|
|
assert.equal(gladiator.minimumErrorChance, 0.08);
|
|
assert.equal(shouldArenaBotChooseCorrectly(1, gladiator.minimumErrorChance, () => 0.93), false);
|
|
assert.equal(arenaDampening(0), 0.1);
|
|
assert.equal(arenaDampening(540_000), 1);
|
|
assert.equal(arenaMatchTolerance(0), 100);
|
|
assert.equal(arenaMatchTolerance(30_000), 400);
|
|
assert.equal(arenaDrDuration(8000, 2, 0).durationMs, 2000);
|
|
assert.equal(arenaDrDuration(8000, 3, 15_000).durationMs, 8000);
|
|
});
|
|
|
|
test("every AI band honors its settings and seeded minimum-error floor", () => {
|
|
for (const [index, band] of ARENA_RATING_BANDS.entries()) {
|
|
const profile = arenaAiProfile(band.min);
|
|
assert.equal(profile.title, band.title);
|
|
assert.equal(profile.reactionMinSeconds, band.reaction[0]);
|
|
assert.equal(profile.reactionMaxSeconds, band.reaction[1]);
|
|
assert.equal(profile.healerInterruptChance, band.healerInterrupt);
|
|
assert.equal(profile.coordinatedCcChance, band.coordinatedCc);
|
|
assert.equal(profile.correctDefensiveChance, band.defensive);
|
|
assert.equal(profile.minimumErrorChance, band.error);
|
|
|
|
const random = seededRandom(index + 1);
|
|
const decisions = Array.from({ length: 4000 }, () => shouldArenaBotChooseCorrectly(1, profile.minimumErrorChance, random));
|
|
const errorRate = decisions.filter((decision) => !decision).length / decisions.length;
|
|
assert.ok(errorRate >= profile.minimumErrorChance - 0.025, `${band.title} fell below its minimum-error floor`);
|
|
assert.ok(errorRate <= profile.minimumErrorChance + 0.025, `${band.title} exceeded its seeded error tolerance`);
|
|
}
|
|
});
|