diff --git a/components/Book/Page.tsx b/components/Book/Page.tsx index 5ad2f55..77fe064 100644 --- a/components/Book/Page.tsx +++ b/components/Book/Page.tsx @@ -19,11 +19,9 @@ export default function Page( ) { return (
{props.pageNumber !== 0 && (
{props.pageNumber}
diff --git a/components/Book/SixGamePage.tsx b/components/Book/SixGamePage.tsx index bbce0bd..e3ef331 100644 --- a/components/Book/SixGamePage.tsx +++ b/components/Book/SixGamePage.tsx @@ -6,14 +6,14 @@ import ContentPage from "./ContentPage"; import MoveListTable from "../Chess/MoveListTable"; import MoveListInline from "../Chess/MoveListInline"; -function GameRow(props: { index: number; settings: Settings; game: Game, totalMoveRows: number, fullLength: Boolean }) { +function GameRow(props: { settings: Settings; game: Game, otherGameMoves: number }) { const moves = props.game.moves.split(" "); // Find the best variant to use to get the most out the move data. let size = "text-xs"; return ( -
+
- {props.totalMoveRows > 35 * (Number(props.fullLength) + 1) && moves.length > 40 * (Number(props.fullLength) + 1) + {(props.otherGameMoves + props.game.movesCount) < 65 * 2 ? ( - + ) : ( - + )}
); @@ -38,32 +38,42 @@ export default function SixGamePage(props: PageProps & { games: Game[] }) { return b.movesCount - a.movesCount; }); - const totalMoveRows = sortedByMoveCountGames[0].movesCount + (sortedByMoveCountGames[3]?.movesCount ?? 0) / 2; + const columns: [number, number | undefined][] = []; + const offset = sortedByMoveCountGames.length - 1; + + for (let i = 0; i < 3; i++) { + if (!sortedByMoveCountGames[i]) { + break; + } + + const newColumn: [number, number | undefined] = [i, undefined]; + const key = offset - i; + if (offset - 1 > 2 && sortedByMoveCountGames[key]) { + newColumn[1] = key; + } - let yGap = 'gap-y-4'; - if (totalMoveRows < 80) { - yGap = 'gap-y-20'; - } else if (totalMoveRows < 160) { - yGap = 'gap-y-12'; - } else if (totalMoveRows < 220) { - yGap = 'gap-y-8'; - } else { - yGap = 'gap-y-4'; + columns.push(newColumn); } return ( -
- {sortedByMoveCountGames.map((game, i) => ( - - ))} +
+ { + columns.map((pair) => (
+ + {pair[1] && } +
)) + }
); 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( + pages.length ? "turned" : ""} + onClick={pageClickHandler} + pageNumber={pages.length} + > +
+

Stats

+ + + + + + + + + + + +
Games:{props.data.games.length.toLocaleString()}
Players: + {Array.from(new Set(props.data.games.flatMap(game => [game.white.name, game.black.name]))).length.toLocaleString()} +
+ +
+
+

The best game

+ + ➠ + +
+

+ Based on the highest ELO of both players combined. +

+
+
+
, + ); + 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( + pages.length ? "turned" : ""} + onClick={pageClickHandler} + > +
+ Notes +
+
, + ); + } + if (pages.length % 2 === 0) { 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; }