I Built a Chess Engine for 6x6 Crazyhouse, Became #1 on Chess.com, and Then Got Banned

A backend developer spent 9 months building a specialized chess engine in Rust for a niche Crazyhouse variant, achieving #1 rank before being banned for computer assistance.

Hi, Habr. My name is Vladimir, I'm a backend developer. This is my first article here — about how a pet project for a niche chess variant went from "what if..." to the number one spot on chess.com. No neural networks. Pure alpha-beta search, written in Rust.

This article will be useful for those interested in chess programming, CPU-bound task optimization, or the Python + Rust integration via PyO3.

What This Is All About

Chess.com has a "Variants" section — alternative chess modes. One of them is Minihouse (also known as 6×6 Crazyhouse). The rules are as follows:

  • A 6×6 board instead of 8×8 (no queen, one pawn each)
  • Crazyhouse mechanics: a captured piece goes into your "hand," and you can place it back on any free square instead of making a regular move

This creates a completely different dynamic. In standard chess, material is lost permanently. Here, every trade gives both players new pieces to drop onto the board. Tactics dominate over strategy. Games often end in 5–10 moves with a sudden checkmate from a dropped knight.

For standard chess, there are Stockfish, Leela Chess Zero, and dozens of other engines. For standard Crazyhouse (8×8), there are specialized variants. But for 6×6 Crazyhouse — there was nothing. A void.

I thought: fine, I'll write one myself.

Timeline: 9 Months from First Commit to #1

Nine months sounds impressive, but in reality this was a "sixth terminal" project — that's what I call the terminal you only get to when there's a spare moment between work and higher-priority tasks. Not full-time development, but stolen moments between main projects.

The first commit was in pure Python. Here's how it all progressed:

Stage 1: Naive Python (May–June 2025)

The classic: minimax with alpha-beta pruning, all in Python. The game logic for Crazyhouse had to be written from scratch — no ready-made libraries exist for a 6×6 board with drop rules. The result was about 860 lines in gamestate.py: move generation, validation, check handling, pawn promotion (on 6×6 they promote quickly).

The engine played. Badly. Search depth of 3–4 moves, seconds per move. For Crazyhouse, where every move branches into dozens of variations due to possible drops, this wasn't enough.

Stage 2: Python Optimizations (July–October 2025)

I added everything I could without changing the language:

  • Transposition table with Zobrist hashing
  • Null-move pruning (with a nuance: it disables when the opponent has pieces in hand — otherwise you miss dangerous drops)
  • Late Move Reduction (LMR)
  • Iterative deepening with aspiration windows
  • Move ordering: MVV-LVA, killer moves, history heuristic

It got better. Depth 5–6. But Python isn't meant for searching millions of positions.

Stage 3: Rewriting the Core in Rust (November 2025)

The key commit: "Rewrite chess engine core in Rust via PyO3 for ~50x speedup."

I rewrote the entire search and evaluation function in Rust, keeping the Python wrapper for GUI and bot compatibility. The bridge uses PyO3 — Rust compiles into a native Python module that's called with a regular import.

The result: search depth 8–10 moves, ~3.8 seconds per move. For a 6×6 board, this is serious depth — the search tree with drops on a small board is very "bushy."

The Rust core is ~3,300 lines:

ModulePurposeLOC
search.rsAlpha-beta + PVS + quiescence + null-move~1000
gamestate.rsMove generation, make/undo~750
eval.rsPosition evaluation function~440
types.rsData types (pieces, moves, board)~330
cache.rsSQLite position cache~100
zobrist.rsPosition hashing~75
lib.rsPyO3 bindings~600

Stage 4: Bot for Chess.com (December 2025)

Next, I needed a way to play against real people. I wrote automation with Playwright (~2,000 lines): login, finding a Minihouse game, recognizing the board, making moves, handling results. The bot logs into chess.com through a regular Chrome browser, finds a game, and plays.

An important note: on chess.com, I always openly stated that a bot was playing. No deception — opponents knew who they were dealing with.

Stage 5: Overnight Training on the Server (January–February 2026)

I have a dedicated server with 12 CPU cores. During the day, they're occupied with work tasks. But at night — they're free.

