Falguni commited on
Commit
b12ef0b
·
1 Parent(s): 63103fa

Add annotation support

Browse files
Files changed (2) hide show
  1. app.py +96 -27
  2. src/thinksqure_engine.py +126 -1
app.py CHANGED
@@ -1,16 +1,53 @@
1
  from typing import Optional, Tuple
2
  import gradio as gr
3
 
 
 
4
  from src.thinksqure_engine import ThinkSquareEngine
5
 
6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  def play_chess(
8
- move: Optional[str] = "", fen: Optional[str] = "", draw_ascii: bool = True
 
 
 
9
  ) -> Tuple:
10
  """Play a move in a chess game.
11
  Prerequisites:
12
- - User must be asked if they want to play as white or black. If user chooses black, pass an empty string in the first move for the engine to play as white.
 
 
 
13
  - User must be asked if they want a board drawn in ASCII format.
 
 
 
14
  - If a move is provided, it must be in long algebraic notation (e.g., "e4", "Nf3", "Bb5").
15
 
16
  To start a new game:
@@ -25,10 +62,11 @@ def play_chess(
25
  Args:
26
  move: The move to play in long algebraic notation. If None, the engine will play a move.
27
  fen: The FEN string representing the board state prior to the user's last move. If None, the game starts from the initial position.
28
- draw_ascii: Whether to draw the board in ASCII format. Defaults to True.
 
29
 
30
  Returns:
31
- The best move played by the engine, the updated board state in FEN notation, and a string representation of the board if draw_ascii is True else None.
32
  """
33
 
34
  if move is None or move == "" or move.lower() == "none" or move.lower() == "null":
