Bitboards Explained: Representing a Chess Board in 64 Bits

My chess engine was getting slower, and I couldn't stop adding features. Every new evaluation term, every new rule I encoded, every extra ply of search depth — each one made the whole thing crawl a little more. I'd built the board as a tidy 8×8 array of piece objects, and it worked. It just didn't fly. And I wanted it to fly.

So I did what everyone does. I started reading how the fast engines were built. And every single serious one — every forum post, every open-source project, every write-up worth reading — eventually said the same word: bitboards. Then it would show me a wall of hexadecimal constants and shift operators that looked less like a chess board and more like someone had fallen asleep on the keyboard with the number lock on.

I bounced off bitboards three separate times before they finally clicked. This article is the explanation I wish I'd had on day one — the one that starts with why they exist before drowning you in << and &. If you're building a Java chess engine and you've hit the same wall, I think this will save you a few of the days I lost.

Table of Contents

The Problem With Traditional Board Representations

Let me defend the array first, because that's where nearly everyone starts, and rightly so.

When you're first writing chess engine programming code, you reach for whatever mirrors how you see the board. There are a few classic approaches:

  • 2D array — a Piece[8][8] grid. It looks exactly like a board. board[0][0] is a corner.
  • 1D array — a Piece[64] (or the "0x88" trick with a 128-length array). Same idea, flattened, and often faster to index.
  • Object-per-piece — a full Piece object per square, with color, type, and behavior baked in.
  • Piece lists — instead of storing the board, you store a list of "white knight on c3, black rook on a8," and so on.

These are genuinely good for beginners. They're readable. You can print them. When you're debugging why your bishop won't move, you can literally look at the grid. Here's the kind of thing I started with:

// The friendly beginner board
Piece[][] board = new Piece[8][8];
board[0][4] = new King(Color.WHITE);
board[1][3] = new Pawn(Color.WHITE);

// "Is there a piece on this square?" — trivially readable
if (board[row][col] != null) {
    // do something
}

Nothing wrong with that. I shipped a fully working, rules-legal engine on top of exactly this. The problem isn't correctness. The problem is what happens when you ask the board to do something a lot.

Move generation is the hot path in any engine. To find all the moves for, say, your rooks, an array forces you to:

  1. Loop over squares to find your rooks.
  2. For each rook, loop outward in four directions.
  3. At each step, branch: is this square empty? enemy? friendly? off the board?
  4. Repeat this millions of times per second during search.

Every one of those steps is a memory access, a comparison, and a branch. Modern CPUs hate unpredictable branches and love data that sits close together. An array of piece objects scatters your board across the heap (each object is a separate allocation), so you're chasing pointers and thrashing the cache. The looping and branching dominate your runtime. You're doing an enormous amount of per-square bookkeeping when what you really want is to reason about the whole board at once.

That last sentence is the seed of the whole idea. Hold onto it.

What Exactly Is a Bitboard?

Here's the observation that makes bitboards possible, and it's almost too simple to feel important:

A chess board has 64 squares. A 64-bit integer has 64 bits. So one integer can carry one piece of yes/no information about every square on the board at the same time.

In Java, the 64-bit integer is the primitive long. That's it. That's the datatype. One long is one bitboard.

Now, what does a single bitboard represent? A single true/false fact per square. For example, "is there a white pawn here?" You'd have one bitboard for white pawns, where a bit is 1 if a white pawn stands on that square and 0 otherwise.

A full position isn't one bitboard — it's a small stack of them. A common set-up is twelve:

  • White pawns, knights, bishops, rooks, queens, king (6 bitboards)
  • Black pawns, knights, bishops, rooks, queens, king (6 bitboards)

From those twelve you can cheaply derive others: all white pieces (OR the six white boards together), all black pieces, all occupied squares, all empty squares. Each of those is one long, and each answers a whole-board question in a single value.

The mental shift — and it took me embarrassingly long — is this: you stop thinking "what's on square e4?" and start thinking "which squares have property X?" The array asks about one square at a time. The bitboard describes all sixty-four at once. That reframing is the entire game.

Key Takeaway. A bitboard isn't "the board." It's one layer of the board — one yes/no question answered for all 64 squares in a single 64-bit number. You stack several layers to describe a full position.

