[object Object]

← back to CelebritySignatures

⚡ Lightning Round (6th game): a real museum painting flashes up, 5-SECOND timer to name the ARTIST then the TITLE (auto-fails on timeout, no time to look it up), both-right bonus per painting (+5), fame-adjacent distractors from the same painting pool; timer bar drains green->red (TK-10181)

92e20cbbf73d59c30a6d62e2f4f9a92714e6b5d6 · 2026-08-03 13:58:08 -0700 · Steve Abrams

Files touched

Diff

commit 92e20cbbf73d59c30a6d62e2f4f9a92714e6b5d6
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon Aug 3 13:58:08 2026 -0700

    ⚡ Lightning Round (6th game): a real museum painting flashes up, 5-SECOND timer to name the ARTIST then the TITLE (auto-fails on timeout, no time to look it up), both-right bonus per painting (+5), fame-adjacent distractors from the same painting pool; timer bar drains green->red (TK-10181)
---
 public/game.html | 86 ++++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 83 insertions(+), 3 deletions(-)

diff --git a/public/game.html b/public/game.html
index 66d794c..d6fddab 100644
--- a/public/game.html
+++ b/public/game.html
@@ -67,6 +67,12 @@
   .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; }
   @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; }
@@ -135,6 +141,7 @@
       <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>
@@ -239,6 +246,30 @@ async function buildRounds(setPrep){
       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;
@@ -297,9 +328,22 @@ const PROMPTS = {
   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;
@@ -307,6 +351,14 @@ function showRound(){
   $('#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 = `<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>`)
@@ -331,6 +383,26 @@ function showRound(){
   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='';
@@ -375,7 +447,15 @@ function pick(i){
 }
 
 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 =
@@ -384,9 +464,9 @@ function finish(){
     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">${esc(a.full_name)}</a>`).join(' · ')
-    : 'A perfect run.';
+  $('#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);

← 1762123 auto-save: 2026-08-03T13:54:39 (1 files) — public/game.html  ·  back to CelebritySignatures  ·  auto-save: 2026-08-03T14:24:54 (6 files) — data/portraits.js 4562ef6 →