Eval-Bar mit Stockfish-Streaming, LESS entfernt, Move-Lock, AGENTS-Regeln
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -4,3 +4,4 @@ __pycache__/
|
|||||||
lara-chess/
|
lara-chess/
|
||||||
node_modules/
|
node_modules/
|
||||||
package-lock.json
|
package-lock.json
|
||||||
|
stockfish.exe
|
||||||
|
|||||||
@@ -3,5 +3,5 @@
|
|||||||
- Nach jeder Änderung am Proxy (server.py) oder an den Requests (app.js) muss der Proxy neu gestartet werden: erst alten Prozess killen (`Get-Process -Name python | Stop-Process -Force`), dann unsichtbar starten.
|
- Nach jeder Änderung am Proxy (server.py) oder an den Requests (app.js) muss der Proxy neu gestartet werden: erst alten Prozess killen (`Get-Process -Name python | Stop-Process -Force`), dann unsichtbar starten.
|
||||||
- Commits und Push erfolgen nur durch expliziten Befehl des Nutzers.
|
- Commits und Push erfolgen nur durch expliziten Befehl des Nutzers.
|
||||||
- Der Server (server.py) wird immer unsichtbar gestartet: `Start-Process -NoNewWindow -FilePath "python" -ArgumentList "server.py"`
|
- Der Server (server.py) wird immer unsichtbar gestartet: `Start-Process -NoNewWindow -FilePath "python" -ArgumentList "server.py"`
|
||||||
- **LESS-Workflow**: CSS-Quelldatei ist `style.less`. Bei Serverstart wird automatisch `style.css` via `node build-less.js` kompiliert. Bei manuellen Änderungen an `style.less`: `node build-less.js` ausführen, dann Server neustarten.
|
- Bei Updates (Polling/Code-Änderungen) niemals den aktuell angezeigten Zug wechseln. Der Benutzer bleibt auf dem von ihm ausgewählten Zug.
|
||||||
- `style.css` ist auto-generiert – keine manuellen Änderungen dort vornehmen.
|
|
||||||
|
|||||||
19
index.html
19
index.html
@@ -4,10 +4,8 @@
|
|||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
<title>🏆 Lara Kiesewetter – Live Schachturnier</title>
|
<title>🏆 Lara Kiesewetter – Live Schachturnier</title>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.css">
|
<link rel="stylesheet" href="https://unpkg.com/@chrisoakman/chessboardjs@1.0.0/dist/chessboard-1.0.0.min.css">
|
||||||
<link rel="stylesheet/less" href="less/style.less">
|
<link rel="stylesheet" href="style.css">
|
||||||
<script>less = { env: 'development' };</script>
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/less@4.6.4/dist/less.min.js"></script>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
@@ -32,7 +30,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="player-clock" id="black-clock">--:--:--</div>
|
<div class="player-clock" id="black-clock">--:--:--</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="board"></div>
|
<div id="board-row">
|
||||||
|
<div id="board"></div>
|
||||||
|
<div id="eval-bar-container">
|
||||||
|
<div id="eval-bar">
|
||||||
|
<div id="eval-bar-fill"></div>
|
||||||
|
<div id="eval-bar-marker"></div>
|
||||||
|
</div>
|
||||||
|
<div id="eval-info">
|
||||||
|
<span id="eval-score">0.00</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="player-white" class="player-info">
|
<div id="player-white" class="player-info">
|
||||||
<div class="player-avatar">⬜</div>
|
<div class="player-avatar">⬜</div>
|
||||||
<div class="player-details">
|
<div class="player-details">
|
||||||
|
|||||||
165
js/app.js
165
js/app.js
@@ -11,10 +11,13 @@ let currentGame = null;
|
|||||||
let allLaraGames = [];
|
let allLaraGames = [];
|
||||||
let currentMoveIndex = -1;
|
let currentMoveIndex = -1;
|
||||||
let userSelectedGame = false;
|
let userSelectedGame = false;
|
||||||
let userScrolledMoves = false;
|
let evalAbortController = null;
|
||||||
|
let lastEvalFen = null;
|
||||||
let currentRound = 0;
|
let currentRound = 0;
|
||||||
let roundPgns = {};
|
let roundPgns = {};
|
||||||
let pollId = 0;
|
let pollId = 0;
|
||||||
|
let pollInterval = null;
|
||||||
|
let updateTimer = null;
|
||||||
|
|
||||||
async function fetchRoundPGN(round) {
|
async function fetchRoundPGN(round) {
|
||||||
const res = await fetch(`https://www.deutsche-schachjugend.de/2026/odjm-d/partien/${round}.pgn?t=${Date.now()}`);
|
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) {
|
if (!userSelectedGame) {
|
||||||
const liveGame = getLiveGame(allLaraGames);
|
const liveGame = getLiveGame(allLaraGames);
|
||||||
const newGame = liveGame || getLatestGame(allLaraGames);
|
currentGame = liveGame || getLatestGame(allLaraGames);
|
||||||
if (newGame !== currentGame) {
|
|
||||||
userScrolledMoves = false;
|
|
||||||
}
|
|
||||||
currentGame = newGame;
|
|
||||||
}
|
}
|
||||||
updateBoard();
|
updateBoard();
|
||||||
updatePlayerInfo();
|
updatePlayerInfo();
|
||||||
@@ -82,26 +81,20 @@ async function loadPGN(showOverlay = true) {
|
|||||||
function updateBoard() {
|
function updateBoard() {
|
||||||
if (!currentGame) return;
|
if (!currentGame) return;
|
||||||
|
|
||||||
chess = new Chess();
|
|
||||||
|
|
||||||
// Spiegele das Brett, wenn Lara Schwarz hat
|
// Spiegele das Brett, wenn Lara Schwarz hat
|
||||||
const laraIsBlack = currentGame.black.toLowerCase().includes('kiesewetter');
|
const laraIsBlack = currentGame.black.toLowerCase().includes('kiesewetter');
|
||||||
const orientation = laraIsBlack ? 'black' : 'white';
|
const orientation = laraIsBlack ? 'black' : 'white';
|
||||||
|
|
||||||
// Führe alle Züge aus
|
// Führe alle Züge aus
|
||||||
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
|
const nonResultMoves = currentGame.moves.filter(m => !m.isResult);
|
||||||
for (const move of nonResultMoves) {
|
|
||||||
try {
|
// Aktuellen Move beibehalten — nie automatisch zum letzten Zug springen
|
||||||
chess.move(move.san);
|
if (currentMoveIndex >= nonResultMoves.length || currentMoveIndex < -1) {
|
||||||
} catch {
|
|
||||||
// Ignoriere ungültige Züge
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!userScrolledMoves) {
|
|
||||||
currentMoveIndex = nonResultMoves.length - 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++) {
|
for (let i = 0; i <= currentMoveIndex && i < nonResultMoves.length; i++) {
|
||||||
try {
|
try {
|
||||||
chess.move(nonResultMoves[i].san);
|
chess.move(nonResultMoves[i].san);
|
||||||
@@ -131,6 +124,105 @@ function updateBoard() {
|
|||||||
|
|
||||||
// Highlight active player
|
// Highlight active player
|
||||||
highlightActivePlayer();
|
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 < -1) index = -1;
|
||||||
if (index >= nonResultMoves.length) index = nonResultMoves.length - 1;
|
if (index >= nonResultMoves.length) index = nonResultMoves.length - 1;
|
||||||
|
|
||||||
userScrolledMoves = index < nonResultMoves.length - 1;
|
|
||||||
currentMoveIndex = index;
|
currentMoveIndex = index;
|
||||||
|
|
||||||
chess = new Chess();
|
chess = new Chess();
|
||||||
@@ -160,6 +251,8 @@ function goToMove(index) {
|
|||||||
highlightActivePlayer();
|
highlightActivePlayer();
|
||||||
highlightLastMove();
|
highlightLastMove();
|
||||||
updateClocks(index);
|
updateClocks(index);
|
||||||
|
syncEvalBarHeight();
|
||||||
|
updateEvaluation();
|
||||||
|
|
||||||
document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current'));
|
document.querySelectorAll('#moves-list .move').forEach(el => el.classList.remove('current'));
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
@@ -382,11 +475,6 @@ function updateMovesList() {
|
|||||||
const curMove = movesList.querySelector(`[data-index="${currentMoveIndex}"]`);
|
const curMove = movesList.querySelector(`[data-index="${currentMoveIndex}"]`);
|
||||||
if (curMove) curMove.classList.add('current');
|
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', () => {
|
entry.addEventListener('click', () => {
|
||||||
userSelectedGame = true;
|
userSelectedGame = true;
|
||||||
userScrolledMoves = false;
|
|
||||||
currentGame = game;
|
currentGame = game;
|
||||||
updateBoard();
|
updateBoard();
|
||||||
updatePlayerInfo();
|
updatePlayerInfo();
|
||||||
@@ -531,9 +618,11 @@ function updateTimestamp() {
|
|||||||
* Startet Auto-Refresh alle 30 Sekunden
|
* Startet Auto-Refresh alle 30 Sekunden
|
||||||
*/
|
*/
|
||||||
function startAutoRefresh() {
|
function startAutoRefresh() {
|
||||||
|
clearInterval(pollInterval);
|
||||||
|
clearInterval(updateTimer);
|
||||||
|
|
||||||
const myId = ++pollId;
|
const myId = ++pollId;
|
||||||
let lastUpdate = Date.now();
|
let lastUpdate = Date.now();
|
||||||
let pollInterval, timer;
|
|
||||||
|
|
||||||
document.getElementById('refresh-timer').textContent = '0s';
|
document.getElementById('refresh-timer').textContent = '0s';
|
||||||
document.getElementById('refresh-timer').style.color = '#4ade80';
|
document.getElementById('refresh-timer').style.color = '#4ade80';
|
||||||
@@ -541,13 +630,13 @@ function startAutoRefresh() {
|
|||||||
loadPGN(true);
|
loadPGN(true);
|
||||||
|
|
||||||
pollInterval = setInterval(() => {
|
pollInterval = setInterval(() => {
|
||||||
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; }
|
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(updateTimer); return; }
|
||||||
loadPGN(false);
|
loadPGN(false);
|
||||||
lastUpdate = Date.now();
|
lastUpdate = Date.now();
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
timer = setInterval(() => {
|
updateTimer = setInterval(() => {
|
||||||
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(timer); return; }
|
if (pollId !== myId) { clearInterval(pollInterval); clearInterval(updateTimer); return; }
|
||||||
const elapsed = Date.now() - lastUpdate;
|
const elapsed = Date.now() - lastUpdate;
|
||||||
const remaining = Math.max(0, 30000 - elapsed);
|
const remaining = Math.max(0, 30000 - elapsed);
|
||||||
const s = Math.floor(remaining / 1000);
|
const s = Math.floor(remaining / 1000);
|
||||||
@@ -626,8 +715,24 @@ if (window.ResizeObserver) {
|
|||||||
ro.observe(document.getElementById('board'));
|
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
|
* Init
|
||||||
*/
|
*/
|
||||||
loadPGN();
|
loadPGN();
|
||||||
startAutoRefresh();
|
startAutoRefresh();
|
||||||
|
|
||||||
|
// Eval-Bar initialisieren
|
||||||
|
chess = new Chess();
|
||||||
|
updateEvaluation();
|
||||||
|
|||||||
544
less/style.less
544
less/style.less
@@ -1,544 +0,0 @@
|
|||||||
// Farbvariablen
|
|
||||||
@primary: #e94560;
|
|
||||||
@primary-glow: rgba(233, 69, 96, 0.3);
|
|
||||||
@bg-dark: rgba(0, 0, 0, 0.3);
|
|
||||||
@bg-darker: rgba(0, 0, 0, 0.4);
|
|
||||||
@text-muted: #aaa;
|
|
||||||
@text-light: #e0e0e0;
|
|
||||||
@text-white: #fff;
|
|
||||||
@accent-green: #4ade80;
|
|
||||||
@gold: #ffd700;
|
|
||||||
@font-mono: 'Courier New', monospace;
|
|
||||||
|
|
||||||
* {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
||||||
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
|
||||||
color: @text-light;
|
|
||||||
min-height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
background: rgba(0, 0, 0, 0.4);
|
|
||||||
padding: 16px 24px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 12px;
|
|
||||||
border-bottom: 2px solid @primary;
|
|
||||||
|
|
||||||
h1 {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
color: @text-white;
|
|
||||||
text-shadow: 0 0 10px rgba(233, 69, 96, 0.5);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#status-bar {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: @text-muted;
|
|
||||||
}
|
|
||||||
|
|
||||||
#last-update, #refresh-timer {
|
|
||||||
font-family: @font-mono;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: @accent-green;
|
|
||||||
background: @bg-darker;
|
|
||||||
padding: 4px 10px;
|
|
||||||
border-radius: 6px;
|
|
||||||
border: 1px solid rgba(74, 222, 128, 0.2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#refresh-btn {
|
|
||||||
background: @primary;
|
|
||||||
border: none;
|
|
||||||
color: white;
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
border-radius: 50%;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
transition: transform 0.3s, background 0.3s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: #ff6b6b;
|
|
||||||
transform: rotate(180deg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#main-content {
|
|
||||||
display: flex;
|
|
||||||
gap: 24px;
|
|
||||||
padding: 24px;
|
|
||||||
max-width: 1400px;
|
|
||||||
margin: 0 auto;
|
|
||||||
align-items: flex-start;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Board Section */
|
|
||||||
#board-section {
|
|
||||||
flex: 1;
|
|
||||||
min-width: 0;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-info {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
padding: 12px 16px;
|
|
||||||
background: @bg-dark;
|
|
||||||
border-radius: 12px;
|
|
||||||
border: 2px solid transparent;
|
|
||||||
transition: border-color 0.3s, box-shadow 0.3s;
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-color: @primary;
|
|
||||||
box-shadow: 0 0 15px @primary-glow;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-avatar {
|
|
||||||
font-size: 2rem;
|
|
||||||
width: 48px;
|
|
||||||
height: 48px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border-radius: 50%;
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-details {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-name {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 1.1rem;
|
|
||||||
color: @text-white;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-elo {
|
|
||||||
font-size: 0.85rem;
|
|
||||||
color: @text-muted;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-clock {
|
|
||||||
font-family: @font-mono;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
font-weight: bold;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
padding: 6px 12px;
|
|
||||||
border-radius: 8px;
|
|
||||||
min-width: 100px;
|
|
||||||
text-align: center;
|
|
||||||
color: @accent-green;
|
|
||||||
}
|
|
||||||
|
|
||||||
#board {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
|
|
||||||
[data-square].last-move-highlight {
|
|
||||||
box-shadow: inset 0 0 3px 3px rgba(255, 200, 0, 0.7);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#pgn-panel, #pgn-panel-mobile {
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pgn-panel-mobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pgn-panel--desktop,
|
|
||||||
.pgn-panel--mobile {
|
|
||||||
background: @bg-dark;
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pgn-panel--desktop {
|
|
||||||
padding: 12px 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pgn-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: @primary;
|
|
||||||
font-weight: bold;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-pgn-btn {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
border: none;
|
|
||||||
color: #ccc;
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
border-radius: 6px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
transition: background 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.2);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.pgn-text {
|
|
||||||
font-family: @font-mono;
|
|
||||||
font-size: 0.78rem;
|
|
||||||
line-height: 1.5;
|
|
||||||
color: @text-muted;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
max-height: 160px;
|
|
||||||
overflow-y: auto;
|
|
||||||
padding: 8px;
|
|
||||||
background: @bg-darker;
|
|
||||||
border-radius: 6px;
|
|
||||||
-webkit-user-select: text;
|
|
||||||
user-select: text;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Info Section */
|
|
||||||
#info-section {
|
|
||||||
flex: 0 0 380px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#game-info h2 {
|
|
||||||
font-size: 1.3rem;
|
|
||||||
color: @primary;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#result-info {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
padding: 8px 12px;
|
|
||||||
background: @bg-dark;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#moves-panel, #all-games-panel, #standings-panel, #pgn-panel-mobile {
|
|
||||||
background: @bg-dark;
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 16px;
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
color: @primary;
|
|
||||||
font-size: 1rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#standings-content {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.6;
|
|
||||||
|
|
||||||
.standings-loading {
|
|
||||||
color: #888;
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.standings-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 6px 0;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
border-bottom: none;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.standings-label {
|
|
||||||
color: @text-muted;
|
|
||||||
}
|
|
||||||
|
|
||||||
.standings-value {
|
|
||||||
color: @text-white;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.standings-rank {
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: bold;
|
|
||||||
color: @primary;
|
|
||||||
text-align: center;
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.standings-rank-label {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #888;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.standings-header {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
color: @gold;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#moves-list {
|
|
||||||
max-height: 300px;
|
|
||||||
overflow-y: auto;
|
|
||||||
font-family: @font-mono;
|
|
||||||
font-size: 0.95rem;
|
|
||||||
line-height: 1.8;
|
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
gap: 4px;
|
|
||||||
|
|
||||||
.move-number {
|
|
||||||
color: #888;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.move {
|
|
||||||
color: @text-light;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 2px 6px;
|
|
||||||
border-radius: 4px;
|
|
||||||
transition: background 0.2s;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: @primary-glow;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.current {
|
|
||||||
background: @primary;
|
|
||||||
color: @text-white;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.lara-move {
|
|
||||||
color: @gold;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.opp-move {
|
|
||||||
color: @text-muted;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#all-games-list {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-entry {
|
|
||||||
padding: 10px 12px;
|
|
||||||
background: rgba(255, 255, 255, 0.05);
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: background 0.2s;
|
|
||||||
border-left: 3px solid transparent;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.active {
|
|
||||||
border-left-color: @primary;
|
|
||||||
background: rgba(233, 69, 96, 0.15);
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-round {
|
|
||||||
font-weight: bold;
|
|
||||||
color: @primary;
|
|
||||||
font-size: 0.85rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-players {
|
|
||||||
font-size: 0.9rem;
|
|
||||||
color: #ccc;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.game-result {
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #888;
|
|
||||||
margin-top: 2px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Overlays */
|
|
||||||
#loading-overlay, #error-overlay {
|
|
||||||
position: fixed;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
background: rgba(26, 26, 46, 0.95);
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
z-index: 1000;
|
|
||||||
gap: 16px;
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#error-overlay button {
|
|
||||||
padding: 10px 24px;
|
|
||||||
background: @primary;
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
border-radius: 8px;
|
|
||||||
cursor: pointer;
|
|
||||||
font-size: 1rem;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 50px;
|
|
||||||
height: 50px;
|
|
||||||
border: 4px solid @primary-glow;
|
|
||||||
border-top-color: @primary;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 0.8s linear infinite;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
to { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Scrollbar */
|
|
||||||
::-webkit-scrollbar {
|
|
||||||
width: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-track {
|
|
||||||
background: rgba(0, 0, 0, 0.2);
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
::-webkit-scrollbar-thumb {
|
|
||||||
background: @primary;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.round-info-mobile {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Responsive */
|
|
||||||
@media (max-width: 900px) {
|
|
||||||
#main-content {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#info-section {
|
|
||||||
flex: none;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
}
|
|
||||||
|
|
||||||
html, body {
|
|
||||||
overflow-x: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#board-section {
|
|
||||||
width: 100%;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
#board,
|
|
||||||
.player-info {
|
|
||||||
max-width: 100%;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#board {
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
#board > div {
|
|
||||||
max-width: 100%;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
.round-info-mobile {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
max-width: 500px;
|
|
||||||
font-size: 1.3rem;
|
|
||||||
color: @primary;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
#round-info {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#result-info {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.pgn-panel--desktop {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
#pgn-panel-mobile {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-info {
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-avatar {
|
|
||||||
width: 36px;
|
|
||||||
height: 36px;
|
|
||||||
font-size: 1.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-name {
|
|
||||||
font-size: 0.95rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.player-clock {
|
|
||||||
font-size: 1.1rem;
|
|
||||||
min-width: 80px;
|
|
||||||
padding: 4px 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
font-size: 1.2rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
{
|
{
|
||||||
"scripts": {
|
"scripts": {},
|
||||||
"build:css": "node build-less.js"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/css": "^1.2.0",
|
"@eslint/css": "^1.2.0",
|
||||||
"@eslint/js": "^10.0.1",
|
"@eslint/js": "^10.0.1",
|
||||||
"@eslint/json": "^1.2.0",
|
"@eslint/json": "^1.2.0",
|
||||||
"@eslint/markdown": "^8.0.2",
|
"@eslint/markdown": "^8.0.2",
|
||||||
"eslint": "^10.4.0",
|
"eslint": "^10.4.0",
|
||||||
"globals": "^17.6.0",
|
"globals": "^17.6.0"
|
||||||
"less": "^4.6.4"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
207
server.py
207
server.py
@@ -1,27 +1,144 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
Lokaler Server für Lara Schachturnier
|
Lokaler Server für Lara Schachturnier
|
||||||
Serviert statische Dateien direkt.
|
Serviert statische Dateien und bietet Stockfish-Analyse.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import http.server
|
import http.server
|
||||||
import socketserver
|
import socketserver
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import socket
|
||||||
|
|
||||||
PORT = int(os.environ.get("PORT", 8111))
|
PORT = int(os.environ.get("PORT", 8111))
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
|
STOCKFISH_PATH = os.environ.get("STOCKFISH_PATH")
|
||||||
|
if not STOCKFISH_PATH:
|
||||||
|
win_path = os.path.join(BASE_DIR, "stockfish.exe")
|
||||||
|
if os.path.exists(win_path):
|
||||||
|
STOCKFISH_PATH = win_path
|
||||||
|
else:
|
||||||
|
STOCKFISH_PATH = "stockfish"
|
||||||
|
|
||||||
|
STOCKFISH_DEPTH = int(os.environ.get("STOCKFISH_DEPTH", 15))
|
||||||
|
|
||||||
|
_stockfish_lock = threading.Lock()
|
||||||
|
|
||||||
|
|
||||||
|
class StockfishEngine:
|
||||||
|
def __init__(self, path):
|
||||||
|
self.path = path
|
||||||
|
self.proc = None
|
||||||
|
|
||||||
|
def start(self):
|
||||||
|
if self.proc:
|
||||||
|
return
|
||||||
|
flags = 0
|
||||||
|
if sys.platform == "win32":
|
||||||
|
flags = subprocess.CREATE_NO_WINDOW
|
||||||
|
|
||||||
|
self.proc = subprocess.Popen(
|
||||||
|
[self.path],
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
bufsize=1,
|
||||||
|
creationflags=flags,
|
||||||
|
)
|
||||||
|
self._send("uci")
|
||||||
|
self._read_until("uciok")
|
||||||
|
self._send("isready")
|
||||||
|
self._read_until("readyok")
|
||||||
|
print(f"[STOCKFISH] Engine gestartet: {self.path}")
|
||||||
|
|
||||||
|
def _send(self, cmd):
|
||||||
|
self.proc.stdin.write(cmd + "\n")
|
||||||
|
self.proc.stdin.flush()
|
||||||
|
|
||||||
|
def _read_until(self, marker):
|
||||||
|
while True:
|
||||||
|
line = self.proc.stdout.readline().strip()
|
||||||
|
if line == marker:
|
||||||
|
return
|
||||||
|
|
||||||
|
def evaluate(self, fen):
|
||||||
|
self._send(f"position fen {fen}")
|
||||||
|
self._send(f"go movetime 10000")
|
||||||
|
|
||||||
|
score_cp = None
|
||||||
|
score_mate = None
|
||||||
|
bestmove = None
|
||||||
|
pv = None
|
||||||
|
last_depth = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
line = self.proc.stdout.readline().strip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if "score cp" in line:
|
||||||
|
m = re.search(r"score cp (-?\d+)", line)
|
||||||
|
if m:
|
||||||
|
score_cp = int(m.group(1))
|
||||||
|
if "score mate" in line:
|
||||||
|
m = re.search(r"score mate (-?\d+)", line)
|
||||||
|
if m:
|
||||||
|
score_mate = int(m.group(1))
|
||||||
|
if " pv " in line:
|
||||||
|
pv = line.split(" pv ", 1)[1]
|
||||||
|
|
||||||
|
depth_m = re.search(r"depth (\d+)", line)
|
||||||
|
if depth_m and score_cp is not None:
|
||||||
|
new_depth = int(depth_m.group(1))
|
||||||
|
if new_depth != last_depth:
|
||||||
|
last_depth = new_depth
|
||||||
|
yield {
|
||||||
|
"scoreCp": score_cp,
|
||||||
|
"scoreMate": score_mate,
|
||||||
|
"bestMove": None,
|
||||||
|
"pv": pv,
|
||||||
|
"depth": new_depth,
|
||||||
|
}
|
||||||
|
|
||||||
|
if line.startswith("bestmove"):
|
||||||
|
parts = line.split()
|
||||||
|
bestmove = parts[1] if len(parts) > 1 else None
|
||||||
|
break
|
||||||
|
|
||||||
|
yield {
|
||||||
|
"scoreCp": score_cp,
|
||||||
|
"scoreMate": score_mate,
|
||||||
|
"bestMove": bestmove,
|
||||||
|
"pv": pv,
|
||||||
|
}
|
||||||
|
|
||||||
|
def stop(self):
|
||||||
|
if self.proc:
|
||||||
|
self.proc.terminate()
|
||||||
|
self.proc = None
|
||||||
|
|
||||||
|
|
||||||
|
_engine = StockfishEngine(STOCKFISH_PATH)
|
||||||
|
|
||||||
|
|
||||||
|
class Handler(http.server.BaseHTTPRequestHandler):
|
||||||
|
|
||||||
class StaticHandler(http.server.BaseHTTPRequestHandler):
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path == "/":
|
if self.path == "/":
|
||||||
self.path = "/index.html"
|
self.path = "/index.html"
|
||||||
|
|
||||||
filepath = os.path.join(os.path.dirname(os.path.abspath(__file__)), self.path.lstrip("/"))
|
filepath = os.path.join(BASE_DIR, self.path.lstrip("/"))
|
||||||
|
|
||||||
if os.path.isfile(filepath):
|
if os.path.isfile(filepath):
|
||||||
content_types = {
|
content_types = {
|
||||||
".html": "text/html",
|
".html": "text/html",
|
||||||
".css": "text/css",
|
".css": "text/css",
|
||||||
".less": "text/css",
|
|
||||||
".js": "application/javascript",
|
".js": "application/javascript",
|
||||||
".json": "application/json",
|
".json": "application/json",
|
||||||
".png": "image/png",
|
".png": "image/png",
|
||||||
@@ -37,29 +154,101 @@ class StaticHandler(http.server.BaseHTTPRequestHandler):
|
|||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
self.send_header("Content-Type", f"{content_type}; charset=utf-8")
|
self.send_header("Content-Type", f"{content_type}; charset=utf-8")
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Cache-Control", "no-cache")
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
self.wfile.write(content)
|
self.wfile.write(content)
|
||||||
else:
|
else:
|
||||||
self.send_response(404)
|
self.send_response(404)
|
||||||
self.end_headers()
|
self.end_headers()
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path == "/evaluate":
|
||||||
|
length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = self.rfile.read(length) if length else b"{}"
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(body)
|
||||||
|
fen = data.get("fen", "")
|
||||||
|
except (json.JSONDecodeError, TypeError):
|
||||||
|
self._send_json({"error": "Invalid JSON"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
|
if not fen:
|
||||||
|
self._send_json({"error": "Missing fen"}, 400)
|
||||||
|
return
|
||||||
|
|
||||||
|
self.send_response(200)
|
||||||
|
self.send_header("Content-Type", "application/x-ndjson; charset=utf-8")
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Cache-Control", "no-cache")
|
||||||
|
self.send_header("Connection", "close")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.flush()
|
||||||
|
|
||||||
|
# TCP_NODELAY für sofortiges Senden
|
||||||
|
try:
|
||||||
|
self.connection.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self.close_connection = True
|
||||||
|
try:
|
||||||
|
with _stockfish_lock:
|
||||||
|
try:
|
||||||
|
_engine.start()
|
||||||
|
except FileNotFoundError:
|
||||||
|
self.wfile.write(
|
||||||
|
json.dumps({"error": "Stockfish nicht gefunden"}).encode("utf-8") + b"\n"
|
||||||
|
)
|
||||||
|
self.wfile.flush()
|
||||||
|
return
|
||||||
|
|
||||||
|
for result in _engine.evaluate(fen):
|
||||||
|
data = json.dumps(result).encode("utf-8") + b"\n"
|
||||||
|
self.wfile.write(data)
|
||||||
|
self.wfile.flush()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[STOCKFISH] Fehler: {e}")
|
||||||
|
self.wfile.write(
|
||||||
|
json.dumps({"error": str(e)}).encode("utf-8") + b"\n"
|
||||||
|
)
|
||||||
|
self.wfile.flush()
|
||||||
|
else:
|
||||||
|
self._send_json({"error": "Not found"}, 404)
|
||||||
|
|
||||||
|
def _send_json(self, obj, status=200):
|
||||||
|
self.send_response(status)
|
||||||
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
||||||
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
|
self.send_header("Cache-Control", "no-cache")
|
||||||
|
self.end_headers()
|
||||||
|
self.wfile.write(json.dumps(obj).encode("utf-8"))
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
print(f"[{self.log_date_time_string()}] {args[0]}")
|
print(f"[{self.log_date_time_string()}] {args[0]}")
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print(" [TROPHY] Lara Kiesewetter – Live Schachturnier")
|
print(" [TROPHY] Lara Kiesewetter - Live Schachturnier")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
print(f" Server läuft auf: http://localhost:{PORT}")
|
print(f" Server laeuft auf: http://localhost:{PORT}")
|
||||||
print(f" Drücke Ctrl+C zum Beenden")
|
if os.path.exists(STOCKFISH_PATH) or STOCKFISH_PATH == "stockfish":
|
||||||
|
print(f" Stockfish-Analyse aktiv (depth={STOCKFISH_DEPTH})")
|
||||||
|
else:
|
||||||
|
print(f" Stockfish nicht gefunden unter: {STOCKFISH_PATH}")
|
||||||
|
print(f" Druecke Ctrl+C zum Beenden")
|
||||||
print("=" * 50)
|
print("=" * 50)
|
||||||
|
|
||||||
with socketserver.TCPServer(("", PORT), StaticHandler) as httpd:
|
socketserver.ThreadingTCPServer.allow_reuse_address = True
|
||||||
|
with socketserver.ThreadingTCPServer(("", PORT), Handler) as httpd:
|
||||||
print(f"\n[SERVER] Server gestartet: http://localhost:{PORT}\n")
|
print(f"\n[SERVER] Server gestartet: http://localhost:{PORT}\n")
|
||||||
try:
|
try:
|
||||||
httpd.serve_forever()
|
httpd.serve_forever()
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("\n[BYE] Server gestoppt.")
|
print("\n[BYE] Server gestoppt.")
|
||||||
|
_engine.stop()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
60
style.css
60
style.css
@@ -133,6 +133,66 @@ header h1 {
|
|||||||
#board [data-square].last-move-highlight {
|
#board [data-square].last-move-highlight {
|
||||||
box-shadow: inset 0 0 3px 3px rgba(255, 200, 0, 0.7);
|
box-shadow: inset 0 0 3px 3px rgba(255, 200, 0, 0.7);
|
||||||
}
|
}
|
||||||
|
/* Board Row: Board + Eval-Bar nebeneinander */
|
||||||
|
#board-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 540px;
|
||||||
|
}
|
||||||
|
#eval-bar-container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 36px;
|
||||||
|
min-height: 350px;
|
||||||
|
}
|
||||||
|
#eval-bar {
|
||||||
|
flex: 1;
|
||||||
|
width: 16px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border-radius: 8px;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||||
|
}
|
||||||
|
#eval-bar-fill {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
width: 100%;
|
||||||
|
background: linear-gradient(to top, #f0f0f0 0%, #d0d0d0 100%);
|
||||||
|
transition: height 0.3s ease;
|
||||||
|
border-radius: 0 0 7px 7px;
|
||||||
|
height: 50%;
|
||||||
|
}
|
||||||
|
#eval-bar-marker {
|
||||||
|
position: absolute;
|
||||||
|
left: -2px;
|
||||||
|
right: -2px;
|
||||||
|
height: 2px;
|
||||||
|
background: #000;
|
||||||
|
transition: top 0.3s ease;
|
||||||
|
z-index: 2;
|
||||||
|
box-shadow: 0 0 3px rgba(0, 0, 0, 0.5);
|
||||||
|
top: 50%;
|
||||||
|
}
|
||||||
|
#eval-info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
#eval-score {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #fff;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
#pgn-panel,
|
#pgn-panel,
|
||||||
#pgn-panel-mobile {
|
#pgn-panel-mobile {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user