152 lines
4.2 KiB
JavaScript
152 lines
4.2 KiB
JavaScript
/*
|
|
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;
|
|
}
|