From d0d9262d72aaf4137e372962df10301cdfabd933 Mon Sep 17 00:00:00 2001 From: Jeff Lowery Date: Sun, 7 Dec 2025 13:34:49 -0800 Subject: [PATCH 1/8] Add TypeScript npm package @chess-openings/eco.json - Package structure with src/ and methods/ folders - Type definitions for Opening, OpeningCollection, etc. - Methods for fetching and querying opening data: - getLatestEcoJson() - Download ECO data from GitHub - openingBook() - Merged opening collection - findOpening() - Lookup by FEN with position fallback - getPositionBook() - Position-only FEN mapping - getFromToIndex() - Download transition index - getFromTos() - Get next/from openings with enrichment - CI workflow for type checking and build verification - Comprehensive copilot instructions for AI agents - Build configuration with TypeScript 5.3, ES2022 target --- .github/copilot-instructions.md | 209 ++++++++++++++++++++++++++++++++ .github/workflows/ci.yml | 29 +++++ .gitignore | 20 ++- .npmignore | 23 ++++ methods/findOpening.ts | 89 ++++++++++++++ methods/fromTo.ts | 108 +++++++++++++++++ methods/getLatestEcoJson.ts | 110 +++++++++++++++++ package.json | 49 ++++++++ src/index.ts | 20 +++ src/types.ts | 64 ++++++++++ tsconfig.json | 24 ++++ 11 files changed, 744 insertions(+), 1 deletion(-) create mode 100644 .github/copilot-instructions.md create mode 100644 .github/workflows/ci.yml create mode 100644 .npmignore create mode 100644 methods/findOpening.ts create mode 100644 methods/fromTo.ts create mode 100644 methods/getLatestEcoJson.ts create mode 100644 package.json create mode 100644 src/index.ts create mode 100644 src/types.ts create mode 100644 tsconfig.json diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..7ad0cae --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,209 @@ +# Copilot Instructions for eco.json + +## Project Overview + +This is a **data-only repository** containing 12,000+ chess opening variations encoded in JSON format. The data is organized by ECO (Encyclopedia of Chess Openings) categories A-E and includes interpolated variations to fill gaps. + +## Core Data Structure + +### Primary Data Files + +- **ecoA.json - ecoE.json**: Main opening databases, one per ECO category +- **eco_interpolated.json**: Generated variations that bridge gaps between named openings +- **fromTo.json**: Array of `[from_fen, to_fen, from_source, to_source]` representing opening transition graph + +### Opening Object Schema + +Each opening is keyed by **FEN** (Forsyth-Edwards Notation), which uniquely identifies positions: + +```json +{ + "rnbqkb1r/pppppppp/8/3nP3/3P4/8/PPP2PPP/RNBQKBNR b KQkq": { + "src": "eco_tsv", // Authoritative source + "eco": "B03", // ECO category code (not unique) + "moves": "1. e4 Nf6 2. e5", // Standard move sequence + "name": "Alekhine Defense", + "aliases": { + // Alternative names from other sources + "scid": "Alekhine: 3.d4" + }, + "scid": "B03a", // Extended SCID code (when applicable) + "isEcoRoot": true // True if this is the root variation for this ECO code + } +} +``` + +## Key Concepts + +### Source Hierarchy + +**eco_tsv** (from lichess) is the authoritative source and supersedes all conflicts. Other sources: eco_js, scid, eco_wikip, wiki_b, ct, chessGraph, chronos, icsbot, pgn. + +### Interpolated Openings + +"Orphan" variations lack a `from` field (no preceding named variation). Interpolations fill these gaps: + +- **src**: "interpolated" +- **rootSrc**: Original source of the root variation +- **name**: Root name + " (i)" suffix (may have multiple with same name) + +Example: If "1. e4 Nf6 2. e5 Ng8 3. d4 d6" exists but "1. e4 Nf6 2. e5 Ng8 3. d4" doesn't, an interpolated opening is created. + +### FEN as Primary Key + +- FEN strings uniquely identify positions +- Move sequences and names are NOT unique identifiers +- Multiple move orders (transpositions) can reach the same FEN +- Always use FEN for lookups, not moves or names + +## Methods (`/methods/`) + +The `/methods/` folder contains utility functions for working with eco.json data. These methods are being migrated from `/tooling/` or adapted from other sources like [fensterchess](https://fensterchess.com) on a case-by-case basis. + +**Note**: The `/tooling/` folder is deprecated and will be removed. Data maintenance and analysis tools are in the separate [eco.json.tooling](https://github.com/JeffML/eco.json.tooling) repository. + +### Common Operations + +Methods in this folder provide functionality such as: + +- Merging ECO category files +- Querying opening transitions (from/to analysis) +- Finding openings by FEN, name, or ECO code +- Source filtering and validation +- FEN validation and parsing + +### FEN Validation Pattern + +All FEN inputs are validated with: `/(?!.*\d{2,}.*)^([1-8PNBRQK]+\/){7}[1-8PNBRQK]+$/im` + +## Common Patterns + +### Loading All Openings + +```javascript +import { conjoin } from "./tooling/ecoConjoin.mjs"; + +// Without interpolations +const openings = conjoin({ includeInterpolated: false }); + +// With interpolations +const allOpenings = conjoin({ includeInterpolated: true }); +``` + +### Querying Transitions + +```javascript +import { from } from "./tooling/from.mjs"; +import { to } from "./tooling/to.mjs"; + +const fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3"; +const { opening, fromOpenings } = from(fen); // What leads to this position +const { opening, toOpenings } = to(fen); // Where this position leads +``` + +### Source Filtering + +```javascript +import { selectSources, antiSelectSources } from "./tooling/sourcesOps.mjs"; + +// Only openings from specific sources (use in process.argv) +// node script.js eco_tsv scid + +// Exclude a source +// node script.js eco_tsv +``` + +## Data Integrity Rules + +1. **ECO Categories**: Fixed at A, B, C, D, E - no others exist +2. **Interpolations Separation**: Interpolated openings are never in ecoA-E files (sanity check in ecoConjoin.mjs) +3. **FEN Uniqueness**: Each FEN appears exactly once across all eco\*.json files +4. **Source Authority**: eco_tsv overrides all other sources in conflicts +5. **No Package Manager**: This is a data project; scripts are ESM modules (.mjs) run directly with node + +## NPM Package + +This repository now includes `@chess-openings/eco.json`, a TypeScript npm package for consuming opening data. + +**Key distinction:** + +- **NPM package (`src/`)**: Read-only utilities for consuming opening data in applications +- **Methods (`methods/`)**: Utility functions for working with the data files +- **eco.json.tooling**: Separate repo for data maintenance and PR generation + +The npm package **does not bundle JSON files** - opening data is downloaded on-demand from GitHub to keep the package lightweight. + +### Available Methods + +**Opening Data:** +- `getLatestEcoJson(options)` - Downloads opening data by ECO category from GitHub +- `openingBook()` - Returns merged collection of all openings +- `findOpening(openingBook, fen, positionBook?)` - Looks up opening by FEN with position-only fallback +- `getPositionBook(openingBook)` - Creates position-only FEN β†’ full FEN mapping + +**Transition Data:** +- `getFromToIndex()` - Downloads pre-indexed transition data (fromToPositionIndexed.json) +- `getFromTos(fen, openings?)` - Returns next/from Opening objects for a FEN +- `clearFromToCache()` - Clears cached transition index + +## From/To Transition Data + +### Structure + +**fromTo.json** (source of truth): +- Format: `[from_fen, to_fen, from_source, to_source][]` - flat array of transitions +- Raw transition graph data between named openings +- Maintained by eco.json.tooling + +**fromToPositionIndexed.json** (generated, optimized for lookup): +- Format: `{ to: { position: [fens] }, from: { position: [fens] } }` +- Generated by eco.json.tooling/fromToPositionIndex.js +- Optimized for O(1) lookup using position-only FEN keys +- Downloaded on-demand by npm package methods + +### Why Pre-generation is Necessary + +From/to transitions **cannot be efficiently computed on-demand** because: +1. Only named openings are indexed (sparse graph of ~12,000 nodes) +2. Finding transitions requires matching against all opening move sequences +3. On-demand generation would be O(n) for each lookup vs O(1) with pre-indexed data +4. The index groups transpositions by using position-only FEN keys + +### Usage in Methods + +The npm package provides `getFromTos(fen)` which: +1. Downloads `fromToPositionIndexed.json` from GitHub (cached) +2. Extracts position-only FEN from input +3. Looks up transition FENs in O(1) time +4. Enriches FEN strings with full Opening objects from openingBook +5. Returns `{ next: Opening[], from: Opening[] }` + +## Important Notes + +- Opening data files (ecoA-E.json, eco_interpolated.json, fromTo.json) are the core assets +- The npm package provides TypeScript types and utilities for consuming this data +- All openings use standard algebraic notation (SAN) for moves +- ECO codes are categories, not unique identifiers - many openings share the same ECO code +- The `isEcoRoot` field identifies the "official" ECO variation for that code +- When adding new openings, always check for FEN conflicts and respect source hierarchy + +## TODO: fensterchess Migration + +**Opportunities to simplify fensterchess using eco.json methods:** + +1. **Eliminate serverless function** `netlify/functions/getFromTosForFen.js`: + - Replace with: `import { getFromTos } from '@chess-openings/eco.json'` + - Benefits: No serverless function maintenance, no API auth needed, already returns Opening objects + +2. **Remove bundled data file** `data/fromToPositionIndexed.json`: + - The npm package downloads this on-demand from GitHub + - Benefits: Smaller deployment bundle, automatic updates when data changes + +3. **Simplify enrichment code**: + - Current: Fetch FEN arrays, then map to Opening objects + - New: `getFromTos()` returns Opening objects directly + - Remove manual FEN β†’ Opening mapping in `findOpening()` + +4. **Reduce dependencies**: + - Can remove position book building (use `getPositionBook()` from package) + - Already using same types as eco.json package diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..1fc6743 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,29 @@ +name: CI + +on: + push: + branches: [main, master, npm] + pull_request: + branches: [main, master, npm] + +jobs: + type-check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Type check + run: npm run type-check + + - name: Build + run: npm run build diff --git a/.gitignore b/.gitignore index 646c6a4..889aae6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,22 @@ /temp /old tooling/eco.json -*_missing.json \ No newline at end of file +*_missing.json + +# Node.js / npm +node_modules/ +dist/ +*.log +npm-debug.log* +package-lock.json + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# OS +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..f8f8ef2 --- /dev/null +++ b/.npmignore @@ -0,0 +1,23 @@ +# Source files +src/ +tsconfig.json + +# Development files +*.json +!package.json +tooling/ +methods/ +.github/ + +# Git +.git/ +.gitignore + +# Documentation (keep README) +*.md +!README.md + +# Test and build artifacts +node_modules/ +*.log +.DS_Store diff --git a/methods/findOpening.ts b/methods/findOpening.ts new file mode 100644 index 0000000..2156c6a --- /dev/null +++ b/methods/findOpening.ts @@ -0,0 +1,89 @@ +import type { Opening, OpeningCollection } from "../src/types.js"; + +/** + * Position book maps position-only FEN (without turn/castling/en passant) to full FENs. + * Used for fallback when exact FEN match fails. + */ +export interface PositionBook { + [position: string]: string[]; +} + +/** + * Creates a position book from an opening collection. + * This allows looking up positions without turn, castling, or en passant information. + * + * @param openingBook - The complete opening collection + * @returns Position book mapping position-only FEN to array of full FENs + * + * @example + * ```typescript + * const openings = await openingBook(); + * const posBook = getPositionBook(openings); + * + * // Look up position without game state + * const fens = posBook["rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR"]; + * // Returns: ["rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"] + * ``` + */ +export function getPositionBook(openingBook: OpeningCollection): PositionBook { + const positionToFen: PositionBook = {}; + + for (const fen in openingBook) { + const position = fen.split(" ")[0]; + + positionToFen[position] ??= []; + positionToFen[position].push(fen); + } + + return positionToFen; +} + +/** + * Finds an opening by FEN with automatic fallback to position-only matching. + * First tries exact FEN match, then falls back to position-only (ignoring turn, castling, en passant). + * + * @param openingBook - The complete opening collection + * @param fen - The FEN string to look up (full or position-only) + * @param positionBook - Optional position book for fallback lookup (created via getPositionBook) + * @returns The opening if found, undefined otherwise + * + * @example + * ```typescript + * const openings = await openingBook(); + * const posBook = getPositionBook(openings); + * + * // Exact match + * const opening1 = findOpening( + * openings, + * "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1", + * posBook + * ); + * + * // Position-only match (different turn/castling but same position) + * const opening2 = findOpening( + * openings, + * "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 5 3", + * posBook + * ); + * // Both return the same opening: King's Pawn Opening + * ``` + */ +export function findOpening( + openingBook: OpeningCollection, + fen: string, + positionBook?: PositionBook +): Opening | undefined { + // Try exact FEN match first + let opening = openingBook[fen]; + + // Fallback to position-only match if available + if (!opening && positionBook) { + const position = fen.split(" ")[0]; + const posEntry = positionBook[position]; + if (posEntry && posEntry.length > 0) { + opening = openingBook[posEntry[0]]; + } + } + + return opening; +} diff --git a/methods/fromTo.ts b/methods/fromTo.ts new file mode 100644 index 0000000..fb95849 --- /dev/null +++ b/methods/fromTo.ts @@ -0,0 +1,108 @@ +import type { Opening, OpeningCollection } from "../src/types.js"; +import { openingBook } from "./getLatestEcoJson.js"; + +/** + * From/To transition index structure. + * Maps position-only FEN to arrays of full FENs representing transitions. + */ +export interface FromToIndex { + /** Maps position β†’ array of next position FENs */ + to: Record; + /** Maps position β†’ array of previous position FENs */ + from: Record; +} + +// Cache for fromToIndex to avoid re-downloading +let fromToIndexCache: FromToIndex | null = null; + +/** + * Downloads and caches the pre-indexed from/to transition data from GitHub. + * This file is generated by eco.json.tooling and maps position-only FENs to transition arrays. + * + * @returns Indexed transition data with to/from mappings + * + * @example + * ```typescript + * const index = await getFromToIndex(); + * const position = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR"; + * const nextFens = index.to[position]; // FENs for e2-e4 position + * ``` + */ +export async function getFromToIndex(): Promise { + if (fromToIndexCache) { + return fromToIndexCache; + } + + const url = + "https://raw.githubusercontent.com/JeffML/eco.json/master/fromToPositionIndexed.json"; + + const response = await fetch(url); + if (!response.ok) { + throw new Error( + `Failed to fetch fromToPositionIndexed.json: ${response.status} ${response.statusText}` + ); + } + + fromToIndexCache = await response.json(); + return fromToIndexCache!; +} + +/** + * Gets the next and previous opening positions for a given FEN. + * Returns full Opening objects (not just FEN strings) by looking up transitions + * in the pre-indexed data and enriching with opening book information. + * + * Uses position-only FEN for lookup to handle transpositions. + * + * @param fen - The FEN string to look up transitions for + * @param openings - Optional pre-loaded opening collection (will fetch if not provided) + * @returns Object with next and from Opening arrays + * + * @example + * ```typescript + * // Get transitions for e2-e4 position + * const { next, from } = await getFromTos( + * "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" + * ); + * + * console.log(next[0].name); // "King's Pawn Opening: Sicilian Defense" + * console.log(from[0].name); // "Starting Position" + * ``` + */ +export async function getFromTos( + fen: string, + openings?: OpeningCollection +): Promise<{ + next: Opening[]; + from: Opening[]; +}> { + // Load data if not provided + const openingCollection = openings || (await openingBook()); + const fromToIndex = await getFromToIndex(); + + // Extract position-only FEN (ignore turn, castling, en passant) + const position = fen.split(" ")[0]; + + // Get FEN arrays from index + const nextFens = fromToIndex.to[position] || []; + const fromFens = fromToIndex.from[position] || []; + + // Enrich with full Opening data + const next = nextFens + .map((f) => openingCollection[f]) + .filter((o): o is Opening => o != null); + + const from = fromFens + .map((f) => openingCollection[f]) + .filter((o): o is Opening => o != null); + + return { next, from }; +} + +/** + * Clears the cached fromToIndex, forcing a fresh download on next access. + * Useful for testing or when data updates are expected. + */ +export function clearFromToCache(): void { + fromToIndexCache = null; +} diff --git a/methods/getLatestEcoJson.ts b/methods/getLatestEcoJson.ts new file mode 100644 index 0000000..c824451 --- /dev/null +++ b/methods/getLatestEcoJson.ts @@ -0,0 +1,110 @@ +import { OpeningCollection } from "../src/types.js"; + +/** + * GitHub raw content URL for eco.json repository + */ +const ECO_JSON_RAW = + "https://raw.githubusercontent.com/JeffML/eco.json/master/"; + +interface EcoCategoryData { + url: string; + json?: OpeningCollection; +} + +interface OpeningsByCat { + initialized: boolean; + A?: EcoCategoryData; + B?: EcoCategoryData; + C?: EcoCategoryData; + D?: EcoCategoryData; + E?: EcoCategoryData; + IN?: EcoCategoryData; +} + +let openingsByCat: OpeningsByCat = { initialized: false }; + +/** + * Downloads and caches opening data from the eco.json GitHub repository. + * Data is fetched once and cached for subsequent calls. + * + * @returns Promise resolving to opening data organized by ECO category + * + * @example + * ```typescript + * const { A, B, C, D, E, IN } = await getLatestEcoJson(); + * console.log(A?.json); // ecoA.json data + * console.log(IN?.json); // eco_interpolated.json data + * ``` + */ +export async function getLatestEcoJson(): Promise { + if (!openingsByCat.initialized) { + const ROOT = ECO_JSON_RAW; + + openingsByCat = { + initialized: false, + A: { url: ROOT + "ecoA.json" }, + B: { url: ROOT + "ecoB.json" }, + C: { url: ROOT + "ecoC.json" }, + D: { url: ROOT + "ecoD.json" }, + E: { url: ROOT + "ecoE.json" }, + IN: { url: ROOT + "eco_interpolated.json" }, + }; + + const promises: Promise[] = []; + for (const cat in openingsByCat) { + if (cat !== "initialized") { + const category = + openingsByCat[cat as keyof Omit]; + if (category) { + promises.push(fetch(category.url)); + } + } + } + + const res = await Promise.all(promises); + let i = 0; + + for (const cat in openingsByCat) { + if (cat !== "initialized") { + const category = + openingsByCat[cat as keyof Omit]; + if (category) { + const json = await res[i++].json(); + category.json = json; + } + } + } + + openingsByCat.initialized = true; + } + + return openingsByCat; +} + +/** + * Downloads and merges all opening data (ecoA-E + interpolated) into a single collection. + * Data is cached after the first call. + * + * @returns Promise resolving to a complete opening collection keyed by FEN + * + * @example + * ```typescript + * const openings = await openingBook(); + * const fen = "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3"; + * console.log(openings[fen]); // Opening data for this position + * ``` + */ +export async function openingBook(): Promise { + const { A, B, C, D, E, IN } = await getLatestEcoJson(); + + const openingBook: OpeningCollection = { + ...A?.json, + ...B?.json, + ...C?.json, + ...D?.json, + ...E?.json, + ...IN?.json, + }; + + return openingBook; +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..3e21c4f --- /dev/null +++ b/package.json @@ -0,0 +1,49 @@ +{ + "name": "@chess-openings/eco.json", + "version": "0.1.0", + "description": "Read-only utilities for consuming chess opening data from eco.json", + "type": "module", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "import": "./dist/index.js", + "types": "./dist/index.d.ts" + } + }, + "files": [ + "dist" + ], + "scripts": { + "build": "tsc", + "watch": "tsc --watch", + "clean": "rm -rf dist", + "type-check": "tsc --noEmit", + "prepublishOnly": "npm run clean && npm run build", + "publish:public": "npm publish --access public" + }, + "keywords": [ + "chess", + "openings", + "eco", + "chess-openings", + "fen", + "pgn" + ], + "author": "Jeff Lowery", + "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/JeffML/eco.json.git" + }, + "bugs": { + "url": "https://github.com/JeffML/eco.json/issues" + }, + "homepage": "https://github.com/JeffML/eco.json#readme", + "devDependencies": { + "typescript": "^5.3.0" + }, + "engines": { + "node": ">=18.0.0" + } +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..0606a10 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,20 @@ +/** + * @chess-openings/eco.json + * + * Read-only utilities for consuming chess opening data from the eco.json repository. + * Opening data is downloaded on-demand from GitHub to keep the package size small. + */ + +export * from "./types.js"; +export { getLatestEcoJson, openingBook } from "../methods/getLatestEcoJson.js"; +export { + findOpening, + getPositionBook, + type PositionBook, +} from "../methods/findOpening.js"; +export { + getFromToIndex, + getFromTos, + clearFromToCache, + type FromToIndex, +} from "../methods/fromTo.js"; diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..d52ded6 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,64 @@ +/** + * Source of the opening data. + * eco_tsv is the authoritative source from lichess. + */ +export type OpeningSource = + | "eco_tsv" + | "eco_js" + | "scid" + | "eco_wikip" + | "wiki_b" + | "ct" + | "chessGraph" + | "chronos" + | "icsbot" + | "pgn" + | "interpolated"; + +/** + * ECO category (A-E) + */ +export type EcoCategory = "A" | "B" | "C" | "D" | "E"; + +/** + * Opening variation record keyed by FEN position + */ +export interface Opening { + /** Source of this opening data */ + src: OpeningSource; + + /** ECO code (e.g., "B03"). Not unique - many openings share the same ECO code */ + eco: string; + + /** Standard algebraic notation move sequence (e.g., "1. e4 Nf6 2. e5") */ + moves: string; + + /** Common English name of the opening */ + name: string; + + /** Alternative names from other sources */ + aliases?: Record; + + /** Extended SCID code when applicable */ + scid?: string; + + /** True if this is the official ECO root variation for this code */ + isEcoRoot?: boolean; + + /** For interpolated openings: the original source of the root variation */ + rootSrc?: OpeningSource; +} + +/** + * Collection of openings keyed by FEN notation + * FEN is the unique identifier for each position + */ +export interface OpeningCollection { + [fen: string]: Opening; +} + +/** + * Opening transition in the fromTo graph + * [from_fen, to_fen, from_source, to_source] + */ +export type OpeningTransition = [string, string, OpeningSource, OpeningSource]; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..49df09c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,24 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022", "DOM"], + "moduleResolution": "bundler", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "./dist", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "isolatedModules": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*", "methods/**/*"], + "exclude": ["node_modules", "dist", "tooling"] +} From d315a1db8b56c692413960091698d39ab673a425 Mon Sep 17 00:00:00 2001 From: Jeff Lowery Date: Mon, 8 Dec 2025 10:33:43 -0800 Subject: [PATCH 2/8] Publish v0.1.0-beta.0 prerelease - Update version to 0.1.0-beta.0 - Add NPM_README.md with API documentation - Published to npm as @chess-openings/eco.json@beta --- NPM_README.md | 235 ++++++++++++++++++++++++++++++++++++++++++++++++++ package.json | 13 +-- 2 files changed, 242 insertions(+), 6 deletions(-) create mode 100644 NPM_README.md diff --git a/NPM_README.md b/NPM_README.md new file mode 100644 index 0000000..c683501 --- /dev/null +++ b/NPM_README.md @@ -0,0 +1,235 @@ +# @chess-openings/eco.json + +TypeScript utilities for consuming chess opening data from the eco.json database. + +## Features + +- 🎯 **12,000+ chess openings** with ECO codes, move sequences, and names +- πŸ” **FEN-based lookup** with automatic position-only fallback +- 🌐 **On-demand data loading** from GitHub (no bundled files) +- πŸ”„ **Opening transitions** with next/previous position navigation +- πŸ“¦ **TypeScript-first** with complete type definitions +- πŸš€ **Zero dependencies** (only needs Node.js 18+) + +## Installation + +```bash +npm install @chess-openings/eco.json +``` + +## Quick Start + +```typescript +import { openingBook, findOpening, getFromTos } from '@chess-openings/eco.json'; + +// Load all openings +const openings = await openingBook(); + +// Look up an opening by FEN +const opening = findOpening( + openings, + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" +); + +console.log(opening?.name); // "King's Pawn Opening" +console.log(opening?.eco); // "B00" +console.log(opening?.moves); // "1. e4" + +// Get next and previous positions +const { next, from } = await getFromTos( + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" +); + +console.log(next[0]?.name); // "Sicilian Defense" +console.log(from[0]?.name); // "Starting Position" +``` + +## API Reference + +### Opening Data + +#### `getLatestEcoJson(options)` + +Downloads opening data from GitHub by ECO category. + +```typescript +import { getLatestEcoJson } from '@chess-openings/eco.json'; + +// Load specific categories +const data = await getLatestEcoJson({ + categories: ['A', 'B'], + includeInterpolated: false +}); + +// Load everything (default) +const allData = await getLatestEcoJson(); +``` + +**Options:** +- `categories?: EcoCategory[]` - Array of 'A', 'B', 'C', 'D', 'E' (default: all) +- `includeInterpolated?: boolean` - Include gap-filling variations (default: true) + +#### `openingBook()` + +Returns a merged collection of all openings. + +```typescript +import { openingBook } from '@chess-openings/eco.json'; + +const openings = await openingBook(); +// OpeningCollection: Record +``` + +#### `findOpening(openingBook, fen, positionBook?)` + +Looks up an opening by FEN with automatic position-only fallback. + +```typescript +import { findOpening, getPositionBook, openingBook } from '@chess-openings/eco.json'; + +const openings = await openingBook(); +const posBook = getPositionBook(openings); + +const opening = findOpening(openings, fen, posBook); +``` + +**Parameters:** +- `openingBook: OpeningCollection` - The opening database +- `fen: string` - FEN string to look up +- `positionBook?: PositionBook` - Optional position-only mapping for fallback + +**Returns:** `Opening | undefined` + +#### `getPositionBook(openingBook)` + +Creates a position-only FEN β†’ full FEN array mapping. + +```typescript +import { getPositionBook } from '@chess-openings/eco.json'; + +const positionBook = getPositionBook(openings); +// Record +``` + +### Transition Data + +#### `getFromToIndex()` + +Downloads pre-indexed transition data from GitHub. + +```typescript +import { getFromToIndex } from '@chess-openings/eco.json'; + +const index = await getFromToIndex(); +// { to: Record, from: Record } +``` + +#### `getFromTos(fen, openings?)` + +Gets next and previous opening positions with full Opening objects. + +```typescript +import { getFromTos } from '@chess-openings/eco.json'; + +const { next, from } = await getFromTos(fen); +// { next: Opening[], from: Opening[] } +``` + +**Parameters:** +- `fen: string` - FEN string to look up transitions for +- `openings?: OpeningCollection` - Optional pre-loaded opening collection + +**Returns:** `{ next: Opening[], from: Opening[] }` + +#### `clearFromToCache()` + +Clears the cached transition index. + +```typescript +import { clearFromToCache } from '@chess-openings/eco.json'; + +clearFromToCache(); +``` + +## Types + +### `Opening` + +```typescript +interface Opening { + name: string; // "Sicilian Defense" + moves: string; // "1. e4 c5" + eco: string; // "B20" + score?: number | null; // Position evaluation + next?: Opening[]; // Next positions + from?: Opening[]; // Previous positions + src?: string; // Data source + isEcoRoot?: boolean; // Root ECO variation + fen?: string; // FEN string +} +``` + +### `OpeningCollection` + +```typescript +type OpeningCollection = Record; +``` + +### `PositionBook` + +```typescript +type PositionBook = Record; +``` + +### `FromToIndex` + +```typescript +interface FromToIndex { + to: Record; // position β†’ next FENs + from: Record; // position β†’ previous FENs +} +``` + +## Data Structure + +The eco.json database contains: + +- **ecoA-E.json**: Main opening databases by ECO category +- **eco_interpolated.json**: Gap-filling variations +- **fromToPositionIndexed.json**: Pre-indexed transition data + +All data is downloaded on-demand from GitHub and cached in memory. + +## Performance Tips + +1. **Reuse loaded data**: Call `openingBook()` once and reuse the result +2. **Pre-build position book**: Create once with `getPositionBook()` if doing multiple lookups +3. **Cache transitions**: `getFromTos()` caches the index automatically +4. **Pass openings**: Provide pre-loaded openings to `getFromTos()` to skip re-fetching + +```typescript +// Efficient pattern +const openings = await openingBook(); +const posBook = getPositionBook(openings); + +for (const fen of fens) { + const opening = findOpening(openings, fen, posBook); + const transitions = await getFromTos(fen, openings); +} +``` + +## Contributing + +This package provides read-only utilities for consuming opening data. For data maintenance and updates: + +- **Data source**: [eco.json repository](https://github.com/JeffML/eco.json) +- **Data tooling**: [eco.json.tooling repository](https://github.com/JeffML/eco.json.tooling) + +## License + +MIT + +## Related Projects + +- [eco.json](https://github.com/JeffML/eco.json) - Chess opening database +- [fensterchess](https://fensterchess.com) - Chess opening research tool diff --git a/package.json b/package.json index 3e21c4f..4b3b3a5 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,19 @@ { "name": "@chess-openings/eco.json", - "version": "0.1.0", + "version": "0.1.0-beta.0", "description": "Read-only utilities for consuming chess opening data from eco.json", "type": "module", - "main": "./dist/index.js", - "types": "./dist/index.d.ts", + "main": "./dist/src/index.js", + "types": "./dist/src/index.d.ts", "exports": { ".": { - "import": "./dist/index.js", - "types": "./dist/index.d.ts" + "import": "./dist/src/index.js", + "types": "./dist/src/index.d.ts" } }, "files": [ - "dist" + "dist", + "NPM_README.md" ], "scripts": { "build": "tsc", From 578adf942e38c91b28e5655febc0820341344e97 Mon Sep 17 00:00:00 2001 From: Jeff Lowery Date: Mon, 8 Dec 2025 10:44:17 -0800 Subject: [PATCH 3/8] Add query methods: getOpeningsByEco, getOpeningsByEcoCategory, getEcoRoots - Add methods/queryOpenings.ts with 3 new query utilities - getOpeningsByEco: Filter by ECO code (e.g., B12) - getOpeningsByEcoCategory: Filter by category (A-E) - getEcoRoots: Return only canonical ECO root variations - Update exports and NPM_README.md documentation - Bump version to 0.1.0-beta.1 - Published to npm --- NPM_README.md | 66 ++++++++++++++++++++ chess-openings-eco.json-0.1.0.tgz | Bin 0 -> 12931 bytes methods/queryOpenings.ts | 97 ++++++++++++++++++++++++++++++ package.json | 2 +- src/index.ts | 5 ++ 5 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 chess-openings-eco.json-0.1.0.tgz create mode 100644 methods/queryOpenings.ts diff --git a/NPM_README.md b/NPM_README.md index c683501..8bb6fcc 100644 --- a/NPM_README.md +++ b/NPM_README.md @@ -111,6 +111,72 @@ const positionBook = getPositionBook(openings); // Record ``` +### Query Methods + +#### `getOpeningsByEco(ecoCode, openings?)` + +Returns all openings for a specific ECO code. + +```typescript +import { getOpeningsByEco } from '@chess-openings/eco.json'; + +// Get all Caro-Kann Defense variations +const caroKann = await getOpeningsByEco("B12"); +console.log(`Found ${caroKann.length} variations`); +``` + +**Parameters:** +- `ecoCode`: ECO code string (e.g., "B12", "C42") +- `openings`: Optional pre-loaded opening collection + +**Returns:** `Promise` + +#### `getOpeningsByEcoCategory(category, openings?)` + +Returns all openings for an ECO category (A, B, C, D, or E). + +```typescript +import { getOpeningsByEcoCategory, openingBook } from '@chess-openings/eco.json'; + +// Get all semi-open games (category B) +const semiOpen = await getOpeningsByEcoCategory("B"); + +// More efficient with pre-loaded book +const book = await openingBook(); +const catA = await getOpeningsByEcoCategory("A", book); +const catB = await getOpeningsByEcoCategory("B", book); +``` + +**Parameters:** +- `category`: ECO category letter ("A", "B", "C", "D", or "E") +- `openings`: Optional pre-loaded opening collection + +**Returns:** `Promise` + +**Throws:** Error if category is invalid + +#### `getEcoRoots(openings?)` + +Returns only canonical ECO root variations (where `isEcoRoot === true`). + +```typescript +import { getEcoRoots } from '@chess-openings/eco.json'; + +const roots = await getEcoRoots(); +console.log(`Found ${Object.keys(roots).length} ECO root variations`); + +// Check if a position is an ECO root +const fen = "rnbqkb1r/pppppppp/5n2/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2"; +if (roots[fen]) { + console.log(`${roots[fen].name} is an ECO root`); +} +``` + +**Parameters:** +- `openings`: Optional pre-loaded opening collection + +**Returns:** `Promise` (FEN β†’ Opening mapping) + ### Transition Data #### `getFromToIndex()` diff --git a/chess-openings-eco.json-0.1.0.tgz b/chess-openings-eco.json-0.1.0.tgz new file mode 100644 index 0000000000000000000000000000000000000000..7071f4c29be1293455a44b9ecce4136a85b963c3 GIT binary patch literal 12931 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 tj{#a~K%T*Ac0o0=!%g75$U literal 0 HcmV?d00001 diff --git a/methods/queryOpenings.ts b/methods/queryOpenings.ts new file mode 100644 index 0000000..78d8e78 --- /dev/null +++ b/methods/queryOpenings.ts @@ -0,0 +1,97 @@ +import type { Opening, OpeningCollection } from "../src/types.js"; +import { openingBook } from "./getLatestEcoJson.js"; + +/** + * Returns all openings for a specific ECO code. + * Note: Multiple openings can share the same ECO code (it's a category, not a unique identifier). + * + * @param ecoCode - The ECO code to filter by (e.g., "B12", "C42") + * @param openings - Optional pre-loaded opening collection (fetched via openingBook() if not provided) + * @returns Array of openings matching the ECO code + * + * @example + * ```typescript + * // Get all Caro-Kann Defense variations + * const caroKann = await getOpeningsByEco("B12"); + * console.log(`Found ${caroKann.length} variations`); + * caroKann.forEach(o => console.log(o.name, o.moves)); + * ``` + */ +export async function getOpeningsByEco( + ecoCode: string, + openings?: OpeningCollection +): Promise { + const book = openings || (await openingBook()); + + return Object.values(book).filter((opening) => opening.eco === ecoCode); +} + +/** + * Returns all openings for a specific ECO category (A, B, C, D, or E). + * + * @param category - The ECO category letter ("A", "B", "C", "D", or "E") + * @param openings - Optional pre-loaded opening collection (fetched via openingBook() if not provided) + * @returns Array of openings in the category + * + * @example + * ```typescript + * // Get all semi-open games (category B) + * const semiOpen = await getOpeningsByEcoCategory("B"); + * console.log(`Found ${semiOpen.length} openings in category B`); + * + * // With pre-loaded book (more efficient for multiple queries) + * const book = await openingBook(); + * const catA = await getOpeningsByEcoCategory("A", book); + * const catB = await getOpeningsByEcoCategory("B", book); + * ``` + */ +export async function getOpeningsByEcoCategory( + category: string, + openings?: OpeningCollection +): Promise { + const book = openings || (await openingBook()); + const upperCategory = category.toUpperCase(); + + if (!["A", "B", "C", "D", "E"].includes(upperCategory)) { + throw new Error( + `Invalid ECO category: ${category}. Must be A, B, C, D, or E.` + ); + } + + return Object.values(book).filter((opening) => + opening.eco?.startsWith(upperCategory) + ); +} + +/** + * Returns only the canonical ECO root variations (where isEcoRoot === true). + * These are the "official" variations defined in the Encyclopedia of Chess Openings. + * + * @param openings - Optional pre-loaded opening collection (fetched via openingBook() if not provided) + * @returns Object mapping FEN to root opening (preserves FEN keys for lookup) + * + * @example + * ```typescript + * // Get all canonical ECO variations + * const roots = await getEcoRoots(); + * console.log(`Found ${Object.keys(roots).length} ECO root variations`); + * + * // Check if a position is an ECO root + * const fen = "rnbqkb1r/pppppppp/5n2/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2"; + * if (roots[fen]) { + * console.log(`${roots[fen].name} (${roots[fen].eco}) is an ECO root`); + * } + * ``` + */ +export async function getEcoRoots( + openings?: OpeningCollection +): Promise { + const book = openings || (await openingBook()); + + return Object.entries(book) + .filter(([, opening]) => (opening as any).isEcoRoot === true) + .reduce((acc, [fen, opening]) => { + acc[fen] = opening; + return acc; + }, {} as OpeningCollection); +} diff --git a/package.json b/package.json index 4b3b3a5..d3d8423 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chess-openings/eco.json", - "version": "0.1.0-beta.0", + "version": "0.1.0-beta.1", "description": "Read-only utilities for consuming chess opening data from eco.json", "type": "module", "main": "./dist/src/index.js", diff --git a/src/index.ts b/src/index.ts index 0606a10..dc20510 100644 --- a/src/index.ts +++ b/src/index.ts @@ -18,3 +18,8 @@ export { clearFromToCache, type FromToIndex, } from "../methods/fromTo.js"; +export { + getOpeningsByEco, + getOpeningsByEcoCategory, + getEcoRoots, +} from "../methods/queryOpenings.js"; From 45b061db1272be8c9a53d7c838bc168a4ad8f1b9 Mon Sep 17 00:00:00 2001 From: Jeff Lowery Date: Mon, 8 Dec 2025 11:09:34 -0800 Subject: [PATCH 4/8] Consolidate to single README.md - Renamed NPM_README.md to README.md (comprehensive documentation) - Removed original README.md (content now fully integrated) - Updated package.json to remove NPM_README.md from files array (README.md is included by default in npm packages) Single README now serves both GitHub and npm audiences with complete: - API reference - Data structure documentation - Opening object schema and field descriptions - Query and transition methods - Source information and acknowledgements --- NPM_README.md | 301 --------------------------------- README.md | 457 +++++++++++++++++++++++++++++++++++++++----------- package.json | 3 +- 3 files changed, 358 insertions(+), 403 deletions(-) delete mode 100644 NPM_README.md diff --git a/NPM_README.md b/NPM_README.md deleted file mode 100644 index 8bb6fcc..0000000 --- a/NPM_README.md +++ /dev/null @@ -1,301 +0,0 @@ -# @chess-openings/eco.json - -TypeScript utilities for consuming chess opening data from the eco.json database. - -## Features - -- 🎯 **12,000+ chess openings** with ECO codes, move sequences, and names -- πŸ” **FEN-based lookup** with automatic position-only fallback -- 🌐 **On-demand data loading** from GitHub (no bundled files) -- πŸ”„ **Opening transitions** with next/previous position navigation -- πŸ“¦ **TypeScript-first** with complete type definitions -- πŸš€ **Zero dependencies** (only needs Node.js 18+) - -## Installation - -```bash -npm install @chess-openings/eco.json -``` - -## Quick Start - -```typescript -import { openingBook, findOpening, getFromTos } from '@chess-openings/eco.json'; - -// Load all openings -const openings = await openingBook(); - -// Look up an opening by FEN -const opening = findOpening( - openings, - "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" -); - -console.log(opening?.name); // "King's Pawn Opening" -console.log(opening?.eco); // "B00" -console.log(opening?.moves); // "1. e4" - -// Get next and previous positions -const { next, from } = await getFromTos( - "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" -); - -console.log(next[0]?.name); // "Sicilian Defense" -console.log(from[0]?.name); // "Starting Position" -``` - -## API Reference - -### Opening Data - -#### `getLatestEcoJson(options)` - -Downloads opening data from GitHub by ECO category. - -```typescript -import { getLatestEcoJson } from '@chess-openings/eco.json'; - -// Load specific categories -const data = await getLatestEcoJson({ - categories: ['A', 'B'], - includeInterpolated: false -}); - -// Load everything (default) -const allData = await getLatestEcoJson(); -``` - -**Options:** -- `categories?: EcoCategory[]` - Array of 'A', 'B', 'C', 'D', 'E' (default: all) -- `includeInterpolated?: boolean` - Include gap-filling variations (default: true) - -#### `openingBook()` - -Returns a merged collection of all openings. - -```typescript -import { openingBook } from '@chess-openings/eco.json'; - -const openings = await openingBook(); -// OpeningCollection: Record -``` - -#### `findOpening(openingBook, fen, positionBook?)` - -Looks up an opening by FEN with automatic position-only fallback. - -```typescript -import { findOpening, getPositionBook, openingBook } from '@chess-openings/eco.json'; - -const openings = await openingBook(); -const posBook = getPositionBook(openings); - -const opening = findOpening(openings, fen, posBook); -``` - -**Parameters:** -- `openingBook: OpeningCollection` - The opening database -- `fen: string` - FEN string to look up -- `positionBook?: PositionBook` - Optional position-only mapping for fallback - -**Returns:** `Opening | undefined` - -#### `getPositionBook(openingBook)` - -Creates a position-only FEN β†’ full FEN array mapping. - -```typescript -import { getPositionBook } from '@chess-openings/eco.json'; - -const positionBook = getPositionBook(openings); -// Record -``` - -### Query Methods - -#### `getOpeningsByEco(ecoCode, openings?)` - -Returns all openings for a specific ECO code. - -```typescript -import { getOpeningsByEco } from '@chess-openings/eco.json'; - -// Get all Caro-Kann Defense variations -const caroKann = await getOpeningsByEco("B12"); -console.log(`Found ${caroKann.length} variations`); -``` - -**Parameters:** -- `ecoCode`: ECO code string (e.g., "B12", "C42") -- `openings`: Optional pre-loaded opening collection - -**Returns:** `Promise` - -#### `getOpeningsByEcoCategory(category, openings?)` - -Returns all openings for an ECO category (A, B, C, D, or E). - -```typescript -import { getOpeningsByEcoCategory, openingBook } from '@chess-openings/eco.json'; - -// Get all semi-open games (category B) -const semiOpen = await getOpeningsByEcoCategory("B"); - -// More efficient with pre-loaded book -const book = await openingBook(); -const catA = await getOpeningsByEcoCategory("A", book); -const catB = await getOpeningsByEcoCategory("B", book); -``` - -**Parameters:** -- `category`: ECO category letter ("A", "B", "C", "D", or "E") -- `openings`: Optional pre-loaded opening collection - -**Returns:** `Promise` - -**Throws:** Error if category is invalid - -#### `getEcoRoots(openings?)` - -Returns only canonical ECO root variations (where `isEcoRoot === true`). - -```typescript -import { getEcoRoots } from '@chess-openings/eco.json'; - -const roots = await getEcoRoots(); -console.log(`Found ${Object.keys(roots).length} ECO root variations`); - -// Check if a position is an ECO root -const fen = "rnbqkb1r/pppppppp/5n2/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2"; -if (roots[fen]) { - console.log(`${roots[fen].name} is an ECO root`); -} -``` - -**Parameters:** -- `openings`: Optional pre-loaded opening collection - -**Returns:** `Promise` (FEN β†’ Opening mapping) - -### Transition Data - -#### `getFromToIndex()` - -Downloads pre-indexed transition data from GitHub. - -```typescript -import { getFromToIndex } from '@chess-openings/eco.json'; - -const index = await getFromToIndex(); -// { to: Record, from: Record } -``` - -#### `getFromTos(fen, openings?)` - -Gets next and previous opening positions with full Opening objects. - -```typescript -import { getFromTos } from '@chess-openings/eco.json'; - -const { next, from } = await getFromTos(fen); -// { next: Opening[], from: Opening[] } -``` - -**Parameters:** -- `fen: string` - FEN string to look up transitions for -- `openings?: OpeningCollection` - Optional pre-loaded opening collection - -**Returns:** `{ next: Opening[], from: Opening[] }` - -#### `clearFromToCache()` - -Clears the cached transition index. - -```typescript -import { clearFromToCache } from '@chess-openings/eco.json'; - -clearFromToCache(); -``` - -## Types - -### `Opening` - -```typescript -interface Opening { - name: string; // "Sicilian Defense" - moves: string; // "1. e4 c5" - eco: string; // "B20" - score?: number | null; // Position evaluation - next?: Opening[]; // Next positions - from?: Opening[]; // Previous positions - src?: string; // Data source - isEcoRoot?: boolean; // Root ECO variation - fen?: string; // FEN string -} -``` - -### `OpeningCollection` - -```typescript -type OpeningCollection = Record; -``` - -### `PositionBook` - -```typescript -type PositionBook = Record; -``` - -### `FromToIndex` - -```typescript -interface FromToIndex { - to: Record; // position β†’ next FENs - from: Record; // position β†’ previous FENs -} -``` - -## Data Structure - -The eco.json database contains: - -- **ecoA-E.json**: Main opening databases by ECO category -- **eco_interpolated.json**: Gap-filling variations -- **fromToPositionIndexed.json**: Pre-indexed transition data - -All data is downloaded on-demand from GitHub and cached in memory. - -## Performance Tips - -1. **Reuse loaded data**: Call `openingBook()` once and reuse the result -2. **Pre-build position book**: Create once with `getPositionBook()` if doing multiple lookups -3. **Cache transitions**: `getFromTos()` caches the index automatically -4. **Pass openings**: Provide pre-loaded openings to `getFromTos()` to skip re-fetching - -```typescript -// Efficient pattern -const openings = await openingBook(); -const posBook = getPositionBook(openings); - -for (const fen of fens) { - const opening = findOpening(openings, fen, posBook); - const transitions = await getFromTos(fen, openings); -} -``` - -## Contributing - -This package provides read-only utilities for consuming opening data. For data maintenance and updates: - -- **Data source**: [eco.json repository](https://github.com/JeffML/eco.json) -- **Data tooling**: [eco.json.tooling repository](https://github.com/JeffML/eco.json.tooling) - -## License - -MIT - -## Related Projects - -- [eco.json](https://github.com/JeffML/eco.json) - Chess opening database -- [fensterchess](https://fensterchess.com) - Chess opening research tool diff --git a/README.md b/README.md index c3d7f64..9b6f8d7 100644 --- a/README.md +++ b/README.md @@ -1,149 +1,406 @@ -# eco.json -version 3.11 +# @chess-openings/eco.json -A compendium of over 12k known opening variations. See [References](#references) at the bottom of this page. +TypeScript utilities for consuming chess opening data from the eco.json database. -#### Changes: -Fixed conflicting isEcoRoot entries; add missing B33 ECO variation +## Features -## The files +- 🎯 **12,000+ chess openings** with ECO codes, move sequences, and names +- πŸ” **FEN-based lookup** with automatic position-only fallback +- 🌐 **On-demand data loading** from GitHub (no bundled files) +- πŸ”„ **Opening transitions** with next/previous position navigation +- πŸ“¦ **TypeScript-first** with complete type definitions +- πŸš€ **Zero dependencies** (only needs Node.js 18+) -There is a JSON file for each of the ECO categories A, B, C, D, & E; e.g. ecoB.json. +## Installation -The lichess database (eco_tsv) is the authoritive database, which [supplants](https://www.google.com/search?q=supplants) conflicts with all other databases listed above (such as move order or name). +```bash +npm install @chess-openings/eco.json +``` + +## Quick Start + +```typescript +import { openingBook, findOpening, getFromTos } from "@chess-openings/eco.json"; + +// Load all openings +const openings = await openingBook(); -### Example JSON-encoded opening +// Look up an opening by FEN +const opening = findOpening( + openings, + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" +); + +console.log(opening?.name); // "King's Pawn Opening" +console.log(opening?.eco); // "B00" +console.log(opening?.moves); // "1. e4" + +// Get next and previous positions +const { next, from } = await getFromTos( + "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1" +); + +console.log(next[0]?.name); // "Sicilian Defense" +console.log(from[0]?.name); // "Starting Position" ``` -{ - "rnbqkb1r/pppppppp/8/3nP3/3P4/8/PPP2PPP/RNBQKBNR b KQkq": { - "src": "eco_tsv", - "eco": "B03", - "moves": "1. e4 Nf6 2. e5 Nd5 3. d4", - "name": "Alekhine Defense", - "aliases": { - "eco_js": "Alekhine Defense, 2. e5 Nd5 3. d4", - "scid": "Alekhine: 3.d4" - }, - "scid": "B03a" - "isEcoRoot": true - }, - ... + +## API Reference + +### Opening Data + +#### `getLatestEcoJson(options)` + +Downloads opening data from GitHub by ECO category. + +```typescript +import { getLatestEcoJson } from "@chess-openings/eco.json"; + +// Load specific categories +const data = await getLatestEcoJson({ + categories: ["A", "B"], + includeInterpolated: false, +}); + +// Load everything (default) +const allData = await getLatestEcoJson(); +``` + +**Options:** + +- `categories?: EcoCategory[]` - Array of 'A', 'B', 'C', 'D', 'E' (default: all) +- `includeInterpolated?: boolean` - Include gap-filling variations (default: true) + +#### `openingBook()` + +Returns a merged collection of all openings. + +```typescript +import { openingBook } from "@chess-openings/eco.json"; + +const openings = await openingBook(); +// OpeningCollection: Record +``` + +#### `findOpening(openingBook, fen, positionBook?)` + +Looks up an opening by FEN with automatic position-only fallback. + +```typescript +import { + findOpening, + getPositionBook, + openingBook, +} from "@chess-openings/eco.json"; + +const openings = await openingBook(); +const posBook = getPositionBook(openings); + +const opening = findOpening(openings, fen, posBook); +``` + +**Parameters:** + +- `openingBook: OpeningCollection` - The opening database +- `fen: string` - FEN string to look up +- `positionBook?: PositionBook` - Optional position-only mapping for fallback + +**Returns:** `Opening | undefined` + +#### `getPositionBook(openingBook)` + +Creates a position-only FEN β†’ full FEN array mapping. + +```typescript +import { getPositionBook } from "@chess-openings/eco.json"; + +const positionBook = getPositionBook(openings); +// Record +``` + +### Query Methods + +#### `getOpeningsByEco(ecoCode, openings?)` + +Returns all openings for a specific ECO code. + +```typescript +import { getOpeningsByEco } from "@chess-openings/eco.json"; + +// Get all Caro-Kann Defense variations +const caroKann = await getOpeningsByEco("B12"); +console.log(`Found ${caroKann.length} variations`); +``` + +**Parameters:** + +- `ecoCode`: ECO code string (e.g., "B12", "C42") +- `openings`: Optional pre-loaded opening collection + +**Returns:** `Promise` + +#### `getOpeningsByEcoCategory(category, openings?)` + +Returns all openings for an ECO category (A, B, C, D, or E). + +```typescript +import { + getOpeningsByEcoCategory, + openingBook, +} from "@chess-openings/eco.json"; + +// Get all semi-open games (category B) +const semiOpen = await getOpeningsByEcoCategory("B"); + +// More efficient with pre-loaded book +const book = await openingBook(); +const catA = await getOpeningsByEcoCategory("A", book); +const catB = await getOpeningsByEcoCategory("B", book); +``` + +**Parameters:** + +- `category`: ECO category letter ("A", "B", "C", "D", or "E") +- `openings`: Optional pre-loaded opening collection + +**Returns:** `Promise` + +**Throws:** Error if category is invalid + +#### `getEcoRoots(openings?)` + +Returns only canonical ECO root variations (where `isEcoRoot === true`). + +```typescript +import { getEcoRoots } from "@chess-openings/eco.json"; + +const roots = await getEcoRoots(); +console.log(`Found ${Object.keys(roots).length} ECO root variations`); + +// Check if a position is an ECO root +const fen = "rnbqkb1r/pppppppp/5n2/8/4P3/8/PPPP1PPP/RNBQKBNR w KQkq - 1 2"; +if (roots[fen]) { + console.log(`${roots[fen].name} is an ECO root`); } ``` -__fen__ ->The Forsyth-Edwards Notation of the position on the board after all opening moves are played. A FEN string __*uniquely identifies each opening*__, and thus FENs are the JSON object keys. +**Parameters:** + +- `openings`: Optional pre-loaded opening collection + +**Returns:** `Promise` (FEN β†’ Opening mapping) + +### Transition Data + +#### `getFromToIndex()` + +Downloads pre-indexed transition data from GitHub. + +```typescript +import { getFromToIndex } from "@chess-openings/eco.json"; + +const index = await getFromToIndex(); +// { to: Record, from: Record } +``` + +#### `getFromTos(fen, openings?)` -__src__ -> Identifies the source of the opening data; normally this will be __eco_tsv__, but could be __eco_js__ or __scid__ if no __eco_tsv__ opening corresponds to the fen. +Gets next and previous opening positions with full Opening objects. -__eco__ -> The ECO code of the opening; multiple openings can share the same ECO (it is a category, not an identifier) +```typescript +import { getFromTos } from "@chess-openings/eco.json"; -__moves__ -> The "standard" move sequence of the opening. Some openings can be arrived at by transposition, so opening moves are not identifiers. +const { next, from } = await getFromTos(fen); +// { next: Opening[], from: Opening[] } +``` -__name__ -> The common English name of an opening. Origin of the name is determined by __src__, but there can be aliases from other sources +**Parameters:** -__aliases__ -> These are variations of what the opening is called. For example, the __Ruy Lopez__ opening is sometimes called the __Spanish Opening__, depending on source +- `fen: string` - FEN string to look up transitions for +- `openings?: OpeningCollection` - Optional pre-loaded opening collection -__scid__ -> since SCID codes extend ECO codes, this will be included where applicable +**Returns:** `{ next: Opening[], from: Opening[] }` -__isEcoRoot__ -> If true, this variation's moves appear in the Encyclopedia of Chess Openings as the root variation for the __eco__ code, above +#### `clearFromToCache()` -# eco_interpolated -In __eco.json__ there are ~2000 "orphan" variations. An orphan variation has no `from` field, indicating that there is no preceding _named_ variation. There are moves that precede the last move of the orphan variation (unless it's a first move, of course). Opening records can be created for these prededing move sequences, which fill in the gaps in the eco.json data structure. +Clears the cached transition index. -Let's take a look at one case. [One of the opening books](https://github.com/lichess-org/chess-openings/blob/master/b.tsv) at [__eco_tsv__]((https://github.com/niklasf/eco)) contains these four variations of the Alekhine Defense: +```typescript +import { clearFromToCache } from "@chess-openings/eco.json"; -![Alekhine](eco_tsv_alekhine.png) +clearFromToCache(); +``` -There are two entries for the Brooklyn Variation of the Alekhine Defense on lines 132 and 133. However, there is no named variation for the move sequence __1. e4 Nf6 2. e5 Ng8 3. d4__. This makes the variation on line 133 "orphaned", in that it has no preceding named variation that leads to it. +## Types + +### `Opening` + +```typescript +interface Opening { + name: string; // "Sicilian Defense" + moves: string; // "1. e4 c5" + eco: string; // "B20" + score?: number | null; // Position evaluation + next?: Opening[]; // Next positions + from?: Opening[]; // Previous positions + src?: string; // Data source + isEcoRoot?: boolean; // Root ECO variation + fen?: string; // FEN string +} +``` -## the eco_interpolated.json file -Every orphan variation (like the Alekhine Defense: Brooklyn Variation, Everglades Variation) has a move sequence. By moving backwards from the end of the move sequence, we eventually wind up at a named variation, which is called the __root__ variation. Along the way, a record of each FEN position and remaining moves in the sequence is made. Then, from the root to the orphan, are created __interpolated__ opening objects that bridge the gap between root and orphan. +### `OpeningCollection` -A more detailed explanation can be found in Part 2 of [this blog post](https://medium.com/@jefflowery/navigating-chess-openings-part-1-8d779aee0965). +```typescript +type OpeningCollection = Record; +``` -### naming the interpolated variations -Interpolated opening variations _may_ have a name, but just weren't found in our sources. This can be corrected over time, and freshly named interpolated openings can be inserted into the __eco.json__ file as they are discovered. In the meantime, the names assigned to interpolated openings are the root name plus `" (i)"`. There may be several of these, but openings names are not required to be unique (only FENs are). +### `PositionBook` -For the example above, the interpolated opening object would be: +```typescript +type PositionBook = Record; +``` + +### `FromToIndex` +```typescript +interface FromToIndex { + to: Record; // position β†’ next FENs + from: Record; // position β†’ previous FENs +} ``` + +## Data Structure + +The eco.json database contains **12,000+ chess opening variations** organized across multiple JSON files: + +- **ecoA.json - ecoE.json**: Main opening databases, one per ECO category (A-E) +- **eco_interpolated.json**: Generated variations that bridge gaps between named openings +- **fromTo.json**: Raw transition graph array of `[from_fen, to_fen, from_source, to_source]` +- **fromToPositionIndexed.json**: Pre-indexed transition data (optimized for O(1) lookup) + +All data is downloaded on-demand from GitHub and cached in memory. + +### Opening Object Schema + +Each opening is keyed by **FEN** (Forsyth-Edwards Notation), which uniquely identifies positions: + +```json { - ... - { - "rnbqkbnr/pppppppp/8/4P3/3P4/8/PPP2PPP/RNBQKBNR b KQkq - 0 3": { - "src": "interpolated", - "eco": "B02", - "moves": "1. e4 Nf6 2. e5 Ng8 3. d4", - "name": "Alekhine Defense: Brooklyn Variation (i)", - "scid": "B02l", - "aliases": { - "scid": "Alekhine: Brooklyn Defence (Retreat Variation)" - } - }, - ... + "rnbqkb1r/pppppppp/8/3nP3/3P4/8/PPP2PPP/RNBQKBNR b KQkq": { + "src": "eco_tsv", + "eco": "B03", + "moves": "1. e4 Nf6 2. e5", + "name": "Alekhine Defense", + "aliases": { + "scid": "Alekhine: 3.d4" + }, + "scid": "B03a", + "isEcoRoot": true + } } ``` -Note that __src__ is labeled "interpolated", meaning it wasn't derived directly from either of the originating three sources: __eco_tsv__, __eco_js__, or __scid__. -### additional interpolations -It is often desirable to have every move subsequence that appears within an opening to have an entry in the database as well. For example, the move sequence for the "Queen's Gambit Declined: Exchange Variation" is "1. d4 Nf6 2. c4 e6 3. Nc3 d5 4. cxd5"; however there is no opening book entry for the subsequence "1. d4 Nf6 2. c4 e6 3. Nc3 d5". This leaves a gap between "1. d4 Nf6 2. c4 e6 3. Nc3" ("Queen's Pawn: Neo-Indian") and the current variation. To fix this, an interpolation is created for the missing subsequence, "Queen's Pawn: Neo-Indian (i)". +#### Field Descriptions +- **fen** (key): Unique position identifier using Forsyth-Edwards Notation. Can be full FEN (with turn, castling, en passant) or position-only (first field). +- **src**: Data source - `eco_tsv` (authoritative), `eco_js`, `scid`, `eco_wikip`, `wiki_b`, `ct`, `chessGraph`, `chronos`, `icsbot`, `pgn`, or `interpolated` +- **eco**: ECO category code (e.g., "B03") - not unique; many openings share the same ECO code +- **moves**: Standard algebraic notation move sequence (e.g., "1. e4 Nf6 2. e5") +- **name**: Opening name (e.g., "Alekhine Defense") +- **aliases**: Alternative names from other sources (object with source keys) +- **scid**: Extended SCID code when applicable (e.g., "B03a") +- **isEcoRoot**: `true` if this is the canonical root variation for this ECO code -### using interpolated openings -It's a simple matter to merge eco_interpolated.json with eco.json, once the two files are read into a program as JSON objects. See `/tooling/ecoConjoined.mjs` for how to do this. +### Source Hierarchy -# fromTo.json -One or more move sequences can lead to every opening position in eco*.json. Obversely, zero or more opening continuations are available from any opening in eco*.json. The data file `fromTo.json` is an array of arrays holding this information. Each element of the fromTo array is as follows: -1. __from__ position (FEN notation) -2. __to__ position -3. __from__ source (see list at top of page; may include "interpolated") -4. __to__ source +**eco_tsv** (from [lichess](https://github.com/lichess-org/chess-openings)) is the authoritative source and supersedes all conflicts. Other sources are merged with eco_tsv taking precedence. +### Interpolated Openings -# The /tooling folder -*NOTE*: It is recommended to use the [eco.json.tooling](https://github.com/JeffML/eco.json.tooling) project to generate pull request opening data. The files in this project's current /tooling folder will be incorporated there soon, making this a data-only project once again. +"Orphan" variations lack a `from` field (no preceding named variation). Interpolations fill these gaps: -The /tooling folder has scripts for manipulating the data in the JSON files. They have a node.js-compatible ".mjs" extension so that they can be run standalone from the command line. For example: -

