← back to CelebritySignatures
The Signature Games: 5-game arcade at /game — Whose Hand? (sig->name), Who Signed This? (real AIC museum artwork->artist, Steve's ask), Match the Hand (sig->pick their artwork of 4), Early or Late? (two dated sigs from one person's evolution timeline), Date the Hand (sig->century); shared engine, streak scoring, per-game best scores, artwork gallery prefetch with progress (TK-10181)
133e8ee668d96ebf88ee8ee10ddd3141da539ff7 · 2026-08-03 12:57:11 -0700 · Steve Abrams
Files touched
Diff
commit 133e8ee668d96ebf88ee8ee10ddd3141da539ff7
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Mon Aug 3 12:57:11 2026 -0700
The Signature Games: 5-game arcade at /game — Whose Hand? (sig->name), Who Signed This? (real AIC museum artwork->artist, Steve's ask), Match the Hand (sig->pick their artwork of 4), Early or Late? (two dated sigs from one person's evolution timeline), Date the Hand (sig->century); shared engine, streak scoring, per-game best scores, artwork gallery prefetch with progress (TK-10181)
---
public/game.html | 314 +++++++++++++++++++++++++++++++++++++++++++------------
1 file changed, 249 insertions(+), 65 deletions(-)
diff --git a/public/game.html b/public/game.html
index c8fd84e..69f62ec 100644
--- a/public/game.html
+++ b/public/game.html
@@ -3,7 +3,7 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
-<title>Whose Hand? — the signature identification game</title>
+<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>
@@ -15,23 +15,37 @@
.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:680px; margin:0 auto; padding:30px 20px 80px; }
+ 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); }
- /* setup */
- .setup label { display:block; font-size:12px; text-transform:uppercase; letter-spacing:.06em; color:var(--muted); margin:16px 0 7px; }
+ /* 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; }
- .sigbox { background:#fff; border:1px solid var(--line); border-radius:12px; height:170px; display:flex; align-items:center; justify-content:center; padding:20px; margin-bottom:18px; }
- .sigbox img { max-width:100%; max-height:100%; object-fit:contain; }
+ .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; }
+ .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); }
@@ -39,9 +53,13 @@
.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; }
/* 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; }
@@ -49,7 +67,7 @@
.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; }
- @media (max-width:520px){ .choices{grid-template-columns:1fr} }
+ @media (max-width:560px){ .choices{grid-template-columns:1fr} .games{grid-template-columns:1fr} }
</style>
</head>
<body>
@@ -63,18 +81,26 @@
</nav>
</div>
<main>
- <h1>Whose Hand?</h1>
- <p class="tagline">Ten signatures. Four names each. How well do you know history's handwriting?</p>
+ <h1>The Signature Games</h1>
+ <p class="tagline">Five 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>
+ </div>
<label>Difficulty</label>
<div class="opts" id="diffOpts">
- <button class="opt on" data-d="icons">Icons <small>(the 100 most famous)</small></button>
+ <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>Category</label>
+ <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>
@@ -83,6 +109,7 @@
<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>
@@ -91,7 +118,8 @@
<span class="streak" id="streak"></span>
<span>Score <b id="score">0</b></span>
</div>
- <div class="sigbox"><img id="sigImg" alt="Mystery signature"></div>
+ <div class="prompt" id="prompt"></div>
+ <div class="stage" id="stage"></div>
<div class="choices" id="choices"></div>
<div class="reveal" id="reveal"></div>
</div>
@@ -106,40 +134,153 @@
</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; };
-let ALL=[], POOL=[], game=null;
+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 diff='icons', cat='All';
+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/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 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; }
+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 { // 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;
+ try {
+ const rounds = await buildRounds(t => $('#prep').textContent = t);
+ game = { rounds, i:0, score:0, streak:0, misses:[] };
+ $('#setup').hidden = true; $('#results').hidden = true; $('#play').hidden = false;
+ showRound();
+ } catch(e){ alert(e.message); }
+ $('#goBtn').disabled = false; $('#prep').hidden = true; $('#prep').textContent = 'Hanging the gallery…';
+};
-$('#goBtn').onclick = () => {
- let pool = cat==='All' ? ALL : ALL.filter(r=>r.category===cat);
- pool = [...pool].sort((a,b)=>notability(b)-notability(a)).slice(0, DIFF_N[diff]);
- if (pool.length < 8) { alert('Not enough signatures in this tier — try a broader setting.'); return; }
- POOL = pool;
- game = { rounds: [], i: 0, score: 0, streak: 0, misses: [] };
- const picks = shuffled(pool).slice(0,10);
- game.rounds = picks.map(answer => {
- // distractors: same category, adjacent fame (fair but hard)
- const near = pool.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,3);
- while (ds.length<3) { const x = shuffled(pool).find(r=>r!==answer && !ds.includes(r)); if(!x) break; ds.push(x); }
- return { answer, options: shuffled([answer, ...ds]) };
- });
- $('#setup').hidden = true; $('#results').hidden = true; $('#play').hidden = false;
- showRound();
+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?',
};
function showRound(){
@@ -147,50 +288,93 @@ function showRound(){
$('#round').textContent = game.i+1;
$('#score').textContent = game.score;
$('#streak').textContent = game.streak>=2 ? `🔥 ${game.streak} streak` : '';
- $('#sigImg').src = r.answer.signature_image_url;
+ $('#prompt').textContent = PROMPTS[gameKey];
$('#reveal').innerHTML = '';
- $('#choices').innerHTML = r.options.map((o,i)=>`<button class="choice" data-i="${i}">${o.full_name}</button>`).join('');
- document.querySelectorAll('.choice').forEach(b=>b.onclick=()=>pick(+b.dataset.i));
+ const stage = $('#stage'), ch = $('#choices');
+ if (gameKey==='whose' || gameKey==='century'){
+ stage.innerHTML = `<img class="sig" src="${esc(r.answer.signature_image_url)}" alt="Mystery signature">`;
+ 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 = `<img class="sig" src="${esc(r.answer.signature_image_url)}" alt="Signature of the artist">`;
+ 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"><img referrerpolicy="no-referrer" src="${esc(r.left.url)}" alt="signature A"><div class="lab">A</div></div>
+ <div class="d"><img referrerpolicy="no-referrer" src="${esc(r.right.url)}" alt="signature B"><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 pick(i){
const r = game.rounds[game.i];
- const chosen = r.options[i];
- const right = chosen === r.answer;
- document.querySelectorAll('.choice').forEach((b,j)=>{
- b.disabled = true;
- if (r.options[j]===r.answer) b.classList.add('right');
- else if (j===i) b.classList.add('wrong');
- else b.classList.add('dim');
- });
- if (right) { game.streak++; game.score += 10 + Math.min(game.streak-1,5)*2; }
- else { game.streak = 0; game.misses.push(r.answer); }
- const a = r.answer;
- $('#reveal').innerHTML = `${right?'✓ Correct —':'✗ It was'} <b>${a.full_name}</b>`
- + ` (${a.category}${a.death_date?', d. '+String(a.death_date).slice(0,4):''})`
- + ` · <a href="/?artist=${qidOf(a)}" target="_blank" rel="noopener noreferrer">see their page →</a>`
- + `<button class="next" id="nextBtn">${game.i===9?'See results':'Next signature →'}</button>`;
- $('#nextBtn').onclick = () => { game.i++; game.i>9 ? finish() : showRound(); };
+ 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(){
$('#play').hidden = true; $('#results').hidden = false;
- const s = game.score;
+ const n = game.rounds.length, rightN = n - game.misses.length, s = game.score;
$('#finalScore').textContent = s;
- const right = 10 - game.misses.length;
$('#verdict').textContent =
- right===10 ? 'Master archivist. The auction houses should be calling.' :
- right>=8 ? 'A connoisseur’s eye.' :
- right>=6 ? 'A promising apprentice.' :
- right>=4 ? 'The gallery guide is that way…' :
- 'Everyone starts somewhere.';
+ 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 = game.misses.length
- ? '<b>Worth a second look:</b><br>' + game.misses.map(a=>`<a href="/?artist=${qidOf(a)}" target="_blank" rel="noopener noreferrer">${a.full_name}</a>`).join(' · ')
- : 'A perfect run — every hand identified.';
- const key = 'whosehand.best.'+diff+'.'+cat;
+ ? '<b>Worth a second look:</b><br>' + game.misses.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 score (${diff} · ${cat}): ${best}`;
+ $('#best').textContent = `Best (${gameKey} · ${diff}): ${best}`;
}
$('#againBtn').onclick = () => { $('#results').hidden = true; $('#setup').hidden = false; };
</script>
← dbeb105 pre-deploy: strip temp Figma capture scripts; Authors collag
·
back to CelebritySignatures
·
mobile pass: responsive masthead (wrap + scrollable nav), 2- 0faa6c2 →