The Game You're About to Build

Build a Chess Game with Rust and WebAssembly

Before writing any Rust, play the thing. Clone the companion repo, run npm install && npm run dev, and beat the computer once. Notice what you’re touching: pieces glide when they move, your game survives a page reload, the help drawer explains castling without pausing the game. All of it runs from static files. There is no server thinking about chess anywhere.

This lesson maps the territory and then builds the smallest possible piece of it: a TypeScript project that compiles and serves a page. Checkpoint: lesson-01.

Two halves and a narrow bridge

The finished app is one Rust crate and one web component. The crate, chess-engine, owns everything that can be wrong: the rules, the board state, the computer opponent. The component, <chess-board>, owns everything you can see. Between them sits WebAssembly, and the API that crosses it is deliberately primitive: FEN strings out, moves in algebraic notation, comma-separated square lists for highlights. No JSON, no serde, no shared structs.

That narrowness is a feature you’ll feel in every later lesson. The Rust side stays testable with plain cargo test because it never touches a JS type. The TypeScript side stays honest because it can’t reach into the engine and poke — it can only ask.

The other structural decision worth naming now: the interface is one component. No board component owning square components owning piece components. A chessboard renders from a single string in one pass, and splitting it up would only create seams for state to leak through.

An empty page that proves the toolchain

Everything below goes in a fresh directory. The package.json starts with three scripts and two dev dependencies:

{
  "name": "rust-wasm-chess",
  "version": "0.1.0",
  "description": "A chess game in the browser: a Rust engine compiled to WebAssembly, a Lit web component in front of it",
  "license": "MIT",
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "devDependencies": {
    "typescript": "^7.0.2",
    "vite": "^8.2.1"
  }
}

The Vite config earns its keep in lesson 5; today it only pins the port:

import { defineConfig } from 'vite';

export default defineConfig({
  server: {
    port: 3000,
    host: true
  }
});

index.html is the app shell. Vite treats it as the entry point, follows the module script, and compiles whatever it finds:

<body>
  <h1>rust-wasm-chess</h1>
  <script type="module" src="/src/main.ts"></script>
</body>

The flags that look wrong on purpose

Copy the full tsconfig.json from the answer key, then actually read it, because two lines look like mistakes:

"experimentalDecorators": true,
"useDefineForClassFields": false

Lit’s @customElement and @property decorators are the legacy TC39 proposal, and they only work when class fields compile the old way. Flip either flag and lesson 6’s component will construct but never react to anything. The rest of the file is strict-mode TypeScript with moduleResolution: "bundler", which is what lets Vite own the import story.

Build it

File Action What goes in it
package.json write Name, type: "module", the three vite scripts, typescript + vite as devDependencies
tsconfig.json copy Strict config with the two Lit-required decorator flags
vite.config.ts write defineConfig with the dev-server port
index.html write Shell page with a placeholder heading and the module script tag
src/main.ts write A console.log proving the toolchain compiles TypeScript

Done when: npm run dev serves the heading on localhost:3000 with your log line in the console, and npm run build emits a dist/ directory.

Answer key: lesson-01 (first lesson, so the tag tree is the whole answer).

Challenge: the two strange flags

Write down, in one sentence each, what experimentalDecorators and useDefineForClassFields actually change about the compiled output. The Lit decorator docs have the answer. You’ll appreciate having done this when lesson 6 works on the first try.

Next lesson, the Rust begins: one Cargo.toml with more to say than most, and a crate that compiles to two different targets without changing a line.