Amogh1221 commited on
Commit
3ed6de8
Β·
1 Parent(s): dc7e9f2

Update main.py

Browse files
Files changed (1) hide show
  1. main.py +146 -32
main.py CHANGED
@@ -70,8 +70,13 @@ app.add_middleware(
70
  )
71
 
72
  # Paths relative to the Docker container
73
- ENGINE_PATH = os.environ.get("ENGINE_PATH", "/app/engine/deepcastle")
74
- NNUE_PATH = os.environ.get("NNUE_PATH", "/app/engine/output.nnue")
 
 
 
 
 
75
 
76
  class MoveRequest(BaseModel):
77
  fen: str
@@ -116,21 +121,92 @@ def home():
116
 
117
  @app.get("/health")
118
  def health():
119
- if not os.path.exists(ENGINE_PATH):
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."""
@@ -140,19 +216,21 @@ def get_normalized_score(info) -> tuple[float, Optional[int]]:
140
  if raw.is_mate():
141
  m = raw.mate() or 0
142
  return (10000.0 if m > 0 else -10000.0), m
143
- return raw.score() or 0.0, None
144
 
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)
@@ -194,12 +272,54 @@ async def get_move(request: MoveRequest):
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
@@ -347,7 +467,7 @@ def get_move_classification(
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
 
@@ -483,12 +603,6 @@ async def analyze_game(request: AnalyzeRequest):
483
  except Exception as e:
484
  print(f"Analysis Error: {e}")
485
  raise HTTPException(status_code=500, detail=str(e))
486
- finally:
487
- if engine:
488
- try:
489
- await engine.quit()
490
- except Exception:
491
- pass
492
 
493
 
494
  if __name__ == "__main__":
 
70
  )
71
 
72
  # Paths relative to the Docker container
73
+ DEEPCASTLE_ENGINE_PATH = os.environ.get(
74
+ "DEEPCASTLE_ENGINE_PATH",
75
+ os.environ.get("ENGINE_PATH", "/app/engine_bin/deepcastle"),
76
+ )
77
+ STOCKFISH_ENGINE_PATH = os.environ.get("STOCKFISH_ENGINE_PATH", "/usr/games/stockfish")
78
+ NNUE_PATH = os.environ.get("NNUE_PATH", "/app/engine_bin/output.nnue")
79
+ NNUE_SMALL_PATH = os.environ.get("NNUE_SMALL_PATH", "/app/engine_bin/small_output.nnue")
80
 
81
  class MoveRequest(BaseModel):
82
  fen: str
 
121
 
122
  @app.get("/health")
123
  def health():
124
+ missing = []
125
+ if not os.path.exists(DEEPCASTLE_ENGINE_PATH):
126
+ missing.append("deepcastle")
127
+ if not os.path.exists(STOCKFISH_ENGINE_PATH):
128
+ missing.append("stockfish")
129
+ if missing:
130
+ return {"status": "error", "message": f"Missing engine binary: {', '.join(missing)}"}
131
+ return {"status": "ok", "engines": ["deepcastle", "stockfish"]}
132
+
133
+ # Global engine instances to save memory and improve performance
134
+ _GLOBAL_DEEPCASTLE_ENGINE = None
135
+ _GLOBAL_STOCKFISH_ENGINE = None
136
+
137
+ async def _get_or_start_engine(engine_path: str, *, role: str, options: Optional[dict] = None):
138
+ global _GLOBAL_DEEPCASTLE_ENGINE, _GLOBAL_STOCKFISH_ENGINE
139
+
140
+ current_engine = _GLOBAL_DEEPCASTLE_ENGINE if role == "deepcastle" else _GLOBAL_STOCKFISH_ENGINE
141
+ if current_engine is not None:
142
  try:
143
+ if not current_engine.is_terminated():
144
+ return current_engine
145
  except Exception:
146
+ if role == "deepcastle":
147
+ _GLOBAL_DEEPCASTLE_ENGINE = None
148
+ else:
149
+ _GLOBAL_STOCKFISH_ENGINE = None
150
+
151
+ if not os.path.exists(engine_path):
152
+ raise HTTPException(status_code=500, detail=f"{role} binary NOT FOUND at {engine_path}")
153
+
154
+ print(f"[DEBUG] Attempting to start {role} engine at {engine_path}")
155
+ try:
156
+ transport, engine = await chess.engine.popen_uci(engine_path)
157
+ print(f"[DEBUG] {role} process started. ID: {transport.get_pid()}")
158
+
159
+ if options:
160
+ await engine.configure(options)
161
+
162
+ if role == "deepcastle":
163
+ if os.path.exists(NNUE_PATH):
164
+ try:
165
+ await engine.configure({"EvalFile": NNUE_PATH})
166
+ print("[DEBUG] DeepCastle big net loaded successfully.")
167
+ except Exception as ne:
168
+ print(f"[ERROR] DeepCastle big net load failed: {str(ne)}")
169
+ else:
170
+ print(f"[WARNING] DeepCastle big net not found at {NNUE_PATH}")
171
+
172
+ if os.path.exists(NNUE_SMALL_PATH):
173
+ try:
174
+ await engine.configure({"EvalFileSmall": NNUE_SMALL_PATH})
175
+ print("[DEBUG] DeepCastle small net loaded successfully.")
176
+ except Exception as ne:
177
+ print(f"[ERROR] DeepCastle small net load failed: {str(ne)}")
178
+ else:
179
+ print(f"[WARNING] DeepCastle small net not found at {NNUE_SMALL_PATH}")
180
+
181
+ _GLOBAL_DEEPCASTLE_ENGINE = engine
182
+ else:
183
+ _GLOBAL_STOCKFISH_ENGINE = engine
184
+
185
+ return engine
186
+ except Exception as e:
187
+ print(f"[CRITICAL] {role} failed to start: {str(e)}")
188
+ # Try to gather more info by running the binary directly briefly
189
+ import subprocess
190
+ try:
191
+ diag = subprocess.run([engine_path, "uci"], capture_output=True, text=True, timeout=2)
192
+ print(f"[DIAG] {role} output: {diag.stdout} | Error: {diag.stderr}")
193
+ except Exception as de:
194
+ print(f"[DIAG] Could not run diagnosis: {str(de)}")
195
+ raise HTTPException(status_code=500, detail=f"{role} crash: {str(e)}")
196
+
197
+ async def get_deepcastle_engine():
198
+ return await _get_or_start_engine(
199
+ DEEPCASTLE_ENGINE_PATH,
200
+ role="deepcastle",
201
+ options={"Hash": 128, "Threads": 1},
202
+ )
203
+
204
+ async def get_stockfish_engine():
205
+ return await _get_or_start_engine(
206
+ STOCKFISH_ENGINE_PATH,
207
+ role="stockfish",
208
+ options={"Hash": 128, "Threads": 1},
209
+ )
210
 
211
  def get_normalized_score(info) -> tuple[float, Optional[int]]:
212
  """Returns the score from White's perspective in centipawns."""
 
