Spaces:
Running
Running
Update main.py
Browse files
main.py
CHANGED
|
@@ -9,8 +9,18 @@ import chess.engine
|
|
| 9 |
import asyncio
|
| 10 |
import json
|
| 11 |
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
from contextlib import asynccontextmanager
|
|
|
|
|
|
|
| 14 |
class ConnectionManager:
|
| 15 |
def __init__(self):
|
| 16 |
# match_id -> list of websockets
|
|
@@ -133,6 +143,8 @@ class EnginePool:
|
|
| 133 |
await self.engines.put(engine)
|
| 134 |
self.all_engines.append(engine)
|
| 135 |
print(f" [+] Engine {i+1}/{self.size} ready.")
|
|
|
|
|
|
|
| 136 |
except Exception as e:
|
| 137 |
print(f" [!] Failed to start engine {i+1}: {e}")
|
| 138 |
|
|
@@ -169,15 +181,7 @@ class EnginePool:
|
|
| 169 |
except:
|
| 170 |
pass
|
| 171 |
|
| 172 |
-
pool = EnginePool(size=
|
| 173 |
-
|
| 174 |
-
@app.on_event("startup")
|
| 175 |
-
async def startup_event():
|
| 176 |
-
await pool.start()
|
| 177 |
-
|
| 178 |
-
@app.on_event("shutdown")
|
| 179 |
-
async def shutdown_event():
|
| 180 |
-
await pool.stop()
|
| 181 |
|
| 182 |
def get_normalized_score(info) -> tuple[float, Optional[int]]:
|
| 183 |
"""Returns the score from White's perspective in centipawns."""
|
|
@@ -257,6 +261,16 @@ def get_win_percentage_from_cp(cp: int) -> float:
|
|
| 257 |
win_chances = 2.0 / (1.0 + math.exp(MULTIPLIER * cp_ceiled)) - 1.0
|
| 258 |
return 50.0 + 50.0 * win_chances
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
def get_win_percentage(info: dict) -> float:
|
| 261 |
score = info.get("score")
|
| 262 |
if not score:
|
|
@@ -398,8 +412,8 @@ async def analyze_game(request: AnalyzeRequest):
|
|
| 398 |
player_is_white = (request.player_color.lower() == "white")
|
| 399 |
fen_history = [board.fen()]
|
| 400 |
move_history = []
|
| 401 |
-
|
| 402 |
-
|
| 403 |
current_score, _ = get_normalized_score(infos_before[0])
|
| 404 |
|
| 405 |
for i, san_move in enumerate(request.moves):
|
|
@@ -467,12 +481,14 @@ async def analyze_game(request: AnalyzeRequest):
|
|
| 467 |
)
|
| 468 |
|
| 469 |
move_gain = score_after - score_before if is_white_turn else score_before - score_after
|
| 470 |
-
cpl = max(0, -move_gain)
|
| 471 |
-
|
| 472 |
-
|
|
|
|
|
|
|
| 473 |
if is_player_turn:
|
| 474 |
-
|
| 475 |
-
|
| 476 |
counts[cls] = counts.get(cls, 0) + 1
|
| 477 |
|
| 478 |
analysis_results.append(MoveAnalysis(
|
|
@@ -488,9 +504,18 @@ async def analyze_game(request: AnalyzeRequest):
|
|
| 488 |
))
|
| 489 |
infos_before = infos_after
|
| 490 |
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
|
| 495 |
return AnalyzeResponse(
|
| 496 |
accuracy=round(accuracy, 1),
|
|
@@ -505,4 +530,5 @@ async def analyze_game(request: AnalyzeRequest):
|
|
| 505 |
|
| 506 |
if __name__ == "__main__":
|
| 507 |
import uvicorn
|
|
|
|
| 508 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|
|
|
|
| 9 |
import asyncio
|
| 10 |
import json
|
| 11 |
|
| 12 |
+
@asynccontextmanager
|
| 13 |
+
async def lifespan(app: FastAPI):
|
| 14 |
+
# Startup: Initialize the engine pool
|
| 15 |
+
await pool.start()
|
| 16 |
+
yield
|
| 17 |
+
# Shutdown: Clean up the engine pool
|
| 18 |
+
await pool.stop()
|
| 19 |
+
|
| 20 |
+
app = FastAPI(title="Deepcastle Engine API", lifespan=lifespan)
|
| 21 |
from contextlib import asynccontextmanager
|
| 22 |
+
|
| 23 |
+
# βββ Multiplaying / Challenge Manager ββββββββββββββββββββββββββββββββββββββββββ
|
| 24 |
class ConnectionManager:
|
| 25 |
def __init__(self):
|
| 26 |
# match_id -> list of websockets
|
|
|
|
| 143 |
await self.engines.put(engine)
|
| 144 |
self.all_engines.append(engine)
|
| 145 |
print(f" [+] Engine {i+1}/{self.size} ready.")
|
| 146 |
+
# Give the system some room to breathe between processes
|
| 147 |
+
await asyncio.sleep(0.5)
|
| 148 |
except Exception as e:
|
| 149 |
print(f" [!] Failed to start engine {i+1}: {e}")
|
| 150 |
|
|
|
|
| 181 |
except:
|
| 182 |
pass
|
| 183 |
|
| 184 |
+
pool = EnginePool(size=4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 185 |
|
| 186 |
def get_normalized_score(info) -> tuple[float, Optional[int]]:
|
| 187 |
"""Returns the score from White's perspective in centipawns."""
|
|
|
|
| 261 |
win_chances = 2.0 / (1.0 + math.exp(MULTIPLIER * cp_ceiled)) - 1.0
|
| 262 |
return 50.0 + 50.0 * win_chances
|
| 263 |
|
| 264 |
+
def get_move_accuracy(win_pct_before: float, win_pct_after: float, is_white_move: bool) -> float:
|
| 265 |
+
"""Lichess-style win%-based per-move accuracy (0β100)."""
|
| 266 |
+
if is_white_move:
|
| 267 |
+
diff = win_pct_before - win_pct_after
|
| 268 |
+
else:
|
| 269 |
+
diff = (100.0 - win_pct_before) - (100.0 - win_pct_after)
|
| 270 |
+
|
| 271 |
+
accuracy = 103.1668 * math.exp(-0.04354 * max(0.0, diff)) - 3.1669
|
| 272 |
+
return max(0.0, min(100.0, accuracy))
|
| 273 |
+
|
| 274 |
def get_win_percentage(info: dict) -> float:
|
| 275 |
score = info.get("score")
|
| 276 |
if not score:
|
|
|
|
| 412 |
player_is_white = (request.player_color.lower() == "white")
|
| 413 |
fen_history = [board.fen()]
|
| 414 |
move_history = []
|
| 415 |
+
player_move_accuracies: List[float] = []
|
| 416 |
+
player_cpls: List[float] = [] # keep for estimated_elo
|
| 417 |
current_score, _ = get_normalized_score(infos_before[0])
|
| 418 |
|
| 419 |
for i, san_move in enumerate(request.moves):
|
|
|
|
| 481 |
)
|
| 482 |
|
| 483 |
move_gain = score_after - score_before if is_white_turn else score_before - score_after
|
| 484 |
+
cpl = max(0.0, min(1000.0, -move_gain))
|
| 485 |
+
|
| 486 |
+
# Lichess-style per-move accuracy using win%
|
| 487 |
+
move_acc = get_move_accuracy(win_pct_before, win_pct_after, is_white_turn)
|
| 488 |
+
|
| 489 |
if is_player_turn:
|
| 490 |
+
player_move_accuracies.append(move_acc)
|
| 491 |
+
player_cpls.append(cpl)
|
| 492 |
counts[cls] = counts.get(cls, 0) + 1
|
| 493 |
|
| 494 |
analysis_results.append(MoveAnalysis(
|
|
|
|
| 504 |
))
|
| 505 |
infos_before = infos_after
|
| 506 |
|
| 507 |
+
# NEW β Lichess win%-based accuracy
|
| 508 |
+
if player_move_accuracies:
|
| 509 |
+
# Lichess uses harmonic mean blended with arithmetic mean
|
| 510 |
+
arithmetic_mean = sum(player_move_accuracies) / len(player_move_accuracies)
|
| 511 |
+
harmonic_mean = len(player_move_accuracies) / sum(1.0 / max(a, 0.1) for a in player_move_accuracies)
|
| 512 |
+
accuracy = (arithmetic_mean + harmonic_mean) / 2.0
|
| 513 |
+
else:
|
| 514 |
+
accuracy = 0.0
|
| 515 |
+
|
| 516 |
+
# Elo from avg CPL using exponential decay calibrated to your 3600 engine
|
| 517 |
+
avg_cpl = sum(player_cpls) / max(1, len(player_cpls))
|
| 518 |
+
estimated_elo = int(max(400, min(3600, round(3600 * math.exp(-0.01 * avg_cpl)))))
|
| 519 |
|
| 520 |
return AnalyzeResponse(
|
| 521 |
accuracy=round(accuracy, 1),
|
|
|
|
| 530 |
|
| 531 |
if __name__ == "__main__":
|
| 532 |
import uvicorn
|
| 533 |
+
# Hugging Face Spaces port is 7860
|
| 534 |
uvicorn.run(app, host="0.0.0.0", port=7860)
|