Amogh1221 commited on
Commit
30ca8f1
Β·
verified Β·
1 Parent(s): e9d64c0

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +192 -37
main.py CHANGED
@@ -10,6 +10,8 @@ import asyncio
10
  import json
11
 
12
  app = FastAPI(title="Deepcastle Engine API")
 
 
13
  class ConnectionManager:
14
  def __init__(self):
15
  # match_id -> list of websockets
@@ -194,7 +196,137 @@ async def get_move(request: MoveRequest):
194
  pass
195
 
196
 
197
- # ─── Game Review Route ─────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
198
  @app.post("/analyze-game", response_model=AnalyzeResponse)
199
  async def analyze_game(request: AnalyzeRequest):
200
  engine = None
@@ -205,13 +337,8 @@ async def analyze_game(request: AnalyzeRequest):
205
 
206
  analysis_results = []
207
 
208
- # We need the pre-move evaluation of the very first position
209
- info_before = await engine.analyse(board, limit)
210
- current_score, _ = get_normalized_score(info_before)
211
-
212
- # To track accuracy
213
- total_cpl: float = 0.0
214
- player_moves_count: int = 0
215
 
216
  counts = {
217
  "Brilliant": 0, "Great": 0, "Best": 0,
@@ -220,53 +347,79 @@ async def analyze_game(request: AnalyzeRequest):
220
  }
221
 
222
  player_is_white = (request.player_color.lower() == "white")
 
 
 
 
 
 
223
 
224
  for i, san_move in enumerate(request.moves):
225
  is_white_turn = board.turn == chess.WHITE
226
  is_player_turn = is_white_turn if player_is_white else not is_white_turn
227
 
228
- # The current_score is the score BEFORE this move
229
  score_before = current_score
230
 
231
- # Push move
232
  try:
233
  move = board.parse_san(san_move)
234
- board.push(move)
235
  except Exception:
236
- break # Invalid move, stop analysis here
 
 
 
 
 
237
 
238
- # Get eval AFTER move
239
- info_after = await engine.analyse(board, limit)
240
- score_after, _ = get_normalized_score(info_after)
 
 
 
 
 
 
241
 
242
- # Update current score for next iteration
 
 
 
 
 
 
 
 
243
  current_score = score_after
244
 
245
- # Calculate Centipawn Loss (diff between score before and score after)
246
- cpl = max(0, score_before - score_after) if is_white_turn else max(0, score_after - score_before)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  cpl = min(cpl, 1000.0)
248
-
249
- # Only track these stats for the requested player
250
  if is_player_turn:
251
  total_cpl += cpl
252
  player_moves_count += 1
253
-
254
- # Classification mapping
255
- if cpl <= 15:
256
- cls = "Best"
257
- elif cpl <= 35:
258
- cls = "Excellent"
259
- elif cpl <= 75:
260
- cls = "Good"
261
- elif cpl <= 150:
262
- cls = "Inaccuracy"
263
- elif cpl <= 300:
264
- cls = "Mistake"
265
- else:
266
- cls = "Blunder"
267
-
268
- if is_player_turn:
269
- counts[cls] += 1
270
 
271
  analysis_results.append(MoveAnalysis(
272
  move_num=i+1,
@@ -277,6 +430,8 @@ async def analyze_game(request: AnalyzeRequest):
277
  score_before=score_before / 100.0,
278
  score_after=score_after / 100.0
279
  ))
 
 
280
 
281
  # Win probability matching accuracy formula
282
  # Accuracy = 100 * exp(-0.02 * avg_cpl) smoothed
 
10
  import json
11
 
12
  app = FastAPI(title="Deepcastle Engine API")
13
+
14
+ # ─── Multiplaying / Challenge Manager ──────────────────────────────────────────
15
  class ConnectionManager:
16
  def __init__(self):
17
  # match_id -> list of websockets
 
196
  pass
197
 
198
 
