497 lines
16 KiB
JavaScript
497 lines
16 KiB
JavaScript
/**
|
||
* Lara Kiesewetter – Live Schachturnier
|
||
* Haupt-Application
|
||
*/
|
||
|
||
const PLAYER_NAME = 'Kiesewetter, Lara';
|
||
|
||
let board = null;
|
||
let chess = null;
|
||
let currentGame = null;
|
||
let allLaraGames = [];
|
||
let serverLastFetch = null;
|
||
let laraColor = null;
|
||
let currentMoveIndex = -1;
|
||
let userSelectedGame = false;
|
||
let userScrolledMoves = false;
|
||
let lastMtime = 0;
|
||
let pollId = 0;
|
||
|
||
/**
|
||
* Lädt die PGN-Datei und aktualisiert die Anzeige
|
||
*/
|
||
async function loadPGN(showOverlay = true) {
|
||
const currentPollId = ++pollId;
|
||
if (showOverlay) showLoading(true);
|
||
hideError();
|
||
|
||
try {
|
||
const [pgnResponse, statusResponse] = await Promise.all([
|
||
fetch(`http://localhost:8111/pgn?since=${lastMtime}`),
|
||
fetch('http://localhost:8111/status').catch(() => null)
|
||
]);
|
||
|
||
if (currentPollId !== pollId) return;
|
||
if (!pgnResponse.ok) throw new Error(`HTTP ${pgnResponse.status}`);
|
||
|
||
const mtimeHeader = pgnResponse.headers.get('X-Cache-Mtime');
|
||
if (mtimeHeader) lastMtime = parseFloat(mtimeHeader);
|
||
|
||
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);
|
||
const newGame = liveGame || getLatestGame(allLaraGames);
|
||
if (newGame !== currentGame) {
|
||
userScrolledMoves = false;
|
||
}
|
||
currentGame = newGame;
|
||
}
|
||
updateBoard();
|
||
updatePlayerInfo();
|
||
updateMovesList();
|
||
updateAllGamesList();
|
||
updateTimestamp();
|
||
updateStandings();
|
||
|
||
showLoading(false);
|
||
|
||
} catch (error) {
|
||
if (currentPollId !== pollId) return;
|
||
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
|
||
}
|
||
}
|
||
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 (e) {
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
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;
|
||
|
||
userScrolledMoves = 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();
|
||
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;
|
||
|
||
// 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}`;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 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 = `
|
||
<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;
|
||
userScrolledMoves = false;
|
||
currentGame = game;
|
||
updateBoard();
|
||
updatePlayerInfo();
|
||
updateMovesList();
|
||
updateAllGamesList();
|
||
});
|
||
|
||
list.appendChild(entry);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Lädt die Turniertabelle vom Proxy und zeigt Laras Platzierung an
|
||
*/
|
||
function updateStandings() {
|
||
fetch('http://localhost:8111/standings')
|
||
.then(res => {
|
||
if (!res.ok) throw new Error('Fehler beim Laden');
|
||
return res.json();
|
||
})
|
||
.then(data => {
|
||
const container = document.getElementById('standings-content');
|
||
if (!data || data.error) {
|
||
container.innerHTML = '<div class="standings-loading">Daten nicht verfügbar</div>';
|
||
return;
|
||
}
|
||
container.innerHTML = `
|
||
<div class="standings-rank">${data.rank}.</div>
|
||
<div class="standings-rank-label">Tabellenplatz</div>
|
||
<div class="standings-header">${data.round_info || 'nach Runde 1'}</div>
|
||
<div class="standings-row">
|
||
<span class="standings-label">Punkte</span>
|
||
<span class="standings-value">${data.points}</span>
|
||
</div>
|
||
<div class="standings-row">
|
||
<span class="standings-label">Siege</span>
|
||
<span class="standings-value">${data.wins}</span>
|
||
</div>
|
||
<div class="standings-row">
|
||
<span class="standings-label">Unentschieden</span>
|
||
<span class="standings-value">${data.draws}</span>
|
||
</div>
|
||
<div class="standings-row">
|
||
<span class="standings-label">Niederlagen</span>
|
||
<span class="standings-value">${data.losses}</span>
|
||
</div>
|
||
`;
|
||
})
|
||
.catch(err => {
|
||
document.getElementById('standings-content').innerHTML =
|
||
'<div class="standings-loading">Daten nicht verfügbar</div>';
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 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 long-polling: nach jeder Antwort sofort die nächste Anfrage stellen
|
||
*/
|
||
function startAutoRefresh() {
|
||
document.getElementById('refresh-timer').textContent = '● Live';
|
||
document.getElementById('refresh-timer').style.color = '#4ade80';
|
||
|
||
const myId = ++pollId;
|
||
(async function poll() {
|
||
while (pollId === myId) {
|
||
await loadPGN(false);
|
||
}
|
||
})();
|
||
}
|
||
|
||
/**
|
||
* 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 – startet neuen Long-Poll-Zyklus
|
||
*/
|
||
document.getElementById('refresh-btn').addEventListener('click', () => {
|
||
pollId++;
|
||
loadPGN(true).then(() => startAutoRefresh());
|
||
});
|
||
|
||
/**
|
||
* 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();
|