@@ -48,32 +86,63 @@ def play_chess(
48
 
49
  bestmove_san = ThinkSquareEngine.get_best_move(fen)
50
  fen_after_move = ThinkSquareEngine.get_fen_after_move(bestmove_san, fen)
51
-
52
- board_str = (
53
- ThinkSquareEngine.get_ascii_board(fen_after_move) if draw_ascii else None
54
- )
 
 
 
 
 
 
 
 
 
 
55
 
56
  return bestmove_san, fen_after_move, board_str
57
 
58
 
59
- demo = gr.Interface(
60
- fn=play_chess,
61
- inputs=[
62
- gr.Textbox(
63
- label="Your Move (in standard algebraic notation - SAN) (Optional)",
64
- placeholder="e.g. e4, Nf3, Bb5, etc. Optional - ",
65
- value=None,
66
- ),
67
- gr.Textbox(
68
  label="FEN String (optional)",
69
- placeholder="Optional - Leave blank to start from initial position",
70
  value=None,
71
- ),
72
- gr.Checkbox(label="Draw ASCII Board", value=True),
73
- ],
74
- outputs=("text", "text", "text"),
75
- title="Play Chess with an engine",
76
- description="This interface allows you to play chess against the Stockfish engine. You can provide a move in standard algebraic notation and an optional FEN string representing the board state. The engine will respond with its best move, the updated FEN string, and an ASCII representation of the board if requested.",
77
- )
78
-
79
- demo.launch(mcp_server=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  from typing import Optional, Tuple
2
  import gradio as gr
3
 
4
+ from src.util.board_vis import colored_unicode_board
5
+ from src.util.pgn_util import export_pgn, read_pgn
6
  from src.thinksqure_engine import ThinkSquareEngine
7
 
8
 
9
+ def annotate_pgn(file, analysis_time_per_move: float = 0.1):
10
+ """Annotate a PGN file with engine analysis.
11
+
12
+ Args:
13
+ file: The PGN file to be annotated.
14
+ analysis_time_per_move: Time in seconds for engine analysis per move.
15
+
16
+ Returns:
17
+ The annotated PGN file.
18
+ """
19
+ game = read_pgn(file.name)
20
+
21
+ try:
22
+ analysis_time_per_move = float(analysis_time_per_move)
23
+ except ValueError:
24
+ raise ValueError("Analysis time must be a number.")
25
+
26
+ annotated_game = ThinkSquareEngine.annotate(
27
+ game, analysis_time=analysis_time_per_move
28
+ )
29
+
30
+ output_path = export_pgn(annotated_game)
31
+
32
+ return output_path
33
+
34
+
35
  def play_chess(
36
+ move: Optional[str] = "",
37
+ fen: Optional[str] = "",
38
+ draw_board: bool = True,
39
+ render_mode: str = "ascii",
40
  ) -> Tuple:
41
  """Play a move in a chess game.
42
  Prerequisites:
43
+ - User must be asked if they want to play as white or black.
44
+ - If user chooses black, pass an empty string in the first move for the engine to play as white.
45
+ - If the user chooses white, ask the user for a move and pass it in the first move for the engine to play as black.
46
+
47
  - User must be asked if they want a board drawn in ASCII format.
48
+ - If they do, pass `draw_board=True` to this function and print the board in ASCII format.
49
+ - If they do not, pass `draw_board=False`.
50
+
51
  - If a move is provided, it must be in long algebraic notation (e.g., "e4", "Nf3", "Bb5").
52
 
53
  To start a new game:
 
62
  Args:
63
  move: The move to play in long algebraic notation. If None, the engine will play a move.
64
  fen: The FEN string representing the board state prior to the user's last move. If None, the game starts from the initial position.
65
+ draw_board: Whether to draw the board in ASCII/Unicode format. Defaults to True.
66
+ render_mode: The rendering mode for the board. Defaults to "ascii". This can be "ascii", "svg", or "unicode".
67
 
68
  Returns:
69
+ The best move played by the engine, the updated board state in FEN notation, and a string representation of the board if draw_board is True else None.
70
  """
71
 
72
  if move is None or move == "" or move.lower() == "none" or move.lower() == "null":
 
86
 
87
  bestmove_san = ThinkSquareEngine.get_best_move(fen)
88
  fen_after_move = ThinkSquareEngine.get_fen_after_move(bestmove_san, fen)
89
+ if draw_board:
90
+ if render_mode == "ascii":
91
+ board_str = ThinkSquareEngine.render_board_ascii(fen_after_move)
92
+ elif render_mode == "svg":
93
+ board_str = ThinkSquareEngine.render_board_svg(fen_after_move)
94
+ elif render_mode == "unicode":
95
+ board_str = colored_unicode_board(fen_after_move)
96
+ else:
97
+ raise ValueError(
98
+ "Invalid render mode. Choose 'ascii', 'svg', or 'unicode'."
99
+ )
100
+
101
+ else:
102
+ board_str = None
103
 
104
  return bestmove_san, fen_after_move, board_str
105
 
106
 
107
+ with gr.Blocks(title="ThinkSquare") as app:
108
+
109
+ with gr.Tab("Play Chess"):
110
+ gr.Markdown("### Play Chess with an engine")
111
+ move_input = gr.Textbox(
112
+ label="Your Move (SAN)", placeholder="e4, Nf3...", value=None
113
+ )
114
+ fen_input = gr.Textbox(
 
115
  label="FEN String (optional)",
116
+ placeholder="Leave blank to start from initial position",
117
  value=None,
118
+ )
119
+ draw_board_checkbox = gr.Checkbox(label="Draw Board", value=True)
120
+ play_btn = gr.Button("Submit Move")
121
+
122
+ best_move_output = gr.Textbox(label="Best Move by Engine (SAN)")
123
+ updated_fen_output = gr.Textbox(label="Updated FEN")
124
+ board_output = gr.Textbox(label="Board (ASCII)", visible=True)
125
+
126
+ play_btn.click(
127
+ fn=play_chess,
128
+ inputs=[move_input, fen_input, draw_board_checkbox],
129
+ outputs=[best_move_output, updated_fen_output, board_output],
130
+ )
131
+
132
+ with gr.Tab("PGN Annotation"):
133
+ gr.Markdown("### PGN Analyzer")
134
+ pgn_file = gr.File(label="Upload PGN", file_types=[".pgn"])
135
+ analysis_time = gr.Textbox(
136
+ label="Analysis Time per Move (seconds)", value="0.1"
137
+ )
138
+ analyze_btn = gr.Button("Annotate PGN")
139
+
140
+ annotated_pgn_file = gr.File(label="Download Annotated PGN")
141
+
142
+ analyze_btn.click(
143
+ fn=annotate_pgn,
144
+ inputs=[pgn_file, analysis_time],
145
+ outputs=annotated_pgn_file,
146
+ )
147
+
148
+ app.launch(mcp_server=True)
src/thinksqure_engine.py CHANGED
@@ -2,6 +2,10 @@ from pathlib import Path
2
  from typing import Optional
3
  import chess
4
  import chess.engine
 
 
 
 
5
 
6
 
7
  class ThinkSquareEngine:
@@ -21,6 +25,108 @@ class ThinkSquareEngine:
21
 
22
  return bestmove_san
23
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
24
  @staticmethod
25
  def is_valid_move(
26
  move_san: str,
@@ -58,7 +164,7 @@ class ThinkSquareEngine:
58
  return None
59
 
60
  @staticmethod
61
- def get_ascii_board(fen: Optional[str] = None) -> str:
62
  if fen is None:
63
  fen = chess.STARTING_FEN
64
 
@@ -66,3 +172,22 @@ class ThinkSquareEngine:
66
  ascii_representation = str(board)
67
 
68
  return ascii_representation
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  from typing import Optional
3
  import chess
4
  import chess.engine
5
+ import chess.svg
6
+ import chess.pgn
7
+
8
+ from src.util.pgn_util import add_variation
9
 
10
 
11
  class ThinkSquareEngine:
 
25
 
26
  return bestmove_san
27
 
28
+ @staticmethod
29
+ def get_engine_analysis(board, analysis_time=0.1):
30
+ with chess.engine.SimpleEngine.popen_uci(ThinkSquareEngine._ENGINE) as engine:
31
+ pre_info = engine.analyse(board, chess.engine.Limit(time=analysis_time))
32
+ return pre_info
33
+
34
+ @staticmethod
35
+ def _perform_post_analysis_and_add_comment(
36
+ analysis_time,
37
+ board,
38
+ played_node,
39
+ pre_eval,
40
+ engine_best_move_san,
41
+ pv,
42
+ ):
43
+ post_info = ThinkSquareEngine.get_engine_analysis(board, analysis_time)
44
+ post_eval = post_info["score"].white().score(mate_score=100000)
45
+
46
+ # Evaluation drop
47
+ eval_drop = (
48
+ (pre_eval - post_eval)
49
+ if pre_eval is not None and post_eval is not None
50
+ else 0
51
+ )
52
+
53
+ # Classification
54
+ if eval_drop > 200:
55
+ label = "Blunder"
56
+ elif eval_drop > 100:
57
+ label = "Mistake"
58
+ elif eval_drop > 50:
59
+ label = "Inaccuracy"
60
+ elif eval_drop < -150:
61
+ label = "Super Brilliant"
62
+ elif eval_drop < -60:
63
+ label = "Brilliant"
64
+ elif abs(eval_drop) <= 30:
65
+ label = None
66
+ else:
67
+ label = "Good"
68
+
69
+ if label is not None:
70
+ comment = f"{label}. "
71
+ if eval_drop > 0 and engine_best_move_san is not None:
72
+ comment += f"Better was {engine_best_move_san} "
73
+ played_node.comment = comment
74
+ if pv is not None:
75
+ add_variation(played_node.parent, pv)
76
+ else:
77
+ played_node.comment = comment
78
+
79
+ @staticmethod
80
+ def annotate(game, analysis_time: float = 0.1):
81
+
82
+ if not isinstance(game, chess.pgn.Game):
83
+ raise ValueError("Input must be a chess.pgn.Game object")
84
+
85
+ if not game.variations:
86
+ raise ValueError("Game must have at least one variation")
87
+
88
+ if analysis_time <= 0:
89
+ raise ValueError("Analysis time must be greater than 0")
90
+
91
+ node = game
92
+
93
+ while node.variations:
94
+ board = node.board()
95
+ played_node = node.variation(0)
96
+ played_move = played_node.move
97
+
98
+ # Get engine's best move BEFORE the actual move
99
+ pre_info = ThinkSquareEngine.get_engine_analysis(board, analysis_time)
100
+
101
+ pre_eval = pre_info["score"].white().score(mate_score=100000)
102
+
103
+ # Best move suggestion
104
+ engine_best_move = pre_info.get("pv", [None])[0]
105
+ engine_best_move_san = (
106
+ board.san(engine_best_move) if engine_best_move else None
107
+ )
108
+
109
+ # Get principal variation (PV)
110
+ pv = pre_info.get("pv", [])
111
+
112
+ # Make the played move and get new evaluation
113
+ played_move_san = board.san(played_move) if played_move else None
114
+ board.push(played_move)
115
+
116
+ if played_move_san != engine_best_move_san:
117
+ ThinkSquareEngine._perform_post_analysis_and_add_comment(
118
+ analysis_time,
119
+ board,
120
+ played_node,
121
+ pre_eval,
122
+ engine_best_move_san,
123
+ pv,
124
+ )
125
+
126
+ node = played_node
127
+
128
+ return game
129
+
130
  @staticmethod
131
  def is_valid_move(
132
  move_san: str,
 
164
  return None
165
 
166
  @staticmethod
167
+ def render_board_ascii(fen: Optional[str] = None) -> str:
168
  if fen is None:
169
  fen = chess.STARTING_FEN
170
 
 
172
  ascii_representation = str(board)
173
 
174
  return ascii_representation
175
+
176
+ @staticmethod
177
+ def render_board_svg(fen: Optional[str] = None):
178
+ if fen is None:
179
+ fen = chess.STARTING_FEN
180
+
181
+ board = chess.Board(fen)
182
+ svg = chess.svg.board(board=board)
183
+ return svg
184
+
185
+ @staticmethod
186
+ def render_board_unicode(fen: Optional[str] = None) -> str:
187
+ if fen is None:
188
+ fen = chess.STARTING_FEN
189
+
190
+ board = chess.Board(fen)
191
+ unicode_representation = board.unicode(invert_color=False)
192
+
193
+ return unicode_representation