);
diff --git a/components/Book/index.tsx b/components/Book/index.tsx
index e649760..aec5b60 100644
--- a/components/Book/index.tsx
+++ b/components/Book/index.tsx
@@ -102,6 +102,53 @@ export default function Book(
,
);
+ // Stat Page.
+ pages.push(
+
,
+ );
+
const gamePages = convertGamesToPages(props.data.settings, props.data.games);
for (const gamePage of gamePages) {
if (gamePage.type === 'one') {
@@ -143,6 +190,23 @@ export default function Book(
}
}
+ // Notes
+ for (let i = 0; i < 2; i++) {
+ pages.push(
+
,
);
+ // Spine
+ // pages.push(
+ // pages.length ? "turned" : ""}
+ // onClick={pageClickHandler}
+ // pageNumber={-pages.length}
+ // >
+ //
+ //
+
+ //
+
+ //
+ //
The {props.data.user.name} Chess Book
+ //
+ //
+ // ♞
+ //
+ //
+ // ,
+ // );
+
+ console.log({ allPages: pages.length });
+
return (
{
+ return b.movesCount - a.movesCount;
+ });
-/** Total ply for an array of games. */
-function totalPly(games: Game[]): number {
- return games.reduce((s, g) => s + ply(g), 0);
-}
-
-/** Max ply in an array of games. */
-function maxPly(games: Game[]): number {
- return games.length ? Math.max(...games.map(ply)) : 0;
-}
+ const limit = 85 * 2;
-/** Median ply of an array (used for similarity). */
-function medianPly(games: Game[]): number {
- if (!games.length) return 0;
- const sorted = games.map(ply).sort((a, b) => a - b);
- const mid = Math.floor(sorted.length / 2);
- return sorted.length % 2
- ? sorted[mid]
- : (sorted[mid - 1] + sorted[mid]) / 2;
+ return ((sortedByMoveCountGames[0]?.movesCount ?? 0) + (sortedByMoveCountGames[5]?.movesCount ?? -42)) < limit
+ && ((sortedByMoveCountGames[1]?.movesCount ?? 0) + (sortedByMoveCountGames[4]?.movesCount ?? -42)) < limit
+ && ((sortedByMoveCountGames[2]?.movesCount ?? 0) + (sortedByMoveCountGames[3]?.movesCount ?? -42)) < limit
}
-/** Distance between two games for clustering (percentile based). */
-function distance(a: Game, b: Game): number {
- const longer = Math.max(ply(a), ply(b));
- const shorter = Math.min(ply(a), ply(b));
- return (longer - shorter) / longer; // 0..1
-}
-
-/** Cluster games into groups of “similar” length. */
-function clusterByLength(games: Game[]): Game[][] {
- const CLUSTERS = 8; // tweak if you want more/fewer buckets
- const sorted = [...games].sort((a, b) => ply(a) - ply(b));
- const step = Math.ceil(sorted.length / CLUSTERS);
- const out: Game[][] = [];
- for (let i = 0; i < sorted.length; i += step) {
- out.push(sorted.slice(i, i + step));
+function validateGamesFor3Page(games: (Game | undefined)[]): boolean {
+ const realGames = games.filter((game) => game?.id !== undefined);
+ if (realGames.length === 3) {
+ return realGames.every(g => (g?.movesCount ?? 0) <= 50 * 2);
+ }
+ if (realGames.length === 2) {
+ return realGames.every(g => (g?.movesCount ?? 0) <= 80 * 2);
+ }
+ if (realGames.length === 1) {
+ return (realGames[0]?.movesCount ?? 0) < 200 * 2;
}
- return out;
-}
-
-/* ---------- page builders ------------------------------------------------ */
-
-/** Try to build a 3-page (3 games) that respects the 165-move limit. */
-function tryThree(pool: Game[]): Game[] | null {
- if (pool.length < 3) return null;
- // prefer candidates whose median is close to the pool median
- const poolMedian = medianPly(pool);
- const candidates = pool
- .map((_, i) => pool.slice(i, i + 3))
- .filter((g) => g.length === 3 && totalPly(g) <= 330)
- .sort(
- (a, b) =>
- Math.abs(medianPly(a) - poolMedian) -
- Math.abs(medianPly(b) - poolMedian)
- );
- return candidates.length ? candidates[0] : null;
-}
-
-/** Try to build a 6-page (6 games) that respects the 100-move row limit. */
-function trySix(pool: Game[]): Game[] | null {
- if (pool.length < 6) return null;
- // brute-force all 6-length slices (cheap for small arrays)
- const candidates = pool
- .map((_, i) => pool.slice(i, i + 6))
- .filter((g) => g.length === 6)
- .filter((g) => {
- const top = maxPly(g.slice(0, 3));
- const bot = maxPly(g.slice(3, 6));
- return top + bot <= 200;
- })
- .sort((a, b) => totalPly(a) - totalPly(b)); // prefer tighter packing
- return candidates.length ? candidates[0] : null;
-}
-/** Try to build a 6-page with only 3 games (template allows empty slots). */
-function trySixWithThree(pool: Game[]): Game[] | null {
- if (pool.length < 3) return null;
- return pool.slice(0, 3);
+ return false;
}
/* ---------- main export -------------------------------------------------- */
-
export default function convertGamesToPages(
_settings: Settings,
games: Game[]
): ChessPage[] {
if (!games.length) return [];
- /* 1. Strength order ---------------------------------------------------- */
- const strength = [...games].sort((a, b) => scoreGame(b) - scoreGame(a));
+ // Refactor the pool to be sorted by strength
+ const pool = [...games].sort((a, b) => scoreGame(b) - scoreGame(a));
- /* 2. Best game always gets a full page --------------------------------- */
+ // Build up an array of our page.
const pages: ChessPage[] = [];
- const best = strength.shift()!;
- pages.push({ type: "one", games: [best] });
- /* 3. Cluster remaining games by similar length ------------------------- */
- const clusters = clusterByLength(strength); // array of Game[]
+ // Greedy page building
+ let batch: Game[] = [];
+ let current: Game | undefined = undefined;
- /* 4. Greedy page building inside each cluster -------------------------- */
- for (const cluster of clusters) {
- let pool = [...cluster].sort((a, b) => scoreGame(b) - scoreGame(a)); // still strength order inside cluster
+ // Add games to pages until we run out of games.
+ while (current = pool.shift()) {
+ batch.push(current);
- while (pool.length) {
- let used: Game[] | null = null;
+ // First page is always a single game to highlight the best one
+ if (pages.length === 0) {
+ pages.push({ type: "one", games: [batch[0]] });
+ batch.length = 0;
+ continue;
+ }
- /* 6-page (6 games) */
- if ((used = trySix(pool))) {
- pages.push({ type: "six", games: used as [Game, Game, Game, Game, Game, Game] });
- pool = pool.slice(used.length);
- continue;
- }
+ // If we're in the first 1% of games or first 10 games, try make a 3-game page
+ const inTopPercent = batch.length < 30
+ && (pool.length < games.length * 0.01 || games.length - pool.length < 10);
- /* 3-page (3 games) */
- if ((used = tryThree(pool))) {
- pages.push({ type: "three", games: used as [Game, Game, Game] });
- pool = pool.slice(used.length);
- continue;
+ // Try make a 3 page with 3 games if inTopPercent
+ if (inTopPercent && batch.length === 3 && validateGamesFor3Page(batch)) {
+ pages.push({ type: "three", games: [...batch] as any });
+ batch.length = 0;
+ continue;
+ }
+
+ // If batch has less than 6 games, keep adding unless there are no more games,
+ if (batch.length !== 6 && pool.length > 0) {
+ continue;
+ }
+
+ // Try make a 6 page with 6 games
+ if (validateGamesFor6Page(batch)) {
+ pages.push({ type: "six", games: [...batch] as any });
+ batch.length = 0;
+ continue;
+ }
+
+ // Try make a 3 page with 3 games
+ // Try every combination of 3 games in the batch
+ let foundThreeForThree = false;
+ for (let i = 0; i < batch.length - 2 && !foundThreeForThree; i++) {
+ for (let j = i + 1; j < batch.length - 1 && !foundThreeForThree; j++) {
+ for (let k = j + 1; k < batch.length && !foundThreeForThree; k++) {
+ const trio = [batch[i], batch[j], batch[k]];
+ if (validateGamesFor3Page(trio)) {
+ pages.push({ type: "three", games: trio as any });
+ // Remove the used games from batch
+ batch = batch.filter((_, idx) => idx !== i && idx !== j && idx !== k);
+ foundThreeForThree = true;
+ }
+ }
}
+ }
+ if (foundThreeForThree) {
+ continue;
+ }
- /* 6-page with only 3 games */
- if ((used = trySixWithThree(pool))) {
- pages.push({ type: "six", games: used as [Game, Game, Game, Game, Game, Game] });
- pool = pool.slice(used.length);
- continue;
+ // Try make a 6 page with 3 games
+ // Try every combination of 3 games in the batch
+ let foundThreeForSix = false;
+ for (let i = 0; i < batch.length - 2 && !foundThreeForSix; i++) {
+ for (let j = i + 1; j < batch.length - 1 && !foundThreeForSix; j++) {
+ for (let k = j + 1; k < batch.length && !foundThreeForSix; k++) {
+ const trio = [batch[i], batch[j], batch[k]];
+ if (validateGamesFor6Page(trio)) {
+ pages.push({ type: "six", games: trio as any });
+ // Remove the used games from batch
+ batch = batch.filter((_, idx) => idx !== i && idx !== j && idx !== k);
+ foundThreeForSix = true;
+ }
+ }
}
+ }
+ if (foundThreeForSix) {
+ continue;
+ }
- /* Nothing fitted – pad with single pages */
- pages.push({ type: "one", games: [pool.shift()!] });
+ // Try make a 3 page with any pair of games
+ let foundPairForThree = false;
+ for (let i = 0; i < batch.length - 1 && !foundPairForThree; i++) {
+ for (let j = i + 1; j < batch.length && !foundPairForThree; j++) {
+ const pair = [batch[i], batch[j]];
+ if (validateGamesFor3Page(pair)) {
+ pages.push({ type: "three", games: pair as any });
+ // Remove the used games from batch
+ batch = batch.filter((_, idx) => idx !== i && idx !== j);
+ foundPairForThree = true;
+ }
+ }
}
+ if (foundPairForThree) {
+ continue;
+ }
+
+ // Make a 1 page with the game with the most moves
+ const maxMovesIdx = batch.reduce(
+ (maxIdx, g, idx, arr) =>
+ g.moves.length > arr[maxIdx].moves.length ? idx : maxIdx,
+ 0
+ );
+ pages.push({ type: "one", games: [batch.splice(maxMovesIdx, 1)[0]] });
+ }
+
+ const stats: Record = {};
+ for (const page of pages) {
+ const key = page.type;
+ stats[key] = (stats[key] ?? 0) + 1;
}
- /* 5. Done -------------------------------------------------------------- */
- console.log({ pageCount: pages.length });
- pages.length = Math.min(40, pages.length); // limit to 40 pages
+ console.log(stats);
+
+ // All done.
+ // console.log({ pageCount: pages.length });
+ // pages.length = Math.min(5, pages.length); // limit to 40 pages
return pages;
}