# STORYLINE WEB OBJECT CONVERSION PROMPT

**Purpose**
Convert a React/Vite Tetris training game into a **Storyline-compatible portable web object** that:

- Loads questions from an external CSV file
- Uses only relative paths (works when dropped into Storyline)
- Produces a self-contained `dist-storyline/` folder
- Requires **no rebuild** to update questions

---

## CRITICAL RULES

1. **All questions must be loaded from `./questions.csv` via fetch**
2. **`correct_answer` is a LETTER (A, B, C, or D):**
   - `A` = option_1 is correct
   - `B` = option_2 is correct
   - `C` = option_3 is correct
   - `D` = option_4 is correct
   - Use a letter, not a number. This avoids any 0-based vs 1-based confusion.
3. **Every file reference must use `./` relative paths**
4. **The Question interface must match EXACTLY as specified below**
5. **All function signatures must match the existing game code**
6. **CSV must have exactly 8 columns** - see STEP 7 for detailed format
7. The build output must go in `dist-storyline/` and include:
   - `index.html` (all code inlined as single file)
   - `questions.csv` (external, editable data file)

---

## FINAL OUTPUT STRUCTURE

```
dist-storyline/
├── index.html       <- Drop this into Storyline as web object
└── questions.csv    <- Edit this to change questions (no rebuild needed)
```

---

## STEP -1: Detect Project Type (DO THIS BEFORE ANY OTHER STEP)

**Purpose:** STEP 0 is a destructive repair pass tuned for Figma Make exports — it deletes and rewrites `package.json`, `vite.config.ts`, and `postcss.config.mjs`. Running it against a clean GitHub-style repo would clobber working config. This step decides whether STEP 0 is needed.

### Detection — run these checks

A project is a **Figma Make export** if **any one** of these is true:

