Add tactic position generators and tests
This commit is contained in:
@@ -0,0 +1,54 @@
|
||||
# Tactical and Gambit Fixture Sets
|
||||
|
||||
This directory collects JSON fixtures for automated regression tests and training data generation.
|
||||
Each file contains narrowly scoped cases for a single tactical motif or gambit family so that Jest
|
||||
or other tooling can load them directly without hard-coding FEN strings in test suites.
|
||||
|
||||
## File naming
|
||||
- `tactics/<pattern>.json` – Examples for one tactical motif. Each file follows the same schema.
|
||||
- `gambits.json` – Openings that should be recognised by move-sequence matching.
|
||||
|
||||
## Common schema
|
||||
Every tactical case shares the following shape:
|
||||
|
||||
```json
|
||||
{
|
||||
"patternType": "PIN",
|
||||
"description": "Short explanation of the motif and what the move achieves.",
|
||||
"cases": [
|
||||
{
|
||||
"id": "unique-string",
|
||||
"initialFen": "...",
|
||||
"sideToMove": "white",
|
||||
"bestMove": { "san": "Bb5", "uci": "f1b5" },
|
||||
"resultingFen": "...",
|
||||
"expectedPattern": {
|
||||
"type": "PIN",
|
||||
"side": "white",
|
||||
"attackerSquares": ["b5"],
|
||||
"targetSquares": ["c6"],
|
||||
"keySquares": ["e8"],
|
||||
"explanationKey": "pin_to_king"
|
||||
},
|
||||
"context": "Optional PGN fragment or free-text rationale.",
|
||||
"tags": ["opening", "classical"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
This layout aligns with the `TacticalPattern` interface defined in the library contract and keeps
|
||||
enough metadata for round-trip testing: the move that produces the tactic, the resulting position,
|
||||
and the expected pattern payload.
|
||||
|
||||
## Using the fixtures in tests
|
||||
- Load the JSON files and iterate through `cases` to drive `detectTactics`,
|
||||
`findTacticalOpportunitiesForSide`, or higher-level exercise generators.
|
||||
- The `initialFen` represents the position **before** the critical move is played.
|
||||
- Apply `bestMove` to reach `resultingFen`, then assert that the reported pattern matches
|
||||
`expectedPattern`.
|
||||
|
||||
## Adding more cases
|
||||
When adding new fixtures, prefer minimal positions that isolate the intended motif so failures
|
||||
are easy to debug. Include a short `context` note so the scenario can be recreated or visualised
|
||||
quickly when a test fails.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"gambits": [
|
||||
{
|
||||
"id": "kings_gambit",
|
||||
"name": "King's Gambit",
|
||||
"ecoCodes": ["C30", "C31", "C32", "C33"],
|
||||
"side": "white",
|
||||
"moveSequenceSan": ["e4", "e5", "f4"]
|
||||
},
|
||||
{
|
||||
"id": "evans_gambit",
|
||||
"name": "Evans Gambit",
|
||||
"ecoCodes": ["C51"],
|
||||
"side": "white",
|
||||
"moveSequenceSan": ["e4", "e5", "Nf3", "Nc6", "Bc4", "Bc5", "b4"]
|
||||
},
|
||||
{
|
||||
"id": "smith_morra_gambit",
|
||||
"name": "Smith-Morra Gambit",
|
||||
"ecoCodes": ["B21"],
|
||||
"side": "white",
|
||||
"moveSequenceSan": ["e4", "c5", "d4", "cxd4", "c3"]
|
||||
},
|
||||
{
|
||||
"id": "benko_gambit",
|
||||
"name": "Benko Gambit",
|
||||
"ecoCodes": ["A57", "A58", "A59"],
|
||||
"side": "black",
|
||||
"moveSequenceSan": ["d4", "Nf6", "c4", "c5", "d5", "b5"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"patternType": "BACK_RANK_WEAKNESS",
|
||||
"description": "Classic back-rank mate with no escape squares for the king.",
|
||||
"cases": [
|
||||
{
|
||||
"id": "back_rank_rook_mate",
|
||||
"initialFen": "6k1/5ppp/8/8/8/8/5PPP/R5K1 w - - 0 1",
|
||||
"sideToMove": "white",
|
||||
"bestMove": { "san": "Ra8#", "uci": "a1a8" },
|
||||
"resultingFen": "R5k1/5ppp/8/8/8/8/5PPP/6K1 b - - 1 1",
|
||||
"expectedPattern": {
|
||||
"type": "BACK_RANK_WEAKNESS",
|
||||
"side": "white",
|
||||
"attackerSquares": ["a8"],
|
||||
"targetSquares": ["g8"],
|
||||
"explanationKey": "back_rank_mate"
|
||||
},
|
||||
"context": "No luft and the king is boxed in; the rook delivers mate on the back rank.",
|
||||
"tags": ["mate", "fundamental"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"patternType": "DISCOVERED_CHECK",
|
||||
"description": "Bishop steps aside to reveal a rook check along the open file.",
|
||||
"cases": [
|
||||
{
|
||||
"id": "discovered_check_rook_on_open_file",
|
||||
"initialFen": "4k3/8/8/8/8/8/4B3/4R1K1 w - - 0 1",
|
||||
"sideToMove": "white",
|
||||
"bestMove": { "san": "Bb5+", "uci": "e2b5" },
|
||||
"resultingFen": "4k3/8/8/1B6/8/8/8/4R1K1 b - - 1 1",
|
||||
"expectedPattern": {
|
||||
"type": "DISCOVERED_CHECK",
|
||||
"side": "white",
|
||||
"attackerSquares": ["e1"],
|
||||
"targetSquares": ["e8"],
|
||||
"keySquares": ["b5"],
|
||||
"explanationKey": "rook_file_discovery"
|
||||
},
|
||||
"context": "Moving the bishop from e2 to b5 exposes the rook on e1, delivering check on the e-file.",
|
||||
"tags": ["discovery", "rook_file"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"patternType": "DOUBLE_ATTACK",
|
||||
"description": "Queen move that checks the king while targeting an undefended rook.",
|
||||
"cases": [
|
||||
{
|
||||
"id": "double_attack_queen_check_and_rook",
|
||||
"initialFen": "r3k3/8/8/8/8/8/8/3Q2K1 w - - 0 1",
|
||||
"sideToMove": "white",
|
||||
"bestMove": { "san": "Qa4+", "uci": "d1a4" },
|
||||
"resultingFen": "r3k3/8/8/8/Q7/8/8/6K1 b - - 1 1",
|
||||
"expectedPattern": {
|
||||
"type": "DOUBLE_ATTACK",
|
||||
"side": "white",
|
||||
"attackerSquares": ["a4"],
|
||||
"targetSquares": ["e8", "a8"],
|
||||
"explanationKey": "queen_check_and_rook"
|
||||
},
|
||||
"context": "A single queen move gives check on e8 and simultaneously eyes the rook on a8.",
|
||||
"tags": ["queen", "double_attack"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"patternType": "FORK",
|
||||
"description": "Knight fork hitting king and queen simultaneously.",
|
||||
"cases": [
|
||||
{
|
||||
"id": "fork_knight_checks_king_and_queen",
|
||||
"initialFen": "4k3/5q2/8/5N2/8/8/8/6K1 w - - 0 1",
|
||||
"sideToMove": "white",
|
||||
"bestMove": { "san": "Nd6+", "uci": "f5d6" },
|
||||
"resultingFen": "4k3/5q2/3N4/8/8/8/8/6K1 b - - 1 1",
|
||||
"expectedPattern": {
|
||||
"type": "FORK",
|
||||
"side": "white",
|
||||
"attackerSquares": ["d6"],
|
||||
"targetSquares": ["e8", "f7"],
|
||||
"explanationKey": "knight_fork"
|
||||
},
|
||||
"context": "The knight hop gives check on e8 while simultaneously attacking the queen on f7.",
|
||||
"tags": ["tactics", "knight"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"patternType": "OVERLOADING",
|
||||
"description": "The defending rook on f8 is overloaded, forced to choose between guarding the back rank and protecting f7.",
|
||||
"cases": [
|
||||
{
|
||||
"id": "overloading_rook_on_f8",
|
||||
"initialFen": "5rk1/5p2/8/3Q4/8/8/8/4R1K1 w - - 0 1",
|
||||
"sideToMove": "white",
|
||||
"bestMove": { "san": "Qxf7+", "uci": "d5f7" },
|
||||
"resultingFen": "5rk1/5Q2/8/8/8/8/8/4R1K1 b - - 0 1",
|
||||
"expectedPattern": {
|
||||
"type": "OVERLOADING",
|
||||
"side": "white",
|
||||
"attackerSquares": ["f7"],
|
||||
"targetSquares": ["f8", "f7"],
|
||||
"explanationKey": "back_rank_vs_defense"
|
||||
},
|
||||
"context": "Capturing on f7 lures the only defender; after ...Rxf7 the back rank collapses to Re8#.",
|
||||
"tags": ["overloading", "back_rank_setup"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"patternType": "PIN",
|
||||
"description": "Classical bishop pin on the Ruy Lopez diagonal, tying the c6-knight to the e8-king.",
|
||||
"cases": [
|
||||
{
|
||||
"id": "pin_ruy_lopez_knight_to_king",
|
||||
"initialFen": "r1bqkbnr/pppp1ppp/2n5/4p3/4P3/5N2/PPPP1PPP/RNBQKB1R w KQkq - 2 3",
|
||||
"sideToMove": "white",
|
||||
"bestMove": { "san": "Bb5", "uci": "f1b5" },
|
||||
"resultingFen": "r1bqkbnr/pppp1ppp/2n5/1B2p3/4P3/5N2/PPPP1PPP/RNBQK2R b KQkq - 3 3",
|
||||
"expectedPattern": {
|
||||
"type": "PIN",
|
||||
"side": "white",
|
||||
"attackerSquares": ["b5"],
|
||||
"targetSquares": ["c6"],
|
||||
"keySquares": ["e8"],
|
||||
"explanationKey": "pin_to_king"
|
||||
},
|
||||
"context": "1.e4 e5 2.Nf3 Nc6 3.Bb5 with the knight pinned to the king.",
|
||||
"tags": ["opening", "ruy_lopez"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"patternType": "SKEWER",
|
||||
"description": "Bishop skewer forcing the king off the diagonal so the trailing queen can be captured.",
|
||||
"cases": [
|
||||
{
|
||||
"id": "skewer_bishop_on_long_diagonal",
|
||||
"initialFen": "7q/6k1/8/8/8/8/1B6/6K1 w - - 0 1",
|
||||
"sideToMove": "white",
|
||||
"bestMove": { "san": "Bc3+", "uci": "b2c3" },
|
||||
"resultingFen": "7q/6k1/8/8/8/2B5/8/6K1 b - - 1 1",
|
||||
"expectedPattern": {
|
||||
"type": "SKEWER",
|
||||
"side": "white",
|
||||
"attackerSquares": ["c3"],
|
||||
"targetSquares": ["g7", "h8"],
|
||||
"keySquares": ["g7"],
|
||||
"explanationKey": "king_in_front_of_queen"
|
||||
},
|
||||
"context": "Minimal material: the move Bc3+ attacks the king on g7; after the king moves the queen on h8 is lost.",
|
||||
"tags": ["endgame", "long_diagonal"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"patternType": "TRAPPED_PIECE",
|
||||
"description": "Illustrates a bishop trapped in the corner with no safe squares.",
|
||||
"cases": [
|
||||
{
|
||||
"id": "trapped_corner_bishop",
|
||||
"initialFen": "6kb/6P1/5K2/8/8/8/8/8 w - - 0 1",
|
||||
"sideToMove": "white",
|
||||
"bestMove": { "san": "gxh8=Q+", "uci": "g7h8q" },
|
||||
"resultingFen": "6kQ/8/5K2/8/8/8/8/8 b - - 0 1",
|
||||
"expectedPattern": {
|
||||
"type": "TRAPPED_PIECE",
|
||||
"side": "white",
|
||||
"attackerSquares": ["g7"],
|
||||
"targetSquares": ["h8"],
|
||||
"explanationKey": "corner_trap"
|
||||
},
|
||||
"context": "The black bishop on h8 cannot escape because of the pawn on g7 and the king on f6.",
|
||||
"tags": ["endgame", "conversion"]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user