← 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=>({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[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>