Amogh1221 commited on
Commit
648d960
Β·
verified Β·
1 Parent(s): 7111f1d

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +210 -197
main.py CHANGED
@@ -10,6 +10,7 @@ import asyncio
10
  import json
11
 
12
  app = FastAPI(title="Deepcastle Engine API")
 
13
 
14
  # ─── Multiplaying / Challenge Manager ──────────────────────────────────────────
15
  class ConnectionManager:
@@ -120,17 +121,65 @@ def health():
120
  return {"status": "error", "message": "Engine binary not found"}
121
  return {"status": "ok", "engine": "Deepcastle"}
122
 
123
- async def get_engine():
124
- if not os.path.exists(ENGINE_PATH):
125
- raise HTTPException(status_code=500, detail="Engine binary not found")
126
- transport, engine = await chess.engine.popen_uci(ENGINE_PATH)
127
- if os.path.exists(NNUE_PATH):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  try:
129
- await engine.configure({"EvalFile": NNUE_PATH})
130
- await engine.configure({"Hash": 512, "Threads": 2})
131
- except Exception:
132
- pass
133
- return engine
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
134
 
135
  def get_normalized_score(info) -> tuple[float, Optional[int]]:
136
  """Returns the score from White's perspective in centipawns."""
@@ -145,61 +194,49 @@ def get_normalized_score(info) -> tuple[float, Optional[int]]:
145
  # ─── Engine Inference Route ────────────────────────────────────────────────────
146
  @app.post("/move", response_model=MoveResponse)
147
  async def get_move(request: MoveRequest):
148
- engine = None
149
  try:
150
- engine = await get_engine()
151
- board = chess.Board(request.fen)
152
- limit = chess.engine.Limit(time=request.time, depth=request.depth)
153
-
154
- result = await engine.play(board, limit)
155
- info = await engine.analyse(board, limit)
156
-
157
- # From White's perspective in CP -> converted to Pawns for UI
158
- score_cp, mate_in = get_normalized_score(info)
159
-
160
- depth = info.get("depth", 0)
161
- nodes = info.get("nodes", 0)
162
- nps = info.get("nps", 0)
163
-
164
- pv_board = board.copy()
165
- pv_parts = []
166
- for m in info.get("pv", [])[:5]:
167
- if m in pv_board.legal_moves:
168
- try:
169
- pv_parts.append(pv_board.san(m))
170
- pv_board.push(m)
171
- except Exception:
172
- break
173
- else:
174
- break
175
- pv = " ".join(pv_parts)
176
-
177
- # Map mate score to pawns representation to not break old UI
178
- score_pawns = score_cp / 100.0 if abs(score_cp) < 9900 else (100.0 if score_cp > 0 else -100.0)
179
-
180
- # Check for opening name
181
- board_fen_only = board.fen().split(" ")[0]
182
- opening_name = openings_db.get(board_fen_only)
183
-
184
- return MoveResponse(
185
- bestmove=result.move.uci(),
186
- score=score_pawns,
187
- depth=depth,
188
- nodes=nodes,
189
- nps=nps,
190
- pv=pv,
191
- mate_in=mate_in,
192
- opening=opening_name
193
- )
194
  except Exception as e:
195
  print(f"Error: {e}")
196
  raise HTTPException(status_code=500, detail=str(e))
197
- finally:
198
- if engine:
199
- try:
200
- await engine.quit()
201
- except Exception:
202
- pass
203
 
204
 
205
  import math
