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>
60 lines
1.7 KiB
TypeScript
60 lines
1.7 KiB
TypeScript
/**
|
|
* A character's console grid.
|
|
*
|
|
* Blocked cells are drawn as holes, free cells as the piece covering them, and
|
|
* borders only where a piece meets something else - so a build reads as six to
|
|
* eight objects rather than twenty squares.
|
|
*/
|
|
import { pieceColour, renderBoard } from "../domain/render.ts";
|
|
|
|
export interface BoardProps {
|
|
cells: readonly number[];
|
|
placement: readonly number[];
|
|
pieces: number;
|
|
/** Called with the piece index under the pointer, or null. */
|
|
onHover?: (piece: number | null) => void;
|
|
labelFor?: (piece: number) => string;
|
|
size?: number;
|
|
}
|
|
|
|
export function Board({
|
|
cells,
|
|
placement,
|
|
pieces,
|
|
onHover,
|
|
labelFor,
|
|
size = 34,
|
|
}: BoardProps) {
|
|
const render = renderBoard(cells, placement);
|
|
const free = new Set(cells);
|
|
|
|
return (
|
|
<div
|
|
className="board"
|
|
style={{ gridTemplateColumns: `repeat(${render.cols}, ${size}px)` }}
|
|
onPointerLeave={() => onHover?.(null)}
|
|
>
|
|
{Array.from({ length: render.rows * render.cols }, (_, index) => {
|
|
if (!free.has(index)) {
|
|
return <div key={index} className="board-cell board-cell--blocked" />;
|
|
}
|
|
const cell = render.cells.find((entry) => entry.cell === index)!;
|
|
return (
|
|
<div
|
|
key={index}
|
|
className="board-cell"
|
|
title={labelFor?.(cell.piece)}
|
|
onPointerEnter={() => onHover?.(cell.piece)}
|
|
style={{
|
|
background: pieceColour(cell.piece, pieces),
|
|
borderTopWidth: cell.top ? 2 : 0,
|
|
borderRightWidth: cell.right ? 2 : 0,
|
|
borderBottomWidth: cell.bottom ? 2 : 0,
|
|
borderLeftWidth: cell.left ? 2 : 0,
|
|
}}
|
|
/>
|
|
);
|
|
})}
|
|
</div>
|
|
);
|
|
}
|