190 lines
5.2 KiB
Go
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
|
|
}
|