1. A `src/app/` directory exists
2. `package.json` contains a top-level `"pnpm"` field
3. `package.json` contains a `"peerDependencies"` block that lists `react` or `react-dom`
4. `package.json` contains `"fs"`, `"path"`, or `"url"` as dependency entries
5. `package.json` contains any duplicated dependency in the form `"<name>@<version>": "npm:<name>@<version>"` (Figma Make's pnpm-style override pattern)
6. An empty `postcss.config.mjs` exists at the project root
7. `vite.config.ts` references `figmaAssetResolver`

Quick bash one-liner that returns `FIGMA_MAKE` or `GITHUB_STYLE`:

```bash
if [ -d "src/app" ] \
  || grep -q '"pnpm"' package.json 2>/dev/null \
  || grep -qE '"(react|react-dom)":\s*"[^"]+"' <(sed -n '/peerDependencies/,/}/p' package.json 2>/dev/null) \
  || grep -qE '"(fs|path|url)":' package.json 2>/dev/null \
  || grep -qE '"[a-z@/.-]+@[0-9][^"]*":\s*"npm:' package.json 2>/dev/null \
  || ([ -f "postcss.config.mjs" ] && [ ! -s "postcss.config.mjs" ]) \
  || grep -q "figmaAssetResolver" vite.config.ts 2>/dev/null
then
  echo "FIGMA_MAKE — run STEP 0 before STEP 1"
else
  echo "GITHUB_STYLE — skip STEP 0, go straight to STEP 1"
fi
```

### Decision

- **If detection returned `FIGMA_MAKE`:** proceed to STEP 0, then STEP 1.
- **If detection returned `GITHUB_STYLE`:** skip STEP 0 entirely and go straight to STEP 1. Do not delete or rewrite `package.json`, `vite.config.ts`, or `postcss.config.mjs` — they're already correct.

If you are uncertain, treat the project as `GITHUB_STYLE` and skip STEP 0. STEP 0 is destructive; running it on the wrong project type will break a working repo. It is safer to skip STEP 0 and discover a real Figma-Make-specific problem during STEP 1 than to silently overwrite valid config.

---

## STEP 0: Repair Figma Make Export (ONLY IF STEP -1 RETURNED `FIGMA_MAKE`)

**Purpose:** Figma Make exports break in Replit/npm environments because they use pnpm-only conventions, list Node built-ins as packages, and put React in optional peerDependencies. This step normalizes the export so npm install actually works and Tailwind v4 actually builds. Without this step, the page will render as unstyled black-on-white text.

**Skip this entire step if STEP -1 returned `GITHUB_STYLE`.** Running it against a clean repo will overwrite working `package.json`, `vite.config.ts`, and `postcss.config.mjs`.

### 0.1 — Detect and normalize folder structure

Figma Make exports put app code under `src/app/`. The rest of this prompt assumes the GitHub-style layout (`src/App.tsx`, `src/components/`). Move files up one level:

```bash
# If src/app/ exists, flatten it into src/
if [ -d "src/app" ]; then
  mv src/app/* src/
  rmdir src/app
fi

# If src/styles/index.css exists, copy it to src/index.css (the entry main.tsx will import)
if [ -f "src/styles/index.css" ]; then
  cp src/styles/index.css src/index.css
fi
```

Update `src/main.tsx` to import from the flattened paths:

```typescript
import { createRoot } from "react-dom/client";
import App from "./App.tsx";
import "./index.css";

createRoot(document.getElementById("root")!).render(<App />);
```

### 0.2 — Replace `package.json` with a clean version

**Delete the existing `package.json` entirely** and write a new one. Do NOT try to merge or patch — Figma Make's package.json has duplicated dep entries (`"pkg": "x", "pkg@x": "npm:pkg@x"`) that confuse npm, lists Node built-ins (`fs`, `path`, `url`) as deps which will fail install, and puts `react`/`react-dom` in optional `peerDependencies` which means npm won't install them.

Build the new `package.json` like this:

1. Keep all entries from the old `dependencies` block, but:
   - Drop every duplicated `"name@version": "npm:name@version"` entry (keep only the plain `"name": "version"` form)
   - Delete `fs`, `path`, and `url` entries
2. Move `react: "18.3.1"` and `react-dom: "18.3.1"` from `peerDependencies` into `dependencies`
3. Delete the `peerDependencies`, `peerDependenciesMeta`, and `pnpm` sections entirely
4. Add `@types/react`, `@types/react-dom`, `typescript`, `@types/node`, `autoprefixer`, `postcss` to `devDependencies`
5. Change `@vitejs/plugin-react` to `@vitejs/plugin-react-swc` (faster, more reliable in Replit)
6. Add standard scripts: `dev`, `build`, `preview`

Template to follow (fill in deps from the old file using the rules above):

```json
{
  "name": "tetris-training",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview"
  },
  "dependencies": {
    "react": "^18.3.1",
    "react-dom": "^18.3.1",
    "papaparse": "^5.4.1"
    // ...plus every cleaned-up dep from the old file
  },
  "devDependencies": {
    "@types/node": "^20.10.0",
    "@types/papaparse": "^5.3.14",
    "@types/react": "^18.3.0",
    "@types/react-dom": "^18.3.0",
    "@vitejs/plugin-react-swc": "^3.10.2",
    "autoprefixer": "^10.4.16",
    "postcss": "^8.4.32",
    "tailwindcss": "^4.1.12",
    "@tailwindcss/vite": "^4.1.12",
    "typescript": "^5.3.0",
    "vite": "^6.3.5",
    "vite-plugin-singlefile": "^2.0.0"
  }
}
```

### 0.3 — Delete conflicting config files

```bash
# Empty postcss.config.mjs conflicts with @tailwindcss/vite in some setups
rm -f postcss.config.mjs
```

### 0.4 — Replace `vite.config.ts` with a clean version

The Figma Make `vite.config.ts` has a custom `figmaAssetResolver` plugin that crashes if `src/assets/` doesn't exist, and uses an alias pointing at the now-moved `src/app/` directory. Replace it entirely:

```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import tailwindcss from '@tailwindcss/vite';
import path from 'path';

export default defineConfig({
  plugins: [react(), tailwindcss()],
  base: './',
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  build: {
    target: 'esnext',
    outDir: 'dist',
  },
  server: {
    port: 3000,
    open: true,
  },
});
```

### 0.5 — Verify the CSS entry uses Tailwind v4 syntax correctly

Open `src/index.css` (the one you just copied in 0.1). The first lines should be:

```css
@import 'tailwindcss';
@import './globals.css';
```

If you see `@import 'tailwindcss' source(none);` with a separate `@source '...'` directive, replace it with the plain `@import 'tailwindcss';` shown above. The `source(none)` form is fragile in cloud build environments.

If `src/styles/default_theme.css` or `src/styles/tw-animate-css` imports exist and the matching files exist, leave those `@import` lines. If they reference packages that aren't installed, delete those lines.

### 0.6 — Add a clean `index.html`

If the existing `index.html` is missing the `<base>` tag, add it. Replace with:

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <base href="./" />
    <title>Tetris Training</title>
    <style>html, body { height: 100%; margin: 0; } #root { height: 100%; }</style>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>
```

### 0.7 — Install and smoke-test

```bash
rm -rf node_modules package-lock.json
npm install
npm run dev
```

**Verification — DO NOT proceed to STEP 1 until all of these pass:**

- [ ] `npm install` completes with **zero errors** (warnings about peer deps are OK)
- [ ] `node_modules/react/` exists
- [ ] `node_modules/@tailwindcss/vite/` exists
- [ ] `npm run dev` starts Vite without errors
- [ ] Opening the dev URL shows the game with **proper styling** (colors, layout, fonts) — NOT plain unstyled text on a white background
- [ ] Browser console shows no red errors about missing modules or failed CSS imports

If styling is broken (unstyled text), Tailwind didn't build. Check that:
1. `src/index.css` exists at that exact path
2. `src/main.tsx` imports `./index.css`
3. `@tailwindcss/vite` is in devDependencies AND in `vite.config.ts` plugins array
4. There is no `postcss.config.mjs` file in the project root

Only after the dev server renders a properly styled game, continue to STEP 1.

---

## STEP 1: Install Dependencies

Add these to your project:

```bash
npm install papaparse
npm install -D @types/papaparse vite-plugin-singlefile
```

---

## STEP 2: Create `src/components/questionService.ts`

This file replaces the static `questionBank.ts` with CSV-loading functionality.

**IMPORTANT:** The interface must match EXACTLY what the game components expect.

```typescript
import Papa from 'papaparse';

// Interface MUST match what QuestionDialog and App.tsx expect
export interface Question {
  id: string;                    // Auto-generated row id (q_1, q_2, ...)
  question: string;              // The question text
  options: string[];             // Array of 4 answer options
  correctAnswer: number;         // 0-based index, derived from the A-D letter
  feedbackCorrect?: string;      // Shown after a correct answer
  feedbackIncorrect?: string;    // Shown after a wrong answer
}

// CSV row after header normalization (lowercase, underscores/spaces stripped),
// so option_1 / option1 / "Option 1" all map to the same field.
interface CSVRow {
  question: string;
  option1: string;
  option2: string;
  option3: string;
  option4: string;
  correctanswer: string;         // letter: A, B, C, or D
  feedbackcorrect: string;
  feedbackincorrect: string;
}

let cachedQuestions: Question[] | null = null;

// Convert an A-D letter (case-insensitive) to a 0-based index. Falls back to 0.
function letterToIndex(value: string): number {
  const c = (value || '').trim().toUpperCase();
  const idx = ['A', 'B', 'C', 'D'].indexOf(c);
  return idx >= 0 ? idx : 0;
}

/**
 * Load and parse questions from CSV file
 */
export async function loadQuestions(): Promise<Question[]> {
  if (cachedQuestions) return cachedQuestions;

  try {
    const response = await fetch('./questions.csv');
    if (!response.ok) throw new Error('Failed to fetch CSV');

    const text = await response.text();

    const parsed = Papa.parse<CSVRow>(text, {
      header: true,
      skipEmptyLines: true,
      // Normalize headers so option_1, option1, and "Option 1" all match.
      transformHeader: (header) => header.trim().toLowerCase().replace(/[_\s]+/g, '')
    });

    cachedQuestions = parsed.data.map((row, index) => ({
      id: `q_${index + 1}`,
      question: row.question?.trim() || '',
      options: [
        row.option1?.trim() || '',
        row.option2?.trim() || '',
        row.option3?.trim() || '',
        row.option4?.trim() || ''
      ],
      correctAnswer: letterToIndex(row.correctanswer),
      feedbackCorrect: row.feedbackcorrect?.trim() || undefined,
      feedbackIncorrect: row.feedbackincorrect?.trim() || undefined
    }));

    console.log(`Loaded ${cachedQuestions.length} questions from CSV`);
    return cachedQuestions;
  } catch (error) {
    console.warn('CSV failed to load, using fallback questions:', error);
    return fallbackQuestions;
  }
}

/**
 * Get a batch of questions to use (shuffled).
 */
export async function getQuestionsForLevel(_level: number, count: number = 3): Promise<Question[]> {
  const all = await loadQuestions();

  // Difficulty was removed from the schema; just shuffle and return `count`.
  const shuffled = [...all].sort(() => Math.random() - 0.5);
  return shuffled.slice(0, Math.min(count, shuffled.length));
}

/**
 * Get a random question that hasn't been used yet
 * @param usedQuestionIds - Array of question IDs already shown to player
 */
export function getRandomQuestion(usedQuestionIds: string[]): Question {
  const all = cachedQuestions || fallbackQuestions;

  const available = all.filter(q => !usedQuestionIds.includes(q.id));

  if (available.length === 0) {
    // All questions used - reset and pick any
    return all[Math.floor(Math.random() * all.length)];
  }

  return available[Math.floor(Math.random() * available.length)];
}

/**
 * Initialize questions (call this early, e.g., in App useEffect)
 */
export async function initializeQuestions(): Promise<void> {
  await loadQuestions();
}

/**
 * Calculate bonus points for quiz performance
 */
export function calculateQuizBonus(
  correctAnswers: number,
  totalQuestions: number,
  level: number
): number {
  const baseBonus = 100;
  const levelMultiplier = level;
  const accuracyBonus = (correctAnswers / totalQuestions) * 2;

  return Math.floor(baseBonus * levelMultiplier * accuracyBonus);
}

/**
 * Calculate points for a single question based on correctness and speed
 * @param isCorrect - Whether the answer was correct
 * @param timeElapsed - Time taken to answer in seconds (max 30)
 * @returns Points to add (positive for correct, negative for wrong)
 */
export function calculateQuestionPoints(isCorrect: boolean, timeElapsed: number): number {
  if (!isCorrect) {
    return -50; // Wrong answer penalty
  }

  // Correct answer: base 100 points + speed bonus
  const basePoints = 100;

  // Speed bonus calculation (0-200 extra points)
  let speedBonus = 0;
  if (timeElapsed <= 5) {
    speedBonus = 200;
  } else if (timeElapsed <= 15) {
    speedBonus = 150 - ((timeElapsed - 5) * 5);
  } else if (timeElapsed <= 25) {
    speedBonus = 100 - ((timeElapsed - 15) * 5);
  } else {
    speedBonus = Math.max(0, 50 - ((timeElapsed - 25) * 10));
  }

  return Math.floor(basePoints + speedBonus);
}

// Fallback questions if CSV fails to load
const fallbackQuestions: Question[] = [
  {
    id: 'fallback_1',
    question: 'What is the largest ocean in the world?',
    options: ['Atlantic Ocean', 'Indian Ocean', 'Pacific Ocean', 'Arctic Ocean'],
    correctAnswer: 2,
    feedbackCorrect: 'Correct. The Pacific is the largest ocean.',
    feedbackIncorrect: 'Not quite. The Pacific Ocean is the largest.'
  },
  {
    id: 'fallback_2',
    question: 'What is the capital of France?',
    options: ['London', 'Berlin', 'Madrid', 'Paris'],
    correctAnswer: 3,
    feedbackCorrect: 'Correct. Paris is the capital of France.',
    feedbackIncorrect: 'Not quite. The capital of France is Paris.'
  },
  {
    id: 'fallback_3',
    question: 'How many continents are there?',
    options: ['5', '6', '7', '8'],
    correctAnswer: 2,
    feedbackCorrect: 'Correct. There are 7 continents.',
    feedbackIncorrect: 'Not quite. There are 7 continents.'
  },
  {
    id: 'fallback_4',
    question: 'What is 8 x 7?',
    options: ['54', '56', '58', '63'],
    correctAnswer: 1,
    feedbackCorrect: 'Correct. 8 x 7 = 56.',
    feedbackIncorrect: 'Not quite. 8 x 7 = 56.'
  },
  {
    id: 'fallback_5',
    question: 'How many days are in a year?',
    options: ['360', '365', '370', '375'],
    correctAnswer: 1,
    feedbackCorrect: 'Correct. A regular year has 365 days.',
    feedbackIncorrect: 'Not quite. A regular year has 365 days (366 in a leap year).'
  }
];

// Re-export Question type for other files
export type { Question };
```

> **Feedback display:** Wherever the game previously showed `question.explanation` after an answer, show `question.feedbackCorrect` when the learner is right and `question.feedbackIncorrect` when wrong. Both are optional, so guard against empty values.

---

## STEP 3: Update Imports in `src/App.tsx`

Find and replace the questionBank import:

```typescript
// REMOVE this line:
import { getRandomQuestion, calculateQuestionPoints, type Question } from './components/questionBank';

// ADD this line:
import { getRandomQuestion, calculateQuestionPoints, initializeQuestions, type Question } from './components/questionService';
```

Add initialization in App.tsx useEffect (near top of component):

```typescript
// Add this useEffect to pre-load questions
useEffect(() => {
  initializeQuestions().catch(console.error);
}, []);
```

---

## STEP 4: Create `vite.config.storyline.ts`

```typescript
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react-swc';
import { viteSingleFile } from 'vite-plugin-singlefile';
import path from 'path';

export default defineConfig({
  plugins: [react(), viteSingleFile()],
  base: './',
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src'),
    },
  },
  build: {
    target: 'es2015',
    outDir: 'dist-storyline',
    assetsInlineLimit: 100000000,
    cssCodeSplit: false,
    rollupOptions: {
      output: {
        inlineDynamicImports: true,
        manualChunks: undefined,
      }
    }
  }
});
```

---

## STEP 5: Update `index.html`

Add base tag in the `<head>` section:

```html
<head>
  <base href="./" />
  <!-- other head content -->