I wrote a systemd service + timer. Every day at 00:00 UTC, a process launches. It waits until 02:00 UTC (when the load from work tasks is guaranteed to have dropped), then plays against itself for 8 hours: AI as white vs. AI as black. Search depth 6, with 20% of moves being "exploration" (instead of the best move, the second-best is chosen). At 10:00 UTC — graceful shutdown, resources return to work tasks.

# minichesstrain.timer
[Timer]
OnCalendar=*-*-* 00:00:00
Persistent=false

Overnight, several dozen games accumulate, with each position and best move saved to SQLite. By February 2026 — 27,519 cached positions. When the engine encounters a familiar position, it doesn't waste time recalculating — it immediately pulls the answer from the cache.

Evaluation Function: How Crazyhouse Differs from Standard Chess

This is the most interesting part from an engineering perspective. You can't just take a standard chess eval function — Crazyhouse has different priorities.

1. Pieces in Hand Are Worth More Than Pieces on the Board

In standard chess, a knight is worth ~320 points. In Crazyhouse, a knight in hand is worth more — it can be instantly dropped on any free square. The engine evaluates hand pieces with a 60–100% multiplier on the base value.

2. Progressive Drop Threat

The more pieces in hand, the more dangerous each subsequent one becomes. One piece in hand is a threat. Three pieces in hand is checkmate in 2 moves. The formula is nonlinear:

let drop_threat = (hand_total) * DROP_THREAT_BONUS 
    + hand_total.max(1) * (hand_total - 1).max(0) * 10;

3. King Safety Is Everything

On a 6×6 board, the king has nowhere to hide. A pawn shield (pawns in front of the king) is valued at +55 points per pawn. But an exposed king when the opponent has pieces in hand — that's a penalty of up to –390 points. The engine essentially panics when the king is exposed.

if pawn_shield == 0 && opponent_hand_pieces > 0 {
    score -= EXPOSED_KING_PENALTY * opponent_hand_pieces.min(3);
    // Up to -130 * 3 = -390 penalty
}

4. Knight Drop with Check

A knight in hand that can be dropped adjacent to the enemy king earns a separate +40 bonus. A rook that can land on the king's file gets +25. The engine evaluates not only current threats but also latent ones — pieces in hand ready to attack.

5. Passed Pawns Are Devalued

In standard chess, a passed pawn is a strength. In Crazyhouse, the opponent can simply drop a piece in its path. Therefore, the bonus for passed pawns is significantly reduced compared to classical chess.

Neural Network? Tried It. Didn't Work.

The project has an nn/ folder with an AlphaZero approach: CNN for policy + value (PyTorch), MCTS, RL wrapper. I can write small neural networks, and there were many experiments — different architectures, different training approaches. But nothing useful came out of it for this task. Perhaps I didn't try hard enough — but more likely it's due to objective constraints:

  • Small dataset. There's no game database for 6×6 Crazyhouse — everything has to be self-generated
  • Limited resources. I have 12 CPU cores at night, not a GPU cluster
  • Alpha-beta turned out to be good enough. On a small board, classical search to depth 8–10 with a good eval function covers most tactical strikes

In the end, a hand-tuned eval function of 440 lines of Rust turned out to be more effective than all my neural network experiments. For a niche with no competition, well-tuned classical methods work.

Results: 228 Games on Chess.com

The bot played 228 games on chess.com in Minihouse mode. Overall statistics:

MetricValue
Wins212 (93.0%)
Losses12
Draws4
Checkmate139 games
Opponent resigned / timed out65 games
Average winning game length8.1 moves
Rating (peak)~2300

The average winning game lasted 8 moves. Most people simply don't have time to understand what happened: the engine finds tactics with drops that humans can't see.

The Key Stage: Games Against the World's Top 20

The most important leap in engine quality came not from code optimization, but from the right opponents. Self-play and random matches didn't reveal strategic weaknesses — the engine consistently won but didn't improve. Everything changed when I arranged games with players from the world's top 20 in Minihouse.

One top-tier opponent agreed to play against the bot regularly — despite the engine's agonizingly long think time. These games exposed holes in the eval function: an incorrectly valued drop here, a weak pawn shield there, a positional threat the engine couldn't see over there. After each loss, I analyzed, adjusted the evaluation, and progress accelerated dramatically.

