| Server IP : 202.61.199.114 / Your IP : 216.73.217.139 Web Server : nginx/1.22.1 System : Linux de.arni-solutions.de 6.1.0-49-amd64 #1 SMP PREEMPT_DYNAMIC Debian 6.1.174-1 (2026-05-26) x86_64 User : web20 ( 1018) PHP Version : 8.4.23 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /tmp/rrpkg/ |
Upload File : |
/* Reel Royale – Stadt Land Fluss (Link-Multiplayer). */
import { $, toast } from '../util.js';
const boot = window.SLF_BOOT || {};
const DEFAULT_CATS = ['Stadt', 'Land', 'Fluss', 'Name', 'Tier', 'Beruf'];
const getCats = () => (R.state && R.state.cats && R.state.cats.length ? R.state.cats : DEFAULT_CATS);
const LETTERS = 'ABCDEFGHIKLMNOPRSTUW'.split('');
const R = { code: null, token: null, myId: null, version: -1, state: null, players: [], status: null, poll: null, renderedPhase: null, saveT: null };
const app = () => $('slfApp');
// Zuletzt benutzte Kategorien merken (für die nächste Runde / neuen Raum)
const CATS_KEY = 'slf_cats';
function loadSavedCats() { try { const a = JSON.parse(localStorage.getItem(CATS_KEY) || 'null'); return Array.isArray(a) && a.length ? a : null; } catch { return null; } }
function saveCats(cats) { try { if (cats && cats.length) localStorage.setItem(CATS_KEY, JSON.stringify(cats)); } catch {} }
// Aktiven Raum merken + in der URL halten, damit ein Reload niemanden aus der Runde wirft.
function rememberRoom() {
try { if (R.code) localStorage.setItem('slf_last', R.code); } catch {}
try { if (R.code) history.replaceState(null, '', location.pathname + '?room=' + R.code); } catch {}
}
function lastRoom() { try { const c = localStorage.getItem('slf_last'); return c && localStorage.getItem('slf_' + c) ? c : null; } catch { return null; } }
async function api(body) {
const r = await fetch('api/room.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
return r.json();
}
const promptName = () => (window.prompt('Dein Name:', boot.username || 'Spieler') || 'Spieler').slice(0, 30);
const esc = (s) => { const d = document.createElement('div'); d.textContent = s; return d.innerHTML; };
const isHost = () => R.myId === 0;
const pickLetter = (used) => { const a = LETTERS.filter((l) => !(used || []).includes(l)); const p = a.length ? a : LETTERS; return p[(Math.random() * p.length) | 0]; };
const initialState = () => ({ phase: 'lobby', round: 0, letter: '', cats: (loadSavedCats() || DEFAULT_CATS).slice(), answers: {}, done: {}, scores: {}, used: [], history: [], readyRound: {} });
const shareLink = () => location.origin + location.pathname + '?room=' + R.code;
function setRoom(d) { R.code = d.code; R.token = d.token; R.myId = d.your_id != null ? d.your_id : (d.you && d.you.id); R.version = d.version; R.state = d.state; R.players = d.players; R.status = d.status; rememberRoom(); }
function applyServer(d) { if (d.version === R.version) { R.players = d.players; return false; } R.version = d.version; R.state = d.state; R.players = d.players; R.status = d.status; return true; }
function startPoll() { stopPoll(); R.poll = setInterval(poll, 1500); }
function stopPoll() { if (R.poll) { clearInterval(R.poll); R.poll = null; } }
async function poll() { if (!R.code) return; let d; try { d = await api({ action: 'state', code: R.code, token: R.token }); } catch { return; } if (!d.ok) return; if (applyServer(d)) render(); else if (R.state && R.state.phase === 'writing') updateWritingStatus(); maybeStartWriting(); }
// ---------- Aktionen ----------
async function createRoom() {
const name = boot.username || promptName();
const d = await api({ action: 'create', game: 'slf', name, maxp: 8, state: initialState(), status: 'lobby' });
if (!d.ok) return toast('SLF', d.message, 'error');
setRoom(d); localStorage.setItem('slf_' + d.code, d.token); startPoll(); render();
}
async function joinRoom(code) {
const stored = localStorage.getItem('slf_' + code);
const d = stored ? await api({ action: 'join', code, token: stored }) : await api({ action: 'join', code, name: boot.username || promptName() });
if (!d.ok) { try { if (localStorage.getItem('slf_last') === code) localStorage.removeItem('slf_last'); } catch {} toast('SLF', d.message, 'error'); showMenu(); return; }
setRoom(d); localStorage.setItem('slf_' + code, d.token); startPoll(); render();
}
async function replaceState(partial, status) {
const st = Object.assign({}, R.state, partial);
const d = await api({ action: 'replace', code: R.code, token: R.token, state: st, status: status || R.status });
if (d.ok) applyServer(d), render();
}
async function startGame() {
let cats = getCats();
const box = document.getElementById('catBox');
if (box) { const p = box.value.split(/[\n,]+/).map((x) => x.trim()).filter(Boolean); if (p.length) cats = [...new Set(p)].slice(0, 12); }
saveCats(cats); // für nächste Runde / neuen Raum merken
// Spielstart → Bereit-Phase (sperrt neue Beitritte: status playing)
await replaceState({ phase: 'ready', round: 1, cats, scores: {}, used: [], history: [], answers: {}, done: {}, votes: {}, readyRound: {} }, 'playing');
}
async function nextRound() {
const s = R.state; const used = [...(s.used || []), s.letter];
await replaceState({ phase: 'ready', round: s.round + 1, scores: commitScores(), used, history: pushHistory(s), answers: {}, done: {}, votes: {}, readyRound: {} }, 'playing');
}
async function endGame() { await replaceState({ phase: 'done', scores: commitScores(), history: pushHistory(R.state) }, 'done'); }
// Bereit-Phase: alle bestätigen → Gastgeber startet das Schreiben
async function confirmReady() {
await api({ action: 'merge', code: R.code, token: R.token, patch: { readyRound: { [R.myId]: 1 } } });
const d = await api({ action: 'state', code: R.code, token: R.token });
if (applyServer(d)) render();
maybeStartWriting();
}
function allReady() { const rr = R.state.readyRound || {}; return R.players.length >= 1 && R.players.every((p) => rr[p.id]); }
function maybeStartWriting() {
if (R.starting || !isHost() || !R.state || R.state.phase !== 'ready' || !allReady()) return;
R.starting = true;
startWritingPhase().finally(() => { R.starting = false; });
}
async function startWritingPhase() {
const s = R.state; const used = s.used || [];
await replaceState({ phase: 'writing', letter: pickLetter(used), answers: {}, done: {}, votes: {} }, 'playing');
}
async function saveMy() {
const ans = {};
getCats().forEach((c, i) => { const el = document.getElementById('slf_in_' + i); if (el) ans[c] = el.value.trim(); });
await api({ action: 'merge', code: R.code, token: R.token, patch: { answers: { [R.myId]: ans }, done: { [R.myId]: true } } });
}
async function stopRound() {
await saveMy();
const d = await api({ action: 'state', code: R.code, token: R.token }); applyServer(d);
await replaceState({ phase: 'reveal', votes: {} }, 'reveal');
}
// Jede Antwort gilt zunächst als akzeptiert. Sie fliegt nur raus, wenn die ÜBERZAHL
// der Spieler sie per 👎 anzweifelt. Der Autor zählt automatisch als „behalten" und
// kann die eigene Antwort nicht bewerten. Bei Gleichstand (50/50) entscheidet die
// Stimme des Spieladmins/Gastgebers (id 0).
function downVoters(s, cat, id) {
const v = (s.votes || {})[cat + '|' + id] || {};
return Object.keys(v).filter((k) => v[k] && Number(k) !== id).map(Number);
}
function rejected(s, cat, id) {
const down = downVoters(s, cat, id);
const remove = down.length;
if (remove === 0) return false;
const keep = R.players.length - remove; // Autor + alle, die nicht angezweifelt haben
if (remove > keep) return true; // Überzahl dagegen
if (remove === keep) return down.includes(0); // 50/50 → Stimme des Gastgebers (id 0) entscheidet
return false;
}
// Detaillierte Rundenpunkte je Kategorie (berücksichtigt Anzweiflungen).
function computeDetail(s) {
const ids = R.players.map((p) => p.id);
const letter = (s.letter || '').toUpperCase();
const cats = s.cats && s.cats.length ? s.cats : DEFAULT_CATS;
const totals = {}; const perCat = {};
ids.forEach((id) => { totals[id] = 0; perCat[id] = {}; });
const valid = (id, cat) => {
const a = (((s.answers[id] || {})[cat]) || '').trim();
if (!a || a[0].toUpperCase() !== letter) return null; // leer / falscher Buchstabe
// Jede Antwort gilt – außer die Überzahl hat sie angezweifelt.
return rejected(s, cat, id) ? null : a;
};
cats.forEach((cat) => {
const counts = {}; const ok = [];
ids.forEach((id) => { const a = valid(id, cat); if (a) { ok.push(id); const k = a.toLowerCase(); counts[k] = (counts[k] || 0) + 1; } });
ids.forEach((id) => {
const a = valid(id, cat); let pts = 0;
if (a) { const k = a.toLowerCase(); pts = ok.length === 1 ? 20 : (counts[k] > 1 ? 5 : 10); }
perCat[id][cat] = pts; totals[id] += pts;
});
});
return { totals, perCat };
}
function computeRound(s) { return computeDetail(s).totals; }
function pushHistory(s) {
const d = computeDetail(s);
const entry = { round: s.round, letter: s.letter, cats: (s.cats && s.cats.length ? s.cats : DEFAULT_CATS).slice(), answers: JSON.parse(JSON.stringify(s.answers || {})), perCat: d.perCat, totals: d.totals, names: R.players.map((p) => ({ id: p.id, name: p.name })) };
return [...(s.history || []), entry];
}
async function vote(cat, id) {
const key = cat + '|' + id;
const cur = ((R.state.votes || {})[key] || {})[R.myId];
await api({ action: 'merge', code: R.code, token: R.token, patch: { votes: { [key]: { [R.myId]: cur ? 0 : 1 } } } });
const d = await api({ action: 'state', code: R.code, token: R.token });
if (applyServer(d)) render();
}
function commitScores() {
const round = computeRound(R.state);
const scores = Object.assign({}, R.state.scores || {});
R.players.forEach((p) => (scores[p.id] = (scores[p.id] || 0) + (round[p.id] || 0)));
return scores;
}
// ---------- Rendering ----------
function nameOf(id) { const p = R.players.find((x) => x.id === id); return p ? p.name : ('Spieler ' + (id + 1)); }
function render() {
const s = R.state;
if (!s) { app().innerHTML = '<p style="text-align:center">Lade …</p>'; return; }
// Beim Tippen/Editieren nicht neu aufbauen, nur dynamische Teile aktualisieren
if (s.phase === 'lobby' && R.renderedPhase === 'lobby') { updateLobbyDynamic(); return; }
if (s.phase === 'writing' && R.renderedPhase === 'writing') { updateWritingStatus(); return; }
R.renderedPhase = s.phase;
if (s.phase === 'lobby') return renderLobby();
if (s.phase === 'ready') return renderReady();
if (s.phase === 'writing') return renderWriting();
if (s.phase === 'reveal') return renderReveal();
if (s.phase === 'done') return renderDone();
}
function renderReady() {
const s = R.state; const rr = s.readyRound || {}; const iReady = !!rr[R.myId];
const cnt = R.players.filter((p) => rr[p.id]).length;
app().innerHTML = `
<div class="rr-room-card">
<h2 class="rr-room-title">Runde ${s.round}</h2>
<p class="rr-room-sub">Bereitmachen – es startet, sobald alle bereit sind (${cnt}/${R.players.length})</p>
<div class="rr-room-players">${R.players.map((p) => `<span class="rr-pill${p.id === R.myId ? ' me' : ''}">${esc(p.name)}${p.id === 0 ? ' 👑' : ''}${p.id === R.myId ? ' (du)' : ''} ${rr[p.id] ? '✅' : '⏳'}</span>`).join('')}</div>
${iReady ? '<div class="rr-room-hint">✅ Du bist bereit – warte auf die anderen …</div>' : '<button class="rr-btn rr-btn-gold rr-btn-block" id="readyBtn">Ich bin bereit</button>'}
${histBtnHTML()}
<a class="rr-btn rr-btn-ghost rr-btn-block" href="index.php">‹ Lobby</a>
</div>`;
if (!iReady) $('readyBtn').onclick = confirmReady;
bindHistBtn();
}
// ---------- Historie ----------
function histBtnHTML() { return (R.state.history && R.state.history.length) ? '<button class="rr-btn rr-btn-ghost rr-btn-block" id="histBtn">📜 Historie</button>' : ''; }
function bindHistBtn() { const b = $('histBtn'); if (b) b.onclick = showHistory; }
function showHistory() {
const h = R.state.history || [];
const body = h.length ? h.slice().reverse().map((e) => {
const names = e.names || R.players.map((p) => ({ id: p.id, name: p.name }));
const ids = names.map((n) => n.id);
const nm = (id) => { const n = names.find((x) => x.id === id); return n ? n.name : ('#' + id); };
return `<div class="rr-hist-round"><h4>Runde ${e.round} · Buchstabe ${esc(e.letter)}</h4>
<div class="rr-slf-table-wrap"><table class="rr-slf-table">
<thead><tr><th>Kategorie</th>${ids.map((id) => `<th>${esc(nm(id))}</th>`).join('')}</tr></thead><tbody>
${e.cats.map((c) => `<tr><td class="rr-slf-cat">${esc(c)}</td>${ids.map((id) => { const a = ((e.answers[id] || {})[c] || '').trim(); const p = (e.perCat[id] || {})[c] || 0; return `<td>${a ? esc(a) : '–'} <b class="hist-pts">${p}</b></td>`; }).join('')}</tr>`).join('')}
<tr class="rr-slf-sum"><td>Runde gesamt</td>${ids.map((id) => `<td>+${e.totals[id] || 0}</td>`).join('')}</tr>
</tbody></table></div></div>`;
}).join('') : '<p class="rr-room-hint">Noch keine Runden gespielt.</p>';
const ov = document.createElement('div'); ov.className = 'rr-modal';
ov.innerHTML = `<div class="rr-modal-card"><div class="rr-modal-head"><h3>📜 Historie</h3><button class="rr-icon-btn" id="histClose">✕</button></div><div class="rr-hist-body">${body}</div></div>`;
document.body.appendChild(ov);
ov.addEventListener('click', (ev) => { if (ev.target === ov) ov.remove(); });
ov.querySelector('#histClose').onclick = () => ov.remove();
}
function lobbyPills() {
return R.players.map((p) => `<span class="rr-pill${p.id === R.myId ? ' me' : ''}">${esc(p.name)}${p.id === 0 ? ' 👑' : ''}${p.id === R.myId ? ' (du)' : ''}</span>`).join('');
}
function updateLobbyDynamic() {
const pc = document.getElementById('lobbyPlayers');
if (pc) pc.innerHTML = lobbyPills();
const cc = document.getElementById('lobbyCount');
if (cc) cc.textContent = `(${R.players.length})`;
const sb = document.getElementById('startBtn');
if (sb) sb.disabled = R.players.length < 2;
const sub = document.getElementById('lobbySub');
if (sub) sub.textContent = `Lade Freunde per Link ein – ${R.players.length} dabei`;
}
function renderLobby() {
app().innerHTML = `
<div class="rr-room-card">
<h1 class="rr-room-title">Stadt Land Fluss</h1>
<p class="rr-room-sub" id="lobbySub">Lade Freunde per Link ein – ${R.players.length} dabei</p>
<div class="rr-roster-head">👥 In dieser Runde <span id="lobbyCount">(${R.players.length})</span></div>
<div class="rr-room-players" id="lobbyPlayers">${lobbyPills()}</div>
<div class="rr-share"><input id="shareLink" readonly value="${shareLink()}"><button class="rr-btn rr-btn-gold" id="copyLink">Kopieren</button></div>
${isHost()
? `<div class="rr-slf-cats"><label>Kategorien (frei wählbar – eine pro Zeile, z. B. Automarke):</label><textarea id="catBox" rows="6">${esc(getCats().join('\n'))}</textarea></div>
<button class="rr-btn rr-btn-gold rr-btn-block" id="startBtn" ${R.players.length < 2 ? 'disabled' : ''}>Spiel starten</button>${R.players.length < 2 ? '<p class="rr-room-hint">Mindestens 2 Spieler nötig.</p>' : ''}`
: '<p class="rr-room-hint">Warte, bis der Gastgeber startet …</p>'}
<a class="rr-btn rr-btn-ghost rr-btn-block" href="index.php">‹ Lobby</a>
</div>`;
bindShare();
if (isHost()) $('startBtn') && ($('startBtn').onclick = startGame);
}
function renderWriting() {
const s = R.state; const mine = s.answers[R.myId] || {};
const iDone = !!s.done[R.myId];
app().innerHTML = `
<div class="rr-room-card">
<div class="rr-slf-letterbar"><span>Runde ${s.round}</span><div class="rr-slf-letter">${esc(s.letter)}</div><span id="doneCount"></span></div>
<div class="rr-slf-form">
${getCats().map((c, i) => `<label class="rr-slf-field"><span>${esc(c)}</span><input id="slf_in_${i}" value="${esc(mine[c] || '')}" autocomplete="off" ${iDone ? 'disabled' : ''}></label>`).join('')}
</div>
${iDone
? '<div class="rr-room-hint">✅ Abgegeben – warte auf die anderen …</div>'
: '<button class="rr-btn rr-btn-gold rr-btn-block" id="stopBtn">STOPP – Runde beenden</button>'}
</div>`;
if (!iDone) {
$('stopBtn').onclick = stopRound;
getCats().forEach((c, i) => { const el = $('slf_in_' + i); el.addEventListener('input', () => { clearTimeout(R.saveT); R.saveT = setTimeout(saveMy, 700); }); });
}
updateWritingStatus();
}
function updateWritingStatus() {
const s = R.state; if (!s || s.phase !== 'writing') return;
const done = R.players.filter((p) => (s.done || {})[p.id]).length;
const dc = $('doneCount'); if (dc) dc.textContent = `${done}/${R.players.length} fertig`;
}
function renderReveal() {
const s = R.state; const ids = R.players.map((p) => p.id);
// Eigene Spalte wird in der Bewertungsphase ganz ausgeblendet (Platzgründe) – nur die anderen werden bewertet.
const otherIds = ids.filter((id) => id !== R.myId);
const others = R.players.filter((p) => p.id !== R.myId);
const cats = s.cats && s.cats.length ? s.cats : DEFAULT_CATS;
const live = computeRound(s);
const proj = (id) => (s.scores[id] || 0) + (live[id] || 0);
const standings = [...ids].sort((a, b) => proj(b) - proj(a));
const cell = (id, c, ci) => {
const a = ((s.answers[id] || {})[c] || '').trim();
if (!a) return '<td>–</td>';
const isRej = rejected(s, c, id);
const n = downVoters(s, c, id).length;
const v = (s.votes || {})[c + '|' + id] || {};
const mine = !!v[R.myId];
return `<td class="${isRej ? 'slf-rej' : ''}"><div class="slf-cell"><span>${esc(a)}</span>
<button class="slf-vote${mine ? ' on' : ''}" data-ci="${ci}" data-id="${id}" title="Antwort anzweifeln (Überzahl → 0 Punkte)">👎${n || ''}</button></div></td>`;
};
app().innerHTML = `
<div class="rr-room-card">
<h2 class="rr-room-title">Runde ${s.round} · Buchstabe ${esc(s.letter)}</h2>
<p class="rr-room-hint">Jede Antwort zählt zunächst. 👎 anzweifeln – nur wenn die <b>Überzahl</b> dagegen ist, fliegt sie raus (0 Punkte). Bei Gleichstand entscheidet der Gastgeber 👑. Deine eigenen Antworten werden hier nicht angezeigt – sie erscheinen in der 📜 Historie.</p>
<div class="rr-slf-table-wrap"><table class="rr-slf-table">
<thead><tr><th>Kategorie</th>${others.map((p) => `<th>${esc(p.name)}</th>`).join('')}</tr></thead>
<tbody>
${cats.map((c, ci) => `<tr><td class="rr-slf-cat">${esc(c)}</td>${otherIds.map((id) => cell(id, c, ci)).join('')}</tr>`).join('')}
<tr class="rr-slf-sum"><td>Punkte</td>${otherIds.map((id) => `<td>+${live[id] || 0}</td>`).join('')}</tr>
</tbody>
</table></div>
<div class="rr-slf-scores">${standings.map((id, i) => `<div class="rr-score-row${i === 0 ? ' lead' : ''}"><span>${i + 1}. ${esc(nameOf(id))}</span><b>${proj(id)}</b></div>`).join('')}</div>
${isHost() ? '<div class="rr-room-actions"><button class="rr-btn rr-btn-gold" id="nextBtn">Nächste Runde</button><button class="rr-btn rr-btn-ghost" id="endBtn">Spiel beenden</button></div>' : '<p class="rr-room-hint">Warte auf die nächste Runde …</p>'}
${histBtnHTML()}
</div>`;
document.querySelectorAll('.slf-vote').forEach((b) => {
const cat = cats[+b.dataset.ci], id = +b.dataset.id;
b.onclick = () => vote(cat, id);
});
bindHistBtn();
if (isHost()) { $('nextBtn').onclick = nextRound; $('endBtn').onclick = endGame; }
}
function renderDone() {
const s = R.state; const ids = R.players.map((p) => p.id).sort((a, b) => (s.scores[b] || 0) - (s.scores[a] || 0));
app().innerHTML = `
<div class="rr-room-card">
<h1 class="rr-room-title">🏆 Endstand</h1>
<div class="rr-slf-scores">${ids.map((id, i) => `<div class="rr-score-row${i === 0 ? ' lead' : ''}"><span>${['🥇','🥈','🥉'][i] || (i + 1 + '.')} ${esc(nameOf(id))}</span><b>${s.scores[id] || 0}</b></div>`).join('')}</div>
${histBtnHTML()}
${isHost() ? '<button class="rr-btn rr-btn-gold rr-btn-block" id="againBtn">Neue Partie</button>' : ''}
<a class="rr-btn rr-btn-ghost rr-btn-block" href="index.php">‹ Lobby</a>
</div>`;
bindHistBtn();
if (isHost()) $('againBtn').onclick = () => replaceState({ phase: 'lobby', round: 0, scores: {}, used: [], answers: {}, done: {}, votes: {}, readyRound: {} }, 'lobby');
}
function bindShare() {
const c = $('copyLink'); if (!c) return;
c.onclick = () => { const i = $('shareLink'); i.select(); navigator.clipboard?.writeText(i.value).then(() => toast('SLF', 'Link kopiert!', 'win')).catch(() => {}); };
}
function stopMenuPoll() { if (R.menuPoll) { clearInterval(R.menuPoll); R.menuPoll = null; } }
function showMenu() {
stopPoll(); R.renderedPhase = null;
const last = lastRoom();
app().innerHTML = `
<div class="rr-room-card">
<h1 class="rr-room-title">Stadt Land Fluss</h1>
<p class="rr-room-sub">Eigene Runde erstellen oder einem offenen Raum beitreten</p>
${last ? '<button class="rr-btn rr-btn-gold rr-btn-block" id="resumeBtn">▶ Zurück in deine letzte Runde</button>' : ''}
<button class="rr-btn ${last ? 'rr-btn-ghost' : 'rr-btn-gold'} rr-btn-block" id="newBtn">➕ Neue Runde erstellen</button>
<div class="rr-roomlist-head"><span>Offene Räume</span><button class="rr-mini" id="refreshRooms" title="Aktualisieren">↻</button></div>
<div id="roomList" class="rr-roomlist"><p class="rr-room-hint">Lade …</p></div>
<a class="rr-btn rr-btn-ghost rr-btn-block" href="index.php">‹ Zurück zur Lobby</a>
</div>`;
if (last) { const rb = $('resumeBtn'); if (rb) rb.onclick = () => { stopMenuPoll(); joinRoom(last); }; }
$('newBtn').onclick = () => { stopMenuPoll(); createRoom(); };
$('refreshRooms').onclick = refreshMenuRooms;
refreshMenuRooms();
stopMenuPoll(); R.menuPoll = setInterval(refreshMenuRooms, 3000);
}
async function refreshMenuRooms() {
let rooms = [];
try { const d = await api({ action: 'list', game: 'slf' }); if (d.ok) rooms = d.rooms; } catch {}
const el = $('roomList'); if (!el) return;
if (!rooms.length) { el.innerHTML = '<p class="rr-room-hint">Keine offenen Räume – erstelle einen neuen.</p>'; return; }
el.innerHTML = '';
rooms.forEach((r) => {
const row = document.createElement('div'); row.className = 'rr-roomrow';
row.innerHTML = `<div class="rr-roomrow-info"><b>${esc(r.host)}s Runde</b><span>${r.count}/${r.maxp} Spieler</span></div>`;
const b = document.createElement('button'); b.className = 'rr-btn rr-btn-gold'; b.textContent = 'Beitreten';
b.onclick = () => { stopMenuPoll(); joinRoom(r.code); };
row.appendChild(b); el.appendChild(row);
});
}
if (boot.codeParam) joinRoom(boot.codeParam);
else if (lastRoom()) joinRoom(lastRoom()); // Reload/Wiederkehr → automatisch zurück in die Runde (Fallback: Menü bei !ok)
else showMenu();