feat: NTE gear optimizer web app

Client-side optimizer for Neverness to Everness console builds. Imports a
gear export from nte-history-exporter, keeps it in IndexedDB, and solves for
the best module placement per character or across a team.

- domain/: pure ports of the Python research scripts (shapes, cartridges,
  board tiling, stats, scoring, set bonuses, Arc effects, sheet prediction).
  No DOM, no storage imports, so they stay worker-safe and Node-testable.
- solver/: single-character solve with branch and bound, plus a three-phase
  team solve (portfolio, leximin, column generation) over disjoint item sets.
  Runs in a Worker when one can be constructed, inline otherwise.
- db/: three storage tiers selected by use rather than feature detection,
  fail-closed import validation, and undo that restores both sides of a steal.
- ui/: React 19 views for items, characters and teams; the board is drawn as
  the deliverable rather than a score.
- Two build targets: a hosted bundle and a single self-contained nte.html
  with no external requests.

Values the model cannot measure are reported as unavailable rather than
guessed: cartridge set bonuses, the level 80 base-stat multiplier, and the
distinction between "optimal for this packing" and globally optimal.

145 tests, tsc clean, both build targets clean.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
goober 2026-08-21 11:38:17 +03:00
commit 5ce255fa6f
157 changed files with 92600 additions and 0 deletions

298
tests/db.test.ts Normal file
View file