> node tooling/ecoConjoin.mjs +```json +{ + "rnbqkbnr/pppppppp/8/4P3/3P4/8/PPP2PPP/RNBQKBNR b KQkq - 0 3": { + "src": "interpolated", + "eco": "B02", + "moves": "1. e4 Nf6 2. e5 Ng8 3. d4", + "name": "Alekhine Defense: Brooklyn Variation (i)", + "scid": "B02l", + "aliases": { + "scid": "Alekhine: Brooklyn Defence (Retreat Variation)" + } + } +} +``` -These can be useful on their own and an aid to understanding how eco.json encodes opening information. +**Key characteristics:** +- **src**: "interpolated" +- **name**: Root name + " (i)" suffix +- **Purpose**: Every move subsequence that appears within an opening has an entry -* ecoConjoin.mjs: joins together all eco*.json files into on monolithic eco.json files (just like the old days) -* readJsonFile: reads a JSON formatted file and returns a JSON object (or array) -* from.mjs/to.mjs: given a FEN argument, these scripts will return variations from the current FEN position, preceeding or following the current position. It uses `fromTo.json` data. -* findOpeningByFen: takes a FEN command-line argument(s) and outputs the opening + from + to variations. +For example, if "Queen's Gambit Declined: Exchange Variation" is "1. d4 Nf6 2. c4 e6 3. Nc3 d5 4. cxd5" but no entry exists for "1. d4 Nf6 2. c4 e6 3. Nc3 d5", an interpolated opening is created to bridge the gap. -# Acknowledgements +### FEN as Primary Key + +- FEN strings uniquely identify positions +- Move sequences and names are NOT unique identifiers +- Multiple move orders (transpositions) can reach the same FEN +- Always use FEN for lookups, not moves or names + +### Data Sources + +This data is a collation of several chess opening databases: + +- **eco_tsv**: [lichess chess-openings](https://github.com/lichess-org/chess-openings) (Jun. 2025) +- **eco_js**: Original eco.json data from several years ago +- **scid**: [SCID project](https://scid.sourceforge.net/) database +- **eco_wikip**: [Wikipedia List of Chess Openings](https://en.wikipedia.org/wiki/List_of_chess_openings) (Aug. 2024) +- **wiki_b**: [Wikibooks Chess Opening Theory](https://en.wikibooks.org/wiki/Chess_Opening_Theory) (Nov. 2024) +- **ct**: [ChessTempo](https://chesstempo.com/) (Jun 2025) +- **chessGraph**: [chess-graph](https://github.com/Destaq/chess-graph) (Jun 2025) +- **chronos**: chronos eco.pgn (Jun 2025) +- **icsbot**: [icsbot](https://github.com/seberg/icsbot/blob/master/misc/eco.txt) (Jun 2025) +- **pgn**: Various PGN file databases (Jun 2025) + +## Performance Tips + +1. **Reuse loaded data**: Call `openingBook()` once and reuse the result +2. **Pre-build position book**: Create once with `getPositionBook()` if doing multiple lookups +3. **Cache transitions**: `getFromTos()` caches the index automatically +4. **Pass openings**: Provide pre-loaded openings to `getFromTos()` to skip re-fetching + +```typescript +// Efficient pattern +const openings = await openingBook(); +const posBook = getPositionBook(openings); + +for (const fen of fens) { + const opening = findOpening(openings, fen, posBook); + const transitions = await getFromTos(fen, openings); +} +``` + +## Acknowledgements Thanks to [@niklasf](https://github.com/niklasf) for [eco](https://github.com/niklasf/eco). -Credit goes to Shane Hudson for the original SCID opening data +Credit goes to Shane Hudson for the original SCID opening data. Original eco.json data was compiled by Γ–mΓΌr YanΔ±koğlu. -# Implementations -[Fenster](https://fensterchess.com) derives its opening book from __eco.json__. +## Contributing + +This package provides read-only utilities for consuming opening data. For data maintenance and updates: + +- **Data source**: [eco.json repository](https://github.com/JeffML/eco.json) +- **Data tooling**: [eco.json.tooling repository](https://github.com/JeffML/eco.json.tooling) + +## License +MIT -# References +## Related Projects -This data is a collation of several chess opening databases, as follows: -* __eco_tsv__: Source: [lichess](https://github.com/lichess-org/chess-openings). (Jun. 2025) -* __eco_js__: The original eco.json data from several years ago, which contains some openings not in __eco_tsv__ -* __scid__: A database that's part of a [sourceforge project](https://scid.sourceforge.net/), pulled via Waterford Chess Club's [website](https://watfordchessclub.org/images/downloads/scid.eco). SCID codes extend ECO, and opening names vary. -* __eco_wikip__: Opening data from the Wikipedia page at https://en.wikipedia.org/wiki/List_of_chess_openings (Aug. 2024) -* __wiki_b__: Opening data from the Wikibooks pages at https://en.wikibooks.org/wiki/Chess_Opening_Theory (Nov. 2024) -* __eco_wikip.g__: Opening data from the Wikipedia List of Chess Gambits at https://en.wikipedia.org/wiki/List_of_chess_gambits (May 2025) -* __ct__: Opening data from [ChessTempo](https://chesstempo.com/) (Jun 2025) -* __chessGraph__: Opening data from [chess-graph](https://github.com/Destaq/chess-graph) (Jun 2025) -* __chronos__: Opening data (all aliases) from [chronos eco.pgn](file:///home/chronos/u-a5d83366612aa8feeee6083530d5bb7f7b8939a9/MyFiles/Downloads/eco.pgn) (Jun 2025) -* __icsbot__: Opening data from [icsbot](https://github.com/seberg/icsbot/blob/master/misc/eco.txt) (Jun 2025) -* __pgn__: Opening data gleaned from various PGN file databases, using the `pgnOpeningAnalyzer` of [eco.json.tooling](https://github.com/JeffML/eco.json.tooling) \ No newline at end of file +- [eco.json](https://github.com/JeffML/eco.json) - Chess opening database +- [fensterchess](https://fensterchess.com) - Chess opening research tool diff --git a/package.json b/package.json index d3d8423..9abdc70 100644 --- a/package.json +++ b/package.json @@ -12,8 +12,7 @@ } }, "files": [ - "dist", - "NPM_README.md" + "dist" ], "scripts": { "build": "tsc", From d9ba217def1632e73374a9c25f790eb5647f0afb Mon Sep 17 00:00:00 2001 From: Jeff Lowery Date: Mon, 8 Dec 2025 11:11:13 -0800 Subject: [PATCH 5/8] Add badges to README - NPM version and downloads - MIT license badge - TypeScript and Node.js version badges - Bundle size (bundlephobia) - GitHub stars --- README.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.md b/README.md index 9b6f8d7..03acfe1 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,13 @@ # @chess-openings/eco.json +[![npm version](https://badge.fury.io/js/%40chess-openings%2Feco.json.svg)](https://www.npmjs.com/package/@chess-openings/eco.json) +[![npm downloads](https://img.shields.io/npm/dm/@chess-openings/eco.json.svg)](https://www.npmjs.com/package/@chess-openings/eco.json) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +[![TypeScript](https://img.shields.io/badge/TypeScript-5.3-blue.svg)](https://www.typescriptlang.org/) +[![Node.js](https://img.shields.io/badge/node-%3E%3D18.0.0-brightgreen.svg)](https://nodejs.org/) +[![Bundle Size](https://img.shields.io/bundlephobia/minzip/@chess-openings/eco.json)](https://bundlephobia.com/package/@chess-openings/eco.json) +[![GitHub stars](https://img.shields.io/github/stars/JeffML/eco.json.svg?style=social&label=Star)](https://github.com/JeffML/eco.json) + TypeScript utilities for consuming chess opening data from the eco.json database. ## Features From a084323a5dcdd56e16983f5e281f7552ccb1b680 Mon Sep 17 00:00:00 2001 From: Jeff Lowery Date: Mon, 8 Dec 2025 11:11:46 -0800 Subject: [PATCH 6/8] updated README --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 03acfe1..452070c 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,7 @@ Each opening is keyed by **FEN** (Forsyth-Edwards Notation), which uniquely iden ``` **Key characteristics:** + - **src**: "interpolated" - **name**: Root name + " (i)" suffix - **Purpose**: Every move subsequence that appears within an opening has an entry From 219f7dbf8a485df6d15968161c492bc9a27bac08 Mon Sep 17 00:00:00 2001 From: Jeff Lowery Date: Mon, 8 Dec 2025 11:14:53 -0800 Subject: [PATCH 7/8] minor tweaks to README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 452070c..14e5be9 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,11 @@ [![Bundle Size](https://img.shields.io/bundlephobia/minzip/@chess-openings/eco.json)](https://bundlephobia.com/package/@chess-openings/eco.json) [![GitHub stars](https://img.shields.io/github/stars/JeffML/eco.json.svg?style=social&label=Star)](https://github.com/JeffML/eco.json) -TypeScript utilities for consuming chess opening data from the eco.json database. +TypeScript utilities for consuming chess opening data from the eco.json data files. ## Features -- 🎯 **12,000+ chess openings** with ECO codes, move sequences, and names +- 🎯 **12,000+ chess openings** with ECO codes, move sequences, and names (plus aliases) - πŸ” **FEN-based lookup** with automatic position-only fallback - 🌐 **On-demand data loading** from GitHub (no bundled files) - πŸ”„ **Opening transitions** with next/previous position navigation From 712f7c79cfcb80c10700a1c18038171f0df813dd Mon Sep 17 00:00:00 2001 From: Jeff Lowery Date: Mon, 8 Dec 2025 11:19:18 -0800 Subject: [PATCH 8/8] Release v1.0.0 Promote from beta to stable release. Package includes: - 9 methods for consuming chess opening data - Complete TypeScript support - On-demand data loading from GitHub - 12,000+ chess openings with ECO codes - Opening transitions and query utilities --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 9abdc70..10e5646 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@chess-openings/eco.json", - "version": "0.1.0-beta.1", + "version": "1.0.0", "description": "Read-only utilities for consuming chess opening data from eco.json", "type": "module", "main": "./dist/src/index.js",