PSQT (Piece-Square Tables)

1. What is PSQT?

It is a traditional evaluation technique where every piece gets:

  • a base material value (pawn = 100, queen = 900…)
  • plus a square bonus/penalty depending on where it stands

Example intuition:

  • Knights are better in the center → bonus on d4/e4
  • Pawns advanced are better → bonus on 6th/7th rank
  • King is safer in corner early → penalty for being central in middlegame

So evaluation includes:

Score = Material + Positional(square bonuses)

2. Piece values in Stockfish

Stockfish separates piece values into two phases:

Value PieceValue[PHASE_NB][PIECE_NB] = {
  { VALUE_ZERO, PawnValueMg, KnightValueMg, BishopValueMg, RookValueMg, QueenValueMg },
  { VALUE_ZERO, PawnValueEg, KnightValueEg, BishopValueEg, RookValueEg, QueenValueEg }
};
enum Value : int {
  VALUE_ZERO      = 0,
  VALUE_DRAW      = 0,
  VALUE_KNOWN_WIN = 10000,
  VALUE_MATE      = 32000,
  VALUE_INFINITE  = 32001,
  VALUE_NONE      = 32002,

  VALUE_MATE_IN_MAX_PLY  =  VALUE_MATE - 2 * MAX_PLY,
  VALUE_MATED_IN_MAX_PLY = -VALUE_MATE + 2 * MAX_PLY,

  PawnValueMg   = 188,   PawnValueEg   = 248,
  KnightValueMg = 753,   KnightValueEg = 832,
  BishopValueMg = 826,   BishopValueEg = 897,
  RookValueMg   = 1285,  RookValueEg   = 1371,
  QueenValueMg  = 2513,  QueenValueEg  = 2650,

  MidgameLimit  = 15258, EndgameLimit  = 3915
};

Why two values?

Because piece importance changes:

  • Knights are strong early (MG)
  • Pawns and king activity matter more late (EG)

So Stockfish stores:

  • PawnValueMg, PawnValueEg
  • KnightValueMg, KnightValueEg
  • etc.

3. Score type: Middlegame + Endgame packed together

Stockfish does not store evaluation as a single number.

Instead it stores a pair:

Score = (middlegame_score, endgame_score)

Macro

#define S(mg, eg) make_score(mg, eg)

So,

S(-16, 7)

means:

  • -16 in middlegame
  • +7 in endgame

Stockfish later blends MG/EG depending on game phase.

4. The Bonus table: Positional square bonuses

const Score Bonus[][RANK_NB][FILE_NB/2]

This stores:

  • For each piece type
  • For each rank
  • For half the board files (A–D)

Why only half files?

Chessboard is symmetric:

  • A-file mirrors H-file
  • B mirrors G, etc.

So Stockfish saves space:

File f = min(file, FILE_H - file);

Meaning:

  • file A and H use same bonus
  • file B and G same
  • etc.
// Bonus[PieceType][Square / 2] contains Piece-Square scores. For each piece
// type on a given square a (middlegame, endgame) score pair is assigned. Table
// is defined for files A..D and white side: it is symmetric for black side and
// second half of the files.
const Score Bonus[][RANK_NB][int(FILE_NB) / 2] = {
  { },
  { // Pawn
   { S(  0, 0), S(  0, 0), S(  0, 0), S( 0, 0) },
   { S(-16, 7), S(  1,-4), S(  7, 8), S( 3,-2) },
   { S(-23,-4), S( -7,-5), S( 19, 5), S(24, 4) },
   { S(-22, 3), S(-14, 3), S( 20,-8), S(35,-3) },
   { S(-11, 8), S(  0, 9), S(  3, 7), S(21,-6) },
   { S(-11, 8), S(-13,-5), S( -6, 2), S(-2, 4) },
   { S( -9, 3), S( 15,-9), S( -8, 1), S(-4,18) }
  },
  { // Knight
   { S(-143, -97), S(-96,-82), S(-80,-46), S(-73,-14) },
   { S( -83, -69), S(-43,-55), S(-21,-17), S(-10,  9) },
   { S( -71, -50), S(-22,-39), S(  0, -8), S(  9, 28) },
   { S( -25, -41), S( 18,-25), S( 43,  7), S( 47, 38) },
   { S( -26, -46), S( 16,-25), S( 38,  2), S( 50, 41) },
   { S( -11, -55), S( 37,-38), S( 56, -8), S( 71, 27) },
   { S( -62, -64), S(-17,-50), S(  5,-24), S( 14, 13) },
   { S(-195,-110), S(-66,-90), S(-42,-50), S(-29,-13) }
  },
  • White pawns have 0 score on 1 st rank because they can’t be present there
  • Knights at corners are heavily penallized, while knights at center have the highest scores

5. psq[][]: Final precomputed lookup table

Score psq[PIECE_NB][SQUARE_NB];

// init() initializes piece-square tables: the white halves of the tables are
// copied from Bonus[] adding the piece value, then the black halves of the
// tables are initialized by flipping and changing the sign of the white scores.
void init() {

  for (Piece pc = W_PAWN; pc <= W_KING; ++pc)
  {
      PieceValue[MG][~pc] = PieceValue[MG][pc];
      PieceValue[EG][~pc] = PieceValue[EG][pc];

      Score v = make_score(PieceValue[MG][pc], PieceValue[EG][pc]);

      for (Square s = SQ_A1; s <= SQ_H8; ++s)
      {
          File f = std::min(file_of(s), FILE_H - file_of(s));
          psq[ pc][ s] = v + Bonus[pc][rank_of(s)][f];
          psq[~pc][~s] = -psq[pc][s];
      }
  }
}

This is the final table used during evaluation:

  • For every piece (white and black)
  • For every square (64 squares)

So later evaluation becomes extremely fast:

score += PSQT::psq[piece][square];

No computation needed at runtime.

  • ~pc means “same piece, opposite color”
  • ~s means “same square, flipped vertically”

It works because ~ has a custom overload

inline Piece operator~(Piece pc) {
  return Piece(pc ^ 8); // Swap color of piece B_KNIGHT -> W_KNIGHT
}
inline Square operator~(Square s) {
  return Square(s ^ SQ_A8); // Vertical flip SQ_A1 -> SQ_A8
}