-
Notifications
You must be signed in to change notification settings - Fork 4
Add pawn structure, bishop pair, and rook file evaluation #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -157,6 +157,46 @@ | |
| EG_KING, | ||
| ) | ||
|
|
||
| ############ | ||
| # Evaluation bonuses/penalties beyond PST | ||
| ############ | ||
|
|
||
| # Bishop pair bonus (having both bishops) | ||
| MG_BISHOP_PAIR_BONUS = 30 | ||
| EG_BISHOP_PAIR_BONUS = 50 | ||
|
|
||
| # Rook on open file (no pawns on file) | ||
| MG_ROOK_OPEN_FILE = 20 | ||
| EG_ROOK_OPEN_FILE = 10 | ||
|
|
||
| # Rook on semi-open file (no friendly pawns on file) | ||
| MG_ROOK_SEMI_OPEN_FILE = 10 | ||
| EG_ROOK_SEMI_OPEN_FILE = 5 | ||
|
|
||
| # Isolated pawn penalty (no friendly pawns on adjacent files) | ||
| MG_ISOLATED_PAWN = -10 | ||
| EG_ISOLATED_PAWN = -20 | ||
|
|
||
| # Doubled pawn penalty (multiple friendly pawns on the same file) | ||
| MG_DOUBLED_PAWN = -10 | ||
| EG_DOUBLED_PAWN = -15 | ||
|
|
||
| # Passed pawn bonus by rank (rank 1 to 8, index 0 unused) | ||
| # Bonus increases as pawn advances; huge in endgame | ||
| # Index = rank for white (rank 2..7 are the meaningful ones; 1 and 8 don't exist for pawns) | ||
| MG_PASSED_PAWN_BONUS = (0, 0, 5, 10, 20, 40, 60, 0) | ||
| EG_PASSED_PAWN_BONUS = (0, 0, 10, 20, 40, 70, 120, 0) | ||
|
|
||
| # File masks: for each file (0-7), a set of square indices on that file | ||
| FILE_SQUARES: tuple[set[int], ...] = tuple( | ||
| {file + rank * 8 for rank in range(8)} for file in range(8) | ||
| ) | ||
|
|
||
| # Adjacent files for each file (0-7) | ||
| ADJACENT_FILES: tuple[tuple[int, ...], ...] = tuple( | ||
| tuple(f for f in (file - 1, file + 1) if 0 <= f <= 7) for file in range(8) | ||
| ) | ||
|
|
||
| ############ | ||
| # Tapered Evaluation: https://www.chessprogramming.org/Tapered_Eval | ||
| # Phase values are used to determine on what phase of the game | ||
|
|
@@ -263,16 +303,16 @@ def inner(board: chess.Board): | |
| @board_evaluation_cache | ||
| def board_evaluation(board: chess.Board) -> float: | ||
| """ | ||
| This functions receives a board and assigns a value to it, it acts as | ||
| an evaluation function of the current state for this game. It returns | ||
|
|
||
| Evaluates the board using PeSTO PST values plus structural bonuses: | ||
| - Pawn structure (passed, isolated, doubled pawns) | ||
| - Bishop pair bonus | ||
| - Rook on open/semi-open file | ||
|
|
||
| Arguments: | ||
| - board: current board state. | ||
|
|
||
| Returns: | ||
| - total_value(int): integer representing | ||
| current value for this board. | ||
| - total_value(int): integer representing current value for this board. | ||
| """ | ||
|
|
||
| phase = get_phase(board) | ||
|
|
@@ -282,15 +322,150 @@ def board_evaluation(board: chess.Board) -> float: | |
| eg_white = 0 | ||
| eg_black = 0 | ||
|
|
||
| # Track piece locations for structural evaluation | ||
| white_pawns: list[int] = [] | ||
| black_pawns: list[int] = [] | ||
| white_rooks: list[int] = [] | ||
| black_rooks: list[int] = [] | ||
| white_bishop_count = 0 | ||
| black_bishop_count = 0 | ||
|
|
||
| # iterate only occupied squares via piece_map() | ||
| for square, piece in board.piece_map().items(): | ||
| pt = piece.piece_type | ||
| if piece.color == chess.WHITE: | ||
| mg_white += MG_PESTO[pt][square ^ 56] + MG_PIECE_VALUES[pt] | ||
| eg_white += EG_PESTO[pt][square ^ 56] + EG_PIECE_VALUES[pt] | ||
| if pt == chess.PAWN: | ||
| white_pawns.append(square) | ||
| elif pt == chess.ROOK: | ||
| white_rooks.append(square) | ||
| elif pt == chess.BISHOP: | ||
| white_bishop_count += 1 | ||
| else: | ||
| mg_black += MG_PESTO[pt][square] + MG_PIECE_VALUES[pt] | ||
| eg_black += EG_PESTO[pt][square] + EG_PIECE_VALUES[pt] | ||
| if pt == chess.PAWN: | ||
| black_pawns.append(square) | ||
| elif pt == chess.ROOK: | ||
| black_rooks.append(square) | ||
| elif pt == chess.BISHOP: | ||
| black_bishop_count += 1 | ||
|
|
||
| # Pawn file sets for structural analysis | ||
| white_pawn_files = set() | ||
| black_pawn_files = set() | ||
| white_pawns_per_file: dict[int, int] = {} | ||
| black_pawns_per_file: dict[int, int] = {} | ||
|
|
||
| for sq in white_pawns: | ||
| f = sq % 8 | ||
| white_pawn_files.add(f) | ||
| white_pawns_per_file[f] = white_pawns_per_file.get(f, 0) + 1 | ||
|
|
||
| for sq in black_pawns: | ||
| f = sq % 8 | ||
| black_pawn_files.add(f) | ||
| black_pawns_per_file[f] = black_pawns_per_file.get(f, 0) + 1 | ||
|
|
||
| # --- Pawn structure --- | ||
| # White pawns | ||
| for sq in white_pawns: | ||
| f = sq % 8 | ||
| r = sq // 8 + 1 # rank 1-8 (rank 1 = row 0) | ||
|
|
||
| # Doubled pawn: more than one pawn on same file | ||
| if white_pawns_per_file[f] > 1: | ||
| mg_white += MG_DOUBLED_PAWN | ||
| eg_white += EG_DOUBLED_PAWN | ||
|
Comment on lines
+377
to
+380
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Doubled pawn penalty scales per-pawn If this is intentional, consider renaming the constants (e.g. Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||
|
|
||
| # Isolated pawn: no friendly pawns on adjacent files | ||
| has_adjacent = any(af in white_pawn_files for af in ADJACENT_FILES[f]) | ||
| if not has_adjacent: | ||
| mg_white += MG_ISOLATED_PAWN | ||
| eg_white += EG_ISOLATED_PAWN | ||
|
|
||
| # Passed pawn: no enemy pawns on same or adjacent files that can block/capture | ||
| is_passed = True | ||
| check_files = (f,) + ADJACENT_FILES[f] | ||
| for cf in check_files: | ||
| for bsq in black_pawns: | ||
| bf = bsq % 8 | ||
| br = bsq // 8 + 1 | ||
| if bf == cf and br > r: # black pawn ahead of white pawn | ||
| is_passed = False | ||
| break | ||
| if not is_passed: | ||
| break | ||
| if is_passed and 2 <= r <= 7: | ||
| mg_white += MG_PASSED_PAWN_BONUS[r] | ||
| eg_white += EG_PASSED_PAWN_BONUS[r] | ||
|
|
||
| # Black pawns | ||
| for sq in black_pawns: | ||
| f = sq % 8 | ||
| r = sq // 8 + 1 # rank 1-8 | ||
|
|
||
| # Doubled pawn | ||
| if black_pawns_per_file[f] > 1: | ||
| mg_black += MG_DOUBLED_PAWN | ||
| eg_black += EG_DOUBLED_PAWN | ||
|
|
||
| # Isolated pawn | ||
| has_adjacent = any(af in black_pawn_files for af in ADJACENT_FILES[f]) | ||
| if not has_adjacent: | ||
| mg_black += MG_ISOLATED_PAWN | ||
| eg_black += EG_ISOLATED_PAWN | ||
|
|
||
| # Passed pawn (for black, no white pawns ahead = lower rank number) | ||
| is_passed = True | ||
| check_files = (f,) + ADJACENT_FILES[f] | ||
| for cf in check_files: | ||
| for wsq in white_pawns: | ||
| wf = wsq % 8 | ||
| wr = wsq // 8 + 1 | ||
| if wf == cf and wr < r: # white pawn ahead of black pawn | ||
| is_passed = False | ||
| break | ||
| if not is_passed: | ||
| break | ||
| if is_passed and 2 <= r <= 7: | ||
| # For black, rank 7 is closest to promotion (like white rank 2) | ||
| # Mirror the rank: black rank 7 -> index 2, rank 2 -> index 7 | ||
| bonus_rank = 9 - r | ||
| mg_black += MG_PASSED_PAWN_BONUS[bonus_rank] | ||
| eg_black += EG_PASSED_PAWN_BONUS[bonus_rank] | ||
|
|
||
| # --- Bishop pair bonus --- | ||
| if white_bishop_count >= 2: | ||
| mg_white += MG_BISHOP_PAIR_BONUS | ||
| eg_white += EG_BISHOP_PAIR_BONUS | ||
| if black_bishop_count >= 2: | ||
| mg_black += MG_BISHOP_PAIR_BONUS | ||
| eg_black += EG_BISHOP_PAIR_BONUS | ||
|
|
||
| # --- Rook on open/semi-open file --- | ||
| for sq in white_rooks: | ||
| f = sq % 8 | ||
| if f not in white_pawn_files: | ||
| if f not in black_pawn_files: | ||
| # Open file: no pawns at all | ||
| mg_white += MG_ROOK_OPEN_FILE | ||
| eg_white += EG_ROOK_OPEN_FILE | ||
| else: | ||
| # Semi-open file: no friendly pawns | ||
| mg_white += MG_ROOK_SEMI_OPEN_FILE | ||
| eg_white += EG_ROOK_SEMI_OPEN_FILE | ||
|
|
||
| for sq in black_rooks: | ||
| f = sq % 8 | ||
| if f not in black_pawn_files: | ||
| if f not in white_pawn_files: | ||
| mg_black += MG_ROOK_OPEN_FILE | ||
| eg_black += EG_ROOK_OPEN_FILE | ||
| else: | ||
| mg_black += MG_ROOK_SEMI_OPEN_FILE | ||
| eg_black += EG_ROOK_SEMI_OPEN_FILE | ||
|
|
||
| # calculate board score based on phase | ||
| if board.turn == chess.WHITE: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unused
FILE_SQUARESconstantFILE_SQUARESis defined here but never referenced anywhere in the codebase. This appears to be dead code left over from development. Consider removing it to keep the module clean.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!