Skip to content
This repository was archived by the owner on Mar 5, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 14 additions & 14 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,33 @@ on:
push:
branches: [master]
paths:
- 'package.json'
- 'src/**'
- 'methods/**'
- "package.json"
- "src/**"
- "methods/**"

jobs:
publish:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
registry-url: 'https://registry.npmjs.org'
node-version: "18"
cache: "npm"
registry-url: "https://registry.npmjs.org"

- name: Install dependencies
run: npm ci

- name: Type check
run: npm run type-check

- name: Build
run: npm run build

- name: Check if version changed
id: version-check
run: |
Expand All @@ -45,13 +45,13 @@ jobs:
echo "changed=false" >> $GITHUB_OUTPUT
echo "Version unchanged at $PACKAGE_VERSION"
fi

- name: Publish to NPM
if: steps.version-check.outputs.changed == 'true'
run: npm publish --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

- name: Create Git tag
if: steps.version-check.outputs.changed == 'true'
run: |
Expand Down
67 changes: 67 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,73 @@ const opening = findOpening(openings, fen, posBook);

**Returns:** `Opening | undefined`

#### `lookupByMoves(chess, openingBook, options?)`

Finds an opening by walking backward through move history. Works with any chess library implementing the `ChessGameLike` interface (chess.js, @chess-pgn/chess-pgn, etc.).

This function tries to find an opening match at the current position. If no match is found, it undoes moves one by one and tries again, allowing it to find the nearest named opening even when the current position has moved beyond standard opening theory.

```typescript
import { ChessPGN } from "@chess-pgn/chess-pgn";
import {
openingBook,
lookupByMoves,
getPositionBook,
} from "@chess-openings/eco.json";

const chess = new ChessPGN();
chess.loadPgn("1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6");

const openings = await openingBook();
const posBook = getPositionBook(openings);

const result = lookupByMoves(chess, openings, { positionBook: posBook });

console.log(result.opening?.name); // "Ruy Lopez"
console.log(result.movesBack); // 0 (exact match at current position)
```

**Parameters:**

- `chess: ChessGameLike` - Chess game instance (must have `fen()`, `undo()`, `load()` methods)
- `openingBook: OpeningCollection` - The opening database
- `options?: LookupByMovesOptions`
- `maxMovesBack?: number` - Maximum moves to walk backward (default: 50 plies / move 25)
- `positionBook?: PositionBook` - Position book for better matching

**Returns:** `LookupByMovesResult`

- `opening: Opening | undefined` - The opening found (or undefined)
- `movesBack: number` - Number of moves walked backward (0 = exact match)

**ChessGameLike Interface:**

Your chess library must implement:

```typescript
interface ChessGameLike {
fen(): string; // Returns current FEN
undo(): unknown; // Undoes last move (returns truthy on success, falsy/throws when no moves)
load(fen: string): void; // Loads position from FEN
}
```

Compatible libraries: chess.js, @chess-pgn/chess-pgn, and others with similar APIs.

**Example - Position beyond opening theory:**

```typescript
// Many moves deep into a game
chess.loadPgn(
"1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Na5"
);

const result = lookupByMoves(chess, openings, { maxMovesBack: 10 });

console.log(result.opening?.name); // "Ruy Lopez: Closed"
console.log(result.movesBack); // 3 (walked back 3 moves to find opening)
```

#### `getPositionBook(openingBook)`

Creates a position-only FEN → full FEN array mapping.
Expand Down
68 changes: 68 additions & 0 deletions examples/lookupByMoves.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/**
* Example usage of lookupByMoves with @chess-pgn/chess-pgn
*
* This demonstrates how to find chess openings from move sequences
* using the backward search algorithm.
*/

import { ChessPGN } from "@chess-pgn/chess-pgn";
import {
openingBook,
lookupByMoves,
getPositionBook,
} from "@chess-openings/eco.json";

async function main() {
// Load opening data
const openings = await openingBook();
const posBook = getPositionBook(openings);

// Example 1: Exact opening match
const chess1 = new ChessPGN();
chess1.loadPgn("1. e4 e5 2. Nf3 Nc6 3. Bb5");

const result1 = lookupByMoves(chess1, openings, { positionBook: posBook });
console.log("Example 1 - Ruy Lopez:");
console.log(` Opening: ${result1.opening?.name}`);
console.log(` ECO: ${result1.opening?.eco}`);
console.log(` Moves back: ${result1.movesBack}`);
console.log();

// Example 2: Position beyond opening theory
const chess2 = new ChessPGN();
chess2.loadPgn(
"1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Na5"
);

const result2 = lookupByMoves(chess2, openings, {
positionBook: posBook,
maxMovesBack: 10,
});
console.log("Example 2 - Deep in Ruy Lopez:");
console.log(` Opening: ${result2.opening?.name}`);
console.log(` ECO: ${result2.opening?.eco}`);
console.log(` Moves back: ${result2.movesBack}`);
console.log();

// Example 3: Uncommon opening
const chess3 = new ChessPGN();
chess3.loadPgn("1. f4 e5 2. fxe5 d6 3. exd6 Bxd6");

const result3 = lookupByMoves(chess3, openings, { positionBook: posBook });
console.log("Example 3 - From Gambit:");
console.log(` Opening: ${result3.opening?.name}`);
console.log(` ECO: ${result3.opening?.eco}`);
console.log(` Moves back: ${result3.movesBack}`);
console.log();

// Example 4: Non-opening position
const chess4 = new ChessPGN();
chess4.loadPgn("1. a3 b6 2. b4 c5 3. c3 d5");

const result4 = lookupByMoves(chess4, openings, { positionBook: posBook });
console.log("Example 4 - Rare line:");
console.log(` Opening: ${result4.opening?.name || "Not found"}`);
console.log(` Moves back: ${result4.movesBack}`);
}

