From ec1ebaa525c0092c8b8ae9b2a8efa81220afcb67 Mon Sep 17 00:00:00 2001 From: Martin Date: Mon, 25 May 2026 20:09:09 +0200 Subject: [PATCH] Eval-Bar mit Stockfish-Streaming, LESS entfernt, Move-Lock, AGENTS-Regeln --- .gitignore | 1 + AGENTS.md | 4 +- index.html | 19 +- js/app.js | 165 ++++++++++++--- less/style.less | 544 ------------------------------------------------ package.json | 7 +- server.py | 207 +++++++++++++++++- style.css | 60 ++++++ 8 files changed, 412 insertions(+), 595 deletions(-) delete mode 100644 less/style.less diff --git a/.gitignore b/.gitignore index eac2525..5fc1bc0 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ __pycache__/ lara-chess/ node_modules/ package-lock.json +stockfish.exe diff --git a/AGENTS.md b/AGENTS.md index 5f4992b..8252a8b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -3,5 +3,5 @@ - Nach jeder Änderung am Proxy (server.py) oder an den Requests (app.js) muss der Proxy neu gestartet werden: erst alten Prozess killen (`Get-Process -Name python | Stop-Process -Force`), dann unsichtbar starten. - Commits und Push erfolgen nur durch expliziten Befehl des Nutzers. - Der Server (server.py) wird immer unsichtbar gestartet: `Start-Process -NoNewWindow -FilePath "python" -ArgumentList "server.py"` -- **LESS-Workflow**: CSS-Quelldatei ist `style.less`. Bei Serverstart wird automatisch `style.css` via `node build-less.js` kompiliert. Bei manuellen Änderungen an `style.less`: `node build-less.js` ausführen, dann Server neustarten. -- `style.css` ist auto-generiert – keine manuellen Änderungen dort vornehmen. +- Bei Updates (Polling/Code-Änderungen) niemals den aktuell angezeigten Zug wechseln. Der Benutzer bleibt auf dem von ihm ausgewählten Zug. + diff --git a/index.html b/index.html index 061603b..d774443 100644 --- a/index.html +++ b/index.html @@ -4,10 +4,8 @@ 🏆 Lara Kiesewetter – Live Schachturnier - - - - + +
@@ -32,7 +30,18 @@
--:--:--
-
+
+
+
+
+
+
+
+
+ 0.00 +
+
+
diff --git a/js/app.js b/js/app.js index 50ff6e6..f0852e8 100644 --- a/js/app.js +++ b/js/app.js @@ -11,10 +11,13 @@ let currentGame = null; let allLaraGames = []; let currentMoveIndex = -1; let userSelectedGame = false; -let userScrolledMoves = false; +let evalAbortController = null; +let lastEvalFen = null; let currentRound = 0; let roundPgns = {}; let pollId = 0; +let pollInterval = null; +let updateTimer = null; async function fetchRoundPGN(round) { const res = await fetch(`https://www.deutsche-schachjugend.de/2026/odjm-d/partien/${round}.pgn?t=${Date.now()}`); @@ -54,11 +57,7 @@ async function loadPGN(showOverlay = true) { if (!userSelectedGame) { const liveGame = getLiveGame(allLaraGames); - const newGame = liveGame || getLatestGame(allLaraGames); - if (newGame !== currentGame) { - userScrolledMoves = false; - } - currentGame = newGame; + currentGame = liveGame || getLatestGame(allLaraGames); } updateBoard(); updatePlayerInfo(); @@ -82,26 +81,20 @@ async function loadPGN(showOverlay = true) { function updateBoard() { if (!currentGame) return; - chess = new Chess(); - // Spiegele das Brett, wenn Lara Schwarz hat const laraIsBlack = currentGame.black.toLowerCase().includes('kiesewetter'); const orientation = laraIsBlack ? 'black' : 'white'; // Führe alle Züge aus const nonResultMoves = currentGame.moves.filter(m => !m.isResult); - for (const move of nonResultMoves) { - try { - chess.move(move.san); - } catch { - // Ignoriere ungültige Züge - } - } - if (!userScrolledMoves) { + + // Aktuellen Move beibehalten — nie automatisch zum letzten Zug springen + if (currentMoveIndex >= nonResultMoves.length || currentMoveIndex < -1) { currentMoveIndex = nonResultMoves.length - 1; - } else if (currentMoveIndex >= 0) { - // Benutzer hat navigiert – zeige Brett an seinem ausgewählten Zug - chess = new Chess(); + } + + chess = new Chess(); + if (currentMoveIndex >= 0) { for (let i = 0; i <= currentMoveIndex && i < nonResultMoves.length; i++) { try { chess.move(nonResultMoves[i].san); @@ -131,6 +124,105 @@ function updateBoard() { // Highlight active player highlightActivePlayer(); + + syncEvalBarHeight(); + updateEvaluation(); +} + +function syncEvalBarHeight() { + const boardEl = document.getElementById('board'); + const evalContainer = document.getElementById('eval-bar-container'); + if (boardEl && evalContainer) { + const h = boardEl.offsetHeight; + if (h > 100) evalContainer.style.minHeight = h + 'px'; + } +} + +/** + * Sendet FEN an Stockfish und aktualisiert die Eval-Bar + */ +async function updateEvaluation() { + if (!chess) return; + const fen = chess.fen(); + + if (lastEvalFen === fen && evalAbortController) return; + lastEvalFen = fen; + + if (evalAbortController) { + evalAbortController.abort(); + } + evalAbortController = new AbortController(); + + try { + const res = await fetch('/evaluate', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ fen }), + signal: evalAbortController.signal, + }); + if (!res.ok) throw new Error('Eval fehlgeschlagen'); + + const reader = res.body.getReader(); + const decoder = new TextDecoder(); + let buffer = ''; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + + buffer += decoder.decode(value, { stream: true }); + const lines = buffer.split('\n'); + buffer = lines.pop(); + + for (const line of lines) { + if (!line.trim()) continue; + try { + displayEval(JSON.parse(line)); + } catch {} + } + } + + if (buffer.trim()) { + try { displayEval(JSON.parse(buffer)); } catch {} + } + } catch (e) { + if (e.name === 'AbortError') return; + const el = document.getElementById('eval-score'); + if (el) el.textContent = '?'; + } +} + +function displayEval(data) { + const evalScore = document.getElementById('eval-score'); + const barFill = document.getElementById('eval-bar-fill'); + const barMarker = document.getElementById('eval-bar-marker'); + + let scoreText = '0.00'; + let fillPercent = 50; + + if (data.scoreMate !== null && data.scoreMate !== undefined && data.scoreMate !== 0) { + const mate = parseInt(data.scoreMate); + const whiteMate = chess && chess.turn() === 'w' ? mate : -mate; + if (whiteMate > 0) { + scoreText = '#' + whiteMate; + fillPercent = 100; + } else { + scoreText = '#' + Math.abs(whiteMate); + fillPercent = 0; + } + } else if (data.scoreCp !== null && data.scoreCp !== undefined) { + const cp = parseInt(data.scoreCp); + const whiteCp = chess && chess.turn() === 'w' ? cp : -cp; + const pawns = (whiteCp / 100).toFixed(2); + scoreText = (whiteCp > 0 ? '+' : '') + pawns; + fillPercent = 50 + whiteCp / 14; + fillPercent = Math.max(0, Math.min(100, fillPercent)); + } + + evalScore.textContent = scoreText; + + if (barFill) barFill.style.height = fillPercent + '%'; + if (barMarker) barMarker.style.top = (100 - fillPercent) + '%'; } /** @@ -144,7 +236,6 @@ function goToMove(index) { if (index < -1) index = -1; if (index >= nonResultMoves.length) index = nonResultMoves.length - 1; - userScrolledMoves = index < nonResultMoves.length - 1; currentMoveIndex = index; chess = new Chess(); @@ -160,6 +251,8 @@ function goToMove(index) { highlightActivePlayer(); highlightLastMove(); updateClocks(index); + syncEvalBarHeight(); + updateEvaluation(); document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current')); if (index >= 0) { @@ -382,11 +475,6 @@ function updateMovesList() { const curMove = movesList.querySelector(`[data-index="${currentMoveIndex}"]`); if (curMove) curMove.classList.add('current'); } - - // Scroll to bottom nur wenn Benutzer nicht manuell navigiert hat - if (!userScrolledMoves) { - movesList.scrollTop = movesList.scrollHeight; - } } /** @@ -426,7 +514,6 @@ function updateAllGamesList() { entry.addEventListener('click', () => { userSelectedGame = true; - userScrolledMoves = false; currentGame = game; updateBoard(); updatePlayerInfo(); @@ -531,9 +618,11 @@ function updateTimestamp() { * Startet Auto-Refresh alle 30 Sekunden */ function startAutoRefresh() { + clearInterval(pollInterval); + clearInterval(updateTimer); + const myId = ++pollId; let lastUpdate = Date.now(); - let pollInterval, timer; document.getElementById('refresh-timer').textContent = '0s'; document.getElementById('refresh-timer').style.color = '#4ade80'; @@ -541,13 +630,13 @@ function startAutoRefresh() { loadPGN(true); pollInterval = setInterval(() => { - if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; } + if (pollId !== myId) { clearInterval(pollInterval); clearInterval(updateTimer); return; } loadPGN(false); lastUpdate = Date.now(); }, 30000); - timer = setInterval(() => { - if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; } + updateTimer = setInterval(() => { + if (pollId !== myId) { clearInterval(pollInterval); clearInterval(updateTimer); return; } const elapsed = Date.now() - lastUpdate; const remaining = Math.max(0, 30000 - elapsed); const s = Math.floor(remaining / 1000); @@ -626,8 +715,24 @@ if (window.ResizeObserver) { ro.observe(document.getElementById('board')); } +/** + * Page Visibility API – Timer pausieren wenn Tab unsichtbar + */ +document.addEventListener('visibilitychange', () => { + if (document.hidden) { + clearInterval(pollInterval); + clearInterval(updateTimer); + } else { + startAutoRefresh(); + } +}); + /** * Init */ loadPGN(); startAutoRefresh(); + +// Eval-Bar initialisieren +chess = new Chess(); +updateEvaluation(); diff --git a/less/style.less b/less/style.less deleted file mode 100644 index 1d5b539..0000000 --- a/less/style.less +++ /dev/null @@ -1,544 +0,0 @@ -// Farbvariablen -@primary: #e94560; -@primary-glow: rgba(233, 69, 96, 0.3); -@bg-dark: rgba(0, 0, 0, 0.3); -@bg-darker: rgba(0, 0, 0, 0.4); -@text-muted: #aaa; -@text-light: #e0e0e0; -@text-white: #fff; -@accent-green: #4ade80; -@gold: #ffd700; -@font-mono: 'Courier New', monospace; - -* { - margin: 0; - padding: 0; - box-sizing: border-box; -} - -body { - font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; - background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%); - color: @text-light; - min-height: 100vh; -} - -header { - background: rgba(0, 0, 0, 0.4); - padding: 16px 24px; - display: flex; - justify-content: space-between; - align-items: center; - flex-wrap: wrap; - gap: 12px; - border-bottom: 2px solid @primary; - - h1 { - font-size: 1.5rem; - color: @text-white; - text-shadow: 0 0 10px rgba(233, 69, 96, 0.5); - } -} - -#status-bar { - display: flex; - align-items: center; - gap: 16px; - font-size: 0.85rem; - color: @text-muted; -} - -#last-update, #refresh-timer { - font-family: @font-mono; - font-size: 0.9rem; - color: @accent-green; - background: @bg-darker; - padding: 4px 10px; - border-radius: 6px; - border: 1px solid rgba(74, 222, 128, 0.2); -} - -#refresh-btn { - background: @primary; - border: none; - color: white; - width: 36px; - height: 36px; - border-radius: 50%; - cursor: pointer; - font-size: 1.2rem; - transition: transform 0.3s, background 0.3s; - - &:hover { - background: #ff6b6b; - transform: rotate(180deg); - } -} - -#main-content { - display: flex; - gap: 24px; - padding: 24px; - max-width: 1400px; - margin: 0 auto; - align-items: flex-start; -} - -/* Board Section */ - #board-section { - flex: 1; - min-width: 0; - display: flex; - flex-direction: column; - align-items: center; - gap: 12px; - } - -.player-info { - display: flex; - align-items: center; - gap: 12px; - width: 100%; - max-width: 500px; - padding: 12px 16px; - background: @bg-dark; - border-radius: 12px; - border: 2px solid transparent; - transition: border-color 0.3s, box-shadow 0.3s; - - &.active { - border-color: @primary; - box-shadow: 0 0 15px @primary-glow; - } -} - -.player-avatar { - font-size: 2rem; - width: 48px; - height: 48px; - display: flex; - align-items: center; - justify-content: center; - background: rgba(255, 255, 255, 0.1); - border-radius: 50%; - flex-shrink: 0; -} - -.player-details { - flex: 1; - display: flex; - flex-direction: column; - gap: 2px; -} - -.player-name { - font-weight: 600; - font-size: 1.1rem; - color: @text-white; -} - -.player-elo { - font-size: 0.85rem; - color: @text-muted; -} - -.player-clock { - font-family: @font-mono; - font-size: 1.3rem; - font-weight: bold; - background: rgba(0, 0, 0, 0.5); - padding: 6px 12px; - border-radius: 8px; - min-width: 100px; - text-align: center; - color: @accent-green; -} - -#board { - width: 100%; - max-width: 500px; - - [data-square].last-move-highlight { - box-shadow: inset 0 0 3px 3px rgba(255, 200, 0, 0.7); - } -} - -#pgn-panel, #pgn-panel-mobile { - width: 100%; - max-width: 500px; -} - -#pgn-panel-mobile { - display: none; -} - -.pgn-panel--desktop, -.pgn-panel--mobile { - background: @bg-dark; - border-radius: 12px; -} - -.pgn-panel--desktop { - padding: 12px 16px; -} - -.pgn-header { - display: flex; - justify-content: space-between; - align-items: center; - margin-bottom: 8px; - color: @primary; - font-weight: bold; - font-size: 0.9rem; -} - -.copy-pgn-btn { - background: rgba(255, 255, 255, 0.1); - border: none; - color: #ccc; - width: 32px; - height: 32px; - border-radius: 6px; - cursor: pointer; - font-size: 1rem; - transition: background 0.2s; - - &:hover { - background: rgba(255, 255, 255, 0.2); - } -} - -.pgn-text { - font-family: @font-mono; - font-size: 0.78rem; - line-height: 1.5; - color: @text-muted; - white-space: pre-wrap; - word-break: break-all; - max-height: 160px; - overflow-y: auto; - padding: 8px; - background: @bg-darker; - border-radius: 6px; - -webkit-user-select: text; - user-select: text; -} - -/* Info Section */ -#info-section { - flex: 0 0 380px; - display: flex; - flex-direction: column; - gap: 20px; -} - -#game-info h2 { - font-size: 1.3rem; - color: @primary; - margin-bottom: 8px; -} - -#result-info { - font-size: 1.1rem; - padding: 8px 12px; - background: @bg-dark; - border-radius: 8px; -} - -#moves-panel, #all-games-panel, #standings-panel, #pgn-panel-mobile { - background: @bg-dark; - border-radius: 12px; - padding: 16px; - - h3 { - margin-bottom: 12px; - color: @primary; - font-size: 1rem; - } -} - -#standings-content { - font-size: 0.9rem; - line-height: 1.6; - - .standings-loading { - color: #888; - font-style: italic; - } -} - -.standings-row { - display: flex; - justify-content: space-between; - padding: 6px 0; - border-bottom: 1px solid rgba(255, 255, 255, 0.05); - - &:last-child { - border-bottom: none; - } -} - -.standings-label { - color: @text-muted; -} - -.standings-value { - color: @text-white; - font-weight: 600; -} - -.standings-rank { - font-size: 2rem; - font-weight: bold; - color: @primary; - text-align: center; - padding: 8px 0; -} - -.standings-rank-label { - font-size: 0.8rem; - color: #888; - text-align: center; -} - -.standings-header { - text-align: center; - margin-bottom: 8px; - color: @gold; - font-size: 0.85rem; -} - -#moves-list { - max-height: 300px; - overflow-y: auto; - font-family: @font-mono; - font-size: 0.95rem; - line-height: 1.8; - display: flex; - flex-wrap: wrap; - gap: 4px; - - .move-number { - color: #888; - font-weight: bold; - } - - .move { - color: @text-light; - cursor: pointer; - padding: 2px 6px; - border-radius: 4px; - transition: background 0.2s; - - &:hover { - background: @primary-glow; - } - - &.current { - background: @primary; - color: @text-white; - } - } - - .lara-move { - color: @gold; - font-weight: bold; - } - - .opp-move { - color: @text-muted; - } -} - -#all-games-list { - display: flex; - flex-direction: column; - gap: 8px; -} - -.game-entry { - padding: 10px 12px; - background: rgba(255, 255, 255, 0.05); - border-radius: 8px; - cursor: pointer; - transition: background 0.2s; - border-left: 3px solid transparent; - - &:hover { - background: rgba(255, 255, 255, 0.1); - } - - &.active { - border-left-color: @primary; - background: rgba(233, 69, 96, 0.15); - } - - .game-round { - font-weight: bold; - color: @primary; - font-size: 0.85rem; - } - - .game-players { - font-size: 0.9rem; - color: #ccc; - margin-top: 2px; - } - - .game-result { - font-size: 0.8rem; - color: #888; - margin-top: 2px; - } -} - -/* Overlays */ -#loading-overlay, #error-overlay { - position: fixed; - top: 0; - left: 0; - width: 100%; - height: 100%; - background: rgba(26, 26, 46, 0.95); - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - z-index: 1000; - gap: 16px; - - p { - font-size: 1.2rem; - } -} - -#error-overlay button { - padding: 10px 24px; - background: @primary; - color: white; - border: none; - border-radius: 8px; - cursor: pointer; - font-size: 1rem; - margin-top: 8px; -} - -.spinner { - width: 50px; - height: 50px; - border: 4px solid @primary-glow; - border-top-color: @primary; - border-radius: 50%; - animation: spin 0.8s linear infinite; -} - -@keyframes spin { - to { transform: rotate(360deg); } -} - -/* Scrollbar */ -::-webkit-scrollbar { - width: 6px; -} - -::-webkit-scrollbar-track { - background: rgba(0, 0, 0, 0.2); - border-radius: 3px; -} - -::-webkit-scrollbar-thumb { - background: @primary; - border-radius: 3px; -} - -.round-info-mobile { - display: none; -} - -/* Responsive */ -@media (max-width: 900px) { - #main-content { - flex-direction: column; - align-items: center; - padding: 12px; - } - - #info-section { - flex: none; - width: 100%; - max-width: 500px; - } - - html, body { - overflow-x: hidden; - } - - #board-section { - width: 100%; - align-items: stretch; - } - - #board, - .player-info { - max-width: 100%; - width: 100%; - } - - #board { - overflow: hidden; - } - - #board > div { - max-width: 100%; - box-sizing: border-box; - } - - .round-info-mobile { - display: block; - width: 100%; - max-width: 500px; - font-size: 1.3rem; - color: @primary; - margin: 0 auto; - } - - #round-info { - display: none; - } - - #result-info { - display: none; - } - - .pgn-panel--desktop { - display: none; - } - - #pgn-panel-mobile { - display: block; - } - - .player-info { - padding: 8px 12px; - } - - .player-avatar { - width: 36px; - height: 36px; - font-size: 1.4rem; - } - - .player-name { - font-size: 0.95rem; - } - - .player-clock { - font-size: 1.1rem; - min-width: 80px; - padding: 4px 8px; - } - - header h1 { - font-size: 1.2rem; - } -} diff --git a/package.json b/package.json index 67886c2..bbf6cdb 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,11 @@ { - "scripts": { - "build:css": "node build-less.js" - }, + "scripts": {}, "devDependencies": { "@eslint/css": "^1.2.0", "@eslint/js": "^10.0.1", "@eslint/json": "^1.2.0", "@eslint/markdown": "^8.0.2", "eslint": "^10.4.0", - "globals": "^17.6.0", - "less": "^4.6.4" + "globals": "^17.6.0" } } diff --git a/server.py b/server.py index 8a1bd92..20ba60c 100644 --- a/server.py +++ b/server.py @@ -1,27 +1,144 @@ #!/usr/bin/env python3 """ Lokaler Server für Lara Schachturnier -Serviert statische Dateien direkt. +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 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", 15)) + +_stockfish_lock = threading.Lock() + + +class StockfishEngine: + def __init__(self, path): + self.path = path + self.proc = None + + 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._send("uci") + self._read_until("uciok") + self._send("isready") + self._read_until("readyok") + print(f"[STOCKFISH] Engine gestartet: {self.path}") + + def _send(self, cmd): + self.proc.stdin.write(cmd + "\n") + self.proc.stdin.flush() + + def _read_until(self, marker): + while True: + line = self.proc.stdout.readline().strip() + if line == marker: + return + + def evaluate(self, fen): + self._send(f"position fen {fen}") + self._send(f"go movetime 10000") + + score_cp = None + score_mate = None + bestmove = None + pv = None + last_depth = 0 + + while True: + line = self.proc.stdout.readline().strip() + 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 + break + + yield { + "scoreCp": score_cp, + "scoreMate": score_mate, + "bestMove": bestmove, + "pv": pv, + } + + def stop(self): + if self.proc: + self.proc.terminate() + self.proc = None + + +_engine = StockfishEngine(STOCKFISH_PATH) + + +class Handler(http.server.BaseHTTPRequestHandler): -class StaticHandler(http.server.BaseHTTPRequestHandler): def do_GET(self): if self.path == "/": self.path = "/index.html" - filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.path.lstrip("/")) + filepath = os.path.join(BASE_DIR, self.path.lstrip("/")) if os.path.isfile(filepath): content_types = { ".html": "text/html", ".css": "text/css", - ".less": "text/css", ".js": "application/javascript", ".json": "application/json", ".png": "image/png", @@ -37,29 +154,101 @@ class StaticHandler(http.server.BaseHTTPRequestHandler): self.send_response(200) self.send_header("Content-Type", f"{content_type}; charset=utf-8") 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("Connection", "close") + 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 + + self.close_connection = True + try: + with _stockfish_lock: + try: + _engine.start() + except FileNotFoundError: + self.wfile.write( + json.dumps({"error": "Stockfish nicht gefunden"}).encode("utf-8") + b"\n" + ) + self.wfile.flush() + return + + for result in _engine.evaluate(fen): + data = json.dumps(result).encode("utf-8") + b"\n" + self.wfile.write(data) + self.wfile.flush() + except Exception as e: + print(f"[STOCKFISH] Fehler: {e}") + self.wfile.write( + json.dumps({"error": str(e)}).encode("utf-8") + b"\n" + ) + self.wfile.flush() + 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(" [TROPHY] Lara Kiesewetter – Live Schachturnier") + print(" [TROPHY] Lara Kiesewetter - Live Schachturnier") print("=" * 50) - print(f" Server läuft auf: http://localhost:{PORT}") - print(f" Drücke Ctrl+C zum Beenden") + 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) - with socketserver.TCPServer(("", PORT), StaticHandler) as httpd: + socketserver.ThreadingTCPServer.allow_reuse_address = True + with socketserver.ThreadingTCPServer(("", PORT), Handler) as httpd: print(f"\n[SERVER] Server gestartet: http://localhost:{PORT}\n") try: httpd.serve_forever() except KeyboardInterrupt: print("\n[BYE] Server gestoppt.") + _engine.stop() + if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/style.css b/style.css index 7d3eba7..efc5591 100644 --- a/style.css +++ b/style.css @@ -133,6 +133,66 @@ header h1 { #board [data-square].last-move-highlight { box-shadow: inset 0 0 3px 3px rgba(255, 200, 0, 0.7); } +/* Board Row: Board + Eval-Bar nebeneinander */ +#board-row { + display: flex; + gap: 8px; + width: 100%; + max-width: 540px; +} +#eval-bar-container { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + flex-shrink: 0; + width: 36px; + min-height: 350px; +} +#eval-bar { + flex: 1; + width: 16px; + background: #1a1a1a; + border-radius: 8px; + position: relative; + overflow: hidden; + border: 1px solid rgba(255, 255, 255, 0.15); +} +#eval-bar-fill { + position: absolute; + bottom: 0; + width: 100%; + background: linear-gradient(to top, #f0f0f0 0%, #d0d0d0 100%); + transition: height 0.3s ease; + border-radius: 0 0 7px 7px; + height: 50%; +} +#eval-bar-marker { + position: absolute; + left: -2px; + right: -2px; + height: 2px; + background: #000; + transition: top 0.3s ease; + z-index: 2; + box-shadow: 0 0 3px rgba(0, 0, 0, 0.5); + top: 50%; +} +#eval-info { + display: flex; + flex-direction: column; + align-items: center; + gap: 1px; + flex-shrink: 0; +} +#eval-score { + font-family: 'Courier New', monospace; + font-size: 0.75rem; + font-weight: bold; + color: #fff; + text-align: center; + white-space: nowrap; +} #pgn-panel, #pgn-panel-mobile { width: 100%;