Spaces:
Running
Running
Download cmx.js from CyberMax-tools/pawfoil: direct link, hf CLI and curl.
- Browser
- Download file 7.6 kB
-
https://huggingface.co/spaces/CyberMax-tools/pawfoil/resolve/main/cmx.js
- Command line
-
hf download hf://spaces/CyberMax-tools/pawfoil/cmx.js
-
curl -L -o cmx.js https://huggingface.co/spaces/CyberMax-tools/pawfoil/resolve/main/cmx.js
7.6 kB
| // CyberMax Fun kit: shared browser helpers for the CyberMax games (copied into each game's web/ folder by | |
| // scripts/games/sync_kit.py, so every game stays self-contained and sellable on its own). | |
| // Counter beacon (game + event + channel only: no cookies, no IDs, nothing personal), daily seed, share, PWA install. | |
| // (c) 2026 CyberMax. All rights reserved. | |
| export const SITE = 'https://cybermaxtools.com'; | |
| export const API = 'https://toys.cybermaxtools.com'; | |
| export const EPOCH = Date.UTC(2026, 9, 2); // 2 Oct 2026 = puzzle #1 for every CyberMax daily game | |
| /** Today's date in the player's own time zone as YYYY-MM-DD (everyone gets a new puzzle at their local midnight). */ | |
| export function today(d = new Date()) { | |
| return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; | |
| } | |
| /** Puzzle number for a YYYY-MM-DD date (#1 = 2026-10-02). */ | |
| export function dayNumber(iso) { | |
| const [y, m, d] = iso.split('-').map(Number); | |
| return Math.round((Date.UTC(y, m - 1, d) - EPOCH) / 864e5) + 1; | |
| } | |
| export function dateOf(n) { | |
| const t = new Date(EPOCH + (n - 1) * 864e5); | |
| return t.toISOString().slice(0, 10); | |
| } | |
| /** 32-bit string hash (FNV-1a) and a small fast PRNG (mulberry32): the same seed gives the same game everywhere. */ | |
| export function hash(str) { | |
| let h = 0x811c9dc5; | |
| for (let i = 0; i < str.length; i++) { h ^= str.charCodeAt(i); h = Math.imul(h, 0x01000193); } | |
| return h >>> 0; | |
| } | |
| export function rng(seed) { | |
| let a = typeof seed === 'string' ? hash(seed) : seed >>> 0; | |
| return function () { | |
| 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; | |
| }; | |
| } | |
| /** Count one event (play, win, share, install, pro, cta). Never breaks the page; skipped on localhost and in tests. */ | |
| export function count(game, ev, ch = '') { | |
| try { | |
| if (typeof location === 'undefined' || /^(localhost|127\.0\.0\.1)$/.test(location.hostname) || navigator.webdriver) return; | |
| const u = `${API}/e?t=${encodeURIComponent(game)}&a=${encodeURIComponent(ev)}&c=${encodeURIComponent(String(ch).toLowerCase().replace(/[^a-z0-9-]/g, '').slice(0, 40))}`; | |
| if (navigator.sendBeacon && navigator.sendBeacon(u)) return; | |
| fetch(u, { method: 'POST', keepalive: true, mode: 'no-cors' }).catch(() => {}); | |
| } catch { /* never break the game */ } | |
| } | |
| export function store(key, val) { | |
| try { | |
| if (val === undefined) return JSON.parse(localStorage.getItem(key) || 'null'); | |
| localStorage.setItem(key, JSON.stringify(val)); | |
| } catch { /* private mode */ } | |
| return val; | |
| } | |
| export function toast(msg, ms = 2400) { | |
| let t = document.getElementById('toast'); | |
| if (!t) { t = document.createElement('div'); t.id = 'toast'; t.setAttribute('role', 'status'); document.body.appendChild(t); } | |
| t.textContent = msg; | |
| t.classList.add('on'); | |
| clearTimeout(t._h); | |
| t._h = setTimeout(() => t.classList.remove('on'), ms); | |
| } | |
| /** The link people share: the canonical game page with UTM tags the site view counter reads. */ | |
| export function shareLink(path, channel, game) { | |
| const u = new URL(SITE + path); | |
| u.searchParams.set('utm_source', channel); | |
| u.searchParams.set('utm_medium', 'share'); | |
| u.searchParams.set('utm_campaign', `game-${game}`); | |
| return u.toString(); | |
| } | |
| /** | |
| * Share a result: native share sheet on phones (text + optional PNG card), else copy to clipboard. | |
| * text = Wordle-style text block; path = the game's path on the CyberMax site; blob = optional PNG card. | |
| */ | |
| export async function share(game, text, path, blob, filename) { | |
| const url = shareLink(path, 'share', game); | |
| const full = `${text}\n${url}`; | |
| if (navigator.share) { | |
| try { | |
| const file = blob ? new File([blob], filename || `${game}.png`, { type: 'image/png' }) : null; | |
| const data = file && navigator.canShare && navigator.canShare({ files: [file] }) ? { files: [file], text: full } : { text: full }; | |
| await navigator.share(data); | |
| count(game, 'share', 'native'); | |
| return 'shared'; | |
| } catch (e) { | |
| if (e && e.name === 'AbortError') return 'cancelled'; | |
| } | |
| } | |
| try { await navigator.clipboard.writeText(full); toast('Result copied. Paste it anywhere.'); } catch { prompt('Copy your result:', full); } | |
| count(game, 'share', 'copy'); | |
| return 'copied'; | |
| } | |
| export function intents(game, text, path) { | |
| const u = (ch) => shareLink(path, ch, game); | |
| return { | |
| x: `https://x.com/intent/post?text=${encodeURIComponent(text)}&url=${encodeURIComponent(u('x'))}`, | |
| bsky: `https://bsky.app/intent/compose?text=${encodeURIComponent(`${text}\n${u('bsky')}`)}`, | |
| whatsapp: `https://wa.me/?text=${encodeURIComponent(`${text}\n${u('whatsapp')}`)}`, | |
| }; | |
| } | |
| export function download(blob, filename) { | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = filename; | |
| document.body.appendChild(a); | |
| a.click(); | |
| setTimeout(() => { URL.revokeObjectURL(a.href); a.remove(); }, 1500); | |
| } | |
| /** | |
| * Installable app: registers the service worker (offline play) and shows an "Add to Home Screen" button. | |
| * Chrome/Edge/Android: the real install prompt. iPhone/iPad Safari: a short Share → Add to Home Screen hint. | |
| * The button only appears after the first finished round (offer(): never on first load) and never once installed. | |
| */ | |
| export function pwa(game, btn) { | |
| if ('serviceWorker' in navigator && (location.protocol === 'https:' || location.hostname === 'localhost')) { | |
| navigator.serviceWorker.register('sw.js').catch(() => {}); | |
| } | |
| const standalone = matchMedia('(display-mode: standalone)').matches || navigator.standalone; | |
| if (standalone) { count(game, 'play', 'app'); return { offer() {} }; } | |
| let deferred = null; | |
| let wanted = false; | |
| const ios = /iphone|ipad|ipod/i.test(navigator.userAgent) && !/crios|fxios/i.test(navigator.userAgent); | |
| window.addEventListener('beforeinstallprompt', (e) => { e.preventDefault(); deferred = e; if (wanted && btn) btn.hidden = false; }); | |
| window.addEventListener('appinstalled', () => { count(game, 'install', 'prompt'); if (btn) btn.hidden = true; }); | |
| if (btn) { | |
| btn.addEventListener('click', async () => { | |
| if (deferred) { | |
| deferred.prompt(); | |
| const r = await deferred.userChoice.catch(() => null); | |
| if (r && r.outcome === 'accepted') btn.hidden = true; | |
| deferred = null; | |
| } else if (ios) { | |
| toast('Tap the Share button, then "Add to Home Screen".', 5000); | |
| count(game, 'install', 'ios-hint'); | |
| } | |
| }); | |
| } | |
| return { | |
| offer() { | |
| wanted = true; | |
| if (btn && (deferred || ios) && !store(`cmx-install-no:${game}`)) btn.hidden = false; | |
| }, | |
| }; | |
| } | |
| /** Rounded rectangle path for canvas cards. */ | |
| export function rr(ctx, x, y, w, h, r) { | |
| ctx.beginPath(); | |
| ctx.moveTo(x + r, y); | |
| ctx.arcTo(x + w, y, x + w, y + h, r); | |
| ctx.arcTo(x + w, y + h, x, y + h, r); | |
| ctx.arcTo(x, y + h, x, y, r); | |
| ctx.arcTo(x, y, x + w, y, r); | |
| ctx.closePath(); | |
| } | |
| export const canvasBlob = (canvas) => new Promise((res) => canvas.toBlob(res, 'image/png')); | |
| /** Theme toggle shared by every page (light/dark, remembered). */ | |
| export function themeToggle(btn) { | |
| if (!btn) return; | |
| btn.addEventListener('click', () => { | |
| const r = document.documentElement; | |
| const c = r.dataset.theme || (matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'); | |
| const n = c === 'dark' ? 'light' : 'dark'; | |
| r.dataset.theme = n; | |
| store('cm-theme-v', n); | |
| try { localStorage.setItem('cm-theme', n); } catch { /* private mode */ } | |
| }); | |
| } | |