Put a real world board behind the RANKINGS button
Ranked on KILLS IN A SINGLE RUN. Games survived is the same information at a tenth the resolution — most players land between 5 and 20, so that board is a wall of ties broken by who got there first. Games ride along as the tiebreak and as a column. The image is python:3.12-alpine running server.py now, not nginx. The board needs one process that can write, and everything the nginx conf did here — the no-cache headers and the manifest MIME type — is a few lines of stdlib. /data must be a persistent volume or every deploy resets the ladder to empty. A name belongs to whoever took it first: the client mints a random id once and the row remembers it, so a stranger cannot overwrite your score. The id never leaves the server. That is the whole security model and it is enough. The prompt to sign the board appears on exactly one occasion — a run that beat your own best kill count, on the tap that leaves the death screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1fccc11b1d
commit
3291246425
8 files changed
+374
-48
No files matched your search
@@ -131,7 +131,46 @@ let stPeriod = 'all';
|
||||
const openStats = () => { stats.drawStats(stPeriod, SPRV); go('stats'); };
|
||||
document.getElementById('mStart').addEventListener('click', () => go('setup'));
|
||||
document.getElementById('mStats').addEventListener('click', openStats);
|
||||
document.getElementById('mRanks').addEventListener('click', () => { stats.drawRanks(); go('ranks'); });
|
||||
document.getElementById('mRanks').addEventListener('click', () => { go('ranks'); stats.drawRanks(); });
|
||||
|
||||
// --- THE NAME PROMPT. It appears on exactly one occasion: a run that beat your own best kill
|
||||
// count, on the tap that takes you off the death screen. Anything more often is a form standing
|
||||
// between you and the next match.
|
||||
const nbName = document.getElementById('nbName'), nbMsg = document.getElementById('nbMsg');
|
||||
let nbRun = null;
|
||||
function askName(run) {
|
||||
nbRun = run;
|
||||
document.getElementById('nbScore').innerHTML =
|
||||
`<b>${run.kills.toLocaleString('en-GB')}</b> KILLS · <b>${run.games}</b> GAME${run.games === 1 ? '' : 'S'}`;
|
||||
nbName.value = stats.savedName();
|
||||
nbMsg.textContent = 'PUT IT ON THE WORLD BOARD.';
|
||||
document.getElementById('nbSend').disabled = false;
|
||||
go('newbest');
|
||||
// focus only where a keyboard is already there. On a phone this would throw the soft keyboard up
|
||||
// over the score you just earned, before you have read it.
|
||||
if (!matchMedia('(pointer: coarse)').matches) nbName.focus();
|
||||
}
|
||||
document.getElementById('nbSkip').addEventListener('click', () => { nbRun = null; go('splash'); });
|
||||
document.getElementById('nbSend').addEventListener('click', async () => {
|
||||
const name = stats.tidyName(nbName.value);
|
||||
if (!name) { nbMsg.textContent = 'A NAME FIRST.'; nbName.focus(); return; }
|
||||
nbName.value = name; // show what is actually going on the board
|
||||
if (!nbRun) return go('splash');
|
||||
const btn = document.getElementById('nbSend');
|
||||
btn.disabled = true; nbMsg.textContent = 'SENDING…'; // the double-tap posts twice otherwise
|
||||
try {
|
||||
const r = await stats.submit(name, nbRun.kills, nbRun.games);
|
||||
nbRun = null;
|
||||
go('ranks');
|
||||
stats.drawRanks(r.scores); // the board came back with the reply — no second round trip
|
||||
} catch (e) {
|
||||
btn.disabled = false;
|
||||
nbMsg.textContent = e.taken ? 'THAT NAME IS SPOKEN FOR. PICK ANOTHER.'
|
||||
: 'THE BOARD DID NOT ANSWER. TRY AGAIN?';
|
||||
if (e.taken) { nbName.select(); nbName.focus(); }
|
||||
}
|
||||
});
|
||||
nbName.addEventListener('keydown', e => { if (e.key === 'Enter') document.getElementById('nbSend').click(); });
|
||||
document.getElementById('cStart').addEventListener('click', start);
|
||||
for (const id of ['sBack', 'stBack', 'rkBack'])
|
||||
document.getElementById(id).addEventListener('click', () => go('splash'));
|
||||
@@ -190,8 +229,13 @@ addEventListener('pagehide', () => {
|
||||
const maybeRestart = () => {
|
||||
if (G.modal || G.mode !== 'eaten' || G.eatenT <= EATEN_RESTART) return;
|
||||
restartRun();
|
||||
refreshSplash(); // the run just ended: both splash figures have moved
|
||||
intro.hidden = false; go('splash');
|
||||
refreshSplash(); // the run just ended: the splash count has moved
|
||||
intro.hidden = false;
|
||||
// stats.lastRun() survives restartRun on purpose — it lives in the stats module, not on G, which
|
||||
// has just been restored from PRISTINE. Consumed on read, so the prompt appears once.
|
||||
const run = stats.lastRun();
|
||||
if (run && run.newKillBest && run.kills > 0) askName(run);
|
||||
else go('splash');
|
||||
};
|
||||
cv.addEventListener('pointerdown', maybeRestart);
|
||||
addEventListener('keydown', maybeRestart);
|
||||
|
||||
Reference in new issue
Block a user