Spaces:
Sleeping
Sleeping
| # app.py | |
| import os | |
| import shutil | |
| import gradio as gr | |
| import chess | |
| import chess.svg | |
| import chess.engine | |
| # ---------- Engine discovery ---------- | |
| def detect_stockfish_candidates(): | |
| cands = [] | |
| # env var | |
| env = os.environ.get("STOCKFISH_PATH") | |
| if env: | |
| cands.append(env) | |
| # PATH | |
| which = shutil.which("stockfish") | |
| if which: | |
| cands.append(which) | |
| # common paths | |
| cands += ["/usr/bin/stockfish", "/usr/games/stockfish", "/bin/stockfish", "/opt/conda/bin/stockfish"] | |
| # de-dup while preserving order | |
| seen = set(); uniq = [] | |
| for p in cands: | |
| if p and p not in seen: | |
| uniq.append(p); seen.add(p) | |
| return uniq | |
| def choose_stockfish_path(manual_path: str | None = None): | |
| """Pick first executable path from manual path or detected candidates.""" | |
| if manual_path: | |
| if os.path.isfile(manual_path) and os.access(manual_path, os.X_OK): | |
| return manual_path | |
| for p in detect_stockfish_candidates(): | |
| if os.path.isfile(p) and os.access(p, os.X_OK): | |
| return p | |
| return None | |
| DEFAULT_THREADS = max(1, os.cpu_count() or 2) | |
| DEFAULT_HASH_MB = 512 | |
| DEFAULT_MOVETIME_MS = 1500 | |
| def board_svg(board: chess.Board, last_move=None) -> str: | |
| return chess.svg.board( | |
| board=board, | |
| lastmove=last_move, | |
| check=board.is_check(), | |
| coordinates=True, | |
| size=520, | |
| ) | |
| def new_engine(path: str, threads=DEFAULT_THREADS, hash_mb=DEFAULT_HASH_MB, ponder=False): | |
| if not path: | |
| raise FileNotFoundError("Geen Stockfish-pad opgegeven.") | |
| if not (os.path.isfile(path) and os.access(path, os.X_OK)): | |
| raise FileNotFoundError(f"Stockfish niet uitvoerbaar op pad: {path}") | |
| engine = chess.engine.SimpleEngine.popen_uci(path) | |
| opts = { | |
| "Threads": int(threads), | |
| "Hash": int(hash_mb), | |
| "UCI_LimitStrength": False, | |
| "Skill Level": 20, | |
| "Ponder": ponder, | |
| "Move Overhead": 30, | |
| } | |
| try: | |
| engine.configure(opts) | |
| except Exception as e: | |
| print("UCI-opties zetten gaf een fout (doorgaan):", e) | |
| return engine | |
| def init_state(engine_color: str, movetime_ms: int, depth: int | None, threads: int, hash_mb: int, engine_path: str | None): | |
| board = chess.Board() | |
| # Probeer engine te starten, maar laat UI werken zonder crash | |
| engine = None | |
| picked = choose_stockfish_path(engine_path) | |
| error = None | |
| if picked: | |
| try: | |
| engine = new_engine(picked, threads=threads, hash_mb=hash_mb) | |
| except Exception as e: | |
| error = str(e) | |
| state = { | |
| "board": board, | |
| "engine": engine, | |
| "engine_color": engine_color, | |
| "movetime_ms": movetime_ms, | |
| "depth": depth if depth and depth > 0 else None, | |
| "last_move": None, | |
| "engine_path": picked or (engine_path or ""), | |
| "engine_error": error, | |
| } | |
| # Alleen automatisch zetten als engine beschikbaar is én engine wit is | |
| if state["engine"] is not None and engine_color == "white": | |
| _ = engine_move(state) | |
| return state | |
| def engine_limit(state): | |
| if state["depth"] is not None: | |
| return chess.engine.Limit(depth=int(state["depth"])) | |
| return chess.engine.Limit(time=max(0.05, state["movetime_ms"]/1000.0)) | |
| def render(state): | |
| svg = board_svg(state["board"], last_move=state["last_move"]) | |
| return svg, state["board"].fen() | |
| # --------- Actions --------- | |
| def new_game(engine_color, movetime_ms, depth, threads, hash_mb, engine_path): | |
| state = init_state(engine_color, int(movetime_ms), int(depth) if depth else None, int(threads), int(hash_mb), engine_path.strip() or None) | |
| img, fen = render(state) | |
| if state["engine"]: | |
| msg = f"Nieuwe partij gestart. Engine pad: {state['engine_path']}" | |
| else: | |
| diag = diagnostics() # show where we looked | |
| msg = ( | |
| "Nieuwe partij gestart ZONDER engine. " | |
| "Vul het correcte pad in en klik 'Herstart engine'.\n\n" | |
| + diag | |
| ) | |
| return state, img, fen, msg, state["engine_path"] | |
| def diagnostics(): | |
| lines = ["🔎 Stockfish-detectie:"] | |
| for p in detect_stockfish_candidates(): | |
| ok = "✅" if os.path.isfile(p) and os.access(p, os.X_OK) else "❌" | |
| lines.append(f"{ok} {p}") | |
| env = os.environ.get("STOCKFISH_PATH") | |
| if env: | |
| lines.append(f"Env STOCKFISH_PATH={env}") | |
| lines.append("\nTip: zorg dat je in de Space een bestand 'apt.txt' hebt met:\nstockfish\n" | |
| "en rebuild de Space. Of zet Settings → Variables → STOCKFISH_PATH naar bv. /usr/games/stockfish.") | |
| return "\n".join(lines) | |
| def restart_engine(state, threads, hash_mb, engine_path): | |
| # sluit oude engine | |
| try: | |
| if state.get("engine"): | |
| state["engine"].quit() | |
| except Exception: | |
| pass | |
| # start nieuwe | |
| picked = choose_stockfish_path(engine_path.strip() or None) | |
| if not picked: | |
| state["engine"] = None | |
| state["engine_error"] = "Geen uitvoerbare Stockfish gevonden." | |
| msg = "❌ Geen engine gevonden.\n" + diagnostics() | |
| else: | |
| try: | |
| state["engine"] = new_engine(picked, threads=int(threads), hash_mb=int(hash_mb)) | |
| state["engine_error"] = None | |
| state["engine_path"] = picked | |
| msg = f"✅ Engine gestart op: {picked}" | |
| except Exception as e: | |
| state["engine"] = None | |
| state["engine_error"] = str(e) | |
| msg = f"❌ Engine-fout: {e}\n" + diagnostics() | |
| img, fen = render(state) | |
| return state, img, fen, msg, state.get("engine_path","") | |
| def make_move(state, user_move): | |
| board: chess.Board = state["board"] | |
| if board.is_game_over(): | |
| return state, *render(state), f"Partij is al afgelopen: {board.result()}." | |
| try: | |
| mv = chess.Move.from_uci(user_move.strip()) | |
| if mv not in board.legal_moves: | |
| return state, *render(state), "Ongeldige zet (niet legaal in deze stelling)." | |
| board.push(mv) | |
| state["last_move"] = mv | |
| except Exception as e: | |
| return state, *render(state), f"Ongeldige invoer: {e}" | |
| if board.is_game_over(): | |
| img, fen = render(state) | |
| return state, img, fen, f"Partij afgelopen: {board.result()}." | |
| side_to_move = "white" if board.turn == chess.WHITE else "black" | |
| if side_to_move == state["engine_color"]: | |
| if state["engine"] is None: | |
| msg = "Engine niet beschikbaar — vul 'Engine pad' in en klik 'Herstart engine'." | |
| else: | |
| msg = engine_move(state) | |
| else: | |
| msg = "Jij bent aan zet." | |
| img, fen = render(state) | |
| return state, img, fen, msg | |
| def engine_move(state): | |
| board: chess.Board = state["board"] | |
| engine: chess.engine.SimpleEngine = state["engine"] | |
| if engine is None: | |
| return "Engine niet beschikbaar." | |
| if board.is_game_over(): | |
| return f"Partij afgelopen: {board.result()}." | |
| try: | |
| result = engine.play(board, engine_limit(state)) | |
| board.push(result.move) | |
| state["last_move"] = result.move | |
| if board.is_game_over(): | |
| return f"Engine zet {result.move}. Partij afgelopen: {board.result()}." | |
| else: | |
| return f"Engine zet {result.move}. Jij bent aan zet." | |
| except Exception as e: | |
| return f"Engine-fout: {e}" | |
| def undo_move(state): | |
| board: chess.Board = state["board"] | |
| if len(board.move_stack) == 0: | |
| return state, *render(state), "Geen zetten om terug te nemen." | |
| board.pop() | |
| # Neem er eentje extra terug als engine aan zet zou komen | |
| if state["engine_color"] == ("white" if board.turn == chess.WHITE else "black") and len(board.move_stack) > 0: | |
| board.pop() | |
| state["last_move"] = board.move_stack[-1] if board.move_stack else None | |
| return state, *render(state), "Zet(ten) teruggenomen." | |
| def resign(state): | |
| board: chess.Board = state["board"] | |
| outcome = "1-0" if board.turn == chess.BLACK else "0-1" | |
| return state, *render(state), f"Opgegeven. Resultaat: {outcome}" | |
| # ---------- UI ---------- | |
| CSS = """ | |
| #board { display: flex; justify-content: center; } | |
| #board svg { max-width: 100%; height: auto; } | |
| """ | |
| with gr.Blocks(title="Boss Chess (Stockfish)", css=CSS) as demo: | |
| gr.Markdown("# ♟️ Boss Chess (Stockfish)\nSpeel tegen een sterke NNUE-engine (Stockfish) in je browser.") | |
| with gr.Row(): | |
| board_html = gr.HTML(label="Bord", elem_id="board") | |
| fen_out = gr.Textbox(label="FEN", interactive=False) | |
| with gr.Row(): | |
| user_move = gr.Textbox(label="Jouw zet (UCI, bv. e2e4)", placeholder="e2e4", scale=2) | |
| btn_move = gr.Button("Zet uitvoeren", variant="primary") | |
| btn_undo = gr.Button("Undo") | |
| btn_resign = gr.Button("Resign") | |
| status = gr.Markdown("Klaar.") | |
| with gr.Accordion("⚙️ Instellingen", open=False): | |
| engine_color = gr.Radio(choices=["white","black"], value="black", label="Engine kleur") | |
| movetime = gr.Slider(200, 5000, value=DEFAULT_MOVETIME_MS, step=100, label="Engine denktijd (ms/zet)") | |
| depth = gr.Slider(0, 30, value=0, step=1, label="Max diepte (0=uit)") | |
| threads = gr.Slider(1, DEFAULT_THREADS, value=DEFAULT_THREADS, step=1, label="Threads") | |
| hash_mb = gr.Slider(64, 2048, value=DEFAULT_HASH_MB, step=64, label="Hash (MB)") | |
| engine_path = gr.Textbox(label="Engine pad (optioneel, bv. /usr/games/stockfish)", placeholder="/usr/games/stockfish", value="") | |
| btn_new = gr.Button("Nieuwe partij / herstart") | |
| btn_restart = gr.Button("Herstart engine") | |
| state = gr.State() | |
| # Bindings | |
| btn_new.click(new_game, [engine_color, movetime, depth, threads, hash_mb, engine_path], | |
| [state, board_html, fen_out, status, engine_path]) | |
| btn_restart.click(restart_engine, [state, threads, hash_mb, engine_path], | |
| [state, board_html, fen_out, status, engine_path]) | |
| btn_move.click(make_move, [state, user_move], [state, board_html, fen_out, status]) | |
| btn_undo.click(undo_move, [state], [state, board_html, fen_out, status]) | |
| btn_resign.click(resign, [state], [state, board_html, fen_out, status]) | |
| # Auto-start | |
| demo.load(new_game, [engine_color, movetime, depth, threads, hash_mb, engine_path], | |
| [state, board_html, fen_out, status, engine_path]) | |
| if __name__ == "__main__": | |
| demo.launch() | |