@chesscam/recorder 0.0.0-stage → 0.1.0

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.
Files changed (79) hide show
  1. package/LICENSE.md +21 -0
  2. package/README.md +150 -2
  3. package/dist/chess/board.d.ts +24 -0
  4. package/dist/chess/board.d.ts.map +1 -0
  5. package/dist/chess/board.js +75 -0
  6. package/dist/chess/board.js.map +1 -0
  7. package/dist/chess/moves.d.ts +6 -0
  8. package/dist/chess/moves.d.ts.map +1 -0
  9. package/dist/chess/moves.js +107 -0
  10. package/dist/chess/moves.js.map +1 -0
  11. package/dist/constants.d.ts +17 -0
  12. package/dist/constants.d.ts.map +1 -0
  13. package/dist/constants.js +40 -0
  14. package/dist/constants.js.map +1 -0
  15. package/dist/errors.d.ts +7 -0
  16. package/dist/errors.d.ts.map +1 -0
  17. package/dist/errors.js +11 -0
  18. package/dist/errors.js.map +1 -0
  19. package/dist/game.d.ts +9 -0
  20. package/dist/game.d.ts.map +1 -0
  21. package/dist/game.js +61 -0
  22. package/dist/game.js.map +1 -0
  23. package/dist/index.d.ts +10 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +9 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/recorder.d.ts +37 -0
  28. package/dist/recorder.d.ts.map +1 -0
  29. package/dist/recorder.js +207 -0
  30. package/dist/recorder.js.map +1 -0
  31. package/dist/rules/board.d.ts +33 -0
  32. package/dist/rules/board.d.ts.map +1 -0
  33. package/dist/rules/board.js +97 -0
  34. package/dist/rules/board.js.map +1 -0
  35. package/dist/rules/pgn.d.ts +10 -0
  36. package/dist/rules/pgn.d.ts.map +1 -0
  37. package/dist/rules/pgn.js +59 -0
  38. package/dist/rules/pgn.js.map +1 -0
  39. package/dist/rules/position.d.ts +55 -0
  40. package/dist/rules/position.d.ts.map +1 -0
  41. package/dist/rules/position.js +430 -0
  42. package/dist/rules/position.js.map +1 -0
  43. package/dist/rules/san.d.ts +6 -0
  44. package/dist/rules/san.d.ts.map +1 -0
  45. package/dist/rules/san.js +87 -0
  46. package/dist/rules/san.js.map +1 -0
  47. package/dist/types.d.ts +32 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +2 -0
  50. package/dist/types.js.map +1 -0
  51. package/dist/vision/bayesianTracker.d.ts +30 -0
  52. package/dist/vision/bayesianTracker.d.ts.map +1 -0
  53. package/dist/vision/bayesianTracker.js +351 -0
  54. package/dist/vision/bayesianTracker.js.map +1 -0
  55. package/dist/vision/beamTracker.d.ts +34 -0
  56. package/dist/vision/beamTracker.d.ts.map +1 -0
  57. package/dist/vision/beamTracker.js +197 -0
  58. package/dist/vision/beamTracker.js.map +1 -0
  59. package/dist/vision/model.d.ts +21 -0
  60. package/dist/vision/model.d.ts.map +1 -0
  61. package/dist/vision/model.js +187 -0
  62. package/dist/vision/model.js.map +1 -0
  63. package/dist/vision/position.d.ts +16 -0
  64. package/dist/vision/position.d.ts.map +1 -0
  65. package/dist/vision/position.js +98 -0
  66. package/dist/vision/position.js.map +1 -0
  67. package/dist/vision/replay.d.ts +9 -0
  68. package/dist/vision/replay.d.ts.map +1 -0
  69. package/dist/vision/replay.js +49 -0
  70. package/dist/vision/replay.js.map +1 -0
  71. package/dist/vision/video.d.ts +2 -0
  72. package/dist/vision/video.d.ts.map +1 -0
  73. package/dist/vision/video.js +9 -0
  74. package/dist/vision/video.js.map +1 -0
  75. package/dist/vision/worker.d.ts +19 -0
  76. package/dist/vision/worker.d.ts.map +1 -0
  77. package/dist/vision/worker.js +91 -0
  78. package/dist/vision/worker.js.map +1 -0
  79. package/package.json +54 -4
