feat(ocr): substat-roll validation + rarity inference for GOOD export
Adds the accuracy check yas / Genshin Optimizer use: a substat value is only legitimate if it equals round(sum of 1..6 rolls) from that stat's roll table. Values that fit no combination at either rarity are guaranteed OCR misreads. - src/lib/substatRolls.ts: 5-star roll tables (+ 4-star %/crit tables to tell rarities apart), pure isPlausibleSubstat/implausibleSubstats, and inferRarity (level > 16 or roll-table fit; conservative, defaults to 5). Validates against the union of rarities so valid 4-star pieces are not false-flagged. - Wired in: shouldFlagArtifactForReview routes implausible substats to review; the parser adds an explanatory note; goodInterop export replaces the hardcoded rarity:5 with inferRarity (fixes wrong 4-star exports to GO/IK). - 10 new unit tests; 131 total green; eval still 100%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
implausibleSubstats,
|
||||
inferRarity,
|
||||
isPlausibleSubstat,
|
||||
isPlausibleSubstatValue,
|
||||
parseSubstatEntry,
|
||||
} from "./substatRolls";
|
||||
|
||||
describe("substatRolls parsing", () => {
|
||||
it("parses percent and flat entries", () => {
|
||||
expect(parseSubstatEntry("CRIT DMG+13.2%")).toEqual({ stat: "CRIT DMG", value: 13.2, percent: true });
|
||||
expect(parseSubstatEntry("HP+1,509")).toEqual({ stat: "HP", value: 1509, percent: false });
|
||||
expect(parseSubstatEntry("garbage")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("substatRolls plausibility", () => {
|
||||
it("accepts real single-roll values (5-star)", () => {
|
||||
expect(isPlausibleSubstatValue("CRIT DMG", 7.8)).toBe(true); // 7.77 high roll
|
||||
expect(isPlausibleSubstatValue("CRIT DMG", 5.4)).toBe(true); // 5.44 low roll
|
||||
expect(isPlausibleSubstatValue("CRIT Rate", 3.9)).toBe(true); // 3.89
|
||||
expect(isPlausibleSubstatValue("ATK", 19)).toBe(true); // 19.45
|
||||
expect(isPlausibleSubstatValue("HP", 269)).toBe(true); // 268.88
|
||||
});
|
||||
|
||||
it("accepts multi-roll sums", () => {
|
||||
expect(isPlausibleSubstat("CRIT DMG+13.2%")).toBe(true); // 5.44+7.77 or 6.22+6.99
|
||||
expect(isPlausibleSubstat("Elemental Mastery+68")).toBe(true);
|
||||
expect(isPlausibleSubstat("Energy Recharge+11.7%")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects values that fit no roll combination at either rarity (OCR misreads)", () => {
|
||||
// 8.1% CRIT DMG sits in the gap: single roll maxes at 7.8 (5-star), and the
|
||||
// smallest two-roll sum is 8.2 (4-star), so it is unreachable at both.
|
||||
expect(isPlausibleSubstatValue("CRIT DMG", 8.1)).toBe(false);
|
||||
// 3.5% CRIT DMG is below the lowest possible roll at either rarity.
|
||||
expect(isPlausibleSubstatValue("CRIT DMG", 3.5)).toBe(false);
|
||||
// A dropped digit on flat ATK.
|
||||
expect(isPlausibleSubstatValue("ATK", 5)).toBe(false);
|
||||
});
|
||||
|
||||
it("does not flag unknown stats", () => {
|
||||
expect(isPlausibleSubstatValue("Mystery Stat", 12.3)).toBe(true);
|
||||
});
|
||||
|
||||
it("collects the implausible entries from a substat list", () => {
|
||||
const bad = implausibleSubstats(["CRIT DMG+13.2%", "CRIT DMG+8.1%", "ATK+19"]);
|
||||
expect(bad).toEqual(["CRIT DMG+8.1%"]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("substatRolls rarity inference", () => {
|
||||
it("is 5-star for any artifact leveled past +16", () => {
|
||||
expect(inferRarity(20, ["ATK%+3.5%"])).toBe(5);
|
||||
expect(inferRarity(17, [])).toBe(5);
|
||||
});
|
||||
|
||||
it("detects 5-star from a substat that only fits the 5-star table", () => {
|
||||
// 7.8% CRIT DMG is a 5-star single high roll; not reachable at 4-star.
|
||||
expect(inferRarity(12, ["CRIT DMG+7.8%"])).toBe(5);
|
||||
});
|
||||
|
||||
it("detects 4-star from a substat that only fits the 4-star table", () => {
|
||||
// 4.1% CRIT DMG is a 4-star single low roll; below any 5-star roll.
|
||||
expect(inferRarity(12, ["CRIT DMG+4.1%"])).toBe(4);
|
||||
});
|
||||
|
||||
it("defaults to 5-star when ambiguous", () => {
|
||||
expect(inferRarity(8, [])).toBe(5);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user