@echecs/position 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +63 -0
- package/LICENSE +21 -0
- package/README.md +158 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/index.d.ts +25 -0
- package/dist/internal/index.js +1 -0
- package/dist/internal-BOZCKSNm.js +1 -0
- package/dist/internal-BOZCKSNm.js.map +1 -0
- package/dist/types-DpJMtqKW.d.ts +32 -0
- package/package.json +78 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [1.0.2] - 2026-03-19
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- Lowercase hex literals in Zobrist table seed constants.
|
|
8
|
+
|
|
9
|
+
## [1.0.1] - 2026-03-19
|
|
10
|
+
|
|
11
|
+
### Changed
|
|
12
|
+
|
|
13
|
+
- Extracted primitive constants (`COLORS`, `FILES`, `RANKS`, `PIECE_TYPES`,
|
|
14
|
+
`SQUARES`) into `src/primitives.ts` to eliminate duplication in
|
|
15
|
+
`src/internal/zobrist.ts`. No API or behavior change.
|
|
16
|
+
|
|
17
|
+
## [1.0.0] - 2026-03-19
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `Position` class replacing the `Position` interface — immutable value object
|
|
22
|
+
with private board state and a clean query API
|
|
23
|
+
- Constructor signatures: `new Position()` (starting position),
|
|
24
|
+
`new Position(board)`, and `new Position(board, options)`
|
|
25
|
+
- Getters: `castlingRights`, `enPassantSquare`, `fullmoveNumber`,
|
|
26
|
+
`halfmoveClock`, `hash`, `isCheck`, `isInsufficientMaterial`, `isValid`,
|
|
27
|
+
`turn`
|
|
28
|
+
- Methods: `attackers`, `findPiece`, `isAttacked`, `piece`, `pieces`
|
|
29
|
+
- Zobrist hash (`hash` getter) — deterministic 16-character hex string for
|
|
30
|
+
position identity, computed once and cached
|
|
31
|
+
- `isValid` — validates king presence, no pawns on back ranks, and that the side
|
|
32
|
+
not to move is not in check
|
|
33
|
+
- `isInsufficientMaterial` — FIDE-compliant: K vs K, K+B vs K, K+N vs K, and K+B
|
|
34
|
+
vs K+B with same-color bishops
|
|
35
|
+
- Runtime constants: `COLORS`, `FILES`, `RANKS`, `PIECE_TYPES`, `SQUARES`
|
|
36
|
+
- `PositionOptions` type exported for use in consuming code
|
|
37
|
+
- `Move` and `PromotionPieceType` types exported for companion packages
|
|
38
|
+
- MIT `LICENSE` file
|
|
39
|
+
- `README.md` with installation, quick start, and full API reference
|
|
40
|
+
- GitHub Actions CI/CD workflows (lint, test, format, docs, release, auto-merge)
|
|
41
|
+
|
|
42
|
+
### Changed
|
|
43
|
+
|
|
44
|
+
- `SquareColor` values changed from `'l'`/`'d'` to `'light'`/`'dark'`
|
|
45
|
+
- `STARTING_POSITION` is now a `Position` instance instead of a plain object
|
|
46
|
+
- Standalone query functions (`isAttacked`, `isCheck`, `piece`, `pieces`)
|
|
47
|
+
removed from public API — use `Position` methods instead
|
|
48
|
+
|
|
49
|
+
### Removed
|
|
50
|
+
|
|
51
|
+
- `Position` interface (replaced by class)
|
|
52
|
+
- `startingBoard` export (implementation detail)
|
|
53
|
+
|
|
54
|
+
## [0.1.0] - 2026-03-18
|
|
55
|
+
|
|
56
|
+
### Added
|
|
57
|
+
|
|
58
|
+
- Initial release with `Position` interface, `EMPTY_BOARD`, `STARTING_POSITION`
|
|
59
|
+
- Internal 0x88 board representation (`./internal` export condition)
|
|
60
|
+
- Standalone query functions: `isAttacked`, `isCheck`, `piece`, `pieces`
|
|
61
|
+
- Square utilities: `squareColor`, `squareFile`, `squareRank`
|
|
62
|
+
- Types: `CastlingRights`, `Color`, `File`, `Move`, `Piece`, `PieceType`,
|
|
63
|
+
`PromotionPieceType`, `Rank`, `Square`, `SquareColor`
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Adrian de la Rosa
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
# Position
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@echecs/position)
|
|
4
|
+
[](https://github.com/mormubis/position/actions/workflows/test.yml)
|
|
5
|
+
[](https://codecov.io/gh/mormubis/position)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
[](https://mormubis.github.io/position/)
|
|
8
|
+
|
|
9
|
+
**Position** is a TypeScript library representing a complete chess position —
|
|
10
|
+
the board, turn, castling rights, en passant square, halfmove clock, and
|
|
11
|
+
fullmove number — as an immutable value object with a clean query API. It is the
|
|
12
|
+
foundational package of the ECHECS monorepo. Zero runtime dependencies.
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @echecs/position
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
```typescript
|
|
23
|
+
import { Position, STARTING_POSITION } from '@echecs/position';
|
|
24
|
+
|
|
25
|
+
// Starting position
|
|
26
|
+
const pos = new Position();
|
|
27
|
+
|
|
28
|
+
console.log(pos.turn); // 'w'
|
|
29
|
+
console.log(pos.fullmoveNumber); // 1
|
|
30
|
+
console.log(pos.isCheck); // false
|
|
31
|
+
|
|
32
|
+
// Query the board
|
|
33
|
+
const piece = pos.piece('e1'); // { color: 'w', type: 'k' }
|
|
34
|
+
const whites = pos.pieces('w'); // Map<Square, Piece> of all white pieces
|
|
35
|
+
|
|
36
|
+
// Find all squares a piece occupies
|
|
37
|
+
const whiteKing = pos.findPiece({ color: 'w', type: 'k' }); // ['e1']
|
|
38
|
+
|
|
39
|
+
// Attack queries
|
|
40
|
+
const attackers = pos.attackers('e5', 'b'); // squares of black pieces attacking e5
|
|
41
|
+
const attacked = pos.isAttacked('f7', 'w'); // true if white attacks f7
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## API
|
|
45
|
+
|
|
46
|
+
Full API reference is available at https://mormubis.github.io/position/
|
|
47
|
+
|
|
48
|
+
### Constructor
|
|
49
|
+
|
|
50
|
+
```ts
|
|
51
|
+
new Position()
|
|
52
|
+
new Position(board: Map<Square, Piece>)
|
|
53
|
+
new Position(board: Map<Square, Piece>, options?: {
|
|
54
|
+
castlingRights?: CastlingRights
|
|
55
|
+
enPassantSquare?: Square
|
|
56
|
+
fullmoveNumber?: number
|
|
57
|
+
halfmoveClock?: number
|
|
58
|
+
turn?: Color
|
|
59
|
+
})
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The no-argument form creates the standard chess starting position. Pass a custom
|
|
63
|
+
`board` map and optional `options` to construct any arbitrary position.
|
|
64
|
+
|
|
65
|
+
### Getters
|
|
66
|
+
|
|
67
|
+
| Getter | Type | Description |
|
|
68
|
+
| ------------------------ | --------------------- | -------------------------------------------------------------------------------------------------------------------------------- |
|
|
69
|
+
| `castlingRights` | `CastlingRights` | Which castling moves remain available |
|
|
70
|
+
| `enPassantSquare` | `Square \| undefined` | En passant target square, if any |
|
|
71
|
+
| `fullmoveNumber` | `number` | Fullmove counter (increments after Black's move) |
|
|
72
|
+
| `halfmoveClock` | `number` | Halfmove clock for the fifty-move rule |
|
|
73
|
+
| `hash` | `string` | Zobrist hash string for position identity |
|
|
74
|
+
| `isCheck` | `boolean` | Whether the side to move is in check |
|
|
75
|
+
| `isInsufficientMaterial` | `boolean` | Whether the position is a FIDE draw by insufficient material (K vs K, K+B vs K, K+N vs K, or K+B vs K+B with same-color bishops) |
|
|
76
|
+
| `isValid` | `boolean` | Whether the position is legally reachable |
|
|
77
|
+
| `turn` | `Color` | Side to move (`'w'` or `'b'`) |
|
|
78
|
+
|
|
79
|
+
### Methods
|
|
80
|
+
|
|
81
|
+
#### `attackers(square, color): Square[]`
|
|
82
|
+
|
|
83
|
+
Returns all squares occupied by pieces of `color` that attack `square`.
|
|
84
|
+
|
|
85
|
+
```typescript
|
|
86
|
+
pos.attackers('e5', 'b'); // e.g. ['d7', 'f6']
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### `findPiece(piece): Square[]`
|
|
90
|
+
|
|
91
|
+
Returns all squares occupied by the given piece.
|
|
92
|
+
|
|
93
|
+
```typescript
|
|
94
|
+
pos.findPiece({ color: 'w', type: 'q' }); // ['d1']
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
#### `isAttacked(square, color): boolean`
|
|
98
|
+
|
|
99
|
+
Returns `true` if any piece of `color` attacks `square`.
|
|
100
|
+
|
|
101
|
+
```typescript
|
|
102
|
+
pos.isAttacked('f3', 'w'); // true if white attacks f3
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
#### `piece(square): Piece | undefined`
|
|
106
|
+
|
|
107
|
+
Returns the piece on `square`, or `undefined` if the square is empty.
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
pos.piece('e1'); // { color: 'w', type: 'k' }
|
|
111
|
+
pos.piece('e5'); // undefined (empty in starting position)
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### `pieces(color?): Map<Square, Piece>`
|
|
115
|
+
|
|
116
|
+
Returns a map of all pieces, optionally filtered by `color`.
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
pos.pieces(); // all 32 pieces in starting position
|
|
120
|
+
pos.pieces('w'); // 16 white pieces
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Constants
|
|
124
|
+
|
|
125
|
+
```typescript
|
|
126
|
+
import {
|
|
127
|
+
COLORS, // ['b', 'w']
|
|
128
|
+
FILES, // ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']
|
|
129
|
+
RANKS, // ['1', '2', '3', '4', '5', '6', '7', '8']
|
|
130
|
+
PIECE_TYPES, // ['b', 'k', 'n', 'p', 'q', 'r']
|
|
131
|
+
SQUARES, // all 64 squares, a8–h1 (rank 8 to rank 1)
|
|
132
|
+
EMPTY_BOARD, // empty Map<Square, Piece>
|
|
133
|
+
STARTING_POSITION, // Position instance for the standard starting position
|
|
134
|
+
} from '@echecs/position';
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Types
|
|
138
|
+
|
|
139
|
+
All types are exported for use in consuming code and companion packages.
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
import type {
|
|
143
|
+
CastlingRights, // { bK: boolean; bQ: boolean; wK: boolean; wQ: boolean }
|
|
144
|
+
Color, // 'w' | 'b'
|
|
145
|
+
File, // 'a' | 'b' | ... | 'h'
|
|
146
|
+
Move, // { from: Square; to: Square; promotion: PromotionPieceType | undefined }
|
|
147
|
+
Piece, // { color: Color; type: PieceType }
|
|
148
|
+
PieceType, // 'b' | 'k' | 'n' | 'p' | 'q' | 'r'
|
|
149
|
+
PositionOptions, // options accepted by the Position constructor
|
|
150
|
+
PromotionPieceType, // 'b' | 'n' | 'q' | 'r'
|
|
151
|
+
Rank, // '1' | '2' | ... | '8'
|
|
152
|
+
Square, // 'a1' | 'a2' | ... | 'h8'
|
|
153
|
+
SquareColor, // 'light' | 'dark'
|
|
154
|
+
} from '@echecs/position';
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
`Move` and `PromotionPieceType` are exported for use by companion packages
|
|
158
|
+
(`@echecs/san`, `@echecs/game`) that build on this foundational type.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { a as Piece, c as PromotionPieceType, d as SquareColor, i as Move, l as Rank, n as Color, o as PieceType, r as File, s as PositionOptions, t as CastlingRights, u as Square } from "./types-DpJMtqKW.js";
|
|
2
|
+
|
|
3
|
+
//#region src/position.d.ts
|
|
4
|
+
declare class Position {
|
|
5
|
+
#private;
|
|
6
|
+
constructor(board?: Map<Square, Piece>, options?: PositionOptions);
|
|
7
|
+
get castlingRights(): CastlingRights;
|
|
8
|
+
get enPassantSquare(): Square | undefined;
|
|
9
|
+
get fullmoveNumber(): number;
|
|
10
|
+
get halfmoveClock(): number;
|
|
11
|
+
/**
|
|
12
|
+
* A Zobrist hash of the position as a 16-character hex string.
|
|
13
|
+
* Computed once and cached. Two positions with the same hash are
|
|
14
|
+
* considered identical (with negligible collision probability).
|
|
15
|
+
*
|
|
16
|
+
* @remarks The hash algorithm and format are not stable across major
|
|
17
|
+
* versions. Do not persist hashes across version upgrades.
|
|
18
|
+
*/
|
|
19
|
+
get hash(): string;
|
|
20
|
+
get isInsufficientMaterial(): boolean;
|
|
21
|
+
get isValid(): boolean;
|
|
22
|
+
get isCheck(): boolean;
|
|
23
|
+
get turn(): Color;
|
|
24
|
+
attackers(square: Square, by: Color): Square[];
|
|
25
|
+
findPiece(piece: Piece): Square[];
|
|
26
|
+
isAttacked(square: Square, by: Color): boolean;
|
|
27
|
+
piece(square: Square): Piece | undefined;
|
|
28
|
+
pieces(color?: Color): Map<Square, Piece>;
|
|
29
|
+
}
|
|
30
|
+
//#endregion
|
|
31
|
+
//#region src/primitives.d.ts
|
|
32
|
+
declare const COLORS: Color[];
|
|
33
|
+
declare const FILES: File[];
|
|
34
|
+
declare const RANKS: Rank[];
|
|
35
|
+
declare const PIECE_TYPES: PieceType[];
|
|
36
|
+
declare const SQUARES: Square[];
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/constants.d.ts
|
|
39
|
+
declare const EMPTY_BOARD: Map<"a1" | "a2" | "a3" | "a4" | "a5" | "a6" | "a7" | "a8" | "b1" | "b2" | "b3" | "b4" | "b5" | "b6" | "b7" | "b8" | "c1" | "c2" | "c3" | "c4" | "c5" | "c6" | "c7" | "c8" | "d1" | "d2" | "d3" | "d4" | "d5" | "d6" | "d7" | "d8" | "e1" | "e2" | "e3" | "e4" | "e5" | "e6" | "e7" | "e8" | "f1" | "f2" | "f3" | "f4" | "f5" | "f6" | "f7" | "f8" | "g1" | "g2" | "g3" | "g4" | "g5" | "g6" | "g7" | "g8" | "h1" | "h2" | "h3" | "h4" | "h5" | "h6" | "h7" | "h8", Piece>;
|
|
40
|
+
declare const STARTING_POSITION: Position;
|
|
41
|
+
//#endregion
|
|
42
|
+
//#region src/squares.d.ts
|
|
43
|
+
declare function squareFile(square: Square): File;
|
|
44
|
+
declare function squareRank(square: Square): Rank;
|
|
45
|
+
/**
|
|
46
|
+
* Returns 'dark' or 'light' for the given square.
|
|
47
|
+
* a1 is dark: file index 0 + rank 1 = 1 (odd) → 'dark'.
|
|
48
|
+
* b1 is light: file index 1 + rank 1 = 2 (even) → 'light'.
|
|
49
|
+
*/
|
|
50
|
+
declare function squareColor(square: Square): SquareColor;
|
|
51
|
+
//#endregion
|
|
52
|
+
export { COLORS, type CastlingRights, type Color, EMPTY_BOARD, FILES, type File, type Move, PIECE_TYPES, type Piece, type PieceType, Position, type PositionOptions, type PromotionPieceType, RANKS, type Rank, SQUARES, STARTING_POSITION, type Square, type SquareColor, squareColor, squareFile, squareRank };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{_ as e,a as t,c as n,f as r,h as i,i as a,l as o,n as s,o as c,p as l,r as u,s as d,t as f,u as p}from"./internal-BOZCKSNm.js";function m(e){return e[0]}function h(e){return e[1]}function g(e){return((e.codePointAt(0)??0)-(`a`.codePointAt(0)??0)+Number.parseInt(e[1]??`1`,10))%2==1?`dark`:`light`}const _=[`r`,`n`,`b`,`q`,`k`,`b`,`n`,`r`],v=[`a`,`b`,`c`,`d`,`e`,`f`,`g`,`h`],y=new Map;for(let[e,t]of _.entries()){let n=v[e];n!==void 0&&(y.set(`${n}1`,{color:`w`,type:t}),y.set(`${n}2`,{color:`w`,type:`p`}),y.set(`${n}7`,{color:`b`,type:`p`}),y.set(`${n}8`,{color:`b`,type:t}))}const b=y,x={castlingRights:{bK:!0,bQ:!0,wK:!0,wQ:!0},enPassantSquare:void 0,fullmoveNumber:1,halfmoveClock:0,turn:`w`};var S=class{#e;#t;#n;#r;#i;#a;#o;constructor(e,t){this.#e=new Map(e??b);let n={...x,...t};this.#t=n.castlingRights,this.#n=n.enPassantSquare,this.#r=n.fullmoveNumber,this.#i=n.halfmoveClock,this.#o=n.turn}get castlingRights(){return this.#t}get enPassantSquare(){return this.#n}get fullmoveNumber(){return this.#r}get halfmoveClock(){return this.#i}get hash(){if(this.#a!==void 0)return this.#a;let e=0n;for(let[t,n]of this.#e)e^=u[t]?.[n.type]?.[n.color]??0n;e^=a[this.#o];for(let[t,n]of Object.entries(this.#t))n&&(e^=f[t]??0n);if(this.#n!==void 0){let t=this.#n[0];e^=s[t]}return this.#a=e.toString(16).padStart(16,`0`),this.#a}get isInsufficientMaterial(){let e=[];for(let[t,n]of this.#e)n.type!==`k`&&e.push([t,n]);if(e.length===0)return!0;if(e.length===1){let t=e[0]?.[1];return t?.type===`b`||t?.type===`n`}if(e.every(([,e])=>e.type===`b`)){let t=e[0];if(t===void 0)return!0;let n=g(t[0]);return e.every(([e])=>g(e)===n)}return!1}get isValid(){let e=0,t=0;for(let[n,r]of this.#e)if(r.type===`k`&&(r.color===`b`?e++:t++),r.type===`p`&&(n[1]===`1`||n[1]===`8`))return!1;if(e!==1||t!==1)return!1;let n=this.#o===`w`?`b`:`w`;for(let[e,t]of this.#e)if(t.type===`k`&&t.color===n&&this.isAttacked(e,this.#o))return!1;return!0}get isCheck(){for(let[e,t]of this.#e)if(t.type===`k`&&t.color===this.#o){let t=this.#o===`w`?`b`:`w`;return this.isAttacked(e,t)}return!1}get turn(){return this.#o}#s(e,t,n,i,a){let o=t-n,s=o+119;if(s<0||s>=240||((p[s]??0)&(r[i.type]??0))===0||i.type===`p`&&(a===`w`&&o>0||a===`b`&&o<0))return!1;let c=l[s]??0;if(c===0)return!0;let u=n+c;for(;u!==t;){if(u&136||e[u]!==void 0)return!1;u+=c}return!0}attackers(t,n){let r=i(this.#e),a=e(t),o=[];for(let[t,i]of this.#e){if(i.color!==n)continue;let s=e(t);this.#s(r,a,s,i,n)&&o.push(t)}return o}findPiece(e){let t=[];for(let[n,r]of this.#e)r.color===e.color&&r.type===e.type&&t.push(n);return t}isAttacked(t,n){let r=i(this.#e),a=e(t);for(let[t,i]of this.#e){if(i.color!==n)continue;let o=e(t);if(this.#s(r,a,o,i,n))return!0}return!1}piece(e){return this.#e.get(e)}pieces(e){if(e===void 0)return new Map(this.#e);let t=new Map;for(let[n,r]of this.#e)r.color===e&&t.set(n,r);return t}};const C=new Map,w=new S;export{t as COLORS,C as EMPTY_BOARD,c as FILES,d as PIECE_TYPES,S as Position,n as RANKS,o as SQUARES,w as STARTING_POSITION,g as squareColor,m as squareFile,h as squareRank};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":["#board","#castlingRights","#enPassantSquare","#fullmoveNumber","#halfmoveClock","#turn","#hash","#isAttackedByPiece"],"sources":["../src/squares.ts","../src/starting-board.ts","../src/position.ts","../src/constants.ts"],"sourcesContent":["import type { File, Rank, Square, SquareColor } from './types.js';\n\nfunction squareFile(square: Square): File {\n return square[0] as File;\n}\n\nfunction squareRank(square: Square): Rank {\n return square[1] as Rank;\n}\n\n/**\n * Returns 'dark' or 'light' for the given square.\n * a1 is dark: file index 0 + rank 1 = 1 (odd) → 'dark'.\n * b1 is light: file index 1 + rank 1 = 2 (even) → 'light'.\n */\nfunction squareColor(square: Square): SquareColor {\n const file = (square.codePointAt(0) ?? 0) - ('a'.codePointAt(0) ?? 0);\n const rank = Number.parseInt(square[1] ?? '1', 10);\n return (file + rank) % 2 === 1 ? 'dark' : 'light';\n}\n\nexport { squareColor, squareFile, squareRank };\n","import type { Piece, Square } from './types.js';\n\nconst BACK_RANK_TYPES = ['r', 'n', 'b', 'q', 'k', 'b', 'n', 'r'] as const;\nconst BACK_RANK_FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'] as const;\n\nconst startingBoardMap = new Map<Square, Piece>();\nfor (const [index, type] of BACK_RANK_TYPES.entries()) {\n const file = BACK_RANK_FILES[index];\n if (file === undefined) {\n continue;\n }\n startingBoardMap.set(`${file}1` as Square, { color: 'w', type });\n startingBoardMap.set(`${file}2` as Square, { color: 'w', type: 'p' });\n startingBoardMap.set(`${file}7` as Square, { color: 'b', type: 'p' });\n startingBoardMap.set(`${file}8` as Square, { color: 'b', type });\n}\n\nexport const startingBoard = startingBoardMap;\n","import {\n ATTACKS,\n CASTLING_TABLE,\n DIFF_OFFSET,\n EP_TABLE,\n OFF_BOARD,\n PIECE_MASKS,\n PIECE_TABLE,\n RAYS,\n TURN_TABLE,\n boardFromMap,\n squareToIndex,\n} from './internal/index.js';\nimport { squareColor } from './squares.js';\nimport { startingBoard } from './starting-board.js';\n\nimport type {\n CastlingRights,\n Color,\n File,\n Piece,\n PositionOptions,\n Square,\n} from './types.js';\n\nconst DEFAULT_OPTIONS: Required<Omit<PositionOptions, 'enPassantSquare'>> &\n Pick<PositionOptions, 'enPassantSquare'> = {\n castlingRights: { bK: true, bQ: true, wK: true, wQ: true },\n enPassantSquare: undefined,\n fullmoveNumber: 1,\n halfmoveClock: 0,\n turn: 'w',\n};\n\nexport class Position {\n readonly #board: Map<Square, Piece>;\n readonly #castlingRights: CastlingRights;\n readonly #enPassantSquare: Square | undefined;\n readonly #fullmoveNumber: number;\n readonly #halfmoveClock: number;\n #hash: string | undefined;\n readonly #turn: Color;\n\n constructor(board?: Map<Square, Piece>, options?: PositionOptions) {\n this.#board = new Map(board ?? startingBoard);\n const options_ = { ...DEFAULT_OPTIONS, ...options };\n this.#castlingRights = options_.castlingRights;\n this.#enPassantSquare = options_.enPassantSquare;\n this.#fullmoveNumber = options_.fullmoveNumber;\n this.#halfmoveClock = options_.halfmoveClock;\n this.#turn = options_.turn;\n }\n\n get castlingRights(): CastlingRights {\n return this.#castlingRights;\n }\n\n get enPassantSquare(): Square | undefined {\n return this.#enPassantSquare;\n }\n\n get fullmoveNumber(): number {\n return this.#fullmoveNumber;\n }\n\n get halfmoveClock(): number {\n return this.#halfmoveClock;\n }\n\n /**\n * A Zobrist hash of the position as a 16-character hex string.\n * Computed once and cached. Two positions with the same hash are\n * considered identical (with negligible collision probability).\n *\n * @remarks The hash algorithm and format are not stable across major\n * versions. Do not persist hashes across version upgrades.\n */\n get hash(): string {\n if (this.#hash !== undefined) {\n return this.#hash;\n }\n\n let h = 0n;\n\n for (const [sq, p] of this.#board) {\n h ^= PIECE_TABLE[sq]?.[p.type]?.[p.color] ?? 0n;\n }\n\n h ^= TURN_TABLE[this.#turn];\n\n for (const [right, active] of Object.entries(this.#castlingRights) as [\n string,\n boolean,\n ][]) {\n if (active) {\n h ^= CASTLING_TABLE[right] ?? 0n;\n }\n }\n\n if (this.#enPassantSquare !== undefined) {\n const file = this.#enPassantSquare[0] as File;\n h ^= EP_TABLE[file];\n }\n\n this.#hash = h.toString(16).padStart(16, '0');\n return this.#hash;\n }\n\n get isInsufficientMaterial(): boolean {\n const nonKingEntries: [Square, Piece][] = [];\n for (const [sq, p] of this.#board) {\n if (p.type !== 'k') {\n nonKingEntries.push([sq, p]);\n }\n }\n\n // K vs K\n if (nonKingEntries.length === 0) {\n return true;\n }\n\n // K vs KB or K vs KN\n if (nonKingEntries.length === 1) {\n const sole = nonKingEntries[0]?.[1];\n return sole?.type === 'b' || sole?.type === 'n';\n }\n\n // KB vs KB (any number) — all non-king pieces must be bishops on the same square color\n const allBishops = nonKingEntries.every(([, p]) => p.type === 'b');\n if (allBishops) {\n const first = nonKingEntries[0];\n if (first === undefined) {\n return true;\n }\n const firstSquareColor = squareColor(first[0]);\n return nonKingEntries.every(\n ([sq]) => squareColor(sq) === firstSquareColor,\n );\n }\n\n return false;\n }\n\n get isValid(): boolean {\n let blackKings = 0;\n let whiteKings = 0;\n\n for (const [square, p] of this.#board) {\n if (p.type === 'k') {\n if (p.color === 'b') {\n blackKings++;\n } else {\n whiteKings++;\n }\n }\n\n // No pawns on rank 1 or 8\n if (p.type === 'p' && (square[1] === '1' || square[1] === '8')) {\n return false;\n }\n }\n\n if (blackKings !== 1 || whiteKings !== 1) {\n return false;\n }\n\n // Side not to move must not be in check\n const opponent: Color = this.#turn === 'w' ? 'b' : 'w';\n for (const [square, p] of this.#board) {\n if (\n p.type === 'k' &&\n p.color === opponent &&\n this.isAttacked(square, this.#turn)\n ) {\n return false;\n }\n }\n\n return true;\n }\n\n get isCheck(): boolean {\n for (const [square, p] of this.#board) {\n if (p.type === 'k' && p.color === this.#turn) {\n const opponent: Color = this.#turn === 'w' ? 'b' : 'w';\n return this.isAttacked(square, opponent);\n }\n }\n return false;\n }\n\n get turn(): Color {\n return this.#turn;\n }\n\n #isAttackedByPiece(\n board: (Piece | undefined)[],\n targetIndex: number,\n fromIndex: number,\n p: Piece,\n by: Color,\n ): boolean {\n const diff = targetIndex - fromIndex;\n const tableIndex = diff + DIFF_OFFSET;\n\n if (tableIndex < 0 || tableIndex >= 240) {\n return false;\n }\n\n const attackMask = ATTACKS[tableIndex] ?? 0;\n const pieceMask = PIECE_MASKS[p.type] ?? 0;\n\n if ((attackMask & pieceMask) === 0) {\n return false;\n }\n\n if (p.type === 'p') {\n if (by === 'w' && diff > 0) {\n return false;\n }\n\n if (by === 'b' && diff < 0) {\n return false;\n }\n }\n\n const ray = RAYS[tableIndex] ?? 0;\n\n if (ray === 0) {\n return true;\n }\n\n let index = fromIndex + ray;\n while (index !== targetIndex) {\n if ((index & OFF_BOARD) !== 0) {\n return false;\n }\n\n if (board[index] !== undefined) {\n return false;\n }\n\n index += ray;\n }\n\n return true;\n }\n\n attackers(square: Square, by: Color): Square[] {\n const board = boardFromMap(this.#board);\n const targetIndex = squareToIndex(square);\n const result: Square[] = [];\n\n for (const [sq, p] of this.#board) {\n if (p.color !== by) {\n continue;\n }\n\n const fromIndex = squareToIndex(sq);\n if (this.#isAttackedByPiece(board, targetIndex, fromIndex, p, by)) {\n result.push(sq);\n }\n }\n\n return result;\n }\n\n findPiece(piece: Piece): Square[] {\n const result: Square[] = [];\n for (const [sq, p] of this.#board) {\n if (p.color === piece.color && p.type === piece.type) {\n result.push(sq);\n }\n }\n return result;\n }\n\n isAttacked(square: Square, by: Color): boolean {\n const board = boardFromMap(this.#board);\n const targetIndex = squareToIndex(square);\n\n for (const [sq, p] of this.#board) {\n if (p.color !== by) {\n continue;\n }\n\n const fromIndex = squareToIndex(sq);\n if (this.#isAttackedByPiece(board, targetIndex, fromIndex, p, by)) {\n return true;\n }\n }\n\n return false;\n }\n\n piece(square: Square): Piece | undefined {\n return this.#board.get(square);\n }\n\n pieces(color?: Color): Map<Square, Piece> {\n if (color === undefined) {\n return new Map(this.#board);\n }\n const result = new Map<Square, Piece>();\n for (const [sq, p] of this.#board) {\n if (p.color === color) {\n result.set(sq, p);\n }\n }\n return result;\n }\n}\n","import { Position } from './position.js';\n\nimport type { Piece, Square } from './types.js';\n\nconst EMPTY_BOARD = new Map<Square, Piece>();\n\nconst STARTING_POSITION = new Position();\n\nexport { EMPTY_BOARD, STARTING_POSITION };\n\nexport { COLORS, FILES, PIECE_TYPES, RANKS, SQUARES } from './primitives.js';\n"],"mappings":"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"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { a as Piece, n as Color, o as PieceType, r as File, u as Square } from "../types-DpJMtqKW.js";
|
|
2
|
+
|
|
3
|
+
//#region src/internal/board.d.ts
|
|
4
|
+
declare const OFF_BOARD = 136;
|
|
5
|
+
declare function squareToIndex(square: Square): number;
|
|
6
|
+
declare function indexToSquare(index: number): Square;
|
|
7
|
+
/**
|
|
8
|
+
* Converts a Map<Square, Piece> to the internal 0x88 array representation.
|
|
9
|
+
* The bridge between the public Position type and the 0x88 internal layout.
|
|
10
|
+
*/
|
|
11
|
+
declare function boardFromMap(map: Map<Square, Piece>): (Piece | undefined)[];
|
|
12
|
+
//#endregion
|
|
13
|
+
//#region src/internal/tables.d.ts
|
|
14
|
+
declare const PIECE_MASKS: Record<PieceType, number>;
|
|
15
|
+
declare const DIFF_OFFSET = 119;
|
|
16
|
+
declare const ATTACKS: number[];
|
|
17
|
+
declare const RAYS: number[];
|
|
18
|
+
//#endregion
|
|
19
|
+
//#region src/internal/zobrist.d.ts
|
|
20
|
+
declare const PIECE_TABLE: Record<Square, Partial<Record<PieceType, Record<Color, bigint>>>>;
|
|
21
|
+
declare const TURN_TABLE: Record<Color, bigint>;
|
|
22
|
+
declare const CASTLING_TABLE: Record<string, bigint>;
|
|
23
|
+
declare const EP_TABLE: Record<File, bigint>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { ATTACKS, CASTLING_TABLE, DIFF_OFFSET, EP_TABLE, OFF_BOARD, PIECE_MASKS, PIECE_TABLE, RAYS, TURN_TABLE, boardFromMap, indexToSquare, squareToIndex };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{_ as e,d as t,f as n,g as r,h as i,i as a,m as o,n as s,p as c,r as l,t as u,u as d}from"../internal-BOZCKSNm.js";export{d as ATTACKS,u as CASTLING_TABLE,t as DIFF_OFFSET,s as EP_TABLE,o as OFF_BOARD,n as PIECE_MASKS,l as PIECE_TABLE,c as RAYS,a as TURN_TABLE,i as boardFromMap,r as indexToSquare,e as squareToIndex};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const e=136;function t(e){let t=(e.codePointAt(0)??0)-(`a`.codePointAt(0)??0);return(8-Number.parseInt(e[1]??`1`,10))*16+t}function n(e){let t=8-Math.floor(e/16),n=e%16;return`${String.fromCodePoint((`a`.codePointAt(0)??0)+n)}${t}`}function r(e){let n=Array.from({length:128});for(let[r,i]of e)n[t(r)]=i;return n}const i=[-33,-31,-18,-14,14,18,31,33],a=[-17,-15,15,17],o=[-16,-1,1,16],s=[-17,-16,-15,-1,1,15,16,17],c={b:4,k:16,n:2,p:1,q:12,r:8},l=119,u=Array.from({length:240}).fill(0),d=Array.from({length:240}).fill(0);(function(){for(let e of i)u[e+119]=(u[e+119]??0)|2;for(let e of s)u[e+119]=(u[e+119]??0)|16;for(let e of[15,17])u[e+119]=(u[e+119]??0)|1,u[-e+119]=(u[-e+119]??0)|1;for(let e=0;e<=119;e++)if(!(e&136)){for(let t of o){let n=e+t;for(;!(n&136);){let r=n-e;u[r+119]=(u[r+119]??0)|8,d[r+119]=t,n+=t}}for(let t of a){let n=e+t;for(;!(n&136);){let r=n-e;u[r+119]=(u[r+119]??0)|4,d[r+119]=t,n+=t}}}})();const f=[`b`,`w`],p=[`a`,`b`,`c`,`d`,`e`,`f`,`g`,`h`],m=[`1`,`2`,`3`,`4`,`5`,`6`,`7`,`8`],h=[`b`,`k`,`n`,`p`,`q`,`r`],g=p.flatMap(e=>m.toReversed().map(t=>`${e}${t}`));function _(e){let t=BigInt(e);return()=>(t=t*6364136223846793005n+1442695040888963407n&18446744073709551615n,t)}const v=_(3735928559),y=Object.fromEntries(g.map(e=>[e,Object.fromEntries(h.map(e=>[e,Object.fromEntries(f.map(e=>[e,v()]))]))])),b={b:v(),w:v()},x={bK:v(),bQ:v(),wK:v(),wQ:v()},S=Object.fromEntries(p.map(e=>[e,v()]));export{t as _,f as a,m as c,l as d,c as f,n as g,r as h,b as i,g as l,e as m,S as n,p as o,d as p,y as r,h as s,x as t,u};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"internal-BOZCKSNm.js","names":[],"sources":["../src/internal/board.ts","../src/internal/tables.ts","../src/primitives.ts","../src/internal/zobrist.ts"],"sourcesContent":["import type { Piece, Square } from '../types.js';\n\nconst OFF_BOARD = 0x88;\n\nfunction squareToIndex(square: Square): number {\n const file = (square.codePointAt(0) ?? 0) - ('a'.codePointAt(0) ?? 0);\n const rank = Number.parseInt(square[1] ?? '1', 10);\n return (8 - rank) * 16 + file;\n}\n\nfunction indexToSquare(index: number): Square {\n const rank = 8 - Math.floor(index / 16);\n const file = index % 16;\n return `${String.fromCodePoint(('a'.codePointAt(0) ?? 0) + file)}${rank}` as Square;\n}\n\n/**\n * Converts a Map<Square, Piece> to the internal 0x88 array representation.\n * The bridge between the public Position type and the 0x88 internal layout.\n */\nfunction boardFromMap(map: Map<Square, Piece>): (Piece | undefined)[] {\n const board: (Piece | undefined)[] = Array.from({ length: 128 });\n for (const [square, p] of map) {\n board[squareToIndex(square)] = p;\n }\n return board;\n}\n\nexport { OFF_BOARD, boardFromMap, indexToSquare, squareToIndex };\n","import { OFF_BOARD } from './board.js';\n\nimport type { PieceType } from '../types.js';\n\n// ── Direction constants ───────────────────────────────────────────────────────\n\nconst KNIGHT_OFFSETS_0X88 = [-33, -31, -18, -14, 14, 18, 31, 33] as const;\nconst BISHOP_DIRS_0X88 = [-17, -15, 15, 17] as const;\nconst ROOK_DIRS_0X88 = [-16, -1, 1, 16] as const;\nconst KING_OFFSETS_0X88 = [-17, -16, -15, -1, 1, 15, 16, 17] as const;\n\n// ── Piece bitmasks ────────────────────────────────────────────────────────────\n\nconst PAWN_MASK = 0x01;\nconst KNIGHT_MASK = 0x02;\nconst BISHOP_MASK = 0x04;\nconst ROOK_MASK = 0x08;\nconst KING_MASK = 0x10;\n\nconst PIECE_MASKS: Record<PieceType, number> = {\n b: BISHOP_MASK,\n k: KING_MASK,\n n: KNIGHT_MASK,\n p: PAWN_MASK,\n q: BISHOP_MASK | ROOK_MASK,\n r: ROOK_MASK,\n};\n\n// ── ATTACKS / RAYS lookup tables ──────────────────────────────────────────────\n//\n// ATTACKS[diff + DIFF_OFFSET] — bitmask of piece types that can attack along\n// the vector represented by `diff` (target_index - attacker_index).\n//\n// RAYS[diff + DIFF_OFFSET] — the ray step direction for sliding pieces (0 for\n// non-sliding pieces).\n//\n// DIFF_OFFSET = 119 centres the diff range [-119, +119] at index 0.\n\nconst DIFF_OFFSET = 119;\n\nconst ATTACKS: number[] = Array.from<number>({ length: 240 }).fill(0);\nconst RAYS: number[] = Array.from<number>({ length: 240 }).fill(0);\n\n(function initAttackTables() {\n // Knight\n for (const offset of KNIGHT_OFFSETS_0X88) {\n ATTACKS[offset + DIFF_OFFSET] =\n (ATTACKS[offset + DIFF_OFFSET] ?? 0) | KNIGHT_MASK;\n }\n\n // King\n for (const offset of KING_OFFSETS_0X88) {\n ATTACKS[offset + DIFF_OFFSET] =\n (ATTACKS[offset + DIFF_OFFSET] ?? 0) | KING_MASK;\n }\n\n // Pawns — white attacks at offsets -17 and -15 (toward rank 8 = lower index)\n // black attacks at +15 and +17. Both share PAWN_MASK; color checked at use time.\n for (const offset of [15, 17]) {\n ATTACKS[offset + DIFF_OFFSET] =\n (ATTACKS[offset + DIFF_OFFSET] ?? 0) | PAWN_MASK;\n ATTACKS[-offset + DIFF_OFFSET] =\n (ATTACKS[-offset + DIFF_OFFSET] ?? 0) | PAWN_MASK;\n }\n\n // Sliding pieces — walk every ray from every valid square\n for (let from = 0; from <= 119; from++) {\n if (from & OFF_BOARD) {\n continue;\n }\n\n for (const direction of ROOK_DIRS_0X88) {\n let to = from + direction;\n while (!(to & OFF_BOARD)) {\n const diff = to - from;\n ATTACKS[diff + DIFF_OFFSET] =\n (ATTACKS[diff + DIFF_OFFSET] ?? 0) | ROOK_MASK;\n RAYS[diff + DIFF_OFFSET] = direction;\n to += direction;\n }\n }\n\n for (const direction of BISHOP_DIRS_0X88) {\n let to = from + direction;\n while (!(to & OFF_BOARD)) {\n const diff = to - from;\n ATTACKS[diff + DIFF_OFFSET] =\n (ATTACKS[diff + DIFF_OFFSET] ?? 0) | BISHOP_MASK;\n RAYS[diff + DIFF_OFFSET] = direction;\n to += direction;\n }\n }\n }\n})();\n\nexport { ATTACKS, DIFF_OFFSET, PIECE_MASKS, RAYS };\n","import type { Color, File, PieceType, Rank, Square } from './types.js';\n\nconst COLORS: Color[] = ['b', 'w'];\nconst FILES: File[] = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];\nconst RANKS: Rank[] = ['1', '2', '3', '4', '5', '6', '7', '8'];\nconst PIECE_TYPES: PieceType[] = ['b', 'k', 'n', 'p', 'q', 'r'];\nconst SQUARES: Square[] = FILES.flatMap((f) =>\n RANKS.toReversed().map((r) => `${f}${r}` as Square),\n);\n\nexport { COLORS, FILES, PIECE_TYPES, RANKS, SQUARES };\n","import { COLORS, FILES, PIECE_TYPES, SQUARES } from '../primitives.js';\n\nimport type { Color, File, PieceType, Square } from '../types.js';\n\n// Seeded LCG for deterministic random numbers (no Math.random — must be stable across runs)\nfunction lcg(seed: number): () => bigint {\n let s = BigInt(seed);\n return () => {\n s =\n (s * 6_364_136_223_846_793_005n + 1_442_695_040_888_963_407n) &\n 0xff_ff_ff_ff_ff_ff_ff_ffn;\n return s;\n };\n}\n\nconst next = lcg(0xde_ad_be_ef);\n\n// Piece table: PIECE_TABLE[square][pieceType][color]\nconst PIECE_TABLE: Record<\n Square,\n Partial<Record<PieceType, Record<Color, bigint>>>\n> = Object.fromEntries(\n SQUARES.map((sq) => [\n sq,\n Object.fromEntries(\n PIECE_TYPES.map((pt) => [\n pt,\n Object.fromEntries(COLORS.map((c) => [c, next()])),\n ]),\n ),\n ]),\n) as Record<Square, Partial<Record<PieceType, Record<Color, bigint>>>>;\n\nconst TURN_TABLE: Record<Color, bigint> = {\n b: next(),\n w: next(),\n};\n\nconst CASTLING_TABLE: Record<string, bigint> = {\n bK: next(),\n bQ: next(),\n wK: next(),\n wQ: next(),\n};\n\nconst EP_TABLE: Record<File, bigint> = Object.fromEntries(\n FILES.map((f) => [f, next()]),\n) as Record<File, bigint>;\n\nexport { CASTLING_TABLE, EP_TABLE, PIECE_TABLE, TURN_TABLE };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/types.d.ts
|
|
2
|
+
type Color = 'b' | 'w';
|
|
3
|
+
type File = 'a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h';
|
|
4
|
+
type PieceType = 'b' | 'k' | 'n' | 'p' | 'q' | 'r';
|
|
5
|
+
type PromotionPieceType = 'b' | 'n' | 'q' | 'r';
|
|
6
|
+
type Rank = '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8';
|
|
7
|
+
type Square = `${File}${Rank}`;
|
|
8
|
+
type SquareColor = 'dark' | 'light';
|
|
9
|
+
interface CastlingRights {
|
|
10
|
+
bK: boolean;
|
|
11
|
+
bQ: boolean;
|
|
12
|
+
wK: boolean;
|
|
13
|
+
wQ: boolean;
|
|
14
|
+
}
|
|
15
|
+
interface PositionOptions {
|
|
16
|
+
castlingRights?: CastlingRights;
|
|
17
|
+
enPassantSquare?: Square;
|
|
18
|
+
fullmoveNumber?: number;
|
|
19
|
+
halfmoveClock?: number;
|
|
20
|
+
turn?: Color;
|
|
21
|
+
}
|
|
22
|
+
interface Move {
|
|
23
|
+
from: Square;
|
|
24
|
+
promotion: PromotionPieceType | undefined;
|
|
25
|
+
to: Square;
|
|
26
|
+
}
|
|
27
|
+
interface Piece {
|
|
28
|
+
color: Color;
|
|
29
|
+
type: PieceType;
|
|
30
|
+
}
|
|
31
|
+
//#endregion
|
|
32
|
+
export { Piece as a, PromotionPieceType as c, SquareColor as d, Move as i, Rank as l, Color as n, PieceType as o, File as r, PositionOptions as s, CastlingRights as t, Square as u };
|
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"author": "Adrian de la Rosa <adrian@delarosab.me>",
|
|
3
|
+
"bugs": {
|
|
4
|
+
"url": "https://github.com/mormubis/position/issues"
|
|
5
|
+
},
|
|
6
|
+
"description": "Chess position type and board utilities. Foundation for @echecs/fen, @echecs/san, and @echecs/game.",
|
|
7
|
+
"devDependencies": {
|
|
8
|
+
"@eslint/js": "^10.0.1",
|
|
9
|
+
"@typescript-eslint/parser": "^8.57.0",
|
|
10
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
11
|
+
"@vitest/eslint-plugin": "^1.6.12",
|
|
12
|
+
"eslint": "^10.0.3",
|
|
13
|
+
"eslint-config-prettier": "^10.1.8",
|
|
14
|
+
"eslint-import-resolver-typescript": "^4.4.4",
|
|
15
|
+
"eslint-plugin-import-x": "^4.16.2",
|
|
16
|
+
"eslint-plugin-unicorn": "^63.0.0",
|
|
17
|
+
"husky": "^9.1.7",
|
|
18
|
+
"lint-staged": "^16.4.0",
|
|
19
|
+
"prettier": "^3.8.1",
|
|
20
|
+
"tsdown": "^0.21.4",
|
|
21
|
+
"typedoc": "^0.28.17",
|
|
22
|
+
"typescript": "^5.9.3",
|
|
23
|
+
"typescript-eslint": "^8.57.0",
|
|
24
|
+
"vitest": "^4.1.0"
|
|
25
|
+
},
|
|
26
|
+
"engines": {
|
|
27
|
+
"node": ">=20"
|
|
28
|
+
},
|
|
29
|
+
"exports": {
|
|
30
|
+
".": {
|
|
31
|
+
"import": "./dist/index.js",
|
|
32
|
+
"types": "./dist/index.d.ts"
|
|
33
|
+
},
|
|
34
|
+
"./internal": {
|
|
35
|
+
"import": "./dist/internal/index.js",
|
|
36
|
+
"types": "./dist/internal/index.d.ts"
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"files": [
|
|
40
|
+
"/dist/",
|
|
41
|
+
"LICENSE",
|
|
42
|
+
"README.md",
|
|
43
|
+
"CHANGELOG.md"
|
|
44
|
+
],
|
|
45
|
+
"homepage": "https://github.com/mormubis/position#readme",
|
|
46
|
+
"keywords": [
|
|
47
|
+
"board",
|
|
48
|
+
"chess",
|
|
49
|
+
"fen",
|
|
50
|
+
"no-dependencies",
|
|
51
|
+
"position",
|
|
52
|
+
"typescript"
|
|
53
|
+
],
|
|
54
|
+
"license": "MIT",
|
|
55
|
+
"main": "dist/index.js",
|
|
56
|
+
"name": "@echecs/position",
|
|
57
|
+
"repository": {
|
|
58
|
+
"type": "git",
|
|
59
|
+
"url": "https://github.com/mormubis/position.git"
|
|
60
|
+
},
|
|
61
|
+
"sideEffects": false,
|
|
62
|
+
"type": "module",
|
|
63
|
+
"types": "dist/index.d.ts",
|
|
64
|
+
"version": "1.0.2",
|
|
65
|
+
"scripts": {
|
|
66
|
+
"build": "tsdown",
|
|
67
|
+
"docs": "typedoc",
|
|
68
|
+
"format": "pnpm run format:ci --write",
|
|
69
|
+
"format:ci": "prettier -l \"**/*.+(css|js|json|jsx|md|mjs|mts|ts|tsx|yml|yaml)\"",
|
|
70
|
+
"lint": "pnpm run lint:style && pnpm run lint:types",
|
|
71
|
+
"lint:ci": "pnpm run lint:style --max-warnings 0 && pnpm run lint:types",
|
|
72
|
+
"lint:style": "eslint \"src/**/*.{ts,tsx}\" \"*.mjs\" --fix",
|
|
73
|
+
"lint:types": "tsc --noEmit --project tsconfig.json",
|
|
74
|
+
"test": "vitest run",
|
|
75
|
+
"test:coverage": "pnpm run test --coverage",
|
|
76
|
+
"test:watch": "pnpm run test --watch"
|
|
77
|
+
}
|
|
78
|
+
}
|