hyperchess-wasm 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.
- package/LICENSE +674 -0
- package/README.md +35 -0
- package/dist/bundler/.gitkeep +0 -0
- package/dist/bundler/README.md +37 -0
- package/dist/bundler/hyperchess_wasm.d.ts +229 -0
- package/dist/bundler/hyperchess_wasm.js +9 -0
- package/dist/bundler/hyperchess_wasm_bg.js +2858 -0
- package/dist/bundler/hyperchess_wasm_bg.wasm +0 -0
- package/dist/bundler/hyperchess_wasm_bg.wasm.d.ts +56 -0
- package/dist/bundler/package.json +30 -0
- package/dist/nodejs/.gitkeep +0 -0
- package/dist/nodejs/README.md +37 -0
- package/dist/nodejs/hyperchess_wasm.d.ts +229 -0
- package/dist/nodejs/hyperchess_wasm.js +2867 -0
- package/dist/nodejs/hyperchess_wasm_bg.wasm +0 -0
- package/dist/nodejs/hyperchess_wasm_bg.wasm.d.ts +56 -0
- package/dist/nodejs/package.json +24 -0
- package/dist/web/.gitkeep +0 -0
- package/dist/web/README.md +37 -0
- package/dist/web/hyperchess_wasm.d.ts +310 -0
- package/dist/web/hyperchess_wasm.js +2964 -0
- package/dist/web/hyperchess_wasm_bg.wasm +0 -0
- package/dist/web/hyperchess_wasm_bg.wasm.d.ts +56 -0
- package/dist/web/package.json +28 -0
- package/package.json +65 -0
package/README.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# hyperchess-wasm
|
|
2
|
+
|
|
3
|
+
A thin JS/TS wrapper around `crates/hyperchess-wasm` compiled to WebAssembly — **not** a
|
|
4
|
+
separate implementation of the game rules or the 3D renderer. This package has no Rust source
|
|
5
|
+
of its own; `npm run build` (or `../../scripts/build-wasm-sdk.sh` directly) compiles that
|
|
6
|
+
crate for three targets:
|
|
7
|
+
|
|
8
|
+
| Export | wasm-pack target | Use case |
|
|
9
|
+
| --- | --- | --- |
|
|
10
|
+
| `hyperchess-wasm` | `bundler` | apps importing this through webpack/vite/etc. |
|
|
11
|
+
| `hyperchess-wasm/nodejs` | `nodejs` | server-side consumers (e.g. an HTMX backend) |
|
|
12
|
+
| `hyperchess-wasm/web` | `web` | plain `<script type="module">`, no bundler |
|
|
13
|
+
|
|
14
|
+
All three expose:
|
|
15
|
+
- **`WasmBoard`** — rules + search bindings (`from_hfen`, `legal_moves`, `apply_move`, `hfen`,
|
|
16
|
+
`termination`, `encode`, every searcher entry point). Consumed today by `hyperchess-core`.
|
|
17
|
+
- **`Scene3D`** — WebGPU/WebGL board/piece renderer, taking `WasmBoard::encode()`'s 144-byte
|
|
18
|
+
board format directly. Not yet consumed by any package here — `hyperchess-board-3d`
|
|
19
|
+
(extraction plan §12 Phase 8) will wrap it.
|
|
20
|
+
|
|
21
|
+
**GPLv3, not MIT** (unlike `hyperchess-core`/`board`/`store`/`theme`) — this package's `.wasm`
|
|
22
|
+
binary directly contains the compiled rules+search engine, so it's a GPL derivative by
|
|
23
|
+
construction. Bundling it directly into an app's own bundle makes that app a GPL derivative
|
|
24
|
+
too; consuming it via a Web Worker (`postMessage`) or over the network (the API driver) does
|
|
25
|
+
not — see the extraction plan §4/§5 for the full reasoning and the recommended integration
|
|
26
|
+
patterns.
|
|
27
|
+
|
|
28
|
+
See `docs/sdk-plan/WASM-MIGRATION-PLAN.md` (carried over from the source repo, historical
|
|
29
|
+
context only) for why this package exists and what still used the hand-written TypeScript
|
|
30
|
+
rules engine in `hyperchess-core` before it was replaced by a wrapper around this package.
|
|
31
|
+
|
|
32
|
+
**Known trade-off:** the compiled `.wasm` is ~4.5MB because `Scene3D`'s `wgpu` dependency
|
|
33
|
+
compiles into the same binary as `WasmBoard`, even for consumers who only need 2D rules
|
|
34
|
+
validation — see `crates/hyperchess-wasm/docs/README.md` for the full discussion and the
|
|
35
|
+
proposed Phase 8 fix (a Cargo feature split).
|
|
File without changes
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# hyperchess-wasm
|
|
2
|
+
|
|
3
|
+
Two independent wasm-bindgen surfaces, one wasm-pack build target:
|
|
4
|
+
|
|
5
|
+
- **`WasmBoard`** — rules + search bindings (legal moves, apply-move, every searcher entry
|
|
6
|
+
point). Depends on `hyperchess-rules` + `hyperchess-search`.
|
|
7
|
+
- **`Scene3D`** — WebGPU/WebGL board/piece renderer. Zero dependency on the rules engine —
|
|
8
|
+
takes a 144-byte board encoding as raw bytes (the same format `WasmBoard::encode()`
|
|
9
|
+
produces), not a `Board` reference. The two communicate via that shared byte protocol, not
|
|
10
|
+
Rust-level coupling.
|
|
11
|
+
|
|
12
|
+
The source repo keeps these as genuinely separate crates/build pipelines (`src/hyperchess`
|
|
13
|
+
built directly with `--features wasm`, and a standalone `hyperchess_3d` consumed only by the
|
|
14
|
+
private web app's own hand-loaded WASM, never integrated into the npm SDK workspace). Merging
|
|
15
|
+
them here is a deliberate improvement, not a copy of existing structure — see
|
|
16
|
+
[`docs/hyperchess-core-extraction-plan.md`](../../../docs/hyperchess-core-extraction-plan.md)
|
|
17
|
+
§12 Phase 6. It's also why `hyperchess-rules`/`hyperchess-search` have no wasm-bindgen
|
|
18
|
+
dependency of their own (Phase 1/3) — that lives only here, in the crate whose whole purpose
|
|
19
|
+
is WASM bindings.
|
|
20
|
+
|
|
21
|
+
`geometry`/`pieces` are plain portable math, native-buildable, and used by the `gen_assets`
|
|
22
|
+
dev-tool binary (`cargo run --bin gen_assets`) as well as the wasm32-only renderer — everything
|
|
23
|
+
else here (`board`, `camera`, `gpu`, `obj`, `scene`) is `#[cfg(target_arch = "wasm32")]`, so a
|
|
24
|
+
plain `cargo build`/`cargo test` (no wasm32 target) only touches the portable half.
|
|
25
|
+
|
|
26
|
+
## Known trade-off: bundle size
|
|
27
|
+
|
|
28
|
+
Verified via a real `wasm-pack build --target nodejs` + Node.js smoke test (not just
|
|
29
|
+
`cargo build`): the resulting `.wasm` binary is **~4.5MB**, because `wgpu` (pulled in for
|
|
30
|
+
`Scene3D`) compiles into the same module as `WasmBoard`, even for consumers who only want 2D
|
|
31
|
+
legal-move validation and never touch the 3D renderer. The source repo's `hyperchess-wasm`
|
|
32
|
+
package (rules-only, no 3D) was necessarily smaller. This is the direct cost of the "one
|
|
33
|
+
wasm-pack build target" merge decision (§12 Phase 6) — worth reconsidering once
|
|
34
|
+
`packages/board-3d` (§12 Phase 8) is built: a `scene3d` Cargo feature gating `wgpu`/`camera`/
|
|
35
|
+
`gpu`/`obj`/`scene` out of the default build would let a `WasmBoard`-only consumer opt out of
|
|
36
|
+
the 3D weight, at the cost of no longer being strictly "one build for everything." Flagged as
|
|
37
|
+
an open item, not solved here.
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JS-facing handle. Board state is a 144-byte array: 0 = empty, else
|
|
6
|
+
* `(color << 4) | kind` with kind 1..=8 indexing
|
|
7
|
+
* `[pawn, knight, bishop, rook, queen, king, eagle, hawk]` and color
|
|
8
|
+
* 0 = white, 1 = black (see `board3d.js` for the encoder).
|
|
9
|
+
*/
|
|
10
|
+
export class Scene3D {
|
|
11
|
+
private constructor();
|
|
12
|
+
free(): void;
|
|
13
|
+
[Symbol.dispose](): void;
|
|
14
|
+
/**
|
|
15
|
+
* Starts a glide animation for the piece on `from` moving to `to`. Call
|
|
16
|
+
* this *before* `set_board` with the post-move position — it snapshots
|
|
17
|
+
* the pre-move board to know what to animate (and what's being captured).
|
|
18
|
+
*/
|
|
19
|
+
animate_move(from: number, to: number): void;
|
|
20
|
+
/**
|
|
21
|
+
* Creates the renderer against the given canvas. Async because adapter/device
|
|
22
|
+
* acquisition is a browser Promise under the hood. JS calls this as a static
|
|
23
|
+
* factory (`await Scene3D.create(canvas)`), not `new Scene3D(...)`.
|
|
24
|
+
*/
|
|
25
|
+
static create(canvas: HTMLCanvasElement): Promise<Scene3D>;
|
|
26
|
+
/**
|
|
27
|
+
* True while a move animation is still in flight — keep calling
|
|
28
|
+
* `render()` on every frame until this goes false, then it's fine to
|
|
29
|
+
* idle the render loop until the next interaction.
|
|
30
|
+
*/
|
|
31
|
+
is_animating(): boolean;
|
|
32
|
+
/**
|
|
33
|
+
* Rotate the camera around the board by a *delta* in radians;
|
|
34
|
+
* pitch is clamped internally so the camera cannot flip over the pole.
|
|
35
|
+
*/
|
|
36
|
+
orbit(dyaw: number, dpitch: number): void;
|
|
37
|
+
/**
|
|
38
|
+
* Canvas-space pixel coordinates -> square index (0-143), or -1 if off-board.
|
|
39
|
+
*/
|
|
40
|
+
pick(x: number, y: number): number;
|
|
41
|
+
/**
|
|
42
|
+
* Draw one frame. JS drives the render loop, so this must be called
|
|
43
|
+
* from `requestAnimationFrame` — the renderer never schedules itself.
|
|
44
|
+
*/
|
|
45
|
+
render(): void;
|
|
46
|
+
/**
|
|
47
|
+
* Match the swapchain to a new canvas size, in physical pixels.
|
|
48
|
+
*/
|
|
49
|
+
resize(width: number, height: number): void;
|
|
50
|
+
/**
|
|
51
|
+
* `board` must be exactly 144 bytes (see the encoding note on `Scene3D`).
|
|
52
|
+
*/
|
|
53
|
+
set_board(board: Uint8Array, flipped: boolean): void;
|
|
54
|
+
/**
|
|
55
|
+
* `-1` means "none" for `selected`/`last_from`/`last_to`/`checked_king`.
|
|
56
|
+
*/
|
|
57
|
+
set_selection(selected: number, legal: Uint8Array, last_from: number, last_to: number, checked_king: number): void;
|
|
58
|
+
/**
|
|
59
|
+
* Scale the camera distance by `factor` (`>1` pulls back, `<1` moves
|
|
60
|
+
* in), clamped to the scene's usable range.
|
|
61
|
+
*/
|
|
62
|
+
zoom(factor: number): void;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* JS-facing wrapper around a [`Board`].
|
|
67
|
+
*
|
|
68
|
+
* Every method takes and returns plain strings or primitives rather than
|
|
69
|
+
* exposing engine types across the wasm boundary, so the JS side never needs
|
|
70
|
+
* to mirror the Rust move or piece encodings.
|
|
71
|
+
*/
|
|
72
|
+
export class WasmBoard {
|
|
73
|
+
free(): void;
|
|
74
|
+
[Symbol.dispose](): void;
|
|
75
|
+
/**
|
|
76
|
+
* Material-only adjudication of the current position: 1 = White wins,
|
|
77
|
+
* 2 = Black wins, 3 = draw (within one pawn of equal material).
|
|
78
|
+
*/
|
|
79
|
+
adjudicate_material(): number;
|
|
80
|
+
/**
|
|
81
|
+
* Apply a move given as a UCI string. Returns false if the move is not legal.
|
|
82
|
+
*/
|
|
83
|
+
apply_move(uci: string): boolean;
|
|
84
|
+
/**
|
|
85
|
+
* Run alpha-beta search at the given depth and return the best move as UCI.
|
|
86
|
+
* Returns an empty string if there are no legal moves.
|
|
87
|
+
*/
|
|
88
|
+
best_move(depth: number): string;
|
|
89
|
+
/**
|
|
90
|
+
* Compatibility guided alpha-beta label. Uses the canonical timed search.
|
|
91
|
+
*/
|
|
92
|
+
best_move_guided(depth: number): string;
|
|
93
|
+
/**
|
|
94
|
+
* Compatibility guided iterative label. Uses the canonical timed search.
|
|
95
|
+
*/
|
|
96
|
+
best_move_guided_iterative(depth: number): string;
|
|
97
|
+
/**
|
|
98
|
+
* Run iterative deepening (with transposition table) at the given depth.
|
|
99
|
+
* Better than `best_move` for the same depth budget — TT avoids re-searching
|
|
100
|
+
* positions seen at shallower iterations.
|
|
101
|
+
*/
|
|
102
|
+
best_move_iterative(depth: number): string;
|
|
103
|
+
/**
|
|
104
|
+
* Run MCTS with the given number of simulations and return the best move as UCI.
|
|
105
|
+
* Returns an empty string if there are no legal moves.
|
|
106
|
+
*/
|
|
107
|
+
best_move_mcts(simulations: number): string;
|
|
108
|
+
/**
|
|
109
|
+
* Run the "aggressive" profile (PVS, aspiration windows and the
|
|
110
|
+
* speculative pruning family — reverse futility, frontier futility, delta
|
|
111
|
+
* pruning). The strongest fixed-depth option.
|
|
112
|
+
*/
|
|
113
|
+
best_move_pro(depth: number): string;
|
|
114
|
+
/**
|
|
115
|
+
* Run the stronger strategic-style tactical profile.
|
|
116
|
+
*/
|
|
117
|
+
best_move_strategic(depth: number): string;
|
|
118
|
+
/**
|
|
119
|
+
* Anytime iterative-deepening search with a **wall-clock budget** (and an
|
|
120
|
+
* optional node cap as a deterministic backstop).
|
|
121
|
+
*
|
|
122
|
+
* This is the recommended interactive search: it deepens until `max_depth`,
|
|
123
|
+
* the time budget, or the node cap is hit, then returns the best move from the
|
|
124
|
+
* deepest *completed* depth. The search watches the clock itself
|
|
125
|
+
* (`js_sys::Date::now()`) — essential in the browser, where JS cannot interrupt
|
|
126
|
+
* a running synchronous WASM call, so an external timeout would never fire.
|
|
127
|
+
*
|
|
128
|
+
* * `movetime_ms == 0` → no time cap.
|
|
129
|
+
* * `node_limit == 0` → no node cap.
|
|
130
|
+
* (At least one of `movetime_ms` / `node_limit` / a small `max_depth` should
|
|
131
|
+
* be set, or the search runs to `max_depth` which may be very deep.)
|
|
132
|
+
*/
|
|
133
|
+
best_move_timed(max_depth: number, movetime_ms: number, node_limit: number): string;
|
|
134
|
+
/**
|
|
135
|
+
* Anytime search with a wall-clock budget under a named profile:
|
|
136
|
+
* `"balanced"` (default), `"strategic"`, or `"aggressive"`. Same anytime semantics
|
|
137
|
+
* as [`Self::best_move_timed`] — this is what interactive strategic/aggressive
|
|
138
|
+
* callers should use so the computed move timeout is actually honoured
|
|
139
|
+
* (the fixed-depth `best_move_strategic` / `best_move_pro` wrappers ignore it).
|
|
140
|
+
*/
|
|
141
|
+
best_move_timed_profile(max_depth: number, movetime_ms: number, node_limit: number, profile: string): string;
|
|
142
|
+
/**
|
|
143
|
+
* Returns the board as a 144-byte Uint8Array for WebGPU evaluation.
|
|
144
|
+
* Each byte is the Piece discriminant: 0=empty, 1-8=white, 9-16=black.
|
|
145
|
+
* Piece order: P=1, N=2, B=3, R=4, Q=5, K=6, Eagle=7, Hawk=8 (+ 8 for black).
|
|
146
|
+
*/
|
|
147
|
+
encode(): Uint8Array;
|
|
148
|
+
/**
|
|
149
|
+
* Create a board from a HFEN string. Returns null on parse error.
|
|
150
|
+
*/
|
|
151
|
+
static from_hfen(hfen: string): WasmBoard | undefined;
|
|
152
|
+
/**
|
|
153
|
+
* Returns the current position as a HFEN string.
|
|
154
|
+
*/
|
|
155
|
+
hfen(): string;
|
|
156
|
+
/**
|
|
157
|
+
* Returns true if the side to move is currently in check.
|
|
158
|
+
*/
|
|
159
|
+
in_check(): boolean;
|
|
160
|
+
/**
|
|
161
|
+
* Returns true if the game has ended (checkmate, stalemate, or draw).
|
|
162
|
+
*/
|
|
163
|
+
is_game_over(): boolean;
|
|
164
|
+
/**
|
|
165
|
+
* Returns the square index (0-143) of the king for the side to move, or 255 if not found.
|
|
166
|
+
*/
|
|
167
|
+
king_square(): number;
|
|
168
|
+
/**
|
|
169
|
+
* Returns all legal moves as a space-separated UCI string (e.g. "a3a4 b3b4 ...").
|
|
170
|
+
*/
|
|
171
|
+
legal_moves(): string;
|
|
172
|
+
/**
|
|
173
|
+
* Create a board at the starting position.
|
|
174
|
+
*/
|
|
175
|
+
constructor();
|
|
176
|
+
/**
|
|
177
|
+
* Returns the game result: 0=ongoing, 1=white wins, 2=black wins, 3=draw.
|
|
178
|
+
*/
|
|
179
|
+
result(): number;
|
|
180
|
+
/**
|
|
181
|
+
* [`Self::result`] with material adjudication for move-limit endings:
|
|
182
|
+
* a draw by the internal no-progress rule — and, when `capped` is true, an
|
|
183
|
+
* ongoing position stopped by an external per-game move cap — is decided
|
|
184
|
+
* by material (≥ 1 pawn wins) instead of collapsing to a draw. Genuine
|
|
185
|
+
* draws (stalemate, repetition, insufficient material) are unchanged.
|
|
186
|
+
*/
|
|
187
|
+
result_adjudicated(capped: boolean): number;
|
|
188
|
+
/**
|
|
189
|
+
* Populate game history for draw-rule and search repetition checks.
|
|
190
|
+
*
|
|
191
|
+
* Contract (see `Board::repetition_count`): `history` stores **prior**
|
|
192
|
+
* positions only. Callers pass the full ordered HFEN list of the game
|
|
193
|
+
* (typically including the current position last); a trailing entry equal
|
|
194
|
+
* to the current position is dropped so it is never double-counted.
|
|
195
|
+
* Threefold then fires at `repetition_count() >= 2` (2 prior + current).
|
|
196
|
+
*/
|
|
197
|
+
set_hfen_history(hfens: string): void;
|
|
198
|
+
/**
|
|
199
|
+
* Alias of [`Self::set_hfen_history`] — kept for older callers. Both draw
|
|
200
|
+
* rules and search now share the same prior-positions-only contract.
|
|
201
|
+
*/
|
|
202
|
+
set_search_history(hfens: string): void;
|
|
203
|
+
/**
|
|
204
|
+
* Returns why the game ended: "checkmate", "stalemate", "move_limit",
|
|
205
|
+
* "fivefold_repetition", "threefold_repetition", "insufficient_material", or
|
|
206
|
+
* "ongoing". Delegates to `Board::termination_reason`, the single source of
|
|
207
|
+
* truth also used by the server's termination endpoint, so the WASM/local-game
|
|
208
|
+
* path reports the same specific reason as server-backed games.
|
|
209
|
+
*/
|
|
210
|
+
termination(): string;
|
|
211
|
+
/**
|
|
212
|
+
* Returns whose turn it is: "white" or "black".
|
|
213
|
+
*/
|
|
214
|
+
turn(): string;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Which new piece to calibrate. JS passes `"eagle"` or `"hawk"`.
|
|
219
|
+
*/
|
|
220
|
+
export function calibrate_piece(piece: string, games: number, depth: number, max_half_moves: number): number;
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Single combined init for both wasm-bindgen surfaces in this crate —
|
|
224
|
+
* wasm-bindgen only allows one `#[wasm_bindgen(start)]` function per
|
|
225
|
+
* crate. Merges what were two separate inits in the source repo
|
|
226
|
+
* (`hyperchess::wasm`'s panic hook + `Helper::init()`, and
|
|
227
|
+
* `hyperchess_3d`'s panic hook + `console_log` setup).
|
|
228
|
+
*/
|
|
229
|
+
export function init(): void;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/* @ts-self-types="./hyperchess_wasm.d.ts" */
|
|
2
|
+
import * as wasm from "./hyperchess_wasm_bg.wasm";
|
|
3
|
+
import { __wbg_set_wasm } from "./hyperchess_wasm_bg.js";
|
|
4
|
+
|
|
5
|
+
__wbg_set_wasm(wasm);
|
|
6
|
+
wasm.__wbindgen_start();
|
|
7
|
+
export {
|
|
8
|
+
Scene3D, WasmBoard, calibrate_piece, init
|
|
9
|
+
} from "./hyperchess_wasm_bg.js";
|