Files
zzg b7da55371a
ci / test (push) Canceled after 0s
initial commit
2026-09-25 21:57:09 -07:00

190 lines
5.2 KiB
Go

// Package engine provides position evaluation and search.
package engine
import "github.com/zg/chess/chess"
// Centipawn material values. A pawn is 100, so scores read naturally.
var pieceValue = map[chess.PieceType]int{
chess.Pawn: 100,
chess.Knight: 320,
chess.Bishop: 330,
chess.Rook: 500,
chess.Queen: 900,
chess.King: 0, // the king is never captured, so it has no material value
}
// Piece-square tables, from white's point of view, rank 1 first. They
// encode simple positional knowledge: knights belong in the centre, pawns
// want to advance, the king wants to stay tucked away in the middlegame.
var (
pawnTable = [64]int{
0, 0, 0, 0, 0, 0, 0, 0,
5, 10, 10, -20, -20, 10, 10, 5,
5, -5, -10, 0, 0, -10, -5, 5,
0, 0, 0, 20, 20, 0, 0, 0,
5, 5, 10, 25, 25, 10, 5, 5,
10, 10, 20, 30, 30, 20, 10, 10,
50, 50, 50, 50, 50, 50, 50, 50,
0, 0, 0, 0, 0, 0, 0, 0,
}
knightTable = [64]int{
-50, -40, -30, -30, -30, -30, -40, -50,
-40, -20, 0, 5, 5, 0, -20, -40,
-30, 5, 10, 15, 15, 10, 5, -30,
-30, 0, 15, 20, 20, 15, 0, -30,
-30, 5, 15, 20, 20, 15, 5, -30,
-30, 0, 10, 15, 15, 10, 0, -30,
-40, -20, 0, 0, 0, 0, -20, -40,
-50, -40, -30, -30, -30, -30, -40, -50,
}
bishopTable = [64]int{
-20, -10, -10, -10, -10, -10, -10, -20,
-10, 5, 0, 0, 0, 0, 5, -10,
-10, 10, 10, 10, 10, 10, 10, -10,
-10, 0, 10, 10, 10, 10, 0, -10,
-10, 5, 5, 10, 10, 5, 5, -10,
-10, 0, 5, 10, 10, 5, 0, -10,
-10, 0, 0, 0, 0, 0, 0, -10,
-20, -10, -10, -10, -10, -10, -10, -20,
}
rookTable = [64]int{
0, 0, 0, 5, 5, 0, 0, 0,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
5, 10, 10, 10, 10, 10, 10, 5,
0, 0, 0, 0, 0, 0, 0, 0,
}
queenTable = [64]int{
-20, -10, -10, -5, -5, -10, -10, -20,
-10, 0, 5, 0, 0, 0, 0, -10,
-10, 5, 5, 5, 5, 5, 0, -10,
0, 0, 5, 5, 5, 5, 0, -5,
-5, 0, 5, 5, 5, 5, 0, -5,
-10, 0, 5, 5, 5, 5, 0, -10,
-10, 0, 0, 0, 0, 0, 0, -10,
-20, -10, -10, -5, -5, -10, -10, -20,
}
kingMiddleTable = [64]int{
20, 30, 10, 0, 0, 10, 30, 20,
20, 20, 0, 0, 0, 0, 20, 20,
-10, -20, -20, -20, -20, -20, -20, -10,
-20, -30, -30, -40, -40, -30, -30, -20,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
}
kingEndTable = [64]int{
-50, -30, -30, -30, -30, -30, -30, -50,
-30, -30, 0, 0, 0, 0, -30, -30,
-30, -10, 20, 30, 30, 20, -10, -30,
-30, -10, 30, 40, 40, 30, -10, -30,
-30, -10, 30, 40, 40, 30, -10, -30,
-30, -10, 20, 30, 30, 20, -10, -30,
-30, -20, -10, 0, 0, -10, -20, -30,
-50, -40, -30, -20, -10, -30, -30, -50,
}
)
// MateScore is the magnitude used for a forced mate. It is far outside the
// range any material balance can reach, so a mate always outranks material.
const MateScore = 1_000_000
// Evaluate scores the position in centipawns from the point of view of the
// side to move. Positive means the side to move is better.
func Evaluate(b *chess.Board) int {
// Deciding the phase first lets the king's table switch from "hide" to
// "centralise", which is the single most important phase-dependent
// term in a simple evaluation.
endgame := isEndgame(b)
score := 0
for sq := chess.Square(0); sq < 64; sq++ {
p := b.Piece(sq)
if p.IsEmpty() {
continue
}
value := pieceValue[p.Type()] + positional(p, sq, endgame)
if p.Colour() == chess.White {
score += value
} else {
score -= value
}
}
// Scores are returned from the mover's point of view, so a negamax
// search can simply negate at each level.
if b.Side == chess.Black {
return -score
}
return score
}
// positional returns the piece-square bonus. Black's tables are white's
// mirrored vertically, so only one set of tables is needed.
func positional(p chess.Piece, sq chess.Square, endgame bool) int {
index := int(sq)
if p.Colour() == chess.Black {
index = mirror(index)
}
switch p.Type() {
case chess.Pawn:
return pawnTable[index]
case chess.Knight:
return knightTable[index]
case chess.Bishop:
return bishopTable[index]
case chess.Rook:
return rookTable[index]
case chess.Queen:
return queenTable[index]
case chess.King:
if endgame {
return kingEndTable[index]
}
return kingMiddleTable[index]
}
return 0
}
// mirror flips a square vertically: a1 becomes a8.
func mirror(sq int) int { return sq ^ 56 }
// isEndgame reports whether non-pawn material has fallen far enough that
// endgame king activity matters.
func isEndgame(b *chess.Board) bool {
material := 0
for sq := chess.Square(0); sq < 64; sq++ {
p := b.Piece(sq)
if p.IsEmpty() || p.Type() == chess.Pawn || p.Type() == chess.King {
continue
}
material += pieceValue[p.Type()]
}
// Roughly: both sides down to a queen, or a rook and a minor each.
return material <= 2*(pieceValue[chess.Queen]+pieceValue[chess.Bishop])
}
// Material returns the raw material balance in centipawns from white's
// point of view, ignoring position.
func Material(b *chess.Board) int {
score := 0
for sq := chess.Square(0); sq < 64; sq++ {
p := b.Piece(sq)
if p.IsEmpty() {
continue
}
if p.Colour() == chess.White {
score += pieceValue[p.Type()]
} else {
score -= pieceValue[p.Type()]
}
}
return score
}