(function () {
// --- Konfiguration ---
const MEMORIZE_TIME_SECONDS = 15;
const ACTIVE_CHALLENGE_STORAGE_KEY = "loremIpsumActiveChallenge";
const CHALLENGE_DATA_PREFIX = "[[loremIpsumChallenge:";
// 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;
let inputStartMs = null;
// 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;
let activeChallenge = null;
// --- 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;
inputStartMs = null;
// 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();
}
inputStartMs = Date.now();
}
// Entfernt alles, was beim Vergleichen nicht zaehlen soll.
function normalizeWord(word) {
return word
.toLowerCase()
.replace(/ä/g, "ae")
.replace(/ö/g, "oe")
.replace(/ü/g, "ue")
.replace(/ß/g, "ss")
.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 getChallengeService() {
if (!window.config || !window.ChallengeService) {
return null;
}
return new window.ChallengeService(window.config);
}
function getMessageService() {
if (!window.config || !window.MessageService) {
return null;
}
return new window.MessageService(window.config);
}
function readActiveChallenge() {
// Challenge-Runden werden aus der Nachrichten-Seite gestartet.
// Der dafuer gespeicherte Kontext entscheidet, welcher API-/Nachrichten-Flow nach dem Spiel laeuft.
const raw = sessionStorage.getItem(ACTIVE_CHALLENGE_STORAGE_KEY);
if (!raw) {
return null;
}
try {
const challenge = JSON.parse(raw);
if (!challenge || challenge.id === undefined || challenge.id === null || !challenge.opponent) {
return null;
}
return challenge;
} catch {
return null;
}
}
function hasActiveChallenge() {
return Boolean(activeChallenge && activeChallenge.id !== undefined && activeChallenge.id !== null);
}
function isChallengeSecondRound() {
// In der zweiten Runde spielt der Herausforderer gegen den bereits bekannten Gegner-Score.
return hasActiveChallenge()
&& activeChallenge.role === "challenger"
&& activeChallenge.opponentScore !== null
&& activeChallenge.opponentScore !== undefined;
}
function isChallengeFirstRound() {
// In der ersten Runde spielt der herausgeforderte User und sendet sein Resultat an den Herausforderer.
return hasActiveChallenge() && activeChallenge.role === "opponent";
}
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) {
// Normale Spielrunden werden direkt im Score-/Leaderboard-Backend gespeichert.
// Challenge-Runden verwenden je nach Rolle einen separaten Ablauf weiter unten.
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",
);
}
function renderChallengeResult(score) {
// Zeigt das lokale Ergebnis der finalen Challenge-Runde sofort an,
// unabhaengig davon, ob der Backend-Abschluss erfolgreich ist.
if (!scoreSaveFeedback || !isChallengeSecondRound()) {
return;
}
const opponentScore = Number(activeChallenge.opponentScore);
if (Number.isNaN(opponentScore)) {
return;
}
const auth = getAuth();
const ownName = auth?.username || "Du";
const opponentName = activeChallenge.opponent;
const result = score > opponentScore
? "winner"
: score < opponentScore
? "loser"
: "draw";
const headline = result === "winner"
? "Du gewinnst die Challenge"
: result === "loser"
? opponentName + " gewinnt die Challenge"
: "Unentschieden";
const outcomeText = result === "winner"
? "Gewonnen"
: result === "loser"
? "Verloren"
: "Unentschieden";
const outcomeImage = result === "winner"
? "image/sieg.png"
: result === "loser"
? "image/verloren.png"
: "image/unentschieden.png";
const graphic = document.createElement("div");
graphic.className = "play-challenge-result play-challenge-result-" + result;
const outcomeHeader = document.createElement("div");
outcomeHeader.className = "challenge-outcome challenge-outcome-" + (result === "winner" ? "win" : result === "loser" ? "loss" : "draw");
const outcomeImg = document.createElement("img");
outcomeImg.src = outcomeImage;
outcomeImg.alt = outcomeText;
const outcomeLabel = document.createElement("strong");
outcomeLabel.textContent = outcomeText;
outcomeHeader.append(outcomeImg, outcomeLabel);
const title = document.createElement("div");
title.className = "play-challenge-result-title";
title.textContent = headline;
const scores = document.createElement("div");
scores.className = "play-challenge-result-scores";
const ownScore = document.createElement("div");
ownScore.className = "play-challenge-result-score";
ownScore.innerHTML = "";
ownScore.querySelector("strong").textContent = ownName;
ownScore.querySelector("span").textContent = score + " Punkte";
const otherScore = document.createElement("div");
otherScore.className = "play-challenge-result-score";
otherScore.innerHTML = "";
otherScore.querySelector("strong").textContent = opponentName;
otherScore.querySelector("span").textContent = opponentScore + " Punkte";
scores.append(ownScore, otherScore);
graphic.append(outcomeHeader, title, scores);
scoreSaveFeedback.insertAdjacentElement("afterend", graphic);
}
async function completeChallenge(scoreData) {
// Primaerer Abschluss ueber das Challenge-Backend.
// Wenn dieser Endpoint ablehnt, faellt submitScore auf den Nachrichten-Fallback zurueck.
const auth = getAuth();
if (!auth || !hasActiveChallenge()) {
return;
}
const challengeService = getChallengeService();
if (!challengeService) {
showScoreSaveFeedback(
"Score gespeichert, aber der Challenge-Service konnte nicht geladen werden.",
"warning",
);
return;
}
const result = await challengeService.completeChallenge(
auth.username,
auth.password,
activeChallenge.id,
scoreData.score,
scoreData.time,
scoreData.text,
scoreData.userWrittenText,
);
if (!result.ok) {
console.warn("Challenge-Abschluss nicht moeglich, nutze Nachrichten-Fallback. Status:", result.status);
return false;
}
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
activeChallenge = null;
showScoreSaveFeedback(
"Challenge abgeschlossen. Beide User erhalten eine Ergebnisnachricht.",
"success",
);
if (typeof window.updateMessagesNavState === "function") {
window.updateMessagesNavState();
}
return true;
}
async function finishFirstChallengeRound(scoreData) {
// Der Gegner speichert zuerst seinen Score und informiert danach den Herausforderer.
await saveScore(scoreData);
await notifyChallenger(scoreData);
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
activeChallenge = null;
showScoreSaveFeedback("Resultat wurde an den Herausforderer gesendet.", "success");
if (typeof window.updateMessagesNavState === "function") {
window.updateMessagesNavState();
}
}
async function notifyChallenger(scoreData) {
// Nutzt den bestehenden Nachrichten-Endpunkt, um dem Herausforderer den Gegner-Score zu senden.
const auth = getAuth();
const messageService = getMessageService();
if (!auth || !messageService || !activeChallenge.challenger) {
return;
}
const challengeData = {
id: activeChallenge.id,
challenger: activeChallenge.challenger,
opponent: auth.username,
opponentScore: scoreData.score,
};
const messageText =
CHALLENGE_DATA_PREFIX +
JSON.stringify(challengeData) +
"]]" +
"\n" +
auth.username +
" hat gespielt und " +
scoreData.score +
" Punkte erreicht. Jetzt bist du dran.";
const result = await messageService.postMessage(
auth.username,
auth.password,
activeChallenge.challenger,
"challenge",
messageText,
);
if (!result.ok) {
showScoreSaveFeedback(
"Challenge wurde gespeichert, aber die Nachricht an den Herausforderer konnte nicht gesendet werden.",
"warning",
);
}
}
function getChallengeWinner(challengerScore, opponentScore) {
if (challengerScore > opponentScore) {
return activeChallenge.challenger || getAuth()?.username || "Herausforderer";
}
if (challengerScore < opponentScore) {
return activeChallenge.opponent || "Gegner";
}
return "draw";
}
async function sendChallengeResultMessage(recipient, resultData, text) {
const auth = getAuth();
const messageService = getMessageService();
if (!auth || !messageService || !recipient) {
return false;
}
const messageText =
CHALLENGE_DATA_PREFIX +
JSON.stringify(resultData) +
"]]" +
"\n" +
text;
const result = await messageService.postMessage(
auth.username,
auth.password,
recipient,
"challenge",
messageText,
);
return result.ok;
}
async function finishFinalChallengeWithMessages(scoreData) {
// Fallback fuer den finalen Challenge-Abschluss:
// Score speichern, Gewinner lokal berechnen und Ergebnis per Nachricht an den Gegner senden.
const auth = getAuth();
if (!auth || !activeChallenge) {
return;
}
await saveScore(scoreData);
const opponentScore = Number(activeChallenge.opponentScore);
const winner = getChallengeWinner(scoreData.score, opponentScore);
const resultData = {
id: activeChallenge.id,
challenger: activeChallenge.challenger || auth.username,
opponent: activeChallenge.opponent,
challengerScore: scoreData.score,
opponentScore: opponentScore,
winner: winner,
};
const winnerText = winner === "draw"
? "Unentschieden"
: "Gewinner: " + winner;
const resultText =
"Challenge abgeschlossen. " +
resultData.challenger +
": " +
resultData.challengerScore +
" Punkte, " +
resultData.opponent +
": " +
resultData.opponentScore +
" Punkte. " +
winnerText +
".";
const opponentMessageSent = await sendChallengeResultMessage(
activeChallenge.opponent,
resultData,
resultText,
);
sessionStorage.removeItem(ACTIVE_CHALLENGE_STORAGE_KEY);
activeChallenge = null;
if (!opponentMessageSent) {
showScoreSaveFeedback(
"Score gespeichert, aber die Ergebnisnachricht an den Gegner konnte nicht gesendet werden.",
"warning",
);
return;
}
showScoreSaveFeedback(
"Challenge abgeschlossen. Ergebnisnachricht wurde an den Gegner gesendet.",
"success",
);
if (typeof window.updateMessagesNavState === "function") {
window.updateMessagesNavState();
}
}
async function submitScore() {
// Gemeinsamer Abschluss fuer normale Spiele und Challenge-Runden.
// Der aktive Challenge-Kontext entscheidet, welcher Speicher-/Nachrichtenfluss verwendet wird.
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);
renderChallengeResult(score);
const inputDurationSeconds = inputStartMs
? Math.max(1, Math.round((Date.now() - inputStartMs) / 1000))
: MEMORIZE_TIME_SECONDS;
// Genau dieser Rundentext wird gespeichert, damit Leaderboard/Score-Details nachvollziehbar bleiben.
const scoreData = {
score: score,
time: inputDurationSeconds,
text: currentGameText,
userWrittenText: userInput,
};
try {
if (isChallengeFirstRound()) {
showScoreSaveFeedback("Resultat wird an den Herausforderer gesendet...", "info");
await finishFirstChallengeRound(scoreData);
} else if (hasActiveChallenge()) {
showScoreSaveFeedback("Challenge-Resultat wird gesendet...", "info");
let completed = false;
try {
completed = await completeChallenge(scoreData);
} catch (error) {
console.warn("Challenge-Abschluss fehlgeschlagen, nutze Nachrichten-Fallback.", error);
}
if (!completed) {
showScoreSaveFeedback("Backend-Abschluss nicht moeglich. Ergebnis wird ueber Nachrichten gesendet...", "info");
await finishFinalChallengeWithMessages(scoreData);
}
} else {
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() {
// Die Spielseite wird dynamisch geladen; daher werden DOM-Elemente und Events hier initialisiert.
clearInterval(timerInterval);
activeChallenge = readActiveChallenge();
// 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");
const challengeHint = document.getElementById("challengeHint");
if (challengeHint && activeChallenge) {
const opponentScore = activeChallenge.opponentScore !== null && activeChallenge.opponentScore !== undefined
? " Score: " + activeChallenge.opponentScore + "."
: "";
const hintPrefix = activeChallenge.role === "opponent"
? "Du spielst zuerst gegen "
: "Finale Runde gegen ";
challengeHint.textContent = hintPrefix + activeChallenge.opponent + "." + opponentScore;
challengeHint.classList.remove("d-none");
}
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.");
}
});
if (userTextInput) {
userTextInput.addEventListener("keydown", (e) => {
const key = e.key.toLowerCase();
if (
(e.ctrlKey || e.metaKey) &&
(key === "c" || key === "v" || key === "x" || key === "a")
) {
e.preventDefault();
alert("Yeah, cheating is not sooo nice...");
}
});
userTextInput.addEventListener("paste", (e) => e.preventDefault());
userTextInput.addEventListener("copy", (e) => e.preventDefault());
userTextInput.addEventListener("cut", (e) => e.preventDefault());
userTextInput.addEventListener("input", () => {
if (!inputStartMs) {
inputStartMs = Date.now();
}
});
}
if (targetTextDisplay) {
targetTextDisplay.addEventListener("copy", (e) => e.preventDefault());
targetTextDisplay.style.userSelect = "none";
}
};
})();