Visualizing 64 Bits

This is where it clicked for me, so let's go slowly and draw it.

First, pick a mapping from squares to bit indices. The most common convention (and the one I use) is Little-Endian Rank-File, where A1 is bit 0 and H8 is bit 63. Bits fill up left to right, bottom to top:

Square -> Bit index (LERF mapping)

     a   b   c   d   e   f   g   h
   +---+---+---+---+---+---+---+---+
 8 |56 |57 |58 |59 |60 |61 |62 |63 |   rank 8
   +---+---+---+---+---+---+---+---+
 7 |48 |49 |50 |51 |52 |53 |54 |55 |
   +---+---+---+---+---+---+---+---+
 6 |40 |41 |42 |43 |44 |45 |46 |47 |
   +---+---+---+---+---+---+---+---+
 5 |32 |33 |34 |35 |36 |37 |38 |39 |
   +---+---+---+---+---+---+---+---+
 4 |24 |25 |26 |27 |28 |29 |30 |31 |   d4 = bit 27
   +---+---+---+---+---+---+---+---+
 3 |16 |17 |18 |19 |20 |21 |22 |23 |
   +---+---+---+---+---+---+---+---+
 2 | 8 | 9 |10 |11 |12 |13 |14 |15 |
   +---+---+---+---+---+---+---+---+
 1 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |   a1 = bit 0, h1 = bit 7
   +---+---+---+---+---+---+---+---+

So the formula for a square is index = rank * 8 + file, with rank and file both counting from 0. A1 is rank 0, file 0 → bit 0. D4 is rank 3, file 3 → 3*8 + 3 = 27. H8 is rank 7, file 7 → 63.

Now let's put a single piece down. A white king on E1 (bit 4). As raw binary, reading from bit 63 down to bit 0:

bit:  63                                                    0
      00000000 00000000 00000000 00000000 00000000 00000000 00000000 00010000
                                                              (bit 4 is set = e1)

hex:  0x0000000000000010
dec:  16

The whole board state "there is a white king on e1, and nothing else about white kings" is the number 16. That's genuinely all it is.

Here's the part that makes it readable once you get used to it — the same bitboard drawn as a board, one bit per square, is the format I ended up printing constantly while debugging:

White king on e1 (bit 4):

 8  . . . . . . . .
 7  . . . . . . . .
 6  . . . . . . . .
 5  . . . . . . . .
 4  . . . . . . . .
 3  . . . . . . . .
 2  . . . . . . . .
 1  . . . . 1 . . .
    a b c d e f g h

Let's do a busier one. The white pawns in the starting position all sit on rank 2 — that's bits 8 through 15. Set all eight:

White pawns, start position (bits 8-15):

 8  . . . . . . . .
 7  . . . . . . . .
 6  . . . . . . . .
 5  . . . . . . . .
 4  . . . . . . . .
 3  . . . . . . . .
 2  1 1 1 1 1 1 1 1
 1  . . . . . . . .
    a b c d e f g h

binary: ...00000000 11111111 00000000
hex:    0x000000000000FF00
dec:    65280

That 0xFF00 is a value you'll see so often you start recognizing it on sight — it's "the whole second rank." Once you can flip between the three views — board, binary, hex — bitboards stop being cryptic. The hex constants in other people's engines aren't magic. They're just squares.

Why Engines Love Bitboards

So we've saved some memory — the entire board is twelve longs, 96 bytes, instead of a heap full of objects. Nice, but that's not the real reason. The real reason is how the CPU treats them.

A long fits in a single CPU register. When you AND two bitboards together, the processor operates on all 64 bits in one instruction. Think about what that means for chess:

  • Speed. "Which of my pawns can capture something?" becomes a couple of shifts and ANDs — a handful of instructions — instead of a loop over the board.
  • Parallelism across squares. One bitwise op is effectively 64 tiny operations happening at once. You're not iterating; the hardware does all squares simultaneously.
  • Fewer branches. Instead of "if empty… else if enemy… else if friendly…" per square, you express the same logic as intersections of bitboards. Fewer branches means the CPU's branch predictor stops guessing wrong and stalling.
  • Cache locality. The whole position lives in a dozen registers or a single cache line, not scattered pointers. Data the CPU can find instantly is data it can chew through fast.
  • Dedicated instructions. Counting set bits or finding the lowest set bit are things processors do in hardware, which turns "loop over pieces" into single instructions (more on this soon).

