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
+36
-7
@@ -115,13 +115,14 @@
|
||||
/* ONE overlay, four panels, swapped by #intro[data-stage]. Keeping the backdrop and the card frame
|
||||
alive across every screen is the point — a screen that unmounts the whole overlay flashes the
|
||||
court through between menus. */
|
||||
#splash, #setup, #stats, #ranks { display: none; }
|
||||
#splash, #setup, #stats, #ranks, #newbest { display: none; }
|
||||
#intro[data-stage="splash"] #splash { display: flex; flex-direction: column; flex: 1; }
|
||||
/* flex COLUMN, not block: it is what lets the footer's margin-top:auto reach the bottom of a
|
||||
screen whose content does not fill it */
|
||||
#intro[data-stage="setup"] #setup,
|
||||
#intro[data-stage="stats"] #stats,
|
||||
#intro[data-stage="ranks"] #ranks { display: flex; flex-direction: column; flex: 1;
|
||||
#intro[data-stage="ranks"] #ranks,
|
||||
#intro[data-stage="newbest"] #newbest { display: flex; flex-direction: column; flex: 1;
|
||||
animation: drop 0.3s ease-out; }
|
||||
/* the overlay is HIDDEN during play, not removed: a restart puts it straight back, and the
|
||||
display:grid above would otherwise win over the hidden attribute */
|
||||
@@ -250,6 +251,21 @@
|
||||
.rkrow span { flex: 1; min-width: 0; font: 400 11px Silkscreen, monospace; color: var(--cream);
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.rkrow i { font: 400 11px Silkscreen, monospace; font-style: normal; color: var(--dim); }
|
||||
/* ---- the name prompt. It is the only screen in the game that asks for typing, so the field is
|
||||
deliberately big, centred and already focused: on a phone this appears with the keyboard
|
||||
rising under it and there is no room for a second tap to find the input. */
|
||||
.cent { text-align: center; }
|
||||
#newbest h2 { font-size: 30px; margin-bottom: 6px; }
|
||||
#nbScore { margin: 0 0 22px; font: 400 11px Silkscreen, monospace; color: var(--dim);
|
||||
letter-spacing: 1px; }
|
||||
#nbScore b { color: var(--acid); font-weight: 400; }
|
||||
#nbName { display: block; width: 100%; box-sizing: border-box; margin: 0 auto 10px;
|
||||
padding: 14px 10px; background: var(--solid); border: 2px solid var(--line);
|
||||
border-radius: 0; color: var(--acid); font: 700 20px Silkscreen, monospace;
|
||||
letter-spacing: 3px; text-align: center; text-transform: uppercase;
|
||||
-webkit-appearance: none; -webkit-tap-highlight-color: transparent; }
|
||||
#nbName:focus { outline: none; border-color: var(--acid); }
|
||||
#nbName::placeholder { color: var(--dim); letter-spacing: 2px; font-size: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
@@ -271,7 +287,7 @@
|
||||
<button class="btn big" id="mStart">PLAY MATCH</button>
|
||||
<div class="two">
|
||||
<button class="btn sec" id="mStats">STATS<i id="mKills">0 KILLS</i></button>
|
||||
<button class="btn sec" id="mRanks">RANKINGS<i>SEASON 1</i></button>
|
||||
<button class="btn sec" id="mRanks">RANKINGS<i>WORLD BOARD</i></button>
|
||||
</div>
|
||||
<p id="build"></p>
|
||||
</div>
|
||||
@@ -304,14 +320,27 @@
|
||||
<div id="stW"></div>
|
||||
<div class="foot" id="stFoot"></div>
|
||||
</div>
|
||||
<!-- placeholder ladder until the leaderboard task lands — see stats.drawRanks -->
|
||||
<div id="ranks">
|
||||
<div class="hdr"><button class="chev" id="rkBack">‹</button><h2>RANKINGS</h2></div>
|
||||
<h3>SEASON 1<span>UNRANKED · OFFLINE</span></h3>
|
||||
<div class="hdr"><button class="chev" id="rkBack">‹</button><h2>WORLD BOARD</h2></div>
|
||||
<h3>BEST SINGLE RUN<span>KILLS · GAMES</span></h3>
|
||||
<div id="rkList"></div>
|
||||
</div>
|
||||
<!-- shown only when a run beats your own best kill count — see main.js askName -->
|
||||
<div id="newbest">
|
||||
<div class="mid">
|
||||
<h2 class="cent">NEW PERSONAL BEST</h2>
|
||||
<p id="nbScore"></p>
|
||||
<input id="nbName" maxlength="12" placeholder="YOUR NAME" autocomplete="off"
|
||||
autocapitalize="characters" spellcheck="false">
|
||||
<p class="none cent" id="nbMsg">PUT IT ON THE WORLD BOARD.</p>
|
||||
</div>
|
||||
<div class="foot bare">
|
||||
<button class="btn big" id="nbSend">SIGN THE BOARD</button>
|
||||
<button class="btn" id="nbSkip">NOT THIS TIME</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script type="module" src="main.js?v=123"></script>
|
||||
<script type="module" src="main.js?v=124"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user