199
+ # ─── Game Review Route & Move Classification Helpers ─────────────────────────
200
+ import math
201
+ from typing import Optional, List, Tuple
202
+
203
+ def get_win_percentage_from_cp(cp: int) -> float:
204
+ cp_ceiled = max(-1000, min(1000, cp))
205
+ MULTIPLIER = -0.00368208
206
+ win_chances = 2.0 / (1.0 + math.exp(MULTIPLIER * cp_ceiled)) - 1.0
207
+ return 50.0 + 50.0 * win_chances
208
+
209
+ def get_win_percentage(info: dict) -> float:
210
+ score = info.get("score")
211
+ if not score:
212
+ return 50.0
213
+ white_score = score.white()
214
+ if white_score.is_mate():
215
+ mate_val = white_score.mate()
216
+ return 100.0 if mate_val > 0 else 0.0
217
+ return get_win_percentage_from_cp(white_score.score())
218
+
219
+ def is_losing_or_alt_winning(pos_win_pct: float, alt_win_pct: float, is_white_move: bool) -> bool:
220
+ is_losing = pos_win_pct < 50.0 if is_white_move else pos_win_pct > 50.0
221
+ is_alt_winning = alt_win_pct > 97.0 if is_white_move else alt_win_pct < 3.0
222
+ return is_losing or is_alt_winning
223
+
224
+ def get_has_changed_outcome(last_win_pct: float, pos_win_pct: float, is_white_move: bool) -> bool:
225
+ diff = (pos_win_pct - last_win_pct) * (1 if is_white_move else -1)
226
+ return diff > 10.0 and ((last_win_pct < 50.0 and pos_win_pct > 50.0) or (last_win_pct > 50.0 and pos_win_pct < 50.0))
227
+
228
+ def get_is_only_good_move(pos_win_pct: float, alt_win_pct: float, is_white_move: bool) -> bool:
229
+ diff = (pos_win_pct - alt_win_pct) * (1 if is_white_move else -1)
230
+ return diff > 10.0
231
+
232
+ def is_simple_recapture(fen_two_moves_ago: str, previous_move: chess.Move, played_move: chess.Move) -> bool:
233
+ if previous_move.to_square != played_move.to_square:
234
+ return False
235
+ b = chess.Board(fen_two_moves_ago)
236
+ return b.piece_at(previous_move.to_square) is not None
237
+
238
+ def get_material_difference(board: chess.Board) -> int:
239
+ values = {chess.PAWN: 1, chess.KNIGHT: 3, chess.BISHOP: 3, chess.ROOK: 5, chess.QUEEN: 9, chess.KING: 0}
240
+ w = sum(values.get(p.piece_type, 0) for p in board.piece_map().values() if p.color == chess.WHITE)
241
+ b = sum(values.get(p.piece_type, 0) for p in board.piece_map().values() if p.color == chess.BLACK)
242
+ return w - b
243
+
244
+ def get_is_piece_sacrifice(board: chess.Board, played_move: chess.Move, best_pv: list) -> bool:
245
+ if not best_pv:
246
+ return False
247
+ start_diff = get_material_difference(board)
248
+ white_to_play = board.turn == chess.WHITE
249
+
250
+ sim_board = board.copy()
251
+ moves = [played_move] + best_pv
252
+ if len(moves) % 2 == 1:
253
+ moves = moves[:-1]
254
+
255
+ captured_w = []
256
+ captured_b = []
257
+ non_capturing = 1
258
+
259
+ for m in moves:
260
+ if m in sim_board.legal_moves:
261
+ captured_piece = sim_board.piece_at(m.to_square)
262
+ if sim_board.is_en_passant(m):
263
+ captured_piece = chess.Piece(chess.PAWN, not sim_board.turn)
264
+
265
+ if captured_piece:
266
+ if sim_board.turn == chess.WHITE:
267
+ captured_b.append(captured_piece.piece_type)
268
+ else:
269
+ captured_w.append(captured_piece.piece_type)
270
+ non_capturing = 1
271
+ else:
272
+ non_capturing -= 1
273
+ if non_capturing < 0:
274
+ break
275
+ sim_board.push(m)
276
+ else:
277
+ break
278
+
279
+ for p in captured_w[:]:
280
+ if p in captured_b:
281
+ captured_w.remove(p)
282
+ captured_b.remove(p)
283
+
284
+ if abs(len(captured_w) - len(captured_b)) <= 1 and all(p == chess.PAWN for p in captured_w + captured_b):
285
+ return False
286
+
287
+ end_diff = get_material_difference(sim_board)
288
+ mat_diff = end_diff - start_diff
289
+ player_rel = mat_diff if white_to_play else -mat_diff
290
+
291
+ return player_rel < 0
292
+
293
+ def get_move_classification(
294
+ last_win_pct: float,
295
+ pos_win_pct: float,
296
+ is_white_move: bool,
297
+ played_move: chess.Move,
298
+ best_move_before: chess.Move,
299
+ alt_win_pct: float,
300
+ fen_two_moves_ago: str,
301
+ uci_next_two_moves: tuple,
302
+ board_before_move: chess.Board,
303
+ best_pv_after: list
304
+ ) -> str:
305
+ diff = (pos_win_pct - last_win_pct) * (1 if is_white_move else -1)
306
+
307
+ if alt_win_pct is not None and diff >= -2.0:
308
+ if get_is_piece_sacrifice(board_before_move, played_move, best_pv_after):
309
+ if not is_losing_or_alt_winning(pos_win_pct, alt_win_pct, is_white_move):
310
+ return "Brilliant"
311
+
312
+ if alt_win_pct is not None and diff >= -2.0:
313
+ is_recapture = False
314
+ if fen_two_moves_ago and uci_next_two_moves:
315
+ is_recapture = is_simple_recapture(fen_two_moves_ago, uci_next_two_moves[0], uci_next_two_moves[1])
316
+
317
+ if not is_recapture and not is_losing_or_alt_winning(pos_win_pct, alt_win_pct, is_white_move):
318
+ if get_has_changed_outcome(last_win_pct, pos_win_pct, is_white_move) or get_is_only_good_move(pos_win_pct, alt_win_pct, is_white_move):
319
+ return "Great"
320
+
321
+ if best_move_before and played_move == best_move_before:
322
+ return "Best"
323
+
324
+ if diff < -20.0: return "Blunder"
325
+ if diff < -10.0: return "Mistake"
326
+ if diff < -5.0: return "Inaccuracy"
327
+ if diff < -2.0: return "Good"
328
+ return "Excellent"
329
+
330
  @app.post("/analyze-game", response_model=AnalyzeResponse)
