380 lines
14 KiB
JavaScript
380 lines
14 KiB
JavaScript
(function() {
|
|
// --- Konfiguration ---
|
|
const MEMORIZE_TIME_SECONDS = 15;
|
|
|
|
// Bausteine fuer den zufaelligen Rundentext. Alles bleibt lokal, damit das Spiel ohne Backend starten kann.
|
|
const TEXT_PARTS = {
|
|
subjects: [
|
|
"Der flinke Entwickler",
|
|
"Die neugierige Studentin",
|
|
"Ein mueder Professor",
|
|
"Das kleine Frontend",
|
|
"Der mutige Browser",
|
|
"Eine schlaue Funktion",
|
|
"Der vergessliche Server",
|
|
"Die kreative Gruppe"
|
|
],
|
|
actions: [
|
|
"sortiert leise",
|
|
"debuggt geduldig",
|
|
"vergleicht heimlich",
|
|
"speichert vorsichtig",
|
|
"rendert ploetzlich",
|
|
"zaehlt konzentriert",
|
|
"testet neugierig",
|
|
"kompiliert langsam"
|
|
],
|
|
objects: [
|
|
"sieben blaue Buttons",
|
|
"drei lange Variablen",
|
|
"neun goldene Woerter",
|
|
"vier kaputte Formulare",
|
|
"acht schnelle Requests",
|
|
"zwei leuchtende Karten",
|
|
"fuenf stille Fehlermeldungen",
|
|
"sechs winzige Icons"
|
|
],
|
|
places: [
|
|
"im hellen Dashboard",
|
|
"unter dem dunklen Navbar",
|
|
"neben dem alten Footer",
|
|
"zwischen Login und Leaderboard",
|
|
"vor dem ersten Kaffee",
|
|
"waehrend der Lernphase",
|
|
"hinter dem lokalen Server",
|
|
"mitten im Semesterprojekt"
|
|
],
|
|
endings: [
|
|
"Danach lacht der Code, weil alles endlich funktioniert.",
|
|
"Am Ende merkt sich niemand die Semikolons, aber alle die Punkte.",
|
|
"Kurz darauf blinkt die Konsole und behauptet, sie sei unschuldig.",
|
|
"Spaeter landet der Score im Ranking und wartet auf Applaus.",
|
|
"Dabei bleibt die Seite ruhig, obwohl der Timer dramatisch tickt.",
|
|
"Zum Schluss gewinnt, wer die Woerter sauber in Reihenfolge bringt."
|
|
]
|
|
};
|
|
|
|
let timerInterval;
|
|
let currentTime = 0;
|
|
|
|
// Der aktuell angezeigte Text muss bis zur Auswertung stabil bleiben.
|
|
let currentGameText = "";
|
|
let lastGeneratedText = "";
|
|
|
|
// DOM-Referenzen werden erst gesetzt, nachdem pages/play.html dynamisch geladen wurde.
|
|
let phaseStart;
|
|
let phaseMemorize;
|
|
let phaseInput;
|
|
let phaseResult;
|
|
let targetTextDisplay;
|
|
let timerDisplay;
|
|
let userTextInput;
|
|
let resultScore;
|
|
let resultOriginal;
|
|
let resultInput;
|
|
let gameStatus;
|
|
let scoreSaveFeedback;
|
|
let btnSubmitScore;
|
|
|
|
// --- Funktionen ---
|
|
|
|
function getRandomItem(items) {
|
|
return items[Math.floor(Math.random() * items.length)];
|
|
}
|
|
|
|
// Erstellt pro Runde zwei zufaellige Saetze plus Schluss-Satz und vermeidet direkte Wiederholungen.
|
|
function generateGameText() {
|
|
let generatedText = "";
|
|
|
|
do {
|
|
const firstSentence = [
|
|
getRandomItem(TEXT_PARTS.subjects),
|
|
getRandomItem(TEXT_PARTS.actions),
|
|
getRandomItem(TEXT_PARTS.objects),
|
|
getRandomItem(TEXT_PARTS.places)
|
|
].join(" ") + ".";
|
|
|
|
const secondSentence = [
|
|
getRandomItem(TEXT_PARTS.subjects),
|
|
getRandomItem(TEXT_PARTS.actions),
|
|
getRandomItem(TEXT_PARTS.objects),
|
|
getRandomItem(TEXT_PARTS.places)
|
|
].join(" ") + ".";
|
|
|
|
generatedText = firstSentence + " " + secondSentence + " " + getRandomItem(TEXT_PARTS.endings);
|
|
} while (generatedText === lastGeneratedText);
|
|
|
|
lastGeneratedText = generatedText;
|
|
return generatedText;
|
|
}
|
|
|
|
function startGame() {
|
|
if (!phaseStart || !phaseMemorize) return;
|
|
|
|
// Startansicht ausblenden und den neu generierten Text fuer die Lernphase anzeigen.
|
|
phaseStart.classList.add('d-none');
|
|
phaseMemorize.classList.remove('d-none');
|
|
|
|
if(gameStatus) {
|
|
gameStatus.textContent = "Lernphase";
|
|
gameStatus.className = "badge fs-6 px-3 py-2";
|
|
gameStatus.style.backgroundColor = "#ffd166";
|
|
gameStatus.style.color = "#1b1b2f";
|
|
}
|
|
|
|
currentGameText = generateGameText();
|
|
if(targetTextDisplay) targetTextDisplay.textContent = currentGameText;
|
|
|
|
// Nach Ablauf des Timers wird automatisch zur Eingabe gewechselt.
|
|
currentTime = MEMORIZE_TIME_SECONDS;
|
|
if(timerDisplay) timerDisplay.textContent = currentTime;
|
|
|
|
timerInterval = setInterval(() => {
|
|
currentTime--;
|
|
if(timerDisplay) timerDisplay.textContent = currentTime;
|
|
|
|
if (currentTime <= 0) {
|
|
endMemorizePhase();
|
|
}
|
|
}, 1000);
|
|
}
|
|
|
|
function endMemorizePhase() {
|
|
clearInterval(timerInterval);
|
|
|
|
if (!phaseMemorize || !phaseInput) return;
|
|
|
|
// Text verschwindet, Eingabefeld erscheint: ab hier zaehlt nur noch das Gedaechtnis.
|
|
phaseMemorize.classList.add('d-none');
|
|
phaseInput.classList.remove('d-none');
|
|
|
|
if(gameStatus) {
|
|
gameStatus.textContent = "Eingabe";
|
|
gameStatus.className = "badge fs-6 px-3 py-2";
|
|
gameStatus.style.backgroundColor = "#4a6fa5";
|
|
gameStatus.style.color = "#fff";
|
|
}
|
|
|
|
if(userTextInput) {
|
|
userTextInput.value = "";
|
|
userTextInput.focus();
|
|
}
|
|
}
|
|
|
|
// Entfernt alles, was beim Vergleichen nicht zaehlen soll.
|
|
function normalizeWord(word) {
|
|
return word.toLowerCase().replace(/[.,\/#!$%\^&\*;:{}=\-_`~()]/g, "");
|
|
}
|
|
|
|
// Behält die sichtbaren Woerter separat, damit Satzzeichen in der Ergebnisanzeige erhalten bleiben.
|
|
function getWords(text) {
|
|
return text.split(/\s+/).filter(word => word.length > 0);
|
|
}
|
|
|
|
function calculateScore(original, input) {
|
|
if (!original || !input) return 0;
|
|
|
|
// Score-Regel: gleiche Woerter an gleicher Position, Satzzeichen und Grossschreibung ignoriert.
|
|
const cleanOriginal = getWords(original).map(normalizeWord).filter(word => word.length > 0);
|
|
const cleanInput = getWords(input).map(normalizeWord).filter(word => word.length > 0);
|
|
|
|
let correctWords = 0;
|
|
const limit = Math.min(cleanOriginal.length, cleanInput.length);
|
|
|
|
for (let i = 0; i < limit; i++) {
|
|
if (cleanInput[i] === cleanOriginal[i]) {
|
|
correctWords++;
|
|
}
|
|
}
|
|
|
|
return correctWords;
|
|
}
|
|
|
|
// Baut ein einzelnes farbiges Wort-Label fuer den Ergebnisvergleich.
|
|
function createWordBadge(word, isCorrect) {
|
|
const badge = document.createElement("span");
|
|
badge.className = "badge me-1 mb-1 " + (isCorrect ? "text-bg-success" : "text-bg-danger");
|
|
badge.textContent = word;
|
|
return badge;
|
|
}
|
|
|
|
// Markiert Original und Eingabe nach derselben Positionslogik wie calculateScore().
|
|
function renderWordComparison(original, input) {
|
|
if (!resultOriginal || !resultInput) return;
|
|
|
|
const originalWords = getWords(original);
|
|
const inputWords = getWords(input);
|
|
|
|
resultOriginal.innerHTML = "";
|
|
resultInput.innerHTML = "";
|
|
|
|
// Original: rot, wenn das eingegebene Wort an dieser Position fehlt oder falsch ist.
|
|
originalWords.forEach((word, index) => {
|
|
const isCorrect = normalizeWord(word) === normalizeWord(inputWords[index] || "");
|
|
resultOriginal.appendChild(createWordBadge(word, isCorrect));
|
|
});
|
|
|
|
// Eingabe: rot, wenn das Wort nicht zum Originalwort an derselben Position passt.
|
|
inputWords.forEach((word, index) => {
|
|
const isCorrect = normalizeWord(word) === normalizeWord(originalWords[index] || "");
|
|
resultInput.appendChild(createWordBadge(word, isCorrect));
|
|
});
|
|
}
|
|
|
|
function getAuth() {
|
|
if (!window.AppAuth || typeof window.AppAuth.getAuth !== "function") {
|
|
return null;
|
|
}
|
|
|
|
return window.AppAuth.getAuth();
|
|
}
|
|
|
|
function getScoreService() {
|
|
if (!window.config || !window.ScoreService) {
|
|
return null;
|
|
}
|
|
|
|
return new window.ScoreService(window.config);
|
|
}
|
|
|
|
function showScoreSaveFeedback(message, type) {
|
|
if (!scoreSaveFeedback) return;
|
|
|
|
scoreSaveFeedback.className = "alert alert-" + type + " mb-4";
|
|
scoreSaveFeedback.textContent = message;
|
|
scoreSaveFeedback.classList.remove("d-none");
|
|
}
|
|
|
|
async function saveScore(scoreData) {
|
|
const auth = getAuth();
|
|
if (!auth || !auth.username || !auth.password) {
|
|
showScoreSaveFeedback(
|
|
"Score wurde nur lokal berechnet. Bitte einloggen, damit er im Leaderboard gespeichert wird.",
|
|
"warning"
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Auth-Daten kommen aus login.js; der ScoreService setzt daraus die Backend-Header.
|
|
const scoreService = getScoreService();
|
|
if (!scoreService) {
|
|
showScoreSaveFeedback("Score-Service konnte nicht geladen werden.", "danger");
|
|
return;
|
|
}
|
|
|
|
showScoreSaveFeedback("Score wird gespeichert...", "info");
|
|
|
|
const result = await scoreService.postScore(
|
|
auth.username,
|
|
auth.password,
|
|
scoreData.score,
|
|
scoreData.time,
|
|
scoreData.text,
|
|
scoreData.userWrittenText
|
|
);
|
|
|
|
if (result.ok) {
|
|
const place = result.body && result.body.place ? " Platz " + result.body.place + "." : "";
|
|
showScoreSaveFeedback("Score erfolgreich gespeichert." + place, "success");
|
|
return;
|
|
}
|
|
|
|
if (result.status === 401) {
|
|
showScoreSaveFeedback("Score konnte nicht gespeichert werden: Login ist nicht gültig.", "danger");
|
|
return;
|
|
}
|
|
|
|
showScoreSaveFeedback("Score konnte nicht gespeichert werden (Status " + result.status + ").", "danger");
|
|
}
|
|
|
|
async function submitScore() {
|
|
if (!userTextInput) return;
|
|
|
|
const userInput = userTextInput.value.trim();
|
|
if (!userInput) {
|
|
alert("Bitte geben Sie einen Text ein.");
|
|
return;
|
|
}
|
|
|
|
if (btnSubmitScore) {
|
|
btnSubmitScore.disabled = true;
|
|
btnSubmitScore.textContent = "Wird ausgewertet...";
|
|
}
|
|
|
|
const score = calculateScore(currentGameText, userInput);
|
|
|
|
// Ergebnis sofort anzeigen; das Speichern im Backend passiert danach asynchron.
|
|
if (phaseInput) phaseInput.classList.add('d-none');
|
|
if (phaseResult) phaseResult.classList.remove('d-none');
|
|
|
|
if(gameStatus) {
|
|
gameStatus.textContent = "Abgeschlossen";
|
|
gameStatus.className = "badge fs-6 px-3 py-2";
|
|
gameStatus.style.backgroundColor = "#28a745";
|
|
gameStatus.style.color = "#fff";
|
|
}
|
|
|
|
if (resultScore) resultScore.textContent = score;
|
|
renderWordComparison(currentGameText, userInput);
|
|
|
|
// Genau dieser Rundentext wird gespeichert, damit Leaderboard/Score-Details nachvollziehbar bleiben.
|
|
const scoreData = {
|
|
score: score,
|
|
time: MEMORIZE_TIME_SECONDS,
|
|
text: currentGameText,
|
|
userWrittenText: userInput
|
|
};
|
|
|
|
console.log("Score bereit zum Senden:", scoreData);
|
|
|
|
try {
|
|
await saveScore(scoreData);
|
|
} catch (error) {
|
|
console.error("Fehler beim Speichern des Scores:", error);
|
|
showScoreSaveFeedback("Score konnte wegen eines technischen Fehlers nicht gespeichert werden.", "danger");
|
|
} finally {
|
|
if (btnSubmitScore) {
|
|
btnSubmitScore.disabled = false;
|
|
btnSubmitScore.textContent = "Auswerten & Absenden";
|
|
}
|
|
}
|
|
}
|
|
|
|
window.initPlayPage = function initPlayPage() {
|
|
clearInterval(timerInterval);
|
|
|
|
// Die Navigation laedt play.html per fetch; deshalb werden die Elemente erst hier gesucht.
|
|
phaseStart = document.getElementById('phaseStart');
|
|
phaseMemorize = document.getElementById('phaseMemorize');
|
|
phaseInput = document.getElementById('phaseInput');
|
|
phaseResult = document.getElementById('phaseResult');
|
|
|
|
targetTextDisplay = document.getElementById('targetTextDisplay');
|
|
timerDisplay = document.getElementById('timerDisplay');
|
|
userTextInput = document.getElementById('userTextInput');
|
|
resultScore = document.getElementById('resultScore');
|
|
resultOriginal = document.getElementById('resultOriginal');
|
|
resultInput = document.getElementById('resultInput');
|
|
gameStatus = document.getElementById('gameStatus');
|
|
scoreSaveFeedback = document.getElementById('scoreSaveFeedback');
|
|
|
|
const btnStart = document.getElementById('btnStartGame');
|
|
btnSubmitScore = document.getElementById('btnSubmitScore');
|
|
const btnRestart = document.getElementById('btnRestart');
|
|
const btnLeaderboard = document.getElementById('btnLeaderboard');
|
|
|
|
if (btnStart) btnStart.addEventListener('click', startGame);
|
|
if (btnSubmitScore) btnSubmitScore.addEventListener('click', submitScore);
|
|
if (btnRestart) btnRestart.addEventListener('click', () => window.loadPage("play", "nav-play"));
|
|
if (btnLeaderboard) btnLeaderboard.addEventListener('click', () => {
|
|
const navLink = document.getElementById('nav-leaderboard');
|
|
if (navLink) {
|
|
navLink.click(); // Nutzt die bestehende Navigation inklusive Active-State.
|
|
} else {
|
|
console.warn("Sidebar Link #nav-leaderboard nicht gefunden.");
|
|
}
|
|
});
|
|
};
|
|
|
|
})();
|