github-actions[bot]
deploy prod from 06dbd16a01ddcfe02b2d936c681e3e4eaa9b141f
8e756fd
Raw
History Blame Contribute Delete
16.5 kB
"""Phase-independent raw position features (per side)."""
from __future__ import annotations
from dataclasses import dataclass
from . import attacks
from . import bitboards as bb
from . import constants as C
from . import masks
from .board import Board
from .evaluate import EvalInfo, init_eval_info
from .types import (
BISHOP,
BLACK,
KING,
KNIGHT,
PAWN,
QUEEN,
ROOK,
WHITE,
score_mg,
)
@dataclass
class SideRawFeatures:
"""Raw counts and indices before 0–100 normalization."""
closedness_index: int = 0
open_files: int = 0
rammed_pawns: int = 0
pawn_count: int = 0
isolated_pawns: int = 0
backward_pawns: int = 0
stacked_pawns: int = 0
connected_pawns: int = 0
candidate_passers: int = 0
passed_pawns: int = 0
passed_pressure: int = 0
king_attacks: int = 0
king_attackers: int = 0
king_weak_squares: int = 0
king_defenders: int = 0
shelter_mg: int = 0
knight_mobility: int = 0
bishop_mobility: int = 0
rook_mobility: int = 0
queen_mobility: int = 0
outposts: int = 0
center_control: int = 0
space_restricted: int = 0
threat_units: int = 0
pawn_push_threats: int = 0
complexity_units: int = 0
flank_pawns: int = 0
material_index: int = 0
@dataclass
class RawPositionFeatures:
white: SideRawFeatures
black: SideRawFeatures
phase_index: int = 0
# Human piece-count weights (pawn=1 … queen=9); king omitted.
_PIECE_WEIGHT = (1, 3, 3, 5, 9, 0)
_STARTING_MATERIAL_INDEX = (
8 * _PIECE_WEIGHT[PAWN]
+ 2 * _PIECE_WEIGHT[KNIGHT]
+ 2 * _PIECE_WEIGHT[BISHOP]
+ 2 * _PIECE_WEIGHT[ROOK]
+ _PIECE_WEIGHT[QUEEN]
)
def _files_without_pawn(pawns_colour: int) -> int:
n = 0
for f in range(8):
if not (pawns_colour & bb.FILES[f]):
n += 1
return n
def _closedness_index(pawn_count: int, rammed: int, open_files: int) -> int:
n = pawn_count + 3 * rammed - 4 * open_files
return max(0, min(8, n // 3))
def _probe_pawns(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us, them = colour, 1 - colour
forward = 8 if colour == WHITE else -8
my_pawns = board.pieces[PAWN] & board.colours[us]
enemy_pawns = board.pieces[PAWN] & board.colours[them]
raw.pawn_count = bb.popcount(my_pawns)
raw.rammed_pawns = bb.popcount(ei.rammed_pawns[us])
raw.open_files = _files_without_pawn(my_pawns)
raw.closedness_index = _closedness_index(raw.pawn_count, raw.rammed_pawns, raw.open_files)
ei.attacked_by2[us] = ei.pawn_attacks[us] & ei.attacked[us]
ei.attacked[us] |= ei.pawn_attacks[us]
ei.attacked_by[us][PAWN] = ei.pawn_attacks[us]
atk = ei.pawn_attacks[us] & ei.king_areas[them]
ei.king_attacks_count[them] += bb.popcount(atk)
temp = my_pawns
while temp:
sq, temp = bb.poplsb(temp)
f = bb.file_of(sq)
neighbors = my_pawns & masks.adjacent_files_masks(f)
backup = my_pawns & masks.passed_pawn_masks(them, sq)
stoppers = enemy_pawns & masks.passed_pawn_masks(us, sq)
threats = enemy_pawns & attacks.pawn_attacks(us, sq)
support = my_pawns & attacks.pawn_attacks(them, sq)
push_threats = enemy_pawns & attacks.pawn_attacks(us, sq + forward)
push_support = my_pawns & attacks.pawn_attacks(them, sq + forward)
leftovers = stoppers ^ threats ^ push_threats
if not stoppers:
ei.passed_pawns |= 1 << sq
elif not leftovers and bb.popcount(push_support) >= bb.popcount(push_threats):
raw.candidate_passers += 1
flag = 1 if bb.popcount(support) >= bb.popcount(threats) else 0
ei.pkeval[us] += C.PawnCandidatePasser[flag][bb.relative_rank_of(us, sq)]
if not threats and not neighbors:
raw.isolated_pawns += 1
ei.pkeval[us] += C.PawnIsolated[f]
if bb.several(bb.FILES[f] & my_pawns):
# ``int(bitboard)`` would be wrong; ``PawnStacked`` has rows 0 and 1 only.
flag = int(
(bool(stoppers) and (bool(threats) or bool(neighbors)))
or bool(stoppers & ~masks.forward_file_masks(us, sq))
)
if flag:
raw.stacked_pawns += 1
ei.pkeval[us] += C.PawnStacked[flag][f]
if neighbors and push_threats and not backup:
raw.backward_pawns += 1
flag = 1 if not (bb.FILES[f] & enemy_pawns) else 0
ei.pkeval[us] += C.PawnBackwards[flag][bb.relative_rank_of(us, sq)]
elif masks.pawn_connected_masks(us, sq) & my_pawns:
raw.connected_pawns += 1
ei.pkeval[us] += C.PawnConnected32[bb.relative_square32(us, sq)]
def _probe_kings_pawns(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us = colour
my_pawns = board.pieces[PAWN] & board.colours[us]
enemy_pawns = board.pieces[PAWN] & board.colours[1 - us]
king_sq = ei.king_square[us]
dist = masks.king_pawn_file_distance(board.pieces[PAWN], king_sq)
ei.pkeval[us] += C.KingPawnFileProximity[dist]
kf = bb.file_of(king_sq)
for file in range(max(0, kf - 1), min(8, kf + 2)):
ours = my_pawns & bb.FILES[file] & masks.forward_ranks_masks(us, bb.rank_of(king_sq))
our_dist = 7 if not ours else abs(bb.rank_of(king_sq) - bb.rank_of(bb.backmost(us, ours)))
theirs = (
enemy_pawns & bb.FILES[file] & masks.forward_ranks_masks(us, bb.rank_of(king_sq))
)
their_dist = 7 if not theirs else abs(
bb.rank_of(king_sq) - bb.rank_of(bb.backmost(us, theirs))
)
on_king_file = file == kf
ei.pkeval[us] += C.KingShelter[on_king_file][file][our_dist]
ei.pksafety[us] += C.SafetyShelter[on_king_file][our_dist]
blocked = our_dist != 7 and our_dist == their_dist - 1
ei.pkeval[us] += C.KingStorm[blocked][bb.MIRROR_FILE[file]][their_dist]
ei.pksafety[us] += C.SafetyStorm[blocked][their_dist]
raw.shelter_mg = score_mg(ei.pksafety[us])
def _probe_knights(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us, them = colour, 1 - colour
enemy_pawns = board.pieces[PAWN] & board.colours[them]
temp = board.pieces[KNIGHT] & board.colours[us]
ei.attacked_by[us][KNIGHT] = 0
while temp:
sq, temp = bb.poplsb(temp)
att = attacks.knight_attacks(sq)
ei.attacked_by2[us] |= att & ei.attacked[us]
ei.attacked[us] |= att
ei.attacked_by[us][KNIGHT] |= att
if bb.test_bit(masks.outpost_ranks_masks(us), sq) and not (
masks.outpost_square_masks(us, sq) & enemy_pawns
):
raw.outposts += 1
count = bb.popcount(ei.mobility_areas[us] & att)
raw.knight_mobility += count
att_k = att & ei.king_areas[them] & ~ei.pawn_attacks_by2[them]
if att_k:
ei.king_attacks_count[them] += bb.popcount(att_k)
ei.king_attackers_count[them] += 1
ei.king_attackers_weight[them] += C.SafetyKnightWeight
def _probe_bishops(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us, them = colour, 1 - colour
enemy_pawns = board.pieces[PAWN] & board.colours[them]
temp = board.pieces[BISHOP] & board.colours[us]
ei.attacked_by[us][BISHOP] = 0
while temp:
sq, temp = bb.poplsb(temp)
att = attacks.bishop_attacks(sq, ei.occupied_minus_bishops[us])
ei.attacked_by2[us] |= att & ei.attacked[us]
ei.attacked[us] |= att
ei.attacked_by[us][BISHOP] |= att
if bb.test_bit(masks.outpost_ranks_masks(us), sq) and not (
masks.outpost_square_masks(us, sq) & enemy_pawns
):
raw.outposts += 1
count = bb.popcount(ei.mobility_areas[us] & att)
raw.bishop_mobility += count
att_k = att & ei.king_areas[them] & ~ei.pawn_attacks_by2[them]
if att_k:
ei.king_attacks_count[them] += bb.popcount(att_k)
ei.king_attackers_count[them] += 1
ei.king_attackers_weight[them] += C.SafetyBishopWeight
def _probe_rooks(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us, them = colour, 1 - colour
temp = board.pieces[ROOK] & board.colours[us]
ei.attacked_by[us][ROOK] = 0
while temp:
sq, temp = bb.poplsb(temp)
att = attacks.rook_attacks(sq, ei.occupied_minus_rooks[us])
ei.attacked_by2[us] |= att & ei.attacked[us]
ei.attacked[us] |= att
ei.attacked_by[us][ROOK] |= att
count = bb.popcount(ei.mobility_areas[us] & att)
raw.rook_mobility += count
att_k = att & ei.king_areas[them] & ~ei.pawn_attacks_by2[them]
if att_k:
ei.king_attacks_count[them] += bb.popcount(att_k)
ei.king_attackers_count[them] += 1
ei.king_attackers_weight[them] += C.SafetyRookWeight
def _probe_queens(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us, them = colour, 1 - colour
temp = board.pieces[QUEEN] & board.colours[us]
occupied = board.colours[WHITE] | board.colours[BLACK]
ei.attacked_by[us][QUEEN] = 0
while temp:
sq, temp = bb.poplsb(temp)
att = attacks.queen_attacks(sq, occupied)
ei.attacked_by2[us] |= att & ei.attacked[us]
ei.attacked[us] |= att
ei.attacked_by[us][QUEEN] |= att
count = bb.popcount(ei.mobility_areas[us] & att)
raw.queen_mobility += count
att_k = att & ei.king_areas[them] & ~ei.pawn_attacks_by2[them]
if att_k:
ei.king_attacks_count[them] += bb.popcount(att_k)
ei.king_attackers_count[them] += 1
ei.king_attackers_weight[them] += C.SafetyQueenWeight
def _probe_king_defenders(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us = colour
defenders = (
(board.pieces[PAWN] & board.colours[us])
| (board.pieces[KNIGHT] & board.colours[us])
| (board.pieces[BISHOP] & board.colours[us])
)
raw.king_defenders = bb.popcount(defenders & ei.king_areas[us])
def _probe_king_safety(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us, them = colour, 1 - colour
raw.king_attacks = ei.king_attacks_count[us]
raw.king_attackers = ei.king_attackers_count[us]
weak = (
ei.attacked[them]
& ~ei.attacked_by2[us]
& (~ei.attacked[us] | ei.attacked_by[us][QUEEN] | ei.attacked_by[us][KING])
)
raw.king_weak_squares = bb.popcount(weak & ei.king_areas[us])
def _probe_passed(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us, them = colour, 1 - colour
my_passers = board.colours[us] & ei.passed_pawns
raw.passed_pawns = bb.popcount(my_passers)
occupied = board.colours[WHITE] | board.colours[BLACK]
temp = my_passers
pressure = 0
while temp:
sq, temp = bb.poplsb(temp)
rank = bb.relative_rank_of(us, sq)
bitboard = attacks.pawn_advance(1 << sq, 0, us)
can_advance = not (bitboard & occupied)
safe_advance = not (bitboard & ei.attacked[them])
pressure += rank + (2 if can_advance else 0) + (2 if safe_advance else 0)
if bb.several(masks.forward_file_masks(us, sq) & my_passers):
continue
pressure += masks.distance_between(sq, ei.king_square[them]) // 2
raw.passed_pressure = pressure
def _probe_threats(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us, them = colour, 1 - colour
rank3 = bb.RANK_3 if us == WHITE else bb.RANK_6
friendly = board.colours[us]
enemy = board.colours[them]
occupied = friendly | enemy
pawns = friendly & board.pieces[PAWN]
knights = friendly & board.pieces[KNIGHT]
bishops = friendly & board.pieces[BISHOP]
rooks = friendly & board.pieces[ROOK]
queens = friendly & board.pieces[QUEEN]
by_pawns = ei.attacked_by[them][PAWN]
by_minors = ei.attacked_by[them][KNIGHT] | ei.attacked_by[them][BISHOP]
by_majors = ei.attacked_by[them][ROOK] | ei.attacked_by[them][QUEEN]
poorly = (ei.attacked[them] & ~ei.attacked[us]) | (
ei.attacked_by2[them] & ~ei.attacked_by2[us] & ~ei.attacked_by[us][PAWN]
)
weak_minors = (knights | bishops) & poorly
overloaded = (
(knights | bishops | rooks | queens)
& ei.attacked[us]
& ~ei.attacked_by2[us]
& ei.attacked[them]
& ~ei.attacked_by2[them]
)
push = attacks.pawn_advance(pawns, occupied, us)
push |= attacks.pawn_advance(push & ~by_pawns & rank3, occupied, us)
push &= ~by_pawns & (ei.attacked[us] | ~ei.attacked[them])
push = attacks.pawn_attack_span(push, enemy & ~ei.attacked_by[us][PAWN], us)
units = 0
units += bb.popcount(pawns & ~by_pawns & poorly)
units += bb.popcount((knights | bishops) & by_pawns)
units += bb.popcount((knights | bishops) & by_minors)
units += bb.popcount(weak_minors & by_majors)
units += bb.popcount(rooks & (by_pawns | by_minors))
units += bb.popcount(weak_minors & ei.attacked_by[them][KING])
units += bb.popcount(rooks & poorly & ei.attacked_by[them][KING])
units += bb.popcount(queens & ei.attacked[them])
units += bb.popcount(overloaded)
raw.threat_units = units
raw.pawn_push_threats = bb.popcount(push)
def _probe_space(ei: EvalInfo, board: Board, colour: int, raw: SideRawFeatures) -> None:
us, them = colour, 1 - colour
friendly = board.colours[us]
enemy = board.colours[them]
uncontrolled = (
ei.attacked_by2[them]
& ei.attacked[us]
& ~ei.attacked_by2[us]
& ~ei.attacked_by[us][PAWN]
)
raw.space_restricted = bb.popcount(uncontrolled & (friendly | enemy)) + bb.popcount(
uncontrolled & ~friendly & ~enemy
)
minors_majors = board.pieces[KNIGHT] | board.pieces[BISHOP]
if (
bb.popcount(minors_majors)
+ 2 * bb.popcount(board.pieces[ROOK] | board.pieces[QUEEN])
> 12
):
center = ~ei.attacked[them] & (ei.attacked[us] | friendly) & bb.CENTER_BIG
raw.center_control = bb.popcount(center)
def _probe_complexity(board: Board, colour: int, raw: SideRawFeatures) -> None:
us = colour
my_pawns = board.pieces[PAWN] & board.colours[us]
raw.pawn_count = bb.popcount(my_pawns)
raw.flank_pawns = bb.popcount(my_pawns & (bb.LEFT_FLANK | bb.RIGHT_FLANK))
minors_majors = (
board.pieces[KNIGHT]
| board.pieces[BISHOP]
| board.pieces[ROOK]
| board.pieces[QUEEN]
)
side_pieces = minors_majors & board.colours[us]
raw.complexity_units = raw.pawn_count + raw.flank_pawns + bb.popcount(side_pieces)
def _side_material_index(board: Board, colour: int) -> int:
total = 0
for pt in range(6):
n = bb.popcount(board.pieces[pt] & board.colours[colour])
total += n * _PIECE_WEIGHT[pt]
return total
def extract_raw_features(board: Board) -> RawPositionFeatures:
"""Extract phase-independent raw features for both sides."""
ei = EvalInfo()
init_eval_info(board, ei)
white = SideRawFeatures()
black = SideRawFeatures()
white.material_index = _side_material_index(board, WHITE)
black.material_index = _side_material_index(board, BLACK)
for colour, raw in ((WHITE, white), (BLACK, black)):
_probe_pawns(ei, board, colour, raw)
_probe_kings_pawns(ei, board, colour, raw)
for colour, raw in ((WHITE, white), (BLACK, black)):
_probe_knights(ei, board, colour, raw)
_probe_bishops(ei, board, colour, raw)
_probe_rooks(ei, board, colour, raw)
_probe_queens(ei, board, colour, raw)
_probe_king_defenders(ei, board, colour, raw)
for colour, raw in ((WHITE, white), (BLACK, black)):
_probe_king_safety(ei, board, colour, raw)
_probe_passed(ei, board, colour, raw)
_probe_threats(ei, board, colour, raw)
_probe_space(ei, board, colour, raw)
_probe_complexity(board, colour, raw)
phase = (
4 * bb.popcount(board.pieces[QUEEN])
+ 2 * bb.popcount(board.pieces[ROOK])
+ bb.popcount(board.pieces[KNIGHT] | board.pieces[BISHOP])
)
return RawPositionFeatures(white=white, black=black, phase_index=phase)
__all__ = [
"SideRawFeatures",
"RawPositionFeatures",
"extract_raw_features",
"_STARTING_MATERIAL_INDEX",
]