Marcel0123's picture
Upload app.py
3a9b614 verified
# 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()