/** * Lara Kiesewetter – Live Schachturnier * Haupt-Application */ /* global $, parsePGN, filterLaraGames, getLiveGame, getLatestGame, Chess, Chessboard */ let board = null; let chess = null; let currentGame = null; let allLaraGames = []; let currentMoveIndex = -1; let userSelectedGame = false; let userScrolledMoves = false; let currentRound = 0; let roundPgns = {}; let pollId = 0; async function fetchRoundPGN(round) { const res = await fetch(`https://www.deutsche-schachjugend.de/2026/odjm-d/partien/${round}.pgn?t=${Date.now()}`); if (!res.ok) return null; return await res.text(); } async function loadPGN(showOverlay = true) { const currentPollId = pollId; if (showOverlay) showLoading(true); hideError(); try { await updateStandings(); if (currentRound === 0) { if (showOverlay) showLoading(false); return; } // Alle Runden immer frisch von der DSJ laden (mit Cache-Busting) const maxRound = currentRound + 1; for (let r = 1; r <= maxRound; r++) { const text = await fetchRoundPGN(r); if (currentPollId !== pollId) return; if (text !== null) roundPgns[r] = text; } // Alle PGNs kombinieren const combinedPgn = Object.values(roundPgns).join('\n\n'); const allGames = parsePGN(combinedPgn); allLaraGames = filterLaraGames(allGames); if (allLaraGames.length === 0) { return; } if (!userSelectedGame) { const liveGame = getLiveGame(allLaraGames); const newGame = liveGame || getLatestGame(allLaraGames); if (newGame !== currentGame) { userScrolledMoves = false; } currentGame = newGame; } updateBoard(); updatePlayerInfo(); updateMovesList(); updateAllGamesList(); updatePGNDisplay(); updateTimestamp(); showLoading(false); } catch (error) { if (currentPollId !== pollId) return; console.error('Fehler beim Laden:', error); showLoading(false); } } /** * Aktualisiert das Schachbrett */ 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) { currentMoveIndex = nonResultMoves.length - 1; } else if (currentMoveIndex >= 0) { // Benutzer hat navigiert – zeige Brett an seinem ausgewählten Zug chess = new Chess(); for (let i = 0; i <= currentMoveIndex && i < nonResultMoves.length; i++) { try { chess.move(nonResultMoves[i].san); } catch { break; } } } if (board) { board.position(chess.fen(), true); board.orientation(orientation); board.resize(); highlightLastMove(); } else { board = Chessboard('board', { position: chess.fen(), orientation: orientation, pieceTheme: 'https://chessboardjs.com/img/chesspieces/wikipedia/{piece}.png', draggable: false, spawnMoveError: false }); // Click auf Züge document.getElementById('moves-list').addEventListener('click', handleMoveClick); } // Highlight active player highlightActivePlayer(); } /** * Gehe zu einem bestimmten Zug (Index) */ function goToMove(index) { if (!currentGame) return; const nonResultMoves = currentGame.moves.filter(m => !m.isResult); if (index < -1) index = -1; if (index >= nonResultMoves.length) index = nonResultMoves.length - 1; userScrolledMoves = index < nonResultMoves.length - 1; currentMoveIndex = index; chess = new Chess(); for (let i = 0; i <= index; i++) { try { chess.move(nonResultMoves[i].san); } catch { break; } } board.position(chess.fen(), true); highlightActivePlayer(); highlightLastMove(); updateClocks(index); document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current')); if (index >= 0) { const moveEl = document.querySelector(`#moves-list [data-index="${index}"]`); if (moveEl) moveEl.classList.add('current'); } } /** * Aktualisiert die Spielerinformationen */ function updatePlayerInfo() { if (!currentGame) return; const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter'); const laraPlayer = laraIsWhite ? currentGame.white : currentGame.black; const laraElo = laraIsWhite ? currentGame.whiteElo : currentGame.blackElo; const laraEmoji = laraIsWhite ? '⬜' : '⬛'; const oppPlayer = laraIsWhite ? currentGame.black : currentGame.white; const oppElo = laraIsWhite ? currentGame.blackElo : currentGame.whiteElo; const oppEmoji = laraIsWhite ? '⬛' : '⬜'; // Top-Panel (player-black) = Gegner document.getElementById('black-name').textContent = oppPlayer; document.getElementById('black-elo').textContent = oppElo ? `(ELO: ${oppElo})` : ''; document.querySelector('#player-black .player-avatar').textContent = oppEmoji; // Bottom-Panel (player-white) = Lara document.getElementById('white-name').textContent = laraPlayer; document.getElementById('white-elo').textContent = laraElo ? `(ELO: ${laraElo})` : ''; document.querySelector('#player-white .player-avatar').textContent = laraEmoji; updateClocks(currentMoveIndex); // Round info const roundInfo = `Runde ${currentGame.round} – ${currentGame.event || 'Turnier'}`; document.getElementById('round-info').textContent = roundInfo; const mobileRound = document.getElementById('round-info-mobile'); if (mobileRound) mobileRound.textContent = roundInfo; // Result info const resultEl = document.getElementById('result-info'); if (currentGame.isLive) { resultEl.innerHTML = '● Laufen'; } else { resultEl.textContent = `Ergebnis: ${currentGame.result}`; } } /** * Aktualisiert die Uhrenanzeige basierend auf dem aktuellen Zugindex */ function updateClocks(moveIndex) { if (!currentGame) return; const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter'); let whiteClock, blackClock; if (moveIndex < 0) { whiteClock = currentGame.whiteClock; blackClock = currentGame.blackClock; } else { const nonResultMoves = currentGame.moves.filter(m => !m.isResult); if (moveIndex < nonResultMoves.length) { whiteClock = nonResultMoves[moveIndex].whiteClock; blackClock = nonResultMoves[moveIndex].blackClock; } } const laraClock = laraIsWhite ? whiteClock : blackClock; const oppClock = laraIsWhite ? blackClock : whiteClock; document.getElementById('black-clock').textContent = formatClock(oppClock); document.getElementById('white-clock').textContent = formatClock(laraClock); } /** * Highlight den Spieler, der gerade am Zug ist */ function highlightActivePlayer() { if (!chess || !currentGame) return; const laraPanel = document.getElementById('player-white'); const oppPanel = document.getElementById('player-black'); laraPanel.classList.remove('active'); oppPanel.classList.remove('active'); const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter'); const isLaraTurn = (chess.turn() === 'w' && laraIsWhite) || (chess.turn() === 'b' && !laraIsWhite); if (isLaraTurn) { laraPanel.classList.add('active'); } else { oppPanel.classList.add('active'); } } /** * Highlight den letzten Zug auf dem Brett */ function highlightLastMove() { if (!board || !chess || !currentGame) return; $('#board [data-square]').removeClass('last-move-highlight'); const nonResultMoves = currentGame.moves.filter(m => !m.isResult); if (currentMoveIndex >= 0 && currentMoveIndex < nonResultMoves.length) { const moves = chess.history({ verbose: true }); if (moves.length > 0) { const lastMoveData = moves[moves.length - 1]; $(`#board [data-square="${lastMoveData.from}"]`).addClass('last-move-highlight'); $(`#board [data-square="${lastMoveData.to}"]`).addClass('last-move-highlight'); } } } /** * Generiert PGN-Text aus einem Spiel-Objekt */ function generatePGN(game) { if (!game) return ''; let pgn = ''; if (game.event) pgn += `[Event "${game.event}"]\n`; if (game.site) pgn += `[Site "${game.site}"]\n`; if (game.date) pgn += `[Date "${game.date}"]\n`; if (game.round) pgn += `[Round "${game.round}"]\n`; if (game.white) pgn += `[White "${game.white}"]\n`; if (game.black) pgn += `[Black "${game.black}"]\n`; if (game.result) pgn += `[Result "${game.result}"]\n`; if (game.whiteElo) pgn += `[WhiteElo "${game.whiteElo}"]\n`; if (game.blackElo) pgn += `[BlackElo "${game.blackElo}"]\n`; if (game.termination) pgn += `[Termination "${game.termination}"]\n`; pgn += '\n'; const nonResultMoves = game.moves.filter(m => !m.isResult); for (let i = 0; i < nonResultMoves.length; i += 2) { const moveNumber = Math.floor(i / 2) + 1; pgn += `${moveNumber}. ${nonResultMoves[i].san}`; if (i + 1 < nonResultMoves.length) { pgn += ` ${nonResultMoves[i + 1].san} `; } } const resultMove = game.moves.find(m => m.isResult); if (resultMove) { pgn += ` ${resultMove.san}`; } return pgn.trim(); } function updatePGNDisplay() { if (!currentGame) return; const text = generatePGN(currentGame); document.querySelectorAll('.pgn-text').forEach(el => el.textContent = text); } /** * Aktualisiert die Zugliste */ function updateMovesList() { if (!currentGame) return; const movesList = document.getElementById('moves-list'); movesList.innerHTML = ''; const nonResultMoves = currentGame.moves.filter(m => !m.isResult); const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter'); for (let i = 0; i < nonResultMoves.length; i += 2) { const moveNumber = Math.floor(i / 2) + 1; const numSpan = document.createElement('span'); numSpan.className = 'move-number'; numSpan.textContent = `${moveNumber}.`; movesList.appendChild(numSpan); if (laraIsWhite) { // Lara (Weiß) zuerst const laraMove = document.createElement('span'); laraMove.className = 'move lara-move'; laraMove.textContent = nonResultMoves[i].san; laraMove.dataset.index = i; movesList.appendChild(laraMove); if (i + 1 < nonResultMoves.length) { const oppMove = document.createElement('span'); oppMove.className = 'move opp-move'; oppMove.textContent = nonResultMoves[i + 1].san; oppMove.dataset.index = i + 1; movesList.appendChild(oppMove); } } else { // Lara (Schwarz) – Gegner zuerst, dann Lara const oppMove = document.createElement('span'); oppMove.className = 'move opp-move'; oppMove.textContent = nonResultMoves[i].san; oppMove.dataset.index = i; movesList.appendChild(oppMove); if (i + 1 < nonResultMoves.length) { const laraMove = document.createElement('span'); laraMove.className = 'move lara-move'; laraMove.textContent = nonResultMoves[i + 1].san; laraMove.dataset.index = i + 1; movesList.appendChild(laraMove); } } } // Mark current move if (currentMoveIndex >= 0) { 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; } } /** * Klick auf einen Zug in der Liste */ function handleMoveClick(e) { if (!e.target.classList.contains('move') || !currentGame) return; goToMove(parseInt(e.target.dataset.index)); } /** * Aktualisiert die Liste aller Partien */ function updateAllGamesList() { const list = document.getElementById('all-games-list'); list.innerHTML = ''; // Sort by round const sorted = [...allLaraGames].sort((a, b) => { return (parseInt(a.round) || 0) - (parseInt(b.round) || 0); }); for (const game of sorted) { const entry = document.createElement('div'); entry.className = 'game-entry'; if (currentGame && game === currentGame) entry.classList.add('active'); const laraIsWhite = game.white.toLowerCase().includes('kiesewetter'); const opponent = laraIsWhite ? game.black : game.white; const color = laraIsWhite ? '⬜ Weiß' : '⬛ Schwarz'; entry.innerHTML = `
Runde ${game.round}
Lara ${color} vs ${opponent}
${game.isLive ? '● Laufen' : game.result}
`; entry.addEventListener('click', () => { userSelectedGame = true; userScrolledMoves = false; currentGame = game; updateBoard(); updatePlayerInfo(); updateMovesList(); updateAllGamesList(); updatePGNDisplay(); }); list.appendChild(entry); } } /** * Lädt die Turniertabelle vom DSJ und zeigt Laras Platzierung an */ async function updateStandings() { try { const res = await fetch(`https://www.deutsche-schachjugend.de/2026/odjm-d/tabelle/?t=${Date.now()}`); if (!res.ok) throw new Error('Fehler beim Laden'); const html = await res.text(); const roundMatch = html.match(/Tabellenstand\s+nach\s+der\s+(\d+)\.\s*Runde/); if (roundMatch) { currentRound = parseInt(roundMatch[1]); } const rows = html.matchAll(/]*>(.*?)<\/tr>/gs); for (const row of rows) { if (!row[1].includes('Lara Kiesewetter')) continue; const cells = row[1].matchAll(/]*>(.*?)<\/td>/gs); const clean = []; for (const cell of cells) { clean.push(cell[1].replace(/<[^>]+>/g, '').trim()); } if (clean.length >= 9) { const data = { rank: clean[0], player: 'Lara Kiesewetter', wins: clean[5], draws: clean[6], losses: clean[7], points: clean[8], round_info: roundMatch ? `nach der ${roundMatch[1]}. Runde` : '', round: currentRound, }; const container = document.getElementById('standings-content'); if (!data || data.error) { container.innerHTML = '
Daten nicht verfügbar
'; return; } container.innerHTML = `
${data.rank}.
Tabellenplatz
${data.round_info || 'nach Runde 1'}
Punkte ${data.points}
Siege ${data.wins}
Unentschieden ${data.draws}
Niederlagen ${data.losses}
`; return; } } document.getElementById('standings-content').innerHTML = '
Daten nicht verfügbar
'; } catch { document.getElementById('standings-content').innerHTML = '
Daten nicht verfügbar
'; } } /** * Format clock string */ function formatClock(clockStr) { if (!clockStr) return '--:--:--'; // Format is HH:MM:SS return clockStr; } /** * Update timestamp */ function updateTimestamp() { const time = new Date(); document.getElementById('last-update').textContent = `Letztes Update: ${time.toLocaleTimeString('de-DE')}`; } /** * Startet Auto-Refresh alle 30 Sekunden */ function startAutoRefresh() { const myId = ++pollId; let lastUpdate = Date.now(); let pollInterval, timer; document.getElementById('refresh-timer').textContent = '0s'; document.getElementById('refresh-timer').style.color = '#4ade80'; loadPGN(true); pollInterval = setInterval(() => { if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; } loadPGN(false); lastUpdate = Date.now(); }, 30000); timer = setInterval(() => { if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; } const elapsed = Date.now() - lastUpdate; const remaining = Math.max(0, 30000 - elapsed); const s = Math.floor(remaining / 1000); document.getElementById('refresh-timer').textContent = `${s}s`; }, 1000); } /** * UI Helpers */ function showLoading(show) { document.getElementById('loading-overlay').style.display = show ? 'flex' : 'none'; } function hideError() { document.getElementById('error-overlay').style.display = 'none'; } /** * Manual refresh button – startet neuen Long-Poll-Zyklus */ document.getElementById('refresh-btn').addEventListener('click', () => { pollId++; startAutoRefresh(); }); /** * PGN in Zwischenablage kopieren */ document.querySelectorAll('.copy-pgn-btn').forEach(btn => { btn.addEventListener('click', async () => { const panel = btn.closest('[class*="pgn-panel"]'); const text = panel.querySelector('.pgn-text').textContent; if (!text) return; try { await navigator.clipboard.writeText(text); btn.textContent = '✅'; setTimeout(() => { btn.textContent = '📋'; }, 1500); } catch { const ta = document.createElement('textarea'); ta.value = text; document.body.appendChild(ta); ta.select(); document.execCommand('copy'); document.body.removeChild(ta); } }); }); /** * Pfeiltasten-Navigation */ document.addEventListener('keydown', (e) => { if (!currentGame) return; if (e.key === 'ArrowLeft') { e.preventDefault(); goToMove(currentMoveIndex - 1); } else if (e.key === 'ArrowRight') { e.preventDefault(); goToMove(currentMoveIndex + 1); } }); /** * Board-Resize bei Fenster- und Container-Änderungen */ window.addEventListener('resize', () => { if (board) board.resize(); }); if (window.ResizeObserver) { const ro = new ResizeObserver(() => { if (board) board.resize(); }); ro.observe(document.getElementById('board')); } /** * Init */ loadPGN(); startAutoRefresh();