Eval-Bar mit Stockfish-Streaming, LESS entfernt, Move-Lock, AGENTS-Regeln

This commit is contained in:
2026-05-25 20:09:09 +02:00
parent 3b1a4ce4e4
commit ec1ebaa525
8 changed files with 412 additions and 595 deletions

165
js/app.js
View File

@@ -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();