← back to CelebritySignatures
public/game.html
577 lines
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>The Signature Games — Celebrity Signatures</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:ital,wght@0,400;0,600;0,700;1,400&display=swap" rel="stylesheet">
<style>
:root { --ink:#1a1a1a; --muted:#6b6b6b; --line:#e6e3dc; --bg:#f7f5f0; --green:#1d7a36; --red:#b3261e; }
* { box-sizing:border-box; }
body { margin:0; font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif; color:var(--ink); background:var(--bg); }
.masthead { position:sticky; top:0; z-index:30; display:flex; align-items:center; gap:22px; height:56px; padding:0 24px; background:#fff; border-bottom:1px solid var(--line); }
.wordmark { font:italic 600 24px/1 'Playfair Display', Georgia, serif; color:var(--ink); text-decoration:none; }
.gnav { margin-left:auto; display:flex; gap:20px; }
.gnav a { font-size:13.5px; color:var(--muted); text-decoration:none; padding:4px 2px; border-bottom:2px solid transparent; }
.gnav a.on, .gnav a:hover { color:var(--ink); border-bottom-color:var(--ink); }
main { max-width:720px; margin:0 auto; padding:30px 20px 80px; }
h1 { font:700 30px/1.2 'Playfair Display', Georgia, serif; margin:0 0 6px; text-align:center; }
.tagline { text-align:center; color:var(--muted); margin:0 0 24px; }
.panel { background:#fff; border:1px solid var(--line); border-radius:16px; padding:26px 28px; box-shadow:0 2px 10px rgba(0,0,0,.05); }
/* game picker */
.games { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
.gcard { border:1px solid var(--line); border-radius:12px; padding:16px 18px; cursor:pointer; background:#fff; text-align:left; font:inherit; transition:transform .1s, border-color .1s; }
.gcard:hover { transform:translateY(-2px); border-color:var(--ink); }
.gcard.on { background:var(--ink); color:#fff; border-color:var(--ink); }
.gcard.on .gd { color:#d8d4cc; }
.gcard b { font:600 16px 'Playfair Display', Georgia, serif; display:block; margin-bottom:3px; }
.gcard .gd { font-size:12.5px; color:var(--muted); line-height:1.4; }
.setup label { display:block; font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:18px 0 7px; }
.opts { display:flex; gap:8px; flex-wrap:wrap; }
.opt { padding:8px 16px; border:1px solid var(--line); border-radius:999px; background:#fff; cursor:pointer; font:inherit; font-size:13.5px; color:var(--muted); }
.opt.on { background:var(--ink); color:#fff; border-color:var(--ink); }
.go { display:block; width:100%; margin-top:24px; padding:13px; border-radius:999px; border:none; background:var(--ink); color:#fff; font:600 16px 'Playfair Display',Georgia,serif; cursor:pointer; }
.go:hover { background:#000; }
.go:disabled { opacity:.6; cursor:default; }
/* play */
.hud { display:flex; justify-content:space-between; align-items:baseline; font-size:13px; color:var(--muted); margin-bottom:14px; }
.hud b { color:var(--ink); font-size:16px; }
.streak { color:var(--green); font-weight:600; }
.prompt { text-align:center; font:italic 400 17px 'Playfair Display', Georgia, serif; color:var(--muted); margin-bottom:12px; }
.stage { background:#fff; border:1px solid var(--line); border-radius:12px; min-height:150px; display:flex; align-items:center; justify-content:center; gap:20px; padding:18px; margin-bottom:18px; }
.stage img { max-height:240px; max-width:100%; object-fit:contain; }
.stage img.sig { max-height:120px; }
.stage .duo { display:flex; gap:16px; align-items:center; justify-content:center; flex-wrap:wrap; }
.duo .d { text-align:center; }
.duo .d img { max-height:110px; max-width:260px; background:#fff; border:1px solid var(--line); border-radius:8px; padding:8px; }
/* Colored-ink signatures on clean white via the shared preview component. */
.stage .sig.signature-preview { display:inline-flex; height:120px; max-height:120px; }
.stage .sig.signature-preview .signature-image { max-height:120px; }
.duo .d .signature-preview { height:110px; max-width:260px; background:#fff; border:1px solid var(--line); border-radius:8px; padding:8px; }
.duo .d .lab { font:600 15px 'Playfair Display',Georgia,serif; margin-top:6px; }
.choices { display:grid; grid-template-columns:1fr 1fr; gap:10px; }
.choice { padding:13px 16px; border:1px solid var(--line); border-radius:10px; background:#fff; cursor:pointer; font:inherit; font-size:14.5px; text-align:center; transition:transform .08s; }
.choice:hover:not(:disabled) { transform:translateY(-1px); border-color:var(--ink); }
.choice:disabled { cursor:default; }
.choice.right { background:var(--green); color:#fff; border-color:var(--green); }
.choice.wrong { background:var(--red); color:#fff; border-color:var(--red); }
.choice.dim { opacity:.45; }
.choice.art { padding:8px; }
.choice.art img { width:100%; height:120px; object-fit:cover; border-radius:6px; display:block; pointer-events:none; }
.choice.art .cl { font-size:11.5px; margin-top:5px; color:inherit; min-height:14px; }
.reveal { margin-top:14px; text-align:center; font-size:14px; min-height:44px; }
.reveal a { color:var(--green); }
.next { display:block; margin:14px auto 0; padding:10px 30px; border-radius:999px; border:1px solid var(--ink); background:var(--ink); color:#fff; font:inherit; cursor:pointer; }
.prep { text-align:center; color:var(--muted); padding:30px 0; font-size:14px; }
.prep.err { color:#b0261a; font-weight:600; }
/* results */
.score-big { font:700 64px/1 'Playfair Display', Georgia, serif; text-align:center; margin:10px 0 2px; }
.verdict { text-align:center; font:italic 400 22px 'Playfair Display', Georgia, serif; color:var(--muted); margin-bottom:16px; }
.misses { font-size:13.5px; color:var(--muted); }
.misses a { color:var(--green); }
.best { text-align:center; font-size:13px; color:var(--muted); margin-top:14px; }
.again { display:block; margin:20px auto 0; padding:12px 34px; border-radius:999px; border:none; background:var(--ink); color:#fff; font:600 15px 'Playfair Display',Georgia,serif; cursor:pointer; }
/* lightning timer bar */
.timerbar { height:8px; border-radius:999px; background:#eee; overflow:hidden; margin-bottom:12px; }
.timerbar[hidden] { display:none; }
.tb-fill { height:100%; width:100%; background:var(--green); border-radius:999px; }
.tb-fill.run { transition:width 5s linear, background 5s linear; width:0%; background:var(--red); }
.tag2 { display:inline-block; font-size:11px; letter-spacing:.06em; text-transform:uppercase; color:var(--muted); background:#f0ece3; border-radius:999px; padding:3px 10px; margin-bottom:8px; }
/* leaderboard + share (results screen) */
.lb { margin-top:18px; border-top:1px solid var(--line); padding-top:14px; }
.lb-head { font:600 15px 'Playfair Display', Georgia, serif; margin-bottom:8px; }
.lb-head span { color:var(--muted); font:400 12px -apple-system,sans-serif; }
.lb-submit { display:flex; gap:8px; margin-bottom:10px; }
.lb-submit input { flex:1; font:inherit; font-size:13.5px; padding:8px 11px; border:1px solid var(--line); border-radius:8px; }
.lb-btn { font:inherit; font-size:13px; padding:8px 14px; border-radius:8px; border:1px solid var(--ink); background:var(--ink); color:#fff; cursor:pointer; white-space:nowrap; }
.lb-btn:disabled { opacity:.5; cursor:default; }
.lb-list { margin:0; padding-left:0; list-style:none; counter-reset:lb; }
.lb-list li { counter-increment:lb; display:flex; justify-content:space-between; font-size:13.5px; padding:5px 2px; border-bottom:1px solid #f0ece3; }
.lb-list li::before { content:counter(lb) "."; color:var(--muted); width:24px; display:inline-block; }
.lb-list li .nm { flex:1; }
.lb-list li.me { font-weight:700; color:var(--green); }
.lb-list li .sc { font-variant-numeric:tabular-nums; }
.share { display:flex; gap:10px; justify-content:center; margin-top:16px; }
.sh-btn { font:inherit; font-size:13.5px; padding:9px 18px; border-radius:999px; border:1px solid var(--line); background:#fff; color:var(--ink); cursor:pointer; text-decoration:none; }
.sh-btn:hover { border-color:var(--ink); }
@media (max-width:560px){ .choices{grid-template-columns:1fr} .games{grid-template-columns:1fr} }
@media (max-width:700px){
.masthead { height:auto; flex-wrap:wrap; padding:10px 14px; gap:8px 14px; }
.wordmark { font-size:19px; }
.gnav { margin-left:0; width:100%; gap:14px; overflow-x:auto; -webkit-overflow-scrolling:touch; }
.gnav a { font-size:12.5px; white-space:nowrap; }
main { padding:18px 12px 60px; }
h1 { font-size:24px; }
.panel { padding:18px 16px; border-radius:12px; }
.stage img { max-height:190px; }
.choice.art img { height:96px; }
.duo .d img { max-width:78vw; }
}
</style>
<!-- Google Analytics 4 (G-2HEVP6TD0J) -->
<script async src="https://www.googletagmanager.com/gtag/js?id=G-2HEVP6TD0J"></script>
<script>window.dataLayer=window.dataLayer||[];function gtag(){dataLayer.push(arguments);}gtag('js',new Date());gtag('config','G-2HEVP6TD0J');</script>
<script src="/assets/ga-events.js" defer></script>
<link rel="canonical" href="https://celebsignatures.com/game">
<meta name="description" content="Six games: name the artist, name the painting, spot the earlier signature, and a 5-second Lightning Round. Play free and climb the leaderboard.">
<link rel="apple-touch-icon" href="/assets/favicon.png">
<link rel="icon" type="image/png" href="/assets/favicon.png">
<meta property="og:type" content="website">
<meta property="og:site_name" content="Celebrity Signatures">
<meta property="og:title" content="The Signature Games — how well do you know history’s hands?">
<meta property="og:description" content="Six games: name the artist, name the painting, spot the earlier signature, and a 5-second Lightning Round. Play free and climb the leaderboard.">
<meta property="og:url" content="https://celebsignatures.com/game">
<meta property="og:image" content="https://celebsignatures.com/assets/og-share.jpg">
<meta name="twitter:card" content="summary_large_image">
<link rel="stylesheet" href="/assets/signature-preview.css">
<script type="module" src="/assets/signature-preview.js"></script>
<meta name="twitter:title" content="The Signature Games — how well do you know history’s hands?">
<meta name="twitter:description" content="Six games: name the artist, name the painting, spot the earlier signature, and a 5-second Lightning Round. Play free and climb the leaderboard.">
<meta name="twitter:image" content="https://celebsignatures.com/assets/og-share.jpg">
</head>
<body>
<div class="masthead">
<a class="wordmark" href="/">Celebrity Signatures</a>
<nav class="gnav">
<a href="/">Browse signatures</a>
<a href="/wallpaper">Create wallpaper</a>
<a href="/game" class="on">Play</a>
<a href="/upload">Upload your signature</a>
</nav>
</div>
<main>
<h1>The Signature Games</h1>
<p class="tagline">Six ways to test your eye for history's handwriting.</p>
<div class="panel setup" id="setup">
<label>Choose your game</label>
<div class="games" id="gamePick">
<button class="gcard on" data-g="whose"><b>Whose Hand?</b><span class="gd">A mystery signature, four names. The classic.</span></button>
<button class="gcard" data-g="art"><b>Who Signed This?</b><span class="gd">A real museum artwork — which artist's hand made it?</span></button>
<button class="gcard" data-g="match"><b>Match the Hand</b><span class="gd">One signature, four artworks. Pick the piece they signed.</span></button>
<button class="gcard" data-g="early"><b>Early or Late?</b><span class="gd">Two signatures, same person, years apart. Which came first?</span></button>
<button class="gcard" data-g="century"><b>Date the Hand</b><span class="gd">Read the ink — which century did this hand belong to?</span></button>
<button class="gcard" data-g="lightning"><b>⚡ Lightning Round</b><span class="gd">A painting flashes up. Name the ARTIST, then the TITLE — 5 seconds each. Both right = bonus.</span></button>
</div>
<label>Difficulty</label>
<div class="opts" id="diffOpts">
<button class="opt on" data-d="icons">Icons <small>(top 100)</small></button>
<button class="opt" data-d="scholar">Scholar <small>(top 500)</small></button>
<button class="opt" data-d="curator">Curator <small>(top 2,000)</small></button>
<button class="opt" data-d="deep">Deep Cuts <small>(everyone)</small></button>
</div>
<label id="catLabel">Category</label>
<div class="opts" id="catOpts">
<button class="opt on" data-c="All">Everything</button>
<button class="opt" data-c="Artists">Artists</button>
<button class="opt" data-c="Authors">Authors</button>
<button class="opt" data-c="Politics">Politics</button>
<button class="opt" data-c="Declaration of Independence">Declaration</button>
</div>
<button class="go" id="goBtn">Begin</button>
<div class="prep" id="prep" hidden>Hanging the gallery…</div>
</div>
<div class="panel" id="play" hidden>
<div class="hud">
<span>Round <b id="round">1</b> / 10</span>
<span class="streak" id="streak"></span>
<span>Score <b id="score">0</b></span>
</div>
<div class="prompt" id="prompt"></div>
<div class="timerbar" id="timerbar" hidden><div class="tb-fill" id="tbFill"></div></div>
<div class="stage" id="stage"></div>
<div class="choices" id="choices"></div>
<div class="reveal" id="reveal"></div>
</div>
<div class="panel" id="results" hidden>
<div class="score-big" id="finalScore"></div>
<div class="verdict" id="verdict"></div>
<div class="misses" id="misses"></div>
<div class="best" id="best"></div>
<div class="lb" id="lbWrap" hidden>
<div class="lb-head">🏆 Leaderboard <span id="lbTag"></span></div>
<div class="lb-submit" id="lbSubmit">
<input id="lbName" maxlength="24" placeholder="Your name (optional)" aria-label="Name for leaderboard">
<button class="lb-btn" id="lbPost">Post my score</button>
</div>
<ol class="lb-list" id="lbList"></ol>
</div>
<div class="share" id="shareRow">
<button class="sh-btn" id="shCopy">🔗 Copy result</button>
<a class="sh-btn" id="shTweet" target="_blank" rel="noopener noreferrer">𝕏 Share</a>
</div>
<button class="again" id="againBtn">Play again</button>
<div class="ad-zone" data-ad-zone="game"></div>
</div>
</main>
<script>
const $ = s => document.querySelector(s);
const esc = s => String(s==null?'':s).replace(/[&<>"]/g,c=>({'&':'&','<':'<','>':'>','"':'"'}[c]));
const qidOf = r => (r.wikidata||'').split('/').pop();
const notability = r => { const m=(r.reason_for_ranking||'').match(/(\d+) language/); return m?+m[1]:0; };
const shuffled = a => { a=[...a]; for(let i=a.length-1;i>0;i--){ const j=Math.floor(Math.random()*(i+1)); [a[i],a[j]]=[a[j],a[i]]; } return a; };
const DIFF_N = { icons:100, scholar:500, curator:2000, deep:Infinity };
let ALL=[], EVO={}, gameKey='whose', diff='icons', cat='All', game=null;
const ART_CACHE = {};
fetch('/api/signatures').then(r=>r.json()).then(d=>{ ALL = d.filter(r => r.signature_image_url && /wikimedia|upload\./.test(r.signature_image_url)); });
fetch('/api/signature-evolution').then(r=>r.ok?r.json():{}).then(e=>{ EVO=e||{}; }).catch(()=>{});
document.querySelectorAll('#gamePick .gcard').forEach(b=>b.onclick=()=>{ gameKey=b.dataset.g;
document.querySelectorAll('#gamePick .gcard').forEach(x=>x.classList.toggle('on',x===b));
// art games draw from Artists; evolution game draws from the dated timelines
const catRelevant = gameKey==='whose' || gameKey==='century';
$('#catLabel').style.display = catRelevant?'':'none';
$('#catOpts').style.display = catRelevant?'':'none';
});
document.querySelectorAll('#diffOpts .opt').forEach(b=>b.onclick=()=>{ diff=b.dataset.d;
document.querySelectorAll('#diffOpts .opt').forEach(x=>x.classList.toggle('on',x===b)); });
document.querySelectorAll('#catOpts .opt').forEach(b=>b.onclick=()=>{ cat=b.dataset.c;
document.querySelectorAll('#catOpts .opt').forEach(x=>x.classList.toggle('on',x===b)); });
function pool(forcedCat){
const c = forcedCat!==undefined ? forcedCat : ((gameKey==='whose'||gameKey==='century') ? cat : 'Artists');
let p = c==='All' ? ALL : ALL.filter(r=>r.category===c);
return [...p].sort((a,b)=>notability(b)-notability(a)).slice(0, DIFF_N[diff]);
}
// one public-domain artwork per artist via the Art Institute of Chicago (CORS-open, keyless)
async function artworkFor(r){
const qid = qidOf(r);
if (qid in ART_CACHE) return ART_CACHE[qid];
let out = null;
try {
const last = r.full_name.toLowerCase().split(' ').pop();
const j = await (await fetch(`https://api.artic.edu/api/v1/artworks/search?q=${encodeURIComponent(r.full_name)}&query[term][is_public_domain]=true&fields=id,title,image_id,artist_title,date_display&limit=8`)).json();
const hits = (j.data||[]).filter(a=>a.image_id && (a.artist_title||'').toLowerCase().includes(last));
if (hits.length){
const a = hits[Math.floor(Math.random()*Math.min(hits.length,4))];
out = { img:`https://www.artic.edu/iiif/2/${a.image_id}/full/600,/0/default.jpg`, title:a.title||'Untitled', date:a.date_display||'', url:`https://www.artic.edu/artworks/${a.id}` };
}
} catch {}
ART_CACHE[qid] = out;
return out;
}
// ── round builders per game ─────────────────────────────────────────────────
function distractors(answer, p, n=3){
const near = p.filter(r=>r!==answer && r.category===answer.category);
const ranked = [...near].sort((a,b)=>Math.abs(notability(a)-notability(answer)) - Math.abs(notability(b)-notability(answer)));
const ds = shuffled(ranked.slice(0,24)).slice(0,n);
while (ds.length<n){ const x = shuffled(p).find(r=>r!==answer && !ds.includes(r)); if(!x) break; ds.push(x); }
return ds;
}
const centuryOf = r => { const y = parseInt(String(r.death_date).slice(0,4),10); return isNaN(y)?null:Math.ceil(y/100); };
const centuryLabel = c => { const suf = c%10===1&&c!==11?'st':c%10===2&&c!==12?'nd':c%10===3&&c!==13?'rd':'th'; return `${c}${suf} century`; };
async function buildRounds(setPrep){
const p = pool();
const rounds = [];
if (gameKey==='whose'){
if (p.length<8) throw new Error('Not enough signatures in this tier.');
for (const answer of shuffled(p).slice(0,10)) rounds.push({ answer, options: shuffled([answer, ...distractors(answer,p)]) });
}
else if (gameKey==='century'){
const dated = p.filter(r=>centuryOf(r));
if (dated.length<10) throw new Error('Not enough dated signatures in this tier.');
for (const answer of shuffled(dated).slice(0,10)){
const c = centuryOf(answer);
const opts = new Set([c]);
let step=1;
while (opts.size<4){ const cand = c + (Math.random()<.5?-step:step); if (cand>=6 && cand<=21) opts.add(cand); step = Math.min(step+1, 6); }
rounds.push({ answer, century:c, options: shuffled([...opts]) });
}
}
else if (gameKey==='early'){
const people = Object.entries(EVO).map(([qid,e])=>({qid, e, dated:(e.sigs||[]).filter(s=>s.year)}))
.filter(x => { const ys=[...new Set(x.dated.map(s=>s.year))]; return ys.length>=2; });
if (people.length<5) throw new Error('Evolution data still loading — try again in a moment.');
for (let i=0;i<10;i++){
const P = people[Math.floor(Math.random()*people.length)];
const years = shuffled([...new Set(P.dated.map(s=>s.year))]);
const [y1,y2] = [years[0], years[1]].sort((a,b)=>a-b);
const sigA = P.dated.find(s=>s.year===y1), sigB = P.dated.find(s=>s.year===y2);
const flip = Math.random()<.5;
rounds.push({ person:P.e.name, qid:P.qid, left:flip?sigB:sigA, right:flip?sigA:sigB, earlier: flip?'right':'left', years:[y1,y2] });
}
}
else if (gameKey==='lightning'){
// build a pool of real paintings {artist,title,img,url}; ask artist then title
const cands = shuffled(p); let idx=0; const pool=[];
while (pool.length<8 && idx<cands.length){
const batch = cands.slice(idx, idx+6); idx+=6;
setPrep(`Loading the gallery… ${pool.length}/8 paintings ready`);
const arts = await Promise.all(batch.map(artworkFor));
for (let i=0;i<batch.length && pool.length<8;i++){
if (arts[i]) pool.push({ artist: batch[i], title: arts[i].title, img: arts[i].img, url: arts[i].url });
}
}
if (pool.length<5) throw new Error('Not enough museum paintings at this tier — try Icons or Scholar.');
const nameOf = x=>x.artist.full_name, titleOf=x=>x.title;
for (const pt of pool){
const otherA = shuffled(pool.filter(x=>x!==pt && nameOf(x)!==nameOf(pt))).slice(0,3);
const otherT = shuffled(pool.filter(x=>x!==pt && titleOf(x)!==titleOf(pt))).slice(0,3);
if (otherA.length<3 || otherT.length<3) continue;
const pid = pt.url;
rounds.push({ q:'artist', pid, painting:pt, correct:nameOf(pt), options: shuffled([nameOf(pt), ...otherA.map(nameOf)]) });
rounds.push({ q:'title', pid, painting:pt, correct:titleOf(pt), options: shuffled([titleOf(pt), ...otherT.map(titleOf)]) });
}
if (rounds.length<8) throw new Error('Not enough distinct paintings — try Icons or Scholar.');
return rounds; // already paired artist→title per painting
}
else { // art + match need artworks
const cands = shuffled(p);
let idx=0;
while (rounds.length<10 && idx<cands.length){
const batch = cands.slice(idx, idx+6); idx+=6;
setPrep(`Hanging the gallery… ${rounds.length}/10 rooms ready`);
const arts = await Promise.all(batch.map(artworkFor));
for (let i=0;i<batch.length && rounds.length<10;i++){
if (!arts[i]) continue;
const answer = batch[i];
if (gameKey==='art'){
rounds.push({ answer, artwork:arts[i], options: shuffled([answer, ...distractors(answer,p)]) });
} else {
rounds.push({ answer, artwork:arts[i], pendingDistractors:true });
}
}
}
if (gameKey==='match'){
// each round needs 3 distractor ARTWORKS too
for (const r of rounds){
const ds = distractors(r.answer, p, 8);
const arts = [];
for (const d of ds){
if (arts.length>=3) break;
const a = await artworkFor(d);
if (a) arts.push({ owner:d, art:a });
}
if (arts.length<3){ r.invalid = true; continue; }
r.options = shuffled([{owner:r.answer, art:r.artwork}, ...arts.slice(0,3)]);
}
const ok = rounds.filter(r=>!r.invalid);
if (ok.length<6) throw new Error('Not enough museum artworks at this tier — try Icons or Scholar.');
rounds.length = 0; rounds.push(...ok.slice(0,10));
}
if (rounds.length<6) throw new Error('Not enough museum artworks at this tier — try Icons or Scholar.');
}
return rounds;
}
// ── engine ──────────────────────────────────────────────────────────────────
$('#goBtn').onclick = async () => {
if (!ALL.length) return;
$('#goBtn').disabled = true; $('#prep').hidden = false; $('#prep').className = 'prep'; $('#prep').textContent = 'Hanging the gallery…';
try {
const rounds = await buildRounds(t => { $('#prep').className = 'prep'; $('#prep').textContent = t; });
game = { rounds, i:0, score:0, streak:0, misses:[] };
$('#setup').hidden = true; $('#results').hidden = true; $('#play').hidden = false;
// Hide prep BEFORE showRound() — buildRounds already succeeded, so if showRound ever throws we don't
// strand a visible prep line over an empty #play panel (Cody gate, TK-10192).
$('#prep').hidden = true; $('#prep').textContent = 'Hanging the gallery…';
showRound();
} catch(e){
// Inline, non-blocking error (was a native alert() that also modally froze the page and, in the
// 5-pass debug, jammed every subsequent game's Begin — TK-10192). Keep #prep visible with the reason.
$('#prep').className = 'prep err'; $('#prep').textContent = e.message;
}
$('#goBtn').disabled = false;
};
const PROMPTS = {
whose: 'Whose hand wrote this?',
art: 'Who signed this piece of art?',
match: 'Which of these did they sign?',
early: 'Same hand, years apart — which is the EARLIER signature?',
century: 'Which century did this hand belong to?',
lightning: '⚡ Quick!',
};
// 5-second countdown for the Lightning round
let LTIMER=null;
function clearTimer(){ if(LTIMER){ clearTimeout(LTIMER); LTIMER=null; } $('#timerbar').hidden=true; }
function startTimer(onOut){
const bar=$('#timerbar'), fill=$('#tbFill');
bar.hidden=false; fill.className='tb-fill'; fill.style.width='100%';
void fill.offsetWidth; // reflow so the transition runs
requestAnimationFrame(()=>fill.classList.add('run'));
LTIMER=setTimeout(onOut, 5000);
}
function showRound(){
clearTimer();
const r = game.rounds[game.i];
$('#round').textContent = game.i+1;
$('#score').textContent = game.score;
$('#streak').textContent = game.streak>=2 ? `🔥 ${game.streak} streak` : '';
$('#prompt').textContent = PROMPTS[gameKey];
$('#reveal').innerHTML = '';
const stage = $('#stage'), ch = $('#choices');
if (gameKey==='lightning'){
stage.innerHTML = `<span class="tag2">${r.q==='artist'?'Name the ARTIST':'Name the PAINTING'}</span>
<img referrerpolicy="no-referrer" src="${esc(r.painting.img)}" alt="Painting">`;
ch.innerHTML = r.options.map((o,i)=>`<button class="choice" data-i="${i}">${esc(o)}</button>`).join('');
document.querySelectorAll('.choice').forEach(b=>b.onclick=()=>lightningPick(b.dataset.i));
startTimer(()=>lightningPick('timeout'));
return;
}
if (gameKey==='whose' || gameKey==='century'){
stage.innerHTML = `<span class="sig signature-preview" data-signature-src="${esc(r.answer.signature_image_url)}" data-signature-label="Mystery signature" data-signature-tint-key="${esc(r.answer.qid||r.answer.full_name||'')}"></span>`;
ch.innerHTML = (gameKey==='whose' ? r.options.map((o,i)=>`<button class="choice" data-i="${i}">${esc(o.full_name)}</button>`)
: r.options.map((c,i)=>`<button class="choice" data-i="${i}">${centuryLabel(c)}</button>`)).join('');
}
else if (gameKey==='art'){
stage.innerHTML = `<img referrerpolicy="no-referrer" src="${esc(r.artwork.img)}" alt="Mystery artwork">`;
ch.innerHTML = r.options.map((o,i)=>`<button class="choice" data-i="${i}">${esc(o.full_name)}</button>`).join('');
}
else if (gameKey==='match'){
stage.innerHTML = `<span class="sig signature-preview" data-signature-src="${esc(r.answer.signature_image_url)}" data-signature-label="Signature of the artist" data-signature-tint-key="${esc(r.answer.qid||r.answer.full_name||'')}"></span>`;
ch.innerHTML = r.options.map((o,i)=>`<button class="choice art" data-i="${i}"><img referrerpolicy="no-referrer" src="${esc(o.art.img)}" alt="artwork option"><span class="cl"></span></button>`).join('');
}
else if (gameKey==='early'){
stage.innerHTML = `<div class="duo">
<div class="d"><span class="signature-preview" data-signature-src="${esc(r.left.url)}" data-signature-label="signature A" data-signature-tint-key="${esc(r.left.qid||r.left.full_name||r.left.url||'A')}"></span><div class="lab">A</div></div>
<div class="d"><span class="signature-preview" data-signature-src="${esc(r.right.url)}" data-signature-label="signature B" data-signature-tint-key="${esc(r.right.qid||r.right.full_name||r.right.url||'B')}"></span><div class="lab">B</div></div>
</div>`;
ch.innerHTML = `<button class="choice" data-i="left">A is earlier</button><button class="choice" data-i="right">B is earlier</button>`;
$('#prompt').textContent = `${r.person} — which is the EARLIER signature?`;
}
document.querySelectorAll('.choice').forEach(b=>b.onclick=()=>pick(b.dataset.i));
}
function lightningPick(i){
clearTimer();
const r = game.rounds[game.i];
const btns = [...document.querySelectorAll('.choice')];
btns.forEach(b=>b.disabled=true);
const chosen = i==='timeout' ? null : r.options[+i];
const right = chosen === r.correct;
btns.forEach((b,j)=>{ if(r.options[j]===r.correct) b.classList.add('right'); else if(String(j)===String(i)) b.classList.add('wrong'); else b.classList.add('dim'); });
// track per-painting so we can award the both-right bonus at the end
game.lres = game.lres || {}; game.lres[r.pid] = game.lres[r.pid] || {artist:false,title:false,painting:r.painting};
game.lres[r.pid][r.q] = right;
if (right){ game.streak++; game.score += 5 + Math.min(game.streak-1,5); } else { game.streak=0; if(r.q==='artist') game.misses.push(r.painting.artist); }
const timedOut = i==='timeout';
$('#reveal').innerHTML =
`${right?'✓':(timedOut?'⏱ Time!':'✗')} ${r.q==='artist'?'Artist':'Painting'}: <b>${esc(r.correct)}</b>`
+ (r.q==='title' ? ` · <a href="${esc(r.painting.url)}" target="_blank" rel="noopener noreferrer">see it →</a>` : '')
+ `<button class="next" id="nextBtn">${game.i===game.rounds.length-1?'See results':'Next →'}</button>`;
$('#nextBtn').onclick = () => { game.i++; game.i>=game.rounds.length ? finish() : showRound(); };
}
function pick(i){
const r = game.rounds[game.i];
let right=false, revealHtml='';
const btns = [...document.querySelectorAll('.choice')];
btns.forEach(b=>b.disabled=true);
if (gameKey==='whose' || gameKey==='art'){
const chosen = r.options[+i];
right = chosen === r.answer;
btns.forEach((b,j)=>{ if(r.options[j]===r.answer) b.classList.add('right'); else if(j===+i) b.classList.add('wrong'); else b.classList.add('dim'); });
const a=r.answer;
revealHtml = `${right?'✓ Correct —':'✗ It was'} <b>${esc(a.full_name)}</b>`
+ (gameKey==='art'?` · <a href="${esc(r.artwork.url)}" target="_blank" rel="noopener noreferrer">“${esc(r.artwork.title)}”${r.artwork.date?', '+esc(r.artwork.date):''} →</a>`:'')
+ ` · <a href="/?artist=${qidOf(a)}" target="_blank" rel="noopener noreferrer">their page →</a>`;
if (!right) game.misses.push(a);
}
else if (gameKey==='century'){
const chosen = r.options[+i];
right = chosen === r.century;
btns.forEach((b,j)=>{ if(r.options[j]===r.century) b.classList.add('right'); else if(j===+i) b.classList.add('wrong'); else b.classList.add('dim'); });
revealHtml = `${right?'✓ Correct —':'✗'} <b>${esc(r.answer.full_name)}</b> died ${esc(String(r.answer.death_date).slice(0,4))} (${centuryLabel(r.century)}) · <a href="/?artist=${qidOf(r.answer)}" target="_blank" rel="noopener noreferrer">their page →</a>`;
if (!right) game.misses.push(r.answer);
}
else if (gameKey==='match'){
const chosen = r.options[+i];
right = chosen.owner === r.answer;
btns.forEach((b,j)=>{ const o=r.options[j];
if(o.owner===r.answer) b.classList.add('right'); else if(j===+i) b.classList.add('wrong'); else b.classList.add('dim');
b.querySelector('.cl').textContent = `${o.owner.full_name} — ${o.art.title}`;
});
revealHtml = `${right?'✓ Correct —':'✗ Their piece was'} <a href="${esc(r.options.find(o=>o.owner===r.answer).art.url)}" target="_blank" rel="noopener noreferrer">“${esc(r.artwork.title)}” →</a> by <b>${esc(r.answer.full_name)}</b>`;
if (!right) game.misses.push(r.answer);
}
else if (gameKey==='early'){
right = i === r.earlier;
btns.forEach(b=>{ if(b.dataset.i===r.earlier) b.classList.add('right'); else b.classList.add('wrong'); });
revealHtml = `${right?'✓ Correct —':'✗'} ${esc(r.person)}: ${r.earlier==='left'?'A':'B'} is from <b>${r.years[0]}</b>, the other from <b>${r.years[1]}</b> · <a href="/?artist=${r.qid}" target="_blank" rel="noopener noreferrer">see the timeline →</a>`;
if (!right) game.misses.push({ full_name:r.person, wikidata:'https://www.wikidata.org/wiki/'+r.qid });
}
if (right){ game.streak++; game.score += 10 + Math.min(game.streak-1,5)*2; } else game.streak=0;
$('#reveal').innerHTML = revealHtml + `<button class="next" id="nextBtn">${game.i===game.rounds.length-1?'See results':'Next →'}</button>`;
$('#nextBtn').onclick = () => { game.i++; game.i>=game.rounds.length ? finish() : showRound(); };
}
function finish(){
clearTimer();
$('#play').hidden = true; $('#results').hidden = false;
let bonusNote = '';
if (gameKey==='lightning' && game.lres){
const paintings = Object.values(game.lres);
const both = paintings.filter(x=>x.artist && x.title);
game.score += both.length * 5; // both-right bonus per painting
bonusNote = `<div class="ap-sub" style="margin-top:8px">⚡ Named BOTH on ${both.length} of ${paintings.length} paintings — <b>+${both.length*5} bonus</b></div>`;
}
const n = game.rounds.length, rightN = n - game.misses.length, s = game.score;
$('#finalScore').textContent = s;
$('#verdict').textContent =
rightN===n ? 'Master archivist. The auction houses should be calling.' :
rightN>=n*0.8 ? 'A connoisseur’s eye.' :
rightN>=n*0.6 ? 'A promising apprentice.' :
rightN>=n*0.4 ? 'The gallery guide is that way…' :
'Everyone starts somewhere.';
$('#misses').innerHTML = bonusNote + (game.misses.length
? '<b>Worth a second look:</b><br>' + [...new Map(game.misses.map(a=>[qidOf(a),a])).values()].map(a=>`<a href="/?artist=${qidOf(a)}" target="_blank" rel="noopener noreferrer">${esc(a.full_name)}</a>`).join(' · ')
: 'A perfect run.');
const key = `siggames.best.${gameKey}.${diff}.${cat}`;
const best = Math.max(s, +(localStorage.getItem(key)||0));
localStorage.setItem(key, best);
$('#best').textContent = `Best (${gameKey} · ${diff}): ${best}`;
// ── social layer: share + leaderboard ──
const gameName = ({whose:'Whose Hand?',art:'Who Signed This?',match:'Match the Hand',early:'Early or Late?',century:'Date the Hand',lightning:'⚡ Lightning Round'})[gameKey]||'The Signature Games';
const shareText = `I scored ${s} on ${gameName} (${diff}) at The Signature Games — can you beat me?`;
const shareUrl = 'https://celebsignatures.com/game';
$('#shTweet').href = `https://twitter.com/intent/tweet?text=${encodeURIComponent(shareText)}&url=${encodeURIComponent(shareUrl)}`;
$('#shCopy').onclick = async () => {
try { await navigator.clipboard.writeText(shareText+' '+shareUrl); $('#shCopy').textContent='✓ Copied'; setTimeout(()=>$('#shCopy').textContent='🔗 Copy result',1500); }
catch { $('#shCopy').textContent='—'; }
};
// leaderboard
const lbKey = { game: gameKey, diff };
$('#lbTag').textContent = `· ${gameName} · ${diff}`;
$('#lbWrap').hidden = false;
$('#lbSubmit').style.display = '';
$('#lbName').value = localStorage.getItem('siggames.name') || '';
renderLeaderboard(null);
fetch(`/api/leaderboard?game=${gameKey}&diff=${diff}`).then(r=>r.json()).then(d=>renderLeaderboard(d.top||[]));
$('#lbPost').disabled = false;
$('#lbPost').onclick = async () => {
$('#lbPost').disabled = true;
const name = ($('#lbName').value||'Anonymous').slice(0,24);
localStorage.setItem('siggames.name', name);
try {
const d = await (await fetch('/api/leaderboard',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({...lbKey, score:s, name})})).json();
renderLeaderboard(d.top||[], name, s, d.rank);
$('#lbSubmit').style.display='none';
} catch { $('#lbPost').disabled=false; }
};
}
function renderLeaderboard(top, meName, meScore, meRank){
const el=$('#lbList');
if (top===null){ el.innerHTML='<li style="border:none;color:#999">loading…</li>'; return; }
if (!top.length){ el.innerHTML='<li style="border:none;color:#999">Be the first — post your score!</li>'; return; }
el.innerHTML = top.map((e,i)=>{
const mine = meName!=null && e.name===meName && e.score===meScore && (i+1)===meRank;
return `<li class="${mine?'me':''}"><span class="nm">${esc(e.name)}</span><span class="sc">${e.score}</span></li>`;
}).join('');
if (meRank && meRank>20) el.innerHTML += `<li class="me" style="border-top:1px dashed var(--line)"><span class="nm">${esc(meName)} (you)</span><span class="sc">${meScore} · #${meRank}</span></li>`;
}
$('#againBtn').onclick = () => { $('#results').hidden = true; $('#setup').hidden = false; $('#lbWrap').hidden = true; };
</script>
</body>
</html>