The Night That Changed Everything

Until February 22nd, the #1 spot in Minihouse on chess.com belonged to another player. My bot was second.

On the evening of the 22nd, I launched the bot in automatic game-finding mode. It played all night — and by the morning of February 23rd, it had played 138 games, winning 134 of them. But most importantly, the engine confidently defeated players from the top 20, including the former number one.

The losses were mostly early on, when the engine hadn't yet encountered a particular opponent's style. Then positions from lost games entered the cache — and the bot stopped stepping on the same rakes.

In the morning, I opened chess.com and saw that my account was #1 in Minihouse. The feeling is something like: you wake up, and your code did overnight what you'd spent 9 months working toward.

Side Effect: I Started Playing Better Myself

A funny thing: while I was writing and testing the engine, I was also playing Minihouse myself, without any computer assistance. Before the project, I consistently hovered in the top 100, on a good streak reaching the top 50. But after months of working on the eval function and analyzing thousands of the bot's games, all these patterns got lodged in my head: knight drops with check, pawn shields, center control on a small board.

The result — I broke into the top 20 in Minihouse. Legitimately, with my own hands. Turns out, writing a chess engine is the best way to learn to play chess.

The Full Stack

Python 3.11 — glue, GUI, bot
├── Rust (PyO3) — search engine, eval, hashing
├── Pygame — local GUI for testing
├── Playwright + Chrome — chess.com automation
├── SQLite — position cache (27.5K entries)
├── systemd timer — overnight self-play on server
└── PyTorch — neural network attempt (not in use)

What I Took Away from This Project

1. Rust via PyO3 is magic. Rewriting the bottleneck from Python to Rust gave a 50x speedup while preserving the same Python API. If you have a CPU-bound task in Python, seriously look into PyO3.

2. Domain-specific eval > generic neural net (with limited resources). Neural networks win when you have data and GPUs. When you don't — hand-tuning an eval function for a specific chess variant works better.

3. A niche is a superpower. In mainstream chess, my engine wouldn't even crack the top 1,000. But in 6×6 Crazyhouse, there's almost no competition — and a small but well-tuned engine can become the best.

4. Self-play on work servers overnight is a viable strategy. 8 hours × 12 cores every night — and in a couple of months, the engine accumulated 27,000 positions that function as an opening book.

5. Null-move pruning must be disabled in Crazyhouse when the opponent has pieces in hand. I learned this after several inexplicable losses — the engine "skipped" a move and failed to see that the opponent could drop a knight with check.

What I'd Do Differently

Looking back:

  • Write the core in Rust from the start. I spent months optimizing Python, but the 50x speedup only came with Rust anyway. Prototyping in Python is fine, but I shouldn't have stayed in it that long.
  • Start playing strong opponents sooner. Self-play and random matches didn't expose weaknesses in the eval function. Real growth only began after games with the top 20.
  • Log more thoroughly. In the early months, I didn't record why the engine chose a specific move. Once I started logging the search tree, debugging the eval became dramatically easier.

Epilogue: The Ban

A few days after reaching the top spot, chess.com banned the bot's account. Formally — for using computer assistance. Technically, they're right: it's literally a computer. Although I openly stated in the profile and in chats that a bot was playing, chess.com's anti-cheat system doesn't make exceptions. The fair play policy applies equally to everyone.

Disappointing? A little. But the #1 ranking is preserved in a screenshot, the code and 27,000 positions aren't going anywhere, and most importantly — the experience and knowledge remain. And honestly, reaching #1 and immediately getting banned is a kind of perfect ending for a pet project.

The project lives on. The engine can be used locally, you can train your own position cache, or just play through the GUI. Or you could write a bot for Lichess — which, by the way, officially supports bot accounts.

The project is fully open source on GitHub. You can fork it, tweak the eval function to match your style, train your own position cache, and launch your own bot.

And if you haven't tried Minihouse — give it a shot. It's chess on steroids: fast, tactically wild, and every game is different from the last. Small board, 2–3 minute games. The perfect format to kill a lunch break.

Happy to answer questions in the comments — especially from those who have worked with chess programming, PyO3, or search optimization.