File size: 18,583 Bytes
b451ad4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8"/>
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Vectorizing Words Jeopardy</title>
  <style>
    :root{
      --blue:#0a49a6;
      --blue-dark:#073a84;
      --gold:#ffd24a;
      --board-gap:10px;
      --card-depth:28px;
    }
    *{box-sizing:border-box}
    body{
      font-family: system-ui, Arial, sans-serif;
      display:flex;flex-direction:column;align-items:center;
      background: radial-gradient(1200px 700px at 50% -200px, #f2f6ff 0%, #e9efff 30%, #cfdafc 100%) fixed;
      margin:0; min-height:100vh;
    }
    h1{ color:#173e8c; margin:22px 0 6px; text-shadow:0 2px 0 #fff;}
    a.source{ font-size:14px; color:#334; margin-bottom:8px; text-decoration:none}
    a.source:hover{ text-decoration:underline }
    .topbar {
      width:min(1100px,95vw);
      display:flex;
      justify-content:space-between;
      align-items:center;
      gap:10px;
      margin-bottom:8px;
    }
    .btn-reset{
      padding:8px 12px; border-radius:10px; font-weight:800; cursor:pointer;
      border:2px solid #0d3a85; color:#fff; background:linear-gradient(180deg,#1362ff,#0d3a85);
      box-shadow:0 8px 18px rgba(0,0,0,.25);
    }
    .btn-reset:hover{ transform: translateY(-1px) }
    .btn-reset:active{ transform: translateY(0) }
    /* Stage / Board */
    .stage{ perspective: 1200px; width:min(1100px, 95vw); }
    #game-board{
      display:grid;
      grid-template-columns: repeat(5, 1fr);
      grid-auto-rows: 120px;
      gap: var(--board-gap);
      padding: var(--board-gap);
      border-radius:18px;
      background: linear-gradient(180deg, #0e1a3a, #01060f);
      box-shadow:
        0 20px 45px rgba(0,0,0,.35),
        inset 0 0 0 4px #000;
      transform: rotateX(8deg);
      transform-origin: top center;
    }
    .category, .card{
      position:relative;
      border-radius:14px;
      user-select:none;
      transform-style: preserve-3d;
      transition: transform .2s ease, box-shadow .2s ease, filter .2s ease;
      box-shadow:
        0 var(--card-depth) calc(var(--card-depth) + 12px) rgba(0,0,0,.35),
        inset 0 0 0 1px rgba(255,255,255,.15);
    }
    .category{
      background: linear-gradient(180deg, #004fbf 0%, #003d93 60%, #003684 100%);
      color:#fff; font-weight:800; text-transform:uppercase; letter-spacing:.3px;
      display:flex; justify-content:center; align-items:center; text-align:center;
      border:1px solid #00122c;
      text-shadow:0 2px 0 rgba(0,0,0,.35);
    }
    .card{
      cursor:pointer; font-weight:900; color: var(--gold);
      background:
        radial-gradient(80% 80% at 50% 25%, rgba(255,255,255,.18), rgba(255,255,255,0) 60%),
        linear-gradient(180deg, var(--blue) 0%, var(--blue-dark) 100%);
      border:1px solid #031b3f;
      display:flex; justify-content:center; align-items:center; text-align:center;
      font-size: clamp(16px, 2.4vw, 26px);
      text-shadow: 0 2px 0 #000, 0 0 10px rgba(255,210,74,.35);
    }
    .card.tilt:hover{
      transform: translateZ(10px) rotateX(var(--rx,0deg)) rotateY(var(--ry,0deg));
      box-shadow:
        0 24px 48px rgba(0,0,0,.4),
        0 0 24px rgba(255,210,74,.15),
        inset 0 0 0 1px rgba(255,255,255,.18);
      filter: saturate(1.1);
    }
    .card:active{ transform: translateZ(0) scale(.985); box-shadow: 0 12px 18px rgba(0,0,0,.45), inset 0 0 0 1px rgba(255,255,255,.12); }
    .card.disabled{ cursor:default; color:#bfc7da; background: linear-gradient(180deg,#6b7aa6,#4e5b85); text-shadow:none; filter:grayscale(.2) brightness(.92); }
    #question-display{ width:min(1000px, 92vw); text-align:center; margin:16px 0 6px; }
    #question-display h2{ margin:8px 0 6px; color:#0d2b6f }
    #question-display p { font-size: 22px; line-height: 1.4; }
    #score{ font-size:24px; font-weight:800; color:#0d2b6f; margin:0 0 8px 0 }
    .answer-container{ display:flex; justify-content:center; flex-wrap:wrap; gap:10px; margin-top:14px }
    .answer-btn{
      margin:0; padding:10px 16px; font-size:18px; cursor:pointer;
      border:2px solid #0d3a85; border-radius:10px; font-weight:800; color:#fff;
      background: linear-gradient(180deg,#1362ff,#0d3a85);
      box-shadow: 0 8px 18px rgba(0,0,0,.25);
      transition: transform .12s ease, box-shadow .12s ease, filter .12s ease;
    }
    .answer-btn:hover{ transform: translateY(-2px); box-shadow:0 12px 26px rgba(0,0,0,.28) }
    .answer-btn:active{ transform: translateY(0); box-shadow:0 8px 18px rgba(0,0,0,.25) }
    .answer-btn.disabled{ background:#aab4cc; color:#445; cursor:not-allowed; border-color:#889 }
    .feedback{ margin-top:10px; font-size:18px; font-weight:800 }
    .dd-overlay{
      position: fixed; inset:0; display:none; align-items:center; justify-content:center;
      z-index: 9999; pointer-events:none;
      background: radial-gradient(60% 60% at 50% 50%, rgba(255,255,255,.08), rgba(0,0,0,.7));
      animation: dd-bg 1.2s ease-in-out 2;
    }
    .dd-text{
      font-size: clamp(40px, 7vw, 120px);
      font-weight: 900; color:#ffe17a; letter-spacing:2px;
      text-shadow:
        0 0 12px rgba(255,225,122,.9),
        0 0 40px rgba(255,225,122,.6),
        0 6px 0 #000;
      animation: flash 1.2s steps(2, jump-none) 2, wobble .9s ease-in-out 2;
    }
    @keyframes flash{ 0%,49%{opacity:1;filter:drop-shadow(0 0 24px rgba(255,225,122,.85));} 50%,100%{opacity:0;filter:none;} }
    @keyframes wobble{ 0%{transform:scale(1) rotate(0)} 50%{transform:scale(1.06) rotate(-2deg)} 100%{transform:scale(1) rotate(0)} }
    @keyframes dd-bg{ 0%{background: radial-gradient(40% 40% at 50% 50%, rgba(255,255,255,.12), rgba(0,0,0,.9));} 100%{background: radial-gradient(60% 60% at 50% 50%, rgba(255,255,255,.08), rgba(0,0,0,.7));} }
    .flash-ring{ position:absolute; inset:-3px; border-radius:14px; pointer-events:none; box-shadow:0 0 0 3px rgba(255,225,122,.95), 0 0 30px 6px rgba(255,225,122,.6); animation: ring 1.2s ease-in-out 2; }
    @keyframes ring{ 0%{opacity:1;transform:scale(1)} 50%{opacity:.1;transform:scale(.96)} 100%{opacity:1;transform:scale(1)} }
    .daily-double-banner{ color:#b30000; font-weight:900; font-size:22px; margin:8px 0 }
    /* Review */
    #review{
      width:min(1000px, 92vw);
      margin:14px 0 24px 0;
      padding:12px;
      background:#fff;
      border:1px solid #ccd3e0;
      border-radius:12px;
      box-shadow:0 6px 20px rgba(0,0,0,.08);
    }
    #review h3{ margin:0 0 8px 0; color:#0d2b6f }
    .missed{ margin:8px 0; text-align:left }
    .missed .q{ font-weight:700 }
    .missed .a{ margin-left:8px }
  </style>
</head>
<body>
  <h1>Vectorizing Words Jeopardy</h1>
  <a class="source" href="https://www.linkedin.com/pulse/turning-text-numbers-word2vec-glove-fasttext-michael-lively-vtwde/" target="_blank">
    Source: Turning Text Into Numbers — Word2Vec, GloVe, FastText
  </a>

  <div class="topbar">
    <div id="score">Score: 0</div>
    <button class="btn-reset" id="reset-btn" title="Start a fresh round">Reset Game</button>
  </div>

  <div class="stage">
    <div id="game-board">
      <div class="category">Embedding Basics</div>
      <div class="category">Word2Vec: CBOW & Skip-gram</div>
      <div class="category">GloVe & Global Stats</div>
      <div class="category">FastText & Subwords</div>
      <div class="category">Training & Tools</div>
      <!-- cards will be injected -->
    </div>
  </div>

  <div id="question-display"></div>
  <div id="review" style="display:none;"></div>

  <audio id="dd-audio" preload="auto">
    <source src="daily-double.mp3" type="audio/mpeg">
  </audio>
  <div id="dd-overlay" class="dd-overlay"><div class="dd-text">DAILY&nbsp;DOUBLE!</div></div>

  <script>
    const categories = [
      "Embedding Basics",
      "Word2Vec: CBOW & Skip-gram",
      "GloVe & Global Stats",
      "FastText & Subwords",
      "Training & Tools"
    ];
    // ✅ Corrected answer keys included
    const questions = [
      // Embedding Basics
      [
        { q: "Compared to Bag of Words/TF-IDF, word embeddings primarily:", a: ["Capture meaning via context, not just frequency", "Apply hand-written linguistic rules", "Ignore context entirely"], correct: 0 },
        { q: "Which metric measures similarity by the angle between vectors?", a: ["Euclidean distance", "Cosine similarity", "Jaccard index"], correct: 1 },
        { q: "A common way to visualize high-dimensional embeddings in 2D is:", a: ["Gradient Descent", "Backpropagation", "PCA or t-SNE"], correct: 2 }
      ],
      // Word2Vec: CBOW & Skip-gram
      [
        { q: "CBOW’s training objective is to:", a: ["Predict the target word from surrounding context words", "Predict the context from the target word", "Factorize the co-occurrence matrix"], correct: 0 },
        { q: "Skip-gram’s training objective is to:", a: ["Count word frequencies", "Predict surrounding context words from a target word", "Remove stopwords before training"], correct: 1 },
        { q: "Which techniques speed up Word2Vec training?", a: ["Bag-of-Words and TF-IDF", "Count vectors and hashing", "Negative sampling and/or hierarchical softmax"], correct: 2 }
      ],
      // GloVe & Global Stats
      [
        { q: "GloVe learns embeddings mainly by:", a: ["Matrix factorization of a word co-occurrence matrix", "Predicting characters from bytes", "Labeling sentences with topics"], correct: 0 },
        { q: "In GloVe, a weighting function is used to:", a: ["Increase the window size automatically", "Balance frequent and rare words so common terms don't dominate", "Normalize vectors to unit length"], correct: 1 },
        { q: "Compared with Word2Vec, GloVe is especially strong at capturing:", a: ["Local syntactic patterns only", "Character-level morphology", "Global relationships across the entire corpus"], correct: 2 }
      ],
      // FastText & Subwords
      [
        { q: "FastText represents a word as:", a: ["A single one-hot vector", "Character n-grams (subword units) combined", "Only its stem or lemma"], correct: 1 }, // fixed
        { q: "A key advantage of FastText over Word2Vec is that it:", a: ["Requires no training data", "Handles out-of-vocabulary words via subwords", "Eliminates the need for context windows"], correct: 1 },
        { q: "FastText training generally uses the same framework as Word2Vec:", a: ["No, it uses topic modeling only", "Partly, but only with CBOW", "Yes, CBOW and/or Skip-gram with subwords"], correct: 2 }
      ],
      // Training & Tools
      [
        { q: "Embedding quality generally improves with:", a: ["Smaller, single-topic corpora", "Random word shuffling", "Larger and more diverse corpora and well-chosen window sizes"], correct: 2 }, // fixed
        { q: "Which library is optimized for production-grade NLP with pretrained vectors?", a: ["NLTK", "spaCy", "A spreadsheet"], correct: 1 },
        { q: "Which statement about classic embeddings is most accurate?", a: ["They are rule-based representations", "They fully model document-level structure like Transformers", "They learn from context but don’t capture full sequence structure; Transformers handle that"], correct: 2 }
      ]
    ];
    let score = 0, answered = 0;
    const total = 15;
    const board = document.getElementById("game-board"),
          qdisp = document.getElementById("question-display"),
          sdisp = document.getElementById("score"),
          review = document.getElementById("review");
    const ddOverlay = document.getElementById("dd-overlay");
    const ddAudio = document.getElementById("dd-audio");
    // Track where the correct answer ended up after shuffling, per "c-r" key
    const shuffleRegistry = new Map();
    const missedQuestions = [];
    const dailyDouble = { col: Math.floor(Math.random() * 5), row: Math.floor(Math.random() * 3) };
    document.getElementById('reset-btn').addEventListener('click', () => {
      // simple reset: reload page to fresh state
      location.reload();
    });
    function createBoard() {
      // inject 15 cards (3 rows x 5 columns)
      for (let row = 0; row < 3; row++) {
        for (let col = 0; col < 5; col++) {
          const card = document.createElement("div");
          card.className = "card tilt";
          card.textContent = `$${(row + 1) * 100}`;
          card.dataset.col = col;
          card.dataset.row = row;
          // tilt effect
          card.addEventListener("mousemove", (e) => {
            const r = card.getBoundingClientRect();
            const x = (e.clientX - r.left) / r.width;
            const y = (e.clientY - r.top) / r.height;
            const ry = (x - 0.5) * 10;
            const rx = (0.5 - y) * 10;
            card.style.setProperty("--ry", `${ry}deg`);
            card.style.setProperty("--rx", `${rx}deg`);
          });
          card.addEventListener("mouseleave", () => {
            card.style.removeProperty("--ry");
            card.style.removeProperty("--rx");
          });
          card.onclick = () => handleCardClick(card);
          board.appendChild(card);
        }
      }
    }
    function handleCardClick(card){
      if (card.classList.contains("disabled")) return;
      const col = +card.dataset.col;
      const row = +card.dataset.row;
      const isDD = (col === dailyDouble.col && row === dailyDouble.row);
      if (isDD){
        triggerDailyDouble(card, () => showQuestion(col, row, card, true));
      } else {
        showQuestion(col, row, card, false);
      }
    }
    function triggerDailyDouble(card, onDone){
      const ring = document.createElement("div");
      ring.className = "flash-ring";
      card.appendChild(ring);
      ddAudio.currentTime = 0;
      ddAudio.play().catch(()=>{});
      ddOverlay.style.display = "flex";
      setTimeout(() => {
        ddOverlay.style.display = "none";
        ring.remove();
        onDone();
      }, 2400);
    }
    function showQuestion(categoryIndex, difficulty, card, isDailyDouble){
      const q = questions[categoryIndex][difficulty];
      // Build shuffled options and record where the correct one ended up
      const options = q.a.map((text, origIdx) => ({ text, origIdx }));
      for (let i = options.length - 1; i > 0; i--) {
        const j = Math.floor(Math.random() * (i + 1));
        [options[i], options[j]] = [options[j], options[i]];
      }
      const shuffledCorrectIndex = options.findIndex(o => o.origIdx === q.correct);
      const key = `${categoryIndex}-${difficulty}`;
      shuffleRegistry.set(key, { shuffledCorrectIndex, isDailyDouble });
      const dailyDoubleBanner = isDailyDouble ? `<div class="daily-double-banner">🎉 DAILY DOUBLE! 🎉</div>` : "";
      qdisp.innerHTML = `
        ${dailyDoubleBanner}
        <h2>${categories[categoryIndex]} for $${(difficulty + 1) * 100}${isDailyDouble ? " (x2)" : ""}</h2>
        <p>${q.q}</p>
        <div class="answer-container">
          ${options.map((opt, i) =>
            `<button class="answer-btn" data-ans="${i}" data-key="${key}">${opt.text}</button>`
          ).join("")}
        </div>
      `;
      // disable the card so it can't be opened again
      card.classList.add("disabled");
      // attach click handlers to answers (no inline booleans)
      document.querySelectorAll('.answer-btn').forEach(btn => {
        btn.addEventListener('click', () => {
          if (btn.classList.contains('disabled')) return;
          const chosenIndex = parseInt(btn.getAttribute('data-ans'), 10);
          const k = btn.getAttribute('data-key');
          checkAnswer(categoryIndex, difficulty, chosenIndex, k);
        }, { once: true });
      });
    }
    function checkAnswer(cat, diff, chosenShuffledIndex, key){
      const q = questions[cat][diff];
      const reg = shuffleRegistry.get(key);
      const isCorrect = (chosenShuffledIndex === reg.shuffledCorrectIndex);
      let val = (diff + 1) * 100;
      if (reg.isDailyDouble) val *= 2;
      // lock buttons (prevent multiple scoring)
      document.querySelectorAll(".answer-btn").forEach(b => {
        b.disabled = true; b.classList.add("disabled");
      });
      if (isCorrect) {
        score += val;
        qdisp.innerHTML += `<p class="feedback" style="color:green;">✅ Correct! +$${val.toLocaleString()}</p>`;
      } else {
        score -= val;
        const correctText = q.a[q.correct];
        const chosenText = q.a[
          // translate shuffled index back to original by comparing text (safe here; could also carry mapping)
          q.a.findIndex((orig, idx) => {
            // find the text for chosenShuffledIndex by looking up the rendered button text
            // simpler: we recorded only shuffled index; to show user's text, just read from DOM:
            return false; // placeholder, we'll get from DOM below
          })
        ];
        // Grab the chosen text from the DOM directly
        const chosenBtn = document.querySelector(`.answer-btn[data-ans="${chosenShuffledIndex}"][data-key="${key}"]`);
        const chosenPretty = chosenBtn ? chosenBtn.textContent : "(your choice)";
        qdisp.innerHTML += `<p class="feedback" style="color:#b00020;">❌ Wrong! −$${val.toLocaleString()}<br/>Correct answer: <em>${correctText}</em></p>`;
        // record for review
        missedQuestions.push({
          category: categories[cat],
          value: `$${(diff+1)*100}${reg.isDailyDouble ? " (x2)" : ""}`,
          question: q.q,
          yourAnswer: chosenPretty,
          correctAnswer: correctText
        });
      }
      sdisp.textContent = `Score: ${score}`;
      answered++; // increment only after answering
      if (answered === total) {
        endGame();
      }
    }
    function endGame(){
      qdisp.innerHTML += `<h2 style="margin-top:12px">Game Over!</h2><p>Your final score: $${score.toLocaleString()}</p>`;
      if (missedQuestions.length){
        const items = missedQuestions.map(m =>
          `<div class="missed">
             <div class="q">(${m.category}${m.value}) ${m.question}</div>
             <div class="a">Your answer: <em>${m.yourAnswer}</em></div>
             <div class="a">Correct: <strong>${m.correctAnswer}</strong></div>
           </div>`
        ).join("");
        review.style.display = "block";
        review.innerHTML = `<h3>Review: Questions to Revisit (${missedQuestions.length})</h3>${items}`;
      } else {
        review.style.display = "block";
        review.innerHTML = `<h3>Perfect Round!</h3><p>You answered all questions correctly 🎉</p>`;
      }
    }
    // Build the board
    createBoard();
  </script>
</body>
</html>