# 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()