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/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";