package/LICENSE.md ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 ChessCam
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 CHANGED
@@ -1,3 +1,151 @@
1
- # Temporary Holding Version
1
+ # @chesscam/recorder
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Records chess moves from a camera, video file or stream of a real board, entirely in the browser. It's the move
4
+ detection from [ChessCam](https://www.chesscam.net), packaged on its own: a piece model (on the CPU, in a worker)
5
+ and a move tracker that follows the game frame by frame and corrects misreads a few seconds on.
6
+
7
+ ```ts
8
+ import { createRecorder, loadModel } from "@chesscam/recorder";
9
+
10
+ const model = await loadModel();
11
+ const recorder = createRecorder({
12
+ model,
13
+ video: document.querySelector("video")!,
14
+ // The board's region of the video, in video pixels
15
+ roi: { x: 438, y: 384, width: 444, height: 280 },
16
+ onUpdate: (game) => showMoves(game.moves),
17
+ onError: (error) => showStatus(error.code)
18
+ });
19
+ recorder.start();
20
+
21
+ // End & Save
22
+ recorder.pause();
23
+ const pgn = recorder.pgn({ White: "Alice", Black: "Bob", Event: "Club blitz" });
24
+ ```
25
+
26
+ `examples/demo` is a working page: a camera or a video file, Start, and End & Save.
27
+
28
+ ## Setup
29
+
30
+ **Bundler.** The CPU fallback runs in a worker, found with `new URL("./worker.js", import.meta.url)`, which Vite and
31
+ webpack follow. With Vite, keep the package out of dependency pre-bundling, which would move its files:
32
+
33
+ ```ts
34
+ optimizeDeps: { exclude: ["@chesscam/recorder"] }
35
+ ```
36
+
37
+ **The LiteRT.js runtime.** By default it loads from jsDelivr. Serve it yourself (copy
38
+ `node_modules/@litertjs/core/wasm/` and pass `loadModel({ wasmUrl })`) to work offline, and so the model can use
39
+ threads: a browser only starts the runtime's threads from the page's own origin. It must be version `LITERT_VERSION`.
40
+
41
+ **Cross-origin isolation.** Threads also need these headers on the page. `credentialless` still lets the model load
42
+ from Hugging Face.
43
+
44
+ ```
45
+ Cross-Origin-Opener-Policy: same-origin
46
+ Cross-Origin-Embedder-Policy: credentialless
47
+ ```
48
+
49
+ On a Pixel 7a the model manages about 6 frames a second on one thread and 15 on four (it uses at most four, as more
50
+ were slower), both above the cap of 4.
51
+
52
+ **The model**, chessq-chesscam-mnv4m-320-v1 (23.9 MB), loads from Hugging Face, pinned to a commit
53
+ (`DEFAULT_MODEL_URL`). Self-host it with `loadModel({ modelUrl })` to load it from the page's own origin, e.g. where
54
+ huggingface.co is blocked.
55
+
56
+ ## Video sources
57
+
58
+ Anything a `<video>` can play and a page can read the pixels of: a camera (`getUserMedia`), a file (blob URL), a
59
+ WebRTC `MediaStream`, or HLS/DASH served with CORS and played with `crossOrigin="anonymous"`. A cross-origin iframe
60
+ (YouTube, Twitch) or DRM-protected video can't be read. Detection runs at up to `maxFps` frames a second (default 4),
61
+ scaled by the video's playback rate.
62
+
63
+ Start recording when the game starts: frames of anything else (title cards, an empty board, hands setting up) can be
64
+ read as moves.
65
+
66
+ ## API
67
+
68
+ ### `loadModel(options?): Promise<Model>`
69
+
70
+ Loads the model on the CPU in a worker, warmed up so the first frame is quick. A page has one model: calling again
71
+ gives the same one until it's deleted.
72
+
73
+ | Option | |
74
+ |---|---|
75
+ | `modelUrl` | The `.tflite` model |
76
+ | `wasmUrl` | Where LiteRT.js's runtime is served, ending in `/` |
77
+
78
+ ### `createRecorder(options): Recorder`
79
+
80
+ Reads moves off the board until destroyed. It starts paused.
81
+
82
+ | Option | |
83
+ |---|---|
84
+ | `model`, `video` | Required |
85
+ | `roi` | The board's region, in video pixels. Without one the whole frame is read, which only works when the board fills it. |
86
+ | `start` | The start position as FEN |
87
+ | `timed` | Time each move, for `[%emt]` in the PGN (default true). Times are in recording time, which a pause stops. |
88
+ | `finalMovesOnly` | Report only moves that can't be taken back, about 1.5 s after they are played; for moves that go straight to an online game |
89
+ | `maxFps` | Detections a second at normal speed (default 4) |
90
+ | `onUpdate(game)` | The game whenever its moves change |
91
+ | `onFps(fps \| null)` | The detection rate about once a second, and null once it stops |
92
+ | `onOrientation(corners)` | The board's corners clockwise from the region's top left, when it turns |
93
+ | `onDetect(detection)` | Each frame read |
94
+ | `onError(error)` | A frame failed; detection carries on with the next |
95
+
96
+ **A later update can change earlier moves.** When the tracker finds it misread a move (often a capture and recapture
97
+ that a hand over the board seemed to show), it replaces it. Keep the whole game from each `onUpdate`, not just the
98
+ newest move.
99
+
100
+ | Method | |
101
+ |---|---|
102
+ | `start()`, `pause()` | A pause counts as no time, for move times or for corrections |
103
+ | `game`, `running` | The game so far, and whether frames are being read |
104
+ | `setGame({ moves, start?, fromOpponent?, times? })` | Changes the game other than by the board, e.g. a takeback or an online opponent's move. The tracker starts again from it. |
105
+ | `reset(start?)` | A new game |
106
+ | `setRoi(roi)`, `setVideo(video)` | |
107
+ | `pgn(headers?)` | The game as PGN, with the seven standard tags, any extra headers, and `[%emt]` move times |
108
+ | `destroy()` | |
109
+
110
+ A game has `moves` (numbered SAN, e.g. `1. e4 e5 2. Nf3`), `fen`, `start`, `lastMove` (UCI), `fromOpponent` and
111
+ `times` (ms since recording started, per move).
112
+
113
+ ### `readPosition(model, video, roi?, turn?)`
114
+
115
+ Reads the position on the board, averaged over three frames, to start a game somewhere other than the start position.
116
+ Gives `{ ok: true, fen }`, or `{ ok: false, problem }` when the board isn't a legal position (`"kings"`,
117
+ `"pawns-on-backrank"`, ...).
118
+
119
+ ### Errors
120
+
121
+ `RecorderError` has a `code` for the app to show in its own words:
122
+
123
+ | Code | |
124
+ |---|---|
125
+ | `not-ready` | No model, or the video has no frame |
126
+ | `worker-stopped` | The CPU worker crashed, e.g. out of memory |
127
+ | `unloaded` | The model was deleted |
128
+ | `detection-failed` | Anything else; see `message` |
129
+
130
+ ## Development
131
+
132
+ ```
133
+ npm install
134
+ npm run check # tests, types, lint
135
+ npm run build # dist/
136
+ ```
137
+
138
+ To try the demo, run `npm install && npm run dev` in `examples/demo`. Add `?video=<url>&t=<start s>&roi=x,y,w,h` to play a
139
+ video, and `&wasm=<url>` to serve the runtime from the page's origin.
140
+
141
+ The code came from ChessCam's `src/vision` and `src/chess` at commit `880c130`. The tracker's settings are tuned on
142
+ replays of recorded games in ChessCam's research repo.
143
+
144
+ ## Licence
145
+
146
+ MIT. The chess rules, SAN, FEN and PGN are its own (`src/rules`). Its dependencies have their own licences:
147
+
148
+ - [LiteRT.js](https://www.npmjs.com/package/@litertjs/core), which runs the model, is Apache-2.0.
149
+ - The default model, [chessq-chesscam](https://huggingface.co/PBatch23888/chessq-chesscam), is loaded at runtime, not
150
+ shipped in the package, and is under the PolyForm Noncommercial License 1.0.0. Commercial use needs a separate
151
+ licence for it.
@@ -0,0 +1,24 @@
1
+ import { type Move, Position } from "../rules/position.js";
2
+ type HistoryEntry = {
3
+ move: Move;
4
+ san: string;
5
+ };
6
+ export declare class GameBoard {
7
+ #private;
8
+ readonly start: string;
9
+ readonly history: HistoryEntry[];
10
+ constructor(start: string);
11
+ get position(): Position;
12
+ get fen(): string;
13
+ get lastMove(): string;
14
+ get moves(): string;
15
+ playSan(san: string): Move | null;
16
+ playUci(uci: string): Move | null;
17
+ undo(): void;
18
+ }
19
+ export declare const makeBoard: (game: {
20
+ moves: string;
21
+ start: string;
22
+ }) => GameBoard;
23
+ export {};
24
+ //# sourceMappingURL=board.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board.d.ts","sourceRoot":"","sources":["../../src/chess/board.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,IAAI,EAAE,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAI3D,KAAK,YAAY,GAAG;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAKhD,qBAAa,SAAS;;IACpB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,OAAO,EAAE,YAAY,EAAE,CAAM;IAGtC,YAAY,KAAK,EAAE,MAAM,EAGxB;IAED,IAAI,QAAQ,IAAI,QAAQ,CAEvB;IAED,IAAI,GAAG,IAAI,MAAM,CAEhB;IAGD,IAAI,QAAQ,IAAI,MAAM,CAErB;IAGD,IAAI,KAAK,IAAI,MAAM,CAalB;IAGD,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAGhC;IAED,OAAO,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAMhC;IAED,IAAI,SAQH;CAOF;AAGD,eAAO,MAAM,SAAS,SAAU;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,KAAG,SAMlE,CAAA"}
@@ -0,0 +1,75 @@
1
+ import { Position } from "../rules/position.js";
2
+ import { makeSan, makeUci, parseSan, parseUci } from "../rules/san.js";
3
+ import { parseMoveText } from "../rules/pgn.js";
4
+ const positionFromFen = (fen) => Position.fromFen(fen);
5
+ // A game from its start position, keeping the moves played so they can be numbered and undone
6
+ export class GameBoard {
7
+ start;
8
+ history = [];
9
+ #position;
10
+ constructor(start) {
11
+ this.start = start;
12
+ this.#position = positionFromFen(start);
13
+ }
14
+ get position() {
15
+ return this.#position;
16
+ }
17
+ get fen() {
18
+ return this.#position.fen();
19
+ }
20
+ // The last move in UCI, or "" before any move
21
+ get lastMove() {
22
+ return (this.history.length === 0) ? "" : makeUci(this.history[this.history.length - 1].move);
23
+ }
24
+ // Numbered SAN from the start position, e.g. "1... e5 2. Nf3"
25
+ get moves() {
26
+ const pos = positionFromFen(this.start);
27
+ let text = "";
28
+ this.history.forEach(({ move, san }, index) => {
29
+ if (pos.turn === "white") {
30
+ text += `${pos.fullmoves}. `;
31
+ }
32
+ else if (index === 0) {
33
+ text += `${pos.fullmoves}... `;
34
+ }
35
+ text += `${san} `;
36
+ pos.play(move);
37
+ });
38
+ return text.trim();
39
+ }
40
+ // Each play returns the move, or null if it is not legal here
41
+ playSan(san) {
42
+ const move = parseSan(this.#position, san);
43
+ return (move === undefined) ? null : this.#play(move, san);
44
+ }
45
+ playUci(uci) {
46
+ const move = parseUci(uci);
47
+ if (move === undefined || !this.#position.isLegal(move)) {
48
+ return null;
49
+ }
50
+ return this.#play(move, makeSan(this.#position, move));
51
+ }
52
+ undo() {
53
+ if (this.history.pop() === undefined) {
54
+ return;
55
+ }
56
+ this.#position = positionFromFen(this.start);
57
+ for (const { move } of this.history) {
58
+ this.#position.play(move);
59
+ }
60
+ }
61
+ #play(move, san) {
62
+ this.history.push({ move, san });
63
+ this.#position.play(move);
64
+ return move;
65
+ }
66
+ }
67
+ // The board for a game's start position and moves (numbered SAN, as a Game keeps them)
68
+ export const makeBoard = (game) => {
69
+ const board = new GameBoard(game.start);
70
+ for (const san of parseMoveText(game.moves)) {
71
+ board.playSan(san);
72
+ }
73
+ return board;
74
+ };
75
+ //# sourceMappingURL=board.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"board.js","sourceRoot":"","sources":["../../src/chess/board.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Move, Position } from \"../rules/position.js\";\nimport { makeSan, makeUci, parseSan, parseUci } from \"../rules/san.js\";\nimport { parseMoveText } from \"../rules/pgn.js\";\n\ntype HistoryEntry = { move: Move; san: string };\n\nconst positionFromFen = (fen: string): Position => Position.fromFen(fen);\n\n// A game from its start position, keeping the moves played so they can be numbered and undone\nexport class GameBoard {\n readonly start: string;\n readonly history: HistoryEntry[] = [];\n #position: Position;\n\n constructor(start: string) {\n this.start = start;\n this.#position = positionFromFen(start);\n }\n\n get position(): Position {\n return this.#position;\n }\n\n get fen(): string {\n return this.#position.fen();\n }\n\n // The last move in UCI, or \"\" before any move\n get lastMove(): string {\n return (this.history.length === 0) ? \"\" : makeUci(this.history[this.history.length - 1].move);\n }\n\n // Numbered SAN from the start position, e.g. \"1... e5 2. Nf3\"\n get moves(): string {\n const pos = positionFromFen(this.start);\n let text = \"\";\n this.history.forEach(({ move, san }, index) => {\n if (pos.turn === \"white\") {\n text += `${pos.fullmoves}. `;\n } else if (index === 0) {\n text += `${pos.fullmoves}... `;\n }\n text += `${san} `;\n pos.play(move);\n });\n return text.trim();\n }\n\n // Each play returns the move, or null if it is not legal here\n playSan(san: string): Move | null {\n const move = parseSan(this.#position, san);\n return (move === undefined) ? null : this.#play(move, san);\n }\n\n playUci(uci: string): Move | null {\n const move = parseUci(uci);\n if (move === undefined || !this.#position.isLegal(move)) {\n return null;\n }\n return this.#play(move, makeSan(this.#position, move));\n }\n\n undo() {\n if (this.history.pop() === undefined) {\n return;\n }\n this.#position = positionFromFen(this.start);\n for (const { move } of this.history) {\n this.#position.play(move);\n }\n }\n\n #play(move: Move, san: string): Move {\n this.history.push({ move, san });\n this.#position.play(move);\n return move;\n }\n}\n\n// The board for a game's start position and moves (numbered SAN, as a Game keeps them)\nexport const makeBoard = (game: { moves: string, start: string }): GameBoard => {\n const board = new GameBoard(game.start);\n for (const san of parseMoveText(game.moves)) {\n board.playSan(san);\n }\n return board;\n}\n"]}
@@ -0,0 +1,6 @@
1
+ import { type Position } from "../rules/position.js";
2
+ import type { MovesPair } from "../types.js";
3
+ import type { GameBoard } from "./board.js";
4
+ export declare const getMovesPairs: (board: Position) => MovesPair[];
5
+ export declare const getMoveText: (board: GameBoard) => string;
6
+ //# sourceMappingURL=moves.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moves.d.ts","sourceRoot":"","sources":["../../src/chess/moves.ts"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,QAAQ,EAAE,MAAM,sBAAsB,CAAC;AAG1E,OAAO,KAAK,EAAa,SAAS,EAAe,MAAM,aAAa,CAAC;AACrE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC;AAoE5C,eAAO,MAAM,aAAa,UAAW,QAAQ,gBAsC5C,CAAA;AAGD,eAAO,MAAM,WAAW,UAAW,SAAS,KAAG,MAa9C,CAAA"}
@@ -0,0 +1,107 @@
1
+ import { parseFen } from "../rules/position.js";
2
+ import { makeSan, makeUci } from "../rules/san.js";
3
+ import { SQUARE_MAP, LABEL_MAP, SQUARE_NAMES, pieceToLabel } from "../constants.js";
4
+ const castlingMap = {
5
+ "g1": [SQUARE_MAP.h1, SQUARE_MAP.f1, LABEL_MAP.R],
6
+ "c1": [SQUARE_MAP.a1, SQUARE_MAP.d1, LABEL_MAP.R],
7
+ "g8": [SQUARE_MAP.h8, SQUARE_MAP.f8, LABEL_MAP.r],
8
+ "c8": [SQUARE_MAP.a8, SQUARE_MAP.d8, LABEL_MAP.r]
9
+ };
10
+ const getData = (pos, move) => {
11
+ const fromSquares = [move.from];
12
+ const toSquares = [move.to];
13
+ const piece = pos.board.get(move.from);
14
+ const targetRole = move.promotion || piece?.role || 'pawn';
15
+ const targets = [pieceToLabel({ role: targetRole, color: pos.turn })];
16
+ // Castling
17
+ if (piece?.role === 'king' && Math.abs(move.to - move.from) === 2) {
18
+ const squareName = SQUARE_NAMES[move.to];
19
+ if (castlingMap[squareName]) {
20
+ const [from, to, target] = castlingMap[squareName];
21
+ fromSquares.push(from);
22
+ toSquares.push(to);
23
+ targets.push(target);
24
+ }
25
+ }
26
+ // En Passant
27
+ else if (piece?.role === 'pawn' && move.to !== move.from && !pos.board.get(move.to) && (move.from % 8 !== move.to % 8)) {
28
+ const capturedPawnSquare = (move.to % 8) + (move.from / 8 | 0) * 8;
29
+ fromSquares.push(capturedPawnSquare);
30
+ }
31
+ const moveData = {
32
+ uci: makeUci(move),
33
+ from: fromSquares,
34
+ to: toSquares,
35
+ targets
36
+ };
37
+ return moveData;
38
+ };
39
+ const combineData = (move1Data, move2Data) => {
40
+ const badSquares = move2Data.from.concat(move2Data.to);
41
+ const from1 = move1Data.from.filter(x => !badSquares.includes(x));
42
+ const to1 = [];
43
+ const targets1 = [];
44
+ for (let i = 0; i < move1Data.to.length; i++) {
45
+ if (badSquares.includes(move1Data.to[i])) {
46
+ continue;
47
+ }
48
+ to1.push(move1Data.to[i]);
49
+ targets1.push(move1Data.targets[i]);
50
+ }
51
+ const from = from1.concat(move2Data.from);
52
+ const to = to1.concat(move2Data.to);
53
+ const targets = targets1.concat(move2Data.targets);
54
+ const data = {
55
+ from,
56
+ to,
57
+ targets
58
+ };
59
+ return data;
60
+ };
61
+ export const getMovesPairs = (board) => {
62
+ const pos = board.clone();
63
+ const movesPairs = [];
64
+ for (const move1 of pos.legalMoves()) {
65
+ const data = getData(pos, move1);
66
+ // Only first moves are shown, so only they need SAN
67
+ const move1Data = { ...data, san: makeSan(pos, move1) };
68
+ const pos2 = pos.clone();
69
+ pos2.play(move1);
70
+ let done = true;
71
+ for (const move2 of pos2.legalMoves()) {
72
+ const move2Data = getData(pos2, move2);
73
+ const movesData = combineData(move1Data, move2Data);
74
+ const movesPair = {
75
+ move1: move1Data,
76
+ move2: move2Data,
77
+ moves: movesData
78
+ };
79
+ movesPairs.push(movesPair);
80
+ done = false;
81
+ }
82
+ if (done) {
83
+ const movesPair = {
84
+ move1: move1Data,
85
+ move2: null,
86
+ moves: null
87
+ };
88
+ movesPairs.push(movesPair);
89
+ }
90
+ }
91
+ return movesPairs;
92
+ };
93
+ // The last one or two moves, numbered from the board's start position, e.g. "3...Nc6 4.Bb5"
94
+ export const getMoveText = (board) => {
95
+ const setup = parseFen(board.start);
96
+ const initialPly = (setup.fullmoves - 1) * 2 + (setup.turn === 'white' ? 0 : 1);
97
+ const firstIndex = Math.max(0, board.history.length - 2);
98
+ return board.history.slice(firstIndex).map(({ san }, offset) => {
99
+ const ply = initialPly + firstIndex + offset;
100
+ const moveNumber = Math.floor(ply / 2) + 1;
101
+ if (ply % 2 === 0) {
102
+ return `${moveNumber}.${san}`;
103
+ }
104
+ return offset === 0 ? `${moveNumber}...${san}` : san;
105
+ }).join(" ");
106
+ };
107
+ //# sourceMappingURL=moves.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"moves.js","sourceRoot":"","sources":["../../src/chess/moves.ts"],"names":[],"mappings":"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","sourcesContent":["import { type Move, parseFen, type Position } from \"../rules/position.js\";\nimport { makeSan, makeUci } from \"../rules/san.js\";\nimport { SQUARE_MAP, LABEL_MAP, SQUARE_NAMES, pieceToLabel } from \"../constants.js\";\nimport type { MovesData, MovesPair, SquaresData } from \"../types.js\";\nimport type { GameBoard } from \"./board.js\";\n\nconst castlingMap: { [id: string]: number[] } = {\n \"g1\": [SQUARE_MAP.h1, SQUARE_MAP.f1, LABEL_MAP.R],\n \"c1\": [SQUARE_MAP.a1, SQUARE_MAP.d1, LABEL_MAP.R],\n \"g8\": [SQUARE_MAP.h8, SQUARE_MAP.f8, LABEL_MAP.r],\n \"c8\": [SQUARE_MAP.a8, SQUARE_MAP.d8, LABEL_MAP.r]\n}\n\nconst getData = (pos: Position, move: Move) => {\n const fromSquares: number[] = [move.from];\n const toSquares: number[] = [move.to];\n\n const piece = pos.board.get(move.from);\n const targetRole = move.promotion || piece?.role || 'pawn';\n const targets = [pieceToLabel({ role: targetRole, color: pos.turn })];\n\n // Castling\n if (piece?.role === 'king' && Math.abs(move.to - move.from) === 2) {\n const squareName = SQUARE_NAMES[move.to];\n if (castlingMap[squareName]) {\n const [from, to, target] = castlingMap[squareName];\n fromSquares.push(from);\n toSquares.push(to);\n targets.push(target);\n }\n }\n // En Passant\n else if (piece?.role === 'pawn' && move.to !== move.from && !pos.board.get(move.to) && (move.from % 8 !== move.to % 8)) {\n const capturedPawnSquare = (move.to % 8) + (move.from / 8 | 0) * 8;\n fromSquares.push(capturedPawnSquare);\n }\n\n const moveData: MovesData = {\n uci: makeUci(move),\n from: fromSquares,\n to: toSquares,\n targets\n }\n return moveData;\n}\n\nconst combineData = (move1Data: MovesData, move2Data: MovesData) => {\n const badSquares = move2Data.from.concat(move2Data.to);\n const from1 = move1Data.from.filter(x => !badSquares.includes(x));\n\n const to1 = [];\n const targets1 = [];\n for (let i = 0; i < move1Data.to.length; i++) {\n if (badSquares.includes(move1Data.to[i])) {\n continue;\n }\n to1.push(move1Data.to[i]);\n targets1.push(move1Data.targets[i]);\n }\n\n const from = from1.concat(move2Data.from);\n const to = to1.concat(move2Data.to);\n const targets = targets1.concat(move2Data.targets);\n\n const data: SquaresData = {\n from,\n to,\n targets\n }\n return data;\n}\n\nexport const getMovesPairs = (board: Position) => {\n const pos = board.clone();\n\n const movesPairs: MovesPair[] = [];\n\n for (const move1 of pos.legalMoves()) {\n const data = getData(pos, move1);\n // Only first moves are shown, so only they need SAN\n const move1Data = { ...data, san: makeSan(pos, move1) };\n\n const pos2 = pos.clone();\n pos2.play(move1);\n\n let done = true;\n for (const move2 of pos2.legalMoves()) {\n const move2Data = getData(pos2, move2);\n\n const movesData = combineData(move1Data, move2Data);\n const movesPair: MovesPair = {\n move1: move1Data,\n move2: move2Data,\n moves: movesData\n }\n movesPairs.push(movesPair);\n done = false;\n }\n\n if (done) {\n const movesPair: MovesPair = {\n move1: move1Data,\n move2: null,\n moves: null\n }\n movesPairs.push(movesPair);\n }\n }\n\n return movesPairs;\n}\n\n// The last one or two moves, numbered from the board's start position, e.g. \"3...Nc6 4.Bb5\"\nexport const getMoveText = (board: GameBoard): string => {\n const setup = parseFen(board.start);\n const initialPly = (setup.fullmoves - 1) * 2 + (setup.turn === 'white' ? 0 : 1);\n const firstIndex = Math.max(0, board.history.length - 2);\n\n return board.history.slice(firstIndex).map(({ san }, offset) => {\n const ply = initialPly + firstIndex + offset;\n const moveNumber = Math.floor(ply / 2) + 1;\n if (ply % 2 === 0) {\n return `${moveNumber}.${san}`;\n }\n return offset === 0 ? `${moveNumber}...${san}` : san;\n }).join(\" \");\n}\n"]}
@@ -0,0 +1,17 @@
1
+ import type { Piece, Role, SquareName } from "./rules/board.js";
2
+ export declare const START_FEN: string;
3
+ export declare const MODEL_SIZE: number;
4
+ export declare const NUM_LABELS: number;
5
+ export declare const EMPTY: number;
6
+ export declare const PIECE_SYMBOLS: Role[];
7
+ export declare const labelToPiece: (label: number) => Piece;
8
+ export declare const pieceToLabel: (piece: Piece) => number;
9
+ export declare const SQUARE_NAMES: SquareName[];
10
+ export declare const LABEL_MAP: {
11
+ [id: string]: number;
12
+ };
13
+ export declare const SQUARE_MAP: {
14
+ [id: string]: number;
15
+ };
16
+ export declare const zeros: (rows: number, columns: number) => number[][];
17
+ //# sourceMappingURL=constants.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEhE,eAAO,MAAM,SAAS,EAAE,MAAmE,CAAC;AAC5F,eAAO,MAAM,UAAU,EAAE,MAAY,CAAC;AAGtC,eAAO,MAAM,UAAU,EAAE,MAAsB,CAAC;AAChD,eAAO,MAAM,KAAK,EAAE,MAA4B,CAAC;AACjD,eAAO,MAAM,aAAa,EAAE,IAAI,EAA0D,CAAC;AAE3F,eAAO,MAAM,YAAY,UAAW,MAAM,KAAG,KAG3C,CAAC;AAEH,eAAO,MAAM,YAAY,UAAW,KAAK,KAAG,MAC6B,CAAC;AAC1E,eAAO,MAAM,YAAY,EAAE,UAAU,EAOY,CAAC;AASlD,eAAO,MAAM,SAAS,EAAE;IAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAmB,CAAC;AASlE,eAAO,MAAM,UAAU,EAAE;IAAE,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAAA;CAAoB,CAAC;AAEpE,eAAO,MAAM,KAAK,SAAU,MAAM,WAAW,MAAM,KAAG,MAAM,EAAE,EAE7D,CAAA"}
@@ -0,0 +1,40 @@
1
+ export const START_FEN = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
2
+ export const MODEL_SIZE = 320;
3
+ // Black pieces, white pieces, then the empty square
4
+ const LABELS = ["b", "k", "n", "p", "q", "r", "B", "K", "N", "P", "Q", "R", "."];
5
+ export const NUM_LABELS = LABELS.length;
6
+ export const EMPTY = LABELS.indexOf(".");
7
+ export const PIECE_SYMBOLS = ["bishop", "king", "knight", "pawn", "queen", "rook"];
8
+ export const labelToPiece = (label) => ({
9
+ role: PIECE_SYMBOLS[label % 6],
10
+ color: (label > 5) ? 'white' : 'black'
11
+ });
12
+ export const pieceToLabel = (piece) => ((piece.color === 'white') ? 6 : 0) + PIECE_SYMBOLS.indexOf(piece.role);
13
+ export const SQUARE_NAMES = ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1',
14
+ 'a2', 'b2', 'c2', 'd2', 'e2', 'f2', 'g2', 'h2',
15
+ 'a3', 'b3', 'c3', 'd3', 'e3', 'f3', 'g3', 'h3',
16
+ 'a4', 'b4', 'c4', 'd4', 'e4', 'f4', 'g4', 'h4',
17
+ 'a5', 'b5', 'c5', 'd5', 'e5', 'f5', 'g5', 'h5',
18
+ 'a6', 'b6', 'c6', 'd6', 'e6', 'f6', 'g6', 'h6',
19
+ 'a7', 'b7', 'c7', 'd7', 'e7', 'f7', 'g7', 'h7',
20
+ 'a8', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8'];
21
+ const makeLabelMap = () => {
22
+ const d = {};
23
+ LABELS.forEach((label, i) => {
24
+ d[label] = i;
25
+ });
26
+ return d;
27
+ };
28
+ export const LABEL_MAP = makeLabelMap();
29
+ const makeSquareMap = () => {
30
+ const d = {};
31
+ SQUARE_NAMES.forEach((square, i) => {
32
+ d[square] = i;
33
+ });
34
+ return d;
35
+ };
36
+ export const SQUARE_MAP = makeSquareMap();
37
+ export const zeros = (rows, columns) => {
38
+ return Array.from({ length: rows }, () => Array(columns).fill(0));
39
+ };
40
+ //# sourceMappingURL=constants.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"constants.js","sourceRoot":"","sources":["../src/constants.ts"],"names":[],"mappings":"AAEA,MAAM,CAAC,MAAM,SAAS,GAAW,0DAA0D,CAAC;AAC5F,MAAM,CAAC,MAAM,UAAU,GAAW,GAAG,CAAC;AACtC,oDAAoD;AACpD,MAAM,MAAM,GAAa,CAAC,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,CAAC,CAAC;AAC3F,MAAM,CAAC,MAAM,UAAU,GAAW,MAAM,CAAC,MAAM,CAAC;AAChD,MAAM,CAAC,MAAM,KAAK,GAAW,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;AACjD,MAAM,CAAC,MAAM,aAAa,GAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAE3F,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAa,EAAS,EAAE,CAAC,CAAC;IACrD,IAAI,EAAE,aAAa,CAAC,KAAK,GAAG,CAAC,CAAC;IAC9B,KAAK,EAAE,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO;CACvC,CAAC,CAAC;AAEH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,KAAY,EAAU,EAAE,CACnD,CAAC,CAAC,KAAK,CAAC,KAAK,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,aAAa,CAAC,OAAO,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC1E,MAAM,CAAC,MAAM,YAAY,GAAiB,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IACvF,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IAC9C,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IAC9C,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IAC9C,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IAC9C,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IAC9C,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI;IAC9C,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;AAElD,MAAM,YAAY,GAAG,GAAG,EAAE;IACxB,MAAM,CAAC,GAA6B,EAAE,CAAC;IACvC,MAAM,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1B,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACf,CAAC,CAAC,CAAA;IACF,OAAO,CAAC,CAAC;AACX,CAAC,CAAA;AACD,MAAM,CAAC,MAAM,SAAS,GAA6B,YAAY,EAAE,CAAC;AAElE,MAAM,aAAa,GAAG,GAAG,EAAE;IACzB,MAAM,CAAC,GAA6B,EAAE,CAAC;IACvC,YAAY,CAAC,OAAO,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QACjC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC,CAAC,CAAA;IACF,OAAO,CAAC,CAAC;AACX,CAAC,CAAA;AACD,MAAM,CAAC,MAAM,UAAU,GAA6B,aAAa,EAAE,CAAC;AAEpE,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,IAAY,EAAE,OAAe,EAAc,EAAE;IACjE,OAAO,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC;AACpE,CAAC,CAAA","sourcesContent":["import type { Piece, Role, SquareName } from \"./rules/board.js\";\n\nexport const START_FEN: string = \"rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1\";\nexport const MODEL_SIZE: number = 320;\n// Black pieces, white pieces, then the empty square\nconst LABELS: string[] = [\"b\", \"k\", \"n\", \"p\", \"q\", \"r\", \"B\", \"K\", \"N\", \"P\", \"Q\", \"R\", \".\"];\nexport const NUM_LABELS: number = LABELS.length;\nexport const EMPTY: number = LABELS.indexOf(\".\");\nexport const PIECE_SYMBOLS: Role[] = [\"bishop\", \"king\", \"knight\", \"pawn\", \"queen\", \"rook\"];\n\nexport const labelToPiece = (label: number): Piece => ({\n role: PIECE_SYMBOLS[label % 6],\n color: (label > 5) ? 'white' : 'black'\n});\n\nexport const pieceToLabel = (piece: Piece): number =>\n ((piece.color === 'white') ? 6 : 0) + PIECE_SYMBOLS.indexOf(piece.role);\nexport const SQUARE_NAMES: SquareName[] = ['a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1',\n 'a2', 'b2', 'c2', 'd2', 'e2', 'f2', 'g2', 'h2',\n 'a3', 'b3', 'c3', 'd3', 'e3', 'f3', 'g3', 'h3',\n 'a4', 'b4', 'c4', 'd4', 'e4', 'f4', 'g4', 'h4',\n 'a5', 'b5', 'c5', 'd5', 'e5', 'f5', 'g5', 'h5',\n 'a6', 'b6', 'c6', 'd6', 'e6', 'f6', 'g6', 'h6',\n 'a7', 'b7', 'c7', 'd7', 'e7', 'f7', 'g7', 'h7',\n 'a8', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8'];\n\nconst makeLabelMap = () => {\n const d: { [id: string]: number } = {};\n LABELS.forEach((label, i) => {\n d[label] = i;\n })\n return d;\n}\nexport const LABEL_MAP: { [id: string]: number } = makeLabelMap();\n\nconst makeSquareMap = () => {\n const d: { [id: string]: number } = {};\n SQUARE_NAMES.forEach((square, i) => {\n d[square] = i;\n })\n return d;\n}\nexport const SQUARE_MAP: { [id: string]: number } = makeSquareMap();\n\nexport const zeros = (rows: number, columns: number): number[][] => {\n return Array.from({ length: rows }, () => Array(columns).fill(0));\n}\n"]}
@@ -0,0 +1,7 @@
1
+ export type RecorderErrorCode = "not-ready" | "worker-stopped" | "unloaded" | "detection-failed";
2
+ export declare class RecorderError extends Error {
3
+ readonly code: RecorderErrorCode;
4
+ constructor(code: RecorderErrorCode, message?: string, options?: ErrorOptions);
5
+ }
6
+ export declare const toRecorderError: (error: unknown) => RecorderError;
7
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AACA,MAAM,MAAM,iBAAiB,GAEzB,WAAW,GAEX,gBAAgB,GAEhB,UAAU,GAEV,kBAAkB,CAAC;AAEvB,qBAAa,aAAc,SAAQ,KAAK;IACtC,QAAQ,CAAC,IAAI,EAAE,iBAAiB,CAAC;IAEjC,YAAY,IAAI,EAAE,iBAAiB,EAAE,OAAO,GAAE,MAAa,EAAE,OAAO,CAAC,EAAE,YAAY,EAIlF;CACF;AAED,eAAO,MAAM,eAAe,UAAW,OAAO,KAAG,aACoE,CAAC"}
package/dist/errors.js ADDED
@@ -0,0 +1,11 @@
1
+ export class RecorderError extends Error {
2
+ code;
3
+ constructor(code, message = code, options) {
4
+ super(message, options);
5
+ this.name = "RecorderError";
6
+ this.code = code;
7
+ }
8
+ }
9
+ export const toRecorderError = (error) => (error instanceof RecorderError) ? error
10
+ : new RecorderError("detection-failed", (error instanceof Error) ? error.message : String(error), { cause: error });
11
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAWA,MAAM,OAAO,aAAc,SAAQ,KAAK;IAC7B,IAAI,CAAoB;IAEjC,YAAY,IAAuB,EAAE,OAAO,GAAW,IAAI,EAAE,OAAsB;QACjF,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;QACxB,IAAI,CAAC,IAAI,GAAG,eAAe,CAAC;QAC5B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;IACnB,CAAC;CACF;AAED,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,KAAc,EAAiB,EAAE,CAAC,CAAC,KAAK,YAAY,aAAa,CAAC,CAAC,CAAC,CAAC,KAAK;IACxG,CAAC,CAAC,IAAI,aAAa,CAAC,kBAAkB,EAAE,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC","sourcesContent":["// Why detection failed, as a code for the app to show in its own words\nexport type RecorderErrorCode =\n // The model isn't loaded, or the video has no frame yet\n | \"not-ready\"\n // The CPU worker crashed (e.g. it ran out of memory), so the model must be loaded again\n | \"worker-stopped\"\n // The model was deleted\n | \"unloaded\"\n // Anything else; the message says more\n | \"detection-failed\";\n\nexport class RecorderError extends Error {\n readonly code: RecorderErrorCode;\n\n constructor(code: RecorderErrorCode, message: string = code, options?: ErrorOptions) {\n super(message, options);\n this.name = \"RecorderError\";\n this.code = code;\n }\n}\n\nexport const toRecorderError = (error: unknown): RecorderError => (error instanceof RecorderError) ? error\n : new RecorderError(\"detection-failed\", (error instanceof Error) ? error.message : String(error), { cause: error });\n"]}
package/dist/game.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import { type GameBoard } from "./chess/board.js";
2
+ import type { Game } from "./types.js";
3
+ export type GameUpdate = Omit<Game, "start" | "times">;
4
+ export declare const newGame: (start?: string) => Game;
5
+ export declare const makeUpdatePayload: (board: GameBoard, fromOpponent?: boolean) => GameUpdate;
6
+ export declare const keptTimes: (game: Game, moves: string) => number[];
7
+ export declare const updateTimes: (game: Game, moves: string, elapsed?: number, ago?: (number | null)[]) => number[];
8
+ export declare const makePgn: (game: Pick<Game, "moves" | "start"> & Partial<Pick<Game, "times">>, headers?: Record<string, string>) => string;
9
+ //# sourceMappingURL=game.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game.d.ts","sourceRoot":"","sources":["../src/game.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAa,MAAM,kBAAkB,CAAC;AAI7D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,YAAY,CAAC;AAGvC,MAAM,MAAM,UAAU,GAAG,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,CAAC;AAEvD,eAAO,MAAM,OAAO,WAAW,MAAM,KAAe,IAElD,CAAC;AAEH,eAAO,MAAM,iBAAiB,UAAW,SAAS,iBAAgB,OAAO,KAAW,UAKlF,CAAC;AAGH,eAAO,MAAM,SAAS,SAAU,IAAI,SAAS,MAAM,KAAG,MAAM,EAQ3D,CAAA;AAMD,eAAO,MAAM,WAAW,SAAU,IAAI,SAAS,MAAM,YAAY,MAAM,QAAO,CAAC,MAAM,GAAG,IAAI,CAAC,EAAE,KAAQ,MAAM,EAW5G,CAAA;AAID,eAAO,MAAM,OAAO,SAAU,IAAI,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,YAC/E,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,KAAQ,MAkBxC,CAAA"}
package/dist/game.js ADDED
@@ -0,0 +1,61 @@
1
+ import { makeBoard } from "./chess/board.js";
2
+ import { Position } from "./rules/position.js";
3
+ import { START_FEN } from "./constants.js";
4
+ import { makeClock, makeOutcome, writePgn } from "./rules/pgn.js";
5
+ export const newGame = (start = START_FEN) => ({
6
+ fen: start, moves: "", start, lastMove: "", fromOpponent: false, times: []
7
+ });
8
+ export const makeUpdatePayload = (board, fromOpponent = false) => ({
9
+ moves: board.moves,
10
+ fen: board.fen,
11
+ lastMove: board.lastMove,
12
+ fromOpponent
13
+ });
14
+ // The times of the moves `moves` shares with the game, from the start
15
+ export const keptTimes = (game, moves) => {
16
+ const before = makeBoard(game).history;
17
+ const after = makeBoard({ moves, start: game.start }).history;
18
+ let kept = 0;
19
+ while ((kept < Math.min(before.length, game.times.length)) && (before[kept].san === after[kept]?.san)) {
20
+ kept += 1;
21
+ }
22
+ return game.times.slice(0, kept);
23
+ };
24
+ // Moves kept from before keep their times, so a move shown before it was confirmed is timed from when it was first
25
+ // seen. New moves are timed at `elapsed`, less how long ago each was seen (`ago`, ms, by index in the game),
26
+ // as a move the tracker corrects may have been played a few seconds before it was shown, and without `elapsed` the
27
+ // game is untimed.
28
+ export const updateTimes = (game, moves, elapsed, ago = []) => {
29
+ if (elapsed === undefined) {
30
+ return [];
31
+ }
32
+ const times = keptTimes(game, moves);
33
+ const after = makeBoard({ moves, start: game.start }).history;
34
+ for (let i = times.length; i < after.length; i++) {
35
+ // Never before the move before it
36
+ times.push(Math.max(elapsed - (ago[i] ?? 0), times[i - 1] ?? 0));
37
+ }
38
+ return times;
39
+ };
40
+ // Full PGN for export, with the seven standard tags plus any extra headers, and with times, the
41
+ // time each move took as a [%emt] comment
42
+ export const makePgn = (game, headers = {}) => {
43
+ const board = makeBoard(game);
44
+ const times = game.times ?? [];
45
+ const moves = board.history.map(({ san }, index) => (index < times.length)
46
+ // Whole seconds, as detection is only good to about a second
47
+ ? { san, comment: `[%emt ${makeClock((times[index] - (times[index - 1] ?? 0)) / 1000)}]` }
48
+ : { san });
49
+ const tags = new Map([["Event", "?"], ["Site", "?"], ["Date", "????.??.??"], ["Round", "?"],
50
+ ["White", "?"], ["Black", "?"], ["Result", makeOutcome(board.position.outcome())]]);
51
+ if (game.start !== START_FEN) {
52
+ tags.set("SetUp", "1");
53
+ tags.set("FEN", game.start);
54
+ }
55
+ for (const [key, value] of Object.entries(headers)) {
56
+ tags.set(key, value);
57
+ }
58
+ const start = Position.fromFen(game.start);
59
+ return writePgn(tags, moves, start.turn, start.fullmoves, tags.get("Result") ?? "*");
60
+ };
61
+ //# sourceMappingURL=game.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"game.js","sourceRoot":"","sources":["../src/game.ts"],"names":[],"mappings":"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","sourcesContent":["import { type GameBoard, makeBoard } from \"./chess/board.js\";\nimport { Position } from \"./rules/position.js\";\nimport { START_FEN } from \"./constants.js\";\nimport { makeClock, makeOutcome, type PgnMove, writePgn } from \"./rules/pgn.js\";\nimport type { Game } from \"./types.js\";\n\n// Everything a move changes; the start position is kept, and the times worked out\nexport type GameUpdate = Omit<Game, \"start\" | \"times\">;\n\nexport const newGame = (start: string = START_FEN): Game => ({\n fen: start, moves: \"\", start, lastMove: \"\", fromOpponent: false, times: []\n});\n\nexport const makeUpdatePayload = (board: GameBoard, fromOpponent: boolean = false): GameUpdate => ({\n moves: board.moves,\n fen: board.fen,\n lastMove: board.lastMove,\n fromOpponent\n});\n\n// The times of the moves `moves` shares with the game, from the start\nexport const keptTimes = (game: Game, moves: string): number[] => {\n const before = makeBoard(game).history;\n const after = makeBoard({ moves, start: game.start }).history;\n let kept = 0;\n while ((kept < Math.min(before.length, game.times.length)) && (before[kept].san === after[kept]?.san)) {\n kept += 1;\n }\n return game.times.slice(0, kept);\n}\n\n// Moves kept from before keep their times, so a move shown before it was confirmed is timed from when it was first\n// seen. New moves are timed at `elapsed`, less how long ago each was seen (`ago`, ms, by index in the game),\n// as a move the tracker corrects may have been played a few seconds before it was shown, and without `elapsed` the\n// game is untimed.\nexport const updateTimes = (game: Game, moves: string, elapsed?: number, ago: (number | null)[] = []): number[] => {\n if (elapsed === undefined) {\n return [];\n }\n const times = keptTimes(game, moves);\n const after = makeBoard({ moves, start: game.start }).history;\n for (let i = times.length; i < after.length; i++) {\n // Never before the move before it\n times.push(Math.max(elapsed - (ago[i] ?? 0), times[i - 1] ?? 0));\n }\n return times;\n}\n\n// Full PGN for export, with the seven standard tags plus any extra headers, and with times, the\n// time each move took as a [%emt] comment\nexport const makePgn = (game: Pick<Game, \"moves\" | \"start\"> & Partial<Pick<Game, \"times\">>,\n headers: Record<string, string> = {}): string => {\n const board = makeBoard(game);\n const times: number[] = game.times ?? [];\n const moves: PgnMove[] = board.history.map(({ san }, index) => (index < times.length)\n // Whole seconds, as detection is only good to about a second\n ? { san, comment: `[%emt ${makeClock((times[index] - (times[index - 1] ?? 0)) / 1000)}]` }\n : { san });\n const tags = new Map<string, string>([[\"Event\", \"?\"], [\"Site\", \"?\"], [\"Date\", \"????.??.??\"], [\"Round\", \"?\"],\n [\"White\", \"?\"], [\"Black\", \"?\"], [\"Result\", makeOutcome(board.position.outcome())]]);\n if (game.start !== START_FEN) {\n tags.set(\"SetUp\", \"1\");\n tags.set(\"FEN\", game.start);\n }\n for (const [key, value] of Object.entries(headers)) {\n tags.set(key, value);\n }\n const start = Position.fromFen(game.start);\n return writePgn(tags, moves, start.turn, start.fullmoves, tags.get(\"Result\") ?? \"*\");\n}\n"]}