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
}