← 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 →