---
name: find4
description: >
    Generates Find4 game JSON from any text source — a URL, a local file, or piped stdin content.
    Find4 is a Connections-style word-grouping game (https://find4.org) where players
    find groups of 4 words that share a hidden theme. Use this skill whenever the user wants to:
    create a Find4 game, generate game content from a document or webpage, produce a game JSON
    file, analyse text into themed word groups, or populate the Find4 library. Also trigger
    when the user mentions "connections game", "word groups", "concept groups", or "game JSON",
    or uses vague phrasing like "make a game from this" or "turn this into something interactive"
    when a URL, file, or document is present.
---

# Find4 Game Generator

Generates a fully post-processed, drag-and-drop-ready JSON file for the Find4 game
(https://find4.org) from any text input: local file, URL, pasted text, or dropped file.

---

## ⚠️ CRITICAL — NO CUSTOM SCRIPTING. EVER.

**Never write your own Python, bash, or any other code to replicate what the Find4 scripts already do.**
This applies unconditionally to:

- Share URL / base64 encoding → **always use** `share_game.sh`
- Color fixing → **always use** `fix_colors.py`
- ID generation → **always use** `add_ids.py`
- Metadata finalization → **always use** `finalize_metadata.py`
- Library rebuilding → **always use** `generate_library.py`

If a script call fails, fix the input and retry. Do not work around it with inline code.

---

## Step 0 — Confirm before starting

**Before doing any work**, present the user with a plan summary and ask for confirmation.

Determine:
- **Input source**: URL, file path, attachment, or pasted text
- **game_count_max**: default `2` (user can override with `--game-count-max N`)
- **Output file**: `output/games/<suggested_name>.json`

```
Generating <game_count_max> new games.

  Source : <resolved source>
  Games  : <game_count_max> game set(s)
  Output : output/games/<expected filename or TBD>
  Output : output/games/screenshots/<expected filename or TBD>.png
  Output : output/<suggested_name>.zip
```

Set `AUTO_ACCEPT=true` and proceed through all remaining steps **without pausing for further confirmation**.
Do NOT ask clarifying questions mid-run unless a hard error requires user input.

---

## Step 0.5 — Resolve skill and script paths

**Always do this before calling any script.**

```bash
if [ "${IS_SANDBOX:-no}" = "yes" ] || [ "${IS_SANDBOX:-no}" = "1" ] || [ "${IS_SANDBOX:-no}" = "true" ]; then
    SKILL_DIR="/mnt/skills/user/find4"
    mkdir -p /tmp/find4
    cp "$SKILL_DIR/scripts/"* /tmp/find4/
    chmod +x /tmp/find4/share_game.sh
    FIND4_SCRIPTS="/tmp/find4"
else
    SKILL_DIR=""
    for candidate in \
        "$HOME/.claude/skills/find4" \
        ".claude/skills/find4" \
        ".claude/plugins/marketplaces/*/skills/find4" \
        "skills/find4"; do
        if [ -d "$candidate/scripts" ]; then
            SKILL_DIR="$candidate"
            break
        fi
    done
    if [ -z "$SKILL_DIR" ]; then
        echo "ERROR: find4 skill not found in any expected location" >&2
        exit 1
    fi
    FIND4_SCRIPTS="$SKILL_DIR/scripts"
fi
```

Immediately after, create all output directories:

```bash
mkdir -p output output/tmp output/games output/games/html output/games/screenshots output/library
```

From this point on, **all script calls must use `$FIND4_SCRIPTS`**, never hardcoded paths.

---

## Step 1 — Resolve and fetch the input text

Select **one** of the following based on the user's message:

### URL

**URL** (`--url https://...` or a bare URL in the message):

Check which fetch method is available:

```bash
if command -v wkhtmltopdf &>/dev/null && command -v pdftotext &>/dev/null; then
    USE_WKHTMLTOPDF=true
else
    USE_WKHTMLTOPDF=false
fi
```

**If `USE_WKHTMLTOPDF=true`:** use `wkhtmltopdf` + `pdftotext`:

```bash
xvfb-run wkhtmltopdf \
  --log-level none \
  --load-error-handling ignore \
  --load-media-error-handling ignore \
  "<URL>" /tmp/find4_source.pdf

pdftotext /tmp/find4_source.pdf - \
  | head -c 120000 \
  > output/tmp/find4_input.txt
```

⚠️ **After extraction, check for empty output** — some JS-heavy pages render as blank PDFs even when wkhtmltopdf exits 0. If `find4_input.txt` is empty or under 100 bytes, fall back to the `WebFetch` tool to fetch the URL directly, and write the returned text to `output/tmp/find4_input.txt`. If both methods fail, report and stop.

**Known limitation:** Wikimedia CDN rate-limits image requests (HTTP 429) — text content is unaffected.

**If `USE_WKHTMLTOPDF=false` (local/Mac):** use the `WebFetch` tool. Write the returned text to `output/tmp/find4_input.txt`. Truncate to ~120,000 characters if needed.

### File

**File** (`--file path/to/file`, recognisable file path, or dropped attachment):

For plaintext files (`.txt`, `.md`, `.csv`, `.json`):
```bash
cp "<filepath>" output/tmp/find4_input.txt
```

For non-plaintext files (`.pdf`, `.docx`, `.html`, etc.), use the appropriate skill or tool to extract text first, then write it to `output/tmp/find4_input.txt`.

Verify the file exists before copying; if it doesn't, report and stop.

### Stdin / inline text

User has pasted raw text, or provided a topic with no file or URL.

Save to `output/tmp/find4_input.txt` using the `Write` tool. Also save a timestamped backup: `output/tmp/find4_input.<HUMANREADABLETIME>.txt`.

---

**After any of the above:** truncate to ~30,000 words if input exceeds that, and log a warning.
Record the source identifier (`<URL>`, `<filename>`, or `"stdin"`) — required for Step 5.

---

## Step 2 — Generate the game JSON

Read `output/tmp/find4_input.txt`. Reference `references/schemas.md` for constraints and `references/game.schema.json` for the exact JSON structure.

**Output ONLY valid JSON** — no prose, no markdown fences, no comments.

### Parameters

| Parameter        | Default                                                                | Override flag         |
| ---------------- | ---------------------------------------------------------------------- | --------------------- |
| `game_count_max` | `2`                                                                    | `--game-count-max N`  |
| `colors`         | `red`, `yellow`, `green`, `blue`, `purple`, `teal`, `orange`, `indigo` | fixed — do not change |
| `skill_levels`   | `Beginner`, `Intermediate`, `Advanced`, `Expert`                       | fixed                 |

Produce exactly `game_count_max` game_sets. Each `group_set` must have exactly 4 groups, each with exactly 4 unique words. Colors within a single `group_set` must all be different.

Write the raw JSON to `output/tmp/find4_raw.json` and a timestamped backup `output/tmp/find4_raw.<HUMANREADABLETIME>.json`.

**Validate before proceeding**: if the JSON is clearly malformed (missing brackets, truncated), regenerate once. If still invalid, report and stop.

---

## Step 3 — Fix colors

```bash
cat output/tmp/find4_raw.json | python3 $FIND4_SCRIPTS/fix_colors.py > output/tmp/find4_colored.json
```

On non-zero exit: inspect stderr, fix the input JSON, retry once. If it fails again, report and stop. Do not skip this step.

---

## Step 4 — Add IDs

```bash
cat output/tmp/find4_colored.json | python3 $FIND4_SCRIPTS/add_ids.py > output/tmp/find4_ids.json
```

Stamps `game_set_id`, `group_set_id`, and `group_item_id` throughout the hierarchy. On non-zero exit: report and stop.

---

## Step 5 — Finalize metadata

```bash
cat output/tmp/find4_ids.json | python3 $FIND4_SCRIPTS/finalize_metadata.py \
  --source "<source_identifier>" > output/tmp/find4_final.json
```

Adds `modified_at`, `source_id`, promotes `id_registry` into `metadata`, and generates a top-level hash ID. On non-zero exit: report and stop.

---

## Step 6 — Save, split, and present

Determine the output filename from `metadata.suggested_name` in the final JSON.
Fall back to `find4_game_<slug_of_theme>.json` if absent.

```bash
cp output/tmp/find4_final.json output/games/<output_filename>
```

### Step 6a — Widget (chat display, display only)

Refer to `references/ui-widget.html.md` for the HTML structure and `references/ui-widget.css.md` for CSS.
Render one `.f4-game` block per game set using the `show_widget` tool.

**The widget is display only.** Never embed share URLs anywhere in it — not in `href`, hidden `<span>` tags, or JS string literals. The `show_widget` tool has a ~10KB payload limit; share URLs alone (~2KB each) would push a two-game widget over the limit. Replace any Play button with a static "Download HTML to play" label (see `references/ui-widget.html.md` for the exact pattern).

#### Platform rendering support

`show_widget` renders in an iframe. Support varies by platform:

| Platform | Widget | Screenshots | HTML files |
|---|---|---|---|
| claude.ai website (desktop or mobile browser) | ✅ renders | ✅ | ✅ |
| Claude native iOS / Android app | ❌ blank | ❌ inline, use `present_files` | ✅ |

**Always** call `show_widget` regardless of platform — it works on the website. Then **always** also call `present_files` with the PNG screenshots and HTML files as a fallback.

After calling `show_widget`, tell the user which platform they're likely on and what to expect. Use this pattern:

- **If on claude.ai website**: "The preview above shows both games. Download the HTML files below to play."
- **If on native app** (no widget rendered, or user reports blank): "The inline widget doesn't render in the native app — use the files below to preview and play. Screenshots are attached."

If the user reports the widget is blank or empty, immediately follow up with `present_files` for the PNGs and HTML.

### Step 6b — Per-game-set standalone HTML files

Split the final JSON into one file per game_set, generate share URLs, and produce standalone HTML:

```bash
# 1. Split
python3 - << 'EOF'
import json, re
with open('output/games/<output_filename>') as f:
    data = json.load(f)
for gs in data['game_sets']:
    single = {**data, 'game_sets': [gs]}
    slug = gs['theme'].lower().replace('&', 'and')
    slug = re.sub(r'[^a-z0-9]+', '-', slug).strip('-')
    with open(f'output/games/{slug}.json', 'w') as out:
        json.dump(single, out)
    print(slug)
EOF

# 2. Generate share URL — always write to file, never capture in a variable
FIND4_URL="https://find4.org" bash $FIND4_SCRIPTS/share_game.sh output/games/<slug>.json \
  > output/games/html/<slug>.html_share_url.txt

# 3. Generate standalone HTML
python3 $FIND4_SCRIPTS/generate_html.py \
  --game-json output/games/<slug>.json \
  --output output/games/html/<slug>.html
```

Repeat steps 2 and 3 for each slug.

#### ⚠️ CRITICAL — Share URL file I/O

Share URLs are 2,000–3,000+ characters. **Always redirect to a file. Never capture in a variable.**

These patterns silently truncate:
```bash
# ❌ bash variable
URL=$(bash share_game.sh ...)

# ❌ Python -c inline string
python3 -c "url = 'https://find4.org/#game=AAAA...'"

# ❌ heredoc with long line
python3 << 'EOF'
url = 'https://...very long...'
EOF
```

The only safe pattern is file I/O:
```bash
# ✅ write to file, read from file
bash $FIND4_SCRIPTS/share_game.sh ... > url.txt
url=$(cat url.txt)    # safe — already in a file
```

Never use `window.open()` or `onclick` for the Play button in standalone HTML — use a plain `<a href="...">`. The `generate_html.py` script handles this correctly; do not override it.

#### Step 6b checklist

- [ ] One split JSON per `game_set`
- [ ] Share URL written to file via `share_game.sh` (never a variable)
- [ ] `generate_html.py` called for each split JSON
- [ ] One `.html` file per `game_set` in `output/games/html/`
- [ ] Widget contains no share URLs

In sandbox: copy HTML files to outputs and call `present_files`:
```bash
cp output/games/html/<slug>.html /mnt/user-data/outputs/<slug>.html
```

---

### Step 6c — Screenshot game cards as preview artifacts

For each standalone HTML file, capture the `.f4-game` card as a PNG:

```bash
python3 $FIND4_SCRIPTS/screenshot_game.py \
  --html output/games/html/<slug>.html \
  --output output/games/screenshots/<slug>.html.png
```

Backend auto-selected (no configuration needed):

| Environment                          | Backend                                                               |
| ------------------------------------ | --------------------------------------------------------------------- |
| macOS/Linux with Docker Desktop      | `docker` — pulls `mcr.microsoft.com/playwright:v1.56.0-jammy`        |
| Claude sandbox / CI (no Docker)      | `local` — uses pre-installed `playwright` Python package              |
| Override                             | `--backend docker\|local\|auto`                                       |

This step is **non-critical** — if it fails, skip and continue with HTML files.

In sandbox: copy PNGs to outputs and call `view` on each for inline rendering:
```bash
cp output/games/screenshots/<slug>.html.png /mnt/user-data/outputs/<slug>.html.png
```

Then call `present_files` with all `.html` and `.png` paths.

#### Step 6c checklist

- [ ] `screenshot_game.py` called once per game set
- [ ] One `.html.png` per game set in `output/games/screenshots/`
- [ ] In sandbox: PNG copied to `/mnt/user-data/outputs/`
- [ ] `present_files` called with all HTML and PNG outputs

---

## Step 7 — Rebuild the library index

```bash
python3 $FIND4_SCRIPTS/generate_library.py \
  --config-dir output/library --library-dir output/games --force
```

Regenerates `output/library/themes.json`. Safe to run repeatedly — deduplicates by `game_set_id`.
On non-zero exit: log warning and continue — library is non-critical and can be rebuilt at any time.

---

## Step 8 — Zip output files

Bundle the deliverables into a single convenience zip at `output/<suggested_name>.zip`.
The zip name comes from `metadata.suggested_name` (strip the `.json` suffix).

Include:
- `output/games/<output_filename>` (combined JSON)
- `output/games/<slug>.json` for each game set
- `output/games/html/<slug>.html` for each game set
- `output/games/screenshots/<slug>.html.png` for each game set (skip any that failed)

```bash
SLUG=$(python3 -c "import json; d=json.load(open('output/tmp/find4_final.json')); print(d['metadata']['suggested_name'].replace('.json',''))")
zip -j output/${SLUG}.zip \
  output/games/<output_filename> \
  output/games/<slug1>.json \
  output/games/<slug2>.json \
  output/games/html/<slug1>.html \
  output/games/html/<slug2>.html \
  output/games/screenshots/<slug1>.html.png \
  output/games/screenshots/<slug2>.html.png \
  2>/dev/null || true
echo "Zip: output/${SLUG}.zip"
```

Use `-j` (junk paths) so the zip contains flat filenames, not nested directories.
This step is **non-critical** — if it fails, log a warning and continue.

In sandbox: copy the zip to outputs and include it in `present_files`:
```bash
cp output/${SLUG}.zip /mnt/user-data/outputs/${SLUG}.zip
```

---

## Error handling

| Condition                         | Action                                                      |
| --------------------------------- | ----------------------------------------------------------- |
| Input fetch/read fails            | Stop immediately, report full error                         |
| `find4_input.txt` empty after URL fetch | Attempt WebFetch fallback; stop if that also fails   |
| JSON generation clearly invalid   | Retry once; stop on second failure                          |
| Script exits non-zero (steps 3–5) | Retry once with fixed input; stop on second failure         |
| Script exits non-zero (step 7)    | Log warning, continue — library is non-critical             |
| Screenshot fails                  | Log warning, continue — non-critical                        |
| Zip creation fails (step 8)       | Log warning, continue — zip is non-critical                 |

---

## Notes

**Why two game_sets by default?** LLM output for complex structured JSON can be unreliable for large outputs. Two game_sets gives a good payload while keeping quality high. Request more with `--game-count-max N`.

**Color palette** — `fix_colors.py` enforces valid colors. Do not skip step 3.

**IDs are content-addressed** — the same content always produces the same IDs, enabling safe deduplication across JSON files in the library.

**Tmp directory** — all intermediate files land in `output/tmp/`. Timestamped backups allow manual recovery if a later step fails.
