The first thing I searched when I started writing move generation for my chess engine was, roughly, "how do rooks and bishops move in a chess engine." The third result mentioned magic bitboards in the opening paragraph. So did the fifth. By the time I'd read four articles I had a browser tab full of 64-bit hexadecimal constants, a diagram about "perfect hashing," and absolutely no idea how a rook was supposed to stop when it bumped into a pawn.
That's not a criticism of those resources — they're written by people who solved this years ago and moved on to the interesting part. But it left me with a strange feeling: everyone was optimising a thing I hadn't built yet. So I closed the tabs and decided to write the dumbest possible sliding piece move generator, and not look at an optimisation until my version was provably correct. I gave myself permission to be slow.
That decision taught me more than any optimisation could have. This is the article I wanted back then: what sliding pieces actually are, how to generate their moves from first principles in Java, every bug I hit, and what I gained by deliberately skipping the clever solution.
Table of Contents
- What makes sliding pieces different?
- My first (very bad) attempt
- Thinking in directions instead of squares
- Walking along a ray
- Building it in Java
- The bugs that consumed most of my time
- Testing every direction
- Performance was better than I expected
- So what are magic bitboards?
- Why I don't regret starting simple
- When simple is actually the better choice
- What I'd improve today
- Lessons learned
- Frequently asked questions
- Conclusion
What Makes Sliding Pieces Different?
Chess has two families of moving pieces. The first jumps: knights and kings move a fixed distance and don't care what sits between them and their destination. A knight on d4 attacks the same eight squares whether the board is empty or packed. The second family slides: rooks, bishops and queens travel along a line for an unbounded number of squares, and everything about their movement depends on what else is on the board.
- Rook — moves along ranks and files. Four directions: north, south, east, west.
- Bishop — moves along diagonals. Four directions: north-east, north-west, south-east, south-west.
- Queen — moves along both. All eight directions. A queen is genuinely just a rook and a bishop standing on the same square.
Three rules govern every slide, and getting any one of them wrong produces illegal chess:
- Continuous movement. The piece occupies every square along the way. It cannot skip over a square and land beyond it.
- Blocking. The first occupied square along the ray ends the slide. If it holds a friendly piece, the piece stops before it. If it holds an enemy piece, the piece may capture and stop on it. Either way, nothing beyond that square is reachable in that direction.
- Board edges. The ray ends at the edge of the board. There is nothing past the h-file.
Compare that with the jumpers. A knight's move list is a fixed set of offsets you precompute once and reuse forever; a king's is the same, only shorter. Pawns are fiddly — different capture and move directions, a double-step, en passant, promotion — but a pawn's reach is still local. You can look at a pawn and its immediate neighbourhood and know everything.
Sliding pieces are the only pieces whose legal moves depend on arbitrarily distant squares. A pawn appearing on g7 changes what a bishop on a1 can do. That's the whole difficulty in one sentence, and it's why an entire branch of chess programming exists just to answer "given this square and this occupancy, what does the rook attack?" quickly.
Key Takeaway. Knights and kings have fixed move sets. Sliding pieces have move sets that are a function of the entire board. Every technique in this article — simple or advanced — is an answer to that one complication.
My First (Very Bad) Attempt
My first instinct was, I think, everyone's first instinct: loop over the whole board and ask each square whether the rook could reach it.
// My first rook generator. Please don't ship this.
for (int r = 0; r < 8; r++) {
for (int c = 0; c < 8; c++) {
if (r == rookRow || c == rookCol) {
moves.add(new Move(rookRow, rookCol, r, c));
}
}
}
It "worked," which is the dangerous part. On an empty board with a lone rook it produced exactly fourteen moves — the correct number — and I felt briefly like a genius. Bishops got the equivalent check with Math.abs(r - row) == Math.abs(c - col) and looked right too.
Then I loaded a real position and my rook casually moved through three of its own pawns to reach the eighth rank.
Of course it did. The loop asks "is this square on my line?" It never asks "can I get there?" There's no notion of a path in that code at all. So I patched it: a check for the destination holding a friendly piece. That fixed self-capture and nothing else — the rook still teleported past enemy pieces onto the square behind them.
So I patched the patch, writing a helper that walked between source and destination looking for blockers. Now every candidate move triggered its own little loop, plus special cases for whether to increment or decrement each coordinate. Eighty-odd lines for one piece type, and I couldn't hold it in my head. The bishop version was worse — four sign combinations, one of which I got backwards, so bishops moving south-west were validated along the north-east path. That one survived two days because it only surfaced when a specific square was occupied.
What I eventually understood is that I was asking the question backwards. "Which squares can this piece reach?" invites you to enumerate the board. But a sliding piece doesn't experience the board as a set of squares — it experiences a handful of lines it walks down until something stops it. The destination isn't an input to the problem. It's an output.
Common Mistakes — the "check every square" approach
- Treating "is on the same rank or file" as the whole rule, ignoring the path entirely
- Checking only the destination square for a blocker, so pieces jump over things
- Allowing captures of your own pieces because you only tested for emptiness
- Hand-rolling direction signs for path validation and getting one combination wrong
- Ending up with nested loops that are impossible to reason about or test
Reflection. The useful thing about that attempt wasn't the code — I deleted all of it. It was learning that a bad model produces code that fights you. Every fix made it longer instead of clearer, and that's a reliable signal the abstraction is wrong. I now treat "my patches are making this worse" as a prompt to step back rather than push harder.
Thinking in Directions Instead of Squares
What fixed this wasn't a clever algorithm. It was a change of mental model, and it happened while I was drawing a board on paper.
I put a rook on d4 and traced lines outward, and it struck me that I wasn't thinking about squares at all — I was thinking about arms reaching out from the piece. Four of them. They're independent: what happens on the north arm has nothing to do with the east arm. Each just extends until it hits something.
In chess programming that arm is called a ray, and once I had the word, the code wrote itself. Here's the rook on d4 with its four rays on an empty board:
Rook on d4 — four rays
a b c d e f g h
+---+---+---+---+---+---+---+---+
8 | | | | N | | | | |
+---+---+---+---+---+---+---+---+
7 | | | | N | | | | |
+---+---+---+---+---+---+---+---+
6 | | | | N | | | | |
+---+---+---+---+---+---+---+---+
5 | | | | N | | | | |
+---+---+---+---+---+---+---+---+
4 | W | W | W | R | E | E | E | E |
+---+---+---+---+---+---+---+---+
3 | | | | S | | | | |
+---+---+---+---+---+---+---+---+
2 | | | | S | | | | |
+---+---+---+---+---+---+---+---+
1 | | | | S | | | | |
+---+---+---+---+---+---+---+---+
And the bishop, same square, four diagonal rays:
Bishop on d4 — four rays
a b c d e f g h
+---+---+---+---+---+---+---+---+
8 | | | | | | | NE| |
+---+---+---+---+---+---+---+---+
7 | NW| | | | | NE| | |
+---+---+---+---+---+---+---+---+
6 | | NW| | | NE| | | |
+---+---+---+---+---+---+---+---+
5 | | | NW| . | NE| | | |
+---+---+---+---+---+---+---+---+
4 | | | | B | | | | |
+---+---+---+---+---+---+---+---+
3 | | | SW| | SE| | | |
+---+---+---+---+---+---+---+---+
2 | | SW| | | | SE| | |
+---+---+---+---+---+---+---+---+
1 | SW| | | | | | SE| |
+---+---+---+---+---+---+---+---+
Eight directions exist in total. Every sliding piece uses some subset:
| Direction | Row change | Column change | Used by |
|---|---|---|---|
| North | +1 | 0 | Rook, Queen |
| South | -1 | 0 | Rook, Queen |
| East | 0 | +1 | Rook, Queen |
| West | 0 | -1 | Rook, Queen |
| North-East | +1 | +1 | Bishop, Queen |
| North-West | +1 | -1 | Bishop, Queen |
| South-East | -1 | +1 | Bishop, Queen |
| South-West | -1 | -1 | Bishop, Queen |
Look at what that table does to the problem. The difference between a rook, a bishop and a queen collapses into which rows you iterate over. The movement logic is identical for all three. I'd been writing three algorithms; there was only ever one, with three different inputs.
Reflection. The breakthrough here was a noun. Once "ray" existed in my head, the code became a direct translation of the thing I was picturing — and code that mirrors your mental model is code you can debug at 1am. I've since noticed the pattern generally: when an implementation feels tangled, I'm usually missing a word for something.
Walking Along a Ray
The algorithm for one piece, in plain English, is five steps. That's the whole thing.
For each direction the piece can travel:
- Start at the piece's square and step one square in that direction.
- Check bounds. If you've walked off the board, this ray is finished. Move to the next direction.
- If the square is empty, record a quiet move and keep stepping in the same direction.
- If the square holds an enemy piece, record a capture — then stop. You may take it, but you can't go past it.
- If the square holds a friendly piece, record nothing and stop. You can't take your own piece and you can't jump it.
Here it is as pseudocode, deliberately written the way you'd say it out loud:
for each direction in piece.directions:
row = startRow + direction.rowStep
col = startCol + direction.colStep
while (row, col) is on the board:
target = board[row][col]
if target is empty:
addMove(start, (row, col)) // quiet move, keep walking
else if target is an enemy:
addMove(start, (row, col)) // capture
break // and stop
else:
break // friendly piece, stop
row = row + direction.rowStep // take another step
col = col + direction.colStep
Five things are worth saying explicitly, because each is a place I'd previously gone wrong:
- The bounds check is the loop condition. Checking bounds before reading
board[row][col]is what prevents the array index exception. Order matters. - The step happens at the end. Increment at the top and you skip the ray's first square — a classic off-by-one that produces a rook unable to move one square.
- Both "stop" cases break the inner loop, not the outer. A blocker ends one ray, not the whole piece. I got this wrong once and my queen only generated moves in a single direction.
- The capture is recorded before the break. Reversing those two lines silently removes every capture from your engine.
- There's no special code for board edges. The bounds check handles it. A rook in a corner just finds two of its four rays terminate immediately.
Notice what's not in there: any mention of rook, bishop or queen. It's piece-agnostic. That's the payoff of thinking in directions.
Building It in Java
Here's the shape of my implementation, trimmed for readability. The board is a plain 8×8 array where 0 means empty, positive values are white pieces and negative values are black. It's not sophisticated, and that's deliberate — I wanted to be able to print it and read it.
Direction arrays
Directions are just pairs of offsets, stored as a 2D int array where each row is {rowStep, colStep}.
// {rowStep, colStep} — N, S, E, W, then NE, NW, SE, SW
private static final int[][] ROOK_DIRS = {
{ 1, 0}, {-1, 0}, { 0, 1}, { 0, -1}
};
private static final int[][] BISHOP_DIRS = {
{ 1, 1}, { 1, -1}, {-1, 1}, {-1, -1}
};
// A queen is literally both sets concatenated
private static final int[][] QUEEN_DIRS = {
{ 1, 0}, {-1, 0}, { 0, 1}, { 0, -1},
{ 1, 1}, { 1, -1}, {-1, 1}, {-1, -1}
};
Board indexing and helpers
These three tiny methods carry surprising weight. Every bounds check goes through onBoard, so if my definition of "on the board" is ever wrong, it's wrong in exactly one place.
private boolean onBoard(int row, int col) {
return row >= 0 && row < 8 && col >= 0 && col < 8;
}
private boolean isEmpty(int row, int col) {
return board[row][col] == EMPTY;
}
/** True if the square holds a piece of the opposite colour to 'white'. */
private boolean isEnemy(int row, int col, boolean white) {
int piece = board[row][col];
if (piece == EMPTY) return false;
return white ? piece < 0 : piece > 0;
}
The single generator that does all the work
This is the method the whole article builds to. It takes a square and a set of directions, and it doesn't know or care what kind of piece it's generating for.
/**
* Walks every given direction from (startRow, startCol), collecting moves
* until each ray is blocked or leaves the board.
*/
private void generateSlidingMoves(int startRow, int startCol,
int[][] directions,
boolean white,
List<Move> moves) {
for (int[] dir : directions) {
int rowStep = dir[0];
int colStep = dir[1];
int row = startRow + rowStep;
int col = startCol + colStep;
while (onBoard(row, col)) {
if (isEmpty(row, col)) {
moves.add(new Move(startRow, startCol, row, col));
} else {
// First piece we meet ends this ray, one way or another.
if (isEnemy(row, col, white)) {
moves.add(new Move(startRow, startCol, row, col));
}
break;
}
row += rowStep;
col += colStep;
}
}
}
In plain English: for each direction, take the two offsets, step once off the origin square, then loop while we're still on the board. Empty square — add a move and fall through to the step at the bottom. Occupied square — add a move only if it's an enemy, then break out of this ray entirely. The outer for picks up the next direction and starts fresh from the origin.
Rook, bishop and queen
With that in place, the three piece types become three one-line methods:
public void generateRookMoves(int row, int col, boolean white, List<Move> moves) {
generateSlidingMoves(row, col, ROOK_DIRS, white, moves);
}
public void generateBishopMoves(int row, int col, boolean white, List<Move> moves) {
generateSlidingMoves(row, col, BISHOP_DIRS, white, moves);
}
public void generateQueenMoves(int row, int col, boolean white, List<Move> moves) {
generateSlidingMoves(row, col, QUEEN_DIRS, white, moves);
}
You could also express the queen as a rook call plus a bishop call into the same list, which is arguably more honest about what a queen is. I used the explicit eight-direction array because it saves a loop setup. Either is fine — taste, not correctness.
Why the helper methods mattered more than I expected
I nearly inlined onBoard and isEnemy to "avoid the overhead." I'm glad I didn't, for reasons that had nothing to do with performance:
- One definition of each rule. My earlier code checked bounds in six places with slightly different conditions. One used
<= 8. Guess which one crashed. - Named conditions read as sentences.
while (onBoard(row, col))tells you what it means; the raw four-way comparison makes you parse it. - They're trivially testable. Ten lines covered
isEnemyfor every piece-and-colour combination, and then I could trust it forever. - Changing board representation touched three methods. When I later experimented with a flat 64-element array, only the helpers changed. The ray walking was untouched.
Reflection. That last point is the one I'd underline. The helpers accidentally created a seam between "how the board is stored" and "how pieces move." I didn't design that — I extracted them to stop repeating myself — but it's why move generation survived a representation change without a rewrite.
The Bugs That Consumed Most of My Time
Let me be honest about the ratio. Writing the generator above took maybe an hour. Making it correct took most of two weekends. Here are the bugs, roughly in the order they humbled me.
1. The rook that wrapped around the board
Before I moved to row/column coordinates, my first ray implementation used a flat 64-element array and stepped east by adding 1 to the index. A rook on h4 generated a move to a5 — not a legal chess move, not even a legal shape of one. I reproduced it with a lone rook on h4 on an empty board: the east ray produced eight moves instead of zero, marching across the next rank.
Index 31 (h4) plus 1 is index 32 (a5), a perfectly valid array index. The array has no concept of an edge, so validating index >= 0 && index < 64 can never catch it. I switched to explicit row and column coordinates, where "off the east edge" means col == 8. Lesson: validate in the coordinate system where the constraint actually exists. Flattening the board is fine; flattening your validation throws away the information you need.
2. Bishops leaking onto the wrong diagonal
The same bug in a different hat, and far harder to spot. A bishop near the a-file produced a move that looked plausible — diagonal square, correct colour — but on the far side of the board. Because it was still a diagonal relative to the wrapped position, nothing screamed in a move list. I only caught it by printing the board with every destination marked rather than reading a list.
Lesson: the rook version of a wrapping bug shouts; the bishop version whispers. Diagonal wrapping produces output that passes a casual glance.
3. Capturing my own pieces
My engine happily played Rxd4 onto its own knight. Evaluation then recorded that it had won a knight, found this delightful, and did it constantly. The cause: if (!isEmpty(row, col)) { addMove(...); break; }. I'd correctly worked out that the first occupied square ends the ray, then forgot that whether to record a move depends on the colour of what's there.
Lesson: "stop here" and "you may take this" are two separate decisions that happen at the same square. Collapsing them into one condition is the most natural mistake in this algorithm.
4. Moving beyond a blocker
Rooks passing straight through enemy pawns — the move list contained both the capture and every square behind it. The cause was continue where I meant break. One word. I didn't find it by reading; I'd read that block a dozen times. I found it by printing the ray as I walked it, one line per square, and watching the walk carry on past a square it had just described as a capture. The output narrated the bug better than my eyes could.
5. The off-by-one that made pieces immobile
During a refactor all my rooks lost the ability to move exactly one square. Two, three, seven — fine. Never one. I'd restructured the loop so the step happened at the top of the body rather than the bottom, skipping each ray's first square. It's invisible on reading, because both versions look reasonable.
Lesson: loops that both step and test have two correct orderings and several plausible wrong ones. I now trace that kind of loop by hand for a two-square case before running it.
6. Duplicate moves from the queen
Move counts for positions with a queen were consistently too high, with exact duplicates — same origin, same destination, twice. I'd originally implemented the queen as "call rook, then call bishop," later switched it to the generic generator with QUEEN_DIRS, and left the old calls sitting above the new one.
Lesson: two implementations of the same rule will eventually both run. Pick one, delete the other. Duplicates are also worse than they look — they inflate node counts, skew move ordering, and quietly corrupt the search statistics you'll later use to judge whether an optimisation helped.
7. Row/column confusion
board[row][col] in one file, board[col][row] in another. Both compile, both run, neither is wrong in isolation. Everything was mirrored along the diagonal — and rooks hid it, because a rook's move set is symmetric that way. It only surfaced when I integrated the board printer and pieces appeared transposed.
The fix was a comment stating the convention at the top of the board class, and a rule that nothing outside that class touches the raw array. I also added a squareName(row, col) helper so debug output spoke in "d4" rather than "3,3" — "d4" is something I can check against a real chess board; "3,3" is something I have to decode first.
Debugging Tip. Print your moves in algebraic notation, not array indices. The moment my log said Rd4-h4 instead of 3,3 -> 3,7, I could spot illegal moves at a glance instead of translating each one in my head. Debug output that speaks your domain's language is worth the twenty minutes it takes to write.
Reflection. Nearly all of these shared a shape: the code was almost right, and the wrongness only appeared in specific positions. That's the defining characteristic of move generation bugs, and the reason systematic testing matters so much here. You cannot find them by staring.
Testing Every Direction
After the fourth bug I stopped fixing things ad hoc and built a small harness. Every ray, in every position, ends for one of three reasons: it hit the edge, it hit a friendly piece, or it hit an enemy piece. So I wrote positions that force each ending, in each direction, from squares where the geometry differs.
- Empty board, centre square. A rook on d4 produces exactly 14 moves, a bishop 13, a queen 27. Easy to verify by hand on a real board, which makes them excellent anchors.
- Empty board, corners. A rook on a1 still has 14 moves but only two live rays; a bishop on a1 has 7. Corners stress the bounds check hardest — half your directions terminate on step one.
- Empty board, every square. Loop all 64 squares and assert the rook always yields 14. Cheap, and it catches any edge-handling asymmetry instantly.
- Friendly blocker. Rook d4, own pawn d6 — the north ray yields d5 and stops. This caught the self-capture bug.
- Enemy blocker. Rook d4, enemy pawn d6 — the ray yields d5 and d6, then stops. This caught the
continue/breakbug. - Adjacent blockers. A ray terminating on its very first square: zero moves that way, and crucially no crash.
- Nearly full board. The starting position, where a rook on a1 has exactly zero legal moves — a wonderfully unambiguous assertion.
- All eight directions blocked. A queen boxed in by its own pieces should generate nothing at all.
The tests are unglamorous, and that's the point:
@Test
void rookOnEmptyBoardHasFourteenMoves() {
Board b = Board.empty();
b.place(3, 3, WHITE_ROOK); // d4
List<Move> moves = new ArrayList<>();
b.generateRookMoves(3, 3, true, moves);
assertEquals(14, moves.size());
}
@Test
void rookStopsBeforeFriendlyPiece() {
Board b = Board.empty();
b.place(3, 3, WHITE_ROOK); // d4
b.place(5, 3, WHITE_PAWN); // d6, own pawn
List<Move> moves = new ArrayList<>();
b.generateRookMoves(3, 3, true, moves);
assertTrue(contains(moves, "d5"));
assertFalse(contains(moves, "d6")); // can't take our own pawn
assertFalse(contains(moves, "d7")); // and can't jump it
}
@Test
void rookCapturesEnemyThenStopsThere() {
Board b = Board.empty();
b.place(3, 3, WHITE_ROOK); // d4
b.place(5, 3, BLACK_PAWN); // d6, enemy pawn
List<Move> moves = new ArrayList<>();
b.generateRookMoves(3, 3, true, moves);
assertTrue(contains(moves, "d5"));
assertTrue(contains(moves, "d6")); // capture is legal
assertFalse(contains(moves, "d7")); // nothing beyond it
}
The other tool that earned its place was a printer that drew a move list's destinations onto an ASCII board. This told me more in one glance than a hundred logged move objects:
Rook d4, own pawn d6, enemy pawn f4:
a b c d e f g h
+---+---+---+---+---+---+---+---+
8 | | | | | | | | |
+---+---+---+---+---+---+---+---+
7 | | | | | | | | |
+---+---+---+---+---+---+---+---+
6 | | | | P | | | | | <- own pawn, ray stops below it
+---+---+---+---+---+---+---+---+
5 | | | | * | | | | |
+---+---+---+---+---+---+---+---+
4 | * | * | * | R | * | x | | | <- x = capture, nothing past it
+---+---+---+---+---+---+---+---+
3 | | | | * | | | | |
+---+---+---+---+---+---+---+---+
2 | | | | * | | | | |
+---+---+---+---+---+---+---+---+
1 | | | | * | | | | |
+---+---+---+---+---+---+---+---+
I should also mention the standard technique, because it's the right thing to graduate to: chess programmers verify move generation with perft, which counts leaf nodes reachable from a position at a given depth and compares against published reference values. It's a far stronger check than hand-written cases, because it exercises rule interactions you'd never think to test. I added it later than I should have — but the directional tests weren't wasted, because when perft told me a count was wrong, they told me where.
Key Takeaway. Test the three ways a ray can end — edge, friendly, enemy — in every direction, from a centre square and a corner. That's a small, finite matrix, and it catches almost every sliding piece bug that exists.
Performance Was Better Than I Expected
I'll be careful here, because this is exactly the kind of section where it's tempting to invent impressive numbers. I didn't build a rigorous benchmark harness comparing implementations under controlled conditions, so I won't quote figures I can't stand behind.
What I can say is qualitative and honest: the naive ray walker was never the thing that made my engine feel slow. That surprised me, given how strongly the internet implied sliding piece generation is the bottleneck you must solve first.
In hindsight it makes sense. A rook has four rays of at most seven squares, so the absolute worst case is 28 iterations of a very short loop — and real positions have pieces on them, so rays terminate early. The work per node is small, predictable, and touches memory that's already hot. When I profiled, the time was going to search and evaluation, not to walking rays.
That's what profiling teaches you that intuition doesn't: the bottleneck is rarely where the tutorials say it is for your particular program. Magic bitboards are a significant, real optimisation for engines searching enormous numbers of positions per second, where move generation genuinely is a top cost. At the scale I was working — depth-limited search, a learning project — the ray walker had headroom to spare. Which gives you the order: make it correct, measure it, then optimise what the measurement points at. I skipped to step three exactly once on this project and spent a day making something faster that was never slow.
So What Are Magic Bitboards?
Having deferred them all article, I owe you an explanation — a conceptual one. I'll stay deliberately shallow, because magic bitboards deserve their own careful treatment and a half-explanation is worse than none.
Start from the problem. For jumping pieces you can precompute everything: a knight on d4 attacks the same eight squares forever, so you build a 64-entry table at startup and never compute again. You'd love to do that for a rook, but you can't — a rook's attack set depends on the blockers around it, and there are many possible blocker arrangements per square. So the question becomes: can we still make it a table lookup, by including the blockers in the key?
That's what magic bitboards do, and the chain of ideas is roughly:
- Occupancy. For a given square, only certain squares can possibly block the rook — the ones along its rays, excluding the outermost, since a piece on the edge blocks nothing beyond itself. That's a small set of relevant squares.
- Hashing. You take the bits describing which of those relevant squares are occupied and turn them into a compact index — traditionally by multiplying by a carefully chosen 64-bit constant and shifting, which scatters the bits into a small range.
- Magic numbers. Those constants are "magic" because they're found by search, not derived. Someone runs a program that tries candidates until it finds one where no two meaningful occupancies collide. That's it. There's no deep mathematics being invoked; there's a lot of trial and error, done once, and the winning numbers get published and reused.
- Lookup tables. The index goes into a precomputed table holding the correct attack bitboard for that square-and-occupancy combination. Move generation becomes a mask, a multiply, a shift and an array read — no loop at all.
Here's the thing to take from this. Every one of those bullets describes an optimisation of the ray walk you already understand. "Relevant occupancy" only means something if you know a rook's ray stops at the first blocker. "The correct attack set for this occupancy" is precisely what my slow loop computes each time. Magic bitboards don't do anything different — they just refuse to redo the walk.
When I finally read an implementation properly, I understood it in an afternoon. Not because I'm quick, but because I'd spent two weekends becoming intimate with the exact problem it solves. The article that had baffled me a year earlier was suddenly describing a faster route to an answer I knew how to compute by hand.
| Dimension | Ray-based generation | Magic bitboards |
|---|---|---|
| Core operation | Loop outward until blocked | Mask, multiply, shift, table lookup |
| Work per query | Proportional to ray length | Effectively constant |
| Memory used | Almost none | Sizeable precomputed tables |
| Startup cost | None | Table generation (and finding magics) |
| Lines of code | Roughly one screen | Considerably more, plus constants |
| Readability | Reads like the chess rule | Opaque without prior context |
| Debuggability | Step through it square by square | Wrong answers give few clues |
| Best suited to | Learning, small engines, correctness reference | Competitive engines at high node counts |
Why I Don't Regret Starting Simple
I could have copied a magic bitboard implementation on day one — it's freely available, well documented, and it would have worked. Here's what I'd have missed.
- Confidence. There's a specific unease that comes from code you didn't derive. If I'd pasted in magics and got a wrong move count, I'd have had no idea whether the bug was mine or my misunderstanding of theirs. Because I built the slow version, I knew move generation was right and could look elsewhere. Narrowing the search space like that is worth a lot over a long project.
- Actual understanding. I can explain why a rook's attacks depend on occupancy, why edge squares don't matter for the occupancy mask, and why a queen needs no code of its own. Those aren't memorised facts — they're things I worked out with a pen and verified in a debugger, which is a different quality of knowing.
- Debugging ability. The bug list above is a catalogue of my own carelessness, but each entry left a transferable habit: validate in the right coordinate system, separate "stop" from "capture," print in domain language.
- Reading other engines. When I see
rookAttacks(square, occupancy)in someone's source now, I know exactly what contract it fulfils regardless of the implementation. Understanding the interface came from implementing the naive version of it. - Maintaining my own code. Six months later I came back to add a feature and could still read my move generator. Code you understood once and code you understood deeply age very differently.
Reflection. Starting simple turned an optimisation from a mystery into a decision. Magic bitboards went from something I'd adopt because strong engines use them to something I could adopt for a reason I could articulate, with a correct reference implementation to test against. That second position is so much better to be in.
When Simple Is Actually the Better Choice
I'm not arguing the simple version is always right. Strong engines use advanced sliding-attack techniques for excellent reasons, and if you need to search as deeply as possible, you'll want them. But there's a substantial set of situations where ray-based generation is genuinely the better engineering choice, not merely an acceptable compromise:
- College projects. Your grade and your viva depend on explaining your design. "I walk each direction until blocked" is defensible. A wall of magic constants you can't derive is a liability under questioning.
- Personal learning projects. If the point is to learn, the slow version teaches. Optimised code you don't understand teaches you how to copy.
- A first chess engine. Castling, en passant, promotion, check detection, pins and legality filtering are all ahead of you, and that's where the real rule complexity lives. Don't spend your budget on a performance problem you haven't measured.
- Practising Java. A genuinely good exercise in arrays, loops, extracted helpers and unit testing — everyday craft skills applied to an interesting problem.
- Learning algorithms. Ray traversal, bounds checking and early termination show up in grid problems, pathfinding and graphics. Chess is a pleasant way to internalise them.
- Technical interviews. The directional solution is the one you can write on a whiteboard in five minutes and explain clearly. Nobody's asking you to recall a magic constant.
- As a correctness oracle. Even in a fast engine, keeping the slow implementation around to cross-check the fast one is genuinely useful.
This is the old premature-optimisation principle, with a wrinkle: the cost of optimising early isn't only wasted effort, it's the complexity you take on before you can evaluate whether it's worth it. Complexity you don't understand is a permanent tax on every future change.
What I'd Improve Today
Being fair to the other side, here's what I'd genuinely change and the order I'd do it in.
- Bitboards for board representation. Not for sliding pieces specifically — for everything. Representing the board as 64-bit longs makes occupancy tests, material counting and evaluation much cheaper, and it's the foundational change that makes the rest possible.
- Precomputed attack tables for jumping pieces. Knight, king and pawn attacks never change. Computing them once at startup into a 64-entry array is a small change with a clear win and no conceptual risk.
- Profile before touching sliding pieces. Actually run a profiler rather than guessing. If move generation isn't near the top, sliding-piece optimisation isn't the next job regardless of what any article says.
- Incremental updates instead of regeneration. A lot of my engine recomputed things from scratch after every move that it could have updated in place. Often a bigger win than optimising generation itself, and cheaper to implement.
- Then, and only then, magic bitboards. With a correct reference implementation to test against and a profile that justifies it. I'd keep the ray walker behind a flag so I could diff outputs across thousands of positions.
The ordering is deliberate: every step is testable against what came before, and justified by a measurement rather than a reputation.
| Aspect | Simple arrays + rays | Bitboards for sliding pieces |
|---|---|---|
| Learning curve | Low — mirrors the chess rule directly | Higher — needs bitwise fluency first |
| Code volume | Small and self-contained | Larger, with masks and constants |
| Speed | Adequate for learning and shallow search | Substantially faster at high node counts |
| Memory | Negligible | Tables, from modest to large |
| Ease of debugging | Print the board and step through | Needs custom bitboard printers |
| Portability of the idea | Transfers to any grid problem | Fairly chess-specific |
| When to choose it | First engine, coursework, reference impl | After profiling says generation is hot |
Lessons Learned
Stepping back from chess entirely, here's what this part of the project changed about how I work.
- Decomposition beats cleverness. Three piece types became one algorithm plus a data table. I found that by asking what rooks, bishops and queens genuinely have in common — a question I now ask first whenever code feels repetitive.
- The right noun is half the design. "Ray" gave me the whole implementation. When I'm stuck now, I try to name the thing I'm missing before I code around it.
- Small helpers are structural, not cosmetic.
onBoardandisEnemylooked like tidying, and turned out to be the seam that let me change board representation without rewriting move generation. - Debugging patience is a practisable skill. The
continue-versus-breakbug taught me that re-reading code you believe is correct is nearly worthless. Make the program tell you what it's doing. - Incremental development compounds. Rook, then bishop, then queen, then blockers, then captures — each step small enough to verify, so "what changed" stayed tiny when something broke.
- Tests are a debugging tool, not a chore. I wrote most of mine after hitting bugs, and they immediately started catching regressions I'd otherwise have shipped.
- "Fast enough" is a respectable answer. Engineering isn't the pursuit of maximum performance; it's meeting a requirement with the least complexity you can get away with. Sometimes the boring loop is the correct professional choice.
Frequently Asked Questions
What are sliding pieces in chess programming?
Sliding pieces are the rook, bishop and queen. Unlike knights and kings they don't move a fixed distance — they travel along a line for as many squares as are free, stopping at the edge of the board or at the first piece they meet. If that piece is an enemy they may capture it and stop there; if it's friendly they stop one square earlier. That dependence on the rest of the board is what makes their move generation harder than the jumping pieces'.
How do you generate rook and bishop moves without magic bitboards?
Use ray-based move generation. Define the directions as offset pairs — four for a rook, four diagonals for a bishop, all eight for a queen. For each direction, step one square at a time from the piece's square, checking bounds first. Empty square: record a quiet move and keep stepping. Enemy piece: record a capture and stop. Friendly piece: stop without recording. That's the entire algorithm, and it's identical for all three piece types.
Are magic bitboards necessary for a chess engine?
No — they're a performance optimisation, not a correctness requirement. They matter greatly for competitive engines searching enormous numbers of positions per second, which is why strong engines such as Stockfish use sophisticated sliding-attack techniques. For coursework, a depth-limited search or a first engine, ray-based generation is correct, readable and typically fast enough. Building the simple version first also makes magic bitboards much easier to learn later.
What's the most common bug in sliding piece move generation?
Board wrapping. With a flat 64-element array, stepping east by adding one to the index lets a rook on the h-file silently continue onto the a-file of the next rank. The same affects bishops on diagonals, where it's harder to spot because the result still looks diagonal. The fix is to validate file and rank separately rather than the raw index — which is why many developers use explicit row and column coordinates for ray walking.
Should beginners learn magic bitboards first?
Most people are better served learning ray-based generation first. Magic bitboards combine occupancy masks, hashing by multiplication and large lookup tables, and none of those are intuitive until you understand what a blocker is and why a sliding piece's attacks depend on it. The simple generator gives you that understanding plus a correct reference implementation to validate the optimised version against.
How do I know my move generation is correct?
Start with targeted tests — a rook on an empty centre square has 14 moves, a bishop 13, a queen 27 — then add friendly blockers, enemy blockers and corners. Once those pass, move to perft, the standard technique of counting leaf nodes at a given depth and comparing against published reference values for known positions. Perft tells you a bug exists; targeted tests tell you where. You want both.
Conclusion
If you'd asked me at the start what I hoped to learn, I'd have said something about performance. Search speed, node counts, the satisfaction of making something fast. That's what the articles I was reading were about, so I assumed that's what the skill was.
It wasn't. What I actually took from sliding piece move generation is that I'm now much slower to reach for a solution I don't understand — not because clever solutions are bad, but because I've felt the concrete difference between owning an implementation and borrowing one. Owning it means that when the program does something strange at 11pm, you have somewhere to start.
The naive ray walker isn't the fastest way to generate rook and bishop moves, and I'd never claim otherwise. What it is, is the version I could reason about, test exhaustively, explain to another person and later use as a yardstick for something faster. Every one of those turned out to be worth more than the speed I gave up — and I only gave up speed I wasn't using.
So if you're at the start of a chess engine, or honestly anything where the internet has strong opinions about the optimal approach: build the obvious version first. Let it be slow. Let it be a bit embarrassing. Get it right, prove it's right, then read about the fast way with the enormous advantage of already knowing what problem it solves.
Techniques change. Magic bitboards weren't always the standard answer, and something else may eventually replace them. What doesn't change is the value of having genuinely understood a problem, because understanding is what lets you evaluate the next clever technique instead of just adopting it. Memorised solutions expire. The understanding underneath them is what you get to keep.