Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 79 additions & 31 deletions src/engine/stockfish_service.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from __future__ import annotations

from pathlib import Path
from typing import Any
from typing import Any, Callable

import chess
import chess.engine
Expand Down Expand Up @@ -84,6 +84,68 @@ def stop(self) -> None:
finally:
self.engine = None

def restart(self) -> None:
"""Restart Stockfish process setelah engine mati/error."""
self.stop()
self.start()

def _recover_engine_after_failure(
self,
) -> None:
"""Buang handle engine lama dan coba start engine baru."""
try:
self.stop()
finally:
self.start()

def _with_engine_recovery(
self,
action: Callable[[], Any],
*,
context: str,
) -> Any:
"""
Jalankan action dengan retry satu kali jika event loop/proses engine mati.

Kadang setelah pipeline berat/self-play banyak Stockfish process, handle
engine UI bisa berada dalam state "engine event loop dead". Tanpa reset,
semua langkah bot berikutnya gagal terus sampai app direstart.
"""
self._ensure_started()

try:
return action()

except (
TimeoutError,
chess.engine.EngineError,
chess.engine.EngineTerminatedError,
) as first_exc:
first_message = str(first_exc).lower()
should_retry = (
isinstance(
first_exc,
chess.engine.EngineTerminatedError,
)
or "event loop dead" in first_message
or "engine process died" in first_message
or "engine terminated" in first_message
)

if not should_retry:
raise StockfishServiceError(
f"{context}: {first_exc}"
) from first_exc

try:
self._recover_engine_after_failure()
return action()

except Exception as second_exc:
raise StockfishServiceError(
f"{context}: {first_exc}; restart gagal: {second_exc}"
) from second_exc

def get_engine_name(self) -> str:
"""Mengambil nama engine yang sedang aktif."""
self._ensure_started()
Expand All @@ -109,10 +171,6 @@ def analyze(
- positif: putih unggul
- negatif: hitam unggul
"""
self._ensure_started()

assert self.engine is not None

if time_limit <= 0:
raise ValueError(
"Waktu analisis harus lebih besar dari 0."
Expand All @@ -123,23 +181,20 @@ def analyze(
"Jumlah kandidat harus lebih besar dari 0."
)

try:
analysis = self.engine.analyse(
def run_analysis() -> Any:
assert self.engine is not None
return self.engine.analyse(
board,
chess.engine.Limit(
time=time_limit,
),
multipv=multipv,
)

except (
TimeoutError,
chess.engine.EngineError,
chess.engine.EngineTerminatedError,
) as exc:
raise StockfishServiceError(
f"Analisis Stockfish gagal: {exc}"
) from exc
analysis = self._with_engine_recovery(
run_analysis,
context="Analisis Stockfish gagal",
)

if isinstance(analysis, dict):
analysis = [analysis]
Expand Down Expand Up @@ -195,10 +250,6 @@ def evaluate_move(
Digunakan ketika langkah aktual pemain tidak masuk
ke daftar kandidat MultiPV.
"""
self._ensure_started()

assert self.engine is not None

if move not in board.legal_moves:
raise ValueError(
f"Langkah tidak legal pada posisi ini: {move.uci()}"
Expand All @@ -209,23 +260,20 @@ def evaluate_move(
"Waktu analisis harus lebih besar dari 0."
)

try:
info = self.engine.analyse(
def run_analysis() -> Any:
assert self.engine is not None
return self.engine.analyse(
board,
chess.engine.Limit(
time=time_limit,
),
root_moves=[move],
)

except (
TimeoutError,
chess.engine.EngineError,
chess.engine.EngineTerminatedError,
) as exc:
raise StockfishServiceError(
f"Evaluasi langkah gagal: {exc}"
) from exc
info = self._with_engine_recovery(
run_analysis,
context="Evaluasi langkah gagal",
)

score = info["score"].pov(
chess.WHITE
Expand Down Expand Up @@ -285,7 +333,7 @@ def _ensure_started(self) -> None:
"Panggil start() terlebih dahulu."
)

def __enter__(self) -> StockfishService:
def __enter__(self) -> "StockfishService":
"""Menjalankan engine ketika masuk context manager."""
self.start()
return self
Expand All @@ -297,4 +345,4 @@ def __exit__(
traceback: object,
) -> None:
"""Menghentikan engine ketika keluar context manager."""
self.stop()
self.stop()
83 changes: 79 additions & 4 deletions src/ui/main_window_parts/persona.py
Original file line number Diff line number Diff line change
Expand Up @@ -318,6 +318,7 @@ def _load_persona(

self._update_control_states()


def _start_persona_creation(
self,
) -> None:
Expand Down Expand Up @@ -351,7 +352,7 @@ def _start_persona_creation(

preset_key = (
self.analysis_preset_combo.currentData()
or "quick"
or "full"
)

preset_key_text = str(
Expand All @@ -362,6 +363,26 @@ def _start_persona_creation(
preset_key_text
]

selected_time_class = (
self.time_class_combo.currentText()
or getattr(
self,
"time_class",
"rapid",
)
or "rapid"
)

selected_time_class = str(
selected_time_class
).strip().lower()

if selected_time_class not in {
"rapid",
"blitz",
}:
selected_time_class = "rapid"

self.persona_build_busy = True

self.chess_board.set_interactive(
Expand All @@ -370,13 +391,21 @@ def _start_persona_creation(

self.persona_dialog = PersonaProgressDialog(
username=username,
preset_label=preset.label,
preset_label=(
f"{preset.label} — Full runtime pipeline"
),
parent=self,
)

self.persona_thread = PersonaCreationThread(
username=username,
preset_key=preset_key_text,
time_class=selected_time_class,
engine_path=getattr(
self.engine,
"engine_path",
"engines/stockfish/stockfish.exe",
),
parent=self,
)

Expand Down Expand Up @@ -410,6 +439,7 @@ def _start_persona_creation(

self.persona_thread.start()


def _on_persona_creation_completed(
self,
result: object,
Expand All @@ -430,10 +460,55 @@ def _on_persona_creation_completed(
)
)

readiness_ready = bool(
getattr(
result,
"readiness_ready",
False,
)
)

recommended_policy = str(
getattr(
result,
"recommended_policy",
"",
)
or ""
)

warning = str(
getattr(
result,
"warning",
"",
)
or ""
)

if self.persona_dialog is not None:
if readiness_ready:
final_message = (
f"Full pipeline selesai: {username} — "
f"{', '.join(completed_modes)}. "
f"Runtime READY: {recommended_policy}"
)
else:
final_message = (
f"Persona {username} selesai: "
f"{', '.join(completed_modes)}. "
"Runtime belum ready; UI akan fallback aman."
)

if warning:
final_message = (
final_message
+ "\n"
+ warning
)

self.persona_dialog.mark_complete(
f"Persona {username} selesai: "
f"{', '.join(completed_modes)}"
final_message
)

self.persona_build_busy = False
Expand Down
Loading