- Treffer-Toleranz: Ziel wird geometrisch + bevorzugt getroffen, auch wenn verdeckt - Zwei Ranglisten: Hall of Fame (Allzeit-Top-5) + Heute (Top-10) - Anti-Cheat: server-autoritative Sessions (start/hit), Score = Server-Zahl, Mindestabstand gegen Auto-Klicker, Deckelung nach Spielzeit; Score ohne Session -> 400 - CLI (server/admin.js): list / delete <name> / clear-today / clear-all - Zwei Betriebsmodi per CLI (live, ohne Neustart): public (/ = Spiel) und private (/ gesperrt, Spiel unter geheimem /pfad) + "not allowed to play"-Seite - Board-Icons von Emojis auf echte SVG-Icons, Board heisst "Hall of Fame" Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
91 lines
2.8 KiB
JavaScript
91 lines
2.8 KiB
JavaScript
// Gemeinsame Ranglisten-Logik fuer Server (index.js) und CLI (admin.js).
|
|
// Zwei Ansichten aus derselben Datei: Allzeit-Top-5 + Heute-Top-10.
|
|
|
|
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const DATA_DIR = join(__dirname, '..', 'data');
|
|
export const LEADERBOARD_FILE = join(DATA_DIR, 'leaderboard.json');
|
|
|
|
const ALLTIME_COUNT = 5;
|
|
const TODAY_COUNT = 10;
|
|
const KEEP_ALLTIME = 100; // Datei klein halten
|
|
|
|
export function readLeaderboard() {
|
|
try {
|
|
if (!existsSync(LEADERBOARD_FILE)) return [];
|
|
const data = JSON.parse(readFileSync(LEADERBOARD_FILE, 'utf8'));
|
|
return Array.isArray(data) ? data : [];
|
|
} catch (err) {
|
|
console.warn('[leaderboard] Datei unlesbar:', err.message);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function writeLeaderboard(list) {
|
|
if (!existsSync(DATA_DIR)) mkdirSync(DATA_DIR, { recursive: true });
|
|
writeFileSync(LEADERBOARD_FILE, JSON.stringify(list, null, 2));
|
|
}
|
|
|
|
export function topN(list, n) {
|
|
return [...list].sort((a, b) => b.score - a.score).slice(0, n);
|
|
}
|
|
|
|
// Server-lokaler Tages-Schluessel (YYYY-M-D)
|
|
function dayKey(d) {
|
|
return `${d.getFullYear()}-${d.getMonth() + 1}-${d.getDate()}`;
|
|
}
|
|
export function isToday(entry) {
|
|
return entry && entry.at && dayKey(new Date(entry.at)) === dayKey(new Date());
|
|
}
|
|
|
|
// Zwei Ranglisten aus derselben Liste
|
|
export function buildBoards(list) {
|
|
return {
|
|
allTime: topN(list, ALLTIME_COUNT),
|
|
today: topN(list.filter(isToday), TODAY_COUNT),
|
|
};
|
|
}
|
|
|
|
// Score eintragen -> aktualisierte Boards. Behaelt Allzeit-Top-100 + alle heutigen Eintraege.
|
|
export function addScore(name, score) {
|
|
const list = readLeaderboard();
|
|
list.push({ name, score, at: new Date().toISOString() });
|
|
|
|
const keep = topN(list, KEEP_ALLTIME);
|
|
const keepSet = new Set(keep);
|
|
for (const e of list) if (isToday(e) && !keepSet.has(e)) keep.push(e);
|
|
|
|
writeLeaderboard(keep);
|
|
return buildBoards(keep);
|
|
}
|
|
|
|
// --- Verwaltung (CLI) ---
|
|
|
|
// Loescht alle Eintraege eines Spielers (Name case-insensitiv exakt). Gibt Anzahl zurueck.
|
|
export function deletePlayer(name) {
|
|
const target = String(name || '').trim().toLowerCase();
|
|
if (!target) return 0;
|
|
const list = readLeaderboard();
|
|
const kept = list.filter((e) => String(e.name || '').trim().toLowerCase() !== target);
|
|
writeLeaderboard(kept);
|
|
return list.length - kept.length;
|
|
}
|
|
|
|
// Leert die gesamte Rangliste. Gibt Anzahl geloeschter Eintraege zurueck.
|
|
export function clearAll() {
|
|
const n = readLeaderboard().length;
|
|
writeLeaderboard([]);
|
|
return n;
|
|
}
|
|
|
|
// Entfernt nur die heutigen Eintraege. Gibt Anzahl zurueck.
|
|
export function clearToday() {
|
|
const list = readLeaderboard();
|
|
const kept = list.filter((e) => !isToday(e));
|
|
writeLeaderboard(kept);
|
|
return list.length - kept.length;
|
|
}
|