Files
lara-schach-live/app.js

345 lines
10 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* Lara Kiesewetter Live Schachturnier
* Haupt-Application
*/
const PGN_URL = 'https://www.deutsche-schachjugend.de/2026/odjm-d/partien/gesamt-utf8.pgn';
const REFRESH_INTERVAL = 60000; // 60 Sekunden
const PLAYER_NAME = 'Kiesewetter, Lara';
let board = null;
let chess = null;
let currentGame = null;
let allLaraGames = [];
let refreshTimer = null;
let countdown = 0;
let serverLastFetch = null;
/**
* Lädt die PGN-Datei und aktualisiert die Anzeige
*/
async function loadPGN() {
showLoading(true);
hideError();
try {
const [pgnResponse, statusResponse] = await Promise.all([
fetch('http://localhost:8111/pgn'),
fetch('http://localhost:8111/status').catch(() => null)
]);
if (!pgnResponse.ok) throw new Error(`HTTP ${pgnResponse.status}`);
if (statusResponse && statusResponse.ok) {
const status = await statusResponse.json();
serverLastFetch = status.last_fetch ? status.last_fetch * 1000 : null;
}
const pgnText = await pgnResponse.text();
const allGames = parsePGN(pgnText);
allLaraGames = filterLaraGames(allGames);
if (allLaraGames.length === 0) {
showError('Keine Partien von Lara gefunden.');
return;
}
// Finde die aktuelle/live Partie
const liveGame = getLiveGame(allLaraGames);
const targetGame = liveGame || getLatestGame(allLaraGames);
currentGame = targetGame;
updateBoard();
updatePlayerInfo();
updateMovesList();
updateAllGamesList();
updateTimestamp();
showLoading(false);
} catch (error) {
console.error('Fehler beim Laden:', error);
showError(`Fehler: ${error.message}`);
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
for (const move of currentGame.moves) {
if (move.isResult) break;
try {
chess.move(move.san);
} catch (e) {
// Ignoriere ungültige Züge
}
}
if (board) board.position(chess.fen(), true);
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();
}
/**
* Aktualisiert die Spielerinformationen
*/
function updatePlayerInfo() {
if (!currentGame) return;
const laraIsWhite = currentGame.white.toLowerCase().includes('kiesewetter');
if (laraIsWhite) {
document.getElementById('white-name').textContent = currentGame.white;
document.getElementById('white-elo').textContent = currentGame.whiteElo ? `(ELO: ${currentGame.whiteElo})` : '';
document.getElementById('white-clock').textContent = formatClock(currentGame.whiteClock);
document.getElementById('black-name').textContent = currentGame.black;
document.getElementById('black-elo').textContent = currentGame.blackElo ? `(ELO: ${currentGame.blackElo})` : '';
document.getElementById('black-clock').textContent = formatClock(currentGame.blackClock);
} else {
document.getElementById('white-name').textContent = currentGame.white;
document.getElementById('white-elo').textContent = currentGame.whiteElo ? `(ELO: ${currentGame.whiteElo})` : '';
document.getElementById('white-clock').textContent = formatClock(currentGame.whiteClock);
document.getElementById('black-name').textContent = currentGame.black;
document.getElementById('black-elo').textContent = currentGame.blackElo ? `(ELO: ${currentGame.blackElo})` : '';
document.getElementById('black-clock').textContent = formatClock(currentGame.blackClock);
}
// Round info
const roundInfo = `Runde ${currentGame.round} ${currentGame.event || 'Turnier'}`;
document.getElementById('round-info').textContent = roundInfo;
// Result info
const resultEl = document.getElementById('result-info');
if (currentGame.isLive) {
resultEl.innerHTML = '<span style="color: #4ade80;">● Laufen</span>';
} else {
resultEl.textContent = `Ergebnis: ${currentGame.result}`;
}
}
/**
* Highlight den Spieler, der gerade am Zug ist
*/
function highlightActivePlayer() {
if (!chess) return;
const whiteEl = document.getElementById('player-white');
const blackEl = document.getElementById('player-black');
whiteEl.classList.remove('active');
blackEl.classList.remove('active');
if (chess.turn() === 'w') {
whiteEl.classList.add('active');
} else {
blackEl.classList.add('active');
}
}
/**
* Aktualisiert die Zugliste
*/
function updateMovesList() {
if (!currentGame) return;
const movesList = document.getElementById('moves-list');
movesList.innerHTML = '';
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
for (let i = 0; i < nonResultMoves.length; i += 2) {
const moveNumber = Math.floor(i / 2) + 1;
// Move number
const numSpan = document.createElement('span');
numSpan.className = 'move-number';
numSpan.textContent = `${moveNumber}.`;
movesList.appendChild(numSpan);
// White move
const whiteMove = document.createElement('span');
whiteMove.className = 'move';
whiteMove.textContent = nonResultMoves[i].san;
whiteMove.dataset.index = i;
movesList.appendChild(whiteMove);
// Black move
if (i + 1 < nonResultMoves.length) {
const blackMove = document.createElement('span');
blackMove.className = 'move';
blackMove.textContent = nonResultMoves[i + 1].san;
blackMove.dataset.index = i + 1;
movesList.appendChild(blackMove);
}
}
// Mark last move
if (nonResultMoves.length > 0) {
const lastMove = movesList.querySelector(`[data-index="${nonResultMoves.length - 1}"]`);
if (lastMove) lastMove.classList.add('current');
}
// Scroll to bottom
movesList.scrollTop = movesList.scrollHeight;
}
/**
* Klick auf einen Zug in der Liste
*/
function handleMoveClick(e) {
if (!e.target.classList.contains('move') || !currentGame) return;
const index = parseInt(e.target.dataset.index);
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
chess = new Chess();
for (let i = 0; i <= index; i++) {
try {
chess.move(nonResultMoves[i].san);
} catch (err) {
break;
}
}
board.position(chess.fen(), true);
// Update current highlight
document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current'));
e.target.classList.add('current');
}
/**
* 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 = `
<div class="game-round">Runde ${game.round}</div>
<div class="game-players">Lara ${color} vs ${opponent}</div>
<div class="game-result">${game.isLive ? '● Laufen' : game.result}</div>
`;
entry.addEventListener('click', () => {
currentGame = game;
updateBoard();
updatePlayerInfo();
updateMovesList();
updateAllGamesList();
});
list.appendChild(entry);
}
}
/**
* Format clock string
*/
function formatClock(clockStr) {
if (!clockStr) return '--:--:--';
// Format is HH:MM:SS
return clockStr;
}
/**
* Update timestamp
*/
function updateTimestamp() {
const time = serverLastFetch ? new Date(serverLastFetch) : new Date();
document.getElementById('last-update').textContent =
`Letztes Update: ${time.toLocaleTimeString('de-DE')}`;
}
/**
* Start auto-refresh
*/
function startAutoRefresh() {
countdown = REFRESH_INTERVAL / 1000;
if (refreshTimer) clearInterval(refreshTimer);
refreshTimer = setInterval(() => {
countdown--;
const mins = Math.floor(countdown / 60);
const secs = countdown % 60;
document.getElementById('refresh-timer').textContent =
`Nächstes Update in: ${String(mins).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
if (countdown <= 0) {
countdown = REFRESH_INTERVAL / 1000;
loadPGN();
}
}, 1000);
}
/**
* UI Helpers
*/
function showLoading(show) {
document.getElementById('loading-overlay').style.display = show ? 'flex' : 'none';
}
function showError(msg) {
document.getElementById('error-message').textContent = msg;
document.getElementById('error-overlay').style.display = 'flex';
}
function hideError() {
document.getElementById('error-overlay').style.display = 'none';
}
/**
* Manual refresh button
*/
document.getElementById('refresh-btn').addEventListener('click', () => {
countdown = REFRESH_INTERVAL / 1000;
loadPGN();
});
/**
* Init
*/
loadPGN();
startAutoRefresh();