I don't want to oversell the hardware angle, because you don't need to be a CPU architect to use bitboards well. The one-sentence version is: modern processors are extraordinarily good at bit manipulation, and bitboards let a chess engine speak the CPU's native language. Move generation stops being a stroll around a grid and becomes arithmetic.

The Bitwise Operators That Finally Made Sense

I'd "known" bitwise operators for years in the way you know a word you've never actually used. Chess is what made them concrete. Here's each one, explained the way it finally landed for me — with a board, not an abstract truth table.

AND (&) — intersection: "squares in both"

AND keeps bits that are set in both operands. In chess terms it's the answer to "which squares satisfy X and Y?" Want to know if your knight's target squares land on any enemy pieces? AND the knight's attack board with the enemy-occupancy board. Whatever survives is a capture.

long captures = knightAttacks & blackPieces;   // targets that hit black

OR (|) — union: "squares in either"

OR sets a bit if it's set in either operand. This is how you combine boards. All white pieces? OR the six white piece bitboards. All occupied squares? OR white and black together.

long whitePieces = wPawns | wKnights | wBishops | wRooks | wQueens | wKing;
long occupied    = whitePieces | blackPieces;

XOR (^) — toggle: "flip these bits"

XOR flips a bit wherever the other operand has a 1. This is the operator I underrated hardest. Moving a piece is a XOR: flip off the origin square, flip on the destination. One operation with a two-bit mask does the whole move on that bitboard.

long fromTo = (1L << from) | (1L << to);
wKnights ^= fromTo;   // knight leaves 'from', arrives at 'to'

NOT (~) — complement: "everything except"

NOT flips every bit. "All empty squares" is simply ~occupied. "Every square that isn't mine" is ~whitePieces. It's the negative space of the board, and you use it constantly.

long empty = ~occupied;   // 1 wherever there's nothing

Left shift (<<) — move toward higher squares

Shifting left multiplies bit positions upward. In LERF mapping, shifting left by 8 moves every piece up one rank — which is exactly a white pawn's single push. Shift by 1 to move one file to the right (with a caveat we'll hit in the bugs section).

long whitePawnPush = (wPawns << 8) & empty;   // pawns advance one rank

Right shift (>> and >>>) — move toward lower squares

Shifting right moves toward lower-numbered squares — down the board for white, which is how black pawns advance. In Java there's a trap here that cost me time: >> is an arithmetic shift that copies the sign bit, while >>> is a logical shift that pulls in zeros. For bitboards you almost always want >>>, because a bitboard is a bag of bits, not a signed number.

long blackPawnPush = (bPawns >>> 8) & empty;   // note: >>>, not >>

Here's a table I kept taped next to my monitor while learning, because I couldn't hold all six in my head at once:

Operator What it does Typical chess use
& (AND)Bits set in bothFind captures; test if a square is occupied; mask off edges
| (OR)Bits set in eitherCombine piece boards; build occupancy; add a square
^ (XOR)Flip where the mask is 1Move a piece (clear from, set to); toggle a square
~ (NOT)Flip every bitEmpty squares; "not my pieces"; invert a mask
<< (left shift)Move bits to higher indicesWhite pawn pushes; sliding up/right; shifting attack masks
>>> (right shift)Move bits to lower indices (zero-fill)Black pawn pushes; sliding down/left

Creating Your First Bitboard in Java

Enough theory. Let's write the tiny toolkit every bitboard engine is built on. None of this is production-grade — it's meant to be read and understood.

An empty board is the simplest possible bitboard:

long board = 0L;   // no bits set = no pieces of this type anywhere

To place a single piece, you set the bit for its square. The idiom is "shift a 1 to the square's index, then OR it in." That 1L (long literal, note the L) matters — plain 1 is an int and shifting it past 31 misbehaves.

// A single white pawn on e2 (bit 12)
long wPawns = 1L << 12;

// Set a bit — place a piece on 'square'
long setBit(long bb, int square) {
    return bb | (1L << square);
}

