Spaces:
Running
Running
Download kenwren/core.js from CyberMax-tools/cybermax: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/kenwren/core.js
- Command line
-
hf download hf://spaces/CyberMax-tools/cybermax/kenwren/core.js
-
curl -L -o core.js https://huggingface.co/spaces/CyberMax-tools/cybermax/resolve/main/kenwren/core.js
10.1 kB
| // Kenwren core: the question bank and the rules (no DOM). The browser uses it to build and play quizzes; the CyberMax | |
| // toys Worker uses a copy (products/web-toys/worker/src/kenwren-core.js, made by scripts/games/sync_kit.py) to check | |
| // and score every play on the server, so the creator's answers never reach a friend's browser before they finish. | |
| // (c) 2026 CyberMax. All rights reserved. | |
| export const QUIZ_LEN = 10; | |
| export const NAME_MAX = 20; | |
| /** Question bank. `{n}` is replaced with the creator's name when friends play. Packs other than "classic" need the Fun Pass. */ | |
| export const QUESTIONS = [ | |
| // classic (free) | |
| { id: 'c01', pack: 'classic', me: 'My perfect weekend is…', you: '{n}\'s perfect weekend is…', opts: ['Out with friends', 'Sofa and a series', 'A trip somewhere new', 'Getting things done'] }, | |
| { id: 'c02', pack: 'classic', me: 'Coffee or tea?', you: 'Coffee or tea for {n}?', opts: ['Coffee, always', 'Tea, obviously', 'Both', 'Neither'] }, | |
| { id: 'c03', pack: 'classic', me: 'My go-to snack is…', you: '{n}\'s go-to snack is…', opts: ['Chips', 'Chocolate', 'Fruit', 'Cheese'] }, | |
| { id: 'c04', pack: 'classic', me: 'Pick my superpower:', you: 'Which superpower would {n} pick?', opts: ['Flying', 'Invisibility', 'Teleporting', 'Reading minds'] }, | |
| { id: 'c05', pack: 'classic', me: 'I am a…', you: '{n} is a…', opts: ['Morning person', 'Night owl', 'Afternoon napper', 'Always tired'] }, | |
| { id: 'c06', pack: 'classic', me: 'My dream holiday is…', you: '{n}\'s dream holiday is…', opts: ['Beach', 'Mountains', 'Big city', 'Road trip'] }, | |
| { id: 'c07', pack: 'classic', me: 'Cats or dogs?', you: 'Cats or dogs for {n}?', opts: ['Cats', 'Dogs', 'Both', 'Neither, fish'] }, | |
| { id: 'c08', pack: 'classic', me: 'On a night out I am…', you: 'On a night out {n} is…', opts: ['First on the dance floor', 'Deep in conversation', 'Ordering more food', 'Home by 11'] }, | |
| { id: 'c09', pack: 'classic', me: 'My favourite season is…', you: '{n}\'s favourite season is…', opts: ['Spring', 'Summer', 'Autumn', 'Winter'] }, | |
| { id: 'c10', pack: 'classic', me: 'Pizza topping I would defend:', you: 'Which pizza topping would {n} defend?', opts: ['Pineapple', 'Pepperoni', 'Mushrooms', 'Just cheese'] }, | |
| { id: 'c11', pack: 'classic', me: 'My phone battery is usually…', you: '{n}\'s phone battery is usually…', opts: ['Above 80%', 'Around half', 'In the red', 'Dead'] }, | |
| { id: 'c12', pack: 'classic', me: 'I would rather…', you: 'Would {n} rather…', opts: ['Sing in public', 'Dance in public', 'Give a speech', 'Stay home'] }, | |
| { id: 'c13', pack: 'classic', me: 'In a group project I am…', you: 'In a group project {n} is…', opts: ['The leader', 'The ideas person', 'The one who does it all', 'Missing'] }, | |
| { id: 'c14', pack: 'classic', me: 'My comfort movie genre:', you: '{n}\'s comfort movie genre:', opts: ['Comedy', 'Horror', 'Romance', 'Action'] }, | |
| { id: 'c15', pack: 'classic', me: 'Texting style:', you: '{n}\'s texting style:', opts: ['Instant reply', 'Replies in 3 days', 'Voice notes only', 'Just emojis'] }, | |
| { id: 'c16', pack: 'classic', me: 'Breakfast for me is…', you: 'Breakfast for {n} is…', opts: ['Big and hot', 'Cereal', 'Just coffee', 'What breakfast?'] }, | |
| { id: 'c17', pack: 'classic', me: 'I would win a game show about…', you: '{n} would win a game show about…', opts: ['Movies and TV', 'Sport', 'Music', 'Random facts'] }, | |
| { id: 'c18', pack: 'classic', me: 'My room is…', you: '{n}\'s room is…', opts: ['Spotless', 'Organised chaos', 'Just chaos', 'Depends on the day'] }, | |
| { id: 'c19', pack: 'classic', me: 'Biggest fear:', you: '{n}\'s biggest fear:', opts: ['Spiders', 'Heights', 'Public speaking', 'The dark'] }, | |
| { id: 'c20', pack: 'classic', me: 'If I won the lottery I would first…', you: 'If {n} won the lottery, first thing:', opts: ['Travel', 'Buy a house', 'Quit my job', 'Tell nobody'] }, | |
| // deep (Fun Pass) | |
| { id: 'd01', pack: 'deep', me: 'What matters most to me:', you: 'What matters most to {n}?', opts: ['Family', 'Freedom', 'Success', 'Fun'] }, | |
| { id: 'd02', pack: 'deep', me: 'When I am stressed I…', you: 'When {n} is stressed they…', opts: ['Talk it out', 'Go quiet', 'Exercise', 'Eat'] }, | |
| { id: 'd03', pack: 'deep', me: 'My love language is…', you: '{n}\'s love language is…', opts: ['Words', 'Time together', 'Gifts', 'Helping out'] }, | |
| { id: 'd04', pack: 'deep', me: 'I make decisions with my…', you: '{n} decides with their…', opts: ['Head', 'Heart', 'Gut', 'Group chat'] }, | |
| { id: 'd05', pack: 'deep', me: 'In ten years I want to be…', you: 'In ten years {n} wants to be…', opts: ['Rich', 'Happy and settled', 'Travelling', 'Famous'] }, | |
| { id: 'd06', pack: 'deep', me: 'I recharge by…', you: '{n} recharges by…', opts: ['Being alone', 'Being with people', 'Being outdoors', 'Sleeping'] }, | |
| { id: 'd07', pack: 'deep', me: 'I would most like to master…', you: '{n} would most like to master…', opts: ['An instrument', 'A language', 'Cooking', 'A sport'] }, | |
| { id: 'd08', pack: 'deep', me: 'My worst habit is…', you: '{n}\'s worst habit is…', opts: ['Being late', 'Overthinking', 'Procrastinating', 'Doom-scrolling'] }, | |
| { id: 'd09', pack: 'deep', me: 'A perfect gift for me is…', you: 'A perfect gift for {n} is…', opts: ['Something handmade', 'An experience', 'Something useful', 'Food'] }, | |
| { id: 'd10', pack: 'deep', me: 'I am happiest when…', you: '{n} is happiest when…', opts: ['Learning something', 'Laughing with friends', 'Achieving a goal', 'Doing nothing'] }, | |
| // spicy-but-clean (Fun Pass) | |
| { id: 's01', pack: 'spicy', me: 'Most likely to go viral for…', you: '{n} is most likely to go viral for…', opts: ['A dance', 'A rant', 'A fail', 'A cooking video'] }, | |
| { id: 's02', pack: 'spicy', me: 'My secret talent is…', you: '{n}\'s secret talent is…', opts: ['Singing', 'Gossip', 'Napping anywhere', 'Winning arguments'] }, | |
| { id: 's03', pack: 'spicy', me: 'I have stalked an ex\'s profile…', you: 'Has {n} stalked an ex\'s profile?', opts: ['Never', 'Once', 'Regularly', 'Right now'] }, | |
| { id: 's04', pack: 'spicy', me: 'Most embarrassing place I fell asleep:', you: 'Where did {n} embarrassingly fall asleep?', opts: ['In class or a meeting', 'On public transport', 'At a party', 'Mid-conversation'] }, | |
| { id: 's05', pack: 'spicy', me: 'I would survive a zombie outbreak for…', you: '{n} would survive a zombie outbreak for…', opts: ['5 minutes', 'A day', 'A month', 'Forever, I am ready'] }, | |
| { id: 's06', pack: 'spicy', me: 'My most used emoji:', you: '{n}\'s most used emoji:', opts: ['😂', '❤️', '💀', '🙄'] }, | |
| { id: 's07', pack: 'spicy', me: 'Who pays on a first date?', you: 'Who pays on a first date, says {n}?', opts: ['I do', 'They do', 'We split', 'Whoever asked'] }, | |
| { id: 's08', pack: 'spicy', me: 'I lie most about…', you: '{n} lies most about…', opts: ['Being on my way', 'Having read the book', 'Being fine', 'Liking the gift'] }, | |
| { id: 's09', pack: 'spicy', me: 'My villain origin story would be…', you: '{n}\'s villain origin story would be…', opts: ['Someone ate my food', 'Slow walkers', 'Spoilers', 'Reply-all emails'] }, | |
| { id: 's10', pack: 'spicy', me: 'Guilty pleasure:', you: '{n}\'s guilty pleasure:', opts: ['Reality TV', 'Fast food at 2 AM', 'Cheesy pop', 'Online shopping'] }, | |
| ]; | |
| export const BY_ID = Object.fromEntries(QUESTIONS.map((q) => [q.id, q])); | |
| export const PACKS = { classic: { name: 'Classic', free: true }, deep: { name: 'Deep', free: false }, spicy: { name: 'Spicy (clean)', free: false } }; | |
| /** Clean a display name: letters, digits, spaces and a few marks, trimmed, 1..NAME_MAX chars; '' if empty. */ | |
| export function cleanName(s) { | |
| return String(s || '').normalize('NFKC').replace(/[^\p{L}\p{N} '._-]/gu, '').replace(/\s+/g, ' ').trim().slice(0, NAME_MAX); | |
| } | |
| /** Validate a new quiz: { name, items: [{ q, a }] } with QUIZ_LEN distinct known questions and a valid answer each. */ | |
| export function checkQuiz(body, pro = false) { | |
| const name = cleanName(body?.name); | |
| if (!name) return { error: 'Add your name first.' }; | |
| const items = Array.isArray(body?.items) ? body.items : []; | |
| if (items.length !== QUIZ_LEN) return { error: `A quiz has ${QUIZ_LEN} questions.` }; | |
| const seen = new Set(); | |
| for (const it of items) { | |
| const q = BY_ID[it?.q]; | |
| if (!q || seen.has(q.id)) return { error: 'Unknown or repeated question.' }; | |
| if (!pro && !PACKS[q.pack].free) return { error: 'That question pack needs the Fun Pass.' }; | |
| if (!Number.isInteger(it.a) || it.a < 0 || it.a >= q.opts.length) return { error: 'Pick an answer for every question.' }; | |
| seen.add(q.id); | |
| } | |
| return { name, items: items.map((it) => ({ q: it.q, a: it.a })) }; | |
| } | |
| /** Score a friend's picks against the creator's answers. */ | |
| export function score(items, picks) { | |
| if (!Array.isArray(picks) || picks.length !== items.length) return null; | |
| let s = 0; | |
| for (let i = 0; i < items.length; i++) { | |
| if (!Number.isInteger(picks[i]) || picks[i] < 0 || picks[i] >= BY_ID[items[i].q].opts.length) return null; | |
| if (picks[i] === items[i].a) s++; | |
| } | |
| return s; | |
| } | |
| /** A fresh suggested quiz: QUIZ_LEN questions from the allowed packs, seeded so "shuffle" is reproducible in tests. */ | |
| export function suggest(seed, packs = ['classic']) { | |
| let a = 2166136261; | |
| for (const ch of String(seed)) { a ^= ch.charCodeAt(0); a = Math.imul(a, 16777619); } | |
| const rnd = () => { a = (a + 0x6d2b79f5) >>> 0; let t = a; t = Math.imul(t ^ (t >>> 15), t | 1); t ^= t + Math.imul(t ^ (t >>> 7), t | 61); return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; | |
| const pool = QUESTIONS.filter((q) => packs.includes(q.pack)).map((q) => q.id); | |
| for (let i = pool.length - 1; i > 0; i--) { const j = Math.floor(rnd() * (i + 1)); [pool[i], pool[j]] = [pool[j], pool[i]]; } | |
| return pool.slice(0, QUIZ_LEN); | |
| } | |
| export function verdict(s) { | |
| return s >= 10 ? 'Soulmate level. You know them better than they do.' : s >= 8 ? 'Best friend energy.' : s >= 6 ? 'Good friend. Room to grow.' : s >= 4 ? 'Acquaintance vibes.' : 'Have you two actually met?'; | |
| } | |