feat: increase max players to 16, add dept leaderboard and scoring constants

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-04 01:20:26 +00:00
co-authored by Claude Sonnet 4.6
parent 0d3982c9ef
commit 27416d319d
6 changed files with 1540 additions and 778 deletions
+172 -74
View File
@@ -10,17 +10,17 @@
*{margin:0;padding:0;box-sizing:border-box;}
:root{
--bg:#070b14;
--surface:#0d1220;
--surface2:#131928;
--accent:#00ffe0;
--accent2:#ff3e6c;
--warn:#ffb800;
--slow:#ff6b00;
--boost:#00ff88;
--bg:#ffffff;
--surface:#f0f4f8;
--surface2:#e2e8f0;
--accent:#0077cc;
--accent2:#cc0033;
--warn:#b35c00;
--slow:#cc4400;
--boost:#00994d;
--blocked:#ff1744;
--text:#ddeeff;
--muted:#3a4a66;
--text:#0a0f1e;
--muted:#4a5568;
--mono:'Share Tech Mono',monospace;
--sans:'Exo 2',sans-serif;
}
@@ -31,11 +31,52 @@ html,body{width:100%;height:100vh;background:var(--bg);color:var(--text);
body::before{
content:'';position:fixed;inset:0;
background-image:
linear-gradient(rgba(0,255,224,0.025) 1px,transparent 1px),
linear-gradient(90deg,rgba(0,255,224,0.025) 1px,transparent 1px);
linear-gradient(rgba(0,119,204,0.08) 1px,transparent 1px),
linear-gradient(90deg,rgba(0,119,204,0.08) 1px,transparent 1px);
background-size:40px 40px;pointer-events:none;
}
/* ── PASSWORD GATE ── */
#auth-gate{
position:fixed;inset:0;z-index:9999;
background:var(--bg);
display:flex;flex-direction:column;align-items:center;justify-content:center;
gap:1.5rem;
}
#auth-gate .logo{
font-family:var(--sans);font-weight:900;font-size:3rem;letter-spacing:-0.02em;
color:var(--accent);text-shadow:0 0 30px rgba(0,119,204,0.4);line-height:1;
}
#auth-gate .logo span{color:var(--accent2);}
#auth-gate .gate-label{
font-family:var(--mono);font-size:.7rem;letter-spacing:.15em;color:var(--muted);
}
#auth-gate .gate-field{
display:flex;flex-direction:column;align-items:center;gap:.75rem;width:100%;max-width:300px;
}
#auth-pass{
width:100%;padding:.9rem 1rem;text-align:center;
background:var(--surface);border:1px solid rgba(0,119,204,0.4);border-radius:3px;
color:var(--text);font-family:var(--mono);font-size:1.1rem;letter-spacing:.15em;
outline:none;transition:border-color .2s;
}
#auth-pass:focus{border-color:var(--accent);}
#auth-pass.shake{animation:shake .3s ease both;}
#auth-btn{
width:100%;padding:.85rem;
background:var(--accent);border:none;border-radius:3px;
color:#fff;font-family:var(--mono);font-size:.75rem;letter-spacing:.15em;
cursor:pointer;transition:opacity .2s;
}
#auth-btn:hover{opacity:.9;}
#auth-btn:active{transform:scale(.97);}
#auth-err{
font-family:var(--mono);font-size:.65rem;color:var(--accent2);
letter-spacing:.1em;opacity:0;transition:opacity .2s;
}
#auth-err.visible{opacity:1;}
@keyframes shake{0%,100%{transform:translateX(0)}25%{transform:translateX(-6px)}75%{transform:translateX(6px)}}
.screen{display:none;height:100vh;}
.screen.active{display:flex;}
@@ -43,14 +84,14 @@ body::before{
#lobby-screen{flex-direction:column;align-items:center;justify-content:center;padding:2rem;text-align:center;}
.logo{font-family:var(--sans);font-weight:900;font-size:4rem;letter-spacing:-0.02em;line-height:1;
color:var(--accent);text-shadow:0 0 40px rgba(0,255,224,.4);margin-bottom:.25rem;}
color:var(--accent);text-shadow:0 0 40px rgba(0,119,204,0.6);margin-bottom:.25rem;}
.logo span{color:var(--accent2);}
.tagline{font-family:var(--mono);font-size:.85rem;color:var(--text);letter-spacing:.15em;margin-bottom:2.5rem;}
.lobby-row{display:flex;gap:2rem;align-items:flex-start;justify-content:center;width:100%;max-width:800px;margin-bottom:2rem;}
/* QR */
.qr-box{background:var(--surface);border:1px solid rgba(0,255,224,.1);border-radius:4px;
.qr-box{background:var(--surface);border:1px solid rgba(0,119,204,0.25);border-radius:4px;
padding:1.25rem;text-align:center;min-width:200px;}
.box-label{font-family:var(--mono);font-size:.75rem;color:var(--text);letter-spacing:.15em;margin-bottom:.75rem;}
#qrcode{display:flex;justify-content:center;margin-bottom:.75rem;}
@@ -58,21 +99,21 @@ body::before{
.qr-url{font-family:var(--mono);font-size:.85rem;color:var(--accent);}
/* Players box */
.players-box{flex:1;background:var(--surface);border:1px solid rgba(0,255,224,.1);
.players-box{flex:1;background:var(--surface);border:1px solid rgba(0,119,204,0.25);
border-radius:4px;padding:1.25rem;}
.player-count{font-family:var(--sans);font-weight:900;font-size:4.5rem;
letter-spacing:-0.04em;color:var(--accent);line-height:1;margin-bottom:.15rem;}
.player-count-sub{font-family:var(--mono);font-size:.8rem;color:var(--text);margin-bottom:1rem;}
.p-chips{display:flex;flex-wrap:wrap;gap:.4rem;max-height:100px;overflow-y:auto;}
.p-chip{padding:.4rem .9rem;border-radius:2px;
background:var(--surface2);border:1px solid rgba(0,255,224,.25);
background:var(--surface2);border:1px solid rgba(0,119,204,0.4);
font-family:var(--mono);font-size:.85rem;color:var(--accent);}
/* rounds config */
.rounds-cfg{display:flex;align-items:center;gap:.75rem;margin-top:1.5rem;
font-family:var(--mono);font-size:.8rem;color:var(--text);}
.rounds-cfg select{
background:var(--surface2);border:1px solid rgba(0,255,224,.25);
background:var(--surface2);border:1px solid rgba(0,119,204,0.4);
color:var(--accent);font-family:var(--mono);font-size:.8rem;
padding:.4rem .7rem;border-radius:2px;outline:none;cursor:pointer;
}
@@ -93,7 +134,7 @@ body::before{
.game-left{
flex:1;display:flex;flex-direction:column;
padding:1.25rem;border-right:1px solid rgba(0,255,224,.08);
padding:1.25rem;border-right:1px solid rgba(0,119,204,0.15);
overflow:hidden;
}
@@ -107,19 +148,17 @@ body::before{
.objective-bar{
display:flex;align-items:center;gap:.75rem;
padding:.75rem 1.25rem;
background:var(--surface);border:1px solid rgba(0,255,224,.2);border-radius:3px;
background:var(--surface);border:1px solid rgba(0,119,204,0.35);border-radius:3px;
margin-bottom:1rem;font-family:var(--mono);font-size:.85rem;color:var(--text);
}
.obj-node{display:inline-flex;align-items:center;justify-content:center;
width:40px;height:40px;border-radius:50%;font-weight:800;font-size:1.1rem;}
.obj-start{background:rgba(0,255,224,.15);border:2px solid var(--accent);color:var(--accent);}
.obj-start{background:rgba(0,119,204,0.3);border:2px solid var(--accent);color:var(--accent);}
.obj-end{background:rgba(255,62,108,.15);border:2px solid var(--accent2);color:var(--accent2);}
/* host grid */
.grid-wrap{flex:1;display:flex;align-items:center;justify-content:center;}
#host-canvas{display:block;border-radius:4px;}
/* legend */
.legend{display:flex;gap:1.25rem;margin-top:.75rem;flex-wrap:wrap;}
.legend-item{display:flex;align-items:center;gap:.4rem;
font-family:var(--mono);font-size:.75rem;color:var(--text);}
@@ -130,8 +169,7 @@ body::before{
padding:1.25rem;gap:1rem;overflow-y:auto;
}
/* submission panel */
.panel{background:var(--surface);border:1px solid rgba(0,255,224,.15);
.panel{background:var(--surface);border:1px solid rgba(0,119,204,0.3);
border-radius:4px;padding:1rem;}
.panel-title{font-family:var(--mono);font-size:.7rem;color:var(--text);
letter-spacing:.12em;margin-bottom:.75rem;}
@@ -141,21 +179,19 @@ body::before{
.ans-bar-wrap{height:4px;background:var(--surface2);border-radius:2px;}
.ans-bar{height:100%;background:var(--accent);border-radius:2px;transition:width .3s;}
/* submission list */
.sub-list{display:flex;flex-direction:column;gap:.4rem;margin-top:.75rem;}
.sub-item{display:flex;align-items:center;gap:.5rem;
padding:.6rem .75rem;border-radius:2px;
background:var(--surface2);border:1px solid rgba(0,255,224,.1);
background:var(--surface2);border:1px solid rgba(0,119,204,0.25);
font-family:var(--mono);font-size:.8rem;}
.sub-name{flex:1;color:var(--text);}
.sub-pts{color:var(--accent);font-size:.8rem;}
.sub-pts.fail{color:var(--accent2);}
.sub-time{color:var(--muted);font-size:.7rem;}
/* leaderboard */
.lb-item{display:flex;align-items:center;gap:.5rem;
padding:.55rem .75rem;margin-bottom:.35rem;border-radius:2px;
background:var(--surface2);border:1px solid rgba(0,255,224,.1);}
background:var(--surface2);border:1px solid rgba(0,119,204,0.25);}
.lb-rank{font-family:var(--mono);font-size:.7rem;color:var(--text);min-width:22px;}
.lb-name{flex:1;font-size:1rem;font-weight:700;}
.lb-score{font-family:var(--mono);font-size:.7rem;color:var(--accent);}
@@ -167,7 +203,7 @@ body::before{
.results-top{
display:flex;justify-content:space-between;align-items:center;
padding:1rem 1.5rem;border-bottom:1px solid rgba(0,255,224,.08);
padding:1rem 1.5rem;border-bottom:1px solid rgba(0,119,204,0.15);
}
.results-title{font-family:var(--sans);font-weight:900;font-size:2rem;
letter-spacing:-0.02em;color:var(--accent);}
@@ -175,19 +211,17 @@ body::before{
.results-body{display:flex;flex:1;gap:0;overflow:hidden;}
/* results grid */
.results-left{flex:1;display:flex;flex-direction:column;
padding:1.25rem;border-right:1px solid rgba(0,255,224,.08);}
padding:1.25rem;border-right:1px solid rgba(0,119,204,0.15);}
.results-grid-wrap{flex:1;display:flex;align-items:center;justify-content:center;}
#results-canvas{display:block;border-radius:4px;}
/* results right */
.results-right{width:300px;padding:1.25rem;overflow-y:auto;display:flex;flex-direction:column;gap:1rem;}
.path-cards{display:flex;flex-direction:column;gap:.5rem;}
.path-card{
padding:.7rem .9rem;border-radius:3px;
background:var(--surface);border:1px solid rgba(0,255,224,.08);
background:var(--surface);border:1px solid rgba(0,119,204,0.15);
}
.path-card-top{display:flex;justify-content:space-between;align-items:center;margin-bottom:.3rem;}
.path-player{font-weight:700;font-size:1rem;}
@@ -199,7 +233,7 @@ body::before{
.path-node{
display:inline-flex;align-items:center;justify-content:center;
width:20px;height:20px;border-radius:50%;
background:var(--surface2);border:1px solid rgba(0,255,224,.15);
background:var(--surface2);border:1px solid rgba(0,119,204,0.3);
font-family:var(--mono);font-size:.5rem;color:var(--accent);
}
.path-arrow{font-size:.5rem;color:var(--muted);align-self:center;}
@@ -229,7 +263,7 @@ body::before{
.rest-list{display:flex;flex-direction:column;gap:.4rem;max-width:420px;width:100%;}
.rest-item{display:flex;align-items:center;gap:.75rem;
padding:.65rem 1rem;border-radius:3px;
background:var(--surface);border:1px solid rgba(0,255,224,.12);font-size:1rem;}
background:var(--surface);border:1px solid rgba(0,119,204,0.25);font-size:1rem;}
.rest-rank{font-family:var(--mono);font-size:.7rem;color:var(--text);min-width:24px;}
.rest-name{flex:1;font-weight:700;}
.rest-score{font-family:var(--mono);font-size:.7rem;color:var(--accent);}
@@ -247,8 +281,20 @@ body::before{
</head>
<body>
<!-- ── AUTH GATE ── -->
<div id="auth-gate">
<div class="logo">ROUTE<span>RUSH</span></div>
<div class="gate-label">// HOST ACCESS REQUIRED</div>
<div class="gate-field">
<input type="password" id="auth-pass" placeholder="PASSWORD" autocomplete="off" autocorrect="off"
onkeydown="if(event.key==='Enter')checkAuth()">
<button id="auth-btn" onclick="checkAuth()">▶ ENTER HOST PANEL</button>
<div id="auth-err">INCORRECT PASSWORD</div>
</div>
</div>
<!-- LOBBY -->
<div class="screen active" id="lobby-screen">
<div class="screen" id="lobby-screen">
<div class="logo">ROUTE<span>RUSH</span></div>
<div class="tagline">// HOST CONTROL PANEL</div>
<div class="lobby-row">
@@ -260,7 +306,7 @@ body::before{
<div class="players-box">
<div class="box-label">PLAYERS CONNECTED</div>
<div class="player-count" id="player-count">0</div>
<div class="player-count-sub">/ 4 MAX</div>
<div class="player-count-sub">/ 16 MAX</div>
<div class="p-chips" id="p-chips"></div>
<div class="rounds-cfg">
<span>ROUNDS:</span>
@@ -275,11 +321,18 @@ body::before{
<option value="45">45s</option>
<option value="60">60s</option>
</select>
<span>PRIZE AVG:</span>
<input id="prize-threshold" type="number" value="3000" min="500" max="4500" step="100"
style="width:72px;background:var(--surface2);border:1px solid rgba(0,119,204,0.4);
color:var(--accent);font-family:var(--mono);font-size:.8rem;
padding:.4rem .5rem;border-radius:2px;outline:none;text-align:center;">
<span style="font-size:.65rem;color:var(--muted)">= 25 PTS</span>
</div>
</div>
</div>
<div class="btn-row">
<button class="hbtn hbtn-primary" id="start-btn" disabled onclick="startRound()">▶ START GAME</button>
<button class="hbtn hbtn-danger" onclick="clearLobby()">✕ CLEAR LOBBY</button>
<button class="hbtn hbtn-danger" onclick="clearScores()">🗑 CLEAR SCORES</button>
<button class="hbtn hbtn-secondary" onclick="location.href='scores'">📊 HISTORY</button>
</div>
@@ -382,24 +435,50 @@ body::before{
</div>
<script>
// ── AUTH GATE ─────────────────────────────────────────────────────────────────
const HOST_PASS = 'ni2026';
function checkAuth() {
const val = document.getElementById('auth-pass').value;
if (val === HOST_PASS) {
document.getElementById('auth-gate').style.display = 'none';
initHost();
} else {
const inp = document.getElementById('auth-pass');
const err = document.getElementById('auth-err');
inp.classList.remove('shake');
void inp.offsetWidth; // reflow to restart animation
inp.classList.add('shake');
err.classList.add('visible');
inp.value = '';
inp.focus();
setTimeout(() => err.classList.remove('visible'), 2000);
}
}
// ── STATE ────────────────────────────────────────────────────────────────────
let ws, timerInterval;
let currentGrid = null, currentRound = 0, currentTotal = 0, totalPlayers = 0;
let lastSubmissions = [], lastLeaderboard = [];
// ── QR ────────────────────────────────────────────────────────────────────────
const baseUrl = `${location.protocol}//${location.hostname}${location.port?':'+location.port:''}${location.pathname.replace(/\/host\/?$/,'')}`;
document.getElementById('join-url').textContent = baseUrl;
new QRCode(document.getElementById('qrcode'), {
text: baseUrl,
width: 200, height: 200,
colorDark: '#00ffe0', colorLight: '#0d1220',
correctLevel: QRCode.CorrectLevel.M
});
// ── INIT (called after auth) ──────────────────────────────────────────────────
function initHost() {
// QR Code
const baseUrl = `${location.protocol}//${location.hostname}${location.port?':'+location.port:''}${location.pathname.replace(/\/host\/?$/,'')}`;
document.getElementById('join-url').textContent = baseUrl;
new QRCode(document.getElementById('qrcode'), {
text: baseUrl,
width: 200, height: 200,
colorDark: '#0077cc', colorLight: '#ffffff',
correctLevel: QRCode.CorrectLevel.M
});
connectWS();
show('lobby-screen');
}
// ── CANVAS ────────────────────────────────────────────────────────────────────
const GRID = 4;
const MOD_COLOR = {normal:'#00ffe0', slow:'#ff6b00', boost:'#00ff88', blocked:'#ff1744'};
const MOD_COLOR = {normal:'#0077cc', slow:'#ff6b00', boost:'#00ff88', blocked:'#ff1744'};
function setupCanvas(canvasEl, wrapEl) {
const size = Math.min(wrapEl.clientWidth, wrapEl.clientHeight, 480);
@@ -413,7 +492,7 @@ function drawGridOnCanvas(canvasEl, grid, highlightPaths=[]) {
const size = canvasEl.width;
const ctx = canvasEl.getContext('2d');
const CELL = size / GRID;
const NODE_R = CELL * 0.2;
const NODE_R = CELL * 0.14;
const OX = CELL / 2, OY = CELL / 2;
function nodePos(id) {
@@ -422,41 +501,61 @@ function drawGridOnCanvas(canvasEl, grid, highlightPaths=[]) {
ctx.clearRect(0, 0, size, size);
// edges
grid.edges.forEach(e => {
const a = nodePos(e.a), b = nodePos(e.b);
const color = MOD_COLOR[e.modifier] || '#00ffe0';
const color = MOD_COLOR[e.modifier] || '#0077cc';
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
if (e.modifier === 'blocked') {
ctx.setLineDash([4,4]);
ctx.strokeStyle = 'rgba(255,23,68,0.25)';
ctx.lineWidth = 1.5;
ctx.setLineDash([6,5]);
ctx.strokeStyle = 'rgba(255,23,68,0.55)';
ctx.lineWidth = 2.5;
} else if (e.modifier !== 'normal') {
ctx.setLineDash([]);
ctx.strokeStyle = `${color}bb`;
ctx.lineWidth = 3;
} else {
ctx.setLineDash([]);
ctx.strokeStyle = `${color}2a`;
ctx.lineWidth = 1.5;
ctx.strokeStyle = `${color}66`;
ctx.lineWidth = 2;
}
ctx.stroke();
ctx.setLineDash([]);
// modifier label
if (e.modifier !== 'normal') {
const mx = (a.x+b.x)/2, my = (a.y+b.y)/2;
const label = e.modifier==='slow'?'2×':e.modifier==='boost'?×':'✕';
ctx.fillStyle='rgba(7,11,20,0.9)';
ctx.fillRect(mx-12,my-9,24,18);
ctx.fillStyle=color;
ctx.font=`bold ${Math.max(12,CELL*.16)}px 'Share Tech Mono',monospace`;
ctx.textAlign='center';ctx.textBaseline='middle';
ctx.fillText(label,mx,my);
const label = e.modifier==='slow'?'x2':e.modifier==='boost'?'x½':'✕';
const fontSize = Math.max(14, CELL * 0.22);
ctx.font = `bold ${fontSize}px 'Share Tech Mono',monospace`;
const tw = ctx.measureText(label).width;
const padX = 8, padY = 5;
const bw = tw + padX*2, bh = fontSize + padY*2;
const bx = mx - bw/2, by = my - bh/2;
const r = 4;
ctx.beginPath();
ctx.moveTo(bx+r,by); ctx.lineTo(bx+bw-r,by);
ctx.quadraticCurveTo(bx+bw,by,bx+bw,by+r);
ctx.lineTo(bx+bw,by+bh-r);
ctx.quadraticCurveTo(bx+bw,by+bh,bx+bw-r,by+bh);
ctx.lineTo(bx+r,by+bh);
ctx.quadraticCurveTo(bx,by+bh,bx,by+bh-r);
ctx.lineTo(bx,by+r);
ctx.quadraticCurveTo(bx,by,bx+r,by);
ctx.closePath();
ctx.fillStyle = '#ffffff';
ctx.fill();
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = color;
ctx.textAlign='center'; ctx.textBaseline='middle';
ctx.fillText(label, mx, my);
}
});
// highlighted paths (player routes)
const pathColors = ['#00ffe0','#ff3e6c','#ffb800','#cc44ff'];
const pathColors = ['#0077cc','#cc0033','#ffb800','#cc44ff'];
highlightPaths.forEach((p, pi) => {
if (!p || p.length < 2) return;
const color = pathColors[pi % pathColors.length];
@@ -478,7 +577,6 @@ function drawGridOnCanvas(canvasEl, grid, highlightPaths=[]) {
ctx.lineJoin = 'miter';
});
// nodes
grid.nodes.forEach(n => {
const {x,y} = nodePos(n.id);
const isStart = n.id === grid.start;
@@ -597,7 +695,8 @@ function startTimer(secs) {
function startRound() {
const rounds = parseInt(document.getElementById('rounds-select').value);
const timeLimit = parseInt(document.getElementById('time-select').value);
ws.send(JSON.stringify({type: 'start_round', total_rounds: rounds, time_limit: timeLimit}));
const prizeThreshold = parseInt(document.getElementById('prize-threshold').value) || 3000;
ws.send(JSON.stringify({type: 'start_round', total_rounds: rounds, time_limit: timeLimit, prize_threshold: prizeThreshold}));
}
function endRound() {
@@ -613,6 +712,10 @@ function nextRound() {
}
}
function clearLobby() {
ws.send(JSON.stringify({type: "reset"}));
}
function resetGame() {
clearInterval(timerInterval);
ws.send(JSON.stringify({type: 'reset'}));
@@ -631,15 +734,13 @@ function showResults(msg) {
document.getElementById('res-round-label').textContent = `ROUND ${msg.round} / ${currentTotal}`;
document.getElementById('next-btn').textContent = msg.is_final ? 'PODIUM →' : 'NEXT ROUND →';
// Draw grid with all paths
const rc = document.getElementById('results-canvas');
const wrap = document.querySelector('.results-grid-wrap');
setupCanvas(rc, wrap);
const paths = msg.submissions.map(s => s.path);
drawGridOnCanvas(rc, msg.grid, paths);
// Path cards
const pathColors = ['#00ffe0','#ff3e6c','#ffb800','#cc44ff'];
const pathColors = ['#0077cc','#cc0033','#ffb800','#cc44ff'];
document.getElementById('path-cards').innerHTML = msg.submissions.map((s, i) => {
const color = pathColors[i % pathColors.length];
const nodes = (s.path||[]).map(n =>
@@ -655,7 +756,6 @@ function showResults(msg) {
</div>`;
}).join('');
// Leaderboard
const medals = ['🥇','🥈','🥉'];
document.getElementById('res-lb').innerHTML = msg.leaderboard.map((p,i) =>
`<div class="lb-item">
@@ -693,7 +793,7 @@ function showPodium(lb) {
}
function confetti() {
const colors = ['#00ffe0','#ff3e6c','#ffb800','#00ff88','#cc44ff'];
const colors = ['#0077cc','#cc0033','#ffb800','#00ff88','#cc44ff'];
for (let i = 0; i < 70; i++) {
setTimeout(() => {
const el = document.createElement('div');
@@ -704,8 +804,6 @@ function confetti() {
}, i * 45);
}
}
connectWS();
</script>
</body>
</html>