Multiple pawns is just multiple bits. The whole starting pawn rank for white is bits 8–15, which we can build by OR-ing, or write directly as the hex constant we met earlier:

long wPawns = 0x000000000000FF00L;   // all of rank 2

// White knights start on b1 (1) and g1 (6)
long wKnights = (1L << 1) | (1L << 6);

Clearing a bit is the mirror image: build a mask with a 1 on the square, flip it with NOT so it's a 1 everywhere except that square, and AND it in. Everything survives except the square you targeted.

// Clear a bit — remove whatever is on 'square'
long clearBit(long bb, int square) {
    return bb & ~(1L << square);
}

Checking occupancy — "is there a piece of this type on this square?" — is an AND and a zero test:

boolean isSet(long bb, int square) {
    return (bb & (1L << square)) != 0L;
}

Deriving the combined boards is where OR shines. Give me the six white boards and I'll give you white occupancy for free:

long whitePieces = wPawns | wKnights | wBishops | wRooks | wQueens | wKing;
long blackPieces = bPawns | bKnights | bBishops | bRooks | bQueens | bKing;
long occupied    = whitePieces | blackPieces;
long empty       = ~occupied;

And moving a piece — the XOR trick from earlier, wrapped up:

long movePiece(long bb, int from, int to) {
    return bb ^ ((1L << from) | (1L << to));
}

Now the two Java standard-library methods that felt like cheating the first time I used them. Iterating over the pieces on a bitboard doesn't need a loop over 64 squares — you jump straight to each set bit using Long.numberOfTrailingZeros, which returns the index of the lowest set bit. You process it, clear it, and repeat until the board is zero:

// Visit every set square without scanning empty ones
long bb = wKnights;
while (bb != 0L) {
    int square = Long.numberOfTrailingZeros(bb);   // lowest set bit's index
    // ... do something with 'square' ...
    bb &= bb - 1;   // clears the lowest set bit
}

That bb &= bb - 1 line is a classic bit trick that removes the lowest set bit in one step. Paired with numberOfTrailingZeros, it means you only ever touch squares that actually have pieces. No wasted iterations on empty squares.

And when you just need a count — "how many pawns do I have?" for material evaluation — there's Long.bitCount, which the JVM compiles down to a single CPU popcount instruction on most hardware:

int whitePawnCount = Long.bitCount(wPawns);   // one instruction, 64 squares

The first time I replaced a piece-counting loop with Long.bitCount and watched it do the same job in a single call, I actually laughed. That's the moment bitboards go from "confusing" to "kind of beautiful."

Move Generation With Bitboards

This is the payoff. Move generation is what bitboards were made for, and once the operators click, whole categories of moves collapse into one or two lines.

Pawn pushes. Every white pawn wants to move up one rank — that's a left shift by 8. But only if the square ahead is empty, so AND with empty. That single expression pushes all white pawns at once:

long singlePush = (wPawns << 8) & empty;

// Double push: from rank 2, into an empty square, only if the single push
// also landed on an empty square.
long rank3 = 0x0000000000FF0000L;
long doublePush = ((singlePush & rank3) << 8) & empty;

Pawn captures. A white pawn captures diagonally — up-left (shift 7) or up-right (shift 9) — landing on an enemy piece. AND with blackPieces:

long capturesLeft  = (wPawns << 7) & blackPieces & NOT_H_FILE;
long capturesRight = (wPawns << 9) & blackPieces & NOT_A_FILE;

Those NOT_H_FILE and NOT_A_FILE masks are edge guards — skip them and pawns "capture" by wrapping around the board, which is the single most common bitboard bug and gets its own war story below.

Knight and king moves don't slide, so they're the easiest of all: for any square, the set of squares a knight can reach is fixed. You precompute it once (again, more soon) and then a knight's moves are just its lookup, ANDed with "not my own pieces" so you don't capture yourself:

long knightMoves = KNIGHT_ATTACKS[square] & ~whitePieces;
long kingMoves   = KING_ATTACKS[square]   & ~whitePieces;