@@ -345,151 +382,127 @@ def get_move_classification(
345
 
346
  @app.post("/analyze-game", response_model=AnalyzeResponse)
347
  async def analyze_game(request: AnalyzeRequest):
348
- engine = None
349
  try:
350
- engine = await get_engine()
351
- board = chess.Board(request.start_fen) if request.start_fen else chess.Board()
352
- limit = chess.engine.Limit(time=request.time_per_move)
353
-
354
- analysis_results = []
355
-
356
- infos_before = await engine.analyse(board, limit, multipv=2)
357
- infos_before = infos_before if isinstance(infos_before, list) else [infos_before]
358
-
359
- counts = {
360
- "Book": 0, "Brilliant": 0, "Great": 0, "Best": 0,
361
- "Excellent": 0, "Good": 0, "Inaccuracy": 0,
362
- "Mistake": 0, "Blunder": 0
363
- }
364
-
365
- player_is_white = (request.player_color.lower() == "white")
366
-
367
- fen_history = [board.fen()]
368
- move_history = []
369
- total_cpl = 0.0
370
- player_moves_count = 0
371
- current_score, _ = get_normalized_score(infos_before[0])
372
-
373
- for i, san_move in enumerate(request.moves):
374
- is_white_turn = board.turn == chess.WHITE
375
- is_player_turn = is_white_turn if player_is_white else not is_white_turn
376
 
377
- score_before = current_score
 
 
378
 
379
- try:
380
- move = board.parse_san(san_move)
381
- except Exception:
382
- break # Invalid move
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
 
384
- info_dict = infos_before[0]
385
- pv_list = info_dict.get("pv", [])
386
- best_move_before = pv_list[0] if pv_list else None
387
-
388
- score_before, _ = get_normalized_score(info_dict)
389
- win_pct_before = get_win_percentage(info_dict)
390
- alt_win_pct_before: Optional[float] = None
391
- if len(infos_before) > 1:
392
- # Find the first alternative move that is not the played move
393
- for line in infos_before:
394
- if line.get("pv") and line.get("pv")[0] != move:
395
- alt_win_pct_before = get_win_percentage(line)
396
- break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
397
 
398
- board_before_move = board.copy()
399
- board.push(move)
400
-
401
- move_history.append(move)
402
- fen_history.append(board.fen())
403
-
404
- infos_after_raw = await engine.analyse(board, limit, multipv=2)
405
- infos_after: List[dict] = infos_after_raw if isinstance(infos_after_raw, list) else [infos_after_raw]
406
-
407
- info_after_dict: dict = infos_after[0]
408
-
409
- win_pct_after = get_win_percentage(info_after_dict)
410
- score_after, _ = get_normalized_score(info_after_dict)
411
- current_score = score_after
412
-
413
- best_pv_after = info_after_dict.get("pv", [])
414
-
415
- fen_two_moves_ago = None
416
- uci_next_two_moves = None
417
- if len(move_history) >= 2:
418
- fen_two_moves_ago = fen_history[-3]
419
- uci_next_two_moves = (move_history[-2], move_history[-1])
420
-
421
- cls = "Book"
422
- opening_name = None
423
- board_fen_only = board.fen().split(" ")[0]
424
- if board_fen_only in openings_db:
425
  cls = "Book"
426
- opening_name = openings_db[board_fen_only]
427
- else:
428
- cls = get_move_classification(
429
- last_win_pct=win_pct_before,
430
- pos_win_pct=win_pct_after,
431
- is_white_move=is_white_turn,
432
- played_move=move,
433
- best_move_before=best_move_before,
434
- alt_win_pct=alt_win_pct_before,
435
- fen_two_moves_ago=fen_two_moves_ago,
436
- uci_next_two_moves=uci_next_two_moves,
437
- board_before_move=board_before_move,
438
- best_pv_after=best_pv_after
439
- )
440
-
441
- move_gain = score_after - score_before if is_white_turn else score_before - score_after
442
- cpl = max(0, -move_gain)
443
- cpl = min(cpl, 1000.0)
444
-
445
- if is_player_turn:
446
- total_cpl += cpl
447
- player_moves_count += 1
448
- counts[cls] = counts.get(cls, 0) + 1
449
-
450
- analysis_results.append(MoveAnalysis(
451
- move_num=i+1,
452
- san=san_move,
453
- fen=board.fen(),
454
- classification=cls,
455
- cpl=float(cpl),
456
- score_before=float(score_before / 100.0),
457
- score_after=float(score_after / 100.0),
458
- best_move=best_move_before.uci() if best_move_before else "",
459
- opening=opening_name
460
- ))
461
-
462
- infos_before = infos_after
463
-
464
- # Win probability matching accuracy formula
465
- # Accuracy = 100 * exp(-0.02 * avg_cpl) smoothed
466
- avg_cpl = total_cpl / max(1, player_moves_count)
467
-
468
- # Simple heuristic mapping for Accuracy & Elo
469
- # 0 avg loss -> 100%
470
- # ~100 avg loss -> ~60%
471
- accuracy = max(10.0, min(100.0, 100.0 * math.exp(-0.005 * avg_cpl)))
472
-
473
- # Estimate Elo based slightly on accuracy
474
- # This is a fun heuristic metric
475
- estimated_elo = int(max(400, min(3600, 3600 - (avg_cpl * 20))))
476
-
477
- return AnalyzeResponse(
478
- accuracy=round(accuracy, 1),
479
- estimated_elo=estimated_elo,
480
- moves=analysis_results,
481
- counts=counts
482
- )
483
-
484
  except Exception as e:
485
  print(f"Analysis Error: {e}")
486
  raise HTTPException(status_code=500, detail=str(e))
487
- finally:
488
- if engine:
489
- try:
490
- await engine.quit()
491
- except Exception:
492
- pass
493
 
494
 
495
  if __name__ == "__main__":
 
10
  import json
11
 
12
  app = FastAPI(title="Deepcastle Engine API")
13
+ from contextlib import asynccontextmanager
14
 
15
  # ─── Multiplaying / Challenge Manager ──────────────────────────────────────────
16
  class ConnectionManager:
 
121
  return {"status": "error", "message": "Engine binary not found"}
122
  return {"status": "ok", "engine": "Deepcastle"}
123
 
124
+ class EnginePool:
125
+ def __init__(self, size=4):
126
+ self.size = size
127
+ self.engines = asyncio.Queue()
128
+ self.all_engines = []
129
+
130
+ async def start(self):
131
+ print(f"Initializing engine pool with {self.size} processes...")
132
+ for i in range(self.size):
133
+ try:
134
+ engine = await self._create_engine()
135
+ await self.engines.put(engine)
136
+ self.all_engines.append(engine)
137
+ print(f" [+] Engine {i+1}/{self.size} ready.")
138
+ except Exception as e:
139
+ print(f" [!] Failed to start engine {i+1}: {e}")
140
+
141
+ async def _create_engine(self):
142
+ if not os.path.exists(ENGINE_PATH):
143
+ raise Exception("Engine binary not found")
144
+ transport, engine = await chess.engine.popen_uci(ENGINE_PATH)
145
+ if os.path.exists(NNUE_PATH):
146
+ try:
147
+ # Set Hash to 512 as requested, keep Threads to 1 to avoid CPU stalling
148
+ await engine.configure({"EvalFile": NNUE_PATH, "Hash": 512, "Threads": 1})
149
+ except Exception:
150
+ pass
151
+ return engine
152
+
153
+ @asynccontextmanager
154
+ async def acquire(self):
155
+ engine = await self.engines.get()
156
  try:
157
+ yield engine
158
+ finally:
159
+ # Check if engine is still alive, if not, restart it
160
+ try:
161
+ await self.engines.put(engine)
162
+ except Exception:
163
+ # If engine is dead, we could restart here, but for now just put back
164
+ await self.engines.put(await self._create_engine())
165
+
166
+ async def stop(self):
167
+ print("Shutting down engine pool...")
168
+ for engine in self.all_engines:
169
+ try:
170
+ await engine.quit()
171
+ except:
172
+ pass
173
+
174
+ pool = EnginePool(size=8)
175
+
176
+ @app.on_event("startup")
177
+ async def startup_event():
178
+ await pool.start()
179
+
180
+ @app.on_event("shutdown")
181
+ async def shutdown_event():
182
+ await pool.stop()
183
 
184
  def get_normalized_score(info) -> tuple[float, Optional[int]]:
185
  """Returns the score from White's perspective in centipawns."""
 
194
  # ─── Engine Inference Route ────────────────────────────────────────────────────
195
  @app.post("/move", response_model=MoveResponse)
196
  async def get_move(request: MoveRequest):
 
197
  try:
198
+ async with pool.acquire() as engine:
199
+ board = chess.Board(request.fen)
200
+ limit = chess.engine.Limit(time=request.time, depth=request.depth)
201
+
202
+ result = await engine.play(board, limit)
203
+ info = await engine.analyse(board, limit)
204
+
205
+ # From White's perspective in CP -> converted to Pawns for UI
206
+ score_cp, mate_in = get_normalized_score(info)
207
+ depth = info.get("depth", 0)
208
+ nodes = info.get("nodes", 0)
209
+ nps = info.get("nps", 0)
210
+
211
+ pv_board = board.copy()
212
+ pv_parts = []
213
+ for m in info.get("pv", [])[:5]:
214
+ if m in pv_board.legal_moves:
215
+ try:
216
+ pv_parts.append(pv_board.san(m))
217
+ pv_board.push(m)
218
+ except Exception:
219
+ break
220
+ else: break
221
+ pv = " ".join(pv_parts)
222
+
223
+ score_pawns = score_cp / 100.0 if abs(score_cp) < 9900 else (100.0 if score_cp > 0 else -100.0)
224
+ board_fen_only = board.fen().split(" ")[0]
225
+ opening_name = openings_db.get(board_fen_only)
226
+
227
+ return MoveResponse(
228
+ bestmove=result.move.uci(),
229
+ score=score_pawns,
230
+ depth=depth,
231
+ nodes=nodes,
232
+ nps=nps,
233
+ pv=pv,
234
+ mate_in=mate_in,
235
+ opening=opening_name
236
+ )
 
 
 
 
 
237
  except Exception as e:
238
  print(f"Error: {e}")
239
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
240
 
241
 
242
  import math
 
382
 
383
  @app.post("/analyze-game", response_model=AnalyzeResponse)
384
  async def analyze_game(request: AnalyzeRequest):
 
385
  try:
386
+ async with pool.acquire() as engine:
387
+ board = chess.Board(request.start_fen) if request.start_fen else chess.Board()
388
+ limit = chess.engine.Limit(time=request.time_per_move)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
389
 
390
+ analysis_results = []
391
+ infos_before = await engine.analyse(board, limit, multipv=2)
392
+ infos_before = infos_before if isinstance(infos_before, list) else [infos_before]
393
 
394
+ counts = {
395
+ "Book": 0, "Brilliant": 0, "Great": 0, "Best": 0,
396
+ "Excellent": 0, "Good": 0, "Inaccuracy": 0,
397
+ "Mistake": 0, "Blunder": 0
398
+ }
399
+
400
+ player_is_white = (request.player_color.lower() == "white")
401
+ fen_history = [board.fen()]
402
+ move_history = []
403
+ total_cpl = 0.0
404
+ player_moves_count = 0
405
+ current_score, _ = get_normalized_score(infos_before[0])
406
+
407
+ for i, san_move in enumerate(request.moves):
408
+ is_white_turn = board.turn == chess.WHITE
409
+ is_player_turn = is_white_turn if player_is_white else not is_white_turn
410
+
411
+ score_before = current_score
412
+ try:
413
+ move = board.parse_san(san_move)
414
+ except Exception:
415
+ break
416
 
417
+ info_dict = infos_before[0]
418
+ pv_list = info_dict.get("pv", [])
419
+ best_move_before = pv_list[0] if pv_list else None
420
+
421
+ score_before, _ = get_normalized_score(info_dict)
422
+ win_pct_before = get_win_percentage(info_dict)
423
+ alt_win_pct_before: Optional[float] = None
424
+ if len(infos_before) > 1:
425
+ for line in infos_before:
426
+ if line.get("pv") and line.get("pv")[0] != move:
427
+ alt_win_pct_before = get_win_percentage(line)
428
+ break
429
+
430
+ board_before_move = board.copy()
431
+ board.push(move)
432
+
433
+ move_history.append(move)
434
+ fen_history.append(board.fen())
435
+
436
+ infos_after_raw = await engine.analyse(board, limit, multipv=2)
437
+ infos_after: List[dict] = infos_after_raw if isinstance(infos_after_raw, list) else [infos_after_raw]
438
+
439
+ info_after_dict: dict = infos_after[0]
440
+ win_pct_after = get_win_percentage(info_after_dict)
441
+ score_after, _ = get_normalized_score(info_after_dict)
442
+ current_score = score_after
443
+ best_pv_after = info_after_dict.get("pv", [])
444
+
445
+ fen_two_moves_ago = None
446
+ uci_next_two_moves = None
447
+ if len(move_history) >= 2:
448
+ fen_two_moves_ago = fen_history[-3]
449
+ uci_next_two_moves = (move_history[-2], move_history[-1])
450
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
451
  cls = "Book"
452
+ opening_name = None
453
+ board_fen_only = board.fen().split(" ")[0]
454
+ if board_fen_only in openings_db:
455
+ cls = "Book"
456
+ opening_name = openings_db[board_fen_only]
457
+ else:
458
+ cls = get_move_classification(
459
+ last_win_pct=win_pct_before,
460
+ pos_win_pct=win_pct_after,
461
+ is_white_move=is_white_turn,
462
+ played_move=move,
463
+ best_move_before=best_move_before,
464
+ alt_win_pct=alt_win_pct_before,
465
+ fen_two_moves_ago=fen_two_moves_ago,
466
+ uci_next_two_moves=uci_next_two_moves,
467
+ board_before_move=board_before_move,
468
+ best_pv_after=best_pv_after
469
+ )
470
+
471
+ move_gain = score_after - score_before if is_white_turn else score_before - score_after
472
+ cpl = max(0, -move_gain)
473
+ cpl = min(cpl, 1000.0)
474
+
475
+ if is_player_turn:
476
+ total_cpl += cpl
477
+ player_moves_count += 1
478
+ counts[cls] = counts.get(cls, 0) + 1
479
+
480
+ analysis_results.append(MoveAnalysis(
481
+ move_num=i+1,
482
+ san=san_move,
483
+ fen=board.fen(),
484
+ classification=cls,
485
+ cpl=float(cpl),
486
+ score_before=float(score_before / 100.0),
487
+ score_after=float(score_after / 100.0),
488
+ best_move=best_move_before.uci() if best_move_before else "",
489
+ opening=opening_name
490
+ ))
491
+ infos_before = infos_after
492
+
493
+ avg_cpl = total_cpl / max(1, player_moves_count)
494
+ accuracy = max(10.0, min(100.0, 100.0 * math.exp(-0.005 * avg_cpl)))
495
+ estimated_elo = int(max(400, min(3600, 3600 - (avg_cpl * 20))))
496
+
497
+ return AnalyzeResponse(
498
+ accuracy=round(accuracy, 1),
499
+ estimated_elo=estimated_elo,
500
+ moves=analysis_results,
501
+ counts=counts
502
+ )
 
 
 
 
 
 
 
503
  except Exception as e:
504
  print(f"Analysis Error: {e}")
505
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
506
 
507
 
508
  if __name__ == "__main__":