File size: 9,522 Bytes
52a4f3c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
"""
Chess position analysis and evaluation.

Provides:
- Material balance calculation
- Piece activity assessment
- Threat detection
- Position description for agent
"""

import logging
import chess as python_chess
from typing import Dict, Any, Optional, List

logger = logging.getLogger(__name__)


class PositionAnalysis:
    """Analyze chess positions and provide context for the agent."""

    # Piece values in centipawns
    PIECE_VALUES = {
        python_chess.PAWN: 100,
        python_chess.KNIGHT: 320,
        python_chess.BISHOP: 330,
        python_chess.ROOK: 500,
        python_chess.QUEEN: 900,
        python_chess.KING: 0,
    }

    @staticmethod
    def material_balance(board: python_chess.Board) -> Dict[str, Any]:
        """
        Calculate material balance.

        Returns:
            Dict with white_material, black_material, balance
        """
        white_material = 0
        black_material = 0

        for piece_type, value in PositionAnalysis.PIECE_VALUES.items():
            white_count = len(board.pieces(piece_type, python_chess.WHITE))
            black_count = len(board.pieces(piece_type, python_chess.BLACK))

            white_material += white_count * value
            black_material += black_count * value

        balance = white_material - black_material

        return {
            "white_material": white_material,
            "black_material": black_material,
            "balance": balance,
            "description": (
                "White is winning" if balance > 300
                else "White is better" if balance > 100
                else "Equal" if abs(balance) <= 100
                else "Black is better" if balance < -100
                else "Black is winning"
            ),
        }

    @staticmethod
    def piece_activity(board: python_chess.Board) -> Dict[str, Any]:
        """
        Assess piece activity (how mobile and threatening pieces are).

        Returns:
            Dict with white_activity, black_activity scores
        """
        white_moves = 0
        black_moves = 0

        white_copy = board.copy()
        white_copy.turn = python_chess.WHITE
        white_moves = len(list(white_copy.legal_moves))

        black_copy = board.copy()
        black_copy.turn = python_chess.BLACK
        black_moves = len(list(black_copy.legal_moves))

        return {
            "white_moves": white_moves,
            "black_moves": black_moves,
            "white_activity": "high" if white_moves > 30 else "normal" if white_moves > 20 else "restricted",
            "black_activity": "high" if black_moves > 30 else "normal" if black_moves > 20 else "restricted",
        }

    @staticmethod
    def king_safety(board: python_chess.Board) -> Dict[str, str]:
        """
        Assess king safety for both sides.

        Returns:
            Dict with white_safety, black_safety descriptions
        """
        def assess_king(color: python_chess.Color) -> str:
            king_square = board.king(color)
            if king_square is None:
                return "unknown"

            # Check if king is in check
            if board.is_check() and board.turn == color:
                return "in_check"

            # Count defending pieces around king
            king_file = python_chess.square_file(king_square)
            king_rank = python_chess.square_rank(king_square)

            has_castling = (
                board.has_kingside_castling_rights(color)
                or board.has_queenside_castling_rights(color)
            )

            if not has_castling and king_file > 2 and king_file < 6:
                return "exposed"

            return "safe"

        return {
            "white_safety": assess_king(python_chess.WHITE),
            "black_safety": assess_king(python_chess.BLACK),
        }

    @staticmethod
    def pawn_structure(board: python_chess.Board) -> Dict[str, Any]:
        """
        Analyze pawn structure (weak squares, passed pawns, etc.).

        Returns:
            Dict with pawn structure analysis
        """
        white_pawns = board.pieces(python_chess.PAWN, python_chess.WHITE)
        black_pawns = board.pieces(python_chess.PAWN, python_chess.BLACK)

        # Check for passed pawns (simplified)
        white_passed = []
        black_passed = []

        for pawn_square in white_pawns:
            pawn_file = python_chess.square_file(pawn_square)
            pawn_rank = python_chess.square_rank(pawn_square)
            # Simplified: pawn is passed if no enemy pawns ahead on adjacent files
            is_passed = True
            for enemy_pawn in black_pawns:
                enemy_file = python_chess.square_file(enemy_pawn)
                if abs(enemy_file - pawn_file) <= 1 and python_chess.square_rank(enemy_pawn) > pawn_rank:
                    is_passed = False
                    break
            if is_passed:
                white_passed.append(python_chess.square_name(pawn_square))

        # Same for black
        for pawn_square in black_pawns:
            pawn_file = python_chess.square_file(pawn_square)
            pawn_rank = python_chess.square_rank(pawn_square)
            is_passed = True
            for enemy_pawn in white_pawns:
                enemy_file = python_chess.square_file(enemy_pawn)
                if abs(enemy_file - pawn_file) <= 1 and python_chess.square_rank(enemy_pawn) < pawn_rank:
                    is_passed = False
                    break
            if is_passed:
                black_passed.append(python_chess.square_name(pawn_square))

        return {
            "white_pawns": len(white_pawns),
            "black_pawns": len(black_pawns),
            "white_passed_pawns": white_passed,
            "black_passed_pawns": black_passed,
            "pawn_balance": "White ahead" if len(white_pawns) > len(black_pawns) else (
                "Black ahead" if len(black_pawns) > len(white_pawns) else "Equal"
            ),
        }

    @staticmethod
    def threats(board: python_chess.Board) -> Dict[str, List[str]]:
        """
        Detect immediate threats (checks, attacks on pieces).

        Returns:
            Dict with white_threats, black_threats
        """
        threats = {
            "white_threats": [],
            "black_threats": [],
        }

        # Check for checks
        if board.is_check():
            if board.turn == python_chess.WHITE:
                threats["white_threats"].append("In check")
            else:
                threats["black_threats"].append("In check")

        # Check for hanging pieces (could be improved)
        for square in python_chess.SQUARES:
            piece = board.piece_at(square)
            if piece is None:
                continue

            # Check if piece is attacked
            if board.is_attacked_by(not piece.color, square):
                piece_name = python_chess.piece_name(piece.piece_type)
                threat_str = f"{piece_name.capitalize()} hanging on {python_chess.square_name(square)}"
                if piece.color == python_chess.WHITE:
                    threats["black_threats"].append(threat_str)
                else:
                    threats["white_threats"].append(threat_str)

        return threats

    @staticmethod
    def get_position_summary(board: python_chess.Board, player_color: Optional[bool] = None) -> Dict[str, Any]:
        """
        Get a comprehensive position summary.

        Args:
            board: Chess board
            player_color: WHITE, BLACK, or None to describe from both perspectives

        Returns:
            Dict with complete position analysis
        """
        material = PositionAnalysis.material_balance(board)
        activity = PositionAnalysis.piece_activity(board)
        safety = PositionAnalysis.king_safety(board)
        pawns = PositionAnalysis.pawn_structure(board)
        threat_list = PositionAnalysis.threats(board)

        summary = {
            "material": material,
            "activity": activity,
            "king_safety": safety,
            "pawn_structure": pawns,
            "threats": threat_list,
            "summary_text": (
                f"Material: {material['description']}. "
                f"White has {activity['white_moves']} legal moves, "
                f"Black has {activity['black_moves']} legal moves."
            ),
        }

        return summary

    @staticmethod
    def describe_position_for_agent(board: python_chess.Board) -> str:
        """
        Create a natural language description of the position for the agent.

        Args:
            board: Chess board

        Returns:
            Human-readable position description
        """
        analysis = PositionAnalysis.get_position_summary(board)
        material = analysis["material"]
        activity = analysis["activity"]
        safety = analysis["king_safety"]
        threats = analysis["threats"]

        lines = [
            f"Position Status: {material['description']}",
            f"White King: {safety['white_safety']}, Black King: {safety['black_safety']}",
            f"White Mobility: {activity['white_activity']}, Black Mobility: {activity['black_activity']}",
        ]

        if threats["white_threats"]:
            lines.append(f"White threats: {', '.join(threats['white_threats'])}")
        if threats["black_threats"]:
            lines.append(f"Black threats: {', '.join(threats['black_threats'])}")

        return "\n".join(lines)


__all__ = ["PositionAnalysis"]