Most Cargo.toml files are a dependency list. This one is an architecture
document, and it’s short enough to read every line. This lesson creates the
chess-engine crate, builds it to WebAssembly for the first time, and runs
its first native tests — the same code, two targets, zero conditional
compilation. Checkpoint:
lesson-02.
The manifest, line by line
Create chess-engine/ inside the project (a plain subdirectory, not a
workspace member of anything) with this manifest:
[package]
name = "chess-engine"
version = "0.1.0"
edition = "2024"
[lib]
crate-type = ["cdylib", "rlib"]
[dependencies]
wasm-bindgen = "0.2.127"
shakmaty = "0.30.1"
console_error_panic_hook = "0.1.7"
[profile.release]
lto = true
opt-level = 3
The load-bearing line is crate-type. cdylib is what wasm-pack compiles
into the .wasm binary; rlib is an ordinary Rust library target, and its
entire job here is to make cargo test work on your machine, no browser or
wasm tooling involved. Delete "rlib" and the crate still builds for the
web, but every test in this course dies. That one word is the testing
strategy.
shakmaty is the chess library: board representation, move generation,
legality, the works. We are not writing a chess engine from scratch, on
purpose — hand-rolled move generation is its own multi-month course, and
what this course teaches is the pipeline around it.
The release profile turns on lto and full optimization because the output
is a file browsers download. Worth knowing what’s absent, too: no serde
(the boundary will be strings), no getrandom (lesson 8 dodges randomness
in a way you’ll want to see), no wee_alloc (the default allocator is
fine).
The first slice of ChessGame
src/lib.rs starts with a struct and four read-only methods:
#[wasm_bindgen]
pub struct ChessGame {
board: Chess,
player_color: Color,
}
#[wasm_bindgen]
impl ChessGame {
#[wasm_bindgen(constructor)]
pub fn new(player_white: bool) -> Self {
Self {
board: Chess::new(),
player_color: if player_white { Color::White } else { Color::Black },
}
}
}
#[wasm_bindgen(constructor)] means JavaScript will call new ChessGame(true). Alongside it, add get_fen() (serialize the whole
position with Fen::from_position), get_turn(), is_human_turn(), and
get_ascii_board() — the full bodies are small and in the answer key. Then
the module’s only free function:
#[wasm_bindgen(start)]
pub fn init() {
console_error_panic_hook::set_once();
}
start runs automatically when the wasm module initializes. Without this
hook, a Rust panic in the browser reads as RuntimeError: unreachable;
with it, you get the actual panic message in the console. It’s three lines
that will save you an evening.
First build, first tests
npx wasm-pack build --target web --out-dir pkg
Wire that into package.json as build:wasm (with wasm-pack added as a dev
dependency, so nobody needs a global install), plus test:rust for the
native side. The build emits chess-engine/pkg/: the .wasm binary, a
JavaScript glue file, and a generated .d.ts — look inside it and find your
constructor already typed.
wasm-pack also drops a .gitignore containing * into pkg/, assuming
you’ll publish to npm. We do the opposite: delete that file and commit
pkg/, so anyone cloning the repo can run the app with no Rust toolchain
installed. The price is remembering to rebuild after touching Rust.
The first tests go in a #[cfg(test)] mod tests at the bottom of lib.rs:
a new game sits at the standard starting FEN with white to move, and a
player who chose black is not on turn. They run natively, in milliseconds,
because of that rlib.
Build it
| File | Action | What goes in it |
|---|---|---|
chess-engine/Cargo.toml |
write | The manifest above, exactly |
chess-engine/src/lib.rs |
write | ChessGame struct, constructor, the four getters, the panic-hook start fn, three tests |
chess-engine/pkg/ |
generated | npm run build:wasm output, .gitignore inside it deleted, committed |
package.json |
modify | Add build:wasm and test:rust scripts, wasm-pack devDependency |
Done when: npm run test:rust reports 3 passed, and
npm run build:wasm leaves a chess_engine_bg.wasm in pkg/.
Answer key: lesson-01...lesson-02.
Challenge: weigh the binary
Build once as-is, then comment out the [profile.release] block and build
again, comparing the size of chess_engine_bg.wasm. Then try
opt-level = "s". Write down which knob moved the number most — for a
717 KB binary shipped to every visitor, this table is worth having.
Next lesson the crate learns to actually play: moves in two notations, promotion that defaults sensibly, and the parsing trick that makes both work through one method.