NICE Chess Engine ♟️
Overview
The Not Intelligent Chess Engine, otherwise known as NICE, is a UCI chess engine that implements key learning principles using low-level languages. NICE is developed in C++ for speed and efficiency, allowing us to utilize low-level operations and CPU instructions to further speed up the engine. The engine uses Data-Oriented Design principles, prioritizing performance over object-oriented data representation.
NICE is live on Lichess for all to try, challenge, and test its strength. It is comparable to Stockfish Level 5, with an estimated Elo rating of 1500-1700.
Technical Highlights
- Board Representation: Hybrid approach using Bitboards and Mailbox
- Move Generation: Optimized for sliders and leapers with precomputed lookup tables
- Search Algorithm: Negamax with Alpha-Beta pruning
- Evaluation: Material + Piece-Square Tables
- Performance: ~1500-1700 Elo rating
Board Representation
There are typically 2 ways to represent they game board in an engine: Mailbox and Bitboards.
Approach 1: Mailbox
Mailbox is the more intuitive approach where the board is represented as an array of 64 elements where each element contains the ID of what type of piece is on that square.
Advantages ✅
- Ultra-fast lookup speed: $O(1)$ time complexity to check what piece is on any specific square
- Perfect for checking if a square is occupied or capturing pieces
Disadvantages ❌
- Inefficient piece location: Must loop through entire 64-element array to find pieces
- Even with only 3 pieces on the board, must scan all 64 squares
- No quick way to know where pieces are without iteration
Approach 2: Bitboards
Bitboard is a way of compressing the idea of the mailbox into a single 64-bit integer. It leverages the fact that there are 64 squares on the board and that modern computer hardware and CPUs have a 64-bit architecture. This means we can fit information of an entire board inside a single CPU register.
Why This Matters
Each bit in the 64-bit integer represents one square on the board. If the bit is 1, there’s a piece on that square. If it’s 0, the square is empty.
| 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
| 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
| 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
| 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
If we visualise the board being numbered as show above, we can map each square to 1 bit in a 64 bit unsigned integer.
| 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
| 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
| 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 |
| 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 |
| 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 |
| 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
| ♙ | ♙ | ♙ | ♙ | ♙ | ♙ | ♙ | ♙ |
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
The board above with pawns can be represented internally as:
0b00000000 00000000 00000000 00000000 00000000 00000000 11111111 00000000
In this example, bits 8 to 15 have been flipped to 1, representing pieces occupying those squares.
Advantages ✅
- Lightning-fast move generation: All move generation is bitwise operations
- Instant attacked square checks: Simple bitmask application
- Fast piece counting: CPU instructions like
LSBandPopCountgive instant results - Compact memory footprint: 13 bitboards = 104 bytes vs Mailbox = 252 bytes
Disadvantages ❌
- No piece type information: Only shows occupied/empty, not what piece is there
- Multiple bitboards needed: One for each piece type (13 total for NICE)
The Solution: Hybrid Approach 🎯
The hybrid approach combines both Bitboards and Mailbox. Notice how the strengths of one representation perfectly complement the weaknesses of the other!
How NICE Uses Both
- Bitboards for fast move generation and attack queries
- Mailbox for instant piece-type lookups
The Trade-off
The only overhead is ensuring both representations stay synchronized to prevent board corruption. This is a small price to pay for the combined benefits of both approaches!
Move Generation
Move Encoding
A move is represented as a compact 32-bit integer for maximum efficiency and minimal memory footprint.
Bit Layout
| Bits | Purpose | Range |
|---|---|---|
| 0-5 | From square | 0-63 |
| 6-11 | To square | 0-63 |
| 12-17 | Flags | Castling, en passant, captures |
| 18-21 | Promoted to | Piece type |
| 22-25 | Piece captured | Piece type |
This compact representation means each move takes just 4 bytes of memory!
Movement Categories
Chess pieces fall into two fundamental categories based on how they move:
🔹 Sliders
Pieces that slide across the board until hitting another piece or the edge
- Rooks (horizontal/vertical)
- Bishops (diagonal)
- Queens (all directions)
🔸 Leapers
Pieces that “teleport” to their destination without blockers affecting them
- Knights (L-shape jumps)
- Kings (one square any direction)
- Pawns (special case: move like sliders, capture like leapers)
Slider Move Generation
Using directional offsets, we calculate the next position iteratively. The process loops until:
- A blocker (piece) is encountered
- The edge of the board is reached
The offsets act as cardinal directions for movement:
| +7 | +9 | ||||||
| ♝ | |||||||
| -9 | -7 | ||||||
| +8 | |||||||
| -1 | ♜ | +1 | |||||
| -8 | |||||||
Leaper Move Generation
Since a Knight at square E4 always attacks the same squares, we don’t need runtime calculations!
Optimization Strategy
- Pre-compute lookup tables for all 64 squares at startup
- Single array lookup at runtime:
Attacks = KnightTable[square_index] - Same approach works for Kings
This turns move generation into an O(1) operation!
| X | X | ||||||
| X | X | ||||||
| ♘ | |||||||
| X | X | ||||||
| X | X | ||||||
Special Case: Pawns ♙
Pawns are unique hybrids:
- Move like sliders (forward only)
- Capture like leapers (diagonally)
We handle them with specific bitmasks accounting for:
- En Passant captures
- Double-push from starting position
Board Evaluation
Material Evaluation
Board evaluation is primarily based on material value - a simple but highly effective approach.
Benefits
- Enables tactical thinking at high search depths
- Engine actively seeks moves that win material
- Foundation for all position evaluation
Positional Evaluation
Material alone creates a tactical genius with no strategic sense. Without positional awareness, the engine might place knights on terrible squares!
Solution: Piece-Square Tables (PST)
PSTs assign positional bonuses for placing pieces on optimal squares.
Example: Knight PST
| -50 | -40 | -30 | -30 | -30 | -30 | -40 | -50 |
| -40 | -20 | 0 | 0 | 0 | 0 | -20 | -40 |
| -30 | 0 | 10 | 15 | 15 | 10 | 0 | -30 |
| -30 | 5 | 15 | 20 | 20 | 15 | 5 | -30 |
| -30 | 0 | 15 | 20 | 20 | 15 | 0 | -30 |
| -30 | 5 | 10 | 15 | 15 | 10 | 5 | -30 |
| -40 | -20 | 0 | 5 | 5 | 0 | -20 | -40 |
| -50 | -40 | -30 | -30 | -30 | -30 | -40 | -50 |
Key Insights from Knight PST
- Center squares (+15 to +20): Knights dominate from the center
- Edge squares (-30 to -50): “Knights on the rim are dim!”
- Strategic positioning rewarded through evaluation bonuses
This teaches the engine positional chess principles through numbers!
Search Algorithm
Negamax with Alpha-Beta Pruning
NICE implements Negamax, an elegant optimization of the Minimax algorithm based on a simple principle:
\[negamax(a, b) = -negamax(-a, -b)\]What’s good for you is equally bad for your opponent
How It Works
- Recursive search to specified depth
- Evaluate leaf nodes using material + PST
- Propagate scores back up the tree
The result is an evaluation tree like this:
graph TD
A1(("-6"))
B1["-2"]
B2["+6"]
B3["-2"]
C1(("+2"))
C2(("-5"))
C3(("-6"))
C4(("+2"))
C5(("+4"))
C6(("+8"))
D1["+5"]
D2["-3"]
D3["-2"]
A1 --- B1
A1 --- B2
A1 --- B3
B1 --- C1
B1 --- C2
B2 --- C3
B3 --- C4
B3 --- C5
B3 --- C6
C2 --- D1
C2 --- D2
C4 --- D3
Alpha-Beta Pruning: The Speed Boost ⚡
Alpha-beta pruning cuts off entire branches when a better solution is already known. This allows massive sections of the search tree to be skipped!
Performance Impact
- Without pruning: Search millions of positions
- With pruning: Skip 60-90% of positions
- Same result, fraction of the time
Move Ordering: Making Pruning Effective
Pruning only works well if good moves are searched first.
NICE’s Move Ordering Strategy
- Captures - Forcing moves that win material
- Killer Moves - Previously successful quiet moves
- Quiet Moves - Everything else
Killer Move Heuristic
Killer moves are quiet moves that caused cutoffs in similar positions. By checking these first, we dramatically improve pruning efficiency.
Think of it as the engine’s memory of what worked before!
Summary
The NICE chess engine combines:
- ✅ Hybrid board representation for speed
- ✅ Efficient move generation (bitwise operations + precomputed tables)
- ✅ Material + positional evaluation
- ✅ Negamax search with alpha-beta pruning
- ✅ Smart move ordering with killer moves
Result: ~1700 Elo performance comparable to Stockfish Level 5!