Files
lara-schach-live/app.js

408 lines
12 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;
let laraColor = null;
let currentMoveIndex = -1;
let userSelectedGame = false;
/**
* 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;
}
// Nur automatisch wechseln, wenn der Benutzer keine andere Partie ausgewählt hat
if (!userSelectedGame) {
const liveGame = getLiveGame(allLaraGames);
currentGame = liveGame || getLatestGame(allLaraGames);
}
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';
laraColor = laraIsBlack ? 'b' : 'w';
// 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 (e) {
// Ignoriere ungültige Züge
}
}
currentMoveIndex = nonResultMoves.length - 1;
if (board) {
board.position(chess.fen(), true);
board.orientation(orientation);
} 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;
currentMoveIndex = index;
chess = new Chess();
for (let i = 0; i <= index; i++) {
try {
chess.move(nonResultMoves[i].san);
} catch (err) {
break;
}
}
board.position(chess.fen(), true);
highlightActivePlayer();
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 laraClock = laraIsWhite ? currentGame.whiteClock : currentGame.blackClock;
const laraEmoji = laraIsWhite ? '⬜' : '⬛';
const oppPlayer = laraIsWhite ? currentGame.black : currentGame.white;
const oppElo = laraIsWhite ? currentGame.blackElo : currentGame.whiteElo;
const oppClock = laraIsWhite ? currentGame.blackClock : currentGame.whiteClock;
const oppEmoji = laraIsWhite ? '⬛' : '⬜';
// Top-Panel (player-black) = Gegner
document.getElementById('black-name').textContent = oppPlayer;
document.getElementById('black-elo').textContent = oppElo ? `(ELO: ${oppElo})` : '';
document.getElementById('black-clock').textContent = formatClock(oppClock);
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.getElementById('white-clock').textContent = formatClock(laraClock);
document.querySelector('#player-white .player-avatar').textContent = laraEmoji;
// 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 || !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');
}
}
/**
* 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
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 = `
<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', () => {
userSelectedGame = true;
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();
});
/**
* Pfeiltasten-Navigation
*/
document.addEventListener('keydown', (e) => {
if (!currentGame) return;
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
if (e.key === 'ArrowLeft') {
e.preventDefault();
goToMove(currentMoveIndex - 1);
} else if (e.key === 'ArrowRight') {
e.preventDefault();
goToMove(currentMoveIndex + 1);
}
});
/**
* Init
*/
loadPGN();
startAutoRefresh();