/* NOTE: this just contains user interface logic; there is probably not much interesting stuff here */ let name, n, x, n_old, n_last; async function init() { const params = (new URL(document.URL)).searchParams; if(params.has('name')) { name = params.get('name'); } else { /* missing name, redirect to start screen */ location.replace('.'); return; } /* people can do more than 2^53 button clicks, so we need big integers */ if(params.has('n') && params.has('x')) { n = BigInt(params.get('n')); x = BigInt(params.get('x')); } else { n = 0n; x = 0n; } n_old = 0n; n_last = -1n; if(typeof prepare == 'function') { await prepare(); } const button = document.querySelector('button'); button.textContent = n; button.addEventListener('click', onClick); setInterval(() => { if(n > n_last) { /* to spare the browser, we do this at most 10 times per second */ const params = new URLSearchParams({name, n, x}); history.replaceState(null, '', '?' + params); button.textContent = n; n_last = n; } }, 100); communicate(); } document.addEventListener('DOMContentLoaded', init); async function communicate() { const error = document.querySelector('#error'); try { /* only submit increased click counts, otherwise just fetch scores */ const path = 'server.php' + ((n_last > n_old) ? location.search : ''); const result = await fetch(path); if(!result.ok) { throw new Error(`${result.status} ${result.statusText}`); } if(n_last > n_old) { n_old = n_last; } const json = await result.json(); if(json.flag && !location.pathname.endsWith('/' + json.flag)) { /* advance to next level */ location.replace(json.flag + location.search); return; } if(json.scores) { displayScores(json.scores, json.time); } if(json.error) { throw new Error(json.error); } const timeStr = new Date(json.time * 1000).toLocaleTimeString('de-DE'); error.textContent = `Zuletzt aktualisiert: ${timeStr}`; } catch(e) { console.warn(e); error.textContent = e; } setTimeout(communicate, 1000); } function displayScores(scores, time) { const table = document.querySelector('table'); while(table.rows.length > 1) { table.deleteRow(-1); } scores.sort((a, b) => { const n_a = BigInt(a.n); const n_b = BigInt(b.n); if(n_a > n_b) { return -1; } if(n_a < n_b) { return 1; } if(a.time < b.time) { return -1; } if(a.time > b.time) { return 1; } return 0; }); scores.forEach((entry) => { const row = table.insertRow(); const nameCell = row.insertCell(); /* protect against unicode bidi control characters */ const bdi = document.createElement('bdi'); bdi.textContent = entry.name; nameCell.appendChild(bdi); nameCell.title = entry.name; const timeCell = row.insertCell(); const date = new Date(entry.time * 1000); if(time - entry.time < 86400) { timeCell.textContent = date.toLocaleTimeString('de-DE'); } else { timeCell.textContent = date.toLocaleDateString('de-DE'); } timeCell.title = date.toLocaleString('de-DE'); const nCell = row.insertCell(); nCell.textContent = entry.n; nCell.title = entry.n; }); } /* compute SHA-256 digest represented as integer (big endian) */ async function sha256(str) { const msg = new TextEncoder().encode(str); const buffer = await crypto.subtle.digest('SHA-256', msg); const bytes = new Uint8Array(buffer); let val = 0n; for(const b of bytes) { val = (val << 8n) | BigInt(b); } return val; } /* compute a^k modulo m */ function pow(a, k, m) { let b = 1n; for(; k; k >>= 1n) { if(k & 1n) { b *= a; b %= m; } a *= a; a %= m; } return b; }