main().catch(console.error);
135 changes: 135 additions & 0 deletions methods/findOpening.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,138 @@ export function findOpening(

return opening;
}

/**
* Minimal interface that chess libraries must implement to use lookupByMoves.
* Compatible with chess.js, @chess-pgn/chess-pgn, and similar libraries.
*/
export interface ChessGameLike {
/** Returns the current position as a FEN string */
fen(): string;

/** Undoes the last move. Returns null/false/throws when no moves to undo. */
undo(): unknown;

/** Loads a position from FEN */
load(fen: string): void;
}

/**
* Result of looking up an opening by moves
*/
export interface LookupByMovesResult {
/** The opening found, or undefined if no opening matches */
opening: Opening | undefined;

/** Number of moves walked backward to find the opening (0 = exact match) */
movesBack: number;
}

/**
* Options for lookupByMoves
*/
export interface LookupByMovesOptions {
/** Maximum number of moves to walk backward (default: 50 plies / move 25) */
maxMovesBack?: number;

/** Position book for fallback matching (optional, improves match rate) */
positionBook?: PositionBook;
}

/**
* Finds a chess opening by walking backward through move history.
* Works with any chess library implementing the ChessGameLike interface.
*
* This function tries to find an opening match at the current position.
* If no match is found, it undoes moves one by one and tries again,
* allowing it to find the nearest named opening even when the current
* position has moved beyond standard opening theory.
*
* The chess game state is restored to its original position after the search.
*
* @param chess - A chess game instance (chess.js, chessPGN, etc.)
* @param openingBook - The opening collection to search
* @param options - Search options (maxMovesBack, positionBook)
* @returns Result containing the opening and number of moves walked back
*
* @example
* ```typescript
* import { ChessPGN } from '@chess-pgn/chess-pgn';
* import { openingBook, lookupByMoves } from '@chess-openings/eco.json';
*
* const chess = new ChessPGN();
* chess.loadPgn('1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6');
*
* const result = lookupByMoves(chess, await openingBook());
* console.log(result.opening?.name); // "Ruy Lopez"
* console.log(result.movesBack); // 0 (exact match)
* ```
*
* @example
* ```typescript
* // Position beyond standard opening theory
* chess.loadPgn('1. e4 e5 2. Nf3 Nc6 3. Bb5 a6 4. Ba4 Nf6 5. O-O Be7 6. Re1 b5 7. Bb3 d6 8. c3 O-O 9. h3 Na5');
*
* const result = lookupByMoves(chess, await openingBook(), { maxMovesBack: 10 });
* console.log(result.opening?.name); // "Ruy Lopez: Closed"
* console.log(result.movesBack); // 3 (walked back 3 moves to find opening)
* ```
*/
export function lookupByMoves(
chess: ChessGameLike,
openingBook: OpeningCollection,
options?: LookupByMovesOptions
): LookupByMovesResult {
const startingFen = chess.fen();
const startPly = options?.maxMovesBack ?? 50; // Start searching at ply 50 (move 25)

try {
// Extract move number and turn from FEN (e.g., "rnbq... w KQkq - 0 45")
const fenParts = startingFen.split(" ");
const moveNumber = parseInt(fenParts[5] || "1", 10);
const isWhiteTurn = fenParts[1] === "w";

// Calculate current ply: (moveNumber - 1) * 2 + (isWhiteTurn ? 0 : 1)
const currentPly = (moveNumber - 1) * 2 + (isWhiteTurn ? 0 : 1);

// If game is longer than startPly, undo back to startPly
if (currentPly > startPly) {
for (let i = 0; i < currentPly - startPly; i++) {
chess.undo();
}
}

// Now walk backward from startPly (or current position if shorter)
let movesBack = currentPly > startPly ? currentPly - startPly : 0;

while (true) {
const opening = findOpening(
openingBook,
chess.fen(),
options?.positionBook
);

if (opening) {
return { opening, movesBack };
}

// Try to undo - if it fails/returns falsy, no more moves
const undoResult = chess.undo();
if (undoResult === null || undoResult === false) {
break;
}

movesBack++;
}

// No opening found
return { opening: undefined, movesBack: 0 };
} catch (e) {
// Some libraries throw when undo() is called with no moves
// Return no opening found
return { opening: undefined, movesBack: 0 };
} finally {
// Always restore original position
chess.load(startingFen);
}
}
15 changes: 13 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@chess-openings/eco.json",
"version": "1.0.3",
"version": "1.1.0",
"description": "Read-only utilities for consuming chess opening data from eco.json",
"type": "module",
"main": "./dist/src/index.js",
Expand Down Expand Up @@ -41,6 +41,7 @@
},
"homepage": "https://github.com/JeffML/eco.json#readme",
"devDependencies": {
"@chess-pgn/chess-pgn": "^1.0.1",
"typescript": "^5.3.0"
},
"engines": {
Expand Down
Loading