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 @@
⬜
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%;