#!/usr/bin/env python3 """ Lokaler Server für Lara Schachturnier Serviert statische Dateien und bietet Stockfish-Analyse. """ import http.server import socketserver import os import subprocess import json import re import sys import threading import socket import queue PORT = int(os.environ.get("PORT", 8111)) BASE_DIR = os.path.dirname(os.path.abspath(__file__)) STOCKFISH_PATH = os.environ.get("STOCKFISH_PATH") if not STOCKFISH_PATH: win_path = os.path.join(BASE_DIR, "stockfish.exe") if os.path.exists(win_path): STOCKFISH_PATH = win_path else: STOCKFISH_PATH = "stockfish" STOCKFISH_DEPTH = int(os.environ.get("STOCKFISH_DEPTH", 25)) _stockfish_lock = threading.Lock() class StockfishEngine: def __init__(self, path): self.path = path self.proc = None self._output_queue = queue.Queue() self._reader_thread = None self._reader_alive = threading.Event() self._cmd_lock = threading.Lock() self._searching = False def start(self): if self.proc: return flags = 0 if sys.platform == "win32": flags = subprocess.CREATE_NO_WINDOW self.proc = subprocess.Popen( [self.path], stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1, creationflags=flags, ) self._reader_alive.set() self._reader_thread = threading.Thread(target=self._read_output, daemon=True) self._reader_thread.start() self._send("uci") self._read_until("uciok") self._send("isready") self._read_until("readyok") print(f"[STOCKFISH] Engine gestartet: {self.path}") def _read_output(self): while self._reader_alive.is_set(): try: line = self.proc.stdout.readline() except Exception: break if not line: break self._output_queue.put(line) def _send(self, cmd): with self._cmd_lock: self.proc.stdin.write(cmd + "\n") self.proc.stdin.flush() def _read_until(self, marker): while True: try: line = self._output_queue.get(timeout=5.0) except queue.Empty: return if line.strip() == marker: return def _drain_queue(self): """Leert die Queue von allen pending Zeilen.""" drained = 0 while True: try: self._output_queue.get_nowait() drained += 1 except queue.Empty: break if drained > 0: print(f"[STOCKFISH] {drained} alte Zeilen verworfen") def _stop_and_wait(self): """Stoppt laufende Suche und wartet auf bestmove.""" if not self._searching: return self._send("stop") while True: try: line = self._output_queue.get(timeout=3.0).strip() except queue.Empty: # Stockfish antwortet nicht auf stop (z.B. weil Suche schon beendet) self._searching = False return if line.startswith("bestmove"): self._searching = False return def evaluate(self, fen): # Alte Suche abbrechen und Queue leeren self._stop_and_wait() self._drain_queue() self._send(f"position fen {fen}") self._send(f"go depth {STOCKFISH_DEPTH} movetime 15000") self._searching = True score_cp = None score_mate = None bestmove = None pv = None last_depth = 0 while True: try: line = self._output_queue.get(timeout=1.0).strip() except queue.Empty: continue if not line: continue if "score cp" in line: m = re.search(r"score cp (-?\d+)", line) if m: score_cp = int(m.group(1)) if "score mate" in line: m = re.search(r"score mate (-?\d+)", line) if m: score_mate = int(m.group(1)) if " pv " in line: pv = line.split(" pv ", 1)[1] depth_m = re.search(r"depth (\d+)", line) if depth_m and score_cp is not None: new_depth = int(depth_m.group(1)) if new_depth != last_depth: last_depth = new_depth yield { "scoreCp": score_cp, "scoreMate": score_mate, "bestMove": None, "pv": pv, "depth": new_depth, } if line.startswith("bestmove"): parts = line.split() bestmove = parts[1] if len(parts) > 1 else None self._searching = False break yield { "scoreCp": score_cp, "scoreMate": score_mate, "bestMove": bestmove, "pv": pv, } def stop(self): self._reader_alive.clear() if self.proc: self.proc.terminate() self.proc = None _engine = StockfishEngine(STOCKFISH_PATH) class Handler(http.server.BaseHTTPRequestHandler): protocol_version = "HTTP/1.1" def do_GET(self): if self.path == "/": self.path = "/index.html" filepath = os.path.normpath(os.path.join(BASE_DIR, self.path.lstrip("/"))) if not filepath.startswith(BASE_DIR): self.send_response(403) self.end_headers() return if os.path.isfile(filepath): content_types = { ".html": "text/html", ".css": "text/css", ".js": "application/javascript", ".json": "application/json", ".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", } ext = os.path.splitext(filepath)[1] content_type = content_types.get(ext, "application/octet-stream") with open(filepath, "rb") as f: content = f.read() self.send_response(200) self.send_header("Content-Type", f"{content_type}; charset=utf-8") self.send_header("Content-Length", str(len(content))) self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Cache-Control", "no-cache") self.end_headers() self.wfile.write(content) else: self.send_response(404) self.end_headers() def do_POST(self): if self.path == "/evaluate": length = int(self.headers.get("Content-Length", 0)) body = self.rfile.read(length) if length else b"{}" try: data = json.loads(body) fen = data.get("fen", "") except (json.JSONDecodeError, TypeError): self._send_json({"error": "Invalid JSON"}, 400) return if not fen: self._send_json({"error": "Missing fen"}, 400) return self.send_response(200) self.send_header("Content-Type", "application/x-ndjson; charset=utf-8") self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Cache-Control", "no-cache") self.send_header("Transfer-Encoding", "chunked") self.end_headers() self.wfile.flush() # TCP_NODELAY für sofortiges Senden try: self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) except Exception: pass def _write_chunk(chunk_data): size = len(chunk_data) self.wfile.write(f"{size:x}\r\n".encode()) self.wfile.write(chunk_data) self.wfile.write(b"\r\n") self.wfile.flush() try: with _stockfish_lock: try: _engine.start() except FileNotFoundError: _write_chunk( json.dumps({"error": "Stockfish nicht gefunden"}).encode("utf-8") ) return for result in _engine.evaluate(fen): try: data = json.dumps(result).encode("utf-8") + b"\n" _write_chunk(data) except (BrokenPipeError, ConnectionResetError, ConnectionAbortedError, OSError): # Client hat Verbindung getrennt (neuer Zug angeklickt / Pfeiltasten) break # Abschluss-Chunk _write_chunk(b"") except Exception as e: print(f"[STOCKFISH] Fehler: {e}") try: _write_chunk( json.dumps({"error": str(e)}).encode("utf-8") ) except Exception: pass else: self._send_json({"error": "Not found"}, 404) def _send_json(self, obj, status=200): self.send_response(status) self.send_header("Content-Type", "application/json; charset=utf-8") self.send_header("Access-Control-Allow-Origin", "*") self.send_header("Cache-Control", "no-cache") self.end_headers() self.wfile.write(json.dumps(obj).encode("utf-8")) def log_message(self, format, *args): print(f"[{self.log_date_time_string()}] {args[0]}") def main(): print("=" * 50) print(" Lara Kiesewetter - Live Schachturnier") print("=" * 50) print(f" Server laeuft auf: http://localhost:{PORT}") if os.path.exists(STOCKFISH_PATH) or STOCKFISH_PATH == "stockfish": print(f" Stockfish-Analyse aktiv (depth={STOCKFISH_DEPTH})") else: print(f" Stockfish nicht gefunden unter: {STOCKFISH_PATH}") print(f" Druecke Ctrl+C zum Beenden") print("=" * 50) socketserver.ThreadingTCPServer.allow_reuse_address = True with socketserver.ThreadingTCPServer(("", PORT), Handler) as httpd: print(f"\n[SERVER] Bereit für Anfragen\n") try: httpd.serve_forever() except KeyboardInterrupt: print("\n[BYE] Server gestoppt.") _engine.stop() if __name__ == "__main__": main()