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>
76 lines
2.5 KiB
TypeScript
76 lines
2.5 KiB
TypeScript
/**
|
|
* Turning a packing into something drawable.
|
|
*
|
|
* The result view has to read as blocks, not as a grid of squares: borders are
|
|
* drawn only on the edges facing a *different* piece, so each polyomino looks
|
|
* like one object rather than three or four cells that happen to share a colour.
|
|
* That is the whole point of the visual - the player looks at a block, reads its
|
|
* stats, and finds the matching module in their own inventory list.
|
|
*/
|
|
import { GRID } from "./board.ts";
|
|
|
|
export interface RenderedCell {
|
|
cell: number;
|
|
row: number;
|
|
col: number;
|
|
/** Index of the piece covering this cell, or -1 for a free board cell. */
|
|
piece: number;
|
|
/** Draw a border on this side: the neighbour is a different piece or off-board. */
|
|
top: boolean;
|
|
right: boolean;
|
|
bottom: boolean;
|
|
left: boolean;
|
|
}
|
|
|
|
export interface BoardRender {
|
|
cells: RenderedCell[];
|
|
rows: number;
|
|
cols: number;
|
|
}
|
|
|
|
/**
|
|
* `cells` are the board's free cells ascending; `placement[i]` is the piece
|
|
* covering `cells[i]`, matching `Tiling`.
|
|
*/
|
|
export function renderBoard(
|
|
cells: readonly number[],
|
|
placement: readonly number[],
|
|
): BoardRender {
|
|
const pieceAt = new Map<number, number>();
|
|
cells.forEach((cell, index) => pieceAt.set(cell, placement[index] ?? -1));
|
|
|
|
const samePiece = (cell: number, deltaRow: number, deltaCol: number): boolean => {
|
|
const row = Math.floor(cell / GRID) + deltaRow;
|
|
const col = (cell % GRID) + deltaCol;
|
|
// A move off the grid is not a neighbour, so that side always gets a border.
|
|
if (row < 0 || row >= GRID || col < 0 || col >= GRID) return false;
|
|
const neighbour = pieceAt.get(row * GRID + col);
|
|
return neighbour !== undefined && neighbour === pieceAt.get(cell);
|
|
};
|
|
|
|
const rendered = cells.map((cell, index) => ({
|
|
cell,
|
|
row: Math.floor(cell / GRID),
|
|
col: cell % GRID,
|
|
piece: placement[index] ?? -1,
|
|
top: !samePiece(cell, -1, 0),
|
|
right: !samePiece(cell, 0, 1),
|
|
bottom: !samePiece(cell, 1, 0),
|
|
left: !samePiece(cell, 0, -1),
|
|
}));
|
|
|
|
return { cells: rendered, rows: GRID, cols: GRID };
|
|
}
|
|
|
|
/**
|
|
* Distinct hues per piece, evenly spaced round the wheel.
|
|
*
|
|
* A build is 6 to 8 pieces, so an even split keeps neighbours far apart in hue
|
|
* without needing a hand-tuned palette that would break as soon as a build had
|
|
* one more piece than the palette had entries.
|
|
*/
|
|
export function pieceColour(piece: number, total: number): string {
|
|
if (piece < 0) return "transparent";
|
|
const hue = Math.round((360 / Math.max(total, 1)) * piece);
|
|
return `hsl(${hue} 62% 55%)`;
|
|
}
|