// Kenwren: make a "how well do you know me" quiz, share it, friends play, private scoreboard. (c) 2026 CyberMax. import { QUESTIONS, BY_ID, PACKS, QUIZ_LEN, cleanName, suggest, verdict } from './core.js'; import { API, SITE, count, store, toast, share, intents, download, canvasBlob, pwa, rr, themeToggle } from './cmx.js'; import * as pass from './funpass.js'; import { initSolo, route as soloRoute, renderHome } from './solo.js'; const GAME = 'kenwren'; const PATH = '/games/kenwren/'; // canonical path on the CyberMax site (share links) const HERE = location.pathname; // in-page links work on the site and on HF copies const $ = (id) => document.getElementById(id); const SKINS = { dusk: { name: 'Dusk', a: '#2A1B3D', b: '#FFD166', free: true }, sunrise: { name: 'Sunrise', a: '#3D1F1B', b: '#FFE08A' }, lagoon: { name: 'Lagoon', a: '#0B2E3A', b: '#B8FFF0' }, bubblegum: { name: 'Bubblegum', a: '#3A1235', b: '#FFF1A8' }, paper: { name: 'Paper', a: '#EFE7D6', b: '#C2412D' } }; let mode = 'make'; // make | play let name = '', packs = ['classic'], qs = [], answers = [], i = 0, quiz = null, swaps = 0; pass.init({ game: GAME, name: 'Kenwren', perks: ['Solo levels 21-40: Memory Lane and Mind Reader', 'Deep and Spicy (clean) question packs', 'Quizzes last a year instead of 30 days', 'Sunrise, Lagoon, Bubblegum and Paper themes'] }); const install = pwa(GAME, $('install')); themeToggle($('theme')); function show(id) { document.querySelectorAll('.screen').forEach((s) => { s.hidden = s.id !== id; }); $('err').hidden = true; $('stage').scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } function err(msg) { const e = $('err'); e.textContent = msg; e.hidden = false; } const quizUrl = (id) => `${SITE}${PATH}?q=${id}`; async function api(path, init) { const r = await fetch(`${API}${path}`, init ? { ...init, headers: { 'content-type': 'application/json' } } : undefined); const j = await r.json().catch(() => ({})); if (!r.ok) throw new Error(j.message || 'Something went wrong. Try again in a moment.'); return j; } const esc = (s) => String(s).replace(/[&<>"]/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"' }[c])); const qText = (q) => (mode === 'make' ? q.me : q.you.replaceAll('{n}', quiz.name)); // ------------------------------------------------------------------ making a quiz function renderPacks() { const el = $('packs'); el.innerHTML = ''; for (const [id, p] of Object.entries(PACKS)) { const b = document.createElement('button'); const lock = !p.free && !pass.isPro(); b.type = 'button'; b.className = 'chip'; b.setAttribute('aria-pressed', String(packs.includes(id))); b.innerHTML = `${p.name}${lock ? ' ๐' : ''}`; b.onclick = () => { if (lock) { pass.open(`pack-${id}`); return; } packs = packs.includes(id) ? packs.filter((x) => x !== id) : [...packs, id]; if (!packs.length) packs = ['classic']; renderPacks(); }; el.appendChild(b); } el.hidden = mode !== 'make'; } $('start').onclick = () => { mode = 'make'; $('name-q').textContent = "What's your name?"; $('name-note').textContent = 'Friends will see this name. No email, no account.'; renderPacks(); show('s-name'); $('name').focus(); }; $('name-form').onsubmit = (e) => { e.preventDefault(); name = cleanName($('name').value); if (!name) { err('Add a name (letters and numbers).'); return; } store('kenwren:name', name); if (mode === 'make') { qs = suggest(`${Date.now()}`, packs); answers = []; i = 0; swaps = 0; count(GAME, 'play', 'make'); } else { qs = quiz.questions; answers = []; i = 0; count(GAME, 'play', 'friend'); } ask(); }; function ask() { const q = BY_ID[qs[i]]; $('bar').style.width = `${(i / QUIZ_LEN) * 100}%`; $('q-count').textContent = `Question ${i + 1} of ${QUIZ_LEN}`; $('q-text').textContent = qText(q); const box = $('opts'); box.innerHTML = ''; q.opts.forEach((o, k) => { const b = document.createElement('button'); b.type = 'button'; b.className = 'opt'; b.textContent = o; b.setAttribute('aria-pressed', String(answers[i] === k)); b.onclick = () => pick(k); box.appendChild(b); }); $('swap').hidden = mode !== 'make'; $('back').disabled = i === 0; show('s-q'); } function pick(k) { answers[i] = k; if (navigator.vibrate) try { navigator.vibrate(8); } catch { /* */ } if (i < QUIZ_LEN - 1) { i++; setTimeout(ask, 120); } else setTimeout(finish, 120); } $('back').onclick = () => { if (i > 0) { i--; ask(); } }; $('swap').onclick = () => { const used = new Set(qs); const pool = QUESTIONS.filter((q) => packs.includes(q.pack) && !used.has(q.id)); if (!pool.length) { toast('No more questions in these packs.'); return; } qs[i] = pool[(swaps++ * 7 + i) % pool.length].id; answers[i] = undefined; ask(); }; document.addEventListener('keydown', (e) => { if (!$('s-q').hidden && /^[1-4]$/.test(e.key)) pick(Number(e.key) - 1); }); async function finish() { $('bar').style.width = '100%'; if (mode === 'make') { try { const st = store('cmx-pro:fun-pass'); const j = await api('/api/quiz', { method: 'POST', body: JSON.stringify({ name, items: qs.map((q, k) => ({ q, a: answers[k] })), pass: st?.code }) }); const mine = store('kenwren:mine') || []; mine.unshift({ id: j.id, token: j.token, name, at: new Date().toISOString().slice(0, 10), expires: j.expires }); store('kenwren:mine', mine.slice(0, 30)); count(GAME, 'win', 'made'); shareScreen({ id: j.id, token: j.token, name, expires: j.expires }); install.offer(); } catch (e) { err(e.message); } } else { try { const j = await api(`/api/quiz/${quiz.id}/play`, { method: 'POST', body: JSON.stringify({ nick: name, picks: answers }) }); count(GAME, 'win', 'friend'); resultScreen(j); install.offer(); } catch (e) { err(e.message); } } } function shareScreen(m) { $('share-name').textContent = m.name; $('share-link').value = quizUrl(m.id); $('days').textContent = m.expires ? `until ${m.expires}` : '30 days'; const text = `How well do you know me? Take my 10-question quiz and see where you rank ๐`; const path = `${PATH}?q=${m.id}`; const it = intents(GAME, text, path); $('sw').href = it.whatsapp; $('sx').href = it.x; $('sb').href = it.bsky; $('share').onclick = async () => share(GAME, text, path, await canvasBlob(card(`How well do you know ${m.name}?`, 'Take my quiz', '10 questions ยท see where you rank')), `kenwren-${m.id}.png`); $('card').onclick = async () => { download(await canvasBlob(card(`How well do you know ${m.name}?`, 'Take my quiz', '10 questions ยท see where you rank')), `kenwren-${m.id}.png`); count(GAME, 'share', 'png'); }; $('results-link').href = `${HERE}?q=${m.id}&t=${m.token}`; show('s-share'); } $('copy').onclick = async () => { try { await navigator.clipboard.writeText($('share-link').value); toast('Link copied. Send it to your friends!'); } catch { $('share-link').select(); } count(GAME, 'share', 'copy'); }; ['sw', 'sx', 'sb'].forEach((id) => $(id).addEventListener('click', () => count(GAME, 'share', { sw: 'whatsapp', sx: 'x', sb: 'bsky' }[id]))); // ------------------------------------------------------------------ playing a friend's quiz function resultScreen(j) { $('r-title').textContent = `How well do you know ${j.name}?`; $('r-score').textContent = `${j.score}/${j.of}`; $('r-verdict').textContent = `${verdict(j.score)} You're #${j.rank} of ${j.board.length >= 20 ? 'the top 20' : j.board.length} on ${j.name}'s board.`; $('r-recap').innerHTML = quiz.questions.map((qid, k) => { const q = BY_ID[qid]; const ok = answers[k] === j.answers[k]; return `