const root = document.querySelector(".play-layout");
const sessionId = root.dataset.session;
const token = root.dataset.token;
const isHost = root.dataset.host === "1";
const playerId = Number(root.dataset.player);
let roundSeen = 0;
const playersEl = document.getElementById("players");
const stageEl = document.getElementById("stage");
const tipEl = document.getElementById("tip");
const inviteEl = document.getElementById("invite");
const hostForm = document.getElementById("host-form");
const startBtn = document.getElementById("start-btn");
const nextBtn = document.getElementById("next-btn");
inviteEl.textContent = "Invite: " + location.origin + "/join/" + sessionId;
let lastChoice = null;
let submitted = false;
let proto = location.protocol === "https:" ? "wss" : "ws";
const ws = new WebSocket(`${proto}://${location.host}/ws/${sessionId}?token=${token}`);
ws.onmessage = (ev) => {
const data = JSON.parse(ev.data);
if (data.type === "error") {
tipEl.textContent = data.message;
return;
}
if (data.type === "state") render(data);
};
function send(payload) {
ws.send(JSON.stringify(payload));
}
if (startBtn) startBtn.onclick = () => send({ action: "start" });
if (nextBtn) nextBtn.onclick = () => send({ action: "next" });
function render(data) {
const s = data.session;
playersEl.innerHTML = data.players
.map(
(p) =>
`
${escapeHtml(p.name)} — ${p.score} pts`
)
.join("");
if (hostForm) hostForm.hidden = s.status !== "lobby";
if (startBtn) startBtn.hidden = s.status !== "lobby";
if (nextBtn) nextBtn.hidden = !(s.status === "playing" && data.round && data.round.status === "revealed");
if (s.status === "lobby") {
submitted = false;
lastChoice = null;
tipEl.textContent = "Lobby time! Host tweaks the rules, then hits START. Share the invite link.";
stageEl.innerHTML = `Waiting for players (${data.players.length}/${s.max_players}). Rounds: ${s.num_rounds}.
`;
return;
}
if (s.status === "finished") {
tipEl.textContent = `Match over! ${data.winner} takes the crown. High score wins the RAL throne.`;
stageEl.innerHTML = `FINAL BOARD
` + playersEl.outerHTML;
return;
}
const r = data.round;
if (!r) return;
if (r.number !== roundSeen) {
roundSeen = r.number;
lastChoice = null;
submitted = false;
}
if ((r.answered_ids || []).includes(playerId)) submitted = true;
if (r.status === "open") {
if (r.bonus) {
tipEl.textContent =
"BONUS ROUND! Harder posting. 150 for the right band, 300 if you nail the exact RAL, -50 if the exact guess is wrong. Also pick hybrid / remote / in-place (+50).";
} else {
tipEl.textContent =
"Read the offer. Pick the closest RAL band. 100 points if you hit it, 0 if not. Optional exact number: +200 or -50.";
}
} else {
tipEl.textContent = "Everyone locked in. Here's the reveal — host can move to the next round.";
}
const job = r.job;
const meta = [];
if (r.bonus) meta.push(`BONUS ROUND`);
meta.push(`ROUND ${r.number}/${s.num_rounds}`);
if (job.company) meta.push(`${escapeHtml(job.company)}`);
if (job.subject) meta.push(`${escapeHtml(job.subject)}`);
if (job.location) meta.push(`${escapeHtml(job.location)}`);
if (job.extra) meta.push(`${escapeHtml(job.extra)}`);
const locked = r.status !== "open" || submitted;
const options = r.options
.map((o) => {
let cls = "opt";
if (lastChoice === o.key) cls += " picked";
if (r.status === "revealed") {
if (o.key === r.correct) cls += " good";
else if (lastChoice === o.key) cls += " bad";
}
return ``;
})
.join("");
let extra = "";
if (job.has_exact) {
extra += ``;
}
if (r.bonus) {
extra += ``;
}
let results = "";
if (r.status === "revealed") {
const ral =
r.ral_exact != null
? `Exact RAL: ${r.ral_exact}`
: `RAL range: ${r.ral_min} - ${r.ral_max}`;
results = `
${ral} · Company: ${escapeHtml(r.company)} · Work: ${escapeHtml(r.work_type)}
${r.answers
.map(
(a) =>
`- ${escapeHtml(a.name)}: ${a.correct ? "HIT" : "MISS"} ${a.precise != null ? "· exact " + a.precise : ""} → ${a.points} pts
`
)
.join("")}
`;
} else {
results = `Locked in: ${r.answered.join(", ") || "nobody yet"}. Waiting: ${r.waiting.join(", ")}
`;
}
stageEl.innerHTML = `
${escapeHtml(job.title)}
${meta.join("")}
${escapeHtml(job.description)}
${options}
${extra}
${r.status === "open" ? `` : ""}
${results}
`;
stageEl.querySelectorAll(".opt").forEach((btn) => {
btn.onclick = () => {
if (locked) return;
lastChoice = btn.dataset.key;
stageEl.querySelectorAll(".opt").forEach((b) => b.classList.remove("picked"));
btn.classList.add("picked");
};
});
const lockBtn = document.getElementById("lock-btn");
if (lockBtn) {
lockBtn.onclick = () => {
if (!lastChoice) {
tipEl.textContent = "Pick a RAL band first, then lock it in.";
return;
}
const preciseEl = document.getElementById("precise");
const workEl = document.getElementById("work-type");
submitted = true;
send({
action: "answer",
choice: lastChoice,
precise: preciseEl && preciseEl.value ? Number(preciseEl.value) : null,
work_type: workEl ? workEl.value : "",
});
};
}
}
function escapeHtml(str) {
return String(str || "")
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}