Spaces:
Runtime error
Runtime error
| """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, | |
| ) | |
| 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 | |
| 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", | |
| ] | |