331
  async def analyze_game(request: AnalyzeRequest):
332
  engine = None
 
337
 
338
  analysis_results = []
339
 
340
+ infos_before = await engine.analyse(board, limit, multipv=2)
341
+ infos_before = infos_before if isinstance(infos_before, list) else [infos_before]
 
 
 
 
 
342
 
343
  counts = {
344
  "Brilliant": 0, "Great": 0, "Best": 0,
 
347
  }
348
 
349
  player_is_white = (request.player_color.lower() == "white")
350
+
351
+ fen_history = [board.fen()]
352
+ move_history = []
353
+ total_cpl = 0.0
354
+ player_moves_count = 0
355
+ current_score, _ = get_normalized_score(infos_before[0])
356
 
357
  for i, san_move in enumerate(request.moves):
358
  is_white_turn = board.turn == chess.WHITE
359
  is_player_turn = is_white_turn if player_is_white else not is_white_turn
360
 
 
361
  score_before = current_score
362
 
 
363
  try:
364
  move = board.parse_san(san_move)
 
365
  except Exception:
366
+ break # Invalid move
367
+
368
+ info_before = infos_before[0]
369
+ win_pct_before = get_win_percentage(info_before)
370
+ best_pv_before = info_before.get("pv", [])
371
+ best_move_before = best_pv_before[0] if best_pv_before else None
372
 
373
+ alt_win_pct_before = None
374
+ if len(infos_before) > 1:
375
+ for line in infos_before:
376
+ if line.get("pv") and line.get("pv")[0] != move:
377
+ alt_win_pct_before = get_win_percentage(line)
378
+ break
379
+
380
+ board_before_move = board.copy()
381
+ board.push(move)
382
 
383
+ move_history.append(move)
384
+ fen_history.append(board.fen())
385
+
386
+ infos_after = await engine.analyse(board, limit, multipv=2)
387
+ infos_after = infos_after if isinstance(infos_after, list) else [infos_after]
388
+ info_after = infos_after[0]
389
+
390
+ win_pct_after = get_win_percentage(info_after)
391
+ score_after, _ = get_normalized_score(info_after)
392
  current_score = score_after
393
 
394
+ best_pv_after = info_after.get("pv", [])
395
+
396
+ fen_two_moves_ago = None
397
+ uci_next_two_moves = None
398
+ if len(move_history) >= 2:
399
+ fen_two_moves_ago = fen_history[-3]
400
+ uci_next_two_moves = (move_history[-2], move_history[-1])
401
+
402
+ cls = get_move_classification(
403
+ last_win_pct=win_pct_before,
404
+ pos_win_pct=win_pct_after,
405
+ is_white_move=is_white_turn,
406
+ played_move=move,
407
+ best_move_before=best_move_before,
408
+ alt_win_pct=alt_win_pct_before,
409
+ fen_two_moves_ago=fen_two_moves_ago,
410
+ uci_next_two_moves=uci_next_two_moves,
411
+ board_before_move=board_before_move,
412
+ best_pv_after=best_pv_after
413
+ )
414
+
415
+ move_gain = score_after - score_before if is_white_turn else score_before - score_after
416
+ cpl = max(0, -move_gain)
417
  cpl = min(cpl, 1000.0)
418
+
 
419
  if is_player_turn:
420
  total_cpl += cpl
421
  player_moves_count += 1
422
+ counts[cls] = counts.get(cls, 0) + 1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
423
 
424
  analysis_results.append(MoveAnalysis(
425
  move_num=i+1,
 
430
  score_before=score_before / 100.0,
431
  score_after=score_after / 100.0
432
  ))
433
+
434
+ infos_before = infos_after
435
 
436
  # Win probability matching accuracy formula
437
  # Accuracy = 100 * exp(-0.02 * avg_cpl) smoothed