@ -0,0 +1,298 @@
import { describe, expect, it } from "vitest";
import { createLocalStorageAdapter, STATE_KEY, type StorageLike } from "../src/db/adapters/local-storage.ts";
import { createMemoryAdapter } from "../src/db/adapters/memory.ts";
import { emptyState, type StoredState } from "../src/db/adapters/types.ts";
import { exportDatabase, importDatabase } from "../src/db/dbfile.ts";
import { parseGearExport } from "../src/db/import.ts";
import { emptyCharacter, type EquipmentRow } from "../src/db/schema.ts";
import {
applyImport,
coveredCells,
editItem,
equip,
equipmentOf,
heldByOthers,
isComplete,
nameOwnerGroup,
resolveCharacter,
todoInGame,
undoLast,
unnamedGroups,
} from "../src/db/store.ts";
const row = (over: Partial<EquipmentRow> = {}): EquipmentRow => ({
instance: "a1",
ownerGroup: null,
characterId: "Zankou",
cells: [1, 2, 3],
origin: "imported",
batchId: "b1",
at: 0,
...over,
});
function stateWith(equipment: EquipmentRow[], over: Partial<StoredState> = {}): StoredState {
const state = emptyState();
state.db.equipment = equipment;
return { ...state, ...over };
}
class FakeStorage implements StorageLike {
data = new Map<string, string>();
quota = Infinity;
getItem(key: string) {
return this.data.get(key) ?? null;
}
setItem(key: string, value: string) {
if (value.length > this.quota) throw new Error("QuotaExceededError");
this.data.set(key, value);
}
removeItem(key: string) {
this.data.delete(key);
}
}
describe("adapters", () => {
it("memory round-trips and does not alias the caller's state", async () => {
const adapter = createMemoryAdapter();
const state = stateWith([row()]);
await adapter.replaceAll(state);
state.db.equipment[0]!.instance = "mutated";
const read = await adapter.read();
expect(read.db.equipment[0]!.instance).toBe("a1");
expect(adapter.durable).toBe(false);
});
it("localStorage round-trips and survives a corrupt value", async () => {
const storage = new FakeStorage();
const adapter = createLocalStorageAdapter(storage);
await adapter.replaceAll(stateWith([row()]));
expect((await adapter.read()).db.equipment).toHaveLength(1);
storage.data.set(STATE_KEY, "{ not json");
expect((await adapter.read()).db.equipment).toEqual([]);
});
it("drops the snapshot before the database when the quota is hit", async () => {
const storage = new FakeStorage();
const adapter = createLocalStorageAdapter(storage);
const state = stateWith([row()]);
state.snapshot = { items: [], equipment: [], characters: [], ownerNames: [], meta: [] };
await adapter.replaceAll(state);
expect(storage.data.size).toBe(2);
storage.quota = 200;
await expect(adapter.replaceAll(state)).rejects.toThrow();
// The database write is what matters; the snapshot is the first thing cut.
expect(storage.getItem("nte-optimizer:snapshot")).toBeNull();
});
it("writes only equipment on the frequent path", async () => {
const adapter = createMemoryAdapter();
const state = stateWith([row()]);
state.db.items = [
{ instance: "a1", kind: "module", itemId: "x", shape: null, cells: null, set: null, level: 0, rarity: "orange", mainStats: [], substats: [] },
];
await adapter.replaceAll(state);
await adapter.writeEquipment([row({ instance: "a2" })], []);
const read = await adapter.read();
expect(read.db.equipment.map((r) => r.instance)).toEqual(["a2"]);
expect(read.db.items).toHaveLength(1);
});
});
describe("store", () => {
it("resolves a character through the owner group, never off the row", () => {
const state = nameOwnerGroup(stateWith([row({ characterId: null, ownerGroup: "g1" })]), "g1", "Sagiri");
const [equipped] = state.db.equipment;
expect(equipped!.characterId).toBeNull();
expect(resolveCharacter(equipped!, state.db.ownerNames)).toBe("Sagiri");
expect(equipmentOf(state.db, "Sagiri")).toHaveLength(1);
});
it("lists groups nobody has named", () => {
const state = stateWith([
row({ characterId: null, ownerGroup: "g1" }),
row({ instance: "a2", characterId: null, ownerGroup: "g2" }),
]);
expect(unnamedGroups(state.db)).toEqual(["g1", "g2"]);
expect(unnamedGroups(nameOwnerGroup(state, "g1", "Zankou").db)).toEqual(["g2"]);
});
it("calls a build incomplete when it no longer covers twenty cells", () => {
const full = [row({ cells: new Array(20).fill(0) })];
expect(coveredCells(full)).toBe(20);
expect(isComplete(full)).toBe(true);
expect(isComplete([row({ cells: [1, 2] })])).toBe(false);
});
it("replaces a character's whole loadout in one action", () => {
const state = stateWith([row({ instance: "old1" }), row({ instance: "old2" })]);
const { state: next } = equip(state, "Zankou", ["new1", "new2"]);
expect(equipmentOf(next.db, "Zankou").map((r) => r.instance).sort()).toEqual(["new1", "new2"]);
expect(next.undo).toHaveLength(1);
expect(next.undo[0]!.before.map((r) => r.instance).sort()).toEqual(["old1", "old2"]);
});
it("names who was left with a hole when it takes their item", () => {
const state = stateWith([
row({ instance: "shared", characterId: "Haniel" }),
row({ instance: "other", characterId: "Haniel" }),
]);
const { state: next, displaced } = equip(state, "Zankou", ["shared"]);
expect(displaced).toEqual([{ characterId: "Haniel", instances: ["shared"] }]);
// Haniel keeps everything else, and is now incomplete rather than wiped.
expect(equipmentOf(next.db, "Haniel").map((r) => r.instance)).toEqual(["other"]);
});
it("restores both sides of a steal with one undo", () => {
const state = stateWith([
row({ instance: "shared", characterId: "Haniel" }),
row({ instance: "mine", characterId: "Zankou" }),
]);
const { state: next } = equip(state, "Zankou", ["shared"]);
const back = undoLast(next);
expect(equipmentOf(back.db, "Haniel").map((r) => r.instance)).toEqual(["shared"]);
expect(equipmentOf(back.db, "Zankou").map((r) => r.instance)).toEqual(["mine"]);
expect(back.undo).toHaveLength(0);
});
it("marks app-assigned rows as things still to do in game", () => {
const state = stateWith([row({ instance: "worn" })]);
const { state: next } = equip(state, "Zankou", ["planned"]);
const todo = todoInGame(next.db);
expect(todo.get("Zankou")!.map((r) => r.instance)).toEqual(["planned"]);
expect(todoInGame(state.db).size).toBe(0);
});
});
describe("import application", () => {
const gearExport = {
format: "nte-gear-export",
format_version: 1,
scan: { cartridges: 0, modules: 1 },
exporter: { version: "0.4.0" },
characters: [{ key: "Zankou", level: 70, breakthroughs: 5 }],
items: [
{
instance: "a1",
kind: "module",
item_id: "cell3_style6_1_Orange",
shape: "cell3_style6",
level: 20,
rarity: "orange",
main_stats: [],
substats: [{ stat: "CritBase", value: 0.06 }],
owner_group: "g1",
},
],
};
it("snapshots, replaces gear, and leaves owner names alone", () => {
let state = stateWith([row({ instance: "stale" })]);
state = nameOwnerGroup(state, "g1", "Zankou");
state.db.characters = [{ ...emptyCharacter("Zankou"), arcId: "fork_vine", level: 60 }];
state.undo = [{ batchId: "b", before: [], after: [], at: 0 }];
const next = applyImport(state, parseGearExport(gearExport));
expect(next.db.items).toHaveLength(1);
expect(next.db.equipment.map((r) => r.instance)).toEqual(["a1"]);
expect(next.db.ownerNames).toHaveLength(1);
expect(next.db.characters[0]!.arcId).toBe("fork_vine");
expect(next.db.characters[0]!.level).toBe(70);
// Undo refers to items that may no longer exist.
expect(next.undo).toEqual([]);
expect(next.snapshot!.equipment.map((r) => r.instance)).toEqual(["stale"]);
});
it("refuses to apply a failed import", () => {
const bad = parseGearExport({ format: "nope" });
expect(() => applyImport(emptyState(), bad)).toThrow();
});
});
describe("database file", () => {
it("round-trips the whole workspace", () => {
const state = nameOwnerGroup(stateWith([row()]), "g1", "Zankou");
const file = exportDatabase(state, "2026-08-21");
const back = importDatabase(JSON.parse(JSON.stringify(file)));
expect(back.ok).toBe(true);
expect(back.migrated).toBe(false);
expect(back.state!.db.ownerNames).toHaveLength(1);
expect(back.state!.db.equipment).toHaveLength(1);
});
it("refuses a file from a newer build rather than dropping fields", () => {
const file = exportDatabase(emptyState(), null);
const newer = { ...file, schema_version: 99 };
const result = importDatabase(newer);
expect(result.ok).toBe(false);
expect(result.problems[0]).toContain("newer build");
});
it("refuses anything that is not a database file", () => {
expect(importDatabase({ format: "nte-gear-export" }).ok).toBe(false);
expect(importDatabase(null).ok).toBe(false);
});
});
describe("use equipped items", () => {
it("locks away only what a resolvable other character is wearing", () => {
let state = stateWith([
row({ instance: "mine", characterId: "Zankou" }),
row({ instance: "theirs", characterId: "Haniel" }),
row({ instance: "unnamed", characterId: null, ownerGroup: "g9" }),
]);
// Zankou keeps their own; Haniel's is off limits.
expect(heldByOthers(state.db, ["Zankou"])).toEqual(["theirs"]);
// Both team members' items are available to the team.
expect(heldByOthers(state.db, ["Zankou", "Haniel"])).toEqual([]);
// An unresolved group belongs to nobody the app can reason about, so it
// stays available rather than hiding most of the pool.
state = nameOwnerGroup(state, "g9", "Adler");
expect(heldByOthers(state.db, ["Zankou"]).sort()).toEqual(["theirs", "unnamed"]);
});
});
describe("editing an item by hand", () => {
it("corrects a value and leaves the rest alone", () => {
const state = emptyState();
state.db.items = [
{
instance: "a1",
kind: "module",
itemId: "cell3_style6_1_Orange",
shape: "cell3_style6",
cells: 3,
set: null,
level: 0,
rarity: "orange",
mainStats: [],
substats: [{ stat: "CritBase", value: 0.03 }],
},
];
const next = editItem(state, "a1", {
level: 20,
substats: [{ stat: "CritBase", value: 0.06 }],
});
expect(next.db.items[0]!.level).toBe(20);
expect(next.db.items[0]!.substats[0]!.value).toBe(0.06);
expect(next.db.items[0]!.shape).toBe("cell3_style6");
// The original is untouched: state is replaced, never mutated.
expect(state.db.items[0]!.level).toBe(0);
});
it("cannot be used to change an item's identity", () => {
const state = emptyState();
state.db.items = [
{
instance: "a1", kind: "module", itemId: "x", shape: null, cells: null,
set: null, level: 0, rarity: "orange", mainStats: [], substats: [],
},
];
const next = editItem(state, "a1", { level: 5 } as never);
expect(next.db.items[0]!.instance).toBe("a1");
});
});