Architecture

Overview

Kirin uses a bitboard-based architecture optimized for modern 64-bit processors. The engine is structured around several core components: board representation, move generation, position evaluation, and tree search.

Board Representation

Bitboard Data Structures

The board is represented using 12 bitboards (one per piece type and color) plus 3 occupancy bitboards:

// Define Bitboard Type
#define U64 unsigned long long

// Piece bitboards: P, N, B, R, Q, K, p, n, b, r, q, k
U64 bitboards[12]; 

// Occupancy bitboards: white, black, both
U64 occupancy[3];

// Game state
int side;              // Side to move (white=0, black=1)
int enpassant;         // En passant target square
int castle;            // Castling rights (4 bits)
U64 hashKey;           // Zobrist hash of position

Square Encoding

Squares are encoded as integers 0-63, with a8=0 and h1=63:

enum {
  a8, b8, c8, d8, e8, f8, g8, h8,
  a7, b7, c7, d7, e7, f7, g7, h7,
  a6, b6, c6, d6, e6, f6, g6, h6,
  a5, b5, c5, d5, e5, f5, g5, h5,
  a4, b4, c4, d4, e4, f4, g4, h4,
  a3, b3, c3, d3, e3, f3, g3, h3,
  a2, b2, c2, d2, e2, f2, g2, h2,
  a1, b1, c1, d1, e1, f1, g1, h1, no_sq
};

Piece Encoding

Pieces are encoded as integers 0-11:

enum { P, N, B, R, Q, K, p, n, b, r, q, k };

Castling Rights

Castling rights are stored as a 4-bit value:

// Binary encoding:
// 0001 (1) = white king-side
// 0010 (2) = white queen-side
// 0100 (4) = black king-side
// 1000 (8) = black queen-side
enum { wk = 1, wq = 2, bk = 4, bq = 8 };

Bit Manipulation Macros

#define setBit(bitboard, square) ((bitboard) |= (1ULL << (square)))
#define getBit(bitboard, square) ((bitboard) & (1ULL << (square)))
#define popBit(bitboard, square) ((bitboard) &= ~(1ULL << (square)))

Chess Position Example

Here is an example starting position:

After 1.e4 e5 2.Nf3:

Move Generation

Move Encoding

Moves are encoded as 24-bit integers containing all move information:

// Binary move representation:
// 0000 0000 0000 0000 0011 1111  source square      0x3f
// 0000 0000 0000 1111 1100 0000  target square      0xfc0
// 0000 0000 1111 0000 0000 0000  piece              0xf000
// 0000 1111 0000 0000 0000 0000  promoted piece     0xf0000
// 0001 0000 0000 0000 0000 0000  capture flag       0x100000
// 0010 0000 0000 0000 0000 0000  double pawn push   0x200000
// 0100 0000 0000 0000 0000 0000  en passant flag    0x400000
// 1000 0000 0000 0000 0000 0000  castling flag      0x800000

#define encodeMove(source, target, piece, promoted, capture, dpp, enpassant, castle) \
  (source) | (target << 6) | (piece << 12) | (promoted << 16) | \
  (capture << 20) | (dpp << 21) | (enpassant << 22) | (castle << 23)

Attack Tables

Leaper pieces (pawns, knights, kings) use pre-computed attack tables:

U64 pawn_attacks[2][64];   // [side][square]
U64 knightAttacks[64];
U64 kingAttacks[64];

Magic Bitboards

Sliding pieces (bishops, rooks, queens) use magic bitboard technique for O(1) attack generation:

U64 bishopMasks[64];
U64 rookMasks[64];
U64 bishopAttacks[64][512];   // 32KB
U64 rookAttacks[64][4096];    // 256KB
U64 bishopMagicNums[64];
U64 rookMagicNums[64];

static inline U64 getBishopAttacks(int square, U64 occupancy) { 
  occupancy &= bishopMasks[square];
  occupancy *= bishopMagicNums[square];
  occupancy >>= 64 - bishopRelevantBits[square];
  return bishopAttacks[square][occupancy];
}