Sliding pieces — rooks, bishops, queens — are the hard part, and I won't pretend otherwise. A rook's moves depend on what's blocking it, so you can't just look them up by square alone; you have to account for occupancy. The conceptual approach is directional: from the rook's square, walk a ray in each direction, stopping when you hit a piece. You can express each direction as a repeated shift-and-mask, or — the fast way real engines use — with precomputed tables indexed by the blocker layout. A rook is horizontal plus vertical rays; a bishop is the diagonals; a queen is simply the union of the two:

long rookMoves   = rookAttacks(square, occupied)   & ~whitePieces;
long bishopMoves = bishopAttacks(square, occupied) & ~whitePieces;
long queenMoves  = rookMoves | bishopMoves;   // queen = rook + bishop

That last line is one of those small joys. A queen is literally a rook OR-ed with a bishop, and the code says exactly that. The directional shifts — up, down, left, right, and the four diagonals — plus edge masking are the whole vocabulary of sliding-piece movement. Get those right and everything else composes out of them.

Debugging Tip. Write a printBitboard(long) helper before you write a single move generator. Seeing the 8×8 grid of 1s and 0s turns invisible bit bugs into things you can spot in two seconds. I wrote mine on day one of the rewrite and used it more than any debugger.

The First Bugs I Hit

This is the section I'd have paid for. Bitboard bugs are a special flavor — the code compiles, runs fast, and produces subtly insane chess. Here are the ones that actually got me.

Bug 1: Pieces wrapping around the edge of the board

My proudest early moment was a one-line pawn capture generator. My least proud moment was watching a pawn on the a-file "capture" a piece on the h-file of the rank below. It had teleported across the entire board.

The cause is obvious in hindsight. When you shift a bitboard left or right by anything that isn't a multiple of 8, bits crossing the edge don't vanish — they slide into the next rank. A pawn on a1 (bit 0) shifted by 7 lands on bit 7, which is h1. The bit didn't fall off the world; there is no edge in a 64-bit integer. I diagnosed it by printing the before-and-after bitboards and literally seeing the stray bit appear on the wrong side. The fix is edge masks: before shifting toward a file, AND away the pieces already on that file so they can't wrap.

static final long FILE_A = 0x0101010101010101L;
static final long FILE_H = 0x8080808080808080L;
static final long NOT_A_FILE = ~FILE_A;
static final long NOT_H_FILE = ~FILE_H;

// Correct: strip the h-file BEFORE a left-by-7 that moves toward the a side
long capturesLeft = ((wPawns & NOT_A_FILE) << 7) & blackPieces;

Bug 2: The 1 vs 1L overflow

I wrote 1 << square for a square of 40 and got nonsense. In Java, 1 is a 32-bit int, and shifting an int by 40 doesn't overflow into a long — the shift amount silently wraps modulo 32, so 1 << 40 is really 1 << 8. My pieces on the top half of the board were landing on completely wrong squares. The fix is one character: 1L. It's such a small mistake and such a maddening bug that I now type 1L reflexively.

Bug 3: >> instead of >>>

Java has no unsigned long. When a bitboard had a piece on a high square, its top bit was set — which, interpreted as a signed number, is negative. Using the arithmetic shift >> then smeared the sign bit downward, lighting up a whole column of squares that had no business being set. Black pawn pushes were spawning phantom pawns. Switching every board shift to the logical >>> fixed it instantly. Rule I now live by: bitboards shift with >>>, never >>.

