"""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", ]