</head>
```

---

## STEP 6: Update `package.json` Scripts

Add the build script (cross-platform compatible):

```json
{
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "build:storyline": "vite build -c vite.config.storyline.ts && node -e \"require('fs').copyFileSync('questions.csv', 'dist-storyline/questions.csv')\""
  },
  "dependencies": {
    "papaparse": "^5.4.1"
  },
  "devDependencies": {
    "@types/papaparse": "^5.3.14",
    "vite-plugin-singlefile": "^2.0.0"
  }
}
```

---

## STEP 7: Create `questions.csv` in Project Root

### EXACT HEADER ROW (copy this exactly)

```
question,option_1,option_2,option_3,option_4,correct_answer,feedback_correct,feedback_incorrect
```

### CSV COLUMN DEFINITIONS (8 columns)

| Column | Type | Required | Description |
|--------|------|----------|-------------|
| `question` | string | YES | The question text displayed to the user |
| `option_1` | string | YES | First answer choice |
| `option_2` | string | YES | Second answer choice |
| `option_3` | string | YES | Third answer choice |
| `option_4` | string | YES | Fourth answer choice |
| `correct_answer` | letter | YES | Correct option as a **letter A-D** (see below) |
| `feedback_correct` | string | optional | Shown after a correct answer |
| `feedback_incorrect` | string | optional | Shown after a wrong answer |

### correct_answer IS A LETTER (A, B, C, or D)

Use a **letter**, not a number. This avoids the classic 0-based vs 1-based off-by-one mistake.

| If the correct answer is... | Set correct_answer to... |
|-----------------------------|--------------------------|
| option_1 (first choice) | `A` |
| option_2 (second choice) | `B` |
| option_3 (third choice) | `C` |
| option_4 (fourth choice) | `D` |

Example: if the second option is correct, `correct_answer` is `B`.

### CSV FORMATTING RULES

1. **Header row is mandatory** - First line must be exactly:
   ```
   question,option_1,option_2,option_3,option_4,correct_answer,feedback_correct,feedback_incorrect
   ```

2. **No spaces after commas** - Write `value1,value2` not `value1, value2`

3. **Commas inside text** - If your text contains commas, wrap the ENTIRE field in double quotes:
   ```
   "What are the three largest countries by area?",Russia,Canada,USA,China,A,"Correct, Russia is the largest.","Not quite. Russia is the largest by area."
   ```

4. **Quotes inside text** - If text contains quotes, double them:
   ```
   "Who wrote ""Romeo and Juliet""?",Shakespeare,Dickens,Austen,Hemingway,A,Correct.,Not quite. Shakespeare wrote it.
   ```

5. **No line breaks inside fields** - Each question must be on a single line

6. **UTF-8 encoding** - Save the CSV file with UTF-8 encoding

### SAMPLE CSV FILE

```csv
question,option_1,option_2,option_3,option_4,correct_answer,feedback_correct,feedback_incorrect
What is the largest ocean in the world?,Atlantic Ocean,Indian Ocean,Pacific Ocean,Arctic Ocean,C,Correct. The Pacific is the largest ocean.,Not quite. The Pacific Ocean is the largest.
What is the capital of France?,London,Berlin,Madrid,Paris,D,Correct. Paris is the capital.,Not quite. The capital of France is Paris.
How many continents are there?,5,6,7,8,C,Correct. There are 7 continents.,Not quite. There are 7 continents.
What is 8 x 7?,54,56,58,63,B,Correct. 8 x 7 = 56.,Not quite. 8 x 7 = 56.
How many planets are in our solar system?,7,8,9,10,B,Correct. There are 8 planets.,Not quite. There are 8 planets.
```

### COMMON MISTAKES TO AVOID

| Mistake | Wrong | Correct |
|---------|-------|---------|
| Using a number instead of a letter | `correct_answer` = `2` | `correct_answer` = `B` |
| Spaces after commas | `question, option_1` | `question,option_1` |
| Unquoted commas in text | `Hello, world` | `"Hello, world"` |
| Wrong column count | 6 or 10 columns | Must have exactly 8 columns |
| Missing header row | Starting with data | First row must be headers |

### VALIDATION CHECKLIST FOR EACH QUESTION

Before adding a question, verify:
- [ ] Question ends with `?`
- [ ] All 4 options are provided (not blank)
- [ ] `correct_answer` is a single letter A, B, C, or D
- [ ] The option at that letter is actually the correct one
- [ ] Feedback columns exist (the values may be blank, but the columns must be present)
- [ ] No unquoted commas in any text field
- [ ] Row has exactly 8 comma-separated values

### ANNOTATED SINGLE ROW EXAMPLE

Here is one complete question row broken down by column:

```
Column 1 (question):           What is the capital of Japan?
Column 2 (option_1):           Beijing
Column 3 (option_2):           Seoul
Column 4 (option_3):           Tokyo                          <- CORRECT
Column 5 (option_4):           Bangkok
Column 6 (correct_answer):     C                              <- Because option_3 is correct
Column 7 (feedback_correct):   Correct. Tokyo is the capital of Japan.
Column 8 (feedback_incorrect): Not quite. Tokyo is the capital of Japan.
```

This becomes the CSV row:
```csv
What is the capital of Japan?,Beijing,Seoul,Tokyo,Bangkok,C,Correct. Tokyo is the capital of Japan.,Not quite. Tokyo is the capital of Japan.
```

---

## STEP 8: Build and Test

1. Run the build:
   ```bash
   npm run build:storyline
   ```

2. The `dist-storyline/` folder should contain:
   - `index.html` (single file, all JS/CSS inlined)
   - `questions.csv` (your question data)

3. Test locally by serving `dist-storyline/`:
   ```bash
   npx serve dist-storyline
   ```

4. Open browser and verify:
   - Game loads
   - Questions display correctly
   - Scoring works (correct = +100-300, wrong = -50)
   - Edit `questions.csv` and refresh - changes appear without rebuild

---

## TESTING CHECKLIST

- [ ] `index.html` is fully inlined in `dist-storyline/`
- [ ] `questions.csv` is external and editable
- [ ] Questions load correctly via `./questions.csv` fetch
- [ ] Correct answers show green, wrong show red
- [ ] Speed bonus scoring works (faster = more points)
- [ ] Wrong answers deduct 50 points
- [ ] CSV edits update game without rebuilding
- [ ] Works in Storyline 360 web object iframe

---

## TROUBLESHOOTING

**Questions not loading:**
- Check browser console for fetch errors
- Ensure `questions.csv` is in same folder as `index.html`
- Verify CSV has correct headers (no extra spaces)

**TypeScript errors:**
- Ensure `@types/papaparse` is installed
- Check that Question interface matches exactly

**Build fails:**
- Run `npm install` to get all dependencies
- Ensure `vite-plugin-singlefile` is in devDependencies

**Storyline iframe issues:**
- Ensure `<base href="./" />` is in index.html head
- All asset references must be relative (`./`)