Bug 4: Wrong square indexing (the off-by-one that isn't)

I mixed up my mapping. In one place I computed rank * 8 + file; in another, half-asleep, I wrote file * 8 + rank. Both are "valid" indices, so nothing crashed. The board was just transposed — everything mirrored along the diagonal. Knights moved like knights, but on the wrong squares. The lesson: pick one square-to-index mapping, write it down, put it in a comment, and route every conversion through a single helper so you can't disagree with yourself.

Bug 5: Little-endian vs big-endian confusion

Related, and it nearly broke me: I copied an attack-table idea from one resource that used A1 = bit 0 and pasted a constant from another that used A8 = bit 0. My board printed upside down and my pawns moved the wrong way — white pawns marching toward their own back rank. Nothing was "wrong" in isolation; two correct systems just disagreed. Once I committed fully to Little-Endian Rank-File everywhere and re-derived my own constants, the ghosts left.

Bug 6: Rooks sliding through pieces

My first sliding generator ignored occupancy and returned the rook's entire rank and file as legal, blockers or not. The rook happily jumped over its own pawns and through enemy pieces. Sliding moves must stop at the first occupied square — that's the entire difficulty of sliding pieces, and it's why blocker-aware attack generation (and eventually magic bitboards) exists. My stopgap was an honest ray walk: step one square at a time, add it to the move set, and break the moment I hit any occupied square. I ended up writing that whole approach up separately in Generating Sliding Piece Moves Without Magic Bitboards, because it turned out to be the thing that finally made the fast techniques make sense.

Bug 7: Bishop attacks that skipped a diagonal

A cousin of the wrap bug. My bishop rays used shifts of 7 and 9 for the diagonals but I forgot the file mask on one direction, so a bishop near the edge leaked onto the wrong diagonal on the far side. It looked almost right, which made it worse — it took a specific position and a printed bitboard to catch the one stray bit.

Common Mistakes — Bitboards

  • Forgetting edge masks, so pieces wrap around the board
  • Using 1 << n instead of 1L << n for squares above 31
  • Using >> (arithmetic) where you need >>> (logical)
  • Two different square-to-index mappings in the same codebase
  • Mixing little-endian and big-endian constants from different sources
  • Sliding pieces that ignore blockers and jump through them

Almost every one of these bugs I found the same way: print the bitboard as a grid and stare at it. When you can see the board, a bit in the wrong place is obvious. When you can only see a hex number, it's a needle in a haystack.

Precomputed Attack Tables

Here's a realization that reshaped how I thought about the whole engine: for the pieces that don't slide, the moves never change. A knight on d4 attacks the same eight squares in every game that has ever been played. So why compute them at runtime at all?

You don't. You compute them once, at startup, and store them in an array indexed by square. This is a precomputed attack table, and it's the reason knight and king move generation is nearly free.

  • Knight attacks. An array of 64 longs. KNIGHT_ATTACKS[27] is a bitboard of every square a knight on d4 hits. You build all 64 in a loop at boot, carefully masking edges so knights near the a- and h-files don't wrap.
  • King attacks. Same idea — 64 longs, the (up to) eight neighbors of each square.
  • Pawn attacks. Two tables (one per color, since pawns are directional), each 64 longs, giving the diagonal capture squares from any square.
  • Occupancy masks. For sliding pieces, precomputed masks of the "relevant" squares whose occupancy actually affects a rook's or bishop's reach along its rays.
// Built once at startup; used millions of times during search
static final long[] KNIGHT_ATTACKS = new long[64];

// Later, in the hot path — no computation, just a lookup:
long moves = KNIGHT_ATTACKS[square] & ~ownPieces;

Sliding pieces are harder because their attacks depend on blockers, so a plain "one table per square" doesn't capture it. The technique serious engines use is magic bitboards — and I'll be honest that this is where I'll stay deliberately shallow, because magic bitboards deserve their own article and I don't want to hand-wave something I'd only half-explain. The one-paragraph intuition: you take the occupancy bits along a rook's or bishop's rays, multiply by a carefully chosen "magic" constant that scatters those bits into a small unique index, and use that index into a precomputed table of attack sets. It feels like dark arts the first time you see it. What matters for now is the principle underneath all of it:

Anything you can compute ahead of time, you should. Trading a little startup work and memory for a lookup in the hottest loop of your engine is one of the best deals in chess programming.

Lookup tables are why bitboard engines search so deep. The expensive thinking happens once, before the game starts.

Performance Comparison

I want to be careful and honest here, because the temptation is to invent impressive numbers. I didn't run a rigorous, publishable benchmark harness comparing my two implementations under controlled conditions, so I'm not going to quote you a made-up "10x faster." What I can give you is a qualitative comparison grounded in why each approach behaves the way it does, plus what I observed: the bitboard version searched noticeably deeper in the same amount of time, and the difference grew as depth increased.

Dimension Array board Bitboard
Move generationLoop over squares, branch per squareWhole-board bitwise ops, few branches
Memory footprintLarger; objects scattered on the heapTiny; ~12 longs, fits in registers/cache
Iterating piecesScan all 64 squaresJump to set bits via trailing-zeros
Piece / occupancy lookupDirect index — very readableAND + zero test — very fast
Material countingLoop and tallySingle Long.bitCount per board
Evaluation (mobility, etc.)Manual per-square logicSet operations across boards
Readability / debuggingExcellent — you can see itHarder — needs helper printers
Implementation effortLowHigher, especially sliding pieces

The pattern is consistent: bitboards win decisively on the operations an engine does millions of times per second, and arrays win on how easy the code is to write and understand. That's not a tie — it's a trade, and which side wins depends entirely on what you're building.

When Bitboards Are NOT the Right Choice

I switched to bitboards and I'm glad I did. That does not mean you should reach for them on day one, and I'd be selling you something dishonest if I said otherwise.

Arrays are the better choice when:

  • You're learning to program. If chess is how you're learning arrays, loops, and objects, a bitboard is a wall between you and the concepts you're actually trying to grasp. Start with the grid.
  • It's a simple game or a school assignment. If the goal is "a working two-player chess game," an array board is faster to write, easier to grade, and impossible to over-engineer.
  • It's your first chess project. Get the rules right first — castling, en passant, promotion, check detection. Those are hard enough without also fighting bit-level bugs. You'll understand bitboards far better after you've felt the array version get slow.
  • Speed genuinely doesn't matter. If you're searching two plies deep for a casual bot, the array is fast enough and you'll never notice the difference.

This is really the premature-optimization lesson wearing a chess hat. Optimizing something you don't yet understand, before you've measured that it's slow, usually costs you more in confusion and bugs than it ever returns in speed. I got value from bitboards precisely because I hit a real, felt performance ceiling first. The pain told me the optimization was worth it. Without that pain, I'd have just been copying magic constants I didn't understand into a program that didn't need them.

Key Takeaway. Bitboards are an optimization, not a starting point. Build the readable array version first, feel it slow down, and then reach for 64 bits. The struggle you skip is the understanding you skip.

Lessons I Learned

Bitboards changed more than my engine's speed. They changed how I think about data.

  • I started seeing binary visually. Before this, a 64-bit number was an abstraction. Now I see a board. 0xFF00 isn't 65280 to me anymore — it's the second rank. Learning to picture bits as a grid was the single most useful mental habit I gained.
  • I learned to think in sets, not loops. "For each square, check a condition" became "intersect these two boards." That set-oriented framing shows up everywhere in programming now, well outside of chess.
  • Reading other engines stopped being terrifying. The open-source code and the classic chess programming resources that once looked like noise became readable. When I open a strong engine's source now, the shifts and masks tell a story I can follow — I'm reading intent, not just symbols.
  • I started appreciating CPU-friendly code. I don't write assembly, but I now have a gut sense for why branches are costly, why data locality matters, and why a single instruction over 64 bits beats a loop. That intuition makes me a better developer even when I'm nowhere near a chess board.
  • Low-level optimization became approachable. Bitboards were my on-ramp to the idea that you can meet the machine where it lives. It's not a dark art reserved for wizards. It's just a different, and sometimes better, way to model a problem.

I want to be clear about what's mine and what isn't. I didn't invent any of this. Bitboards have been part of chess programming for decades — they go back to the earliest Soviet engines of the 1960s and 70s. Everything here is me learning a well-established technique and reporting what the learning actually felt like. The concepts are the field's; the mistakes and the war stories are mine.

If I Were Starting My Chess Engine Again

Hindsight is cheap, but it's also the most useful thing I can hand you. Here's what I'd do differently, knowing what I know now.

  1. Design around bitboards from day one — but only after a throwaway array prototype. Those aren't contradictory. I'd still build a scrappy array version to nail the rules, then architect the "real" engine around bitboards from the start instead of retrofitting them into an array-shaped design. Retrofitting was harder than rewriting.
  2. Commit to one square indexing, in writing. Little-Endian Rank-File, A1 = 0, documented at the top of the file, with every conversion going through a single helper. Half my early bugs were me disagreeing with myself about which bit was which square.
  3. Write the helper functions before the logic. setBit, clearBit, isSet, printBitboard, square-name conversions. Boring, tiny, and they make everything after them trustworthy.
  4. Build visual debugging tools first. That printBitboard grid earned its keep a hundred times over. I'd write it before writing a single move generator, not after the third baffling bug.
  5. Write unit tests around move generation early. Set up a known position, count the moves, compare to a known-correct number. Bit bugs are invisible to the naked eye but obvious to a test that says "expected 20, got 23."
  6. Avoid premature abstraction. Early on I wrapped bitboards in clever classes and generic helpers before I understood the patterns. It obscured the very bit operations I was trying to learn. Keep it flat and explicit until the real abstractions reveal themselves.

Frequently Asked Questions

What is a bitboard in a chess engine?

A bitboard is a 64-bit integer where each bit represents one square on a chess board. Because a chess board has exactly 64 squares and a 64-bit integer has exactly 64 bits, you can map every square to a single bit. A bit set to 1 means something is true for that square — for example, that a white pawn stands there — and a 0 means it doesn't. A full position uses several bitboards, typically one per piece type and colour, combined with fast bitwise operations to generate and test moves.

Why do chess engines use bitboards instead of arrays?

Arrays are readable and perfectly fine for beginners, but they force you to loop over squares and branch constantly during move generation. Bitboards let a single bitwise operation act on all 64 squares at once, so many operations that would be loops become one CPU instruction. That reduces branching, improves cache locality because the whole board fits in a handful of registers, and makes deep search dramatically faster. That speed is why serious engines almost always adopt bitboards eventually.

How do you represent a chess board in 64 bits in Java?

You use the primitive long, a signed 64-bit integer. Assign each square an index from 0 to 63 — commonly A1 = 0 up to H8 = 63. To place a piece, set the matching bit with bb | (1L << square). To remove one, clear the bit with bb & ~(1L << square). To test a square, AND with the mask and check for a non-zero result. A full board uses several longs, one per piece type and colour, from which you derive occupancy and empty-square boards with OR and NOT.

Are bitboards always the best choice for a chess program?

No. Bitboards are the right tool when raw search speed matters, such as a competitive engine searching millions of positions. For a school assignment, a simple two-player game, a first chess project, or anywhere clarity and learning matter more than speed, a plain array board is easier to write, read and debug. Reaching for bitboards too early is a form of premature optimisation that can slow your learning more than it speeds your code.

What is the hardest part of learning bitboards?

For most people it's edge wrapping and square indexing. When you shift a bitboard to model a piece moving sideways, bits can fall off one edge and reappear on the opposite side, producing impossible moves — you fix this by masking out files before shifting. The other common struggle is keeping a consistent mapping between square names like A1 and bit indices, and remembering that Java has no unsigned long, so you must use logical shifts (>>>) and the Long helper methods carefully.

Conclusion

Here's the thing I keep coming back to. Bitboards look scary for exactly one reason: they're unfamiliar. That's it. There's nothing conceptually deep about "each bit is a square" — a child could understand the mapping. What makes them intimidating is the wall of shift operators and hex constants you meet before anyone explains the simple idea underneath. I bounced off that wall three times. You might too. It's not you.

But once it clicks — once you can look at 0xFF00 and see the second rank, once a pawn push becomes a shift and a queen becomes a rook OR a bishop — something shifts in how you read code. The intimidating thing becomes an elegant thing. Move generation stops being a chore and starts being a little bit fun. I mean that literally: writing a whole pawn advance as one line that operates on all sixteen pawns at once is a genuinely satisfying thing to have built.

So my one piece of advice, if you take nothing else: don't memorize the bit tricks. Don't collect magic constants like trading cards. Understand what each bit represents — a square, a yes or a no — and every trick becomes something you can re-derive instead of recall. The shifts and masks are just consequences of the mapping. Learn the mapping cold and the rest follows.

The most intimidating programming concepts have a habit of becoming the most enjoyable ones, but only on the far side of actually understanding them. Bitboards were that for me. They went from a wall I kept hitting to the part of my engine I'm proudest of, and they quietly rewired how I think about representing data in general. Build the array version first, let it get slow, and then — when the pain is real and the reward is earned — pick up your 64 bits. I think you'll enjoy the other side of that wall as much as I did.