chore: восстановление репозитория из снапшота v0.3.1

Прежняя git-история утрачена при переносе проекта на машину владельца
(снапшот без .git). Хэши коммитов в docs/reports/* относятся к утраченной
истории.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
sab.code.lab 2026-08-07 09:34:02 +02:00
commit 1c85091186
184 changed files with 33303 additions and 0 deletions

View file

@ -0,0 +1,141 @@
/**
* Микро-бенчмарк: flood fill vs union-find для поиска групп и дамэ
* на случайных позициях 19×19. Запуск: npx tsx packages/core/bench/group-bench.mts
* (tsx эфемерный инструмент прогона, не зависимость пакета).
* Случайность только createSeededRng с фиксированным seed (детерминизм).
*/
import { performance } from 'node:perf_hooks';
import { createSeededRng } from '../src/index.js';
const SIZE = 19;
const CELLS = SIZE * SIZE;
const POSITIONS = 40;
const ITERATIONS = 300;
/** Случайная позиция: клетки заняты с вероятностью density, цвет 50/50. */
function randomPosition(seed: number, density: number): Uint8Array {
const rng = createSeededRng(seed);
const grid = new Uint8Array(CELLS);
for (let index = 0; index < CELLS; index += 1) {
if (rng.next() < density) grid[index] = rng.next() < 0.5 ? 1 : 2;
}
return grid;
}
/** Соседи клетки по индексу. */
function neighborIndices(index: number): number[] {
const x = index % SIZE;
const y = Math.floor(index / SIZE);
const result: number[] = [];
if (x > 0) result.push(index - 1);
if (x < SIZE - 1) result.push(index + 1);
if (y > 0) result.push(index - SIZE);
if (y < SIZE - 1) result.push(index + SIZE);
return result;
}
/** Flood fill: группы + число дамэ (как в движке, groupAt). */
function groupsFloodFill(grid: Uint8Array): number {
const seen = new Uint8Array(CELLS);
let groups = 0;
let liberties = 0;
for (let start = 0; start < CELLS; start += 1) {
if (grid[start] === 0 || seen[start] === 1) continue;
groups += 1;
const color = grid[start];
const stack = [start];
seen[start] = 1;
const libs = new Set<number>();
while (stack.length > 0) {
const current = stack.pop() as number;
for (const next of neighborIndices(current)) {
if (grid[next] === 0) libs.add(next);
if (grid[next] === color && seen[next] === 0) {
seen[next] = 1;
stack.push(next);
}
}
}
liberties += libs.size;
}
return groups * 1000 + liberties;
}
/** Union-find: группы + число дамэ. */
function groupsUnionFind(grid: Uint8Array): number {
const parent = new Int32Array(CELLS);
for (let index = 0; index < CELLS; index += 1) parent[index] = index;
const find = (value: number): number => {
let root = value;
while (parent[root] !== root) root = parent[root] ?? root;
let node = value;
while (parent[node] !== node) {
const next = parent[node] ?? node;
parent[node] = root;
node = next;
}
return root;
};
for (let index = 0; index < CELLS; index += 1) {
if (grid[index] === 0) continue;
for (const next of neighborIndices(index)) {
if (grid[next] === grid[index]) parent[find(index)] = find(next);
}
}
const libsByRoot = new Map<number, Set<number>>();
for (let index = 0; index < CELLS; index += 1) {
if (grid[index] === 0) continue;
const root = find(index);
let libs = libsByRoot.get(root);
if (libs === undefined) {
libs = new Set<number>();
libsByRoot.set(root, libs);
}
for (const next of neighborIndices(index)) {
if (grid[next] === 0) libs.add(next);
}
}
let liberties = 0;
for (const libs of libsByRoot.values()) liberties += libs.size;
return libsByRoot.size * 1000 + liberties;
}
/** Прогон одного алгоритма на наборе позиций; возвращает мс и контрольную сумму. */
function measure(
positions: Uint8Array[],
algorithm: (grid: Uint8Array) => number,
): { ms: number; checksum: number } {
let checksum = 0;
const started = performance.now();
for (let iteration = 0; iteration < ITERATIONS; iteration += 1) {
for (const grid of positions) checksum = (checksum + algorithm(grid)) % 1000003;
}
return { ms: performance.now() - started, checksum };
}
const rng = createSeededRng(7);
const densities = [0.35, 0.55, 0.75];
let totalFlood = 0;
let totalUnion = 0;
for (const density of densities) {
const positions = Array.from({ length: POSITIONS }, (_, index) =>
randomPosition(Math.floor(rng.next() * 100000), density),
);
const flood = measure(positions, groupsFloodFill);
const union = measure(positions, groupsUnionFind);
if (flood.checksum !== union.checksum) {
console.warn(`ОШИБКА: контрольные суммы не совпали (${flood.checksum} != ${union.checksum})`);
process.exitCode = 1;
}
totalFlood += flood.ms;
totalUnion += union.ms;
console.warn(
`density=${density}: flood fill ${flood.ms.toFixed(1)} мс, union-find ${union.ms.toFixed(1)} мс, ` +
`отношение u/f ${(union.ms / flood.ms).toFixed(2)}`,
);
}
console.warn(
`ИТОГО: flood fill ${totalFlood.toFixed(1)} мс, union-find ${totalUnion.toFixed(1)} мс, ` +
`отношение u/f ${(totalUnion / totalFlood).toFixed(2)} ` +
`(${POSITIONS * ITERATIONS * densities.length} прогонов позиций 19x19)`,
);