A real-time multiplayer kanji and vocabulary game built entirely in Rust. Players get a kanji character (or vocab word) and race to type valid Japanese words containing it. Inspired by Bomb Party on jklm.fun.
Live at moji.fly.dev
You join a lobby, the game shows you a kanji like 日, and you type a word that uses it — like 日本 (にほん). If the word exists in the dictionary and contains the right kanji, you score. First to target score (or last man standing) wins.
There are three game modes:
- Deathmatch — Race to a target score. Fastest fingers win.
- Duel — Turn-based with lives. Miss a word and you lose a life. Last one standing wins.
- Zen — No pressure, no scores. Just practice at your own pace.
It also supports a Vocab content mode where instead of finding words with a kanji, you're given a word and need to type its correct hiragana reading.
Difficulty is configurable across all five JLPT levels (N5–N1), and the lobby leader can mix and match levels, set time limits, and toggle weighted kanji distribution based on frequency.
This is a Rust monorepo with four crates:
| Crate | What it does |
|---|---|
backend |
Axum HTTP server + WebSocket handler, PostgreSQL via SQLx, JWT auth |
frontend |
Leptos SPA compiled to WebAssembly, runs entirely in the browser |
shared |
Types, messages, and server function definitions shared across both sides |
macros |
Procedural macros for reducing boilerplate |
Some other notable pieces:
- WebSockets for all real-time game communication (typing indicators, score updates, prompt changes)
- JWT authentication with support for both registered accounts and anonymous guest sessions
- Argon2 password hashing
- Rate limiting via
tower-governorto prevent WebSocket spam - Profanity filtering on usernames via
rustrict - SQLx compile-time checked queries with offline mode for Docker builds
- Telemetry tracking — the backend silently tracks games played, words guessed, and online presence for analytics
moji/
├── backend/
│ ├── src/
│ │ ├── main.rs # Server entrypoint, routing, middleware
│ │ ├── api.rs # HTTP + WebSocket handlers
│ │ ├── lobby.rs # Game state machine (scoring, turns, prompts)
│ │ ├── state.rs # Shared application state
│ │ ├── models/ # SQLx database models (users, sessions, stats)
│ │ ├── data.rs # CSV data loading (kanji lists, word lists)
│ │ └── error.rs # Error types
│ └── migrations/ # PostgreSQL schema migrations
├── frontend/
│ ├── src/
│ │ ├── main.rs # App shell, routing, theme init
│ │ ├── components/ # Leptos components (home, lobby, game, etc.)
│ │ ├── context.rs # Reactive contexts (auth, game state)
│ │ └── persistence.rs # LocalStorage session management
│ ├── index.html # Trunk entrypoint
│ └── input.css # Tailwind v4 styles
├── shared/ # Cross-crate types and server functions
├── macros/ # Proc macros
├── data/ # JLPT kanji + vocabulary CSVs (N1–N5)
├── Dockerfile # Multi-stage build (cargo-chef + Trunk + Bun)
- Rust (stable)
- PostgreSQL running locally
wasm32-unknown-unknowntarget:rustup target add wasm32-unknown-unknown- Trunk:
cargo install trunk - Bun (for Tailwind CSS processing)
- SQLx CLI (optional, for migrations):
cargo install sqlx-cli
-
Clone and enter the repo:
git clone https://github.com/CldStlkr/moji.git cd moji -
Create a PostgreSQL database and set the connection URL:
createdb moji export DATABASE_URL=postgres://localhost/moji -
Run migrations:
sqlx migrate run --source backend/migrations
-
Start the backend (from the repo root):
cargo run --bin moji-server
-
In another terminal, build and serve the frontend:
cd frontend bunx tailwindcss -i ./input.css -o ./styles.css trunk serve -
Open
http://localhost:8080— create a guest account and start a lobby.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
(required) | PostgreSQL connection string |
JWT_SECRET |
INSECURE_DEFAULT_SECRET |
Secret for signing auth tokens |
HOST |
0.0.0.0 |
Bind address |
PORT |
8080 |
Server port |
PRODUCTION |
(unset) | Set to 1 for production mode (stricter CORS, different asset paths) |
FRONTEND_URL |
https://moji.fly.dev |
Allowed origin in production CORS |
The project deploys to Fly.io with a single command:
fly deployThe Dockerfile uses a multi-stage build:
- cargo-chef for dependency caching
- Backend build with
SQLX_OFFLINE=true(requires runningcargo sqlx prepare --workspacelocally first) - Frontend build with Trunk + Bun (Tailwind v4)
- Final image based on
debian:bookworm-slim(~minimal footprint)
If you modify any SQLx queries, you need to regenerate the offline cache before deploying:
cargo sqlx prepare --workspace
git add .sqlx/ && git commit -m "update sqlx cache"The kanji and vocabulary data comes from:
- Jōyō Kanji List — the official list of kanji for everyday use
- JMdict — comprehensive Japanese-English dictionary project
All data is loaded from CSV files in the data/ directory at server startup.
MIT