216
  if raw.is_mate():
217
  m = raw.mate() or 0
218
  return (10000.0 if m > 0 else -10000.0), m
219
+ return float(raw.score() or 0.0), None
220
 
221
  # ─── Engine Inference Route ────────────────────────────────────────────────────
222
  @app.post("/move", response_model=MoveResponse)
223
  async def get_move(request: MoveRequest):
 
224
  try:
225
+ engine = await get_deepcastle_engine()
226
  board = chess.Board(request.fen)
227
  limit = chess.engine.Limit(time=request.time, depth=request.depth)
228
 
229
+ # Search for best move
230
  result = await engine.play(board, limit)
231
+
232
+ # Get evaluation separately to avoid blocking
233
+ info = await engine.analyse(board, chess.engine.Limit(time=0.1, depth=limit.depth or 12))
234
 
235
  # From White's perspective in CP -> converted to Pawns for UI
236
  score_cp, mate_in = get_normalized_score(info)
 
272
  except Exception as e:
273
  print(f"Error: {e}")
274
  raise HTTPException(status_code=500, detail=str(e))
275
+
276
+ @app.post("/analysis-move", response_model=MoveResponse)
277
+ async def get_analysis_move(request: MoveRequest):
278
+ try:
279
+ engine = await get_stockfish_engine()
280
+ board = chess.Board(request.fen)
281
+ limit = chess.engine.Limit(time=request.time, depth=request.depth)
282
+
283
+ result = await engine.play(board, limit)
284
+ info = await engine.analyse(board, chess.engine.Limit(time=0.1, depth=limit.depth or 12))
285
+
286
+ score_cp, mate_in = get_normalized_score(info)
287
+
288
+ depth = info.get("depth", 0)
289
+ nodes = info.get("nodes", 0)
290
+ nps = info.get("nps", 0)
291
+
292
+ pv_board = board.copy()
293
+ pv_parts = []
294
+ for m in info.get("pv", [])[:5]:
295
+ if m in pv_board.legal_moves:
296
+ try:
297
+ pv_parts.append(pv_board.san(m))
298
+ pv_board.push(m)
299
+ except Exception:
300
+ break
301
+ else:
302
+ break
303
+ pv = " ".join(pv_parts)
304
+
305
+ score_pawns = score_cp / 100.0 if abs(score_cp) < 9900 else (100.0 if score_cp > 0 else -100.0)
306
+
307
+ board_fen_only = board.fen().split(" ")[0]
308
+ opening_name = openings_db.get(board_fen_only)
309
+
310
+ return MoveResponse(
311
+ bestmove=result.move.uci(),
312
+ score=score_pawns,
313
+ depth=depth,
314
+ nodes=nodes,
315
+ nps=nps,
316
+ pv=pv,
317
+ mate_in=mate_in,
318
+ opening=opening_name
319
+ )
320
+ except Exception as e:
321
+ print(f"Analysis move error: {e}")
322
+ raise HTTPException(status_code=500, detail=str(e))
323
 
324
 
325
  import math
 
467
  async def analyze_game(request: AnalyzeRequest):
468
  engine = None
469
  try:
470
+ engine = await get_stockfish_engine()
471
  board = chess.Board(request.start_fen) if request.start_fen else chess.Board()
472
  limit = chess.engine.Limit(time=request.time_per_move)
473
 
 
603
  except Exception as e:
604
  print(f"Analysis Error: {e}")
605
  raise HTTPException(status_code=500, detail=str(e))
 
 
 
 
 
 
606
 
607
 
608
  if __name__ == "__main__":